这一章直接回答一个核心问题:
Claude Code 的 prompt 不是一段固定字符串,而是一套分层拼装、可缓存、可覆盖、可观测的 prompt 管理系统。
本章主要分析四件事:
- 默认 system prompt 到底从哪里来
- 自定义 prompt、agent prompt、append prompt 是怎么覆盖和叠加的
- 哪些上下文其实不在
prompts.ts,而是运行时单独注入 - compact、session memory、memory extraction 这些“专项 prompt”是如何和主 prompt 系统并存的
本章主要依据这些实现:
src/constants/prompts.tssrc/utils/systemPrompt.tssrc/screens/REPL.tsxsrc/utils/queryContext.tssrc/context.tssrc/constants/systemPromptSections.tssrc/main.tsxsrc/services/compact/prompt.tssrc/services/SessionMemory/prompts.tssrc/services/extractMemories/prompts.tssrc/services/api/dumpPrompts.ts
先给结论:
这个项目没有把 prompt 管理做成“一个 system prompt 文件 + 若干 if else”,而是拆成了 6 层:
1. 默认主系统提示
src/constants/prompts.ts
2. 有效 system prompt 组装器
src/utils/systemPrompt.ts
- override
- coordinator
- agent
- custom
- append
3. 运行时上下文注入
src/context.ts
- CLAUDE.md
- currentDate
- git status
- cache breaker
4. 启动期附加指令入口
src/main.tsx
- --system-prompt
- --append-system-prompt
- systemPromptFile / appendSystemPromptFile
- proactive / chrome / teammate addendum
5. Prompt 缓存与失效管理
src/constants/systemPromptSections.ts
- section cache
- dynamic boundary
- cache break
6. 专项 prompt 家族
compact / session memory / extract memories / hooks / insights 等
所以这里真正管理的不是“prompt 文本”,而是:
- 哪些 prompt 属于主循环
- 哪些 prompt 属于子任务
- 哪些内容要长期缓存
- 哪些内容必须逐轮重算
- 哪些内容允许外部覆盖
- 哪些内容可以被导出和审计
相关实现:
如果只看文件名,很容易以为 src/constants/prompts.ts 就是“完整 prompt”。
实际上不是。
真正送进模型前,大致流程如下:
启动参数 / mode / agent / mcp / settings
|
v
getSystemPrompt() 生成默认 system prompt 数组
|
v
buildEffectiveSystemPrompt() 处理优先级覆盖
|
+---- userContext
| - CLAUDE.md
| - currentDate
|
+---- systemContext
- git status
- cacheBreaker
|
v
Query / REPL / Compact / Subagent 调用 API
也就是说,这个项目里的 “prompt” 至少分成三类:
system prompt定义 agent 的身份、规则、工具使用方式和会话级策略。userContext / systemContext属于额外上下文,不直接写死在prompts.ts主模板里。task-specific prompts专门用于 compact、memory extraction、session memory 更新等后台任务。
这一拆分很关键,因为它说明 Claude Code 不是把所有规则都塞进一个超长 system prompt,而是把常驻规则、会话上下文、专项任务说明分开治理。
相关实现:
getSystemPrompt() 的签名是:
export async function getSystemPrompt(
tools: Tools,
model: string,
additionalWorkingDirectories?: string[],
mcpClients?: MCPServerConnection[],
): Promise<string[]>这件事本身就说明设计意图:
- system prompt 被拆成多个 section
- 每个 section 可以单独缓存、单独插拔、单独统计 token
- 后续还能在 section 级别做 cache boundary 和动态失效
getSystemPrompt() 最重要的返回结构如下:
return [
getSimpleIntroSection(outputStyleConfig),
getSimpleSystemSection(),
outputStyleConfig === null ||
outputStyleConfig.keepCodingInstructions === true
? getSimpleDoingTasksSection()
: null,
getActionsSection(),
getUsingYourToolsSection(enabledTools),
getSimpleToneAndStyleSection(),
getOutputEfficiencySection(),
...(shouldUseGlobalCacheScope() ? [SYSTEM_PROMPT_DYNAMIC_BOUNDARY] : []),
...resolvedDynamicSections,
].filter(s => s !== null)这段代码非常重要,因为它揭示了主 prompt 的基本工程策略:
- 前半段是静态主干
- 中间插一个
SYSTEM_PROMPT_DYNAMIC_BOUNDARY - 后半段是动态 section
换句话说,Claude Code 不是只在“内容层面”写 prompt,而是在“缓存层面”设计 prompt。
例如最开头的身份段 getSimpleIntroSection() 会返回:
return `
You are an interactive agent that helps users ...
${CYBER_RISK_INSTRUCTION}
IMPORTANT: You must NEVER generate or guess URLs for the user unless ...
`getSimpleSystemSection() 又会追加一组基础规则,例如:
- 输出给用户的文字如何呈现
- 工具调用被拒绝后不能原样重试
- 外部 tool result 可能存在 prompt injection
- hooks 反馈要视作用户输入
- 上下文会被自动压缩
getSimpleDoingTasksSection() 则是更偏 coding agent 的工作规则,例如:
- 不要过度设计
- 不要额外加注释和类型
- 读文件后再改代码
- 遇到失败先诊断再换策略
- 避免引入安全漏洞
也就是说,默认 system prompt 不是一个短小的身份声明,而是一个非常完整的“执行政策包”。
resolvedDynamicSections 来自这几个 section:
const dynamicSections = [
systemPromptSection('session_guidance', ...),
systemPromptSection('memory', ...),
systemPromptSection('ant_model_override', ...),
systemPromptSection('env_info_simple', ...),
systemPromptSection('language', ...),
systemPromptSection('output_style', ...),
DANGEROUS_uncachedSystemPromptSection('mcp_instructions', ...),
systemPromptSection('scratchpad', ...),
systemPromptSection('frc', ...),
systemPromptSection('summarize_tool_results', ...),
]这些 section 和静态主干不同,它们更依赖运行态:
- 当前启用的 tools
- 当前 settings 里的语言偏好
- 当前模型
- 当前 mcp server 的 instructions
- 当前 memory / scratchpad / output style
这说明 prompt 系统不是“读取模板并替换变量”,而是“运行时装配一组 section”。
相关实现:
真正决定“最后发给模型的 system prompt 长什么样”的,不是 getSystemPrompt(),而是 buildEffectiveSystemPrompt()。
它的注释已经把优先级写得很清楚:
/**
* 0. Override system prompt
* 1. Coordinator system prompt
* 2. Agent system prompt
* 3. Custom system prompt
* 4. Default system prompt
* Plus appendSystemPrompt is always added at the end
*/可以把它翻译成下面这段伪代码:
if overrideSystemPrompt:
final = [overrideSystemPrompt]
else if coordinator mode:
final = [coordinatorPrompt] + [appendPrompt?]
else:
base =
agentPrompt
or customSystemPrompt
or defaultSystemPrompt
if proactive mode and agentPrompt exists:
final = defaultSystemPrompt + ["# Custom Agent Instructions" + agentPrompt]
else:
final = [base]
if appendSystemPrompt:
final += [appendSystemPrompt]
这里最值得注意的是两点:
customSystemPrompt不会 append 到默认 prompt 后面,而是直接替代默认 prompt。appendSystemPrompt不管前面是什么来源,基本都会被挂到最后。
这两条规则决定了 Claude Code 对“覆写”和“加尾注”的工程区分是很严格的。
下面是核心逻辑的原始函数片段:
export function buildEffectiveSystemPrompt({
mainThreadAgentDefinition,
toolUseContext,
customSystemPrompt,
defaultSystemPrompt,
appendSystemPrompt,
overrideSystemPrompt,
}): SystemPrompt {
if (overrideSystemPrompt) {
return asSystemPrompt([overrideSystemPrompt])
}
if (feature('COORDINATOR_MODE') &&
isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE) &&
!mainThreadAgentDefinition) {
return asSystemPrompt([
getCoordinatorSystemPrompt(),
...(appendSystemPrompt ? [appendSystemPrompt] : []),
])
}
const agentSystemPrompt = mainThreadAgentDefinition
? mainThreadAgentDefinition.getSystemPrompt(...)
: undefined
if (agentSystemPrompt && proactiveActive) {
return asSystemPrompt([
...defaultSystemPrompt,
`\n# Custom Agent Instructions\n${agentSystemPrompt}`,
...(appendSystemPrompt ? [appendSystemPrompt] : []),
])
}
return asSystemPrompt([
...(agentSystemPrompt
? [agentSystemPrompt]
: customSystemPrompt
? [customSystemPrompt]
: defaultSystemPrompt),
...(appendSystemPrompt ? [appendSystemPrompt] : []),
])
}这说明 agent prompt 在普通模式下甚至会取代默认 prompt,而不是“在默认 prompt 上加一点 agent 设定”。这是一种很强的角色切换。
相关实现:
Prompt 管理里一个非常容易被忽略的点是:
有些内容不是 system prompt section,而是单独的 context。
getUserContext() 返回的内容主要有两个:
claudeMdcurrentDate
源码逻辑大致如下:
const claudeMd = shouldDisableClaudeMd
? null
: getClaudeMds(filterInjectedMemoryFiles(await getMemoryFiles()))
return {
...(claudeMd && { claudeMd }),
currentDate: `Today's date is ${getLocalISODate()}.`,
}这说明:
CLAUDE.md并不是prompts.ts里写死的一段模板- 它是运行时扫描、读取、拼接后作为 user context 注入
- 日期也不是主 prompt 文本的一部分,而是独立字段
因此,研究 Claude Code 的 prompt 不能只看 constants/prompts.ts,还必须把 context.ts 算进去。
getSystemContext() 主要会追加:
- git status 快照
- cache breaker 注入项
核心逻辑:
return {
...(gitStatus && { gitStatus }),
...(feature('BREAK_CACHE_COMMAND') && injection
? { cacheBreaker: `[CACHE_BREAKER: ${injection}]` }
: {}),
}这说明 system context 的职责不是“身份描述”,而是提供本轮推理必须知道的系统状态。
尤其是 gitStatus 这项,非常像“给 coding agent 的环境前情摘要”。
相关实现:
这一部分决定了 Claude Code 为什么不只是“内置 prompt”,而是“可外部编排的 prompt runtime”。
main.tsx 会读取:
--system-prompt--system-prompt-file--append-system-prompt--append-system-prompt-file
例如:
let appendSystemPrompt = options.appendSystemPrompt;
if (options.appendSystemPromptFile) {
if (options.appendSystemPrompt) {
process.stderr.write(chalk.red('Error: Cannot use both ...'));
process.exit(1);
}
const filePath = resolve(options.appendSystemPromptFile);
appendSystemPrompt = readFileSync(filePath, 'utf8');
}这意味着用户或上层产品可以:
- 完全替换默认 system prompt
- 或只在默认 prompt 尾部追加一层策略
这是两种完全不同的控制力度。
除了 CLI 参数,系统还会在启动过程中继续往 appendSystemPrompt 里塞内容:
- tmux teammate addendum
- Claude in Chrome system prompt
- Claude in Chrome skill hint
- proactive mode prompt
- assistant addendum
- teammate custom agent instructions
例如 proactive mode 会直接追加:
const proactivePrompt = `
# Proactive Mode
You are in proactive mode. Take initiative — explore, act, and make progress without waiting for instructions.
Start by briefly greeting the user.
...`
appendSystemPrompt = appendSystemPrompt
? `${appendSystemPrompt}\n\n${proactivePrompt}`
: proactivePrompt所以 appendSystemPrompt 在工程上并不是“用户偶尔手动加一句”,而是一个正式的追加指令总线。
相关实现:
这一套实现最有工程味的地方,就是它把 prompt 当成缓存对象治理。
export function systemPromptSection(
name: string,
compute: ComputeFn,
): SystemPromptSection {
return { name, compute, cacheBreak: false }
}export function DANGEROUS_uncachedSystemPromptSection(
name: string,
compute: ComputeFn,
_reason: string,
): SystemPromptSection {
return { name, compute, cacheBreak: true }
}这类接口设计的意义很直接:
- 默认 section 都应该缓存
- 如果你要让某段 prompt 每轮重算,就必须显式声明“这是危险操作”
这是一种非常明确的 prompt cache discipline。
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
}),
)
}也就是说,这里缓存的是section 结果,不是整个大 prompt 字符串。
prompts.ts 里专门定义了:
export const SYSTEM_PROMPT_DYNAMIC_BOUNDARY =
'__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__'它的作用不是给模型看的,而是给缓存系统看的:
- boundary 之前尽可能保持稳定
- boundary 之后允许更多 session 级变化
这说明 Claude Code 已经把 prompt prefix cache 当成一级工程问题处理了。
clearSystemPromptSections() 会在 /clear、/compact、worktree 切换等路径被调用。
这表示 prompt cache 不是永久缓存,而是和“会话生命周期事件”绑定:
- clear conversation
- compact conversation
- enter / exit worktree
- resume / restore session
相关实现:
在 REPL 中,可以看到这条关键链路:
const [defaultSystemPrompt, userContext, systemContext] = await Promise.all([
getSystemPrompt(...),
getUserContext(),
getSystemContext(),
])
const systemPrompt = buildEffectiveSystemPrompt({
mainThreadAgentDefinition,
toolUseContext,
customSystemPrompt,
defaultSystemPrompt,
appendSystemPrompt
})
toolUseContext.renderedSystemPrompt = systemPrompt;这里很关键:
- 默认 prompt 和 user/system context 是并行拉取的
- 最终 system prompt 会被挂到
toolUseContext.renderedSystemPrompt - 这个字段后面还能被 fork / subagent / resume 逻辑复用
也就是说,prompt 不只是“发送前临时拼一下”,而是 runtime 里会被持久引用的一份状态。
/compact 并不是拿当前界面的 prompt 文本直接去总结,而是重新计算:
const defaultSysPrompt = await getSystemPrompt(...)
const systemPrompt = buildEffectiveSystemPrompt({
mainThreadAgentDefinition: undefined,
toolUseContext: context,
customSystemPrompt: context.options.customSystemPrompt,
defaultSystemPrompt: defaultSysPrompt,
appendSystemPrompt: context.options.appendSystemPrompt,
})这说明 compact 本身也依赖 prompt 系统,而且它要拿的是一份适合共享 cache key 的 prompt 前缀。
src/utils/queryContext.ts 还提供了 fetchSystemPromptParts() 和 buildSideQuestionFallbackParams(),说明:
- prompt 构造逻辑被抽到共享 helper
- 即使是 side question / print / SDK resume,也能尽量重建出与主会话一致的 prompt 前缀
所以这里的 prompt 管理已经不是 UI 层逻辑,而是 query infrastructure 的一部分。
相关实现:
src/services/compact/prompt.tssrc/services/SessionMemory/prompts.tssrc/services/extractMemories/prompts.ts
主会话 prompt 之外,这个项目还有很多“专项 prompt”。
compact prompt 一上来就先下死命令:
const NO_TOOLS_PREAMBLE = `CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.
- Do NOT use Read, Bash, Grep, Glob, Edit, Write, or ANY other tool.
- You already have all the context you need in the conversation above.
- Tool calls will be REJECTED and will waste your only turn — you will fail the task.
- Your entire response must be plain text: an <analysis> block followed by a <summary> block.
`这类 prompt 和主 system prompt 的风格完全不同。
它不是定义长期身份,而是在单次任务里强力约束输出协议:
- 禁止工具
- 限制格式
- 限制轮次
- 强化总结结构
也就是说,Claude Code 里“prompt engineering”并不是只服务主 agent,而是服务后台操作协议。
SessionMemory/prompts.ts 的默认 update prompt 也很典型:
Your ONLY task is to use the Edit tool to update the notes file, then stop.
Do not call any other tools.
...
- NEVER modify, delete, or add section headers
- NEVER modify or delete the italic _section description_ lines
- ONLY update the actual content that appears BELOW ...这说明 session memory 更新不是“自由总结”,而是“带模板约束的结构化文档维护任务”。
extractMemories/prompts.ts 也不是简单地说“帮我提取记忆”,而是明确限制:
- 可用工具只有 Read / Grep / Glob / 只读 Bash / Edit / Write
- 不允许 MCP、Agent、可写 Bash
- 要先并行读,再并行写
- 只能使用最近若干消息
- 禁止再去读源码验证
这意味着后台 memory agent 的行为并不是模型自由发挥,而是被 prompt 写成了一套轻量协议。
相关实现:
src/services/api/dumpPrompts.tssrc/commands/context/context-noninteractive.tssrc/utils/analyzeContext.ts
createDumpPromptsFetch() 会拦截请求,把:
- init data
- system update
- user messages
- responses
写到:
~/.claude/dump-prompts/<session-or-agent-id>.jsonl
这说明 prompt 不只是内部隐式状态,还能被调试、复盘、审计。
analyzeContext.ts 会把 effective system prompt 拆成 named entries:
const namedEntries = [
...effectiveSystemPrompt
.filter(content => content.length > 0 &&
content !== SYSTEM_PROMPT_DYNAMIC_BOUNDARY)
.map(content => ({ name: extractSectionName(content), content })),
...Object.entries(systemContext)
.filter(([, content]) => content.length > 0)
.map(([name, content]) => ({ name, content })),
]然后逐段算 token。
这说明 Claude Code 的 prompt 系统有一个很成熟的“运营侧视角”:
- 不是只关心 prompt 对不对
- 还关心 prompt 吃了多少 token
- 哪一段最贵
- 哪些段应该继续缓存
-
可组合 默认 prompt、agent prompt、append prompt、userContext、systemContext、专项 prompt 可以并行演进。
-
可缓存 section 化设计 + boundary 让 prompt prefix cache 有工程抓手。
-
可扩展 新功能不必改一个超长字符串,只需要新增 section 或 addendum。
-
可调试 通过
dump-prompts、/context、token 分析,可以看到 prompt 的真实成本。 -
可协议化 compact、memory update、memory extraction 都被做成了单任务协议,不容易跑偏。
-
理解门槛高 只读
prompts.ts会得出错误结论,必须把systemPrompt.ts、context.ts、main.tsx一起看。 -
覆盖关系复杂
override、custom、agent、append、proactive、coordinator的组合已经比较绕。 -
调试难点前移 prompt 问题不一定是模板问题,也可能是 context 注入、section cache、append addendum 或 mode 切换问题。
-
行为依赖 mode 同一个系统在 REPL、compact、subagent、SDK side-question 下,实际拿到的 prompt 形态并不完全一样。
如果把 Claude Code 的 prompt 管理只理解成“prompts.ts 里的一大段 system prompt”,那会漏掉最关键的工程部分。
更准确的说法是:
src/constants/prompts.ts负责定义默认主系统提示的 section 集合src/utils/systemPrompt.ts负责做最终优先级合成src/context.ts负责注入运行态上下文src/main.tsx负责接入 CLI 和 feature addendumsrc/constants/systemPromptSections.ts负责缓存和失效- 多个
services/*/prompts.ts负责后台专项任务的 prompt 协议
所以,Claude Code 的 prompt 不是一个文件,而是一套 runtime。
这也是为什么它能同时做到:
- 主会话可持续运行
- 子任务可切换 prompt 协议
- 上下文成本可控
- prompt 可导出、可缓存、可审计
从工程角度看,这一套实现已经不是“写 prompt”,而是在做 prompt orchestration。