03 - 系统提示词
系统提示词是 Agent 的"宪法"。它在每次 API 调用中随 system[] 字段送达模型,决定模型的身份、行为边界、可感知的环境状态。
读完你能回答
| 问题 |
|---|
| 系统提示词由哪些区块组成?为什么不是一段连续文本? |
| "静态 / 动态"的区分和 prompt cache 是什么关系? |
CLAUDE.md 在哪里进到系统提示词?按什么顺序合并? |
<system-reminder> 和 system[] 有什么区别? |
结构:一段"分层三明治"
系统提示词在 HTTP 请求体里是一个 system[] 数组。数组里每段文本是一块,各自职责不同,而且带不同的 cache_control 标记。把稳定的放前面、易变的放后面,是为了让 Anthropic API 的前缀缓存能最大限度命中。
缓存边界(虚线)之前的部分一旦固定就几乎不变,第二次及以后的请求能直接复用;之后的部分每次会话可能不同,需要重新处理。
| 区块 | 属性 | 典型内容 |
|---|---|---|
| 身份声明 | 静态 | "You are Claude Code..." 自我介绍 |
| 核心行为准则 | 静态 | 工具使用规范、安全要求、输出风格 |
| 任务执行规范 | 静态 | 拆解任务、维护待办、提交代码 |
| 环境信息 | 动态 | cwd、平台、Shell、OS 版本、Git 状态 |
| 会话元信息 | 动态 | 模型标识、知识截止日期、当前日期 |
| 项目规则 | 动态 | 逐级合并的 CLAUDE.md |
| Skills 清单 | 动态 | 当前可用 Skills 与触发条件 |
展开一次真实 system[] 数组的骨架
{
"system": [
{
"type": "text",
"text": "You are Claude Code, Anthropic's official CLI for Claude. ...",
"cache_control": { "type": "ephemeral" }
},
{
"type": "text",
"text": "# 核心行为准则\n- Prefer editing existing files to creating new ones.\n- Do not add comments unless the WHY is non-obvious.\n- ...",
"cache_control": { "type": "ephemeral" }
},
{
"type": "text",
"text": "# 环境\nPrimary working directory: /Volumes/kingston/code/claude-code-learn\nPlatform: darwin\nShell: zsh\nOS Version: Darwin 25.3.0\nToday's date is 2026-04-20."
},
{
"type": "text",
"text": "# 项目规则\n<来自 ~/.claude/CLAUDE.md 与 <repo>/CLAUDE.md 合并后的内容>"
},
{
"type": "text",
"text": "# 可用 Skills\n- simplify: ...\n- less-permission-prompts: ...\n- ..."
}
]
}值得看一眼的细节:
- 静态三段打上
cache_control: ephemeral,动态部分不打 ephemeral缓存 TTL 约 5 分钟,停顿过久需要重建前缀缓存- 没有
cache_control的段不影响缓存,但会被"推到"缓存边界之后
缓存:为什么顺序这么关键
Anthropic API 的 prompt cache 是纯前缀匹配。哪怕只有一个字节不一样,缓存就从那个字节开始失效。
| 前缀状态 | 后续内容 | 命中? |
|---|---|---|
| 第 1 到 N byte 完全相等 | 任意 | ✓ 命中 |
| 第 K byte 不同(K ≤ N) | 之前的部分相同 | 只命中前 K-1 byte,之后全重算 |
| 距上次命中超过 TTL | 任意 | × 未命中,重建 |
这就是为什么不能把动态信息(当前日期、cwd)放在静态规则之前——只要这些内容稍一变化,后面所有静态规则的缓存都作废。
展开一个"放错位置"的反例
❌ 错误排法:
[动态] 当前日期: 2026-04-20
[动态] 工作目录: /repo
[静态] 身份: You are Claude Code...
[静态] 行为准则: ...
结果:日期一变(每天一次),整个 system[] 前缀全部失效,所有静态规则重新计费。✓ 正确排法(Claude Code 采用的):
[静态] 身份: You are Claude Code...
[静态] 行为准则: ...
[静态] 执行规范: ...
←← cache boundary
[动态] 当前日期
[动态] 工作目录
[动态] 项目规则
[动态] Skills 清单
结果:日期/目录/规则变动只影响动态段,静态前缀持续复用。动态区块来源
| 区块 | 数据来源 |
|---|---|
| 环境信息 | 进程环境变量、uname、pwd、git status |
| 会话元信息 | 运行时配置 + 系统时钟 |
| 项目规则 | ~/.claude/CLAUDE.md → <repo>/CLAUDE.md → 子目录 CLAUDE.md 合并 |
| Skills 清单 | 扫描 ~/.claude/skills/ 与项目 .claude/skills/ |
CLAUDE.md 的合并顺序
合并从外到内,更具体的覆盖更泛的。冲突时以最内层为准。
展开一个合并示例
假设全局偏好用中文,项目要求英文 commit:
# ~/.claude/CLAUDE.md
- 日常沟通使用简体中文
- Commit message 使用英文# <repo>/CLAUDE.md
- 分支命名必须以 feat/ 或 fix/ 开头
- 测试必须连真实数据库注入 system[] 的"项目规则"段是两者拼接,后者在下:
[来自 ~/.claude/CLAUDE.md]
- 日常沟通使用简体中文
- Commit message 使用英文
[来自 <repo>/CLAUDE.md]
- 分支命名必须以 feat/ 或 fix/ 开头
- 测试必须连真实数据库如果两份都写了"commit message 用 X",后者(项目级)生效。
system[] vs <system-reminder>
系统提示词只在 API 的 system[] 字段里。运行过程中若需要"打断"式提示(例如"Skills 列表变了""工具调用有更新"),会通过 <system-reminder> 标签注入到 user 消息里,而不是修改 system[]。
| 载体 | 可缓存性 | 生命周期 | 用途 |
|---|---|---|---|
system[] |
可缓存前缀 | 整个会话 | 身份、规则、环境 |
<system-reminder> |
随 user 消息传输,不命中前缀缓存 | 单次 turn | 临时提醒、状态变更 |
展开一次 system-reminder 的真实形态
它不是独立消息,而是塞进 user content 里的一段 XML 标签:
{
"role": "user",
"content": [
{ "type": "text", "text": "<system-reminder>\nSkills 列表有更新:新增 /deploy-staging\n</system-reminder>" },
{ "type": "text", "text": "帮我部署一下" }
]
}模型被训练识别 <system-reminder> 标签并把它当作系统侧的指示而非用户的话。但因为它在 user 消息里,每次 turn 都会重新传输,不享受前缀缓存折扣。
这正是它的设计意图:用它做短暂提醒,不用它做长期规则。
设计意图
把规则写死在系统提示词里,而不是每次用户消息前拼接,是出于两个考虑:
| 动机 | 解释 |
|---|---|
| 缓存友好 | 静态前缀稳定 = 缓存命中率高,延迟和成本都下来 |
| 权限清晰 | 用户消息可被视作"不可信输入",系统提示词是"可信规则";把敏感约束放在 system[] 降低 prompt injection 风险 |