第 17 课:Prompt 组装架构

1 阅读9分钟

模块六:System Prompt | 前置依赖:第 07 课、第 15 课 | 预计学习时间:65 分钟


学习目标

完成本课后,你将能够:

  1. 说明 getSystemPrompt() 的完整组装流程与返回格式
  2. 区分静态节(cacheable)与动态节(session-specific)的设计意图
  3. 解释 SYSTEM_PROMPT_DYNAMIC_BOUNDARY 的缓存分割策略
  4. 描述 systemPromptSection()DANGEROUS_uncachedSystemPromptSection() 的差异
  5. 列举主要静态节与动态节的内容分布

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. MemoryMEMORY.md 内容           │
│  10. Ant Model Override — 内部模型调优             │
│  11. Environment Info   — 运行环境信息             │
│  12. Language           — 语言偏好                 │
│  13. Output Style       — 输出风格                 │
│  14. MCP InstructionsMCP 服务器指令           │
│  15. Scratchpad         — 临时文件目录             │
│  16. FRC               — 函数结果清理              │
│  17. Tool Results       — 工具结果保存提示         │
│  18. Token Budgettoken 预算指引           │
│  19. BriefBrief 模式指引           │
│                                                   │
└─────────────────────────────────────────────────┘

代码实现

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运行时条件汇聚,避免碎片化静态前缀
子 AgentenhanceSystemPromptWithEnvDetails 追加,不走 getSystemPrompt

下一课预告

第 18 课:动态节详解 — 记忆、MCP 指令与环境 — 深入每个动态节的实现细节,包括 MEMORY.md 加载与截断机制、MCP 指令格式化、环境信息计算、scratchpad 目录、以及 token 预算注入。