Files

11 KiB
Raw Permalink Blame History

tags, create time
tags create time
system-prompt
缓存策略
动态组装
Prompt-Cache
2026-06-09 22:15

System Prompt 动态组装 - AI 工作记忆构建

概述

Claude Code 的 System Prompt 不是一段写死的文本,而是通过三阶段管道动态组装的 string[] 数组,支持分块缓存、五级优先级选择、CLAUDE.md 多级合并和多种 Provider 适配。

正文

从数组到 API 调用: System Prompt 的完整链路

System Prompt 在 Claude Code 中是一个 string[] 数组(品牌类型 SystemPrompt,定义于 src/utils/systemPromptType.ts:8),经过组装、分块、缓存标记后发送给 API。

flowchart TD
    A["getSystemPrompt string[] 组装内容"] --> B["buildEffectiveSystemPrompt SystemPrompt 选择优先级路径"]
    B --> C["buildSystemPromptBlocks TextBlockParam[] 分块+cache_control标记"]
  1. getSystemPrompt()(src/constants/prompts.ts:444)—— 收集静态段 + 动态段,插入 SYSTEM_PROMPT_DYNAMIC_BOUNDARY 分界标记
  2. buildEffectiveSystemPrompt()(src/utils/systemPrompt.ts:41)—— 按 Override > Coordinator > Agent > Custom > Default 优先级选择
  3. buildSystemPromptBlocks()(src/services/api/claude.ts:3279)—— 调用 splitSysPromptPrefix() 分块,为每个块附加 cache_control

SystemPrompt 品牌类型

// packages/@ant/model-provider/src/types/systemPrompt.ts
export type SystemPrompt = readonly string[] & {
  readonly __brand: 'SystemPrompt'
}
export function asSystemPrompt(value: readonly string[]): SystemPrompt {
  return value as SystemPrompt  // 零开销类型断言
}

品牌类型(branded type)防止普通 string[] 被意外传入 API 调用——只有通过 asSystemPrompt() 显式转换才能获得 SystemPrompt 类型。

getSystemPrompt(): 内容组装的全景

src/constants/prompts.ts:444 是 System Prompt 的核心工厂函数,返回一个有序数组:

阶段 内容 缓存策略
静态区 Intro Section、System Rules、Doing Tasks、Actions、Using Tools、Tone & Style、Output Efficiency 可跨组织缓存(scope: 'global')
BOUNDARY SYSTEM_PROMPT_DYNAMIC_BOUNDARY 分界标记(不发送给 API,仅用于分割静态区与动态区)
动态区 Session Guidance、Memory、Model Override、Env Info、Language 等 每次会话不同(scope: 'org' 或无缓存)

[!info] Boundary 的作用 它把 System Prompt 分成"不变的静态区"和"因用户/会话而异的动态区"。静态区对所有用户相同,可获得 scope: 'global' 跨组织缓存;动态区每次不同,只能 scope: 'org' 或不缓存。它本身是一个特殊字符串,在发送给 API 前被移除,AI 永远看不到。

动态区的 Section 注册表

动态区通过 systemPromptSection() / DANGEROUS_uncachedSystemPromptSection() 注册(src/constants/systemPromptSections.ts):

// 缓存式 Section: 计算一次,/clear 或 /compact 后才重新计算
systemPromptSection('memory', () => loadMemoryPrompt())

// 危险: 每轮重新计算,会破坏 Prompt Cache
DANGEROUS_uncachedSystemPromptSection(
  'mcp_instructions',
  () => isMcpInstructionsDeltaEnabled() ? null : getMcpInstructionsSection(mcpClients),
  'MCP servers connect/disconnect between turns'  // 必须给出破坏缓存的理由
)

resolveSystemPromptSections() 在每轮查询时解析所有 Section,对于 cacheBreak: false 的 Section,优先使用缓存值。只有 MCP 指令等真正动态的内容使用 DANGEROUS_uncachedSystemPromptSection。

CLAUDE_CODE_SIMPLE 快速路径

当环境变量 CLAUDE_CODE_SIMPLE 为真时,整个 System Prompt 缩减为一行,跳过所有 Section 注册、缓存分块、动态组装——用于最小化 token 消耗的测试场景。

buildEffectiveSystemPrompt(): 五级优先级

src/utils/systemPrompt.ts:41 决定最终使用哪个 System Prompt:

优先级 条件 行为
0. Override overrideSystemPrompt 非空 完全替换,返回 [override]
1. Coordinator COORDINATOR_MODE feature + 环境变量 使用协调者专用提示词
2. Agent mainThreadAgentDefinition 存在 Proactive 模式: 追加到默认提示词尾部;否则: 替换默认提示词
3. Custom --system-prompt 参数指定 替换默认提示词
4. Default 无特殊条件 使用 getSystemPrompt() 完整输出

appendSystemPrompt 始终追加到末尾(Override 除外)。

Provider 系统概述

Claude Code 支持多种 API 提供商:

类别 Provider 环境变量 说明
1P firstParty 默认 Anthropic 官方 API 直连
3P bedrock CLAUDE_CODE_USE_BEDROCK=1 AWS Bedrock 托管服务
3P vertex CLAUDE_CODE_USE_VERTEX=1 Google Vertex AI
3P openai CLAUDE_CODE_USE_OPENAI=1 OpenAI 兼容层(Ollama/DeepSeek/vLLM)
3P gemini CLAUDE_CODE_USE_GEMINI=1 Google Gemini API
3P grok CLAUDE_CODE_USE_GROK=1 xAI Grok

Provider 决定了可用的 beta headers、缓存策略(全局缓存仅 1P 可用)和 Token 计数方式。

缓存策略: 分块、标记、命中

这是 System Prompt 设计中最精密的部分。

Anthropic Prompt Cache 基础: 允许跨请求复用相同的 System Prompt 前缀,按缓存命中量计费。缓存键由内容的 Blake2b 哈希决定——任何字符变化都会导致缓存失效。

splitSysPromptPrefix(): 三种分块模式

src/utils/api.ts:321 是缓存策略的核心:

模式 1: MCP 工具存在时(skipGlobalCacheForSystemPrompt=true)

[attribution header]    -> cacheScope: null     (不缓存)
[system prompt prefix]  -> cacheScope: 'org'    (组织级缓存)
[everything else]       -> cacheScope: 'org'    (组织级缓存)

MCP 工具列表在会话中可能变化,破坏了跨组织缓存的基础,因此降级为组织级。

模式 2: Global Cache + Boundary 存在(1P 专用)

[attribution header]    -> cacheScope: null     (不缓存)
[system prompt prefix]  -> cacheScope: null     (不缓存)
[static content]        -> cacheScope: 'global' (全局缓存! 跨组织共享)
[dynamic content]       -> cacheScope: null     (不缓存)

这是缓存效率最高的模式。SYSTEM_PROMPT_DYNAMIC_BOUNDARY 之前的静态内容对所有用户相同,可跨组织缓存。

[!tip] Boundary 插入条件 SYSTEM_PROMPT_DYNAMIC_BOUNDARY 标记仅在 getAPIProvider() === 'firstParty' 且未禁用实验性功能时插入。3P 用户(Bedrock/Vertex/OpenAI/Gemini)永远不存在 Boundary,始终使用模式 3。

模式 3: 默认(3P 提供商或 Boundary 缺失)

所有内容块使用 cacheScope: 'org'(组织级缓存)。

getCacheControl(): TTL 决策

src/services/api/claude.ts:348 生成的 cache_control 对象支持 1 小时 TTL(通过 GrowthBook 配置的 allowlist 匹配 querySource),会话级资格判定结果在 bootstrap state 中缓存防止中途变化。

缓存破坏: Session-Specific Guidance 的放置

getSessionSpecificGuidanceSection() 的内容必须放在 SYSTEM_PROMPT_DYNAMIC_BOUNDARY 之后,因为它包含运行时条件(enabledTools、isForkSubagentEnabled()、getIsNonInteractiveSession())。这些运行时 bit 如果放在静态区,会产生 2^N 种 Blake2b 哈希变体,完全破坏缓存命中率。

上下文注入: System Context 与 User Context

System Prompt 数组本身不包含运行时上下文。上下文通过两个独立的管道注入:

System Context(src/context.ts:116): 使用 lodash.memoize 缓存,整个会话期间只计算一次。包含 git 状态(5 个并行 git 命令的快照)和可选的缓存破坏器。

User Context(src/context.ts:155): 同样使用 memoize 缓存。包含合并后的 CLAUDE.md 内容和当前日期。禁用条件: CLAUDE_CODE_DISABLE_CLAUDE_MDS 环境变量或 --bare 模式。

注入位置: System Context 追加到 System Prompt 尾部(src/query.ts:449);User Context 通过 prependUserContext() 注入为 <system-reminder> 标签包裹的首条用户消息。

Attribution Header: 计费与安全

每个 API 请求的 System Prompt 首块是 Attribution Header(src/constants/system.ts:30),包含版本标识、入口点标识和可选的客户端证明 token。Header 始终 cacheScope: null——它因版本和指纹不同而变化,不适合缓存。

CLAUDE.md: 项目级知识注入

在项目根目录放一个 CLAUDE.md 文件,就能让 AI "理解" 你的项目——项目概述、开发约定、常用命令、注意事项。

系统会自动发现并合并多级 CLAUDE.md:

flowchart TD
    A["~/.claude/CLAUDE.md 用户全局 个人偏好"] --> B["/project/CLAUDE.md 项目根目录 团队共享"]
    B --> C["/project/src/CLAUDE.md 子目录 模块特定"]

加载逻辑在 src/utils/claudemd.ts 中的 getClaudeMds() 和 getMemoryFiles() 实现——从 CWD 向上遍历目录树,合并所有匹配的 CLAUDE.md 文件内容。

设计洞察: 为什么是 string[] 而非单个 string

将 System Prompt 设计为数组而非单段文本,是为了缓存分块:

  1. Anthropic Prompt Cache 以内容块(TextBlock)为缓存单位
  2. 将 System Prompt 拆为多个块,可以让不变的部分获得独立的缓存命中
  3. 如果是单个 string,任何一个字符变化都会导致整个 System Prompt 的缓存失效
  4. SYSTEM_PROMPT_DYNAMIC_BOUNDARY 标记允许精确地将静态区标记为 scope: 'global'

一次典型的 System Prompt 约 20K+ tokens,通过缓存分块可以节省 30-50% 的输入 token 费用。

兼容层: OpenAI 与 Gemini

Claude Code 提供了 OpenAI 和 Gemini 协议的兼容层,允许使用非 Anthropic 端点。

OpenAI 兼容层(CLAUDE_CODE_USE_OPENAI=1): 支持任意 OpenAI Chat Completions 协议端点(Ollama、DeepSeek、vLLM 等)。采用流适配器模式: 将 Anthropic 格式请求转换为 OpenAI 格式 -> 调用端点 -> 将 SSE 流转换回 BetaRawMessageStreamEvent。

Gemini 兼容层(CLAUDE_CODE_USE_GEMINI=1): 支持 Google Gemini API,同样通过流适配器模式屏蔽差异。

[!warning] 兼容层的限制 使用 3P 兼容层时,部分功能受限: 无精确 token 计数(退回到近似估算)、无全局缓存、部分 beta 功能不可用。

关联笔记