Harness Engineering
第10章 Few-shot、CoT 与动态提示策略
第10章 Few-shot、CoT 与动态提示策略
“The difference between a mediocre agent and a great one is often not the model — it’s the information the model receives at the right moment.” — Claude Code team
“让模型在对的时间看到对的信息,这就是 Agent Harness 的全部艺术。” —— 杨艺韬
本章要点
- Agent 的 Few-shot 不是教模型回答问题,而是教它使用工具
- CoT 决定工具选择正确性——在 Agent 场景中远比传统对话场景重要
- 动态提示注入四层体系:初始化 / System-Reminder / 条件指令 / Skill 模板
- Skill 模式 把常见工作流变成可复用、可版本控制、可分享的模板
- 自一致性验证——Agent 不只”做”,还要”验证做对了”
- Prompt Caching 能把稳定前缀从每轮重复计算中拆出来,但收益取决于缓存命中、供应商计费和 prompt 布局
- 提示词有收益递减点——到某个程度后应转向工具和架构
10.1 Agent 场景下的 Few-shot 重新定义
传统 NLP 中的 Few-shot 是给模型几个”输入→输出”的示例,让它学会生成模式。但在 Agent 场景中,Few-shot 的含义发生了根本变化——它不再教模型如何生成文本,而是教模型如何正确使用工具。
flowchart LR
subgraph Traditional["传统 Few-shot"]
T1[输入: 今天天气如何] --> T2[输出: 今天晴天]
end
subgraph Agent["Agent Few-shot"]
A1[场景: 用户要改函数名] --> A2[工具序列:<br/>1. Grep 搜索引用<br/>2. Edit 逐文件替换<br/>3. Bash 运行测试]
end
Traditional -->|"教会生成"| Result[正确的工具使用模式]
Agent -->|"教会行动"| Result
style Traditional fill:#fef3c7,stroke:#f59e0b
style Agent fill:#dbeafe,stroke:#3b82f6
style Result fill:#dcfce7,stroke:#22c55e
范式的根本转变
传统对话 Few-shot: (Q, A) pairs —— 模仿回答风格
Agent Few-shot: (场景, 工具序列) pairs —— 模仿决策模式
这不是细微的变化,是 prompt engineering 的根本转向。
10.1.1 隐式 Few-shot:规则即示例
Claude Code 没有在系统提示词中放传统的 few-shot 示例,而是通过规则描述来编码工具使用模式。这本质上是一种压缩了的 few-shot——用规则代替了完整的示例:
# 这些规则等价于几十个 few-shot 示例
- To read files use Read instead of cat, head, tail, or sed
- To edit files use Edit instead of sed or awk
- To create files use Write instead of cat with heredoc or echo redirection
- To search for files use Glob instead of find or ls
- To search the content of files, use Grep instead of grep or rg
每一条规则都在告诉模型:“当你遇到 X 场景时,用 Y 工具而不是 Z 工具。“这比给出完整的对话示例更节省 token,但信息密度更高。
隐式 Few-shot 的信息密度对比
| 方式 | 成本特征 | 覆盖场景 | 适合放在哪里 |
|---|---|---|---|
| 一条规则 | 很低 | 一类稳定场景 | 系统提示词、工具描述 |
| 完整对话示例 | 高 | 一个具体场景 | Skill、任务模板、少数关键流程 |
| 规则 + 反例 | 中 | 一类场景 + 防呆 | 工具描述、错误恢复协议 |
“规则 + 反例”往往是甜点:
Use Grep for search. NEVER invoke grep/rg as Bash command.
短短一行,包含了”做什么”和”不要做什么”的双向信号。Claude Code 的 Grep 工具描述就使用这种写法:它要求搜索任务使用 Grep 工具,并明确不要把 grep 或 rg 当作 Bash 命令执行(../claude-code-main/src/tools/GrepTool/prompt.ts:6-15)。这类规则不是装饰性文案,而是工具路由的一部分。
10.1.2 显式 Few-shot:何时需要完整示例
当规则不够清晰、模型反复犯同一类错误时,需要显式的 few-shot 示例。典型场景:
场景一:复杂的工具组合模式
## Example: Renaming a function across the codebase
User: "Rename getUserInfo to fetchUserProfile"
Step 1 — Find all references:
<tool>Grep pattern="getUserInfo" output_mode="files_with_matches"</tool>
Result: src/api.ts, src/hooks/useUser.ts, src/tests/api.test.ts
Step 2 — Edit each file:
<tool>Read file_path="src/api.ts"</tool>
<tool>Edit file_path="src/api.ts" old_string="getUserInfo" new_string="fetchUserProfile" replace_all=true</tool>
(repeat for each file)
Step 3 — Verify:
<tool>Bash command="npm test"</tool>
WRONG approach (common mistake):
- Using Write to rewrite entire files (loses unread content)
- Only changing the definition, forgetting call sites
- Not running tests after rename
场景二:错误恢复的期望行为
## Example: Handling edit failure
<tool>Edit file_path="src/main.ts" old_string="function old(" new_string="function new("</tool>
Error: old_string not found in file
Correct recovery:
1. Read the file to see actual content
2. Find the correct string to match
3. Retry with accurate old_string
WRONG recovery:
- Retrying the exact same edit (will fail again)
- Using Write to overwrite the entire file
场景三:多步骤工具链
当任务涉及 5+ 个工具调用,且顺序很关键时:
## Example: Setting up a new monorepo workspace
1. Glob pattern="package.json" → 找到 workspace root
2. Read package.json → 确认 workspace 配置
3. Bash command="pnpm create vite packages/new-app" → 创建
4. Edit package.json → 注册到 workspace
5. Bash command="pnpm install" → 安装依赖
6. Bash command="pnpm --filter new-app build" → 验证构建
10.1.3 Few-shot 的 Token 经济学
显式 few-shot 示例的成本不只在长度。它还会增加维护成本:示例里的文件名、框架版本、命令参数、错误信息一旦过期,模型会把过期路径当成可模仿的正确路径。在 Agent Harness 中,few-shot 应该先被当作”可执行流程模板”管理,而不是当作漂亮的说明文字管理。
| 策略 | 固定成本 | 维护成本 | 典型风险 |
|---|---|---|---|
| 无 few-shot | 无 | 低 | 模型依赖预训练习惯,容易走错工具 |
| 规则式 few-shot | 低 | 中 | 规则过多后互相冲突 |
| 完整对话示例 | 高 | 高 | 示例过期后会稳定诱导错误 |
| 规则 + 关键示例混合 | 中 | 中 | 需要定期删掉低价值示例 |
实践建议:
- 先用规则式 —— 成本最低
- 监控模型犯错率 —— 量化问题
- 只在反复出错的场景加完整示例 —— 针对性投入,并给示例配回归用例
- 利用 Prompt Caching —— 示例的重复成本被摊薄(见 10.6)
10.1.4 Few-shot 示例的写作要点
一个好的 Agent Few-shot 示例应该包含:
- 情境——用户说了什么,上下文是什么
- 正确路径——应该调用哪些工具、什么顺序、什么参数
- 期望结果——每一步的输出
- 反模式——常见的错误做法(“WRONG approach: …”)
- 恢复路径——出错时怎么办
缺少”反模式”的示例效果较差——模型知道了一条正确路径,但不知道为什么其他路径不对。正反对照的示例学习效率最高。
10.2 Chain-of-Thought:让 Agent 先想后做
10.2.1 为什么 Agent 特别需要 CoT
传统聊天场景中,CoT 提升的是回答的准确性。在 Agent 场景中,CoT 的价值远不止于此——它决定了 工具选择的正确性。
没有 CoT 的 Agent 行为模式:
用户: "帮我修复登录页面的 CSS 问题"
模型: (立即行动) → Edit login.css, 改了一堆样式
结果: 改错了,因为没先读代码了解问题是什么
有 CoT 的 Agent 行为模式:
用户: "帮我修复登录页面的 CSS 问题"
模型: (先思考)
- 用户说的是 CSS 问题,但没说具体是什么问题
- 我应该先看看当前的 CSS 和页面结构
- 可能需要在浏览器中查看效果
模型: (行动) → 先 Read login.css 和 login.vue
→ 分析后发现是 flexbox 对齐问题
→ 精准修改一行 CSS
结果: 一次修复成功
sequenceDiagram
participant U as 用户
participant T as Thinking
participant A as Action
participant Tool as 工具
U->>T: "修复登录 CSS 问题"
Note over T: 推理阶段<br/>1. 问题不明确,先看代码<br/>2. 需要 Read CSS 和模板<br/>3. 再决定怎么修
T->>A: 决策:先读文件
A->>Tool: Read login.css
Tool-->>T: 文件内容
Note over T: 推理阶段<br/>发现 flexbox 对齐问题<br/>justify-content 应该改为 center
T->>A: 决策:精准修改
A->>Tool: Edit login.css
Tool-->>U: 修复完成
如何判断 CoT 是否值得
不要用”思考越多越好”来判断 CoT。更可靠的做法是把任务按决策复杂度分层:
| 任务类型 | 不推理的常见风险 | CoT 的主要价值 | 建议 |
|---|---|---|---|
| 单步查询 | 延迟增加 | 很少 | 直接执行 |
| 单文件小改 | 先读后改即可 | 识别目标文件和约束 | 使用短推理 |
| 多文件重构 | 漏改调用点、改错层级 | 规划搜索、修改和验证顺序 | 使用结构化推理 |
| 模糊需求 | 猜错目标 | 澄清假设、列出可验证路径 | 先推理再行动 |
| 高风险操作 | 忽略回滚和权限 | 枚举副作用和验证点 | 强制风险检查 |
结论不是”复杂任务一定要长 CoT”,而是”决策点越多,越需要显式地把假设、信息缺口和验证动作排出来”。如果任务只有一个确定动作,CoT 只会增加成本;如果任务需要在多个工具链之间选择,CoT 才是质量控制手段。
10.2.2 Extended Thinking
一些模型接口支持把”内部推理预算”作为独立参数配置。不同供应商的参数名、可见性和计费方式会变化,所以在 Harness 设计中不要把某个模型名或固定预算写死在业务逻辑里;把它抽象成”任务风险到推理强度”的策略即可:
type ReasoningLevel = 'off' | 'brief' | 'standard' | 'deep'
function chooseReasoningLevel(task: Task): ReasoningLevel {
if (task.isReadOnly && task.steps.length <= 1) return 'off'
if (task.touchesProduction || task.requiresMigration) return 'deep'
if (task.files.length > 1 || task.requirementsAreAmbiguous) return 'standard'
return 'brief'
}
const response = await llm.call({
model: selectedModel,
reasoning: chooseReasoningLevel(task),
messages: buildMessages(task),
})
Extended Thinking 的适用场景
Extended Thinking 特别适合以下 Agent 决策场景:
| 场景 | 不用 Thinking | 用 Thinking |
|---|---|---|
| 多文件重构 | 直接改,可能遗漏依赖 | 先规划修改顺序和依赖关系 |
| 调试复杂 bug | 盲目搜索,来回多次 | 先分析错误日志,形成假设 |
| 模糊需求 | 猜测用户意图,可能跑偏 | 先列出可能的理解,选择最合理的 |
| 架构决策 | 给出第一个想到的方案 | 对比多个方案的 trade-off |
| 安全敏感操作 | 容易忽视副作用 | 先审查潜在风险 |
Thinking 预算的设置
推理预算应该由”错误代价”而不是”任务描述长度”决定:
| 任务信号 | 推理强度 | 预算策略 |
|---|---|---|
| 只读查询、格式转换、单步命令 | 关闭或极低 | 直接执行,避免浪费延迟 |
| 小范围代码修改 | 低 | 先读目标文件,列出最短修改路径 |
| 多文件变更、测试失败定位 | 中 | 建立假设、逐步缩小范围、保留验证计划 |
| 数据迁移、权限变更、发布操作 | 高 | 先列风险、回滚路径和人工确认点 |
超过任务需要的推理预算会出现两个副作用:模型重复已经确定的事实,或者为了填满推理空间而制造不必要的分支。好的 Harness 应该允许按任务类型调节推理强度,而不是全局打开。
10.2.3 结构化推理提示
即使不使用 Extended Thinking API,也可以通过系统提示词引导模型先推理:
Before taking any action, briefly analyze:
1. What is the user actually asking for?
2. What information do I still need?
3. What's the simplest approach that could work?
4. What could go wrong?
Then proceed with the minimum necessary tool calls.
Claude Code 在多个地方体现了这一理念:
In general, do not propose changes to code you haven't read.
If a user asks about or wants you to modify a file, read it first.
Understand existing code before suggesting modifications.
这些不是”请你思考”的空泛指示——而是具体的行为约束,迫使模型在行动前先获取信息(读代码)。
10.2.4 何时不需要 CoT
CoT 不是免费的——它消耗 token、增加延迟。以下场景应关闭或降低 CoT 力度:
- 延迟敏感:实时聊天机器人
- 简单决策:单步工具调用
- 用户已经思考好了:用户明确告诉你”执行 X 然后 Y”
- 成本敏感:高频、低价值的批处理任务
10.2.5 CoT 的副作用:过度思考
过度使用 CoT 也有问题:
- 分析瘫痪——模型在 thinking 中列出 10 个方案,但做不了决定
- 假思考——thinking 是装样子,最终决策和 thinking 不一致
- 延迟累积——每轮 thinking 加几秒,多轮对话累积明显
对策:在 prompt 中明确”thinking 要简短、结构化、以决定结尾”。
10.3 动态提示注入:Agent 的核心差异化能力
静态的系统提示词无法适应所有场景。动态提示注入——在运行时根据当前上下文按需注入信息——是 Agent 系统区别于简单 chatbot 的核心能力。
flowchart TD
subgraph Static["静态层 (每次相同)"]
S1[基础人格]
S2[角色指令]
S3[工具定义]
end
subgraph Dynamic["动态层 (每次不同)"]
D1[会话初始化注入<br/>git status, 分支, CWD]
D2[中途注入<br/>system-reminder]
D3[条件指令<br/>根据语言/框架启用规则]
D4[Skill 模板<br/>/commit, /review-pr]
end
Static --> Final[最终上下文]
Dynamic --> Final
Final --> LLM[LLM 决策]
style Static fill:#dbeafe,stroke:#3b82f6
style Dynamic fill:#fef3c7,stroke:#f59e0b
10.3.1 会话初始化注入
Claude Code 在每次会话开始时,自动收集并注入当前环境信息:
// Claude Code 会话开始时注入的上下文
const sessionContext = `
# Environment
- Primary working directory: ${cwd}
- Is a git repository: ${isGitRepo}
- Platform: ${platform}
- Shell: ${shell}
- OS Version: ${osVersion}
- Model: ${modelName}
gitStatus: ${gitStatusSnapshot}
Current branch: ${currentBranch}
Main branch: ${mainBranch}
Recent commits:
${recentCommits}
`
这段注入的价值在于:模型不需要用户解释就知道自己在什么环境中工作。用户说”帮我提交代码”,模型已经知道当前有哪些修改、在哪个分支、最近的提交风格是什么——这些信息全部来自初始化注入。
初始化注入的内容选型
什么该注入,什么不该注入?
应该注入(高信息密度,不注入会频繁问用户):
- 工作目录路径
- 操作系统和 shell
- Git 状态和分支
- 最近几条 commit 信息
- 项目根标识文件(package.json / Cargo.toml)
不应该注入(低密度,按需读取更好):
- 完整的文件列表(几千文件时巨大)
- 依赖包版本(按需读取 package.json)
- 历史对话(有专门的记忆系统)
- 所有配置文件内容
10.3.2 中途注入:System Reminders
对话进行中,Harness 可以在消息流中插入 <system-reminder> 标签,向模型传递新信息:
<system-reminder>
The user opened file src/auth.ts in the IDE.
This may or may not be related to the current task.
</system-reminder>
System Reminder 的设计原则
- 非侵入性——它出现在对话流中,但不是用户消息。模型可以自行判断是否相关
- 时效性——传递的是当前时刻的状态,不是永久规则
- 可忽略——明确告知模型”这可能相关也可能不相关”,避免模型过度反应
- 结构化——用
<system-reminder>标签包裹,模型能清楚识别
System Reminder 的使用场景
Claude Code 使用 system-reminder 传递多种运行时信息:
- IDE 中用户打开的文件——可能是用户暗示的关注点
- 可用的 deferred tools 列表——让模型知道能搜什么
- 当前日期时间——时效性提示
- 记忆内容(CLAUDE.md 和 auto-memory)——用户自定义规则
- Skill 加载的模板内容——斜杠命令触发的工作流
- 长时间未响应提示——用户可能在看别的东西
- Tool 失败统计——连续失败时建议换方法
10.3.3 条件指令注入
根据当前上下文动态启用或禁用特定规则:
function buildConditionalInstructions(context: SessionContext): string[] {
const instructions: string[] = []
// 只在 Git 仓库中启用 Git 相关规则
if (context.isGitRepo) {
instructions.push(GIT_SAFETY_PROTOCOL)
instructions.push(COMMIT_GUIDELINES)
instructions.push(PR_CREATION_RULES)
}
// 只在有 package.json 时启用 Node.js 规则
if (context.hasPackageJson) {
instructions.push('Prefer npm/yarn/pnpm over global installs')
}
// 只在 monorepo 中启用相关规则
if (context.isMonorepo) {
instructions.push('Always specify the workspace when running commands')
}
// 只在 Rust 项目中启用 Rust 规则
if (context.hasCargoToml) {
instructions.push(RUST_IDIOMS)
instructions.push(CARGO_COMMANDS)
}
// 只在用户明确授权破坏性操作时启用
if (context.permissionMode === 'auto-edit') {
instructions.push(AUTO_EDIT_GUIDELINES)
}
return instructions
}
条件注入的价值不只是节省 token,更重要的是减少无关规则对模型决策的干扰。如果当前项目不是 Git 仓库,Git 提交协议就不应该进入上下文;如果当前任务只是解释代码,发布、部署和权限修改规则也不应该挤占注意力。
条件注入的决策矩阵
| 条件信号 | 激活的指令 | 不注入时的收益 |
|---|---|---|
有 .git 目录 | Git Protocols | 非 Git 项目不被提交规则污染 |
有 package.json | Node/npm rules | 非 Node 项目不被包管理器规则干扰 |
有 Cargo.toml | Rust idioms | 非 Rust 项目不出现 Cargo 路径假设 |
有 Dockerfile | Docker rules | 不把容器发布流程提前暴露 |
| monorepo 结构 | workspace rules | 单包项目不承担 workspace 复杂度 |
| 有 CI 配置 | CI protocols | 本地小改不被发布流程拖慢 |
plan 模式 | Plan mode only rules | 执行模式不混入只读约束 |
同一个 Harness 在轻量项目和复杂 monorepo 中应该生成不同的提示上下文。上下文越贴近当前任务,模型越不需要在无关规则之间做选择。
10.4 Skill 模板模式
10.4.1 什么是 Skill
Claude Code 的 Skill 系统是动态提示注入的典型应用。用户输入斜杠命令(如 /commit),系统加载一段预定义的提示词模板,注入到当前对话中:
sequenceDiagram
participant U as 用户
participant H as Harness
participant S as Skill 文件
participant L as LLM
U->>H: 输入 /commit
H->>S: 加载 commit.md 模板
S-->>H: 标准化的提交工作流
H->>L: 注入 skill 模板 + 对话上下文
Note over L: 按照模板中的步骤执行:<br/>1. git status<br/>2. git diff<br/>3. git log<br/>4. 生成 commit message<br/>5. 暂存文件<br/>6. 创建提交
L->>U: 按标准流程提交代码
10.4.2 Skill 的设计原则
一个好的 Skill 模板应该:
# commit skill
## 触发条件
当用户说"提交代码"、"commit"、"/commit" 时
## 工作流程
1. Run git status to see all untracked files
2. Run git diff to see both staged and unstaged changes
3. Run git log to see recent commit messages (match style)
4. Analyze all changes and draft a commit message:
- Summarize the nature of changes (new feature, bug fix, etc.)
- Focus on "why" rather than "what"
5. Stage relevant files (NEVER use git add -A)
6. Create the commit with the drafted message
7. Run git status after commit to verify success
## 安全约束
- Do not commit files that likely contain secrets (.env, credentials.json)
- If pre-commit hook fails, fix the issue and create a NEW commit (never --amend)
- Do not push unless explicitly asked
Skill 模式的核心优势
| 优势 | 说明 |
|---|---|
| 可组合 | 不同 skill 可以组合使用(/commit + /review-pr) |
| 用户可扩展 | 用户在 .claude/skills/ 目录下编写自己的 skill |
| 版本可控 | 每个 skill 是独立的 Markdown 文件,可以 Git 管理 |
| 团队共享 | 项目级 skill 可以提交到仓库,团队共享标准工作流 |
| 渐进增强 | 新 skill 不影响已有功能,只是增加新能力 |
| 可 A/B 测试 | 同一场景的多个 skill 可以对比效果 |
10.4.3 团队 Skill 的目录设计
一个前端团队可以把项目级 skill 组织成下面这种目录:
.claude/skills/
├── review-pr.md # 按团队标准 review PR
├── write-test.md # 生成符合团队规范的测试
├── add-feature-flag.md # 添加特性开关的标准流程
├── deploy-staging.md # 部署到 staging 的检查清单
├── create-component.md # 创建新 Vue 组件的脚手架
└── migrate-api-v2.md # 迁移到 v2 API 的工作流
这些 skill 封装了团队的”隐性知识”——本来只有老员工脑子里的工作流,现在每个团队成员和 AI Agent 都能按同一套流程执行。目录里的每个文件最好只解决一种任务:PR review、测试补齐、组件创建、发布检查不要混成一个巨大模板,否则 skill 会退化成另一个不可维护的 system prompt。
10.4.4 Skill vs System Prompt
| 维度 | System Prompt | Skill |
|---|---|---|
| 加载时机 | 每次对话 | 按需触发 |
| 内容范围 | 通用规则 | 特定工作流 |
| 变更频率 | 极低 | 中 |
| 所有者 | 产品 / 平台 | 用户 / 团队 |
| Token 成本 | 固定 | 按需 |
Skill 是对 System Prompt 的补充,不是替代。System Prompt 是骨架,Skill 是四肢。
10.5 自一致性检查:Agent 自我验证
让 Agent 不只是”做了”,还要”验证做对了”:
flowchart TD
A[执行修改] --> B[重新读取修改后的文件]
B --> C{修改正确?}
C -->|是| D[运行测试]
D --> E{测试通过?}
E -->|是| F[✅ 确认完成]
E -->|否| G[分析失败原因]
G --> H[修复并重试]
H --> A
C -->|否| I[撤销修改]
I --> J[尝试不同方案]
J --> A
style F fill:#dcfce7,stroke:#22c55e
style A fill:#dbeafe,stroke:#3b82f6
style I fill:#fee2e2,stroke:#ef4444
Claude Code 的自一致性约束
Claude Code 在 Git 操作中强制要求自一致性:
# 创建提交后必须验证
After committing, run git status to verify success.
# 如果 pre-commit hook 失败
If the commit fails due to pre-commit hook:
fix the issue and create a NEW commit
(不是 --amend,因为 commit 没有成功)
在 Edit 操作中:
# Edit 失败后
If edit fails because old_string is not found:
Read the file to see actual content
Find the correct string to match
Retry with accurate old_string
Do NOT:
Retry the exact same edit (will fail again)
Use Write to overwrite the entire file
自一致性的三个层次
graph TD
V[自一致性验证]
V --> V1[语法层<br/>修改后文件能解析]
V --> V2[语义层<br/>修改后代码运行正确]
V --> V3[业务层<br/>修改后功能符合预期]
V1 --> T1[类型检查 / parse]
V2 --> T2[单元测试]
V3 --> T3[端到端测试 / 人工验证]
style V1 fill:#dbeafe,stroke:#3b82f6
style V2 fill:#fef3c7,stroke:#f59e0b
style V3 fill:#dcfce7,stroke:#22c55e
成本 vs 收益
自一致性检查的成本是额外的工具调用,但它换来的是可观察、可恢复的执行闭环:
场景: Agent 修改了 10 行代码
无验证成本: 0 tools calls
有验证成本: 读取修改后的文件 + 运行相关检查
一旦出错的代价:
小 bug: 用户 5 分钟调试 + 失去信任
中 bug: 小时级排查 + 回退操作
大 bug: 半天修复 + 影响团队 + 数据恢复
在生产环境中,一个未经验证的修改可能导致小时级的调试;而一次范围合适的 npm test、cargo test、类型检查或静态检查,通常能在错误刚产生时把它拦下来。重点不是”每次都跑最重的测试”,而是让验证动作和风险等级匹配。
10.6 Prompt Caching:被低估的性能利器
10.6.1 为什么 Agent 场景特别适合 Prompt Caching
Agent 的一个显著特点是多轮 LLM 调用共享大量相同的前缀——系统提示词、工具定义、角色指令在整个会话中几乎不变。这正是 Prompt Caching 最擅长的场景:
第 1 轮调用: [System Prompt 3000t] + [用户消息 100t]
第 2 轮调用: [System Prompt 3000t] + [历史 500t] + [工具结果 200t]
第 3 轮调用: [System Prompt 3000t] + [历史 1200t] + [工具结果 300t]
...
第 N 轮调用: [System Prompt 3000t] + [历史 Nt] + [工具结果 Mt]
^^^^^^^^^^^^^^^^^^^^^^^^
这 3000t 每次都完全相同 → 缓存!
10.6.2 实现方式
以 Claude Code 源码为例,缓存不是一句”打开缓存”就结束,而是 prompt 分块、稳定边界和 API 参数共同作用的结果。getCacheControl() 返回 type: 'ephemeral',并可按条件附加 TTL 或 cache scope(../claude-code-main/src/services/api/claude.ts:358-374)。随后 buildSystemPromptBlocks() 调用 splitSysPromptPrefix(),只给允许缓存的 system prompt block 挂上 cache_control(../claude-code-main/src/services/api/claude.ts:3213-3235)。
下面是抽象后的形态:
const response = await client.messages.create({
model: selectedModel,
system: [
{
type: 'text',
text: STATIC_SYSTEM_PROMPT,
cache_control: { type: 'ephemeral' } // 标记为可缓存
},
{
type: 'text',
text: buildDynamicContext(session) // 每次不同的动态部分
}
],
tools: [
...TOOL_DEFINITIONS.map(t => ({
...t,
cache_control: { type: 'ephemeral' } // 工具定义也缓存
}))
],
messages: conversationHistory
})
10.6.3 缓存命中条件
Prompt Cache 的命中条件可以拆成四类:
- 稳定前缀——缓存段的文本必须稳定,动态内容不能插入前面
- 连续匹配——缓存通常按前缀命中,越早出现动态内容,后面的稳定内容越难复用
- 缓存标记稳定——
cache_control挂在哪些 block 上要稳定,否则同样的文本也可能走不同缓存路径 - 供应商策略匹配——TTL、scope、可缓存 block 数量、工具定义缓存规则由 API 决定,Harness 只能按当前接口组织请求
Claude Code 的边界机制正是为了解决第一点。SYSTEM_PROMPT_DYNAMIC_BOUNDARY 被定义为静态和动态内容之间的分割标记(../claude-code-main/src/constants/prompts.ts:105-115),getSystemPrompt() 在静态 sections 后插入这个 marker,再追加动态 sections(../claude-code-main/src/constants/prompts.ts:560-576)。splitSysPromptPrefix() 找到 marker 后,把 marker 前的静态 blocks 放进可缓存 scope,marker 后的动态 blocks 保持不可缓存(../claude-code-main/src/utils/api.ts:362-404)。
10.6.4 效果怎么量化
不要在 prompt 文档里写死某个供应商当前价格或固定百分比。更稳的做法是记录四个量:
| 指标 | 含义 | 观察方式 |
|---|---|---|
| stable prefix size | 每轮重复发送的稳定前缀大小 | 统计 system prompt、工具 schema、固定规则长度 |
| dynamic suffix size | 每轮变化的用户、工具结果、环境信息大小 | 统计消息历史和运行时注入 |
| cache hit rate | 可缓存前缀实际命中的比例 | 读取 API 返回的缓存计量字段或网关日志 |
| write/read price ratio | 缓存写入和读取的相对价格 | 使用供应商当期价格表配置,不写进 prompt |
有了这些量,就能做自己的成本模型:如果稳定前缀很大、会话轮次多、动态 suffix 相对小,缓存收益就高;如果每轮都改变 system prompt,或者动态内容被放在最前面,缓存收益会迅速下降。
10.6.5 设计提示词时的缓存意识
为了最大化缓存命中率,把不变的内容放前面,变化的内容放后面:
┌────────────────────────────────┐
│ 静态部分(可缓存) │ ← cache_control: ephemeral
│ - 身份定义 │
│ - 角色指令 │
│ - 工具使用规则 │
│ - 安全协议 │
│ - 输出格式 │
├────────────────────────────────┤
│ 动态部分(每次不同) │ ← 不标记缓存
│ - 当前 git status │
│ - CLAUDE.md 内容 │
│ - 最近文件变更 │
│ - 会话记忆 │
└────────────────────────────────┘
缓存破坏模式:3 个常见错误
- 时间戳混入静态部分
❌ "Current time: 2026-04-16 14:23:05\n\n[系统规则...]"
每秒变一次,前缀很难稳定命中
✅ "[系统规则...]\n\nCurrent time: 2026-04-16 14:23:05"
时间戳放末尾,不影响前面缓存
- 随机 request ID
❌ "Request ID: abc123\n\n[系统规则...]"
✅ "[系统规则...]\n\nRequest ID: abc123"
- 用户名拼入
❌ "Hello {user.name}, I'm Claude...\n\n[工具定义]"
✅ "I'm Claude Code...\n\n[工具定义]\n\nUser: {user.name}"
核心原则:动态内容尽量放在静态前缀之后。如果业务必须把某些动态标识放前面,就要接受缓存收益下降,并把它作为成本模型的一部分。
10.7 提示工程的收益递减
一个残酷的事实:提示词优化到一定程度后,继续投入的回报急剧下降。
graph LR
subgraph ROI["投入 vs 回报"]
A[工具定义清晰] --> B[基础角色和安全规则]
B --> C[Few-shot 示例]
C --> D[措辞微调]
D --> E[形容词和语气偏好]
end
style A fill:#dcfce7,stroke:#22c55e
style B fill:#dcfce7,stroke:#22c55e
style C fill:#fef3c7,stroke:#f59e0b
style D fill:#fee2e2,stroke:#ef4444
style E fill:#fee2e2,stroke:#ef4444
当你发现自己在纠结”用’请’还是’务必‘“的时候,说明提示词优化已经到头了。此时应该把精力转向:
- 更好的工具设计(第 5 章)——让模型有更好的”手”
- 更好的上下文管理(第 4 章)——让模型看到更相关的信息
- 更好的错误恢复(第 7 章)——让系统从错误中自动恢复
- 更好的评估体系(第 18 章)——量化改进效果
10.7.1 源码里的更强信号
提示词是入口,但不是唯一的杠杆。Claude Code 的 FileEdit 工具描述并没有只写”请谨慎编辑”,而是把关键行为约束放进工具本身:编辑前至少读一次文件,保留从 Read 输出里看到的真实缩进,不要把行号前缀塞进 old_string,当 old_string 不唯一时要扩大上下文或使用 replace_all(../claude-code-main/src/tools/FileEditTool/prompt.ts:4-27)。
这说明一个重要边界:如果一个约束直接影响工具能否正确执行,它应该进入工具 schema、工具描述或工具运行时校验;如果它只是高层偏好,才适合留在 system prompt。把执行约束藏在泛泛的 prompt 里,通常不如让工具自己暴露清楚的失败条件。
10.7.2 判断是否到了收益递减点
以下信号说明你已经到了提示词优化的天花板:
- 改动小,效果更小——措辞微调没有明显提升
- 反复回退——改了又改回,说明没有明确方向
- 争论加剧——团队在”要不要加这句”上花大量时间
- 边际成本飙升——每 1% 提升要花几天时间
- 测试用例波动——同一 prompt 不同时间结果不同
此时应该冻结 prompt,转向工具、上下文、评估等其他维度。
10.7.3 提示词的维护纪律
防止 prompt 腐化的三条原则:
- 任何修改都要过回归测试——黄金测试集不可妥协
- 有明确的负责人——不是所有人都能改
- 保留删除权——定期 review,删除无用内容
10.7.4 Prompt 改动的评估协议
提示词改动如果没有评估协议,很容易变成”谁的表达更有说服力”。一个可执行的评估协议至少包含五个字段:
| 字段 | 说明 | 不合格写法 |
|---|---|---|
| 目标行为 | 这次改动希望模型改变什么决策 | ”让模型更聪明” |
| 受影响任务 | 哪些任务应该变化,哪些不应该变化 | ”所有任务都更好” |
| 负向用例 | 改动不能破坏哪些旧行为 | 没有回归样例 |
| 观测指标 | 成功、失败、犹豫、误调用如何记录 | 只看单次演示 |
| 回滚条件 | 什么时候撤回这条 prompt | 没有退出条件 |
这套协议的作用不是追求学术级实验,而是让团队避免把一次偶然成功当成系统性改进。尤其是 Agent 场景,同一个 prompt 可能改善”多文件搜索”,同时恶化”单文件小修”;没有分层评估,就会把局部收益误判为全局收益。
一个实用的回归集可以按任务类型分组:
| 任务组 | 样例 | 应观察的失败信号 |
|---|---|---|
| 搜索定位 | 查找函数定义、定位报错来源 | 使用错误工具、搜索范围过窄 |
| 精准修改 | 单文件改名、配置项调整 | 未读文件就改、替换过宽 |
| 多步修复 | 修测试、修 lint、改接口调用 | 没有验证、重复失败动作 |
| 风险操作 | Git、删除文件、迁移数据 | 未确认、无回滚路径 |
| 用户沟通 | 需求不完整、约束冲突 | 过早执行、不暴露假设 |
Prompt 每次改动后,不需要跑完所有任务,但至少要覆盖它声称会影响的任务组,以及一个不应该被影响的对照组。
10.8 四个反模式
反模式一:堆砌规则到 10K+ tokens
现象:Prompt 越写越长,各种情况都想覆盖。
问题:Lost in the middle,关键指令被淹没;缓存失效频繁;成本上升。
对策:
- 用条件注入,根据上下文激活
- 用 Skill 按需加载
- 规则不超过 5K tokens
反模式二:到处用 Extended Thinking
现象:所有场景都启用,简单任务也 thinking。
问题:延迟翻倍,成本激增,用户感知不到价值。
对策:
- 简单任务关闭 thinking
- 复杂任务用合理预算
- 监控 thinking 的 token 消耗
反模式三:Few-shot 示例过期
现象:示例中的 API 已经变了,但 prompt 还在教老用法。
问题:模型按过时模式操作,持续失败。
对策:
- Few-shot 示例加版本号
- 定期回归测试覆盖示例
- API 变更时同步更新 prompt
反模式四:动态部分放在 prompt 前面
现象:把时间戳、用户名放 prompt 最前面。
问题:稳定前缀被打断,缓存命中率和成本可预测性都会变差。
对策:把动态内容放在稳定前缀之后;确实要提前注入的动态信号,单独建模它对缓存和路由的影响。
10.8.1 Claude Code 源码里印证本章观点的三处实物
读 ../claude-code-main/src/ 能找到本章理论在生产代码里的实物对应:
1. SYSTEM_PROMPT_DYNAMIC_BOUNDARY 字符串字面量(../claude-code-main/src/constants/prompts.ts:105-115)——
export const SYSTEM_PROMPT_DYNAMIC_BOUNDARY =
'__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__'
这是 §10.6.5”动态部分放在 prompt 后面”的实际机制。Claude Code 在 system prompt 中插入这个边界,getSystemPrompt() 把静态 sections 放在边界之前,把动态 sections 放在边界之后(../claude-code-main/src/constants/prompts.ts:560-576)。随后 splitSysPromptPrefix() 依据这个边界生成可缓存和不可缓存的 block(../claude-code-main/src/utils/api.ts:362-404)。
2. <system-reminder> tag 的定义(../claude-code-main/src/constants/prompts.ts:131-133)——本章 §10.3.2 讨论的”中途注入:System Reminders”在源码里有明确入口。系统提示词告诉模型,tool result 和 user message 里可能出现 <system-reminder>,这些标签由系统自动添加,和它所在的具体消息没有直接关系。
这段定义的工程意义是:运行时注入不是偷偷塞一段自然语言,而是要给模型一个可识别的语义边界。没有这个边界,模型可能把系统状态误当成用户新要求。
3. cache_control 不是单点开关——getCacheControl() 统一生成缓存控制对象(../claude-code-main/src/services/api/claude.ts:358-374);用户消息和助手消息转换时可以在最后一个 block 上附加 cache control(../claude-code-main/src/services/api/claude.ts:600-668);buildSystemPromptBlocks() 又会根据 splitSysPromptPrefix() 的结果为 system prompt block 附加 cache control(../claude-code-main/src/services/api/claude.ts:3213-3235)。这说明缓存策略横跨 system prompt、历史消息和工具结果引用,不是一句配置项能概括。
这三处源码对应本章三个核心判断:稳定内容和动态内容要分层,运行时注入要有语义边界,缓存策略要落到请求构造代码里。提示工程在这里已经不是”写一段更好听的系统提示词”,而是上下文编排、工具定义、缓存边界和错误恢复共同构成的基础设施。
10.9 本章小结:动态提示策略的八条原则
动态提示策略的核心思想是在正确的时间给模型正确的信息:
- Few-shot 重定义——在 Agent 中,few-shot 教的是工具使用模式。规则式(隐式)优先,必要时加完整示例
- CoT 是决策质量的工具——复杂任务需要先暴露假设、信息缺口和验证路径;简单任务不应被长推理拖慢
- 动态注入四层体系——初始化注入、中途 Reminder、条件指令、Skill 模板,各司其职
- Skill 模式——将常见工作流标准化为可复用、可组合、可版本控制的提示词模板
- 自一致性——Agent 不只做,还要验证做对了。额外的验证成本远低于错误的修复成本
- Prompt Caching——利用 Agent 多轮调用的重复前缀特性降低重复计算;实际收益取决于稳定前缀、命中率和计费模型
- 知道何时停止——提示词优化有收益递减点,到达后转投工具和架构改进
- 缓存意识设计——动态内容尽量放在稳定前缀之后,不要污染可缓存的部分
核心口号:
Prompt engineering is not about writing better prompts — it’s about building systems where the right information reaches the model at the right time.
物理事实印证:Claude Code 源码里 SYSTEM_PROMPT_DYNAMIC_BOUNDARY 字符串字面量(../claude-code-main/src/constants/prompts.ts:105-115)是静态/动态 prompt 分层的实际机制;<system-reminder> tag 定义就在 system prompt 里教模型识别运行时注入(../claude-code-main/src/constants/prompts.ts:131-133);buildSystemPromptBlocks() 和 getCacheControl() 把缓存策略落实到 API 请求构造中(../claude-code-main/src/services/api/claude.ts:358-374, ../claude-code-main/src/services/api/claude.ts:3213-3235)。这就是”提示工程是基础设施工程”的含义。