从数组到 API 调用:System Prompt 的完整链路
System Prompt 在 Claude Code 中不是一段写死的文本,而是一个string[] 数组(品牌类型 SystemPrompt,定义于 src/utils/systemPromptType.ts:8),经过组装、分块、缓存标记后发送给 API。
三阶段管道
getSystemPrompt()(src/constants/prompts.ts:444)—— 收集静态段 + 动态段,插入SYSTEM_PROMPT_DYNAMIC_BOUNDARY分界标记buildEffectiveSystemPrompt()(src/utils/systemPrompt.ts:41)—— 按 Override > Coordinator > Agent > Custom > Default 优先级选择buildSystemPromptBlocks()(src/services/api/claude.ts:3214)—— 调用splitSysPromptPrefix()分块,为每个块附加cache_control
SystemPrompt 品牌类型
string[] 被意外传入 API 调用——只有通过 asSystemPrompt() 显式转换才能获得 SystemPrompt 类型。
getSystemPrompt():内容组装的全景
src/constants/prompts.ts:444 是 System Prompt 的核心工厂函数,返回一个有序数组:
| 阶段 | 内容 | 缓存策略 |
|---|---|---|
| 静态区 | Intro Section、System Rules、Doing Tasks、Actions、Using Tools、Tone & Style、Output Efficiency | 可跨组织缓存(scope: 'global') |
| BOUNDARY | SYSTEM_PROMPT_DYNAMIC_BOUNDARY = '__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__' | 分界标记(不发送给 API) |
| 动态区 | Session Guidance、Memory、Model Override、Env Info、Language、Output Style、MCP Instructions、Scratchpad、FRC、Summarize Tool Results、Token Budget、Brief | 每次会话不同(scope: 'org' 或无缓存) |
动态区的 Section 注册表
动态区通过systemPromptSection() / DANGEROUS_uncachedSystemPromptSection() 注册,这两个工厂函数定义于 src/constants/systemPromptSections.ts:
resolveSystemPromptSections() 在每轮查询时解析所有 Section,对于 cacheBreak: false 的 Section,优先使用 getSystemPromptSectionCache() 中的缓存值。只有 MCP 指令等真正动态的内容使用 DANGEROUS_uncachedSystemPromptSection。
CLAUDE_CODE_SIMPLE 快速路径
当环境变量 CLAUDE_CODE_SIMPLE 为真时,整个 System Prompt 缩减为一行:
buildEffectiveSystemPrompt():五级优先级
src/utils/systemPrompt.ts:41 决定最终使用哪个 System Prompt:
| 优先级 | 条件 | 行为 |
|---|---|---|
| 0. Override | overrideSystemPrompt 非空 | 完全替换,返回 [override] |
| 1. Coordinator | COORDINATOR_MODE feature + 环境变量 | 使用协调者专用提示词 |
| 2. Agent | mainThreadAgentDefinition 存在 | Proactive 模式:追加到默认提示词尾部;否则:替换默认提示词 |
| 3. Custom | --system-prompt 参数指定 | 替换默认提示词 |
| 4. Default | 无特殊条件 | 使用 getSystemPrompt() 完整输出 |
appendSystemPrompt 始终追加到末尾(Override 除外)。
缓存策略:分块、标记、命中
这是 System Prompt 设计中最精密的部分。Anthropic Prompt Cache 基础
Anthropic API 的 Prompt Cache 允许跨请求复用相同的 System Prompt 前缀,按缓存命中量计费(远低于完整输入价格)。缓存键由内容的 Blake2b 哈希决定——任何字符变化都会导致缓存失效。splitSysPromptPrefix():三种分块模式
src/utils/api.ts:321 是缓存策略的核心,根据条件选择三种分块模式:
模式 1:MCP 工具存在时(skipGlobalCacheForSystemPrompt=true)
模式 2:Global Cache + Boundary 存在(1P 专用)
SYSTEM_PROMPT_DYNAMIC_BOUNDARY 之前的静态内容(Intro、Rules、Tone & Style 等)对所有用户相同,可跨组织缓存。
模式 3:默认(3P 提供商 或 Boundary 缺失)
getCacheControl():TTL 决策
src/services/api/claude.ts:359 生成的 cache_control 对象:
should1hCacheTTL(),第 394 行):
- Bedrock 用户:通过环境变量
ENABLE_PROMPT_CACHING_1H_BEDROCK启用 - 1P 用户:通过 GrowthBook 配置的
allowlist数组匹配querySource,支持前缀通配符(如"repl_main_thread*") - 会话级锁定:资格判定结果在 bootstrap state 中缓存,防止 GrowthBook 配置中途变化导致同一会话内 TTL 不一致
缓存破坏:Session-Specific Guidance 的放置
getSessionSpecificGuidanceSection()(src/constants/prompts.ts:352)的内容必须放在 SYSTEM_PROMPT_DYNAMIC_BOUNDARY 之后。因为它包含:
- 当前会话的 enabledTools 集合
isForkSubagentEnabled()的运行时判定getIsNonInteractiveSession()的结果
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.
CLAUDE_CODE_SIMPLE 模式
当设置了 CLAUDE_CODE_SIMPLE 环境变量时,整个系统提示词会大幅缩减:
上下文注入:System Context 与 User Context
System Prompt 数组本身不包含运行时上下文(git 状态、CLAUDE.md 内容)。上下文通过两个独立的管道注入:System Context(src/context.ts:116)
- 使用
lodash.memoize缓存——整个会话期间只计算一次 - Git 状态快照包含 5 个并行
git命令(branch、defaultBranch、status、log、userName) status超过 2000 字符时截断并附加提示使用 BashTool 获取更多信息systemPromptInjection变更时,通过getUserContext.cache.clear?.()清除所有上下文缓存
User Context(src/context.ts:155)
- CLAUDE.md 禁用条件:
CLAUDE_CODE_DISABLE_CLAUDE_MDS环境变量,或--bare模式(除非通过--add-dir显式指定目录) --bare模式的语义是”跳过我没要求的东西”而非”忽略所有”
注入位置
在src/query.ts:449:
prependUserContext()(src/utils/api.ts:449)注入为 <system-reminder> 标签包裹的首条用户消息,放在所有对话消息之前。
Attribution Header:计费与安全
每个 API 请求的 System Prompt 首块是 Attribution Header(src/constants/system.ts:30),包含:
cc_version:Claude Code 版本 + 指纹cc_entrypoint:入口点标识(REPL / SDK / pipe 等)cch=00000(NATIVE_CLIENT_ATTESTATION 启用时):Bun 原生 HTTP 层在发送前将零替换为计算出的哈希值,服务器验证此 token 确认请求来自真实 Claude Code 客户端
cacheScope: null——它因版本和指纹不同而变化,不适合缓存。
CLAUDE.md:项目级知识注入
这是 Claude Code 最巧妙的设计之一。在项目根目录放一个CLAUDE.md 文件,就能让 AI “理解” 你的项目:
- 项目概述:这个项目做什么、用了什么技术栈
- 开发约定:代码风格、命名规范、分支策略
- 常用命令:怎么构建、怎么测试、怎么部署
- 注意事项:已知的坑、特殊的配置
src/utils/claudemd.ts 中的 getClaudeMds() 和 getMemoryFiles() 实现——从 CWD 向上遍历目录树,合并所有匹配的 CLAUDE.md 文件内容。
设计洞察:为什么是 string[] 而非单个 string
将 System Prompt 设计为数组而非单段文本,是为了 缓存分块:
- Anthropic Prompt Cache 以 内容块(TextBlock)为缓存单位
- 将 System Prompt 拆为多个块,可以让不变的部分(Intro、Rules)获得独立的缓存命中
- 如果是单个
string,任何一个字符变化(如日期更新)都会导致整个 System Prompt 的缓存失效 SYSTEM_PROMPT_DYNAMIC_BOUNDARY标记允许splitSysPromptPrefix()精确地将静态区标记为scope: 'global',动态区不标记或标记为scope: 'org'