系统提示词组装
Agent 系统作者:Claude Code 的系统提示词是 20 段注册表 + 显式静态/动态边界的装配件。拆开它,看真实代码里的分层是怎么做的。
不是一条字符串,是一个装配件
大多数人对”系统提示词”的心智模型:一段文本,开发者写好,调用模型时传进去。
Claude Code 的系统提示词不是这样。它是多层按顺序注入的装配件,在源码里有明确的类型、显式的边界标记、注册表式的组织。不是”写好一段文本”的一次性工作,是一个装配系统。
这一章把这个装配系统拆开讲。所有断言都绑定到源文件路径,括号里标注。
源码入口:三个函数 + 一个边界常量
先定位到核心代码。Claude Code 的系统提示词由三个函数装配出来:
getSystemPrompt(tools, model, ...)(constants/prompts.ts) —— 产出string[],静态段 + 动态段的清单buildEffectiveSystemPrompt({ ... })(utils/systemPrompt.ts) —— 在 override / agent / custom / default 之间选getSystemContext()+getUserContext()(context.ts) —— 拉取 git status / CLAUDE.md / 日期
最终发给模型的前,由 getCacheSharingParams(在 commands/compact/compact.ts 能看到)把这些组合起来。
关键常量:
// constants/prompts.ts
export const SYSTEM_PROMPT_DYNAMIC_BOUNDARY = '__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__'
这是一个字面量字符串 marker,会被放进 prompt 数组中,显式分隔静态内容(可 cache)和动态内容(可能变)。Anthropic 的 API 基于这个 marker 做 global cache scope。下面会详细讲。
五层优先级:谁的 prompt 赢
buildEffectiveSystemPrompt 实现了明确的优先级梯度(源码注释是这么写的):
0. Override system prompt (loop mode, REPLACES all others)
1. Coordinator system prompt (when coordinator mode is active)
2. Agent system prompt (when mainThreadAgentDefinition is set)
- In proactive mode: APPENDED to default (agents add domain behavior on top)
- Otherwise: REPLACES default
3. Custom system prompt (via --system-prompt)
4. Default system prompt (the standard Claude Code prompt)
appendSystemPrompt 永远追加在最后(除了 override 层——override 是完全替换,不留余地)。
几条值得关注的设计决策:
- Override 是核弹级的:
loop mode和类似场景可以完全替换整个系统提示词。这意味着 harness 本身提供了”这条 session 不是常规用法”的逃生舱 - Proactive / Autonomous 模式特殊:agent prompt 追加到 default 而不是替换——因为 autonomous mode 的 default 已经很瘦(身份 + 记忆 + 环境 + proactive 段),agent 加的是 domain-specific 行为,和 default 的 autonomy 层不冲突
--system-promptCLI flag:用户可以从命令行覆盖默认 prompt。这是可观测的产品能力,不是隐藏接口
这意味着自研 agent 的一个设计问题:你的 prompt 系统有几层权威?如果只有一层(就是默认 prompt 加一些 config),那 “loop mode” 这类特殊场景就没地方落——你会被迫把特殊指令写进 default,污染常规 session。
静态 / 动态边界:Cache 的分界线
getSystemPrompt 的返回是按这个布局拼接的:
// constants/prompts.ts 第 560-577 行左右
return [
// --- Static content (cacheable) ---
getSimpleIntroSection(outputStyleConfig),
getSimpleSystemSection(),
getSimpleDoingTasksSection(), // unless output style overrides
getActionsSection(),
getUsingYourToolsSection(enabledTools),
getSimpleToneAndStyleSection(),
getOutputEfficiencySection(),
// === BOUNDARY MARKER - DO NOT MOVE OR REMOVE ===
...(shouldUseGlobalCacheScope() ? [SYSTEM_PROMPT_DYNAMIC_BOUNDARY] : []),
// --- Dynamic content (registry-managed) ---
...resolvedDynamicSections,
].filter(s => s !== null)
注释里甚至写了 “BOUNDARY MARKER - DO NOT MOVE OR REMOVE”。这个边界之所以重要:
- Marker 之前:静态——每个 session 都一样,跨 session 命中 prompt cache
- Marker 之后:动态——CLAUDE.md 可能变、MCP server 可能连接 / 断开、session 级指令每轮可能变
- Cache scope:Anthropic API 对 marker 之前的内容做 global cache scope,命中率远高于 per-session cache
这是 “late binding” 原则在 prompt 组装上的直接落地——不是风格偏好,是一条 cache 命中率的优化红线。
静态 7 段(源码函数名对应的内容,和你在 Claude Code session 里看到的一模一样):
| # | 函数 | 内容 |
|---|---|---|
| 1 | getSimpleIntroSection | ”You are Claude Code, Anthropic’s official CLI for Claude” + 策略 |
| 2 | getSimpleSystemSection | 工程规则(tool results、hooks、prompt injection detection) |
| 3 | getSimpleDoingTasksSection | 风格规则(什么时候简短、什么时候问) |
| 4 | getActionsSection | 不可逆操作的谨慎原则(git push / rm 等) |
| 5 | getUsingYourToolsSection | 工具使用规则(用专用工具优于 Bash、并行调用) |
| 6 | getSimpleToneAndStyleSection | 语气、不用 emoji、file_path:line_number 引用 |
| 7 | getOutputEfficiencySection | 输出效率规则 |
这 7 段几乎永不变化——只有 Anthropic 发版会动它们。所以它们是 cache prefix 的最稳定部分。
动态段:注册表式组织
Marker 之后是动态段。源码用了一个注册表模式(systemPromptSection + resolveSystemPromptSections),每段有名字、lazy producer、cache 策略:
const dynamicSections = [
systemPromptSection('session_guidance', () =>
getSessionSpecificGuidanceSection(enabledTools, skillToolCommands),
),
systemPromptSection('memory', () => loadMemoryPrompt()),
systemPromptSection('ant_model_override', () => getAntModelOverrideSection()),
systemPromptSection('env_info_simple', () =>
computeSimpleEnvInfo(model, additionalWorkingDirectories),
),
systemPromptSection('language', () => getLanguageSection(settings.language)),
systemPromptSection('output_style', () =>
getOutputStyleSection(outputStyleConfig),
),
// DANGEROUS: cache-busting. MCP servers connect/disconnect between turns.
DANGEROUS_uncachedSystemPromptSection(
'mcp_instructions',
() => isMcpInstructionsDeltaEnabled() ? null : getMcpInstructionsSection(mcpClients),
'MCP servers connect/disconnect between turns',
),
systemPromptSection('scratchpad', () => getScratchpadInstructions()),
systemPromptSection('frc', () => getFunctionResultClearingSection(model)),
systemPromptSection(
'summarize_tool_results',
() => SUMMARIZE_TOOL_RESULTS_SECTION,
),
// Ant-only A/B experiment — see below.
...(process.env.USER_TYPE === 'ant' ? [
systemPromptSection('numeric_length_anchors', () =>
'Length limits: keep text between tool calls to ≤25 words. Keep final responses to ≤100 words unless the task requires more detail.',
),
] : []),
// Feature-flagged.
...(feature('TOKEN_BUDGET') ? [ ... ] : []),
...(feature('KAIROS') || feature('KAIROS_BRIEF') ? [ ... ] : []),
]
DANGEROUS_uncachedSystemPromptSection ——显式承认”这段会打 cache”
有一个特殊工厂函数 DANGEROUS_uncachedSystemPromptSection,字面意思”危险的非缓存动态段”。目前只有 mcp_instructions 用它。注释解释了原因:
MCP servers connect/disconnect between turns
MCP server 可能在 turn 之间连接或断开,所以 MCP instruction 段不能被 cache。每次 turn 都重新计算。源码显式命名了这个设计选择的代价——任何改动这段的 PR 都会看到 “DANGEROUS” 提示。
给自研 agent 的启示:让 cache 破坏点显式化。代码里能看到 “这段会破 cache” 比隐性破坏好得多——后者会让团队悄悄贡献出破 cache 的段。
数据驱动的 prompt 工程:numeric_length_anchors
注意上面动态段清单里有一条 ant-only 的 section:
// Numeric length anchors — research shows ~1.2% output token reduction vs
// qualitative "be concise". Ant-only to measure quality impact first.
systemPromptSection(
'numeric_length_anchors',
() => 'Length limits: keep text between tool calls to ≤25 words. Keep final responses to ≤100 words unless the task requires more detail.',
)
“Length limits: keep text between tool calls to ≤25 words. Keep final responses to ≤100 words”——这句你在每个 Claude Code session 的 system prompt 里都能看到。源码注释明确记录:
research shows ~1.2% output token reduction vs qualitative “be concise”
1.2% 的输出 token 降幅,是具体数字对比泛泛”简短”措辞。ant-only 先上线测量质量影响,然后再考虑全量。
给自研 agent 的启示:prompt 里的每一条指令都应该有”为什么这么写”的答案。Claude Code 的内部文化显然是:
- 新 prompt 指令必须能指向数据或者实验结果
- 旧 prompt 指令要进入 A/B 对照才能判断还活着
- 量化措辞(“≤25 words”)经常比定性措辞(“be concise”)有更好的效果——先用数据验证,再推广
Git Status:snapshot in time
动态段里的 env info 来自 getSystemContext()(context.ts),它包含 git status。源码值得看:
// context.ts: getGitStatus()
const [branch, mainBranch, status, log, userName] = await Promise.all([
getBranch(),
getDefaultBranch(),
execFileNoThrow(gitExe(), ['--no-optional-locks', 'status', '--short'], ...),
execFileNoThrow(gitExe(), ['--no-optional-locks', 'log', '--oneline', '-n', '5'], ...),
execFileNoThrow(gitExe(), ['config', 'user.name'], ...),
])
const MAX_STATUS_CHARS = 2000
const truncatedStatus = status.length > MAX_STATUS_CHARS
? status.substring(0, MAX_STATUS_CHARS) +
'\n... (truncated because it exceeds 2k characters. If you need more information, run "git status" using BashTool)'
: status
几个细节:
- 5 条命令并发执行——
Promise.all,不是串行 --no-optional-locks——不阻塞其他 git 操作(很重要:session 启动时跑 git 不该锁死仓库)MAX_STATUS_CHARS = 2000截断——超限留占位:“run git status using BashTool”,告诉 agent 这是被截断过的getSystemContext用memoize——session 启动时算一次,对话过程中不更新(system prompt 里也明写了 “this status is a snapshot in time”)
跳过 git status 的条件:
CLAUDE_CODE_REMOTE环境变量——云端 resume 时没必要再跑一次shouldIncludeGitInstructions()返回 false
CLAUDE.md 加载:6 种 memory 类型
源码里 memory 类型比 3 种多——是 6 种(utils/memory/types.ts):
export const MEMORY_TYPE_VALUES = [
'User', // ~/.claude/CLAUDE.md
'Project', // <repo>/CLAUDE.md(git 跟踪)
'Local', // CLAUDE.local.md(用户的**私有** project instructions,不进 git)
'Managed', // policy/企业管理的配置
'AutoMem', // auto-memory,跨对话持久化
...(feature('TEAMMEM') ? (['TeamMem'] as const) : []), // 团队共享记忆
] as const
每种类型在 system prompt 里的描述后缀(源码 getClaudeMds 第 1169-1177 行):
| 类型 | 描述后缀 |
|---|---|
| Project | ' (project instructions, checked into the codebase)' |
| Local | " (user's private project instructions, not checked in)" |
| TeamMem | ' (shared team memory, synced across the organization)' |
| AutoMem | " (user's auto-memory, persists across conversations)" |
| User | " (user's private global instructions for all projects)" |
加载顺序
getMemoryFiles() (utils/claudemd.ts 第 790 行起) 的顺序:
- Managed(始终优先加载,policy 级)
- Managed
.claude/rules/*.md - User(如果
userSettings启用) - User
~/.claude/rules/*.md - 从文件系统根向 CWD 向下走,每一层检查:
CLAUDE.md(Project).claude/CLAUDE.md(Project).claude/rules/*.md(Project)CLAUDE.local.md(Local)
Nested worktree 规则
源码里有一个看起来别扭但很合理的分支(第 868-884 行):
// When running from a git worktree nested inside its main repo, the upward
// walk passes through both the worktree root AND the main repo root. Both
// contain checked-in files like CLAUDE.md... Skip Project-type files from
// directories above the worktree but within the main repo.
翻译:如果你从一个嵌套在主仓库里的 worktree 里跑 Claude Code,向上走会碰到主仓库根目录,那里也有 CLAUDE.md。不加特殊处理就会加载两次同样的规则。源码显式 skip。
Issue 引用:github.com/anthropics/claude-code/issues/29599——这是真实 bug 修复留下的补丁,不是假想问题。
MEMORY_INSTRUCTION_PROMPT:override 宣言
CLAUDE.md 内容前面会被加一句字面量指令(utils/claudemd.ts 第 89 行):
const MEMORY_INSTRUCTION_PROMPT =
'Codebase and user instructions are shown below. Be sure to adhere to these instructions. ' +
'IMPORTANT: These instructions OVERRIDE any default behavior and you MUST follow them exactly as written.'
这段文字你在每个 Claude Code session 里都能看到。设计意图是 override——用户 / 项目方的指令权威高于默认 prompt。上面讲的 5 层优先级梯度在这里和 CLAUDE.md 层再配合一次:
默认规则 < CLAUDE.md / memory < session guidance < 用户当前消息
后面覆盖前面。层级越清晰,agent 越不会拧巴。
单个 memory 文件的大小上限
// utils/claudemd.ts 第 92 行
export const MAX_MEMORY_CHARACTER_COUNT = 40000
单个 CLAUDE.md 超过 40k 字符会被截断。这是一个具体的工程约束——有 1000 人的 repo 里 CLAUDE.md 容易膨胀,40k 字符约 6-8k tokens。
Env 杀手
Claude Code 暴露了几个环境变量 / CLI flag 可以绕过整个 memory 加载:
| 变量 / flag | 效果 |
|---|---|
CLAUDE_CODE_DISABLE_CLAUDE_MDS | 完全禁用 CLAUDE.md 加载(硬关) |
--bare CLI flag | 跳过自动发现(CWD 向上走),但仍尊重 --add-dir 显式指定 |
CLAUDE_CODE_REMOTE | 跳过 git status(resume 场景省开销) |
CLAUDE_CODE_SIMPLE | 使用极简 system prompt(仅身份 + CWD + date) |
源码注释对 --bare 语义的阐释很值得学习:--bare means "skip what I didn't ask for", not "ignore what I asked for"——显式 --add-dir 仍然生效。这是一个细致的语义选择:参数设计应该传达用户意图,而不是机械执行。
Cache 破坏命令(ant-only)
在 context.ts 里还有一段调试向机制:
// ant-only, ephemeral debugging state
let systemPromptInjection: string | null = null
export function setSystemPromptInjection(value: string | null): void {
systemPromptInjection = value
getUserContext.cache.clear?.()
getSystemContext.cache.clear?.()
}
ant-only 的 BREAK_CACHE_COMMAND 特性启用时,调用 setSystemPromptInjection("任何文本") 会把 [CACHE_BREAKER: 任何文本] 插进 system context——故意打破 prompt cache。用于调试:看看 cache miss 的影响。
给自研 agent 的启示:调试时主动打破 cache 的能力应该内置。生产运维里经常要问”如果这个 prompt 今天冷启动会怎样”,有个一键 cache-bust 的开关比硬改代码强。
工具 schema + error 文本:也是 prompt
(这段内容在之前版本已经覆盖得很到位,这里简要回顾并补源码证据)
工具描述本身是 system prompt 的一部分——通过 getUsingYourToolsSection(enabledTools) 注入。每个工具的描述 / schema / error text 都经过了 Anthropic 的 prompt 工程迭代。
例如 Edit 工具的描述:
- “You must use your
Readtool at least once in the conversation before editing”——教 agent 先读再改 - “This tool will error if you attempt an edit without reading the file”——error text 本身也在指导下一步行为
- “The edit will FAIL if
old_stringis not unique in the file”——预告失败条件
给自研 agent 的启示:error text 写得好,agent 遇错能自我纠偏;写得差(“Invalid argument”),agent 死循环重试。
完整装配顺序(真实 session 观察)
图里的 10 层是用户可见的 session level 分层。源码级看是静态 7 段 + 动态 13 段 + context getters 注入,总数更多——图里做了聚合以便读者理解。
给自研 agent 的要点
- 系统提示词是装配件,不是字符串。分静态段(cache 友好)+ 动态段(每轮可能变),用显式边界标记分隔——这是 cache 命中率优化的直接落地
- 用注册表组织动态段。每段名字 + lazy producer + 是否可 cache。乱塞字符串最终必然有人搞错
- 让 cache 破坏显式化。Claude Code 的
DANGEROUS_uncachedSystemPromptSection命名是故意吓人的——让破 cache 这件事可见可评审 - 多层优先级梯度(override > coordinator > agent > custom > default)+ 追加机制(
appendSystemPrompt)。单层权威在特殊场景一定会爆 - CLAUDE.md 机制:给项目 / 用户 / 企业 / auto-memory 各自的 prompt 接入点,前置显式的 “OVERRIDE” 指令,否则每个项目都要先和默认规则博弈
- Env 杀手:
CLAUDE_CODE_DISABLE_*之类的环境变量不是奢侈品,是紧急刹车。一定要有一键关的办法 - Prompt 指令必须有数据支撑。
numeric_length_anchors的 1.2% 遥测是范例——靠定性评判堆 prompt 规则,长期必失控 - Tool schema + error text 都是 prompt 工程任务,不是顺手写的 schema doc——这是 agent 自我纠偏能力的关键输入
- Memoize session-stable 内容。
getSystemContext/getUserContext都 memoize 了——session 内不变的东西算一次就够
延伸阅读
- Claude Code 源码:
constants/prompts.ts、utils/systemPrompt.ts、context.ts、utils/claudemd.ts、utils/memory/types.ts - 提示词设计 (Prompt Design)——prompt 分层的一般理论
- 上下文压缩——这些层在压缩时怎么处理
- 记忆系统——6 种 memory 类型的详细讲解