Harness Engineering
第8章 System Prompt 分层设计
第8章 System Prompt 分层设计
“A prompt is not a string — it’s an architecture.” — Claude Code team
“System Prompt 承载的是 Agent 的灵魂。灵魂的复杂度决定了能力的上限,但也决定了维护的难度。” —— 杨艺韬
本章要点
- System Prompt 是架构问题,不是文案问题——要像写代码一样严谨
- 五层模型:Base Personality / Role / Tools / Context / Dynamic Rules
- 三种关注点分离:静态指令 / 动态上下文 / 用户自定义——三者生命周期完全不同
- CLAUDE.md 模式——用户自定义的优雅解法,值得在所有 Agent 平台借鉴
- Prompt Caching 对齐——缓存友好的分层设计可节省 70%+ 成本
- 像代码一样治理:版本控制 + 回归测试 + A/B + 变更审计
- 六大反模式:巨石 Prompt / 指令矛盾 / 过度变更 / Token 失控 / 缺乏可观测 / 责任模糊
8.1 System Prompt 不是一段字符串
很多开发者第一次接触 Agent 开发时,system prompt 是这样写的:一个字符串常量,塞在代码里,想到什么加什么,越写越长,最后变成一坨没人敢动的文本。改一个词,三个功能崩了。加一条规则,和前面的指令冲突了。
这是 prompt 的泥球架构——和代码世界里的 Big Ball of Mud 如出一辙。
真实的生产级 Agent 系统不会这样做。如果你去读 Claude Code 的源码,会发现它的 system prompt 是一个精心设计的多层架构,由十几个模块在运行时组装而成。每个模块有明确的职责边界,静态内容和动态内容严格分离,用户自定义和系统默认互不干扰。
System prompt 是一个架构问题,不是一个文案问题。
graph TD
subgraph Assembly["运行时 Prompt 组装"]
L5["Layer 5: 动态规则<br/>(system-reminder, hooks)"] -.->|"对话中途注入"| Final
L4["Layer 4: 上下文注入<br/>(git status, CLAUDE.md, 记忆)"] --> Final
L3["Layer 3: 工具定义<br/>(Read, Edit, Bash...)"] --> Final
L2["Layer 2: 角色指令<br/>(任务规范, 安全协议)"] --> Final
L1["Layer 1: 基础人格<br/>(身份, 能力边界)"] --> Final["最终 System Prompt"]
end
Final --> LLM["发送给 LLM"]
style L1 fill:#dbeafe,stroke:#3b82f6
style L2 fill:#e0e7ff,stroke:#6366f1
style L3 fill:#f3e8ff,stroke:#a855f7
style L4 fill:#fef3c7,stroke:#f59e0b
style L5 fill:#fee2e2,stroke:#ef4444
本章的目标,就是把这个架构拆清楚。
8.2 分层模型:从人格到动态规则
一个设计良好的 system prompt 可以抽象为五个层次,从底层到顶层依次是:
第一层:Base Personality(基础人格层)
这是 Agent 最核心的身份定义。它回答一个问题:你是谁? 包括名字、角色定位、基本行为准则、沟通风格。这一层极少变化,可能整个产品生命周期只改动几次。
你是 Claude,由 Anthropic 开发的 AI 助手。
你诚实、有帮助、无害。
当你不确定时,你会明确说明。
看起来平平无奇,但这一层承担的是”锚定”功能。后续所有层次的指令都建立在这个基础之上。如果基础人格层定义了”你要诚实”,后面的角色指令就不应该让 Agent 编造信息。
变更频率:极低(一年 1-2 次) 所有者:通常由模型提供商或产品负责人决定 Prompt Caching 友好度:⭐⭐⭐⭐⭐(极稳定,天然可缓存)
第二层:Role Instructions(角色指令层)
在基础人格之上,根据具体应用场景定义 Agent 的专业角色。同一个基础人格可以适配不同的角色指令。
Claude Code 的角色指令包括:你是一个编程助手,你在命令行环境中运行,你的任务是帮助用户完成编码工作。这些指令界定了 Agent 的能力范围和行为预期。
你是 Claude Code,一个运行在用户终端中的交互式编程代理。
你的工作环境信息:操作系统、shell、当前工作目录。
你应该完整地完成任务——不要过度设计,也不要半途而废。
Tone and style:
- Short and concise responses
- No emojis unless requested
- Reference file paths as file_path:line_number
角色指令层的变更频率高于人格层,但仍然是相对稳定的。它通常随着产品版本迭代而调整。
变更频率:低(每个产品版本 1-2 次) 所有者:产品团队 + 资深工程师 Prompt Caching 友好度:⭐⭐⭐⭐(稳定,按版本缓存)
第三层:Tool Definitions(工具定义层)
这一层描述 Agent 可以使用哪些工具、每个工具的参数格式和使用约束。工具定义层本身是半动态的——工具集合可能随着用户配置或权限变化。
关键原则:工具定义不仅包括”能做什么”,还必须包括”什么时候该用”和”什么时候不该用”。比如 Claude Code 的文件编辑工具明确指出”你必须先用 Read 工具读过文件才能编辑”,文件搜索工具则强调”不要用 Bash 跑 grep 命令,用专用的 Grep 工具”。
## Edit Tool
Performs exact string replacements in files.
Usage:
- You must use the `Read` tool at least once in the conversation
before editing. This tool will error if you attempt an edit
without reading the file.
- The edit will FAIL if `old_string` is not unique in the file.
Either provide a larger string with more surrounding context
to make it unique or use `replace_all`.
- ALWAYS prefer editing existing files. NEVER write new files
unless explicitly required.
这些使用约束就是工具层的 prompt 架构设计,它们直接影响 Agent 的行为质量。
变更频率:中(每个功能迭代) 所有者:平台工程团队 Prompt Caching 友好度:⭐⭐⭐(基础工具稳定,可选工具动态)
第四层:Context Injection(上下文注入层)
这是真正动态的部分。每次对话开始时,系统根据当前状态注入一系列上下文信息:当前日期、git 状态、项目结构、用户偏好、之前的对话摘要等。
Claude Code 在每次交互中会注入:当前工作目录、git 分支和最近提交、操作系统和 shell 环境信息。这些都不是写死在 prompt 模板里的,而是运行时实时采集后拼装进去的。
# Environment
- Primary working directory: /Users/dev/my-project
- Is a git repository: true
- Platform: darwin
- Shell: zsh
- OS Version: Darwin 23.6.0
# gitStatus
Current branch: main
Status:
M src/app.ts
?? src/new-feature.ts
Recent commits:
a0b32bd book(harness): ch17 rewrite
ef3980f book(harness): ch20 rewrite
上下文注入层的设计难点在于取舍——不是所有可用信息都该注入。每多注入一段文本,都在消耗宝贵的 token 预算。
变更频率:每次会话 所有者:平台工程团队(定义字段),业务逻辑(填充数据) Prompt Caching 友好度:⭐(几乎不可缓存——必须隔离)
第五层:Dynamic Rules(动态规则层)
最顶层是根据特定条件触发的规则。比如用户通过 CLAUDE.md 文件定义的项目级指令,或者根据当前对话内容动态加载的专项规则。
<system-reminder>
The user is working on a production database migration.
Be extra careful with any SQL modification suggestions.
Require explicit user confirmation before any DROP/TRUNCATE.
</system-reminder>
这一层最灵活,也最容易出问题。动态规则可能和底层指令冲突,可能彼此矛盾,可能因为注入时机不对而被模型忽略。因此动态规则层需要格外关注优先级和冲突消解机制——这也是下一章的重点内容。
变更频率:每次会话或每轮对话 所有者:用户(CLAUDE.md)+ 系统(hooks) Prompt Caching 友好度:不可缓存
五层的对比矩阵
| 维度 | 人格 | 角色 | 工具 | 上下文 | 动态规则 |
|---|---|---|---|---|---|
| 变更频率 | 极低 | 低 | 中 | 每会话 | 每轮 |
| 所有者 | 模型商 | 产品 | 平台工程 | 平台+业务 | 用户+系统 |
| Prompt Caching | ✅✅✅ | ✅✅ | ✅ | ❌ | ❌ |
| 测试覆盖要求 | 高 | 高 | 极高 | 中 | 中 |
| 版本控制必需 | ✅ | ✅ | ✅ | ❌ | ❌ |
8.3 Claude Code 的 Prompt 模块化实践
让我们以 Claude Code 为具体案例,看看分层模型如何落地。
Claude Code 的 system prompt 并非一个完整的文本文件,而是由多个功能模块在运行时组装而成。通过分析其源码,可以识别出以下关键模块:
| 模块名称 | 职责 | 层次 | Token 范围 | 缓存 |
|---|---|---|---|---|
| Identity | 身份声明、模型信息 | 基础人格 | ~200 | ✅ |
| Environment | OS、shell、cwd 等运行环境 | 上下文注入 | ~100 | ❌ |
| Tool Descriptions | 每个工具的描述和约束 | 工具定义 | ~5000 | ✅ |
| Task Guidelines | 完成任务的通用方法论 | 角色指令 | ~800 | ✅ |
| Tone & Style | 沟通风格要求(简洁、不用 emoji) | 角色指令 | ~300 | ✅ |
| Output Efficiency | 减少不必要输出的规则 | 角色指令 | ~150 | ✅ |
| Task Tools | TaskCreate/TaskUpdate 指南 | 角色指令 | ~400 | ✅ |
| Git Protocols | Git 操作的详细行为规范 | 动态规则 | ~2000 | ⚠️条件加载 |
| CLAUDE.md | 用户/项目级自定义指令 | 动态规则 | 0-5000 | ❌ |
| Memory Index | 跨会话长期记忆索引 | 上下文注入 | 0-1000 | ❌ |
| System-Reminder | 运行时动态提示 | 动态规则 | 0-500 | ❌ |
每个模块是一个独立的文本片段,有自己的维护者和变更节奏。Identity 模块可能一年改一次,Environment 模块每次对话都重新生成,Git Protocols 模块随着最佳实践积累不断完善。
这种模块化设计带来的好处是显而易见的:
- 独立演进:改动 Git 规范不会意外影响工具描述。
- 条件加载:如果用户没有 git 仓库,Git Protocols 模块可以不加载。
- 可测试性:可以针对单个模块做单元测试,验证它是否正确引导了模型行为。
- 可复用性:Tone & Style 模块可以在不同产品之间共享。
- 缓存友好:稳定模块可以单独作为缓存 block,动态模块隔离出来。
组装的顺序原则
模块的组装顺序不是随意的——遵循”稳定性从上到下递减”:
1. Identity ← 最稳定(缓存友好)
2. Role Instructions
3. Tone & Style
4. Task Guidelines
5. Task Tools
6. Tool Descriptions ← 次稳定
7. Git Protocols ← 条件加载
8. Memory Index ← 会话级
9. CLAUDE.md
10. Environment ← 最动态(不缓存)
11. System-Reminder ← 运行时注入
这个顺序让前面的内容可以被长期缓存,后面动态变化的内容不影响前面的命中。
8.4 关注点分离:静态、动态与用户自定义
prompt 架构的核心设计原则是关注点分离。具体来说,需要把内容按三个维度拆分:
静态指令(Static Instructions)
不随对话变化的部分。身份定义、角色说明、通用行为准则、输出格式要求——这些写一次,所有用户所有对话都一样。静态指令应该存储在代码仓库中,随产品版本一起发布。
管理方式:
- 存储在 code repo
- 走 PR/review 流程
- 有回归测试
- 发布时统一部署
动态上下文(Dynamic Context)
每次对话开始时实时生成的部分。当前时间、环境信息、会话状态、相关文件内容摘要。动态上下文由 harness 层的代码在运行时采集和注入。
管理方式:
- 运行时生成
- 有 fallback(采集失败不阻塞)
- Token 预算硬限制
- 不进入 Prompt Cache
用户自定义(User Customization)
由最终用户或项目维护者提供的指令。这是三者中最不可预测的部分——你无法控制用户会写什么。
管理方式:
- 受信任度低——必须有容错
- 可能和系统指令冲突——需要优先级
- 可能包含敏感信息——需要过滤
- 大小要封顶——防止滥用
为什么分离如此重要?因为每一类内容的生命周期、变更频率和质量保障方式完全不同:
- 静态指令需要 code review,需要 prompt 回归测试。
- 动态上下文需要运行时校验,需要容错处理。
- 用户自定义需要优先级机制和安全过滤。
把它们混在一起,就像把配置文件、环境变量和用户输入全部硬编码到同一个函数里——调试噩梦。
三维分离的可视化
graph TD
Assembly[Prompt 组装器]
Assembly --> S[静态模块<br/>from code repo]
Assembly --> D[动态上下文<br/>from runtime collectors]
Assembly --> U[用户自定义<br/>from CLAUDE.md + settings]
S --> SA[Identity]
S --> SB[Role]
S --> SC[Tools]
S --> SD[Style]
D --> DA[Environment]
D --> DB[Git State]
D --> DC[Memory Index]
U --> UA[Project CLAUDE.md]
U --> UB[User CLAUDE.md]
U --> UC[settings.json hooks]
SA & SB & SC & SD --> Cache[Prompt Cache<br/>命中]
DA & DB & DC --> Fresh[每次重建<br/>不缓存]
UA & UB & UC --> PartialCache[稳定用户配置<br/>可缓存]
Cache --> Final[最终 Prompt]
Fresh --> Final
PartialCache --> Final
style S fill:#dbeafe,stroke:#3b82f6
style D fill:#fef3c7,stroke:#f59e0b
style U fill:#dcfce7,stroke:#22c55e
8.5 CLAUDE.md 模式:用户自定义的优雅解法
CLAUDE.md 是 Claude Code 引入的一个精巧设计,值得深入分析其工程思想。
核心思路很简单:在项目根目录放一个 CLAUDE.md 文件,其内容会被自动注入到 system prompt 中。但简单背后有一系列精心的设计决策:
四个设计决策
1. 层级覆盖机制
CLAUDE.md 可以存在于多个位置:
- 用户级:
~/.claude/CLAUDE.md——跨所有项目 - 项目级:项目根目录的
CLAUDE.md——当前项目 - 目录级:子目录的
CLAUDE.md——特定模块
内层配置可以覆盖外层配置,就像 CSS 的层叠规则或 Git 的 .gitignore 层级。
~/.claude/CLAUDE.md # 用户全局偏好
└─ /project/CLAUDE.md # 项目级(覆盖全局)
└─ /project/mobile/CLAUDE.md # 模块级(覆盖项目)
2. 声明式优先于命令式
CLAUDE.md 的内容是声明式的——“这个项目使用 TypeScript”、“提交消息用中文”、“不要修改 vendor 目录”。它描述约束和偏好,而不是编写执行流程。
# 项目约定
- 使用 TypeScript 严格模式
- 提交消息用中文,格式:type(scope): subject
- 不要修改 vendor/ 目录下的文件
- 测试使用 vitest,不要用 jest
3. 零侵入性
不需要修改任何核心代码,不需要了解 prompt 的内部结构。用户只需要会写 Markdown 就能定制 Agent 行为。这大幅降低了自定义的门槛。
4. 版本可控
CLAUDE.md 可以提交到 Git 仓库,团队成员共享同一套项目级约束。新成员 clone 仓库后自动获得一致的 Agent 行为——这是最容易被低估的价值。
平台工程的通用范式
从架构角度看,CLAUDE.md 模式解决了一个经典的平台工程问题:
如何在不暴露系统内部实现的前提下,给用户提供足够的定制能力?
答案是提供一个定义良好的注入点,配合明确的优先级规则。
这个模式具有高度的可迁移性。如果你在构建自己的 Agent 平台,完全可以借鉴这种设计:
def build_system_prompt(user_id, project_path, conversation):
layers = []
# === 静态层(可缓存)===
layers.append(load_static("identity.txt"))
layers.append(load_static("role_instructions.txt"))
layers.append(load_static("tool_definitions.txt"))
layers.append(load_static("tone_style.txt"))
# === 用户级自定义(次稳定)===
user_config = load_user_config(user_id) # ~/.agent/config.md
if user_config:
layers.append(user_config)
# === 项目级自定义(次稳定)===
project_config = find_project_config(project_path) # .agent.md
if project_config:
layers.append(project_config)
# === 动态上下文层(不缓存)===
layers.append(collect_environment(project_path))
layers.append(collect_git_state(project_path))
layers.append(load_memory_index(user_id, project_path))
# === Hooks 层 ===
for hook in load_hooks(user_id):
layers.append(execute_hook(hook))
# 组装
return "\n\n".join(layers)
同类模式横评
| 产品 | 用户自定义机制 | 核心特性 |
|---|---|---|
| Claude Code | CLAUDE.md 多层覆盖 | Markdown, 层级覆盖, Git 友好 |
| Cursor | .cursorrules | YAML 格式, 项目级 |
| Windsurf | .windsurfrules | Markdown, 项目级 |
| Continue | .continue/config.json | JSON, 配置+指令混合 |
| Zed AI | 内置 settings | GUI 配置 |
Claude Code 的 Markdown + 层级覆盖是被验证最平衡的方案——既保留了可读性,又支持细粒度控制。
8.6 模板组装:运行时拼装的工程细节
理解了分层模型之后,下一个问题是:这些层如何在运行时组装成最终的 system prompt?
最朴素的方式是字符串拼接。但生产级系统需要考虑更多:
条件化加载
不是所有模块在所有场景下都需要。如果当前任务不涉及 git 操作,加载 Git Protocols 模块就是在浪费 token。Claude Code 会根据当前工作目录是否是 git 仓库来决定是否注入 git 相关指令。
class PromptAssembler {
async assemble(context: Context): Promise<string[]> {
const blocks: PromptBlock[] = []
// 总是加载的基础模块
blocks.push(this.identity)
blocks.push(this.role)
// 条件加载
if (context.isGitRepo) {
blocks.push(this.gitProtocols)
}
if (context.hasDockerfile) {
blocks.push(this.dockerProtocols)
}
if (context.hasCI) {
blocks.push(this.ciProtocols)
}
// 用户偏好
if (context.userConfig) {
blocks.push(context.userConfig)
}
// 工具(按可用性)
for (const tool of context.availableTools) {
blocks.push(this.toolDescriptions[tool])
}
return blocks.map(b => b.content)
}
}
顺序敏感性
大模型对 prompt 中信息的位置是敏感的。一般来说:
- Primacy effect:越靠前的内容权重越高
- Recency effect:最末尾的内容也有较高关注度
- Lost in the middle:中间部分最容易被忽略
因此关键指令应该放在 prompt 的开头或结尾。次要约束放中间。
分隔符设计
模块之间需要清晰的视觉分隔,帮助模型理解结构边界。常见做法:
# Markdown 标题(最常用)
# Identity
...
# Tool Usage
...
# XML 标签(结构化更强)
<identity>
...
</identity>
# 自定义分隔线
═══ IDENTITY ═══
...
═══ TOOLS ═══
Claude Code 使用 XML 风格的标签(如 <system-reminder>、<system>)来标注动态注入的内容块,让模型能够区分核心指令和运行时上下文。
Token 预算管理
这是模板组装中最务实的考量。system prompt 和对话历史共享同一个上下文窗口。prompt 越长,留给对话的空间越小。一个 200K 窗口的模型,如果 system prompt 占了 30K,对话就只剩 170K(还要预留输出空间)。
实践中的常见策略:
class TokenBudgetManager {
private maxPromptTokens = 30_000 // 上限
private compactThreshold = 25_000 // 超过就压缩
async fitInBudget(blocks: PromptBlock[]): Promise<PromptBlock[]> {
let total = blocks.reduce((s, b) => s + countTokens(b.content), 0)
if (total <= this.compactThreshold) return blocks
// 策略 1: 降级低优先级模块
blocks = blocks.map(b => {
if (b.priority === "low" && b.compactVersion) {
return { ...b, content: b.compactVersion }
}
return b
})
total = blocks.reduce((s, b) => s + countTokens(b.content), 0)
if (total <= this.maxPromptTokens) return blocks
// 策略 2: 丢弃可选模块
blocks = blocks.filter(b => b.priority !== "optional")
total = blocks.reduce((s, b) => s + countTokens(b.content), 0)
if (total <= this.maxPromptTokens) return blocks
// 策略 3: 告警并截断
logger.warn(`Prompt budget exceeded: ${total}, truncating dynamic context`)
return this.truncateDynamic(blocks)
}
}
核心策略清单:
- 设定 system prompt 的 token 上限(比如不超过窗口的 15%)
- 对动态内容做截断和摘要——git log 只取最近 5 条,文件列表只展示前两层目录
- 对低优先级模块实施”压缩模式”——当 token 紧张时,用精简版替代完整版
- 监控各模块的 token 占比,识别”膨胀”的模块
8.7 Prompt Caching 对齐:为缓存而设计
2024 年 Anthropic 推出的 Prompt Caching 机制,对 prompt 分层架构提出了新要求。
缓存命中的铁律
- 前缀匹配——Cache 是按前缀匹配的。前面改了,后面全部失效。
- 字节级一致——完全一致才能命中。
- 5 分钟 TTL——最后访问后 5 分钟失效。
这意味着:稳定的内容必须在前,动态内容必须在后。
缓存友好的分层
┌─ System Prompt ──────────────────────┐
│ ┌─ Cache Block 1 ─────────────────┐ │
│ │ Identity (static, 200 tokens) │ │
│ │ Role (static, 800 tokens) │ │
│ │ Tone & Style (static, 300 tokens) │ │ ← cache_control 标记
│ │ Tools (static, 5000 tokens) │ │
│ │ Task Guidelines (static, 500) │ │
│ └──────────────────────────────────┘ │
│ │
│ ┌─ Cache Block 2 ─────────────────┐ │
│ │ User CLAUDE.md (semi-static) │ │
│ │ Project CLAUDE.md (semi-static) │ │ ← cache_control 标记
│ └──────────────────────────────────┘ │
│ │
│ ┌─ Uncached ──────────────────────┐ │
│ │ Environment (dynamic) │ │
│ │ Git State (dynamic) │ │ ← 不标记
│ │ Memory Index (dynamic) │ │
│ │ System-Reminder (dynamic) │ │
│ └──────────────────────────────────┘ │
└──────────────────────────────────────┘
缓存命中率优化策略
策略 1:Cache Block 粒度
每个 cache_control 标记都有开销。实践中 2-4 个 block 就够:
- Block 1: 静态系统内容
- Block 2: 用户配置
- Block 3(可选): 稳定的对话历史
策略 2:避免”小改动大失效”
如果在静态部分加一个换行,整个缓存就失效了。要严格控制静态内容的”无意义变动”:
// ❌ 容易失效
function buildStaticPrompt() {
return `${identity}\n${role}\n${new Date().getFullYear()} version` // 每年会变
}
// ✅ 稳定
function buildStaticPrompt() {
return `${identity}\n${role}` // 不要把动态信息混进静态
}
策略 3:合并稳定模块
把多个小静态模块合并成一个 cache block,减少 cache_control 标记的开销。
成本估算
假设一个 Agent:
- 系统 Prompt 静态部分 20K tokens
- 每轮用户消息 + 响应 5K tokens
- 对话 30 轮
不缓存成本(Opus $15/1M input):
30 轮 × (20K + 历史增长) tokens × $15/1M ≈ $15-30
带缓存成本:
第 1 轮: 20K × $18.75/1M (写缓存溢价) = $0.375
后续 29 轮: 20K × $1.50/1M (缓存读取) + 动态部分 × $15/1M
≈ 29 × $0.03 = $0.87
总计: ≈ $1.25,节省 90%+
Prompt Caching 改变了长对话 Agent 的商业模型——分层设计的稳定性回报,直接变成了成本回报。
8.8 把 Prompt 当代码管理
如果 system prompt 是架构,那它就应该享受和代码同等的工程待遇。
版本控制
所有 prompt 文本必须入版本管理。不是放在数据库里由产品经理在后台随便改,而是放在代码仓库里,走 PR 流程,有 review、有 changelog。
每一次 prompt 变更都应该能回答三个问题:
- 改了什么?(diff 可见)
- 为什么改?(commit message 说明)
- 效果如何?(关联的评估结果)
环境隔离
就像代码有 dev/staging/prod 环境,prompt 也应该有。新的 prompt 变更先在开发环境验证,通过评估后再部署到生产。
# prompt-versions.yaml
prompts:
identity:
dev: v1.2.3
staging: v1.2.2
prod: v1.2.1
role:
dev: v2.1.0
staging: v2.0.5
prod: v2.0.5
变更审计
生产环境的 prompt 变更必须有记录。当 Agent 行为出现异常时,第一件事就是查看最近的 prompt 变更。没有审计记录,排查就是大海捞针。
interface PromptChangelogEntry {
version: string
module: string
author: string
timestamp: Date
diff: string
reason: string
evalResults?: EvalMetrics
rolloutPlan: {
canary: number // 初始灰度比例
fullAt: Date // 全量时间
}
}
Prompt 目录结构
一种被验证有效的实践是 prompt 目录结构:
prompts/
├── base/
│ ├── identity.md # 基础人格
│ └── role.md # 角色指令
├── tools/
│ ├── file_read.md # 文件读取工具描述
│ ├── file_edit.md # 文件编辑工具描述
│ └── bash.md # 命令行工具描述
├── protocols/
│ ├── git.md # Git 操作规范
│ └── security.md # 安全相关规则
├── dynamic/
│ ├── environment_tpl.md # 环境注入模板
│ └── memory_tpl.md # 记忆注入模板
├── templates/
│ └── system_prompt.py # 组装逻辑
└── tests/
├── test_identity.py # 人格层测试
├── test_git_protocol.py # Git 规范测试
└── fixtures/ # 测试场景
每个 .md 文件是一个 prompt 模块,templates/ 放组装逻辑,tests/ 放评估用例。这种结构一目了然,新人上手成本极低。
跨团队协作模式
大型团队中,prompt 的不同层往往归属不同团队:
| 层 | 所有团队 | 审批流 |
|---|---|---|
| Base Personality | 产品 + Legal | 产品总监 + 法务 |
| Role Instructions | 产品团队 | PM |
| Tool Definitions | 平台工程 | 架构组 |
| Context Injection | 平台 + 数据 | SRE |
| Dynamic Rules | 用户 / 运营 | N/A (用户自主) |
这种分工有助于避免”所有人都能改系统 prompt”的混乱局面。
8.9 Prompt 的测试与评估
代码有单元测试,prompt 也应该有。但 prompt 测试和传统测试有一个本质区别:输出是非确定性的。同一个 prompt,同一个输入,模型可能给出不同的回答。
因此 prompt 测试更接近”评估”而非”断言”。核心方法包括:
行为测试(Behavioral Testing)
给定一个场景,验证 Agent 的行为是否符合预期。不是检查具体输出文本,而是检查行为特征。
例如,要测试 Git Protocols 模块是否生效:
- 输入:“帮我提交代码”
- 期望行为:Agent 应先运行
git status和git diff,而不是直接git commit - 验证方式:检查 Agent 的工具调用序列
class GitProtocolTest:
scenario = "help me commit the changes"
def verify(self, agent_trace: list[Action]) -> bool:
# Assert: first action is git status or git diff
assert agent_trace[0].tool in {"git status", "git diff"}
# Assert: commit is NOT first action
assert agent_trace[0].tool != "git commit"
# Assert: if commit happens, must come after status/diff
for action in agent_trace:
if action.tool == "git commit":
prior_tools = [a.tool for a in agent_trace[:agent_trace.index(action)]]
assert "git status" in prior_tools or "git diff" in prior_tools
return True
return False
回归测试(Regression Testing)
每次 prompt 变更后,跑一遍核心场景的测试集。如果新改动导致已有场景的通过率下降,就需要审慎评估。
维护一个”黄金测试集”:
- 50-100 个精心挑选的场景
- 覆盖所有关键行为
- 每次 PR 都跑一遍
- 通过率阈值(如 95%)作为 merge gate
A/B 测试
在生产环境中对比不同 prompt 版本的效果。随机将一部分流量分配给新 prompt,对比关键指标:
- 任务完成率
- 用户满意度
- 工具调用次数
- 重试率
- 平均响应 tokens
对抗测试(Red-Teaming)
专门构造试图”破坏”prompt 约束的输入。如果 prompt 规定”不要执行危险的 git 操作”,测试用例就应该包括各种引诱 Agent 执行 git push --force 的请求。
对抗测试覆盖:
- Prompt 注入攻击
- 角色扮演诱导
- 分步诱导(每步看起来无害,组合起来有害)
- 紧迫性压力(“紧急”、“不要问”)
评估框架骨架
class PromptEvalSuite:
def __init__(self, prompt_builder):
self.prompt_builder = prompt_builder
def eval_case(self, scenario, expected_behavior):
prompt = self.prompt_builder.build()
trace = run_agent(prompt, scenario)
return expected_behavior.check(trace)
def run_suite(self, cases):
results = {
"total": len(cases),
"passed": 0,
"failed": [],
"metrics": defaultdict(list),
}
for case in cases:
passed = self.eval_case(case.scenario, case.expected)
if passed:
results["passed"] += 1
else:
results["failed"].append(case.name)
results["pass_rate"] = results["passed"] / results["total"]
return results
关键指标不是 100% 通过率——那在非确定性系统中不现实。而是设定一个可接受的阈值(比如 95%),并监控趋势。如果通过率从 97% 掉到 92%,就需要排查原因。
8.10 六大反模式
最后,列举实践中最常见的 prompt 架构反模式,帮你避坑:
反模式一:巨石 Prompt(Monolithic Prompt)
现象:所有指令塞在一个字符串里,没有结构,没有分层。改动困难,测试不可能,冲突频发。这是最常见也最致命的反模式。
对策:按五层模型重构,每层独立文件,组装器负责拼接。
反模式二:指令矛盾(Conflicting Instructions)
现象:前面说”保持输出简洁”,后面又说”给出详细的解释和示例”。模型遇到矛盾指令时的行为是不可预测的——它可能遵循前者,可能遵循后者,可能试图折中,也可能完全忽略两者。
对策:
- 明确优先级。比如 Claude Code 用
IMPORTANT:前缀标注高优先级指令 - 用层级关系隐式建立优先级(动态规则层 > 角色指令层)
- 矛盾检测工具——扫描所有 prompt 模块,发现潜在冲突
反模式三:过度频繁变更(Over-churn)
现象:每周改一次 system prompt,每次都是大改。结果是没有任何稳定基线,无法做有效的评估对比,也无法积累关于”什么有效什么无效”的工程认知。
对策:不同层采用不同节奏。基础层级每季度审视一次,角色指令层按版本迭代,动态规则层可以按需调整但要有测试覆盖。
反模式四:忽视 Token 成本(Token Bloat)
现象:不断往 prompt 里加内容,从不做减法。某天突然发现 system prompt 占了上下文窗口的一半,对话能力严重退化。
对策:
- 定期审计各模块 token 消耗
- 设定每模块的 token 上限
- 每 PR 报告 token delta
反模式五:缺乏可观测性(No Observability)
现象:不知道当前生产环境跑的是哪个版本的 prompt,不知道某次 prompt 变更是什么时候部署的,出了问题查不到根因。
对策:
- Prompt 版本号嵌入到日志
- 每次请求记录使用的 prompt 版本
- 变更通知接入告警系统
反模式六:责任模糊(Ownership Crisis)
现象:所有人都能改系统 prompt——产品经理加一段、实习生加一段、老板随意加一段。最后没人知道为什么有这些指令,也不敢删。
对策:
- 按层分配所有者(参考 8.8 节跨团队协作模式)
- 每个模块有 CODEOWNERS
- 无 owner 的模块定期清理
8.10.1 实测:§8.3 Claude Code 的”模块化 prompt”在源码里就是 getSimple*Section() 函数链
§8.3 给出 11 个模块的表格——把它在 claude-code-main/src/constants/prompts.ts 里精确定位——
getSystemPrompt(tools, model, ...) 函数(line 444、实测)——返回 string[]、每个元素一个模块——核心是 line 564-578 的 array literal,按顺序拼装——
return [
// === Static content (cacheable) ===
getSimpleIntroSection(outputStyleConfig), // ← Identity
getSimpleSystemSection(), // ← System role
getSimpleDoingTasksSection(), // ← Task guidelines
getActionsSection(), // ← 10.8.1 提到的 git 安全协议在这里
getUsingYourToolsSection(enabledTools), // ← Tool descriptions
getSimpleToneAndStyleSection(), // ← Tone & Style
getOutputEfficiencySection(), // ← Output Efficiency
// === BOUNDARY MARKER - DO NOT MOVE OR REMOVE ===
...(shouldUseGlobalCacheScope() ? [SYSTEM_PROMPT_DYNAMIC_BOUNDARY] : []),
// === Dynamic content (registry-managed) ===
...resolvedDynamicSections,
].filter(s => s !== null)
对照 §8.3 表格、模块对应关系——
| 章节表格模块 | 源码函数 | 实测位置 |
|---|---|---|
| Identity | getSimpleIntroSection() | constants/prompts.ts:175 |
| System role | getSimpleSystemSection() | line 186 |
| Task Guidelines | getSimpleDoingTasksSection() | line 199 |
| Tool Descriptions | getUsingYourToolsSection(enabledTools) | line 269 |
| Tone & Style | getSimpleToneAndStyleSection() | line 430 |
| Output Efficiency | getOutputEfficiencySection() | line 403 |
| Git Protocols | getActionsSection() | line 255(ch10 §10.8.1 实测的 7 条 git 规则不直接在 prompts.ts 里、而在 BashTool/prompt.ts:88-94——getActionsSection 把 BashTool 的 prompt 引入,是引用而不是 inline) |
| Dynamic(CLAUDE.md / system-reminder / hooks) | getSystemRemindersSection / getHooksSection 等 | constants/prompts.ts:127-131 + resolvedDynamicSections 异步注入 |
SYSTEM_PROMPT_DYNAMIC_BOUNDARY 哨兵 在 line 575(实测、与 ch10 §10.8.1 已揭示的 line 114 SYSTEM_PROMPT_DYNAMIC_BOUNDARY = '__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__' 对应)——是 §8.7 “Prompt Caching 对齐:为缓存而设计” 的实际实现:字符串数组里插一个特殊字面量、然后 splitSysPromptPrefix() 按它切前后两段、前段贴 cache_control: ephemeral 后段不贴。
两条值得记住的物理事实——
prompts.ts22 个function get*Section()(实测 grep)——getSimpleIntroSection / getActionsSection / getUsingYourToolsSection / getSimpleToneAndStyleSection / getOutputEfficiencySection / getMcpInstructions / getKnowledgeCutoff / getShellInfoLine / getFunctionResultClearingSection / getBriefSection / getProactiveSection等——§8.3 抽象的”模块化 prompt”在源码里就是这 22 个函数 + getSystemPrompt 的 array filter 拼装机制——印证 §8.3 “模块化是真实的、不是图示”getSystemPrompt函数本身只有约 130 行(line 444-573)——整个 system prompt 的”装配指挥”代码极少——其余都是各 Section 函数的实现——这是 ch10 §10.8.1 测得的”总 prompt 文件 6271 行 / 22 个 Section 函数”在装配层的具体体现:模块化让装配代码极简、复杂度全在各模块内部
串联 ch10 §10.8.1 + §10.4 + §9.4.4——
- ch10 §10.8.1 揭示
SYSTEM_PROMPT_DYNAMIC_BOUNDARY+<system-reminder>是动态注入语义根基 - ch10 §10.4 揭示 prompt 工程量级(22 个 section 函数 / 6271 行)
- ch09 §9.4.4 揭示 7 条 git 规则在 BashTool/prompt.ts
- 本节揭示 §8.3 模块化在 getSystemPrompt 的 array literal + boundary marker 协作
——四节合起来给出 Claude Code prompt 架构的完整源码地图:装配(getSystemPrompt 130 行)+ 模块(22 个 Section 函数 6271 行)+ 缓存机制(DYNAMIC_BOUNDARY 字面量切割)+ 工具 prompt 分散(BashTool/PowerShellTool/GrepTool 各自 prompt.ts)。
8.11 本章小结:prompt 架构的七条原则
System prompt 的分层设计,本质上是把软件工程中久经验证的架构原则应用到 prompt 领域:
- 分层:每一层有明确的职责和稳定性等级
- 模块化:独立的模块可以独立演进、独立测试
- 关注点分离:静态指令、动态上下文、用户自定义,三者各有其管理方式
- 缓存对齐:Prompt Caching 奖励稳定设计——分层本身就是成本优化
- 版本控制:prompt 是代码,不是随手写的便签
- 可测试性:行为评估框架替代简单的字符串比对
- 责任明确:每一层、每一模块都有所有者
这些不是高深的理论,而是工程常识在新领域的应用。但正是因为 prompt 看起来”只是一段文本”,太多人忽视了对它施加工程纪律的必要性。
核心口号:
Prompts are not strings. They are architecture.
像写代码一样写 prompt,像管代码一样管 prompt。
下一章,我们将深入探讨分层设计中最棘手的子问题:当多层指令发生冲突时,优先级如何裁决? 这就是第 9 章”指令优先级”要解决的核心问题。
延伸阅读:Prompt 架构的演化史
“Prompt 作为架构”这个观念、经历了快速演化。2022 年、Prompt 还是”随手写的字符串”——开发者在代码里直接拼接字符串、没有版本管理、没有测试。2023 年、一些前沿团队开始把 Prompt 从代码里抽出来——放进独立的文件、用模板引擎管理、像对待 SQL 一样对待 Prompt。2024 年、完整的”Prompt 工程化”体系浮现——分层设计(system / developer / user 三层)、版本管理(类似 Git)、A/B 测试、性能监控——Prompt 从”字符串”进化为”软件组件”。2025-2026 年、开始出现”Prompt 架构”这个更高层的概念——讨论如何让多层指令协同、如何处理冲突、如何保持一致性。
这段演化史、本质上是”任何复杂软件都要经历的成熟化过程”。从第一版 SQL(字符串拼接)、到 ORM(结构化查询)、到 migration(版本化 schema)——SQL 用了 30 年走完这段路;Prompt 可能只需要 5 年——因为前人已经趟过很多坑、经验可以直接借鉴。作为读者、你赶上 Prompt 架构成熟化的关键期——现在投入、未来几年都能受益。
延伸阅读:多层指令的冲突处理
本章反复提到”多层 Prompt 之间可能冲突”——这是 Prompt 架构最棘手的问题之一。典型场景——“system prompt 要求简洁、developer prompt 要求详细、user 问’请解释清楚’“——LLM 该听谁的?。传统软件里、这种冲突通过”作用域 + 优先级”明确解决(比如 CSS 的 specificity);Prompt 里、LLM 是”感性的裁判”、可能给出任何答案。
解决冲突的几种思路——第一、“显式声明优先级”(prompt 里写”如果 user 要求和 system 冲突、优先遵守 system”);第二、“分层物理隔离”(system 指令硬编码不暴露给 user、user 无法直接覆盖);第三、“运行时裁决层”(在 LLM 外面加一个 policy checker、审查 LLM 输出是否违反上层指令)。每种方法都有优劣——方法一简单但不可靠(LLM 可能不听话)、方法二安全但不灵活(某些场景 user 应该能覆盖)、方法三复杂但最稳健。大规模 Agent 系统往往需要三种结合使用。《OpenClaw 源码》第 13 章、《LangGraph 源码》第 8 章都讨论过类似的”多层安全”——对比阅读能帮你建立全面认识。
延伸阅读:Prompt 的测试与评估
Prompt 工程化的一个核心挑战——如何测试 Prompt。传统代码的测试是”输入 X、期望输出 Y、断言相等”——简单直接。Prompt 的测试是”输入 X、期望输出满足某些属性”——“满足属性”本身就难定义(用户体验、正确性、一致性都要考虑)。目前业界的几种实践——“示例对比测试”(收集典型输入输出对、回归测试 LLM 响应)、“LLM-as-judge”(让另一个 LLM 评判输出质量)、“属性检查”(比如”不应该有中文脏话”、“必须包含引用”、“JSON 格式必须合法”)、“A/B 在线测试”(生产环境两个版本对比真实用户反馈)。
这些测试方法各有局限——示例对比可能漏掉新情况、LLM-as-judge 本身可能偏见、属性检查覆盖不全、A/B 测试周期长。真实项目往往组合使用——开发阶段用示例对比 + LLM-as-judge 做快速反馈、上线前用属性检查做安全 gate、上线后用 A/B 测试持续优化。这种”多种方法组合使用”的策略、是 Prompt 测试的成熟姿态——不依赖任何单一方法。
延伸阅读:System Prompt 与模型能力的耦合
一个容易忽略的话题——System Prompt 和 LLM 模型是深度耦合的。为 GPT-4 优化的 prompt、换到 Claude 3 上可能效果骤降;为 Claude Opus 写的 prompt、在 Haiku 上可能效果不够。原因是不同模型的”指令遵循风格”不同——Claude 更喜欢 XML 标签、GPT 更喜欢 markdown、Llama 对长 system prompt 敏感、Gemini 对示例顺序敏感。这种差异、让”模型无关的 prompt”成为一个美好但未实现的理想。
面对这种耦合、生产级 Agent 系统的对策——“每个模型维护独立的 prompt 集”——针对 GPT 有一套、针对 Claude 有一套、针对 Llama 有一套——框架层面做路由(根据模型选对应的 prompt)。这增加了维护成本、但保证了各模型上的质量。LangChain 的 Partner Packages(见《LangChain 源码》第 17 章)、就是在不同提供商之间抽象差异——理论上可以屏蔽 prompt 差异、实际上生产应用还是需要针对各模型定制。这是 AI 应用开发里一个长期无解的问题——只能通过工程手段管理。
延伸阅读:Prompt 架构对团队协作的影响
Prompt 架构化之后、团队协作模式也会改变。以前——“Prompt 是个人创作”——谁写就谁负责、别人看不懂、改起来没头绪。Prompt 架构化后——“Prompt 是团队资产”——有分层规范、有命名约定、有版本历史——新成员能快速上手、不同人能协作修改。
这种变化、和 SQL 在数据库领域的变化类似——早期 SQL 是”某个 DBA 的私房菜”、现在 SQL 是”团队共享的数据资产”。要完成这种变化、需要几件事——团队里有 prompt 架构师(设计整体分层)、有 prompt 工程师(实现具体 prompt)、有 prompt reviewer(审查 prompt 变更)——就像代码有 architect、engineer、reviewer 一样。这些角色在小团队里可能由同一个人承担、大团队里会专业化分工——和软件工程的成熟路径完全一致——历史不会重演、但会押韵。
延伸阅读:Prompt 与组织知识的关系
一个组织的 prompt 库、是这个组织的”数字化知识”。它反映了组织对”什么问题该怎么回答”的集体智慧——法务团队的 prompt 沉淀了他们对合规的理解、客服团队的 prompt 沉淀了他们对用户沟通的技巧、技术团队的 prompt 沉淀了他们对代码审查的标准。这些 prompt 加在一起、构成组织独特的”集体知识资产”——这是组织最难被竞争对手复制的部分。
意识到这一点、Prompt 就不只是”工具”、而是”战略资产”——需要像对待机密文档一样对待它、需要持续投资维护它、需要让组织的最优秀人才参与共建。聪明的公司、已经在把”最佳实践”编码为 prompt、让每个员工都能借助 AI 享受资深专家的判断水平——这是 AI 时代”组织能力放大”的具体落地。OpenClaw 的 Skill 系统(见《OpenClaw 源码》第 16 章)、就是在技术层面支持这种”组织知识资产化”——值得每个企业读者深入研究。
延伸阅读:Prompt 架构的未来
展望未来——Prompt 架构会走向何方?几个可能方向。第一、“可视化 IDE”——像 VSCode 之于代码一样、出现专门的 Prompt IDE——带语法高亮、自动补全、跨 prompt 跳转、实时 preview。第二、“形式化规约”——用形式化语言描述 prompt 应该满足的属性、用自动工具验证。第三、“Prompt-Code 一体化”——Prompt 和代码混合编辑、互相引用、一起部署。第四、“自动优化”——给定评估函数、让工具自动搜索最优 prompt(DSPy 已经在做)。
这些方向的哪个会成真、现在还难说。但可以确定的是——Prompt 会越来越像传统软件组件、工程化程度越来越高、个人手工写 prompt 的时代正在过去、工具辅助写 prompt 的时代正在到来。作为 AI 工程师、你需要提前为这个趋势做好准备——投资于”prompt 工程化思维”、而不是停留在”字符串拼写技巧”——这是决定你在 AI 时代走多远的关键分水岭。
延伸阅读:Prompt 的”文化负载”
一个经常被忽略的话题——Prompt 是有文化负载的。英文 prompt 在英文 LLM 上效果最好、但在处理中文输入时可能效果打折;中文 prompt 在中文场景效果好、但某些精确指令用英文更清晰。不同文化背景的 user、对同一个 LLM 响应的反应可能完全不同——直接的英美风格可能让东亚用户觉得”不够礼貌”、委婉的东亚风格可能让西方用户觉得”不够直接”。
面对这种文化差异、设计跨地区 Agent 系统时需要额外考虑”本地化 prompt”——不只是翻译、而是根据目标文化调整语气、示例、判断标准。这让”全球化 Agent”成为一个比”全球化普通软件”更复杂的工程——本地化的维度多了好几个。这也是 AI 时代产品经理的新挑战——需要懂技术也懂文化、能把不同文化的沟通习惯编码到 prompt 里——这是一种全新的产品能力。
延伸阅读:Prompt 工程的职业前景
“Prompt 工程师”作为一个新兴职业、在 2023-2024 年一度被热炒——有公司给出 30 万美元年薪招”Prompt Engineer”。到 2025-2026 年、这个职业的形态开始分化——纯粹的”写 prompt”逐渐被工具化(DSPy、自动优化)、但”懂 prompt 架构、懂 LLM 心理、懂业务”的综合型人才依然稀缺。未来几年、可能不会再有”Prompt Engineer”这个专门职位、但”懂 Prompt 的产品工程师/AI 工程师”会成为主流。
这种演化、和历史上的一些专门技能一致——早期”网页设计师”、“SEO 专家”、“DevOps 工程师”都经历过”先独立成职位、再融入其他职位”的过程。核心技能不会消失、但会融入更广的职责范围。作为工程师、与其追逐”Prompt Engineer”这个标签、不如把 prompt 工程作为自己综合能力的一部分——和架构能力、编程能力、产品能力一起、构成 AI 时代的复合型工程师——这种定位、比专业化的单一技能更抗风险。
延伸阅读:为 Prompt 写单元测试
给 prompt 写单元测试、是 2024 年以来兴起的实践。典型做法——用 pytest 这样的测试框架、把 prompt 当作被测对象、输入一组测试用例、验证 LLM 响应满足某些断言。断言的写法比较特殊——不能用严格相等(LLM 输出有随机性)、要用属性检查(包含关键词、长度在范围内、结构符合 schema)。pytest 有专门的插件 pytest-llm、promptfoo 等工具——让 prompt 测试接近传统单元测试的体验。
这种”把 prompt 纳入 CI”的实践、能显著提升 prompt 质量——每次修改 prompt 都自动跑测试、有回归立刻发现。代价是测试成本(每次 CI 都要调 LLM、成本不菲)——折中方案是”关键 prompt 跑真实 LLM、大部分 prompt 跑 mock 或 LLM-as-judge”。这种”测试分层”的思路、和传统软件的测试金字塔(unit → integration → e2e)异曲同工——Prompt 测试也会形成自己的金字塔。