Claude Code 源码解剖

Claude Code 的工程模式总结

把前面拆过的源码机制收束成可迁移的 Agent 工程模式:提示词控制面、缓存稳定、fail-closed、权限梯度、工具级提示词、结构化搜索、能力降维、可观测性和代码审查 Agent 落地。

第 19 章 20 分钟

Claude Code 的工程模式总结

这一章不是再讲一个新子系统,而是把前面 18 章的源码机制收束成一组可迁移的 Agent 工程模式。

不要把这些模式理解成“Claude Code 的最佳实践清单”。更准确地说,它们是源码里反复出现的设计判断:什么时候用 prompt,什么时候用代码硬拦;什么时候让模型自己决定,什么时候把状态外化;什么时候追求自治,什么时候回退到人类;什么时候为了缓存稳定牺牲一点实时性。

核心直觉

生产级 Agent 不是一个更聪明的 prompt,而是一套 harness:提示词定义行为边界,工具定义行动边界,权限定义副作用边界,上下文定义预算边界,缓存定义稳定边界,可观测性定义演进边界。

先看源码抓手

这一章引用的是全书前面反复出现的源码点。先把它们放在一张表里。

模式源码定位关键证据
提示词即控制面constants/prompts.tssystemPromptSections.ts行为约束通过系统提示词和 @@INLINE_0@@ 注入,而不是全写成 if/else
缓存感知设计constants/prompts.tsutils/api.tspromptCacheBreakDetection.tsSYSTEM_PROMPT_DYNAMIC_BOUNDARYsplitSysPromptPrefix()、per-tool hash
失败关闭Tool.tstoolOrchestration.ts新工具默认不可并发、非只读,异常时按不安全处理
A/B 与 Feature Flagfeature()、GrowthBook tengu_*ant-only、远程 flag、缓存友好的 cached flag
先观察再修复promptCacheBreakDetection.tsdenialTracking.ts、analytics用生产数据决定观测粒度和回退阈值
锁存以求稳定beta header、TTL 资格、自动压缩熔断状态一旦进入某边界,不在会话内来回抖动
双层约束FileEditTool、权限、Git 提示词prompt 软约束 + 工具代码硬约束
权限梯度types/permissions.ts、自动权限分类器从 plan/default 到 acceptEdits/auto/dontAsk
工具级提示词BashTool、FileEditTool、GrepTool、AgentTool、SkillTool行为规则贴近对应工具,而不是堆在主提示词
能力降维sessionTitle.tstoolUseSummaryGenerator.tssideQuestion.ts、Fork Agent按路径裁剪模型、工具、回合和上下文
可观测性闭环logEvent()、OTel、cost tracker行为变化必须能回放、对比和归因

总模式图

flowchart TD A["用户任务"] --> B["提示词控制面<br/>稳定规则 + 运行时提醒"] B --> C["Agent Loop"] C --> D["工具边界<br/>schema + tool prompt + output budget"] D --> E["权限边界<br/>fail-closed + 渐进自治"] E --> F["上下文边界<br/>预算 + 截断 + 压缩 + 记忆"] F --> G["缓存边界<br/>稳定前缀 + 锁存 + 断点检测"] G --> H["多 Agent / Skill / Plugin<br/>按需扩展能力"] H --> I["可观测性<br/>事件 + span + 成本 + 失败恢复"] I --> J["版本演进<br/>A/B + flag + 数据回路"] J --> B

这张图表达的不是模块依赖,而是工程回路。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、全局缓存、边界标记等路径放置缓存断点。缓存中断检测还会追踪 systemHashtoolsHashcacheControlHashperToolHashes 和 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.tsperToolHashes 定位哪个工具描述变了,而不是只知道“toolsHash 变了”。

可迁移做法:

  • 文件编辑规则写在编辑工具。
  • Git 风险写在 shell 工具。
  • 网络请求限制写在 HTTP 工具。
  • 数据库只读/写入边界写在 DB 工具。
  • 子 Agent 派生规则写在 Agent 工具。

模式七:结构化搜索优于 Shell 文本解析

Claude Code 没有让模型总是自己跑 grepfind,而是提供 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 提取频率,都可能改变模型决策。直接全量发布会让回归定位变成猜谜。

因此版本演进的最小闭环是:

flowchart LR A["新增行为"] --> B["flag / ant-only"] B --> C["采集事件"] C --> D["对照组比较"] D --> E{"指标正常?"} E -->|是| F["扩大灰度"] E -->|否| G["关闭 flag / 回滚"] G --> H["补观测或修实现"] H --> B

这条链里,flag 没有遥测没有意义,遥测没有回滚也没有意义。

一个代码审查 Agent 怎么落地

原书最后用 Rust 写了一个代码审查 Agent。它没有复刻 Claude Code,而是把模式迁移到另一个产品形态。

核心架构是:

flowchart TD A["Git diff"] --> B["Diff 解析 + Token 预算"] B --> C["逐文件审查 Loop"] C --> D["LLM 审查 diff"] D --> E{"需要更多信息?"} E -->|只读工具| F["受限 bash / grep / cat"] E -->|专项分析| G["skill prompt"] E -->|完成| H["结构化 Finding"] F --> D G --> D H --> I["报告 JSON / Markdown"]

这个例子里的模式映射很清楚:

层级做法对应 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),并阻止 ;|&amp;\$(){}` 等元字符。

这件事很好地说明了本章主线:Agent 自己也需要被审查,harness 本身也会有漏洞。不能因为系统是“安全工具”就假设它安全。

状态和数据结构

数据结构关键字段迁移价值
SystemPromptSectioncacheBreak区分稳定提示词和动态提示词
TOOL_DEFAULTSisConcurrencySafe: falseisReadOnly: false新工具默认保守
DENIAL_LIMITSmaxConsecutive: 3maxTotal: 20自动权限失败后回退人工
TaskownerstatusblockedBy多 Agent 协作状态外化
ContextBudgetmax_total_tokensmax_file_tokensused_tokens上下文预算显式记账
CircuitBreakermax_failuresfailures防止失控循环继续烧钱
ReviewReportfiles_reviewedfiles_skippedduration_msfindingsAgent 输出自身可观测性
StoredCostStatetoken、耗时、成本、行数成本是行为指标,不只是账单

读源码抓手

读源码抓手

收束这一章时,不要背模式名。沿着一个真实任务问:模型看到什么、能调用什么、能改什么、失败怎么办、状态写在哪里、怎么知道它变了。

建议路线:

  • 先读 constants/prompts.tssystemPromptSections.ts,理解提示词如何成为控制面。
  • 再读 Tool.tstoolOrchestration.ts,看默认值如何 fail-closed。
  • 接着读权限章节相关的 types/permissions.tsdenialTracking.ts,看自治如何有梯度。
  • 然后读 Grep、Glob、FileEdit、Bash、Agent、Skill 这些工具提示词,观察规则如何靠近工具。
  • 再回到多 Agent、Memory 和 Telemetry 章节,看状态如何外化、整理和观测。
  • 最后读原书第 30 章的代码审查 Agent,把模式放到一个新产品里验证。

小结

小结

- Claude Code 最值得迁移的不是某个工具实现,而是边界设计:提示词、工具、权限、上下文、缓存和观测各管一层。 - Prompt 适合表达行为偏好,代码必须承接安全和副作用边界。 - 缓存稳定、状态锁存和带外提醒是长会话 Agent 的基础设施,不是性能细节。 - 多 Agent、Memory、Plugin 都必须把关键状态外化,否则无法恢复、调试和治理。 - 真正的生产级 Agent 要能被观测、被灰度、被回滚,也要能审查自己的 harness。