Files

212 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags:
- system-prompt
- 缓存策略
- 动态组装
- Prompt-Cache
create time: 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。
```mermaid
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 品牌类型
```typescript
// 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`):
```typescript
// 缓存式 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:
```mermaid
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 功能不可用。
## 关联笔记
- [[compaction]] - 上下文压缩与 Session Memory
- [[token-budget]] - Token 预算动态计算
- [[project-memory]] - 项目记忆系统与 CLAUDE.md
- [[../conversation/the-loop]] - Agentic Loop 中的 System Prompt 使用