system-prompt.mdx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. ---
  2. title: "System Prompt 动态组装 - AI 工作记忆构建"
  3. description: "深入解析 Claude Code 的 System Prompt 动态组装过程:缓存策略、分界标记、Section 注册表、CLAUDE.md 多级合并,以及如何将零散上下文拼装为 API 可消费的缓存友好结构。"
  4. keywords: ["System Prompt", "系统提示词", "动态组装", "CLAUDE.md", "Prompt Cache", "缓存策略"]
  5. ---
  6. ## 从数组到 API 调用:System Prompt 的完整链路
  7. System Prompt 在 Claude Code 中不是一段写死的文本,而是一个 **`string[]` 数组**(品牌类型 `SystemPrompt`,定义于 `src/utils/systemPromptType.ts:8`),经过组装、分块、缓存标记后发送给 API。
  8. ### 三阶段管道
  9. ```
  10. getSystemPrompt() → string[] (组装内容)
  11. buildEffectiveSystemPrompt() → SystemPrompt (选择优先级路径)
  12. buildSystemPromptBlocks() → TextBlockParam[] (分块 + cache_control 标记)
  13. ```
  14. 1. **`getSystemPrompt()`**(`src/constants/prompts.ts:444`)—— 收集静态段 + 动态段,插入 `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` 分界标记
  15. 2. **`buildEffectiveSystemPrompt()`**(`src/utils/systemPrompt.ts:41`)—— 按 Override > Coordinator > Agent > Custom > Default 优先级选择
  16. 3. **`buildSystemPromptBlocks()`**(`src/services/api/claude.ts:3214`)—— 调用 `splitSysPromptPrefix()` 分块,为每个块附加 `cache_control`
  17. ## SystemPrompt 品牌类型
  18. ```typescript
  19. // src/utils/systemPromptType.ts:8
  20. export type SystemPrompt = readonly string[] & {
  21. readonly __brand: 'SystemPrompt'
  22. }
  23. export function asSystemPrompt(value: readonly string[]): SystemPrompt {
  24. return value as SystemPrompt // 零开销类型断言
  25. }
  26. ```
  27. 品牌类型(branded type)防止普通 `string[]` 被意外传入 API 调用——只有通过 `asSystemPrompt()` 显式转换才能获得 `SystemPrompt` 类型。
  28. ## getSystemPrompt():内容组装的全景
  29. `src/constants/prompts.ts:444` 是 System Prompt 的核心工厂函数,返回一个有序数组:
  30. | 阶段 | 内容 | 缓存策略 |
  31. |------|------|----------|
  32. | **静态区** | Intro Section、System Rules、Doing Tasks、Actions、Using Tools、Tone & Style、Output Efficiency | 可跨组织缓存(`scope: 'global'`) |
  33. | **BOUNDARY** | `SYSTEM_PROMPT_DYNAMIC_BOUNDARY = '__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__'` | 分界标记(不发送给 API) |
  34. | **动态区** | Session Guidance、Memory、Model Override、Env Info、Language、Output Style、MCP Instructions、Scratchpad、FRC、Summarize Tool Results、Token Budget、Brief | 每次会话不同(`scope: 'org'` 或无缓存) |
  35. ### 动态区的 Section 注册表
  36. 动态区通过 `systemPromptSection()` / `DANGEROUS_uncachedSystemPromptSection()` 注册,这两个工厂函数定义于 `src/constants/systemPromptSections.ts`:
  37. ```typescript
  38. // 缓存式 Section:计算一次,/clear 或 /compact 后才重新计算
  39. systemPromptSection('memory', () => loadMemoryPrompt())
  40. // 危险:每轮重新计算,会破坏 Prompt Cache
  41. DANGEROUS_uncachedSystemPromptSection(
  42. 'mcp_instructions',
  43. () => isMcpInstructionsDeltaEnabled() ? null : getMcpInstructionsSection(mcpClients),
  44. 'MCP servers connect/disconnect between turns' // 必须给出破坏缓存的理由
  45. )
  46. ```
  47. `resolveSystemPromptSections()` 在每轮查询时解析所有 Section,对于 `cacheBreak: false` 的 Section,优先使用 `getSystemPromptSectionCache()` 中的缓存值。只有 MCP 指令等真正动态的内容使用 `DANGEROUS_uncachedSystemPromptSection`。
  48. ### `CLAUDE_CODE_SIMPLE` 快速路径
  49. 当环境变量 `CLAUDE_CODE_SIMPLE` 为真时,整个 System Prompt 缩减为一行:
  50. ```typescript
  51. `You are Claude Code, Anthropic's official CLI for Claude.\n\nCWD: ${getCwd()}\nDate: ${getSessionStartDate()}`
  52. ```
  53. 跳过所有 Section 注册、缓存分块、动态组装——用于最小化 token 消耗的测试场景。
  54. ## buildEffectiveSystemPrompt():五级优先级
  55. `src/utils/systemPrompt.ts:41` 决定最终使用哪个 System Prompt:
  56. | 优先级 | 条件 | 行为 |
  57. |--------|------|------|
  58. | **0. Override** | `overrideSystemPrompt` 非空 | 完全替换,返回 `[override]` |
  59. | **1. Coordinator** | `COORDINATOR_MODE` feature + 环境变量 | 使用协调者专用提示词 |
  60. | **2. Agent** | `mainThreadAgentDefinition` 存在 | Proactive 模式:追加到默认提示词尾部;否则:替换默认提示词 |
  61. | **3. Custom** | `--system-prompt` 参数指定 | 替换默认提示词 |
  62. | **4. Default** | 无特殊条件 | 使用 `getSystemPrompt()` 完整输出 |
  63. `appendSystemPrompt` 始终追加到末尾(Override 除外)。
  64. ## 缓存策略:分块、标记、命中
  65. 这是 System Prompt 设计中最精密的部分。
  66. ### Anthropic Prompt Cache 基础
  67. Anthropic API 的 Prompt Cache 允许跨请求复用相同的 System Prompt 前缀,按缓存命中量计费(远低于完整输入价格)。缓存键由内容的 Blake2b 哈希决定——任何字符变化都会导致缓存失效。
  68. ### `splitSysPromptPrefix()`:三种分块模式
  69. `src/utils/api.ts:321` 是缓存策略的核心,根据条件选择三种分块模式:
  70. #### 模式 1:MCP 工具存在时(`skipGlobalCacheForSystemPrompt=true`)
  71. ```
  72. [attribution header] → cacheScope: null (不缓存)
  73. [system prompt prefix] → cacheScope: 'org' (组织级缓存)
  74. [everything else] → cacheScope: 'org' (组织级缓存)
  75. ```
  76. MCP 工具列表在会话中可能变化(连接/断开),破坏了跨组织缓存的基础,因此降级为组织级。
  77. #### 模式 2:Global Cache + Boundary 存在(1P 专用)
  78. ```
  79. [attribution header] → cacheScope: null (不缓存)
  80. [system prompt prefix] → cacheScope: null (不缓存)
  81. [static content] → cacheScope: 'global' (全局缓存!跨组织共享)
  82. [dynamic content] → cacheScope: null (不缓存)
  83. ```
  84. 这是缓存效率最高的模式。`SYSTEM_PROMPT_DYNAMIC_BOUNDARY` 之前的静态内容(Intro、Rules、Tone & Style 等)对所有用户相同,可跨组织缓存。
  85. #### 模式 3:默认(3P 提供商 或 Boundary 缺失)
  86. ```
  87. [attribution header] → cacheScope: null (不缓存)
  88. [system prompt prefix] → cacheScope: 'org' (组织级缓存)
  89. [everything else] → cacheScope: 'org' (组织级缓存)
  90. ```
  91. ### `getCacheControl()`:TTL 决策
  92. `src/services/api/claude.ts:359` 生成的 `cache_control` 对象:
  93. ```typescript
  94. {
  95. type: 'ephemeral',
  96. ttl?: '1h', // 仅特定 querySource 符合条件时
  97. scope?: 'global', // 仅静态区
  98. }
  99. ```
  100. 1 小时 TTL 的判定逻辑(`should1hCacheTTL()`,第 394 行):
  101. - **Bedrock 用户**:通过环境变量 `ENABLE_PROMPT_CACHING_1H_BEDROCK` 启用
  102. - **1P 用户**:通过 GrowthBook 配置的 `allowlist` 数组匹配 `querySource`,支持前缀通配符(如 `"repl_main_thread*"`)
  103. - **会话级锁定**:资格判定结果在 bootstrap state 中缓存,防止 GrowthBook 配置中途变化导致同一会话内 TTL 不一致
  104. ### 缓存破坏:Session-Specific Guidance 的放置
  105. `getSessionSpecificGuidanceSection()`(`src/constants/prompts.ts:352`)的内容必须放在 `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` **之后**。因为它包含:
  106. - 当前会话的 enabledTools 集合
  107. - `isForkSubagentEnabled()` 的运行时判定
  108. - `getIsNonInteractiveSession()` 的结果
  109. 这些运行时 bit 如果放在静态区,会产生 2^N 种 Blake2b 哈希变体(N = 运行时条件数),完全破坏缓存命中率。源码注释明确警告:
  110. > 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.
  111. ### `CLAUDE_CODE_SIMPLE` 模式
  112. 当设置了 `CLAUDE_CODE_SIMPLE` 环境变量时,整个系统提示词会大幅缩减:
  113. ```typescript
  114. return [`You are Claude Code, Anthropic's official CLI for Claude.\n\nCWD: ${getCwd()}\nDate: ${getSessionStartDate()}`]
  115. ```
  116. ## 上下文注入:System Context 与 User Context
  117. System Prompt 数组本身不包含运行时上下文(git 状态、CLAUDE.md 内容)。上下文通过两个独立的管道注入:
  118. ### System Context(`src/context.ts:116`)
  119. ```typescript
  120. export const getSystemContext = memoize(async () => {
  121. return {
  122. gitStatus, // git 分支、状态、最近提交(截断至 MAX_STATUS_CHARS=2000)
  123. cacheBreaker, // 仅 ant 用户的缓存破坏器
  124. }
  125. })
  126. ```
  127. - 使用 `lodash.memoize` 缓存——**整个会话期间只计算一次**
  128. - Git 状态快照包含 5 个并行 `git` 命令(branch、defaultBranch、status、log、userName)
  129. - `status` 超过 2000 字符时截断并附加提示使用 BashTool 获取更多信息
  130. - `systemPromptInjection` 变更时,通过 `getUserContext.cache.clear?.()` 清除所有上下文缓存
  131. ### User Context(`src/context.ts:155`)
  132. ```typescript
  133. export const getUserContext = memoize(async () => {
  134. return {
  135. claudeMd, // 合并后的 CLAUDE.md 内容
  136. currentDate, // "Today's date is YYYY-MM-DD."
  137. }
  138. })
  139. ```
  140. - **CLAUDE.md 禁用条件**:`CLAUDE_CODE_DISABLE_CLAUDE_MDS` 环境变量,或 `--bare` 模式(除非通过 `--add-dir` 显式指定目录)
  141. - `--bare` 模式的语义是"跳过我没要求的东西"而非"忽略所有"
  142. ### 注入位置
  143. 在 `src/query.ts:449`:
  144. ```typescript
  145. // System Context 追加到 System Prompt 尾部
  146. const fullSystemPrompt = asSystemPrompt(
  147. appendSystemContext(systemPrompt, systemContext) // 简单拼接
  148. )
  149. ```
  150. User Context 通过 `prependUserContext()`(`src/utils/api.ts:449`)注入为 `<system-reminder>` 标签包裹的首条用户消息,放在所有对话消息之前。
  151. ## Attribution Header:计费与安全
  152. 每个 API 请求的 System Prompt 首块是 Attribution Header(`src/constants/system.ts:30`),包含:
  153. - **`cc_version`**:Claude Code 版本 + 指纹
  154. - **`cc_entrypoint`**:入口点标识(REPL / SDK / pipe 等)
  155. - **`cch=00000`**(NATIVE_CLIENT_ATTESTATION 启用时):Bun 原生 HTTP 层在发送前将零替换为计算出的哈希值,服务器验证此 token 确认请求来自真实 Claude Code 客户端
  156. Header 始终 `cacheScope: null`——它因版本和指纹不同而变化,不适合缓存。
  157. ## CLAUDE.md:项目级知识注入
  158. 这是 Claude Code 最巧妙的设计之一。在项目根目录放一个 `CLAUDE.md` 文件,就能让 AI "理解" 你的项目:
  159. - **项目概述**:这个项目做什么、用了什么技术栈
  160. - **开发约定**:代码风格、命名规范、分支策略
  161. - **常用命令**:怎么构建、怎么测试、怎么部署
  162. - **注意事项**:已知的坑、特殊的配置
  163. 系统会自动发现并合并多级 CLAUDE.md:
  164. ```
  165. ~/.claude/CLAUDE.md ← 用户全局(个人偏好)
  166. └── /project/CLAUDE.md ← 项目根目录(团队共享)
  167. └── /project/src/CLAUDE.md ← 子目录(模块特定)
  168. ```
  169. 加载逻辑在 `src/utils/claudemd.ts` 中的 `getClaudeMds()` 和 `getMemoryFiles()` 实现——从 CWD 向上遍历目录树,合并所有匹配的 CLAUDE.md 文件内容。
  170. ## 设计洞察:为什么是 `string[]` 而非单个 `string`
  171. 将 System Prompt 设计为数组而非单段文本,是为了 **缓存分块**:
  172. 1. Anthropic Prompt Cache 以 **内容块**(TextBlock)为缓存单位
  173. 2. 将 System Prompt 拆为多个块,可以让不变的部分(Intro、Rules)获得独立的缓存命中
  174. 3. 如果是单个 `string`,任何一个字符变化(如日期更新)都会导致整个 System Prompt 的缓存失效
  175. 4. `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` 标记允许 `splitSysPromptPrefix()` 精确地将静态区标记为 `scope: 'global'`,动态区不标记或标记为 `scope: 'org'`
  176. 这是 Claude Code 在 token 成本优化上的核心设计——一次典型的 System Prompt 约 20K+ tokens,通过缓存分块可以节省 30-50% 的输入 token 费用。