Claude Code 的工程模式总结
这一章不是再讲一个新子系统,而是把前面 18 章的源码机制收束成一组可迁移的 Agent 工程模式。
不要把这些模式理解成“Claude Code 的最佳实践清单”。更准确地说,它们是源码里反复出现的设计判断:什么时候用 prompt,什么时候用代码硬拦;什么时候让模型自己决定,什么时候把状态外化;什么时候追求自治,什么时候回退到人类;什么时候为了缓存稳定牺牲一点实时性。
核心直觉
生产级 Agent 不是一个更聪明的 prompt,而是一套 harness:提示词定义行为边界,工具定义行动边界,权限定义副作用边界,上下文定义预算边界,缓存定义稳定边界,可观测性定义演进边界。
先看源码抓手
这一章引用的是全书前面反复出现的源码点。先把它们放在一张表里。
| 模式 | 源码定位 | 关键证据 |
|---|---|---|
| 提示词即控制面 | constants/prompts.ts、systemPromptSections.ts | 行为约束通过系统提示词和 @@INLINE_0@@ 注入,而不是全写成 if/else |
| 缓存感知设计 | constants/prompts.ts、utils/api.ts、promptCacheBreakDetection.ts | SYSTEM_PROMPT_DYNAMIC_BOUNDARY、splitSysPromptPrefix()、per-tool hash |
| 失败关闭 | Tool.ts、toolOrchestration.ts | 新工具默认不可并发、非只读,异常时按不安全处理 |
| A/B 与 Feature Flag | feature()、GrowthBook tengu_* | ant-only、远程 flag、缓存友好的 cached flag |
| 先观察再修复 | promptCacheBreakDetection.ts、denialTracking.ts、analytics | 用生产数据决定观测粒度和回退阈值 |
| 锁存以求稳定 | beta header、TTL 资格、自动压缩熔断 | 状态一旦进入某边界,不在会话内来回抖动 |
| 双层约束 | FileEditTool、权限、Git 提示词 | prompt 软约束 + 工具代码硬约束 |
| 权限梯度 | types/permissions.ts、自动权限分类器 | 从 plan/default 到 acceptEdits/auto/dontAsk |
| 工具级提示词 | BashTool、FileEditTool、GrepTool、AgentTool、SkillTool | 行为规则贴近对应工具,而不是堆在主提示词 |
| 能力降维 | sessionTitle.ts、toolUseSummaryGenerator.ts、sideQuestion.ts、Fork Agent | 按路径裁剪模型、工具、回合和上下文 |
| 可观测性闭环 | logEvent()、OTel、cost tracker | 行为变化必须能回放、对比和归因 |
总模式图
这张图表达的不是模块依赖,而是工程回路。Agent 每次变强,都不只是加新能力,还要补一层边界和一条观测链。
模式一:提示词是控制面,但不是唯一防线
Claude Code 很多行为约束来自提示词。例如极简主义指令:
"Don't create helpers, utilities, or abstractions for one-time operations.
Don't design for hypothetical future requirements. The right amount of
complexity is what the task actually requires..."
这段代码证明:Claude Code 没有写一个“过度抽象检测器”。这种行为偏好更适合 prompt 表达,因为它依赖语义和上下文。
但提示词只适合表达行为偏好,不适合承担安全边界。编辑文件就是反例。FileEditTool 的提示词会告诉模型“编辑前必须读文件”,工具代码也会在没有 Read 记录时拒绝执行。
可迁移判断
行为风格、工作习惯、任务策略适合放 prompt;权限、文件写入、并发、路径安全必须落到代码。
模式二:稳定提示词和运行时提醒分层
源码里有 systemPromptSection() 和 DANGEROUS_uncachedSystemPromptSection() 这种区分,背后的原则是:稳定规则应该缓存,易变状态应该带外注入。
export function DANGEROUS_uncachedSystemPromptSection(
name: string,
compute: ComputeFn,
_reason: string,
): SystemPromptSection {
return { name, compute, cacheBreak: true }
}
这段代码证明:破坏缓存不是普通选项,而是要显式写出 reason 的危险动作。DANGEROUS_ 前缀逼开发者意识到:每轮变化的提示词会改变缓存前缀,也可能改变模型行为。
@@INLINE_0@@ 是另一层控制面。Plan Mode、Todo 提醒、Read 空文件警告、ToolSearch 延迟提示等,都不必改主系统提示词,而是在运行时按条件注入。
可迁移做法:
| 内容类型 | 放置位置 |
|---|---|
| 长期原则、角色边界、安全宪法 | 稳定系统提示词 |
| 当前模式、一次性警告、工具结果提醒 | 运行时 reminder |
| 工具使用规则 | 工具描述 |
| 项目/用户偏好 | 项目规则或 Memory |
模式三:缓存感知设计是一等约束
Claude Code 把系统提示词分成静态和动态区域。
export const SYSTEM_PROMPT_DYNAMIC_BOUNDARY =
'__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__'
splitSysPromptPrefix() 会根据 MCP、全局缓存、边界标记等路径放置缓存断点。缓存中断检测还会追踪 systemHash、toolsHash、cacheControlHash、perToolHashes 和 beta headers。
这说明缓存不是性能小优化。对于 Agent,缓存前缀包含系统提示词、工具 schema、权限说明和运行边界。一旦前缀频繁变化,成本、延迟和行为稳定性都会一起变差。
锁存也是缓存感知的一部分。Beta Header 一旦发送过,会在会话内持续发送,即使对应功能后来关闭。日期也会在会话开始时记忆化,跨午夜不更新。这样做不是为了“信息最新”,而是为了“会话稳定”。
模式四:失败关闭,显式开放
工具默认值体现 fail-closed。
const TOOL_DEFAULTS = {
isEnabled: () => true,
isConcurrencySafe: (_input?: unknown) => false,
isReadOnly: (_input?: unknown) => false,
isDestructive: (_input?: unknown) => false,
toAutoClassifierInput: () => '',
}
这段代码证明:新工具默认不可并发、默认不是只读、默认不进入自动分类器。想开放能力必须显式声明。
并发编排也一样。如果 isConcurrencySafe() 抛异常,源码把它当成不安全。
try {
return Boolean(tool?.isConcurrencySafe(parsedInput.data))
} catch {
return false
}
这是非常值得迁移的判断:未知不等于允许,异常不等于忽略,缺省不等于安全。
模式五:自治是一条梯度,不是二元开关
Claude Code 的权限不是“自动/手动”二选一,而是多档模式。原文里外部模式包括:
export const EXTERNAL_PERMISSION_MODES = [
'acceptEdits',
'bypassPermissions',
'default',
'dontAsk',
'plan',
] as const
按行为理解,大致可以分成:
| 模式 | 含义 |
|---|---|
plan | 只规划,不执行 |
default | 默认确认 |
acceptEdits | 编辑类操作更自动,其他仍确认 |
auto | 分类器尝试自动判断 |
dontAsk / bypassPermissions | 更强自治或预授权路径 |
自动模式还有拒绝追踪回退:
export const DENIAL_LIMITS = {
maxConsecutive: 3,
maxTotal: 20,
} as const
export function shouldFallbackToPrompting(
state: DenialTrackingState,
): boolean {
return (
state.consecutiveDenials >= DENIAL_LIMITS.maxConsecutive ||
state.totalDenials >= DENIAL_LIMITS.maxTotal
)
}
这段代码证明:自动化不可靠时,系统回退到人工确认。自主不是越多越好,而是能在不确定时收回来。
模式六:工具级提示词比中心化指令更可靠
Git 安全协议放在 BashTool,编辑前读取放在 FileEditTool,搜索语法放在 GrepTool,多 Agent 隔离规则放在 AgentTool,技能预算放在 SkillTool。
这样做的原因很具体:模型决定调用某个工具时,该工具描述就在注意力附近。把所有规则塞到主系统提示词里,长会话后遵守率会下降,也会让缓存前缀变得更臃肿。
工具级提示词还有观测收益。promptCacheBreakDetection.ts 用 perToolHashes 定位哪个工具描述变了,而不是只知道“toolsHash 变了”。
可迁移做法:
- 文件编辑规则写在编辑工具。
- Git 风险写在 shell 工具。
- 网络请求限制写在 HTTP 工具。
- 数据库只读/写入边界写在 DB 工具。
- 子 Agent 派生规则写在 Agent 工具。
模式七:结构化搜索优于 Shell 文本解析
Claude Code 没有让模型总是自己跑 grep 和 find,而是提供 Grep、Glob 这样的只读搜索工具。内部结果是结构化对象:
{
mode: 'content' | 'files_with_matches' | 'count',
numFiles,
filenames,
content,
numLines,
numMatches,
appliedLimit,
appliedOffset,
}
这段结构证明:harness 先掌握结构,再把最小必要信息文本化给模型。模型不应该每轮都重新解析 shell stdout。
更通用的模式是分阶段搜索:
| 阶段 | 返回什么 | 回答什么问题 |
|---|---|---|
| 候选文件层 | 路径、数量、分页、稳定引用 | 哪些文件值得看 |
| 命中摘要层 | 匹配次数、首个位置、片段摘要 | 先展开哪里 |
| 精确片段层 | 行号、上下文窗口、匹配内容 | 具体代码是什么 |
这个模式对大代码库尤其重要。搜索本身不是难点,难点是别让搜索结果把上下文窗口淹没。
模式八:能力降维,不要所有路径都全功能
Claude Code 不是所有 helper 都走主 Agent Loop。
| 路径 | 上下文 | 工具 | 回合 | 模型 |
|---|---|---|---|---|
| 主 Loop | 完整对话 | 完整工具池 | 多轮 | 主模型 |
| 标准子 Agent | 新上下文 | 按角色组装 | 多轮 | 可覆盖 |
/btw 侧问题 | 继承父上下文 | 全禁用 | 单轮 | 继承父模型 |
| 标题/摘要 helper | 极小输入 | 无工具 | 单轮 | 小模型 |
| Extract Memories | 继承必要上下文 | 只读 + memoryDir 写入 | 有上限 | Fork |
这说明“用更小模型”只是能力降维的一种。还可以缩工具、缩回合、缩上下文、缩写入权限。真正的问题是:这条路径到底需要哪几种能力?
可迁移到自己的 Agent 时,先问四个问题:
- 需要完整上下文吗?
- 需要工具吗?
- 需要多轮吗?
- 需要写入权限吗?
如果答案是否,就裁掉。
模式九:高风险行为用双层约束
生产级 Agent 不能只靠 prompt 约束高风险行为。Claude Code 的“编辑前先读取”就是模板:
| 层级 | 作用 |
|---|---|
| prompt 软约束 | 告诉模型为什么要先读 |
| tool 硬约束 | 没读过就拒绝编辑 |
| 错误反馈 | 把拒绝原因返回给模型,让它修正路径 |
这个模式可以迁移到很多地方:
- 数据库写入前必须先查询 schema。
- 删除资源前必须先列出目标。
- 发邮件前必须先生成预览。
- 合并 PR 前必须先读取 CI 状态。
- 执行部署前必须先确认环境和版本。
模式十:状态外化,协作才可控
多 Agent 章节里,Teams 的核心不是“聊天”,而是 TaskList、Mailbox 和权限同步目录。
{
id: string,
owner?: string,
status: 'pending' | 'in_progress' | 'completed',
blocks: string[],
blockedBy: string[],
}
这段任务结构证明:并行协作需要把任务依赖、owner 和状态外化。自然语言协商不够稳定,也不容易恢复。
同样,Memory 用 MEMORY.md 和主题文件外化长期知识,Auto-Dream 用 .consolidate-lock 外化整理时间和 PID,遥测用 JSONL 外化失败批次。Claude Code 的一个共同模式是:不要把关键状态只放在模型脑子里。
模式十一:观测先于修复
缓存中断检测是最好的例子。源码不是直接修“缓存为什么差”,而是先记录 PreviousState,对比字段变化,并根据生产数据增加 perToolHashes。
自动压缩熔断也是数据驱动:
const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3
原文注释记录了原因:曾经有 1,279 个会话出现 50+ 连续失败,最多 3,272 次,每天浪费约 250K API 调用。这个常量不是拍脑袋,而是事故后的工程边界。
可迁移判断:当你觉得“模型突然变笨”,不要先改 prompt。先问:
- 工具列表变了吗?
- 权限模式变了吗?
- 缓存前缀断了吗?
- 压缩发生了吗?
- Memory 注入变了吗?
- Feature Flag 变了吗?
- 失败后有没有重试或降级?
模式十二:版本演进必须有开关和回滚
Claude Code 大量使用 ant-only、GrowthBook tengu_* 和构建时 feature。原因不是喜欢复杂开关,而是 Agent 行为变更很难通过传统单元测试覆盖。
提示词、工具描述、权限策略、压缩阈值、缓存 header、Memory 提取频率,都可能改变模型决策。直接全量发布会让回归定位变成猜谜。
因此版本演进的最小闭环是:
这条链里,flag 没有遥测没有意义,遥测没有回滚也没有意义。
一个代码审查 Agent 怎么落地
原书最后用 Rust 写了一个代码审查 Agent。它没有复刻 Claude Code,而是把模式迁移到另一个产品形态。
核心架构是:
这个例子里的模式映射很清楚:
| 层级 | 做法 | 对应 Claude Code 模式 |
|---|---|---|
| 提示词 | Constitution + runtime section | 稳定提示词和运行时状态分层 |
| 上下文 | 总预算 + 单文件预算 + 截断提示 | 为一切设定预算,告知而非隐藏 |
| 工具 | 只读 bash 白名单和黑名单 | fail-closed 工具边界 |
| 安全 | LLM 只输出 AgentAction,代码决定执行 | 模型不直接拥有副作用 |
| 韧性 | retry + circuit breaker | 有限重试和熔断 |
| 观测 | tracing 记录文件、tokens、findings、耗时 | 先观察再优化 |
原文还给出一个真实漏洞:工具系统用 sh -c 执行命令,即使第一个 token 在白名单里,cat file; uname -a 这类 shell 元字符仍能绕过检查。修复方式是不用 shell,改成 Command::new(program).args(args),并阻止 ;|&\$(){}` 等元字符。
这件事很好地说明了本章主线:Agent 自己也需要被审查,harness 本身也会有漏洞。不能因为系统是“安全工具”就假设它安全。
状态和数据结构
| 数据结构 | 关键字段 | 迁移价值 |
|---|---|---|
SystemPromptSection | cacheBreak | 区分稳定提示词和动态提示词 |
TOOL_DEFAULTS | isConcurrencySafe: false、isReadOnly: false | 新工具默认保守 |
DENIAL_LIMITS | maxConsecutive: 3、maxTotal: 20 | 自动权限失败后回退人工 |
Task | owner、status、blockedBy | 多 Agent 协作状态外化 |
ContextBudget | max_total_tokens、max_file_tokens、used_tokens | 上下文预算显式记账 |
CircuitBreaker | max_failures、failures | 防止失控循环继续烧钱 |
ReviewReport | files_reviewed、files_skipped、duration_ms、findings | Agent 输出自身可观测性 |
StoredCostState | token、耗时、成本、行数 | 成本是行为指标,不只是账单 |
读源码抓手
读源码抓手
收束这一章时,不要背模式名。沿着一个真实任务问:模型看到什么、能调用什么、能改什么、失败怎么办、状态写在哪里、怎么知道它变了。
建议路线:
- 先读
constants/prompts.ts和systemPromptSections.ts,理解提示词如何成为控制面。 - 再读
Tool.ts和toolOrchestration.ts,看默认值如何 fail-closed。 - 接着读权限章节相关的
types/permissions.ts和denialTracking.ts,看自治如何有梯度。 - 然后读 Grep、Glob、FileEdit、Bash、Agent、Skill 这些工具提示词,观察规则如何靠近工具。
- 再回到多 Agent、Memory 和 Telemetry 章节,看状态如何外化、整理和观测。
- 最后读原书第 30 章的代码审查 Agent,把模式放到一个新产品里验证。
小结
小结
- Claude Code 最值得迁移的不是某个工具实现,而是边界设计:提示词、工具、权限、上下文、缓存和观测各管一层。 - Prompt 适合表达行为偏好,代码必须承接安全和副作用边界。 - 缓存稳定、状态锁存和带外提醒是长会话 Agent 的基础设施,不是性能细节。 - 多 Agent、Memory、Plugin 都必须把关键状态外化,否则无法恢复、调试和治理。 - 真正的生产级 Agent 要能被观测、被灰度、被回滚,也要能审查自己的 harness。