Prompt 系统
这份文档回答一个更具体的问题:
这个仓库里的 “prompt 系统” 不是一段固定字符串,而是怎样被组装、缓存、注入、压缩,并最终发给模型的?
先说结论:
这里的 prompt 系统本质上是一个多层拼装管线,不只是 system prompt。
它至少包含 6 类内容:
- 默认 system prompt
- 运行时覆写/追加 prompt
- user context / system context
- 工具 schema 里的 tool prompt
- slash command / skill 扩展出来的 prompt
- compact / summary 等特殊场景 prompt
可以把它理解成:
用户输入
-> processUserInput / slash command 展开
-> getSystemPrompt() 生成默认 system prompt 片段
-> buildEffectiveSystemPrompt() 或等价逻辑做最终合并
-> prependUserContext() / appendSystemContext()
-> toolToAPISchema() 注入工具 prompt
-> buildSystemPromptBlocks() 切成带 cache 语义的 API blocks
-> query() 发给模型
1. Prompt 系统由哪些层组成
1.1 默认 system prompt
默认 system prompt 的核心在 src/constants/prompts.ts 的 getSystemPrompt()。
它不是返回一个大字符串,而是返回 string[],每个元素都是一个 section。这样做有两个直接收益:
- 方便按 section 缓存和增删
- 方便在 API 层按 block 切分,做 prompt cache
这层负责放入模型最基础的行为约束,例如:
- 身份与任务定位
- 工具使用原则
- 安全边界
- 输出风格
- 环境信息
- MCP 指令
- memory / scratchpad / proactive 等动态能力说明
1.2 运行时有效 system prompt
默认 prompt 还不是最终发给模型的 prompt。
真正的“有效 prompt”在两处形成:
- 交互主线程等路径:
src/utils/systemPrompt.ts的buildEffectiveSystemPrompt() - SDK / headless 路径:
src/QueryEngine.ts里直接按同样思路拼装
这一步处理:
overrideSystemPrompt- coordinator prompt
- agent prompt
customSystemPromptappendSystemPrompt
也就是说,仓库里没有唯一一个 prompt 文件,真正生效的是“默认 prompt + 运行时策略”的结果。
1.3 user context / system context
这部分在 src/utils/queryContext.ts、src/utils/api.ts 和 src/query.ts。
系统把 prompt 分成两种不同的上下文载体:
userContext:通过prependUserContext()伪装成一个 meta user message,包在<system-reminder>里插到消息前面systemContext:通过appendSystemContext()直接追加到 system prompt 尾部
也就是说,仓库不把所有上下文都塞进 system prompt,而是区分:
- 哪些更像“用户可参考背景”
- 哪些更像“系统级环境补充”
1.4 工具 prompt
工具本身也是 prompt 系统的一部分。
在 src/Tool.ts 里,每个 Tool 不只有执行逻辑,还有:
prompt()description()- 输入 schema
而 src/utils/api.ts 的 toolToAPISchema() 会调用 tool.prompt(),把它变成 API 里的工具描述。
这意味着模型看到的不只是 “有一个工具叫 BashTool”,还会看到这个工具的使用说明,而这份说明也是 prompt。
1.5 slash command / skill prompt
仓库里很多 command 不是本地命令,而是 type: 'prompt' 的 prompt command。
相关逻辑在:
src/commands.tssrc/utils/processUserInput/processSlashCommand.tsx
这类命令在执行时会调用 getPromptForCommand(),把 /commit、/brief、skill 等展开成一段新的模型输入,并以 meta message 的形式插回消息流。
所以 skill 系统本质上也是 prompt 扩展系统。
1.6 compact prompt
当上下文过长时,系统会用另一套 prompt 要求模型“总结对话”。
这部分在:
src/services/compact/prompt.tssrc/services/compact/compact.ts
它不是沿用主对话 prompt,而是专门构造一段“只输出文本 summary、禁止调工具”的 compact prompt。
所以 compact 不是对消息做字符串裁剪,而是让模型在另一条 prompt 轨道上生产压缩摘要。
2. 默认 system prompt 是怎么生成的
2.1 getSystemPrompt() 返回的是分段结构
src/constants/prompts.ts 的 getSystemPrompt() 会按 section 组装 prompt,典型包含:
- intro
- system
- doing tasks
- actions
- using your tools
- tone and style
- output efficiency
- dynamic sections
这里最关键的设计不是内容本身,而是 “静态 section + 动态 section” 的拆分。
2.2 静态段和动态段被显式分界
文件里定义了:
export const SYSTEM_PROMPT_DYNAMIC_BOUNDARY =
'__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__'
这个 marker 很重要。
它告诉后面的 API 层:
- marker 之前的内容可以作为更稳定的静态前缀
- marker 之后的内容是动态 section,不应和静态前缀共享同一层 cache 语义
也就是说,这个仓库从设计上就把 “prompt 内容” 和 “prompt cache 命中率” 绑在一起考虑了。
2.3 动态 section 有自己的缓存层
src/constants/systemPromptSections.ts 提供了:
systemPromptSection()DANGEROUS_uncachedSystemPromptSection()resolveSystemPromptSections()
普通 dynamic section 会在 session 内缓存,直到 /clear 或 /compact 清空。
这意味着很多“动态 section”其实不是每轮重算,而是“本 session 稳定”。
默认 dynamic section 里比较典型的有:
- session-specific guidance
- memory
- ant model override
- env info
- language
- output style
- scratchpad
- function result clearing
2.4 只有少数 section 被允许显式破坏 cache
最典型的是 MCP instructions。
在 getSystemPrompt() 里,它被放进 DANGEROUS_uncachedSystemPromptSection(),原因写得很直接:MCP server 可能在 turn 之间连接/断开。
这说明系统对 cache 稳定性是强约束:
- 默认 section 一律缓存
- 只有确实会在 turn 间变化、并且必须被模型看到的内容,才允许 cache-break
2.5 proactive 模式走的是另一条 prompt 分支
如果启用了 proactive / kairos,getSystemPrompt() 会直接走一条更短、更自治的 prompt 路径,而不是复用普通交互态的全部 sections。
这说明 prompt 系统不是“所有模式共享一个模板”,而是按运行模式分叉。
3. 最终有效 prompt 的优先级
src/utils/systemPrompt.ts 的 buildEffectiveSystemPrompt() 已经把优先级写得很清楚:
overrideSystemPrompt- coordinator system prompt
- agent system prompt
customSystemPrompt- default system prompt
然后:
appendSystemPrompt一般总是追加在最后- 但如果用了
overrideSystemPrompt,它直接替换全部内容
这里有两个细节很关键。
3.1 agent prompt 有时替换默认 prompt,有时追加
普通情况下,agent prompt 会替换默认 prompt。
但在 proactive 模式下,agent prompt 会被作为:
# Custom Agent Instructions
...
追加到默认 prompt 后面。
也就是说,agent 在这个仓库里不是固定的“独立人格 prompt”,而是会随模式切换“替换式”或“增量式”合并。
3.2 SDK 路径会跳过部分默认上下文
src/utils/queryContext.ts 的 fetchSystemPromptParts() 有一个重要分支:
- 如果设置了
customSystemPrompt- 跳过
getSystemPrompt() - 跳过
getSystemContext()
- 跳过
src/QueryEngine.ts 还会在这种情况下,按需额外注入 memoryMechanicsPrompt。
这意味着 customSystemPrompt 在 SDK/headless 模式里不只是“覆盖默认文案”,而是会改变整条上下文装配路径。
4. Prompt 是怎样进入 query 主循环的
4.1 fetchSystemPromptParts() 先取三块前缀内容
src/utils/queryContext.ts 会先收集:
defaultSystemPromptuserContextsystemContext
这三者一起构成 API cache-key 前缀里的核心部分。
4.2 processUserInput() 会先把 slash command 展开
src/utils/processUserInput/processUserInput.ts 和 processSlashCommand.tsx 会先处理:
- 普通文本输入
- slash command
- skill
- attachment
- hook 注入
其中 prompt command 会被展开成新的 meta user message。
所以模型真正看到的 “用户输入”,很多时候已经不是原始键入文本,而是“扩展过的 prompt 流”。
4.3 query.ts 把三类上下文放到不同位置
src/query.ts 里有两行最关键:
const fullSystemPrompt = asSystemPrompt(
appendSystemContext(systemPrompt, systemContext),
)
messages: prependUserContext(messagesForQuery, userContext)
也就是说:
systemContext被拼到 system promptuserContext被伪装成一个前置 user message
这是 prompt 系统最重要的结构化分层之一。
4.4 发送 API 前还会再加工一遍 system prompt
到 src/services/api/claude.ts,system prompt 还会被继续处理:
- 加 attribution header
- 加 CLI sysprompt prefix
- 按 cache 规则拆 block
- 生成 Anthropic API 需要的
TextBlockParam[]
真正发给模型的并不是 getSystemPrompt() 的原始返回值,而是经过 API 层重排后的 block 集合。
5. 工具 prompt 系统:模型如何理解工具
5.1 工具 schema 不是静态 JSON,而是运行时生成
src/utils/api.ts 的 toolToAPISchema() 会把 Tool 转成 API schema。
这个过程至少会用到:
tool.nametool.prompt()tool.inputSchema/inputJSONSchemastricteager_input_streamingdefer_loading
因此,工具系统本身就是模型 prompt 的一部分,而不是仅仅运行时可调用能力。
5.2 tool.prompt() 结果会被 session 级缓存
toolToAPISchema() 会把 base schema 缓存在 src/utils/toolSchemaCache.ts。
缓存 key 通常是:
tool.name- 某些场景下再加
inputJSONSchema
原因很直接:工具 schema 在 system prompt 前面,一旦字节变化,就会打碎整个 prompt cache 前缀。
所以这里缓存的不是“为了省一点 CPU”,而是为了稳定 prompt bytes。
5.3 工具列表本身也为了 cache 稳定而排序
src/utils/toolPool.ts 的 mergeAndFilterTools() 明确把工具分成:
- built-in contiguous prefix
- MCP suffix
再按名字排序。
这不是美观问题,而是为了尽量避免:
- 某个 MCP 工具晚连接
- 某个工具被去重/过滤
时导致整段工具 schema 顺序抖动。
5.4 ToolSearch 本质上是“按需暴露 prompt”
当 tool search 启用时,src/services/api/claude.ts 会决定哪些工具带 defer_loading,哪些真正进本轮 schema。
所以这个系统不是一次性把所有工具 prompt 全塞给模型,而是支持:
- 先暴露一部分
- 其余工具延迟发现
- 被发现后再进入下一轮 prompt
从 prompt 系统视角看,这其实是“工具 prompt 的按需分页”。
6. Prompt cache 是这个系统的第一原则之一
这个仓库里很多设计看起来像工程细节,实质上都在服务 prompt cache。
6.1 system prompt 被拆成带 scope 的 block
src/utils/api.ts 的 splitSysPromptPrefix() 会把 system prompt 拆成不同 block,并打上:
globalorgnull
几种 cache scope。
当存在 SYSTEM_PROMPT_DYNAMIC_BOUNDARY 时:
- boundary 前静态部分可以走更激进的 cache
- boundary 后动态部分单独处理
6.2 如果 MCP 工具真正出现在工具列表里,就不能安全做全局 system prompt cache
src/services/api/claude.ts 里有 needsToolBasedCacheMarker:
- 如果本轮会渲染 MCP tool schema
- 那么 system prompt 的全局缓存策略要收缩
因为 MCP 工具天然带用户态、连接态差异,不适合和全局静态 prompt 共用同一层缓存假设。
6.3 很多“不要动态变化”的约束都在保护 cache key
例如:
- tool schema session cache
- system prompt section cache
- built-in/MCP 工具分区排序
- dynamic boundary
- clear on
/compact//clear
所以理解这个仓库的 prompt 系统,不能只盯着文案内容,还要盯着“哪些字节必须稳定”。
7. Compact 与 Prompt Too Long 的处理
7.1 compact 本身也是一次单独的 prompt 任务
src/services/compact/prompt.ts 里专门定义了 compact prompt。
它会强调:
- 只能输出文本
- 不许调工具
- 要按固定结构总结历史
这说明 compact 不是 message 层机械裁剪,而是让模型执行一项新的“总结任务”。
7.2 如果 compact 自己也触发 prompt too long,不会直接失败
src/services/compact/compact.ts 会在 compact 请求本身遇到 PTL 时调用:
truncateHeadForPTLRetry()
它会丢掉最旧的一批 API-round groups,再重试 compact。
也就是说,系统对 prompt 过长的处理是分层降级的:
- 先 normal query
- 再 auto/reactive compact
- 如果 compact 也过长,再做 head truncation retry
7.3 compact 后要恢复一些“非摘要状态”
compact 不是只留下 summary。
src/services/compact/compact.ts 还会恢复或保留:
- attachment
- hook 结果
- skill 内容
- plan mode / discovered tools 等元信息
这说明 prompt 系统维护的不只是对话文本,还有“让后续 prompt 继续可用的执行态”。
8. 这个仓库里的 prompt 系统本质是什么
可以把它总结成一句话:
这个仓库实现的不是 “一段 system prompt”,而是一套面向 agent runtime 的 prompt 装配系统。
它有几个鲜明特征:
- prompt 被拆成多层:system、context、tool、skill、compact
- prompt 不是一次性拼接,而是在不同阶段逐层注入
- prompt 内容设计和 cache 稳定性是同时设计的
- 工具描述本身也是 prompt
- slash command / skill 本质上也是 prompt 扩展
- compact 不是裁剪字符串,而是用另一条 prompt 链路做摘要重建
如果只看 getSystemPrompt(),只能看到这套系统的一部分。
真正完整的 prompt 链路,需要一起看:
src/constants/prompts.tssrc/utils/systemPrompt.tssrc/utils/queryContext.tssrc/utils/api.tssrc/query.tssrc/services/api/claude.tssrc/utils/processUserInput/processSlashCommand.tsxsrc/services/compact/prompt.ts