Claude Code 源码解剖

Claude Code 的系统提示词

从 systemPromptSection、SYSTEM_PROMPT_DYNAMIC_BOUNDARY、splitSysPromptPrefix、buildSystemPromptBlocks 和 buildEffectiveSystemPrompt,拆解系统提示词如何成为可缓存的行为控制面。

第 6 章 16 分钟

Claude Code 的系统提示词

Claude Code 的系统提示词不是一段硬编码大字符串,而是一套可分段、可缓存、可覆盖、可降级的控制面。

这一章要解决的是:系统提示词如何从多个来源生成,哪些内容被缓存,哪些内容必须每轮重算,静态规则和动态上下文如何分界,最后又如何变成 Anthropic API 的 TextBlockParam[]

核心直觉

系统提示词不只是“模型应该怎么表现”的文本。它还是 prompt cache 的输入协议:哪些字节稳定、哪些字节变化、哪些块能全局缓存,都由源码明确控制。

先看源码入口

源码定位点关键符号负责什么
restored-src/src/constants/systemPromptSections.ts:10-14SystemPromptSection系统提示词段落抽象:名称、计算函数、缓存策略
restored-src/src/constants/systemPromptSections.ts:43-58resolveSystemPromptSections()并行解析段落,命中或写入段落缓存
restored-src/src/constants/systemPromptSections.ts:65-68clearSystemPromptSections()/clear/compact 后清理段落缓存和 beta header latch
restored-src/src/constants/prompts.ts:114-115SYSTEM_PROMPT_DYNAMIC_BOUNDARY静态区和动态区的带内边界标记
restored-src/src/constants/prompts.ts:444-577getSystemPrompt()构建默认系统提示词数组
restored-src/src/constants/prompts.ts:491-555动态 section 注册memoryenv_info_simplemcp_instructions 等动态段落
restored-src/src/constants/prompts.ts:513-520DANGEROUS_uncachedSystemPromptSection('mcp_instructions', ...)唯一每轮重算的动态 MCP 指令
restored-src/src/utils/api.ts:321-435splitSysPromptPrefix()把系统提示词数组切成带缓存作用域的块
restored-src/src/services/api/claude.ts:3213-3237buildSystemPromptBlocks()SystemPromptBlock[] 转成 API TextBlockParam[]
restored-src/src/utils/systemPrompt.ts:41-123buildEffectiveSystemPrompt()合成 override、coordinator、agent、custom、default、append

读这一章要把“提示词内容”和“缓存协议”放在一起看。很多源码设计不是为了文案优雅,而是为了缓存稳定。

总流程图

flowchart TB A["getSystemPrompt(tools, model, dirs, mcpClients)"] --> B{"CLAUDE_CODE_SIMPLE?"} B -->|是| C["最小提示词<br/>身份 + CWD + 日期"] B -->|否| D{"Proactive / KAIROS?"} D -->|是| E["自治 Agent 精简提示词<br/>不走 section registry"] D -->|否| F["构建动态 sections<br/>systemPromptSection / DANGEROUS"] F --> G["resolveSystemPromptSections()<br/>Promise.all + section cache"] G --> H["组装 systemPrompt[]<br/>静态区 + boundary + 动态区"] H --> I["buildEffectiveSystemPrompt()<br/>override / coordinator / agent / custom / default / append"] I --> J["splitSysPromptPrefix()<br/>global / org / null cacheScope"] J --> K["buildSystemPromptBlocks()<br/>添加 cache_control"] K --> L["Anthropic API system blocks"]

这条链路里有三个边界:

  • 段落边界:SystemPromptSection 管一个段落能否缓存。
  • 静态/动态边界:SYSTEM_PROMPT_DYNAMIC_BOUNDARY 管全局可缓存前缀。
  • 来源优先级边界:buildEffectiveSystemPrompt() 管谁覆盖谁。

段落注册表:系统提示词的最小单位不是字符串,而是 section

原文给出的 section 抽象:

type SystemPromptSection = {
  name: string;
  compute: ComputeFn;
  cacheBreak: boolean;
};

这个结构证明每个段落都有三件事:

字段作用
name缓存键和可观测名称
compute计算段落内容,可能返回 stringnull 或 Promise
cacheBreak是否每轮重算,false 表示可记忆化

创建段落有两条路径:

工厂函数cacheBreak适合内容
systemPromptSection(name, compute)false会话内稳定内容,如语言、环境、memory 读取结果
DANGEROUS_uncachedSystemPromptSection(name, compute, reason)true两轮之间可能变化,且过期会造成错误的内容

DANGEROUS_ 前缀是故意增加 API 摩擦:开发者必须写 reason,代码审查时就能追问“为什么不能缓存”。

resolveSystemPromptSections() 如何缓存

原文给出的核心函数:

export async function resolveSystemPromptSections(
  sections: SystemPromptSection[],
): Promise<(string | null)[]> {
  const cache = getSystemPromptSectionCache();
  return Promise.all(
    sections.map(async s => {
      if (!s.cacheBreak && cache.has(s.name)) {
        return cache.get(s.name) ?? null;
      }
      const value = await s.compute();
      setSystemPromptSectionCacheEntry(s.name, value);
      return value;
    }),
  );
}

这段代码证明四个行为:

行为说明
Promise.all段落并行计算,适合读取 CLAUDE.md、环境信息等 I/O
!s.cacheBreak &amp;&amp; cache.has(s.name)可缓存段落命中后不再重算
null 也缓存“本段不存在”也是稳定结果,避免重复判断
cacheBreak 段落每次 compute易变段落不读缓存

缓存存放在 STATE.systemPromptSectionCache,不是普通模块变量。这样 /clear/compact 可以统一清理。

清理函数:

export function clearSystemPromptSections(): void {
  clearSystemPromptSectionState();
  clearBetaHeaderLatches();
}

这段代码说明 /clear/compact 不只是清聊天记录,也会让系统提示词段落重新计算,并重置 beta header 锁存器。

哪些段落必须每轮重算:mcp_instructions 是关键例子

原文指出源码中唯一典型的未缓存段落是 MCP 指令:

DANGEROUS_uncachedSystemPromptSection(
  'mcp_instructions',
  () =>
    isMcpInstructionsDeltaEnabled()
      ? null
      : getMcpInstructionsSection(mcpClients),
  'MCP servers connect/disconnect between turns',
);

这段代码证明 mcp_instructions 不缓存的原因很具体:MCP server 可能在两轮之间连接或断开。如果第 1 轮缓存了 server A 的指令,第 3 轮 server B 连接了,但缓存仍返回旧指令,模型就永远不知道 B。

对比另一个案例更能看出取舍:原文提到 token_budget 曾经是未缓存段落,因为它依赖当前 turn budget。但这样每次预算变化都会破坏约 20K token 的缓存。后来改写提示词,让“没有预算”时自然成为 no-op,于是可以降级为普通 systemPromptSection

读源码抓手

看到 DANGEROUS_uncachedSystemPromptSection,不要只读内容。一定要读 reason,它才说明这段为什么值得牺牲 prompt cache。

静态/动态边界:SYSTEM_PROMPT_DYNAMIC_BOUNDARY

系统提示词数组里有一个特殊标记:

export const SYSTEM_PROMPT_DYNAMIC_BOUNDARY =
  '__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__';

这个字符串不是给模型看的,而是给下游 splitSysPromptPrefix() 看的带内信号。

getSystemPrompt() 返回数组里,它被放在静态段落和动态段落之间:

getSimpleIntroSection(...)
getSimpleSystemSection()
getSimpleDoingTasksSection()
getActionsSection()
getUsingYourToolsSection(...)
getSimpleToneAndStyleSection()
getOutputEfficiencySection()
SYSTEM_PROMPT_DYNAMIC_BOUNDARY
session_guidance
memory
env_info_simple
language
output_style
mcp_instructions
scratchpad
...
区域内容类型缓存目标
边界前身份、通用行为、工具使用原则、输出风格等跨会话稳定规则尽量 global cache
边界后session guidance、CLAUDE.md、环境、语言、MCP、scratchpad 等会话内容不全局缓存或只会话/组织级处理

边界只在 shouldUseGlobalCacheScope() 为真时插入:

...(shouldUseGlobalCacheScope() ? [SYSTEM_PROMPT_DYNAMIC_BOUNDARY] : [])

原文还引用了 getSessionSpecificGuidanceSection 的源码注释:如果会话变量放在边界前,会让 Blake2b prefix hash 变体按 2^N 膨胀。

这就是系统提示词架构里最重要的缓存不变量:边界前不能混入会话状态。

splitSysPromptPrefix():把逻辑数组切成缓存块

splitSysPromptPrefix()systemPrompt: string[] 转成带 cacheScope 的块。它有三条路径。

flowchart TD A["splitSysPromptPrefix(systemPrompt, options)"] --> B{"shouldUseGlobalCacheScope()<br/>且 skipGlobalCacheForSystemPrompt?"} B -->|是| C["路径 1: MCP 降级<br/>跳过 boundary<br/>合并为 org cache"] B -->|否| D{"shouldUseGlobalCacheScope()?"} D -->|否| E["路径 3: 默认 org cache"] D -->|是| F{"找到 boundary?"} F -->|是| G["路径 2: 全局缓存 + 边界<br/>static = global<br/>dynamic = null"] F -->|否| E

路径 1:MCP 降级

触发条件是 shouldUseGlobalCacheScope() 为真,但 skipGlobalCacheForSystemPrompt 也为真。原文说明这个 flag 来自 claude.ts:只有 MCP 工具实际渲染进请求时才触发。

核心行为:

for (const prompt of systemPrompt) {
  if (!prompt) continue;
  if (prompt === SYSTEM_PROMPT_DYNAMIC_BOUNDARY) continue;
  if (prompt.startsWith('x-anthropic-billing-header')) {
    attributionHeader = prompt;
  } else if (CLI_SYSPROMPT_PREFIXES.has(prompt)) {
    systemPromptPrefix = prompt;
  } else {
    rest.push(prompt);
  }
}

这段代码证明 boundary 在降级路径里被跳过,剩余内容合并为 org 级缓存块。原因是 MCP 工具 schema 本身是用户级动态内容,即使系统提示词能全局缓存,整体请求也已有大块不可全局缓存内容,继续维护全局系统缓存收益变小。

路径 2:全局缓存 + 边界

这是无 MCP 降级时的高收益路径:

const boundaryIndex = systemPrompt.findIndex(
  s => s === SYSTEM_PROMPT_DYNAMIC_BOUNDARY,
);
if (boundaryIndex !== -1) {
  for (let i = 0; i < systemPrompt.length; i++) {
    const block = systemPrompt[i];
    if (!block || block === SYSTEM_PROMPT_DYNAMIC_BOUNDARY) continue;
    if (block.startsWith('x-anthropic-billing-header')) {
      attributionHeader = block;
    } else if (CLI_SYSPROMPT_PREFIXES.has(block)) {
      systemPromptPrefix = block;
    } else if (i < boundaryIndex) {
      staticBlocks.push(block);
    } else {
      dynamicBlocks.push(block);
    }
  }
}

这段代码证明边界前后被分成不同块:

cacheScope说明
attribution headernull计费归因头,不缓存
system prompt prefixnullCLI 前缀标识,不缓存
static content&#39;global&#39;跨组织共享的静态系统规则
dynamic contentnull每会话变化内容,不缓存

路径 3:默认 org 缓存

如果不启用全局缓存,或找不到边界标记,函数会把非特殊内容合成一个 org 级缓存块。

这条路径是保守兜底:没有边界,就不要假装知道哪里能全局缓存。

buildSystemPromptBlocks()cacheScope 变成 API cache_control

splitSysPromptPrefix() 的结果还要经过 buildSystemPromptBlocks()

export function buildSystemPromptBlocks(
  systemPrompt: SystemPrompt,
  enablePromptCaching: boolean,
  options?: { skipGlobalCacheForSystemPrompt?: boolean; querySource?: QuerySource },
): TextBlockParam[] {
  return splitSysPromptPrefix(systemPrompt, {
    skipGlobalCacheForSystemPrompt: options?.skipGlobalCacheForSystemPrompt,
  }).map(block => ({
    type: 'text' as const,
    text: block.text,
    ...(enablePromptCaching && block.cacheScope !== null && {
      cache_control: getCacheControl({
        scope: block.cacheScope,
        querySource: options?.querySource,
      }),
    }),
  }));
}

这段代码证明:缓存不是注释或内部标记,而是最终进入 API 请求的 cache_controlcacheScope !== null 的块才获得 cache_control;动态块或计费头这类 null 块不缓存。

行为链路是:

SYSTEM_PROMPT_DYNAMIC_BOUNDARY
-> splitSysPromptPrefix()
-> SystemPromptBlock.cacheScope
-> buildSystemPromptBlocks()
-> TextBlockParam.cache_control
-> API 后端按 global / org 缓存

getSystemPrompt():默认系统提示词如何构建

getSystemPrompt() 接收工具列表、模型、额外工作目录、MCP clients,返回 string[]

它有三条快速路径:

路径触发条件行为
SimpleCLAUDE_CODE_SIMPLE返回最小提示词,只有身份、cwd、日期
ProactivePROACTIVEKAIROS 活跃返回自治 Agent 精简提示词,不走 section registry
标准路径默认构造动态 sections,解析后拼静态区、边界、动态区

标准路径里的动态段落包括:

段落名称缓存类型内容
session_guidance记忆化工具引导、交互模式提示
memory记忆化CLAUDE.md 记忆文件
ant_model_override记忆化内部模型覆盖指令
env_info_simple记忆化cwd、OS、Shell 等环境信息
language记忆化语言偏好
output_style记忆化输出风格
mcp_instructions易变MCP server 指令
scratchpad记忆化草稿本指令
frc记忆化函数结果清理指令
summarize_tool_results记忆化工具结果摘要指令
numeric_length_anchors记忆化内部长度锚点
token_budget记忆化token 预算指令
brief记忆化KAIROS brief 段落

唯一需要每轮重算的是 mcp_instructions。这让动态区大部分内容仍然可在会话内记忆化,减少重复 I/O 和计算。

buildEffectiveSystemPrompt():多来源优先级

默认提示词还不是最终提示词。实际 API 调用前,还要合成多个来源。

优先级链:

overrideSystemPrompt
> coordinatorSystemPrompt
> agentSystemPrompt
> customSystemPrompt
> defaultSystemPrompt
+ appendSystemPrompt

最高优先级是 override:

if (overrideSystemPrompt) {
  return asSystemPrompt([overrideSystemPrompt]);
}

这段代码证明 override 是完整替换:一旦存在,其他来源包括 appendSystemPrompt 都被忽略。

没有 override 和 coordinator 时,核心合成逻辑:

return asSystemPrompt([
  ...(agentSystemPrompt
    ? [agentSystemPrompt]
    : customSystemPrompt
      ? [customSystemPrompt]
      : defaultSystemPrompt),
  ...(appendSystemPrompt ? [appendSystemPrompt] : []),
]);

这段代码证明:

来源行为
agentSystemPrompt替换 custom/default
customSystemPrompt替换 default
defaultSystemPrompt最低优先级
appendSystemPrompt除 override 外追加到末尾

容易误解

“追加提示词”不是永远追加。overrideSystemPrompt 存在时,append 也不会生效,因为 override 表示整个控制面被替换。

状态和数据结构

结构字段或值影响
SystemPromptSectionname段落缓存键
SystemPromptSectioncompute段落内容来源
SystemPromptSectioncacheBreak是否每轮重算
STATE.systemPromptSectionCache`Map<string, stringnull>`会话内段落缓存
SYSTEM_PROMPT_DYNAMIC_BOUNDARY__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__切分静态区和动态区
SystemPromptBlocktextcacheScopeAPI 之前的缓存块
TextBlockParamcache_control最终 API 缓存控制
Effective system promptoverride/coordinator/agent/custom/default/append最终模型看到的系统指令

设计取舍

取舍点源码体现工程判断
分段而非大字符串SystemPromptSection每段有独立计算和缓存策略,便于维护和失效
DANGEROUS_ API 摩擦必填 reason每轮重算会破坏缓存,必须解释
null 也缓存cache.get(s.name) ?? null“没有该段”也是稳定结果
边界是带内信号SYSTEM_PROMPT_DYNAMIC_BOUNDARY让逻辑数组携带缓存分区信息
会话变量赶到边界后session_guidance 等在动态区防止全局缓存前缀变体爆炸
MCP 触发降级skipGlobalCacheForSystemPrompt动态工具 schema 存在时,不强求全局系统缓存
override 完整替换return [overrideSystemPrompt]需要强控制模式时,避免默认规则混入
append 低侵入补丁appendSystemPrompt 末尾追加支持补充指令,但不打乱主优先级链

读源码抓手

  1. 先看 systemPromptSections.ts,确认 section 的 name / compute / cacheBreak
  2. 再看 resolveSystemPromptSections(),理解段落缓存如何命中和写入。
  3. DANGEROUS_uncachedSystemPromptSection 的调用点,重点读 mcp_instructions 的 reason。
  4. 进入 constants/prompts.ts,找到 SYSTEM_PROMPT_DYNAMIC_BOUNDARY 在返回数组中的位置。
  5. splitSysPromptPrefix() 三条路径,确认 global/org/null cacheScope 如何产生。
  6. buildSystemPromptBlocks(),把 cacheScope 和 API cache_control 对上。
  7. 最后读 buildEffectiveSystemPrompt(),确认 override、coordinator、agent、custom、default、append 谁覆盖谁。

小结

- Claude Code 的系统提示词是分段式控制面,不是单个字符串。 - systemPromptSection 让每个段落拥有独立缓存策略。 - SYSTEM_PROMPT_DYNAMIC_BOUNDARY 把静态全局前缀和动态会话内容分开。 - splitSysPromptPrefix() 把逻辑边界转成 globalorg 或无缓存的 API 块。 - buildEffectiveSystemPrompt() 用清晰优先级链处理多来源提示词,避免不同模式互相污染。