Github 原文:shareAI-lab/learn-claude-code
👳🏻♂️ 这个东西讲的有些靠后了,因为原来的文章是基于流程的添砖加瓦来写的,但这个作为一种基础内容,涉及到了前文中不少的基础知识。如果你有前文关于系统提示词的部分,可以跳转到这里。
问题
在相当长的时间里,我们都认为系统提示词就是一段预设好的文本内容,然后再加上我们之前学到的什么记忆、Skill 的目录等等。
但这是有问题的:
-
提示词无法通用:不同的项目我们就要写不同的提示词,而且到底怎么改才能避免 AI 乱出错,这是个黑盒,没法给一个稳定的结论;
-
修改一处就可能影响全局:提示词一旦长了,就容易出现加一段工具描述,我靠,和之前的冲突了;
-
每次请求都要带上全部内容:这是最恶心的,每次我都要滴里当啷的带着这一大串的东西,其实真的用上的未必是全部,浪费一大堆上下文和 token,而且过长的提示词还容易导致 AI 丢失注意力;
System prompt 应该是运行时根据当前状态组装的配置:哪些工具启用、哪些上下文可见、哪些记忆相关、哪些内容必须保持稳定以命中 prompt cache。
🌂 我们这里补充一下这个 prompt cache 是什么东西。其实就是 AI 的提示词缓存,跟浏览器缓存一样,临时存一下。好处就是可以快速加载,不至于每次都要重新载入,然后 AI 只需要读一次就好了,不至于同一轮对话的 loop 里每次都要重新读取,浪费 token
解决方案
相比之前,其实是多了一个提示词组装的流程,专业上叫做拆分成若干个 section ,然后按需拼接。section 也有不同的类型和内容,加载策略等也都有不同,无论如何,section 是否加载取决于真实状态(工具是否存在、文件是否存在),不是消息里的关键词
| Section | 加载策略 | 内容 | 判断依据 |
|---|---|---|---|
| identity | 始终 | 你是谁、怎么做事 | 始终存在 |
| tools | 始终 | 可用工具列表 | enabled_tools |
| workspace | 始终 | 工作目录 | 始终存在 |
| memory | 按需 | 相关记忆内容 | .memory/MEMORY.md 是否存在 |
工作原理
分段定义的理解就是工程的模块化组装,把提示词做成若干模块化内容,实际使用灵活装配,就像后端曾经喜欢使用的微服务架构一样。
分段定义
既然是模块化的,那就得告诉 AI 都有什么模块,模块叫什么,有什么内容。把一大段字符串拆成字典,每个 key 是一个主题:
PROMPT_SECTIONS = {
"identity": "You are a coding agent. Act, don't explain.",
"tools": "Available tools: bash, read_file, write_file.",
"workspace": f"Working directory: {WORKDIR}",
"memory": "Relevant memories are injected below when available.",
}
每个 section 独立维护。例如,修改 tools 不影响 identity,新增 memory 不动 workspace。
按需拼接
开篇也说了,对话中不是所有 section 每次都需要。当前没有记忆文件,加载 memory section 只是浪费 token。根据 context 的真实状态决定加载哪些,在代码里能看出来其实就是个 if 条件判断:
def assemble_system_prompt(context: dict) -> str:
sections = []
# 始终加载
sections.append(PROMPT_SECTIONS["identity"])
sections.append(PROMPT_SECTIONS["tools"])
sections.append(PROMPT_SECTIONS["workspace"])
# 按需加载 — 基于真实状态,不是关键词
memories = context.get("memories", "")
if memories:
sections.append(f"Relevant memories:\n{memories}")
return "\n\n".join(sections)
“始终加载”的是每轮都需要的:身份、工具、工作目录。“按需加载”的只在特定条件下才有用。
缓存机制
⚠️ 缓存机制需要说明的是,不只是多轮用户提问,更不是指跨thread 的用户提问,而是指再一次用户提问后,AI 在 agent loop 里面循环的每一轮可以走缓存。
在一个 loop 循环中,无论 AI 调用多少次,除了第一次,后续的系统提示词都会需要重新拼接和读取,会变成命中缓存直接读取。
更新方式
那么既然在同一个循环中,只要上下文没变化,我就不重新拼装,那么我要怎么判断变化了要重新拼接呢?
那就是利用一个叫做 context 的对象函数,它可以反应档期那运行态的真实状态,会列出来实际注册的工具、memory 检查等等,每次循环就检查一下 context,如果变了,那就重新组装,没变就走缓存。
CC 中的系统提示词
CC 和上面介绍的相比还不同,CC 不是单纯组装这么简单,也不是只有 4 种 section,而是一个不固定数量的状态,而且整体分为3类:
-
静态 section:即始终加载的额 section;
-
动态 section:按照状态加载,即按需加载 section。
-
易失性 section:只有 MCP 是这种 section,就是说可以出现两轮都是用了,但是一次是断开一个是开启。
因为易失性 section 太特殊且只有 MCP 这一种,后面我们就不再提及。
动态和静态section 的缓存策略
如果按照 CC 的做法,那么一个 prompt 就可以看成是静态 section 组和动态 section 组的结合。为了更方便 AI 区分开他们,CC 在动态和静态的 group 之间插入了一个SYSTEM_PROMPT_DYNAMIC_BOUNDARY标记,这就让提示词变成了:
[静态部分]
你是 Claude Code
工具说明
Agent说明
...
===== BOUNDARY =====
[动态部分]
git status
当前时间
memory
这样 AI 一下就知道,哦,在这个上面的都是静态 section 的提示词,下面都是动态的,这样既可以让静态提示词参与缓存,而动态提示词不参与缓存。