Skip to content

Commit c57e6ee

Browse files
docs: 文档优化完成
1 parent 221fb6e commit c57e6ee

7 files changed

Lines changed: 1088 additions & 253 deletions

File tree

docs/context/system-prompt.mdx

Lines changed: 221 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,225 @@
11
---
22
title: "System Prompt 动态组装 - AI 工作记忆构建"
3-
description: "深入解析 Claude Code 的 System Prompt 动态组装过程:如何将 CLAUDE.md、项目上下文、工具定义和用户偏好拼装为 AI 的工作记忆"
4-
keywords: ["System Prompt", "系统提示词", "动态组装", "CLAUDE.md", "上下文构建"]
3+
description: "深入解析 Claude Code 的 System Prompt 动态组装过程:缓存策略、分界标记、Section 注册表、CLAUDE.md 多级合并,以及如何将零散上下文拼装为 API 可消费的缓存友好结构"
4+
keywords: ["System Prompt", "系统提示词", "动态组装", "CLAUDE.md", "Prompt Cache", "缓存策略"]
55
---
66

7-
{/* 本章目标:解释 System Prompt 的组装过程和设计思想 */}
7+
## 从数组到 API 调用:System Prompt 的完整链路
88

9-
## 什么是 System Prompt
9+
System Prompt 在 Claude Code 中不是一段写死的文本,而是一个 **`string[]` 数组**(品牌类型 `SystemPrompt`,定义于 `src/utils/systemPromptType.ts:8`),经过组装、分块、缓存标记后发送给 API。
1010

11-
每次调用 AI API 时,都需要发送一个 System Prompt——它是 AI 的"人设说明书",告诉 AI:
11+
### 三阶段管道
1212

13-
- 你是谁(Claude Code,一个编程助手)
14-
- 你能做什么(可用工具列表)
15-
- 你在什么环境(操作系统、当前目录、git 状态)
16-
- 你需要遵守什么规则(安全规范、输出格式)
13+
```
14+
getSystemPrompt() → string[] (组装内容)
15+
16+
buildEffectiveSystemPrompt() → SystemPrompt (选择优先级路径)
17+
18+
buildSystemPromptBlocks() → TextBlockParam[] (分块 + cache_control 标记)
19+
```
20+
21+
1. **`getSystemPrompt()`**`src/constants/prompts.ts:444`)—— 收集静态段 + 动态段,插入 `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` 分界标记
22+
2. **`buildEffectiveSystemPrompt()`**`src/utils/systemPrompt.ts:41`)—— 按 Override > Coordinator > Agent > Custom > Default 优先级选择
23+
3. **`buildSystemPromptBlocks()`**`src/services/api/claude.ts:3214`)—— 调用 `splitSysPromptPrefix()` 分块,为每个块附加 `cache_control`
24+
25+
## SystemPrompt 品牌类型
26+
27+
```typescript
28+
// src/utils/systemPromptType.ts:8
29+
export type SystemPrompt = readonly string[] & {
30+
readonly __brand: 'SystemPrompt'
31+
}
32+
export function asSystemPrompt(value: readonly string[]): SystemPrompt {
33+
return value as SystemPrompt // 零开销类型断言
34+
}
35+
```
36+
37+
品牌类型(branded type)防止普通 `string[]` 被意外传入 API 调用——只有通过 `asSystemPrompt()` 显式转换才能获得 `SystemPrompt` 类型。
38+
39+
## getSystemPrompt():内容组装的全景
40+
41+
`src/constants/prompts.ts:444` 是 System Prompt 的核心工厂函数,返回一个有序数组:
42+
43+
| 阶段 | 内容 | 缓存策略 |
44+
|------|------|----------|
45+
| **静态区** | Intro Section、System Rules、Doing Tasks、Actions、Using Tools、Tone & Style、Output Efficiency | 可跨组织缓存(`scope: 'global'`|
46+
| **BOUNDARY** | `SYSTEM_PROMPT_DYNAMIC_BOUNDARY = '__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__'` | 分界标记(不发送给 API) |
47+
| **动态区** | Session Guidance、Memory、Model Override、Env Info、Language、Output Style、MCP Instructions、Scratchpad、FRC、Summarize Tool Results、Token Budget、Brief | 每次会话不同(`scope: 'org'` 或无缓存) |
48+
49+
### 动态区的 Section 注册表
50+
51+
动态区通过 `systemPromptSection()` / `DANGEROUS_uncachedSystemPromptSection()` 注册,这两个工厂函数定义于 `src/constants/systemPromptSections.ts`
52+
53+
```typescript
54+
// 缓存式 Section:计算一次,/clear 或 /compact 后才重新计算
55+
systemPromptSection('memory', () => loadMemoryPrompt())
56+
57+
// 危险:每轮重新计算,会破坏 Prompt Cache
58+
DANGEROUS_uncachedSystemPromptSection(
59+
'mcp_instructions',
60+
() => isMcpInstructionsDeltaEnabled() ? null : getMcpInstructionsSection(mcpClients),
61+
'MCP servers connect/disconnect between turns' // 必须给出破坏缓存的理由
62+
)
63+
```
64+
65+
`resolveSystemPromptSections()` 在每轮查询时解析所有 Section,对于 `cacheBreak: false` 的 Section,优先使用 `getSystemPromptSectionCache()` 中的缓存值。只有 MCP 指令等真正动态的内容使用 `DANGEROUS_uncachedSystemPromptSection`
66+
67+
### `CLAUDE_CODE_SIMPLE` 快速路径
68+
69+
当环境变量 `CLAUDE_CODE_SIMPLE` 为真时,整个 System Prompt 缩减为一行:
70+
71+
```typescript
72+
`You are Claude Code, Anthropic's official CLI for Claude.\n\nCWD: ${getCwd()}\nDate: ${getSessionStartDate()}`
73+
```
74+
75+
跳过所有 Section 注册、缓存分块、动态组装——用于最小化 token 消耗的测试场景。
76+
77+
## buildEffectiveSystemPrompt():五级优先级
78+
79+
`src/utils/systemPrompt.ts:41` 决定最终使用哪个 System Prompt:
80+
81+
| 优先级 | 条件 | 行为 |
82+
|--------|------|------|
83+
| **0. Override** | `overrideSystemPrompt` 非空 | 完全替换,返回 `[override]` |
84+
| **1. Coordinator** | `COORDINATOR_MODE` feature + 环境变量 | 使用协调者专用提示词 |
85+
| **2. Agent** | `mainThreadAgentDefinition` 存在 | Proactive 模式:追加到默认提示词尾部;否则:替换默认提示词 |
86+
| **3. Custom** | `--system-prompt` 参数指定 | 替换默认提示词 |
87+
| **4. Default** | 无特殊条件 | 使用 `getSystemPrompt()` 完整输出 |
88+
89+
`appendSystemPrompt` 始终追加到末尾(Override 除外)。
90+
91+
## 缓存策略:分块、标记、命中
92+
93+
这是 System Prompt 设计中最精密的部分。
94+
95+
### Anthropic Prompt Cache 基础
96+
97+
Anthropic API 的 Prompt Cache 允许跨请求复用相同的 System Prompt 前缀,按缓存命中量计费(远低于完整输入价格)。缓存键由内容的 Blake2b 哈希决定——任何字符变化都会导致缓存失效。
98+
99+
### `splitSysPromptPrefix()`:三种分块模式
100+
101+
`src/utils/api.ts:321` 是缓存策略的核心,根据条件选择三种分块模式:
102+
103+
#### 模式 1:MCP 工具存在时(`skipGlobalCacheForSystemPrompt=true`
104+
105+
```
106+
[attribution header] → cacheScope: null (不缓存)
107+
[system prompt prefix] → cacheScope: 'org' (组织级缓存)
108+
[everything else] → cacheScope: 'org' (组织级缓存)
109+
```
110+
111+
MCP 工具列表在会话中可能变化(连接/断开),破坏了跨组织缓存的基础,因此降级为组织级。
112+
113+
#### 模式 2:Global Cache + Boundary 存在(1P 专用)
17114

18-
## 不是静态模板,而是动态组装
115+
```
116+
[attribution header] → cacheScope: null (不缓存)
117+
[system prompt prefix] → cacheScope: null (不缓存)
118+
[static content] → cacheScope: 'global' (全局缓存!跨组织共享)
119+
[dynamic content] → cacheScope: null (不缓存)
120+
```
121+
122+
这是缓存效率最高的模式。`SYSTEM_PROMPT_DYNAMIC_BOUNDARY` 之前的静态内容(Intro、Rules、Tone & Style 等)对所有用户相同,可跨组织缓存。
123+
124+
#### 模式 3:默认(3P 提供商 或 Boundary 缺失)
125+
126+
```
127+
[attribution header] → cacheScope: null (不缓存)
128+
[system prompt prefix] → cacheScope: 'org' (组织级缓存)
129+
[everything else] → cacheScope: 'org' (组织级缓存)
130+
```
131+
132+
### `getCacheControl()`:TTL 决策
19133

20-
Claude Code 的 System Prompt 不是一段写死的文本,而是根据当前环境**实时组装**的:
134+
`src/services/api/claude.ts:359` 生成的 `cache_control` 对象:
135+
136+
```typescript
137+
{
138+
type: 'ephemeral',
139+
ttl?: '1h', // 仅特定 querySource 符合条件时
140+
scope?: 'global', // 仅静态区
141+
}
142+
```
143+
144+
1 小时 TTL 的判定逻辑(`should1hCacheTTL()`,第 394 行):
145+
- **Bedrock 用户**:通过环境变量 `ENABLE_PROMPT_CACHING_1H_BEDROCK` 启用
146+
- **1P 用户**:通过 GrowthBook 配置的 `allowlist` 数组匹配 `querySource`,支持前缀通配符(如 `"repl_main_thread*"`
147+
- **会话级锁定**:资格判定结果在 bootstrap state 中缓存,防止 GrowthBook 配置中途变化导致同一会话内 TTL 不一致
148+
149+
### 缓存破坏:Session-Specific Guidance 的放置
150+
151+
`getSessionSpecificGuidanceSection()``src/constants/prompts.ts:352`)的内容必须放在 `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` **之后**。因为它包含:
152+
- 当前会话的 enabledTools 集合
153+
- `isForkSubagentEnabled()` 的运行时判定
154+
- `getIsNonInteractiveSession()` 的结果
155+
156+
这些运行时 bit 如果放在静态区,会产生 2^N 种 Blake2b 哈希变体(N = 运行时条件数),完全破坏缓存命中率。源码注释明确警告:
157+
158+
> Each conditional here is a runtime bit that would otherwise multiply the Blake2b prefix hash variants (2^N). See PR #24490, #24171 for the same bug class.
159+
160+
### `CLAUDE_CODE_SIMPLE` 模式
161+
162+
当设置了 `CLAUDE_CODE_SIMPLE` 环境变量时,整个系统提示词会大幅缩减:
163+
164+
```typescript
165+
return [`You are Claude Code, Anthropic's official CLI for Claude.\n\nCWD: ${getCwd()}\nDate: ${getSessionStartDate()}`]
166+
```
21167

22-
<Frame caption="System Prompt 的 6 大组成部分">
23-
<img src="/docs/images/system-prompt-assembly.png" alt="System Prompt 动态组装图" />
24-
</Frame>
168+
## 上下文注入:System Context 与 User Context
25169

26-
| 组成部分 | 内容 | 来源 |
27-
|----------|------|------|
28-
| 基础人设 | 角色定义、行为准则 | 内置模板 |
29-
| 环境信息 | 操作系统、shell 类型、当前日期 | 运行时检测 |
30-
| Git 状态 | 当前分支、最近提交、工作区状态 | `git` 命令输出 |
31-
| 项目知识 | CLAUDE.md 文件内容 | 项目目录层级扫描 |
32-
| 记忆文件 | 用户偏好、项目约定 | 持久化记忆系统 |
33-
| 工具说明 | 每个可用工具的描述和参数 | 工具注册表 |
170+
System Prompt 数组本身不包含运行时上下文(git 状态、CLAUDE.md 内容)。上下文通过两个独立的管道注入:
171+
172+
### System Context(`src/context.ts:116`
173+
174+
```typescript
175+
export const getSystemContext = memoize(async () => {
176+
return {
177+
gitStatus, // git 分支、状态、最近提交(截断至 MAX_STATUS_CHARS=2000)
178+
cacheBreaker, // 仅 ant 用户的缓存破坏器
179+
}
180+
})
181+
```
182+
183+
- 使用 `lodash.memoize` 缓存——**整个会话期间只计算一次**
184+
- Git 状态快照包含 5 个并行 `git` 命令(branch、defaultBranch、status、log、userName)
185+
- `status` 超过 2000 字符时截断并附加提示使用 BashTool 获取更多信息
186+
- `systemPromptInjection` 变更时,通过 `getUserContext.cache.clear?.()` 清除所有上下文缓存
187+
188+
### User Context(`src/context.ts:155`
189+
190+
```typescript
191+
export const getUserContext = memoize(async () => {
192+
return {
193+
claudeMd, // 合并后的 CLAUDE.md 内容
194+
currentDate, // "Today's date is YYYY-MM-DD."
195+
}
196+
})
197+
```
198+
199+
- **CLAUDE.md 禁用条件**`CLAUDE_CODE_DISABLE_CLAUDE_MDS` 环境变量,或 `--bare` 模式(除非通过 `--add-dir` 显式指定目录)
200+
- `--bare` 模式的语义是"跳过我没要求的东西"而非"忽略所有"
201+
202+
### 注入位置
203+
204+
`src/query.ts:449`
205+
206+
```typescript
207+
// System Context 追加到 System Prompt 尾部
208+
const fullSystemPrompt = asSystemPrompt(
209+
appendSystemContext(systemPrompt, systemContext) // 简单拼接
210+
)
211+
```
212+
213+
User Context 通过 `prependUserContext()``src/utils/api.ts:449`)注入为 `<system-reminder>` 标签包裹的首条用户消息,放在所有对话消息之前。
214+
215+
## Attribution Header:计费与安全
216+
217+
每个 API 请求的 System Prompt 首块是 Attribution Header(`src/constants/system.ts:30`),包含:
218+
- **`cc_version`**:Claude Code 版本 + 指纹
219+
- **`cc_entrypoint`**:入口点标识(REPL / SDK / pipe 等)
220+
- **`cch=00000`**(NATIVE_CLIENT_ATTESTATION 启用时):Bun 原生 HTTP 层在发送前将零替换为计算出的哈希值,服务器验证此 token 确认请求来自真实 Claude Code 客户端
221+
222+
Header 始终 `cacheScope: null`——它因版本和指纹不同而变化,不适合缓存。
34223

35224
## CLAUDE.md:项目级知识注入
36225

@@ -49,10 +238,15 @@ Claude Code 的 System Prompt 不是一段写死的文本,而是根据当前
49238
└── /project/src/CLAUDE.md ← 子目录(模块特定)
50239
```
51240

52-
## 缓存策略
241+
加载逻辑在 `src/utils/claudemd.ts` 中的 `getClaudeMds()``getMemoryFiles()` 实现——从 CWD 向上遍历目录树,合并所有匹配的 CLAUDE.md 文件内容。
242+
243+
## 设计洞察:为什么是 `string[]` 而非单个 `string`
244+
245+
将 System Prompt 设计为数组而非单段文本,是为了 **缓存分块**
53246

54-
System Prompt 的 token 消耗不小(可能占总量的 30%+)。为了降低成本,系统使用了缓存机制:
247+
1. Anthropic Prompt Cache 以 **内容块**(TextBlock)为缓存单位
248+
2. 将 System Prompt 拆为多个块,可以让不变的部分(Intro、Rules)获得独立的缓存命中
249+
3. 如果是单个 `string`,任何一个字符变化(如日期更新)都会导致整个 System Prompt 的缓存失效
250+
4. `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` 标记允许 `splitSysPromptPrefix()` 精确地将静态区标记为 `scope: 'global'`,动态区不标记或标记为 `scope: 'org'`
55251

56-
- 不变的部分(基础人设、工具说明)可以跨请求复用
57-
- 变化的部分(git 状态、记忆文件)每次重新生成
58-
- 缓存节点的位置经过精心设计,最大化缓存命中率
252+
这是 Claude Code 在 token 成本优化上的核心设计——一次典型的 System Prompt 约 20K+ tokens,通过缓存分块可以节省 30-50% 的输入 token 费用。

0 commit comments

Comments
 (0)