Skip to main content

系统提示:5–10K Token 的工程系统

你在终端里敲的那一句话,模型实际收到的比你看到的多出一个数量级。Claude Code 的 system prompt 不是一个被「精心写好」的文本,而是一个被工程化拼装的运行时系统:它有缓存边界、有段落类型、有优先级 override、有模型版本兼容处理。这章拆开它的每一层。

它在做什么

System prompt 的职责很朴素:在模型开始「读你的消息」之前,把它应该扮演什么角色、能做什么不能做什么、怎么回答、回答成什么风格这些规则讲清楚。但 Claude Code 把这件事做成了一门独立的工程——这才是有意思的地方。

翻开 prompts.ts911 行代码只干一件事: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 是一个很好的参考基准——它把「什么是好的输出」拆成了可验证的规则。

五层优先级 override

systemPrompt.ts 的另一个发现:system prompt 不是一个固定模板,而是一个五层优先级系统,从高到低依次覆盖:

优先级来源使用场景
1(最高)overrideSystemPrompt测试/调试模式,完全替换
2Coordinator Mode多 Agent 协调模式(第 9 章详述)
3Agent System Prompt特定领域 agent 的指令
4Custom System Prompt用户通过 --system-prompt 指定
5(最低)Default System Prompt标准 Claude Code 指令

还有一个特殊的 appendSystemPrompt不论使用哪一层都会追加到末尾——给系统管理员留的兜底补丁位。

这套设计支持了 Claude Code 从单一 CLI 到多模式平台的演进:普通用户看到 Default;研发用 override 完全替换以测试新指令;Coordinator 模式调用子 agent 时看到 Agent prompt;子 agent 自己又看到自己的 prompt。一层套一层,每层只关心自己范围内的优先级,不需要改动下层。

三种运行模式

源码实际上实现了三套完全不同的 system prompt,根据运行模式切换:

模式 A:简化模式(CLAUDE_CODE_SIMPLE=1

You are Claude Code, Anthropic's official CLI for Claude. CWD:
/path/to/project Date: 2026-04-02

就这么多,三行。用于嵌入式场景(比如作为子进程被调用),不需要完整的行为指导。

模式 B:标准交互模式

就是前面详述的完整 prompt——8 个静态段 + N 个动态段,几千 token。

模式 C:Proactive 模式(KAIROS / 助手模式)

源码里有一段分叉逻辑:

if (proactiveModule?.isProactiveActive()) {
return [
'You are an autonomous agent. Use the available tools to do useful work.',
getSystemRemindersSection(),
await loadMemoryPrompt(),
envInfo,
/* mcp instructions */
];
}

身份定义从「你是 Claude Code,帮用户做编程任务」变成「你是一个自主 Agent,用工具做有用的事」。行为约束也大幅简化。这暗示了 KAIROS 不是 Claude Code 的增强版,而是一个概念上不同的产品——一个不等用户指令就主动行动的 AI。

缓存 TTL 的稳定性工程

一个容易被忽略但非常精巧的设计:缓存 TTL 的稳定性处理

Claude API 支持两种缓存 TTL:5 分钟(默认)和 1 小时(付费用户)。源码中用户是否有资格使用 1 小时缓存的判断,被锁定在 session 启动时

let userEligible = getPromptCache1hEligible();
if (userEligible === null) {
userEligible =
process.env.USER_TYPE === 'ant' ||
(isClaudeAISubscriber() && !currentLimits.isUsingOverage);
setPromptCache1hEligible(userEligible); // 锁定
}

为什么要锁定?因为如果用户在会话中途从免费变成付费(或反过来),TTL 会从 5 分钟变成 1 小时。TTL 变化会改变 cache key,导致整个 prompt cache 失效——浪费大约 20000 token 的重新处理。

所以 Anthropic 选择在 session 开始时就决定 TTL,整个 session 内不变。稳定性比精确性更重要——偶尔用错 TTL 的成本远低于 mid-session cache 失效的成本。

工程教训:System Prompt 是一段工程,不是文字

把上面的所有设计放在一起看,会发现一个清晰的结论:

System prompt 是一个工程系统,不是一段文字。

它需要像代码一样管理版本、测试效果、优化性能。Claude Code 的 prompt 有明确的缓存策略、模块化结构、AB 测试标注、模型版本兼容处理。这不是「写一段好的指令」的问题,而是「构建一个可维护的指令系统」的问题。

几个关键 takeaway:

1. 缓存决定 prompt 结构

不要想好了内容再随便排顺序。先划出缓存边界,把不变的放前面,变的放后面。每一个放错位置的条件分支都在浪费缓存预算。

2. 用 prompt 补偿模型缺陷

Anthropic 不会假装自己的模型完美。Capybara v8 虚假声明率 30%?那就在 prompt 里加反虚假声明指令。模型不够仔细?加「完成前必须验证」。了解你的模型的弱点,然后用 prompt targeting 来修补

3. 内外版本暴露了理想状态

内部版的那些严格要求,就是 Anthropic 认为 AI 应该怎样工作的理想状态。如果你在设计自己的 AI 产品,内部版的 prompt 是个很好的参考基准。

4. 不同模式需要不同的 prompt

Proactive 模式的 prompt 和普通模式完全不同。不要试图用一个通用 prompt 覆盖所有使用场景。场景不同,身份定义和行为约束都应该不同。

5. 缓存系统比你想象的脆弱

社区发现了一个真实案例:Claude Code v2.1.76 前后出现了 prompt cache 回归 bug。两个技术原因叠加:每次请求中 attestation 数据的变化导致缓存哈希失效,以及 anti-distillation 注入的假工具定义在每次请求中不同。

正常情况下,Turn 4 的成本应该降到 Turn 1 的 13%(缓存命中),但 bug 版本中成本反而从 $0.04 膨胀到 $0.40。缓存前缀中任何微小的变化都会导致整个缓存失效,成本膨胀 10–20 倍。这不是 Claude Code 特有的问题,而是所有使用 prompt 缓存的 AI 产品都要警惕的陷阱。


下一章进入工具系统——从「AI 怎么回答」过渡到「AI 能做什么」。59 个工具是怎么注册、怎么编排、怎么和权限系统打交道的,是另一门独立的工程。