模块六:System Prompt | 前置依赖:第 07 课、第 15 课 | 预计学习时间:65 分钟
学习目标
完成本课后,你将能够:
- 说明
getSystemPrompt()的完整组装流程与返回格式 - 区分静态节(cacheable)与动态节(session-specific)的设计意图
- 解释
SYSTEM_PROMPT_DYNAMIC_BOUNDARY的缓存分割策略 - 描述
systemPromptSection()与DANGEROUS_uncachedSystemPromptSection()的差异 - 列举主要静态节与动态节的内容分布
17.1 System Prompt 的全局视角
Claude Code 的 system prompt 不是一个字符串,而是一个 字符串数组 string[]。API 层面,每个数组元素被转为独立的 text 块,这与 Claude API 的 system 参数兼容。
getSystemPrompt 签名
// constants/prompts.ts
export async function getSystemPrompt(
tools: Tools, // 当前可用工具列表
model: string, // 当前模型 ID
additionalWorkingDirectories?: string[],
mcpClients?: MCPServerConnection[],
): Promise<string[]>
极简模式
如果设置了环境变量 CLAUDE_CODE_SIMPLE,系统直接返回一行:
if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
return [
`You are Claude Code, Anthropic's official CLI for Claude.
CWD: ${getCwd()}
Date: ${getSessionStartDate()}`,
]
}
Proactive 模式
当 proactive/kairos 功能活跃时,使用精简的自主 agent prompt:
if (proactiveModule?.isProactiveActive()) {
return [
`\nYou are an autonomous agent. Use the available tools...`,
getSystemRemindersSection(),
await loadMemoryPrompt(),
envInfo,
getLanguageSection(settings.language),
getMcpInstructionsSection(mcpClients),
getScratchpadInstructions(),
getFunctionResultClearingSection(model),
SUMMARIZE_TOOL_RESULTS_SECTION,
getProactiveSection(),
].filter(s => s !== null)
}
17.2 主路径 — 静态节与动态节
正常模式下,system prompt 分为两大区域:
┌─────────────────────────────────────────────────┐
│ 静态节 (Static Sections) │
│ │
│ 跨用户、跨会话不变的内容 │
│ 可以使用 cache_scope: 'global' 缓存 │
│ │
│ 1. Intro Section — 身份定义 │
│ 2. System Section — 系统行为规范 │
│ 3. Doing Tasks Section — 任务执行准则 │
│ 4. Actions Section — 操作谨慎度准则 │
│ 5. Using Tools Section — 工具使用指南 │
│ 6. Tone & Style Section — 语气和格式 │
│ 7. Output Efficiency — 输出效率指引 │
│ │
├── SYSTEM_PROMPT_DYNAMIC_BOUNDARY ────────────────┤
│ │
│ 动态节 (Dynamic Sections) │
│ │
│ 会话级别,可能每次 turn 变化 │
│ 不使用全局缓存 │
│ │
│ 8. Session Guidance — 会话特定指引 │
│ 9. Memory — MEMORY.md 内容 │
│ 10. Ant Model Override — 内部模型调优 │
│ 11. Environment Info — 运行环境信息 │
│ 12. Language — 语言偏好 │
│ 13. Output Style — 输出风格 │
│ 14. MCP Instructions — MCP 服务器指令 │
│ 15. Scratchpad — 临时文件目录 │
│ 16. FRC — 函数结果清理 │
│ 17. Tool Results — 工具结果保存提示 │
│ 18. Token Budget — token 预算指引 │
│ 19. Brief — Brief 模式指引 │
│ │
└─────────────────────────────────────────────────┘
代码实现
return [
// --- Static content (cacheable) ---
getSimpleIntroSection(outputStyleConfig),
getSimpleSystemSection(),
outputStyleConfig === null || outputStyleConfig.keepCodingInstructions === true
? getSimpleDoingTasksSection()
: null,
getActionsSection(),
getUsingYourToolsSection(enabledTools),
getSimpleToneAndStyleSection(),
getOutputEfficiencySection(),
// === BOUNDARY MARKER ===
...(shouldUseGlobalCacheScope()
? [SYSTEM_PROMPT_DYNAMIC_BOUNDARY]
: []),
// --- Dynamic content (registry-managed) ---
...resolvedDynamicSections,
].filter(s => s !== null)
17.3 SYSTEM_PROMPT_DYNAMIC_BOUNDARY
这是整个缓存策略的核心标记:
export const SYSTEM_PROMPT_DYNAMIC_BOUNDARY =
'__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__'
缓存分割原理
API 层面的系统提示:
┌──────────────────────────────────────┐
│ [text block 1: intro] │
│ [text block 2: system] │ cache_scope: 'global'
│ [text block 3: doing tasks] │ → 跨用户共享缓存
│ [text block 4: actions] │
│ [text block 5: tools] │
│ [text block 6: tone] │
│ [text block 7: output] │
│ [text block 8: __BOUNDARY__] │ ← 分界标记
├──────────────────────────────────────┤
│ [text block 9: session guidance] │
│ [text block 10: memory] │ cache_scope: 无(每次发送)
│ [text block 11: env info] │ → 用户/会话特定
│ [text block 12: language] │
│ [text block 13: MCP instructions] │
│ ... │
└──────────────────────────────────────┘
API 层(services/api/claude.ts)使用 splitSysPromptPrefix 函数找到 boundary,将前半部分标记为可全局缓存(Blake2b hash 作为缓存 key),后半部分每次新鲜发送。
为什么需要这个分界?
Claude API 的 prompt caching 依赖前缀匹配。如果动态内容(如 MCP 指令)插在静态内容中间,每次变化都会导致后续所有内容的缓存失效。将不变的内容放在前面,保证前缀稳定,最大化缓存命中率。
变化导致的缓存失效:
坏的布局: [intro][env][system][tools]
↑ ↑变化 → 后续全部失效
好的布局: [intro][system][tools] | [env]
↑─── 缓存命中 ───↑ ↑── 动态重发
17.4 systemPromptSection 缓存机制
动态节使用注册机制管理,每个节是一个 SystemPromptSection 对象:
// constants/systemPromptSections.ts
type SystemPromptSection = {
name: string // 唯一标识
compute: ComputeFn // 计算函数
cacheBreak: boolean // 是否破坏缓存
}
两种创建方式
1. 可缓存(默认)— systemPromptSection()
export function systemPromptSection(
name: string,
compute: ComputeFn,
): SystemPromptSection {
return { name, compute, cacheBreak: false }
}
计算一次后缓存,直到 /clear 或 /compact 重置。适用于会话期间不变的内容(如环境信息、语言偏好)。
2. 不可缓存 — DANGEROUS_uncachedSystemPromptSection()
export function DANGEROUS_uncachedSystemPromptSection(
name: string,
compute: ComputeFn,
_reason: string, // 必须解释为什么需要破坏缓存
): SystemPromptSection {
return { name, compute, cacheBreak: true }
}
每次 turn 重新计算。函数名中的 DANGEROUS 是刻意的吓阻命名,因为不可缓存节会破坏 prompt cache,增加 API 成本。_reason 参数强制调用者记录原因。
当前的 DANGEROUS 节
整个代码库中只有一个 DANGEROUS 节:
DANGEROUS_uncachedSystemPromptSection(
'mcp_instructions',
() => isMcpInstructionsDeltaEnabled()
? null
: getMcpInstructionsSection(mcpClients),
'MCP servers connect/disconnect between turns', // 原因
)
MCP 服务器可能在 turn 之间连接或断开,所以指令必须每次重新获取。当 mcp_instructions_delta 特性启用时,通过 attachment 增量通知代替,这个节就返回 null。
解析流程
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
}),
)
}
缓存清理
export function clearSystemPromptSections(): void {
clearSystemPromptSectionState() // 清除所有节缓存
clearBetaHeaderLatches() // 重置 beta header 锁存器
}
// 在 /clear 和 /compact 时调用
17.5 静态节详解
Intro Section — 身份与安全
function getSimpleIntroSection(outputStyleConfig): string {
return `
You are an interactive agent that helps users ${
outputStyleConfig !== null
? 'according to your "Output Style" below...'
: 'with software engineering tasks.'
}
${CYBER_RISK_INSTRUCTION}
IMPORTANT: You must NEVER generate or guess URLs for the user...`
}
CYBER_RISK_INSTRUCTION 是一条安全指令,防止模型被滥用于网络攻击。
System Section — 行为规范
function getSimpleSystemSection(): string {
const items = [
`All text you output outside of tool use is displayed to the user...`,
`Tools are executed in a user-selected permission mode...`,
`Tool results and user messages may include <system-reminder>...`,
`Tool results may include data from external sources...`,
getHooksSection(), // hooks 行为说明
`The system will automatically compress prior messages...`,
]
return ['# System', ...prependBullets(items)].join('\n')
}
Doing Tasks Section — 任务执行指南
这是最长的静态节(约 2000 字),包含:
# Doing tasks
- 理解上下文后再修改代码
- 不要过度工程化,不要添加未要求的功能
- 不要添加不必要的错误处理
- 不要创建过早的抽象
- 先读文件再修改
- 诊断失败原因后再切换策略
- 注意安全(XSS、SQL 注入等)
- 不添加不必要的注释(ant 构建)
- 如实报告结果(ant 构建)
注意 process.env.USER_TYPE === 'ant' 条件分支,Anthropic 内部用户看到额外的指导:
...(process.env.USER_TYPE === 'ant'
? [
`Default to writing no comments...`,
`Before reporting a task complete, verify it actually works...`,
`Report outcomes faithfully...`,
]
: []),
Actions Section — 操作谨慎度
# Executing actions with care
不可逆操作、影响共享系统的操作、有风险的操作 → 询问确认
示例:
- 删除文件/分支、丢弃数据库表
- force-push、git reset --hard
- 推送代码、创建 PR、发送消息
- 上传内容到第三方工具
Using Tools Section — 工具使用指南
function getUsingYourToolsSection(enabledTools: Set<string>): string {
const providedToolSubitems = [
`To read files use ${FILE_READ_TOOL_NAME} instead of cat...`,
`To edit files use ${FILE_EDIT_TOOL_NAME} instead of sed...`,
`To create files use ${FILE_WRITE_TOOL_NAME} instead of cat heredoc...`,
`To search for files use ${GLOB_TOOL_NAME} instead of find...`,
`To search content use ${GREP_TOOL_NAME} instead of grep...`,
`Reserve ${BASH_TOOL_NAME} for shell execution only.`,
]
// + TodoWrite、并行工具调用指南
}
Tone & Style Section
# Tone and style
- 不使用 emoji(除非用户要求)
- 回复简短精练
- 引用代码时包含 file_path:line_number
- GitHub 引用使用 owner/repo#123 格式
- 工具调用前不用冒号
Output Efficiency Section
ant 用户和外部用户看到不同的指引:
ant 用户:
# Communicating with the user
面向人类写作,不是日志。假设用户不看工具调用。
操作前简述,关键时刻更新,写完整句子。
外部用户:
# Output efficiency
直奔主题。简单方法优先。保持简洁。
聚焦于需要用户输入的决策、里程碑更新、障碍。
17.6 动态节注册
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_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: 数字长度锚定
// feature-gated: token budget
// feature-gated: brief section
]
每个节都是惰性计算的 — 只在第一次 resolveSystemPromptSections 时执行 compute(),之后从缓存读取(除 DANGEROUS 节外)。
17.7 Session-Specific Guidance
getSessionSpecificGuidanceSection 是一个特殊的动态节,包含运行时条件性指引:
function getSessionSpecificGuidanceSection(
enabledTools: Set<string>,
skillToolCommands: Command[],
): string | null {
const items = [
// AskUserQuestion 工具指引(如可用)
hasAskUserQuestionTool ? `If you do not understand...` : null,
// ! 前缀执行提示(非 headless)
!getIsNonInteractiveSession() ? `...type ! <command>...` : null,
// Agent 工具使用指引
hasAgentTool ? getAgentToolSection() : null,
// Explore agent 指引
areExplorePlanAgentsEnabled() ? `...use Explore agent...` : null,
// Skill 工具指引
hasSkills ? `/<skill-name> is shorthand...` : null,
// Discover Skills 指引
DISCOVER_SKILLS_TOOL_NAME ? getDiscoverSkillsGuidance() : null,
// Verification Agent 指引
feature('VERIFICATION_AGENT') ? `...spawn verifier...` : null,
].filter(item => item !== null)
if (items.length === 0) return null
return ['# Session-specific guidance', ...prependBullets(items)].join('\n')
}
为什么这些不在静态节中? 因为每个条件都依赖运行时状态(工具列表、session 类型、feature flags),放在静态节会导致 2^N 个缓存变体,碎片化 prompt cache。
17.8 prependBullets — 格式化辅助
一个看似简单但设计巧妙的工具函数:
export function prependBullets(items: Array<string | string[]>): string[] {
return items.flatMap(item =>
Array.isArray(item)
? item.map(subitem => ` - ${subitem}`) // 子项缩进两格
: [` - ${item}`], // 主项缩进一格
)
}
支持嵌套列表:
const items = [
'Main point',
['Sub point 1', 'Sub point 2'],
'Another main point',
]
// 输出:
// - Main point
// - Sub point 1
// - Sub point 2
// - Another main point
17.9 子 Agent 的 Prompt 增强
子 Agent 不走 getSystemPrompt,而是在现有 prompt 基础上追加环境信息:
export async function enhanceSystemPromptWithEnvDetails(
existingSystemPrompt: string[],
model: string,
additionalWorkingDirectories?: string[],
enabledToolNames?: ReadonlySet<string>,
): Promise<string[]> {
const notes = `Notes:
- Agent threads always have their cwd reset between bash calls...
- In your final response, share file paths (always absolute)...
- For clear communication the assistant MUST avoid using emojis.
- Do not use a colon before tool calls.`
const envInfo = await computeEnvInfo(model, additionalWorkingDirectories)
return [
...existingSystemPrompt,
notes,
...(discoverSkillsGuidance ? [discoverSkillsGuidance] : []),
envInfo,
]
}
子 Agent 使用 computeEnvInfo(完整版),而主 Agent 使用 computeSimpleEnvInfo(精简版)。
课后练习
练习 1:缓存命中分析
假设用户在会话中连接了一个新的 MCP 服务器。分析以下场景的缓存行为:
- 下一个 turn 的静态节是否命中缓存?
- MCP 指令节如何更新?
- 如果启用了
mcp_instructions_delta,行为有何不同?
练习 2:节分类练习
将以下内容分类为"应该放在静态节"还是"应该放在动态节",并解释原因:
- 模型的 knowledge cutoff 日期
- 当前工作目录路径
- "不要使用 emoji" 的指引
- 已安装的 skill 列表
- "使用 FileEdit 而不是 sed" 的指引
练习 3:DANGEROUS 节审计
搜索代码库中所有 DANGEROUS_uncachedSystemPromptSection 调用。对每个调用:
- 它的 reason 参数是什么?
- 是否可以改为可缓存节?如果可以,需要什么前提条件?
练习 4:Prompt 大小估算
阅读 getSystemPrompt 返回的各个节,估算:
- 静态节总字符数(大约)
- 动态节的最大/最小字符数
- 整个 system prompt 在 token 中大约多大?(按 4 字符/token 估算)
本课小结
| 要点 | 内容 |
|---|---|
| 返回格式 | string[] — 每个元素成为 API 的独立 text block |
| 静态节 | 7 个,跨用户不变,支持 global cache scope |
| 动态节 | 12+ 个,会话级,通过 registry 管理 |
| 缓存分界 | SYSTEM_PROMPT_DYNAMIC_BOUNDARY 标记分割点 |
| 节缓存 | systemPromptSection 计算一次后缓存;DANGEROUS 每 turn 重算 |
| Session Guidance | 运行时条件汇聚,避免碎片化静态前缀 |
| 子 Agent | enhanceSystemPromptWithEnvDetails 追加,不走 getSystemPrompt |
下一课预告
第 18 课:动态节详解 — 记忆、MCP 指令与环境 — 深入每个动态节的实现细节,包括 MEMORY.md 加载与截断机制、MCP 指令格式化、环境信息计算、scratchpad 目录、以及 token 预算注入。