SOURCE-CODE DEEP DIVE

Claude Code 是如何把
「你是谁、用户在哪、系统怎样」
装进一次 API 调用的?

本文档追踪 defaultSystemPromptbaseUserContextsystemContext 三个核心上下文从创建到最终发送的完整生命周期 —— 四个阶段,七次转换,两个注入点,三层缓存。 即便你从未读过 Claude Code 源码,读完后也能精确指出每个字节去了哪里、为什么去那里。

核心源码 · QueryEngine.ts · query.ts · queryContext.ts · context.ts · api.ts · claude.ts
本文导航
  1. 一、为什么需要三个上下文?整体认知
  2. 二、四阶段总览:数据如何流动
  3. 三、阶段一 · 数据创建(QueryEngine.submitMessage)
  4. 四、阶段二 · 传入查询(query → queryLoop)
  5. 五、阶段三 · 循环内转换(每轮迭代)
  6. 六、阶段四 · API 层最终转换(queryModel)
  7. 七、完整实例 · 跟随一条用户消息走完全程
  8. 八、缓存策略 · 为什么这样切块
  9. 九、多轮对话 · 不变性与变化性
  10. 十、总结 · 设计要点

01为什么需要三个上下文?

当 Claude Code 调用 Anthropic API 时,需要让模型同时知道三件事 —— 你是谁用户在哪系统当前如何。 这三条信息职责不同、变化频率不同、缓存策略也不同,因此必须分三路独立管理。

defaultSystemPrompt

你是谁、该怎么做

类型string[]
来源getSystemPrompt()
内容:身份介绍、任务执行规则、工具使用指南、语气风格、记忆机制等模型行为级提示词。

baseUserContext

用户的项目环境

类型{ [k: string]: string }
来源getUserContext()
内容:项目里的 AGENTS.md / MEMORY.md 全文 + 当前日期。

systemContext

当前系统状态

类型{ [k: string]: string }
来源getSystemContext()
内容:git 分支/状态/最近提交、可选的 cacheBreaker 调试标记。

关键设计决策

三个值最终会去到 API 请求的不同位置defaultSystemPromptsystemContext 合并到 system 参数;baseUserContext 注入到 messages 数组的最前面, 伪装成一条 isMeta=true 的用户消息。两条独立通道喂给模型,互不重叠。

02四阶段总览

从用户敲下回车的那一刻,到 HTTPS 请求离开你的网卡,三个上下文要经过 4 个阶段、7 次转换。 下图标出每个阶段的入口函数与产出。

flowchart TB User(["用户输入"]) --> S1 subgraph S1["阶段一 · 创建(QueryEngine.submitMessage)"] direction TB FSP["fetchSystemPromptParts()
三路 Promise.all 并行"] FSP --> A1["defaultSystemPrompt
string[]"] FSP --> A2["baseUserContext
{claudeMd, currentDate}"] FSP --> A3["systemContext
{gitStatus?, cacheBreaker?}"] A1 --> B1["systemPrompt
asSystemPrompt(...)"] A2 --> B2["userContext
+ coordinator 上下文"] A3 --> B3["systemContext
透传不变"] end S1 --> S2 subgraph S2["阶段二 · 传入查询(query → queryLoop)"] direction LR Q["query()"] --> QL["queryLoop()
三个值作为不可变参数"] end S2 --> S3 subgraph S3["阶段三 · 循环内转换(每轮迭代)"] direction TB ASC["appendSystemContext
合并 systemContext → fullSystemPrompt"] PUC["prependUserContext
注入 userContext 到消息列表头"] end S3 --> S4 subgraph S4["阶段四 · API 层(queryModel)"] direction TB AT["追加 attributionHeader / CLISyspromptPrefix
追加 advisor / chrome 指令"] AT --> SP["splitSysPromptPrefix
分块 + 缓存标记"] SP --> NORM["normalizeMessagesForAPI
归一化消息"] end S4 --> API[["anthropic.beta.messages.create"]] style S1 fill:#FFF6F0,stroke:#D77757 style S2 fill:#F3EAFF,stroke:#6B4A8E style S3 fill:#FFF7E6,stroke:#C8862C style S4 fill:#E8F5EE,stroke:#2D8659

图 2.1 · 完整数据流(四阶段全景)

阶段速览表

阶段 主要入口 关键转换 三个值的状态
① 创建 QueryEngine.submitMessage
fetchSystemPromptParts
Promise.all 三路并行获取;初步包装 asSystemPrompt;合并 coordinator 上下文。 三个值首次成形
② 传入 queryqueryLoop 仅创建/复用 Langfuse trace,三个值作为不可变参数透传。 不变
③ 循环转换 queryLoop 每轮 appendSystemContext 把 systemContext 拼到 systemPrompt 末尾; prependUserContext 把 userContext 注入消息头。 fullSystemPrompt 出现,消息列表被改写
④ API 层 queryModel in claude.ts 追加归因头/CLI 前缀/advisor 指令;分块;标 cache_control;归一化消息。 转为 TextBlockParam[] + MessageParam[]

03阶段一 · 数据创建

当用户回车后,QueryEngine.submitMessage() 第一件事是调用 fetchSystemPromptParts()。 这是一次三路并行的"启动取数",目的是压缩首包等待时间

Stage 1 · Entry

fetchSystemPromptParts() — 三路并行

// src/utils/queryContext.ts:64-75
const [defaultSystemPrompt, userContext, systemContext] = await Promise.all([
  // 第一路:系统提示词(有自定义则跳过)
  customSystemPrompt !== undefined
    ? Promise.resolve([])
    : getSystemPrompt(tools, mainLoopModel, additionalWorkingDirectories, mcpClients),

  // 第二路:用户侧上下文(始终执行)
  getUserContext(),

  // 第三路:系统侧上下文(有自定义则跳过)
  customSystemPrompt !== undefined ? Promise.resolve({}) : getSystemContext(),
])

为什么自定义 prompt 会让另外两条返回空?

当 SDK 调用方传入 customSystemPrompt 时,语义是"完全替换默认系统提示"。 如果继续拼 systemContext(比如 git 状态),它会附着到自定义提示之上,造成模型困惑 —— 因为本属于默认提示的部分(如"你是 Claude Code")已经不在了,git 状态突然冒出来很奇怪。 所以 customPrompt 模式下,第一路返回 []、第三路返回 {},只保留第二路的 userContext

3.1 第一路:defaultSystemPrompt 怎么生成

getSystemPrompt()src/constants/prompts.ts 生成。返回 string[],每个元素是一个段落。 根据环境变量与 feature flag 选择三种模式之一:

① 简化模式 CLAUDE_CODE_SIMPLE=1

仅一段身份+CWD+日期。脚本/自动化场景使用。

② 主动模式 PROACTIVE/KAIROS

autonomous agent 提示词 + 记忆 + 环境信息 + MCP 指令 + scratchpad。

③ 标准模式(最常用)

分静态/动态两段,中间用 SYSTEM_PROMPT_DYNAMIC_BOUNDARY 分隔,便于缓存分层。

标准模式的内部结构

flowchart LR subgraph STATIC["静态部分(global 缓存候选)"] direction TB SI["getSimpleIntroSection
身份介绍"] SS["getSimpleSystemSection
系统规则"] DT["getSimpleDoingTasksSection
任务执行"] AC["getActionsSection
操作谨慎性"] UT["getUsingYourToolsSection
工具指南"] TS["getSimpleToneAndStyleSection
语气风格"] OE["getOutputEfficiencySection
沟通效率"] SI --> SS --> DT --> AC --> UT --> TS --> OE end BOUNDARY["⫷ SYSTEM_PROMPT_DYNAMIC_BOUNDARY ⫸
边界标记,仅 1P 启用全局缓存时插入"] subgraph DYNAMIC["动态部分(每会话可能不同)"] direction TB SG["session_guidance"] MEM["memory · MEMORY.md 内容"] ENV["env_info_simple
CWD/平台/模型ID"] LANG["language"] MCP["mcp_instructions"] SCR["scratchpad"] FRC["function_result_clearing"] SUM["summarize_tool_results"] SG --> MEM --> ENV --> LANG --> MCP --> SCR --> FRC --> SUM end STATIC --> BOUNDARY --> DYNAMIC style STATIC fill:#E8F5EE,stroke:#2D8659 style DYNAMIC fill:#FFF7E6,stroke:#C8862C style BOUNDARY fill:#FFF6F0,stroke:#D77757,stroke-width:3px

图 3.1 · defaultSystemPrompt 标准模式的段落结构

3.2 第二路:baseUserContext 怎么生成

getUserContext()src/context.ts,返回键值对:

{
  claudeMd: "<AGENTS.md / MEMORY.md 拼接后的全文>",  // 可选
  currentDate: "Today's date is 2026-04-29.",           // 始终
}

claudeMd 的生成步骤

  1. 检查是否禁用:CLAUDE_CODE_DISABLE_CLAUDE_MDS=1--bare 模式跳过;
  2. getMemoryFiles() 递归扫描工作目录,找出全部 AGENTS.mdMEMORY.md
  3. filterInjectedMemoryFiles() 过滤已经通过其他路径注入的;
  4. getClaudeMds() 读取并按层级拼接所有文件内容;
  5. 结果缓存到 setCachedClaudeMdContent(),供 auto-mode 分类器复用。

缓存策略:使用 memoize 缓存,同一会话内只算一次。setSystemPromptInjection()(调试用)会清空缓存强制重算。

3.3 第三路:systemContext 怎么生成

getSystemContext()src/context.ts。结构:

{
  gitStatus: "This is the git status at the start of the conversation.\nCurrent branch: main\n...",
  cacheBreaker: "[CACHE_BREAKER: ...]",  // 仅调试模式
}

gitStatus 的生成 —— 5 个 git 命令并行执行:

命令用途处理
git rev-parse --abbrev-ref HEAD当前分支直接拼入
git remote show origin | grep 'HEAD branch'主分支直接拼入
git --no-optional-locks status --short文件状态限制 2000 字符,超长截断
git --no-optional-locks log --oneline -n 5最近 5 条提交直接拼入
git config user.name用户名直接拼入

3.4 阶段一末尾:初步包装与合并

三路结果回到 QueryEngine 后,会做两次轻量转换:

baseUserContext → userContext

合并 coordinator 上下文(feature 关闭时返回空对象)。

const userContext = {
  ...baseUserContext,
  ...getCoordinatorUserContext(
    mcpClients, scratchpadDir
  ),
}

defaultSystemPrompt → systemPrompt

三段式包装:custom 覆盖、追加 memoryMechanics、追加 appendSystemPrompt。

const systemPrompt = asSystemPrompt([
  ...(customPrompt !== undefined
    ? [customPrompt]
    : defaultSystemPrompt),
  ...(memoryMechanicsPrompt
    ? [memoryMechanicsPrompt] : []),
  ...(appendSystemPrompt
    ? [appendSystemPrompt] : []),
])

asSystemPrompt() 是品牌类型(branded type)包装函数,把 string[] 标记为 SystemPrompt 类型,防止与其他字符串数组混淆。本质仍是 string[]

04阶段二 · 传入查询

阶段二非常薄。query() 是一个异步生成器,不修改三个上下文值, 只做两件事:建 Langfuse trace,然后 yield* 委托给 queryLoop()

Stage 2 · Pass-through

query() 包装层

export async function* query(params: QueryParams): AsyncGenerator<...> {
  // 复用上游 trace(如果是子代理)或新建
  const langfuseTrace = params.toolUseContext.langfuseTrace
    ?? (isLangfuseEnabled() ? createTrace({...}) : null)

  const paramsWithTrace = langfuseTrace
    ? { ...params, toolUseContext: { ...params.toolUseContext, langfuseTrace } }
    : params

  let terminal: Terminal | undefined
  try {
    terminal = yield* queryLoop(paramsWithTrace, consumedCommandUuids)
  } finally {
    if (ownsTrace) endTrace(langfuseTrace, ...)
  }
  return terminal!
}

queryLoop 的不可变参数 vs 可变状态

不可变参数(整个循环不重赋值)

{
  systemPrompt,    // 阶段一产出
  userContext,     // 阶段一产出
  systemContext,   // 阶段一产出
  canUseTool,
  fallbackModel,
  querySource,
  maxTurns,
}

可变状态(每轮迭代可能更新)

{
  messages,                  // 每轮追加 assistant + tool_result
  toolUseContext,
  autoCompactTracking,
  pendingToolUseSummary,
  turnCount,                 // 每轮 +1
  transition,
}

05阶段三 · 循环内转换

每轮迭代代表一次"模型调用 → 工具执行 → 附件注入"的完整回合。 消息预处理管线先对 messages 做收敛,然后两次关键合并: appendSystemContext 拼系统提示词、prependUserContext 注入用户上下文。

5.1 消息预处理管线

每轮开始时,state.messages 经过六道工序,不影响三个上下文值,但会改变最终发送的消息内容:

flowchart LR M0["state.messages"] --> M1["getMessagesAfterCompactBoundary
截取 compact 边界后"] M1 --> M2["applyToolResultBudget
裁剪过长工具输出"] M2 --> M3["snipCompactIfNeeded
HISTORY_SNIP feature"] M3 --> M4["microcompactMessages
缓存编辑"] M4 --> M5["applyCollapsesIfNeeded
CONTEXT_COLLAPSE"] M5 --> M6["autoCompactIfNeeded
token 超限触发"] M6 --> MFQ(["messagesForQuery"]) style MFQ fill:#FFF6F0,stroke:#D77757,stroke-width:2px

图 5.1 · 消息预处理六道管线

5.2 关键合并 ① — appendSystemContext

Stage 3 · Merge System

把 systemContext 拼到 systemPrompt 末尾

// src/utils/api.ts:435-445
export function appendSystemContext(
  systemPrompt: SystemPrompt,
  context: { [k: string]: string },
): string[] {
  return [
    ...systemPrompt,
    Object.entries(context)
      .map(([key, value]) => `${key}: ${value}`)
      .join('\n'),
  ].filter(Boolean)
}
转换示意
输入 1:systemPrompt
["You are Claude Code...", "<system_rules>...", "BOUNDARY", "<tools>..."]
输入 2:systemContext
{ gitStatus: "branch: main\n...", cacheBreaker: "[CB: x]" }
↓ appendSystemContext
输出:fullSystemPrompt
["You are Claude Code...", "<system_rules>...", "BOUNDARY", "<tools>...", "gitStatus: branch: main\n...\ncacheBreaker: [CB: x]"]
⚠️ 注意 systemContext 被拼为单个字符串追加到末尾,不是每个键值对单独成元素。这样动态内容集中在边界标记之后,便于缓存分层。

5.3 关键合并 ② — prependUserContext

Stage 3 · Inject User

把 userContext 注入消息列表最前面

// src/utils/api.ts:447-472
export function prependUserContext(
  messages: Message[],
  context: { [k: string]: string },
): Message[] {
  if (process.env.NODE_ENV === 'test') return messages
  if (Object.entries(context).length === 0) return messages

  return [
    createUserMessage({
      content: `<system-reminder>
As you answer the user's questions, you can use the following context:
${Object.entries(context).map(([key, value]) => `# ${key}\n${value}`).join('\n')}

      IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task.
</system-reminder>`,
      isMeta: true,  // ← 标记为元消息,不在 UI 显示
    }),
    ...messages,
  ]
}

为什么用 <system-reminder> 包裹?

这是软注入策略:告诉模型"这些上下文可能相关也可能不相关",让模型自行判断是否引用。 如果直接以系统指令注入,会强制模型关注每一条上下文,反而稀释真正重要的用户输入。 末尾的 IMPORTANT 提示句明确给模型自由 —— "如果不相关,就不要回应它"。

为什么 isMeta=true?

元消息在消息回放、SDK 输出、UI 展示中会被过滤掉,避免重复显示。它对 API 而言是普通用户消息(API 不识别 meta 标记),但对 Claude Code 内部工具链是隐形的。

5.4 调用 deps.callModel — 阶段三的出口

// src/query.ts:627
for await (const message of deps.callModel({
  messages: prependUserContext(messagesForQuery, userContext),  // ← 注入
  systemPrompt: fullSystemPrompt,                                // ← 已合并
  thinkingConfig: toolUseContext.options.thinkingConfig,
  tools: toolUseContext.options.tools,
  signal: toolUseContext.abortController.signal,
  options: { ... },
})) { ... }

06阶段四 · API 层最终转换

最后一站。deps.callModel() 进入 src/services/api/claude.ts, 在这里完成归一化、追加归因、分块、缓存标记,最终通过 Anthropic SDK 发送 HTTPS 请求。

6.1 消息归一化

let messagesForAPI = normalizeMessagesForAPI(messages, filteredTools)

处理四件事:

6.2 系统提示词最终拼装

fullSystemPrompt 之上,再追加 API 层需要的额外内容:

// src/services/api/claude.ts:1377-1392
systemPrompt = asSystemPrompt([
  getAttributionHeader(fingerprint),       // ① 归因头(计费追踪)
  getCLISyspromptPrefix({...}),            // ② CLI 标识前缀
  ...systemPrompt,                          // ③ fullSystemPrompt 全部
  ...(advisorModel ? [ADVISOR_TOOL_INSTRUCTIONS] : []),       // ④ Advisor 指令(条件)
  ...(injectChromeHere ? [CHROME_TOOL_SEARCH_INSTRUCTIONS] : []), // ⑤ Chrome 指令(条件)
].filter(Boolean))
追加内容作用何时存在
getAttributionHeader基于首条用户消息计算的归因指纹,API 侧用于计费与追踪始终
getCLISyspromptPrefix标记请求来源(交互/非交互、是否有 appendSystemPrompt)始终
...systemPromptfullSystemPrompt(已含 systemContext)始终
ADVISOR_TOOL_INSTRUCTIONSAdvisor 模式启用时的工具说明Advisor feature 启用且模型支持时
CHROME_TOOL_SEARCH_INSTRUCTIONSChrome MCP 工具搜索指令Chrome MCP 工具存在且 tool search 启用时

6.3 分块 + 缓存标记 — splitSysPromptPrefix

这一步把 string[] 转为 API 需要的 TextBlockParam[],并为每块标记缓存作用域。三种策略:

flowchart TB INPUT["systemPrompt: string[]"] --> CHECK{需要缓存?} CHECK -->|否| FLAT["全部不缓存
cache_control: 不设置"] CHECK -->|是| MCP{存在 MCP 工具?} MCP -->|是| ORG["策略一 · MCP 降级
Block 1: attribution(null)
Block 2: CLISysPrefix(org)
Block 3: 其余拼接(org)"] MCP -->|否| BOUND{1P + 边界标记?} BOUND -->|是| GLOBAL["策略二 · 最佳分块
Block 1: attribution(null)
Block 2: CLISysPrefix(null)
Block 3: 静态内容(global)
Block 4: 动态内容(null)"] BOUND -->|否| DEFAULT["策略三 · 默认
Block 1: attribution(null)
Block 2: CLISysPrefix(org)
Block 3: 其余拼接(org)"] style GLOBAL fill:#E8F5EE,stroke:#2D8659,stroke-width:2px style ORG fill:#FFF7E6,stroke:#C8862C style DEFAULT fill:#F8F5F1,stroke:#8B6F5C

图 6.1 · splitSysPromptPrefix 的三种分块策略

策略二(最理想)— 全局缓存命中率最高

仅当 shouldUseGlobalCacheScope() 返回 true(1P provider + 全局缓存允许) SYSTEM_PROMPT_DYNAMIC_BOUNDARY 边界标记找到时启用。 Block 3(静态部分)以 scope: 'global' 标记,跨组织共享缓存, 所有 Claude Code 用户共用同一缓存条目,命中率极高。

每个 Block 转为如下结构:

{
  type: 'text',
  text: block.text,
  ...(enablePromptCaching && block.cacheScope !== null && {
    cache_control: {
      type: 'ephemeral',
      scope: block.cacheScope,  // 'global' or 'org'
    }
  }),
}

6.4 最终 API 请求结构

anthropic.beta.messages.create({
  model: normalizedModel,
  system,                    // ← 分块后的 TextBlockParam[]
  messages: messagesForAPI,  // ← 归一化后的消息(含 prependUserContext 注入)
  tools: allTools,           // ← 工具 schema
  thinking: thinkingConfig,
  betas,                     // ← beta headers
  ...
})

07完整实例 · 用户输入"帮我分析这个文件"

来跟一条真实消息走完全程。场景:Mac 上使用 Claude Code REPL,工作目录 /Users/fanhui/Desktop/github/claude-code, 模型 claude-sonnet-4,项目里有 AGENTS.md,是 git 仓库,无 MCP 服务器,无自定义系统提示词。

7.1 阶段一产出:三个原始值

defaultSystemPrompt(部分省略)

[
  // ━━━ 静态段落(可 global 缓存)━━━
  "You are an interactive agent that helps users with software engineering tasks...",
  "# System\n - All text you output outside of tool use is displayed...",
  "# Doing tasks\n - The user will primarily request you to perform...",
  "# Executing actions with care\n\nCarefully consider the reversibility...",
  "# Using your tools\n - Do not use tools when...",
  "# Tone and style\n - Only use emojis if the user explicitly requests...",
  "# Communicating with the user\n...",

  "SYSTEM_PROMPT_DYNAMIC_BOUNDARY",  // 边界标记

  // ━━━ 动态段落(不缓存或 org 缓存)━━━
  "# Session-specific guidance\n...",
  "# Memory\n\nThis is the content of the project's memory file...",
  "# Environment\n - Primary working directory: /Users/fanhui/...\n - Is a git repository: Yes\n - Platform: darwin...",
  "# Language\n\nWhen responding to the user, use the language...",
  "# Function result clearing\n...",
  "# Summarize tool results\n...",
]

baseUserContext

{
  claudeMd: "# AGENTS.md\n\nThis file provides guidance to Codex...\n## Commands\n```bash\nbun install\n...\n```\n...",
  currentDate: "Today's date is 2026-04-29.",
}

systemContext

{
  gitStatus: `This is the git status at the start of the conversation.
Current branch: main
Main branch: main
Git user: fanhui
Status:
M src/query.ts
M src/QueryEngine.ts

Recent commits:
1173a62 refactor: 统一 log.ts/debug.ts 的测试 mock
7ea69ca fix: 修复 build 过程中的问题`,
}

7.2 阶段三产出:合并后的 fullSystemPrompt 与消息列表

appendSystemContext 把 systemContext 拼为单个字符串追加:

fullSystemPrompt = [
  // ... 前面所有段落保持不变 ...
  "# Summarize tool results\n...",

  // ↓↓↓ 新增:systemContext 单字符串追加 ↓↓↓
  "gitStatus: This is the git status at the start of the conversation.\nCurrent branch: main\nMain branch: main\nGit user: fanhui\nStatus:\nM src/query.ts\nM src/QueryEngine.ts\n\nRecent commits:\n1173a62 refactor: ...\n7ea69ca fix: ...",
]

prependUserContext 在消息列表最前面注入元消息:

// 转换前
[ { role: "user", content: "帮我分析这个文件" } ]

// 转换后
[
  {
    role: "user",
    content: `<system-reminder>
As you answer the user's questions, you can use the following context:
# claudeMd
# AGENTS.md

This file provides guidance to Codex...
[...AGENTS.md 全文...]

# currentDate
Today's date is 2026-04-29.

      IMPORTANT: this context may or may not be relevant to your tasks...
</system-reminder>`,
    isMeta: true,
  },
  { role: "user", content: "帮我分析这个文件" },
]

7.3 阶段四产出:API 请求最终结构

POST https://api.anthropic.com/v1/messages

system 参数(4 个 TextBlockParam):
cache: null Block 1 · attributionHeader
"x-anthropic-billing-header: cc_version=2.1.888; ..."
cache: null Block 2 · CLISyspromptPrefix
"You are Claude Code, Anthropic's official CLI for Claude."
cache: global Block 3 · 静态内容 · 跨组织共享
"You are an interactive agent that helps users...

# System
...

# Doing tasks
...

# Executing actions with care
...

# Using your tools
...

# Tone and style
...

# Communicating with the user
..."
cache: null Block 4 · 动态内容 + systemContext
"# Session-specific guidance
...

# Memory
...

# Environment
 - Primary working directory: /Users/fanhui/Desktop/github/claude-code
 - Is a git repository: Yes
 - Platform: darwin
 - You are powered by claude-sonnet-4...

# Language
...

# Function result clearing
...

# Summarize tool results
...

gitStatus: Current branch: main
M src/query.ts
M src/QueryEngine.ts
Recent commits: ..."
messages 参数:
[
  {
    role: "user",
    content: "<system-reminder>
As you answer the user's questions, you can use the following context:
# claudeMd
# AGENTS.md
[...AGENTS.md 全文...]
# currentDate
Today's date is 2026-04-29.
IMPORTANT: this context may or may not be relevant...
</system-reminder>"
  },
  {
    role: "user",
    content: "帮我分析这个文件"
  }
]

7.4 模型视角:它到底"看到"了什么?

从模型的视角,信息分两条独立通道:

通道 1:system 参数 — "你是谁、该怎么做"
Block 1 · 不缓存
attribution 归因头
Block 2 · 不缓存
CLI 前缀("You are Claude Code")
Block 3 · ✨ global 缓存
身份 + 系统规则 + 任务指南 + 工具使用 + 语气风格 + 沟通效率(约 7 段)
Block 4 · 不缓存
会话指导 + 记忆 + 环境 + 语言 + MCP + gitStatus
通道 2:messages 参数 — "用户在哪、说了啥"
消息 1 · isMeta=true(来自 prependUserContext)
<system-reminder> 包裹的 AGENTS.md 全文 + 当前日期
消息 2 · 用户真实输入
"帮我分析这个文件"

关键观察

systemContext(git 状态)出现在 system 参数 Block 4 末尾在 messages 中;
userContext(AGENTS.md + 日期)出现在 messages 第一条在 system 中;
③ 用户原始输入是 messages 的第二条;
④ 两条通道互补不重复,模型同时拥有完整画面。

08缓存策略 · 为什么这样切块

Anthropic API 的 prompt cache 有三种作用域:global(跨组织)、org(组织内)、不缓存。 命中缓存可以把 token 成本降低 90%、首字节延迟降到 ~30%。三层切分背后的考量:

层级缓存作用域包含什么变化频率缓存价值
前缀 global
(跨组织共享)
身份介绍 + 系统规则 + 任务指南 + 工具使用 + 语气风格 + 沟通效率 极低(版本更新时变) ⭐⭐⭐⭐⭐
动态 org 或不缓存 session_guidance + memory + env_info + MCP 指令 + gitStatus 中等(会话内稳定,跨会话变化) ⭐⭐⭐
尾部 不缓存 advisor 指令 / chrome 指令

SYSTEM_PROMPT_DYNAMIC_BOUNDARY 的作用

这个边界标记的作用是清晰划分静态/动态,让 splitSysPromptPrefix 准确知道在哪一刀切开。 它是字符串字面量 "SYSTEM_PROMPT_DYNAMIC_BOUNDARY",作为数组的一个元素插在静态段落和动态段落之间, 在分块时被识别并移除(不会真的发给模型)。

flowchart LR A["静态段落 1-7
跨用户、跨会话稳定"] --> B[(SYSTEM_PROMPT_
DYNAMIC_BOUNDARY)] B --> C["动态段落 8+
+ systemContext"] A -.缓存.-> CACHE_G["global cache
所有 Claude Code 用户共享"] C -.不缓存或.-> CACHE_O["org cache
当前组织共享"] style A fill:#E8F5EE,stroke:#2D8659 style B fill:#FFF6F0,stroke:#D77757,stroke-width:3px style C fill:#FFF7E6,stroke:#C8862C style CACHE_G fill:#D5EFE0,stroke:#2D8659 style CACHE_O fill:#FFEFD0,stroke:#C8862C

图 8.1 · 边界标记切分静态/动态

MCP 工具对缓存的影响

为什么 MCP 工具会让全局缓存失效?

因为 MCP 工具的描述是因用户而异的(每个用户接的 MCP 服务器不同,工具数量、名字、描述都不同)。 全局缓存要求内容跨组织相同,MCP 描述破坏了这个前提。所以 skipGlobalCacheForSystemPrompt 会把整个系统提示词降级为 org 级缓存。 你 MCP 工具越多,缓存命中率越低 —— 这是个有意识的权衡。

09多轮对话 · 不变性与变化性

当模型回复包含 tool_use 块时,queryLoop 不会结束,而是进入下一轮迭代。 理解什么变、什么不变,是看懂多轮对话内存模型的关键。

9.1 跨轮不变性矩阵

数据跨轮行为说明
systemPrompt 不变 queryLoop 的不可变参数,整个查询循环期间不重赋值
userContext 不变 同上。每轮通过 prependUserContext 重新注入,但内容相同
systemContext 不变 同上。每轮通过 appendSystemContext 合并到 fullSystemPrompt
messages 每轮变化 追加 assistant + toolResults + 附件,或被 compact 替换
messagesForQuery 每轮重算 经预处理管线(budget → snip → micro → collapse → autocompact)

9.2 下一轮消息构成

// src/query.ts:1700
const next: State = {
  messages: [...messagesForQuery, ...assistantMessages, ...toolResults],
  ...
}
sequenceDiagram autonumber participant U as 用户 participant Q as queryLoop participant M as Model API U->>Q: "帮我分析文件" Note over Q: 阶段 3 · 第 1 轮
messages = [user_msg]
+ prependUserContext
= [system-reminder, user_msg] Q->>M: API 调用 ① M-->>Q: assistant + tool_use(Read, "x.ts") Note over Q: 执行 Read 工具 Q->>Q: messages.append(assistant + tool_result) Note over Q: 第 2 轮 · 重新预处理 messages
+ prependUserContext (再次)
userContext 相同,但作为新元素
插在最前面 Q->>M: API 调用 ②(缓存命中前缀) M-->>Q: assistant("文件包含...") Q->>U: 最终回答 Note over Q,M: systemPrompt / userContext / systemContext
在两轮间完全相同 — Anthropic prompt cache
识别重复前缀,避免 token 重复计费

图 9.1 · 双轮 tool_use 流程示意

9.3 compact 后的上下文恢复

当 token 接近上限时,autoCompactIfNeeded 会触发压缩:

// 压缩前
[user1, assistant1, tool_result1, ..., userN, assistantN, tool_resultN]

// 压缩后
[compact_summary, compact_boundary, ...新消息]

三个上下文值不受 compact 影响

压缩只针对消息历史,systemPromptuserContextsystemContext 始终完整传入。所以 compact 后模型仍能看到:环境信息(CWD、git 状态)、AGENTS.md 项目规则、当前日期。 丢失的只是详细对话历史,被摘要替代。

9.4 附件注入(在 tool_result 之后)

工具执行完成后,进入下一轮前会注入多种附件:

附件类型来源说明
队列命令getCommandsByMaxPriority()排队的用户命令转为附件
Memory 预取pendingMemoryPrefetch异步预取的相关记忆文件
Skill 发现pendingSkillPrefetch异步发现的可用技能

附件注入顺序很重要

必须先保证 tool_use / tool_result 配对完整,再注入普通附件。否则 API 会报协议错误(孤立 tool_use 或 tool_result)。

10设计要点总结

① 分离关注点

systemPrompt(行为规则)、userContext(项目环境)、systemContext(系统状态) 三者职责清晰,独立生成、独立缓存、独立注入。

② 缓存友好

静态/动态分离 + 边界标记 + 三层缓存(global/org/null), 最大化 prompt cache 命中率,减少 token 与延迟。

③ 跨轮不变

三个上下文值在 queryLoop 整个生命周期内不变, 保证多轮对话间一致性,并让 prompt cache 能识别重复前缀。

④ Compact 安全

压缩只影响消息历史,三个上下文值始终完整传入。 模型在压缩后仍能看到环境信息和项目规则。

⑤ 自定义覆盖

customSystemPrompt 语义上完全替代默认提示, 避免片段错配(比如 git 状态飞到无 Claude Code 身份的提示之上)。

⑥ 软注入策略

userContext 通过 <system-reminder> 包裹并标记 isMeta, 让模型自行判断相关性,不强制关注每条上下文。

核心数据流(一图回顾)

flowchart LR GSP["getSystemPrompt()"] --> DSP["defaultSystemPrompt"] GUC["getUserContext()"] --> BUC["baseUserContext"] GSC["getSystemContext()"] --> SC["systemContext"] DSP --> SP["systemPrompt
asSystemPrompt()"] BUC --> UC["userContext
+ coordinator"] SP --> ASC{"appendSystemContext"} SC --> ASC ASC --> FSP["fullSystemPrompt"] UC --> PUC{"prependUserContext"} MSGS["messagesForQuery"] --> PUC PUC --> FMSG["最终消息列表"] FSP --> CALL["deps.callModel"] FMSG --> CALL CALL --> ATTR["+ attributionHeader
+ CLISyspromptPrefix
+ advisor / chrome"] ATTR --> SPLIT["splitSysPromptPrefix
分块 + cache_control"] SPLIT --> SYS["system: TextBlockParam[]"] CALL --> NORM["normalizeMessagesForAPI"] NORM --> MSG["messages: MessageParam[]"] SYS --> API[("Anthropic API")] MSG --> API style ASC fill:#FFF6F0,stroke:#D77757,stroke-width:2px style PUC fill:#FFF6F0,stroke:#D77757,stroke-width:2px style API fill:#2A1F1A,color:#fff,stroke:#D77757

图 10.1 · 一图概括:从三路并行取数到 API 调用的全部链路

最后一个洞察

Claude Code 的上下文管理本质上是一个生产者-消费者管线: 三个独立生产者(getSystemPrompt / getUserContext / getSystemContext)并行启动, 中间经过两次轻量合并(appendSystemContext 拼系统、prependUserContext 注入用户), 最后由 API 层完成最后一公里的归一化、分块、缓存标记。 这种"分离 + 延迟合并"的设计让缓存粒度可控、修改局部化、并发取数能并行 —— 你看到的每一个细节,都为这三个目标之一服务。