系统提示:5–10K Token 的工程系统
你在终端里敲的那一句话,模型实际收到的比你看到的多出一个数量级。Claude Code 的 system prompt 不是一个被「精心写好」的文本,而是一个被工程化拼装的运行时系统:它有缓存边界、有段落类型、有优先级 override、有模型版本兼容处理。这章拆开它的每一层。
它在做什么
System prompt 的职责很朴素:在模型开始「读你的消息」之前,把它应该扮演什么角色、能做什么不能做什么、怎么回答、回答成什么风格这些规则讲清楚。但 Claude Code 把这件事做成了一门独立的工程——这才是有意思的地方。
翻开 prompts.ts,911 行代码只干一件事:getSystemPrompt() 接受运行时上下文,返回一个字符串数组,数组每个元素是一段独立的 prompt,最后会被拼接成模型看到的完整指令。
const getSystemPrompt = async (context: PromptContext): Promise<string[]> => {
const sections: string[] = [];
// 静态段:所有用户共享
sections.push(getSimpleIntroSection()); // 身份定义
sections.push(CYBER_RISK_INSTRUCTION); // 安全准则
sections.push(getSimpleSystemSection()); // 系统行为
sections.push(getSimpleDoingTasksSection()); // 做事准则
sections.push(getActionsSection()); // 谨慎行动
sections.push(getUsingYourToolsSection()); // 工具使用
sections.push(getSimpleToneAndStyleSection()); // 风格要求
sections.push(getOutputEfficiencySection()); // 输出效率
// 动态分界线
sections.push(SYSTEM_PROMPT_DYNAMIC_BOUNDARY);
// 动态段:每个用户独立
if (context.claudeMd) sections.push(context.claudeMd);
if (context.memory) sections.push(await loadMemoryPrompt());
sections.push(getEnvInfoSection()); // 当前目录、git 状态
sections.push(getMcpInstructionsSection(context.mcpClients));
sections.push(getLanguagePreferenceSection());
return sections;
};
8 个静态段 + N 个动态段,总长 5000–10000 token。在你按下回车之前,模型已经把这一整本「小册子」读完了。
| 段落 | 源函数 | 核心内容 |
|---|---|---|
| 身份定义 | getSimpleIntroSection() | 「你是 Claude Code,Anthropic 的官方 CLI 工具」 |
| 安全准则 | CYBER_RISK_INSTRUCTION | 安全边界:允许和禁止的操作 |
| 系统行为 | getSimpleSystemSection() | Markdown 渲染、权限模式、hooks 处理 |
| 做事准则 | getSimpleDoingTasksSection() | 不过度工程、不编造数据、不随意删文件 |
| 谨慎行动 | getActionsSection() | 高风险操作前确认、不用破坏性方式解决问题 |
| 工具使用 | getUsingYourToolsSection() | 优先用专用工具而非 Bash |
| 风格要求 | getSimpleToneAndStyleSection() | 不用 emoji、简洁直接、引用代码格式 |
| 输出效率 | getOutputEfficiencySection() | 开门见山、先行动后解释 |
静态部分是所有用户共享的——这一段全世界每个 Claude Code 用户在每个 session 里看到的都一字不差。动 态部分则每个用户独立加载:你的 CLAUDE.md、当前目录信息、记忆文件、MCP 服务器说明、git 状态、语言偏好……
为什么要这样分层?答案写在源码注释里不是为了优雅,而是为了省钱。
缓存分界线:一刀切出来的运营成本
源码里有一个常量专门标出了静态/动态的接缝:
export const SYSTEM_PROMPT_DYNAMIC_BOUNDARY =
'__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__';
这一行字符串本身就是一道缓存作用域的分界。分界线上面的部分用 cacheScope: 'global' 做跨组织缓存——所有用户共用一次;分界线下 面的部分按内容做 hash 缓存,每个用户一份。
源码里那段注释直白得让人惊讶:
Session-variant guidance that would fragment the cacheScope: 'global' prefix if placed before
SYSTEM_PROMPT_DYNAMIC_BOUNDARY. Each conditional here is a runtime bit that would otherwise multiply the Blake2b prefix hash variants (2^N). See PR #24490, #24171 for the same bug class.
翻译成人话:如果分界线上方放了任何「有条件」的内容(比如根据用户类型显示不同文字),缓存命中率会指数级下降——每多一个条件分支,缓存变体数量翻倍(2^N)。这两个 PR 就是修复这类「把动态内容误放到静态区域」的 bug。
为什么这件事值得专门写两个 PR?因为 Claude API 的缓存机制是:两次请求的 prompt 前缀完全相同时,第二次可以直接复用第一次的缓存,不需要重新处理。这意味着所有用户共享的静态段只需要处理一次,后续每次请求都直接命中缓存,按 1/10 的价格计费。把一个有条件的 if-else 误放进静态区域,等于把全局缓存炸成 N 个变体——每个变体各自缓存、各自失效、独立计费。这不是技术洁癖,是直接打到毛利率的事。
⚠️ 一个朴素的工程原则:缓存边界决定 prompt 结构,而不是反过来。 不要先想好要写什么,再随便排列顺序;而是先确定哪些内容在所有 session 里都不变(最稳定)、哪些随用户变化(次稳定)、哪些每次请求都变(不稳定),然后按这个稳定性梯度从前到后排列。稳定的内容放前面,能被全局缓存;不稳定的内容放后面,单独走 hash 缓存。把可变内容塞进稳定区域是缓存系统最大的浪费来源。
三种段落类型
源码里给了三种注册段落的方式,每种对应不同的缓存策略:
| 类型 | 函数 | 缓存行为 | 使用场景 |
|---|---|---|---|
| 静态段 | 直接字符串 | 全局缓存 | 身份定义、安全准则、做事准则 |
| 动态段 | systemPromptSection() | 按内容 hash 缓存 | 环境信息、记忆、语言偏好 |
| 不缓存段 | DANGEROUS_uncachedSystemPromptSection() | 不缓存 | MCP 服务器指令(随时可能变化) |
第三种名字里那个 DANGEROUS_ 不是修辞,是写给后来维护者看的警告牌:
DANGEROUS_uncachedSystemPromptSection(
'mcp_instructions',
() => isMcpInstructionsDeltaEnabled()
? null
: getMcpInstructionsSection(mcpClients),
'MCP servers connect/disconnect between turns',
);
MCP 服务器可能在对话中途连接或断开,所以它的指令不能缓存——缓存了可能会给模型提供过期的信息:明明服务器已经掉了,模型还以为指令有效。但不缓存意味着每次 API 调用都要重新发送这部分内容,等于放弃了那一段的所有缓存红利。所以 Anthropic 选了 DANGEROUS_ 这个词:用这个函数之前三思,确定真的不能缓存。
内外部版本:理想状态照进现实
源码里大量 process.env.USER_TYPE === 'ant' 的判断,把 system prompt 切成了对外版和对内版两套。差异最大的几处:
代码风格指令(仅内部版)
外部版没有特别的代码注释要求。内部版多了四条严格要求:
// 内部版独有
- Default to writing no comments. Only add one when the WHY is non-obvious.
- Don't explain WHAT the code does, since well-named identifiers already do that.
- Don't reference the current task, fix, or callers.
- Before reporting a task complete, verify it actually works.
最后一条单独被源码注释标注成 [capy v8 thoroughness counterweight]——「Capybara v8 模型的「不够仔细」倾向的反制措施」。他们发现这个模型有时候会说「完成了」但其实没验证,所以在 prompt 里明确要求验证。Capybara 是 Anthropic 内部对 Claude 某版本的代号,这个命名直接把模型版本的弱点和对应的 prompt 补丁写在了源码里。
诚实性指令(仅内部版)
Report outcomes faithfully: if tests fail, say so with the relevant
output; if you did not run a verification step, say that rather than
implying it succeeded. Never claim "all tests pass" when output shows
failures…
注释标注:[False-claims mitigation for Capybara v8 (29-30% FC rate vs v4's 16.7%)]。翻译过来就是:Capybara v8 的虚假声明率是 29-30%,比 v4 的 16.7% 高了一倍,所以需要更强的 prompt 来抑制。
这个数据很值得记住。即使是最先进的模型,虚假声明率也不低——Anthropic 对自己模型的缺陷有清醒的认识,并用 prompt 工程来补偿。
输出风格
差异最大的部分。外部版只有 4 行:「开门见山、简洁、别废话」。内部版是一整段长文:
你写的是给人读的文字,不是往控制台打日志。假设用户已经离开了,丢失了上下文,他们不知道你中间用了什么代号、什么缩写。写的时候要让他们能 cold pick up。
还有具体的写作要求:用连贯的散文,避免碎片式表达、过多的破折号、符号和记号;不要把解释性推理塞进表格格子里;避免语义回溯,让读者能线性阅读,不需要回头重新理解前面的内容。
外部版为什么没有这些?可能是因为外部用户更习惯看代码式的简洁输出,也可能是因为还在 A/B 测试阶段。源码注释里确实标注了 [un-gate once validated on external via A/B]——「外部版 A/B 测试通过后就解锁」。
外部版的 prompt 是产品,内外部的差异是理想状态。 内部版的那些严格要求,本质上是 Anthropic 认为「AI 应该怎样工作」的答案。如果你也在设计自己的 AI 产品,内部版的 prompt 是一个很好的参考基准——它把「什么是好的输出」拆成了可验证的规则。