Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第25章:驾驭工程原则

为什么这很重要

在前六篇中,我们从源码层面剖析了 Claude Code 的每一个子系统——工具注册、Agent Loop、系统提示词、上下文压缩、提示词缓存、权限安全、技能系统。这些分析揭示了大量的实现细节,但如果只停留在"它是怎么做的"层面,就浪费了逆向工程最有价值的产出:可复用的工程原则

本章从前 23 章的源码分析中提炼出 6 条驾驭工程(Harness Engineering)核心原则。每条原则都有明确的源码回溯、适用场景和反模式警示。这些原则的共同主题是:在 AI Agent 系统中,控制行为的最佳方式不是编写更多代码,而是设计更好的约束


Claude Code 在 Agent Loop 架构谱系中的位置

在提炼原则之前,有必要先回答一个元问题:Claude Code 是什么类型的 Agent 架构?

学术界将 Agent Loop 归纳为六类模式:单体循环(ReAct 式推理-行动交错)、分层代理(目标-任务-执行三层)、分布式多代理(多角色协作)、反思/元认知循环(Reflexion 式自我改进)、工具增强循环(外部工具驱动状态更新)、学习/在线更新循环(记忆持久化与策略迭代)。大多数框架(LangGraph、AutoGen、CrewAI)选择一到两种模式作为核心抽象。

Claude Code 的独特之处在于:它不是上述任何一种模式的纯实现,而是六种模式的实用主义混合体

┌─────────────────────────────────────────────────────────────┐
│                 Claude Code 架构谱系定位                      │
├──────────────────────┬──────────────────────────────────────┤
│ 学术模式              │ CC 对应实现                           │
├──────────────────────┼──────────────────────────────────────┤
│ 单体循环              │ queryLoop() — 核心 Agent Loop(ch03)│
│ 工具增强循环           │ 40+ 工具的 ReAct 式交错(ch02-04)    │
│ 分层代理              │ Coordinator Mode 战略/执行分层(ch20) │
│ 分布式多代理           │ Team 并行 + Ultraplan 远程委托(ch20)│
│ 反思循环(弱形式)     │ Advisor Tool + stop hooks 反馈(ch21)│
│ 学习循环(弱形式)     │ 跨会话记忆 + CLAUDE.md 持久化(ch24) │
└──────────────────────┴──────────────────────────────────────┘

这种混合不是设计失误,而是务实选择。CC 的核心是一个单体 queryLoop()(模式一),但在此基础上:

  • 工具增强是默认行为——每次迭代都可能调用工具、获取观测、更新状态,这正是 ReAct 的"推理-行动交错"
  • 分层代理按需启用——Coordinator Mode 将"规划"和"执行"拆分到不同层级,高层只决策、低层只执行
  • 分布式多代理按需启用——Team 模式让多个 Agent 通过 SendMessageTool 协作,Ultraplan 将规划卸载到远程容器
  • 反思是隐式的——没有显式的 Reflexion 记忆,但 Advisor Tool 提供了"批评者"角色,stop hooks 提供了"执行后检查"
  • 学习是持久化的——跨会话记忆(~/.claude/memory/)和 CLAUDE.md 使 Agent 能跨会话积累经验,但不更新模型权重

这种"默认简单、按需复杂"的架构哲学贯穿了本章提炼的所有原则。


源码分析

25.1 原则一:提示词即控制面

定义:用系统提示词段落引导模型行为,而非用代码逻辑硬编码限制。

Claude Code 的行为引导绝大多数通过提示词实现,而非通过代码中的 if/else 分支。最典型的例子是极简主义指令:

// restored-src/src/constants/prompts.ts:203
"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 — no speculative abstractions,
but no half-finished implementations either. Three similar lines of code
is better than a premature abstraction."

这段文本不是代码注释——它是发送给模型的实际指令。Claude Code 没有在代码层面检测模型是否过度工程化(这在技术上几乎不可能),而是通过自然语言直接告诉模型"不要这么做"。

同样的模式贯穿整个系统提示词架构(详见第5章)。systemPromptSections.ts 将系统提示词组织为多个可组合的段落,每个段落都有明确的缓存范围(scope: 'global'null)。这种设计使得行为调整只需修改文本,不需要改代码、改测试、走发布流程。

工具提示词是这一原则的精华体现(详见第8章)。BashTool 的 Git 安全协议——"绝不跳过 hooks、绝不 amend、优先指定文件 git add"——完全由提示词文本表达。如果某天团队决定允许 amend,只需删除一行提示词文本,无需触碰任何执行逻辑。

更进一步,Claude Code 并不把所有行为切换都塞进主系统提示词。<system-reminder> 充当了一条带外控制信道:Plan Mode 的多阶段工作流(interview → explore → plan → approve → execute)、Todo/Task gentle reminder、Read 工具的空文件/偏移警告、ToolSearch 的延迟工具提示,都是按条件注入到消息流中的元指令,而不是改写主系统提示词。换句话说,Claude Code 把"稳定宪法"和"运行时开关"拆成了两层控制面:前者追求稳定和可缓存,后者追求按需、短寿命和可替换。

适用边界:用代码处理结构性约束(权限、token 预算),用提示词处理行为性约束(风格、策略、偏好)。

反模式:行为硬编码。为每种不希望的模型行为编写检测器和拦截器,最终得到一个庞大的规则引擎,永远追不上模型能力的变化速度。


25.2 原则二:缓存感知设计是刚需

定义:每次提示词变更都有以 cache_creation token 计量的成本,系统设计必须将缓存稳定性作为一等约束。

SYSTEM_PROMPT_DYNAMIC_BOUNDARY 标记(restored-src/src/constants/prompts.ts:114-115)将系统提示词分为两个区域:

// restored-src/src/constants/prompts.ts:105-115
/**
 * Boundary marker separating static (cross-org cacheable) content
 * from dynamic content.
 * Everything BEFORE this marker in the system prompt array can use
 * scope: 'global'.
 * Everything AFTER contains user/session-specific content and should
 * not be cached.
 */
export const SYSTEM_PROMPT_DYNAMIC_BOUNDARY =
  '__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__'

splitSysPromptPrefix()restored-src/src/utils/api.ts:321-435)实现了三条代码路径来确保缓存断点放置正确:MCP 存在时的 tool-based 缓存、全局缓存+边界标记、默认 org 级别缓存。这个函数的复杂度完全来自缓存优化需求——如果不关心缓存,只需拼接字符串即可。

缓存中断检测系统(详见第14章)追踪近 20 个字段的前后状态变化(restored-src/src/services/api/promptCacheBreakDetection.ts:28-69),包括 systemHashtoolsHashcacheControlHashperToolHashesbetas 等。任何一个字段的变化都可能触发缓存失效。

Beta Header 锁存机制是极端案例:一旦发送过某个 beta header,就永远继续发送,即使对应功能已关闭——因为取消发送会改变请求签名,导致约 50-70K token 的缓存前缀失效。源码中的注释明确记录了锁存的原因:

// restored-src/src/services/api/promptCacheBreakDetection.ts:47-48
/** AFK_MODE_BETA_HEADER presence — should NOT break cache anymore
 *  (sticky-on latched in claude.ts). Tracked to verify the fix. */

日期记忆化(getSessionStartDate())是另一个例证:如果会话跨越午夜,模型看到的日期会"过期"——但这是有意为之,因为日期字符串变化会打断缓存前缀。

反模式:提示词频繁变动。Agent 列表曾内联在系统提示词中,占全球 cache_creation token 的 10.2%(详见第15章)。解决方案是将其移至 system-reminder 消息——这部分在缓存段之外,修改不影响缓存。

/btw 和 SDK side_question 把这套思想推到了另一个方向:缓存安全的侧信道查询。它们不是往主对话里插入一个普通 turn,而是复用主线程在 stop hooks 阶段保存的 cache-safe 前缀快照,再追加一条带 <system-reminder> 的侧问题,启动一个一次性、无工具的 fork,并显式 skipCacheWrite。结果是:侧问题可以共享父会话的前缀缓存,又不会把自己的问答污染回主对话历史。


25.3 原则三:失败关闭,显式开放

定义:系统默认值应选择最安全的选项,只有在显式声明后才允许危险操作。

buildTool() 工厂函数为每个工具属性设置了防御性默认值:

// restored-src/src/Tool.ts:748-761
/**
 * Defaults (fail-closed where it matters):
 * - `isConcurrencySafe` → `false` (assume not safe)
 * - `isReadOnly` → `false` (assume writes)
 * - `isDestructive` → `false`
 * - `checkPermissions` → `{ behavior: 'allow', updatedInput }`
 *   (defer to general permission system)
 * - `toAutoClassifierInput` → `''`
 *   (skip classifier — security-relevant tools must override)
 */
const TOOL_DEFAULTS = {
  isEnabled: () => true,
  isConcurrencySafe: (_input?: unknown) => false,
  isReadOnly: (_input?: unknown) => false,
  ...
}

这意味着新工具默认不可并发执行——partitionToolCalls()restored-src/src/services/tools/toolOrchestration.ts:91-116)会将未声明 isConcurrencySafe: true 的工具放入串行队列。当 isConcurrencySafe 的调用抛出异常时,catch 块也返回 false——保守方向的兜底:

// restored-src/src/services/tools/toolOrchestration.ts:98-108
const isConcurrencySafe = parsedInput?.success
  ? (() => {
      try {
        return Boolean(tool?.isConcurrencySafe(parsedInput.data))
      } catch {
        // If isConcurrencySafe throws, treat as not concurrency-safe
        // to be conservative
        return false
      }
    })()
  : false

权限系统遵循同样的原则(详见第16章)。权限模式从最严格到最宽松:defaultacceptEditsplanbypassPermissionsautodontAsk。系统默认使用 default——用户必须主动选择更宽松的模式。

YOLO 分类器的拒绝追踪是另一种体现(restored-src/src/utils/permissions/denialTracking.ts:12-15):DENIAL_LIMITS 设定连续 3 次或总计 20 次被分类器拒绝后,系统自动回退到用户手动确认——在自动化决策不可靠时,回退到人类决策(完整代码详见第27章模式二)。

反模式:默认开放,出事再关。工具默认可并发执行,某个有副作用的工具在并行执行中产生竞态条件——这种 bug 极难复现和诊断。


25.4 原则四:A/B 测试一切

定义:行为变更先在内部用户群体中验证,通过数据确认后再扩展到所有用户。

Claude Code 拥有 89 个 Feature Flag(详见第23章),其中相当一部分用于 A/B 测试。最值得关注的不是 flag 数量,而是门控模式。

USER_TYPE === 'ant' 门控是最直接的暂存机制(详见第7章)。源码中存在大量的 ant-only 段落,例如 Capybara v8 的过度注释缓解措施:

// restored-src/src/constants/prompts.ts:205-213
...(process.env.USER_TYPE === 'ant'
  ? [
      `Default to writing no comments. Only add one when the WHY
       is non-obvious...`,
      // @[MODEL LAUNCH]: capy v8 thoroughness counterweight
      // (PR #24302) — un-gate once validated on external via A/B
      `Before reporting a task complete, verify it actually works...`,
    ]
  : []),

注释 un-gate once validated on external via A/B 清晰展示了这个流程:先在内部验证,确认有效后通过 A/B 测试推广给外部用户

GrowthBook 集成提供了更精细的实验能力:tengu_* 前缀的 Feature Flag 通过远程配置服务器控制,支持按百分比灰度。_CACHED_MAY_BE_STALE_CACHED_WITH_REFRESH 两种缓存策略的存在(详见第7章),体现了"缓存感知的 A/B 测试"——flag 值的切换不应导致缓存失效。

反模式:Big Bang 发布。直接将行为变更推送给所有用户。在 AI Agent 领域,行为变更的影响通常不是"崩溃"而是"不够好"或"太激进"——需要量化度量和对照组才能发现。


25.5 原则五:先观察再修复

定义:在尝试修复问题之前,先建立可观测性基础设施来理解问题的全貌。

缓存中断检测系统(restored-src/src/services/api/promptCacheBreakDetection.ts)是这一原则的典范。这个系统不修复任何问题——它的全部职责是观察和报告

  1. 调用前recordPromptState() 记录近 20 个字段的快照
  2. 调用后checkResponseForCacheBreak() 对比前后状态,识别哪个字段变化
  3. 生成解释:翻译为人类可读原因——"system prompt changed"、"TTL likely expired"
  4. 生成 DiffcreatePatch() 输出前后提示词状态对比

特别值得注意的是 PreviousState 中的注释风格(restored-src/src/services/api/promptCacheBreakDetection.ts:36-37):

/** Per-tool schema hash. Diffed to name which tool's description changed
 *  when toolSchemasChanged but added=removed=0 (77% of tool breaks per
 *  BQ 2026-03-22). AgentTool/SkillTool embed dynamic agent/command lists. */
perToolHashes: Record<string, number>

这里引用了具体的 BigQuery 查询日期和百分比数据(77%),说明团队在用数据驱动可观测性的粒度设计——不是随意追踪所有字段,而是基于生产数据发现"大多数工具 Schema 变化来自某个特定工具的描述变动",然后有针对性地添加 per-tool 哈希。

YOLO 分类器的 CLAUDE_CODE_DUMP_AUTO_MODE=1(详见第17章)遵循同样模式:提供完整的输入/输出导出能力,让开发者精确理解"分类器为什么拒绝了这个操作"。

反模式:凭直觉修复。看到缓存命中率下降就回滚最近修改,但实际原因可能是 Beta Header 切换、TTL 过期、或 MCP 工具列表变化。


25.6 原则六:锁存(Latch)以求稳定

定义:一旦进入某个状态,就不再摇摆——状态抖动比次优状态更有害。

"锁存"(Latch)模式在 Claude Code 中有多处体现:

Beta Header 锁存(详见第13章):afkModeHeaderLatchedfastModeHeaderLatchedcacheEditingHeaderLatched。会话中首次发送某个 Beta Header 后,后续所有请求继续发送,即使功能已关闭。原因:取消发送改变请求签名,导致缓存前缀失效。

缓存 TTL 资格锁存(详见第13章):should1hCacheTTL() 在会话中只执行一次,结果被锁存。源码注释(promptCacheBreakDetection.ts:50-51)确认:

/** Overage state flip — should NOT break cache anymore (eligibility is
 *  latched session-stable in should1hCacheTTL). Tracked to verify the fix. */
isUsingOverage: boolean

自动压缩熔断器restored-src/src/services/compact/autoCompact.ts:67-70):

// Stop trying autocompact after this many consecutive failures.
// BQ 2026-03-10: 1,279 sessions had 50+ consecutive failures
// (up to 3,272) in a single session, wasting ~250K API calls/day globally.
const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3

连续 3 次失败后锁存到"停止压缩"状态。注释中的 BigQuery 数据(1,279 个会话、250K API 调用/天)提供了充分的工程理由。

反模式:状态抖动。每次请求都重新计算配置,导致状态在不同值之间切换。在缓存系统中意味着缓存键不断变化,命中率趋近于零。


模式提炼

六条原则汇总表

原则核心源码回溯反模式
提示词即控制面prompts.ts:203 + system-reminder 注入链 — 主提示词与消息级提醒协同行为硬编码:为每种不希望的行为写检测器
缓存感知设计prompts.ts:114 + stopHooks/forkedAgent — 动态内容外移与侧信道复用提示词频繁变动:agent 列表内联占 10.2% cache_creation
失败关闭Tool.ts:748-761isConcurrencySafe: false默认开放:新工具直接可并发,出竞态再修
A/B 测试一切prompts.ts:210un-gate once validated via A/BBig Bang 发布:变更直接推送所有用户
先观察再修复promptCacheBreakDetection.ts:36 — 77% 数据驱动凭直觉修复:不看数据直接回滚
锁存以求稳定autoCompact.ts:68-70 — 250K API 调用/天的教训状态抖动:每次请求重新计算所有状态

表 25-1:驾驭工程六原则汇总

原则间的关系

graph TD
    A["原则1: 提示词即控制面<br/>行为引导的主要手段"] --> B["原则2: 缓存感知设计<br/>提示词变更有成本"]
    B --> F["原则6: 锁存以求稳定<br/>避免缓存抖动"]
    A --> C["原则3: 失败关闭<br/>安全默认值"]
    C --> D["原则4: A/B 测试一切<br/>验证后再开放"]
    D --> E["原则5: 先观察再修复<br/>数据驱动决策"]
    E --> B

图 25-1:六条驾驭工程原则的关系图

提示词即控制面出发:既然行为主要由提示词控制,提示词变更就需要缓存感知设计来控制成本,需要锁存以求稳定来防止抖动。行为的安全边界由失败关闭保障,从关闭到开放的过渡需要A/B 测试验证。出现问题时,先观察再修复确保理解全貌后再行动,观察结果反馈到缓存感知设计中。

模式:提示词驱动行为控制

  • 解决的问题:如何引导 AI 模型行为而不与模型能力迭代产生耦合
  • 核心做法:用自然语言提示词表达行为期望,用代码仅处理结构性约束
  • 前置条件:模型具备足够的指令跟随能力

模式:带外控制信道

  • 解决的问题:高频变化的运行时引导会让主系统提示词膨胀、抖动并破坏缓存
  • 核心做法:把稳定的行为宪法留在系统提示词里,把短寿命、条件性指导放进 <system-reminder> 这类元消息
  • 前置条件:模型能够区分用户意图和 harness 注入的控制消息

模式:缓存前缀稳定化

  • 解决的问题:提示词缓存因微小变动频繁失效
  • 核心做法:静态/动态边界分离 + 日期记忆化 + Header 锁存 + Schema 缓存
  • 前置条件:使用支持前缀缓存的 API

模式:缓存安全侧信道查询

  • 解决的问题:快速侧问题打断主循环或破坏主会话的缓存前缀
  • 核心做法:保存主线程的 cache-safe 前缀快照,fork 出一个受限的单次查询,结果不写回主对话历史
  • 前置条件:运行时能够复用父级的 cache-safe 消息前缀,并隔离 sidechain 的状态与 transcript

模式:失败关闭默认值

  • 解决的问题:新增组件引入安全或并发风险
  • 核心做法:所有属性默认为最安全值,显式声明才能解锁
  • 前置条件:有明确的"安全"和"不安全"定义

用户能做什么

  1. 将行为指令与代码逻辑分离。创建行为配置文件(类似 CLAUDE.md),让行为调整不需要代码变更
  2. 在引入提示词缓存前,先设计缓存边界。区分跨用户共享内容和会话级内容
  3. 审查你的默认值。对每个配置项问:如果用户不设置它,系统的行为是最安全的还是最危险的?
  4. 为关键行为变更设计灰度方案。即使只有两个用户群体(内部/外部),也比全量发布安全
  5. 在修复之前添加日志。缓存命中率下降或模型行为异常时,先记录完整上下文,再尝试修复
  6. 识别系统中的"锁存点"。哪些状态在会话生命周期中不应该变化?提前设计稳定性机制
  7. 把高频变化的引导移出主提示词。稳定规则放 system prompt,短寿命运行时开关放 system-reminder 或附件消息
  8. 为快速侧问题设计单独 sidechain。优先选择"无工具、单回合、复用缓存、结果不回写主线程"的实现,而不是硬插进主对话