一场泄露看懂 Claude Code:Harness 是让 Agent 干活靠谱的关键
2026 年 4 月 1 日,Anthropic 的 Claude Code 完整源码通过 npm 包泄露。Source Map 一打开,1903 个文件,51 万行 TypeScript,全部摊在眼前。
源码里藏着一个完整的宠物扭蛋机
大家第一时间在源码里发现了一个完整的宠物系统——Buddy。输入 /buddy 就能”孵化”一只专属 CLI 宠物:18 个物种、5 档稀有度(legendary 仅 1%)、5 项随机属性、6 种眼型、8 种帽子、1% 闪光概率、3 帧 ASCII 动画。每个用户的宠物由 userId + SALT 确定性生成。
一个 51 万行的生产级 AI Agent 里,藏着一个如此用心的宠物系统。但仔细看代码,有几处让人忍不住多想:
证据一:SALT = 'friend-2026-401'——friend + 2026 年 4 月 1 日。泄露日期精确到天。
证据二:Teaser Window 精确到 April 1-7, 2026。注释写的是 "Sustained Twitter buzz instead of a single UTC-midnight spike"——这不像是工程师对内部功能的描述,更像是营销策划的用语。
证据三:18 个物种名全部用 String.fromCharCode(0x…) 构造(hex 编码),原因是 capybara 碰撞了 Anthropic 下一代模型的内部代号(出现在 excluded-strings.txt 黑名单里)。为了不让它特别突出,所有物种都统一编码——“so one doesn’t stand out”。但 capybara 正好是此前被泄露的新模型名字。
证据四:统一 hex 编码反而让每个逆向工程者都去解码了——如果目标是隐藏,效果恰好相反。
这场泄露真的是巧合吗?
有三种可能的解读:
- A. 纯巧合(10%):Buddy 是计划中的愚人节彩蛋,source map 是配置失误,碰巧同一天。需要相当大的巧合。
- B. 技术团队”不小心”(55%):有人在那次构建中”不小心”开启了 source map。法务发 DMCA 是真实的应激反应,但十几个小时的窗口期已经足够代码传遍全球。Buddy 彩蛋是提前埋好的引爆物。
- C. 其他可能:完全意外但事后默许(20%),或公司策划(15%)。
不管答案是什么,结果是一样的:全球开发者免费做了一次深度代码审查和口碑传播。这可能是 2026 年最成功的技术营销,无论是否有意为之。
真正的价值:一扇罕见的窗口
这场泄露的技术价值不在于某个具体实现多巧妙,而在于它提供了一个罕见的窗口:一个日活用户庞大的商业级 AI Agent 产品,在工程层面到底在解决什么问题? 过去两年,AI Agent 从论文概念走向产品现实,但绝大多数公开讨论停留在两个极端——要么是”让模型调工具”的入门教程,要么是”AGI 即将到来”的宏大叙事。中间那一层,几乎没有人讲清楚过。
读完这份源码,最强烈的感受是:Agent 的核心难题不在”让模型调用工具”,而在模型、提示词和工具之外。权限怎么判、错误怎么恢复、上下文怎么管理、缓存怎么保持、并行怎么协调、怎么隐藏中间错误——这些工程才是一个 Agent 产品从 Demo 到生产的真正门槛。而这套”模型之外的一切”,有一个正式的名字:Harness。
本文基于 Claude Code 源码和相关分析,系统性地拆解 Harness Engineering 这一 Agent 工程范式——它是什么、为什么重要、Claude Code 是如何实现的、以及我们能从中学到什么。
一、Harness Engineering:2026 年最值得关注的 Agent 工程范式
从 Prompt 到 Harness 的演进
过去几年,AI 工程实践经历了清晰的演进路线。最早是 Prompt Engineering,关注”问什么”——优化输入给模型的指令。然后是 Context Engineering,关注”看什么”——系统性管理模型能看到的信息。而到了 2026 年,行业正在走向 Harness Engineering,关注”整个系统”——模型运行的全部基础设施。
三者是层层包含的关系:Prompt ⊂ Context ⊂ Harness。
Harness 的定义很直接:模型之外的一切。上下文怎么给、工具怎么调、出错怎么恢复、安全怎么保障、缓存怎么共享、并行怎么协调。模型能力正在趋于商品化,竞争优势正在转移到模型之外的工程实践上。
这不是空谈。LangChain 在 Terminal Bench 2.0 上做过一个实验:不换模型,只改 Harness,准确率从 52.8% 跳到 66.5%,排行榜排名从 30 名开外直接进入前 5。OpenAI 内部也有类似的案例:3 名工程师用 Agent 加合理的 Harness,5 个月完成约百万行代码和约 1500 个 PR。
Claude Code 本身就是 Harness Engineering 最好的实战样本。51 万行 TypeScript 里,绝大部分代码不是在做”让模型调工具”,而是在做工具调用之后的一切。
Agent 的核心公式
理解 Harness 需要先理解 Agent 的核心公式。一个 Agent 要解决三件事:
| 层面 | 取决于 | 类比 |
|---|---|---|
| 智力——能不能想明白 | Model | 大脑 |
| 能力——能不能干成事 | Environment | 手脚 |
| 靠谱——会不会干错事 | 约束/验证/纠正 | 缰绳 |
用公式表达就是 Agent = Model + Harness,而 Harness 包含两大部分:Environment(让 Agent 能干事)和约束/验证/纠正(让 Agent 靠谱地干事)。
在 Claude Code 的实现中,Environment 包括 40 个内置工具、五层上下文压缩、Prompt Cache 经济学、CLAUDE.md 记忆体系、Dream 离线巩固、Side Query 并行调用、投机执行等。约束/验证/纠正则包括 Fail-closed 默认值、工具白名单、Shell 语义解析、LLM 权限分类器、Hook 系统、熔断器、消息扣留、模型降级、死亡螺旋防护等。
两者缺一不可:只有 Environment 是失控的天才,只有约束是安全的废物。后面的内容就按这两条线展开。
二、架构全貌:一个 while(true) 撑起的 1700 行状态机
ReAct 循环的表面与现实
几乎所有 Agent 框架的核心都是一个 ReAct 循环:调用模型 → 解析输出 → 执行工具 → 把结果塞回消息 → 再调用模型。用 Python 伪代码写出来不超过 10 行。Claude Code 也不例外,它的主循环就在 query.ts 里——但这个循环写了 1700 行。
差距在哪里?简单版是一个 while 循环,Claude Code 版是一个有 7 个命名 continue 分支的状态机。简单版出错就报错,Claude Code 做静默升级、多轮接续、消息扣留。简单版工具顺序执行,Claude Code 做流式并行加并发安全标记。简单版没有上下文管理,Claude Code 有五层压缩管线。简单版没有安全检查,Claude Code 有五层权限判断加熔断器。简单版没有缓存考虑,Claude Code 的缓存经济学贯穿全局。
这 1700 行循环里的每一个 continue 分支都带一个语义化的 transition.reason 标签:输出触顶升级、响应式压缩重试、上下文崩塌清理、stop hook 阻塞、token 预算续航……每条”为什么要再跑一轮”都有名字、有追踪、有独立的恢复逻辑。状态在循环顶部解构,在 continue 分支处整体替换,每次迭代都像一个新的回合。这不是随手写的 while(true),而是一个有完整状态机语义的循环。
用 React 写 CLI
Claude Code 有一个出人意料的技术选型:整个终端界面用 React(Ink 框架)渲染。源码中包含 552 个 .tsx 文件,入口文件是 cli.tsx——注意后缀。
乍一看这很反直觉,但考虑到 Claude Code 的 UI 需求就能理解了:流式输出、多工具并行执行状态、权限确认对话框、文件 diff 预览——这些都是高度动态的 UI 场景。传统 CLI 写一个 spinner 就够折腾,Claude Code 需要同时显示好几种实时更新的信息。只有声明式框架能优雅处理这种复杂度。
React 框架的启动成本不低,但 Claude Code 用了一个巧妙的冷启动优化:在 cli.tsx 入口处,--version 参数走一条零导入路径,直接打印编译时内联的版本号然后退出,一个 React 模块都不加载。其他命令走独立的 import() 路径,只有最终进入主循环才加载完整的 React 应用。
七个基础工具构成完备能力
Claude Code 内置了 40 个工具,但核心只需要 7 个就能覆盖几乎所有任务:
| 工具 | 功能 |
|---|---|
| Read / Write / Edit | 文件操作 |
| Glob / Grep | 文件发现与内容搜索 |
| Bash | Shell 命令执行 |
| Agent | 子 Agent 调度 |
Anthropic 有一个核心观点:文件系统是 Agent 的交互总线。所有信息都通过文件来持久化、迭代和版本控制。Claude Code 的实现完美印证了这一点:记忆用 Markdown 文件、配置用 CLAUDE.md、工具结果存磁盘、投机执行用覆盖层文件系统。
三、Environment:让 Agent 能干事
Environment 是 Harness 的第一大组成部分,负责给 Agent 足够的感知、行动和学习能力。Claude Code 在这一层的工程量远超预期——Prompt Cache 经济学、五层上下文压缩、并行 LLM 调用、记忆系统与离线巩固,每一个都值得单独展开。
Prompt Cache 不是优化,是架构约束
如果只能从 Claude Code 源码中学一条原则,我选这条:Prompt Cache 命中率是第一天就要考虑的架构约束,不是上线后再优化的性能问题。
缓存边界写进了系统提示的物理结构
系统提示里有一个显式的 SYSTEM_PROMPT_DYNAMIC_BOUNDARY 标记,把提示词物理切成两段:标记之前的内容跨用户可缓存,标记之后包含会话特定内容。注释带着大写 WARNING:不要移除或重排这个标记,否则缓存逻辑会崩。
这意味着系统提示词的组织结构首先由缓存边界决定,其次才是语义逻辑。大部分人习惯按语义分段写 prompt,但在生产系统里,按缓存边界分段可能更重要。
Fork Agent 的缓存共享
Claude Code 经常需要在主循环之外启动并行 Agent——做压缩、提取记忆、投机执行、生成摘要。每次 fork 时,系统传入一个叫 CacheSafeParams 的参数包,包含系统提示、用户上下文、工具列表、消息前缀、thinking 配置。这些必须和父 Agent 的请求字节级一致,才能命中同一份 Prompt Cache。
代码注释写得非常清楚:API cache key 由 system prompt、tools、model、messages prefix 和 thinking config 共同决定。甚至 thinking config 里的 maxOutputTokens 也会影响——在旧模型上它会改变 budget_tokens,导致缓存失效。
在每个 turn 的边界,系统把当前的 CacheSafeParams 保存到一个全局 slot,供所有 post-turn 的 fork 复用。整个代码库里有 9 个不同的 fork 调用者,每个都走这套缓存共享。
全局影响:每个架构决策都在围绕”不要破坏 cache”做设计让步
工具结果也在为缓存让路。超出阈值的工具输出存磁盘,替换决策被冻结——同一条消息在不同时刻必须产生完全相同的字符串,否则 cache 就废了。消息序列化要求确定性的 JSON key 顺序。工具定义放在系统提示中以保持稳定缓存。
Cache 命中与未命中的差别不是百分之几十,而是成本和延迟的量级差别。这不是微优化,它决定了消息怎么序列化、子 Agent 怎么分叉、工具结果怎么存储。
五层上下文压缩管线
一个常见的误解是”上下文压缩就是调一次 LLM 把历史摘要一下”。Claude Code 的做法说明这远远不够——不同类型的信息有完全不同的”保质期”,需要完全不同的处理方式。
它实际上有五层不同粒度的压缩机制,按顺序依次执行:
Layer 1:Tool Result Budget——最前面的一层。巨量工具输出存磁盘,模型只看预览加文件路径。替换决策冻结以保护缓存。
Layer 2:HISTORY_SNIP——最精细的裁剪。某些消息直接删掉,不做任何摘要。比如搜索返回 500 行但模型只用了 3 行,剩下 497 行是纯噪声,摘要它也是浪费 token,直接删最划算。
Layer 3:Microcompact——在 API 缓存层面做编辑,利用 cache_edits API。不改本地消息内容,而是在 API 请求中附带缓存编辑指令。本地消息历史完全不变,压缩在 API 层完成。
Layer 4:CONTEXT_COLLAPSE——把旧的对话轮次归档成摘要,维护一个类似 git log 的结构。和全量压缩的区别是它保留了结构——哪一轮做了什么、结论是什么。
Layer 5:Autocompact——最后的兜底。先尝试轻量的 session memory 压缩,不够用才做完整压缩。
核心原则是:前面能搞定就不触发后面。大部分时候最重的 Autocompact 根本不需要跑。
熔断器:防止在压缩上无限烧钱
Autocompact 有一个熔断器——连续 3 次压缩失败后就放弃重试。代码注释里直接引用了内部数据:曾经有 1279 个会话出现 50 次以上的连续失败,最极端的一个会话失败了 3272 次,每天全球浪费约 25 万次 API 调用。这个真实数据促成了 MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3 这个常量的加入。
这里的设计启示很明确:不同保质期的信息需要不同的策略。工具中间输出可能几轮之后就没用了(SNIP 直接删),对话结构信息需要压缩但要保留骨架(COLLAPSE 归档),全局背景信息需要持久保留。一种压缩搞不定所有场景,需要一条管线。
Side Query:把”调用 LLM”变成到处撒的轻量操作
Claude Code 有一个叫 sideQuery 的抽象——一个轻量的、非流式的 API 封装,专门用于在主循环之外发起辅助 LLM 调用。它被用在至少五类场景中:
| 调用场景 | 模型 | 说明 |
|---|---|---|
| 权限分类 | 小模型 | 判断工具调用是否安全 |
| 记忆检索 | 小模型 | 哪些 CLAUDE.md 与当前任务相关 |
| Tool Use Summary | Haiku | 异步总结工具操作 |
| Agent 摘要 | 小模型 | 子 Agent 进度报告 |
| 提示建议 | 小模型 | 预测用户下一步 |
其中 Tool Use Summary 的设计尤其精妙:主模型推理时(5-30 秒),Haiku 同时总结上一轮的工具操作。这个摘要在 1 秒内完成,延迟完全被藏在主模型的推理时间里。等到后续需要压缩上下文时,用这个预先生成好的摘要替代原始工具输出,大幅省 token。
这是一个重要的架构观念:主 Agent 循环不应该是唯一调用 LLM 的地方。权限判断、摘要生成、记忆检索这些事情可以用更小更快的模型并行处理。把”调用 LLM”当成可以到处撒的轻量操作,而不是只有在主循环里才能做的重量级事件。
记忆架构:Markdown 为什么比向量数据库更好
Claude Code 的记忆系统没有用向量数据库,而是用纯 Markdown 文件:
| 文件 | 用途 |
|---|---|
CLAUDE.md |
项目级记忆与约定 |
AGENTS.md |
Agent 能力与角色描述 |
memory/YYYY-MM-DD.md |
按日期归档的交互日志 |
MEMORY.md |
核心事实与用户偏好 |
这个选择看似反直觉,实际极其务实。Markdown 文件透明可编辑——直接打开就能看 AI 记了什么,记错了直接改,向量数据库做不到。它天然支持 Git 版本控制,每次记忆修改可追溯可回滚。而且和文件系统交互总线的理念一脉相承,与 Agent 的其他组件天然集成。
Dream:Agent 的”睡眠学习”
源码里有一个叫 Dream 的后台记忆巩固系统。触发条件是:距上次巩固超过 24 小时,且期间有至少 5 个会话,并且能获取文件锁。门控逻辑按成本从低到高排列:先查时间,再扫描会话数量,最后试获取文件锁。
满足条件后,系统在后台启动一个 fork Agent,把近期会话的经验整理成长期记忆文件。巩固过程分四个阶段:
- Orient——扫描记忆目录,读取索引,了解当前知识状态
- Gather——收集新信号,grep 近期会话中值得记住的信息
- Consolidate——合并新信号到已有主题文件,避免近似重复,将相对日期转换为绝对日期,删除被推翻的旧事实
- Prune——修剪索引,保持精简(索引 <25KB,每条 <150 字符)
巩固提示的核心指令这样写道:”You are performing a dream — a reflective pass over your memory files. Synthesize what you’ve learned recently into durable, well-organized memories so that future sessions can orient quickly.”
人类睡觉时大脑巩固记忆,Agent 空闲时巩固会话记忆。叫 Dream 确实贴切。阈值通过 GrowthBook 远程可调,可以针对不同用户群调整巩固频率。
四、约束与验证:让 Agent 不出错
Environment 让 Agent 强大,但只有约束与验证才能让它可靠。Claude Code 在这一层的代码量甚至超过了 Environment——Fail-closed 默认值、多层权限系统、工具编排、投机执行的安全边界、多 Agent 能力分区,每一个都是硬约束。
Fail-closed 的工具默认值
Claude Code 的 buildTool() 工厂函数提供的默认值非常值得品味:
1 | TOOL_DEFAULTS = { |
每一个默认值都选择了更保守的选项。忘了声明只读?当写操作处理,需要更严格权限。忘了声明并发安全?独占运行。忘了声明比错误声明安全得多。这是 Fail-closed 原则在工具系统层面的体现。
LLM 作为权限分类器
Claude Code 的 Auto Mode 实现了一个前瞻的模式:用 LLM 判断另一个 LLM 的工具调用是否安全。
分类器的输入构造特别讲究:它从对话历史中只提取 tool_use block——不包含助手的自由文本。原因很直接:如果分类器能看到助手的自然语言输出,攻击者可以通过让主模型输出特定文本来误导分类器。只看结构化的工具调用,攻击面小得多。
每个工具还有一个 toAutoClassifierInput() 方法控制分类器能看到自己的哪些输入。只读工具(Read、Grep、Glob)被白名单直接跳过。
五层权限判断的完整链路
从工具调用到最终决策,经过五层判断,每层都有明确的否决权:
- Settings 静态规则——alwaysDeny / alwaysAllow / alwaysAsk,快速裁剪
- PreToolUse Hook——用户自定义脚本,退出码 2 表示阻止
- Tool 自身属性——isReadOnly 的工具白名单直接放行
- LLM Auto-Classifier——sideQuery 调用,只看 tool_use block
- 拒绝熔断器——连续 3 次或累计 20 次拒绝后回退交互式提问
设计哲学是每一层只负责一个关注点。静态规则快速裁剪,Hook 支持企业定制,LLM 处理无法预定义的模糊场景,熔断器保底防卡死。
Shell 安全:语义解析远胜于关键词黑名单
Bash 工具的安全机制远不止”禁止 rm -rf /“。源码里有一个完整的命令语义解析器,把每条 git 子命令的每个 flag 都做了类型化标注,处理了 -- 终止符、UNC 路径、复合命令拆解等边界情况。
代码注释里记录了一个真实的安全案例:git diff 的 -S flag 之前被标记为 none(不带参数),但 git 的实际行为是 -S 强制消费下一个 argv。攻击者可以构造 git diff -S -- --output=/tmp/pwned,让验证器认为 -S 不带参数 → 推进 1 个 token → 遇到 -- 停止检查 → --output 未检查 → 任意文件写入。修复方法是把 -S 改成 string 类型。
这个案例说明了一个普遍规律:安全机制的粒度应该和攻击面的粒度匹配。
流式执行 + 并发安全标记
一般 Agent 的实现是等模型说完再执行工具。Claude Code 不等——模型还在流式输出后面的内容,前面的工具就已经在跑了。
每个工具有一个 isConcurrencySafe 标记。读文件、grep 这类只读操作可以并行,写文件、bash 这类需要独占。系统动态分批:同一批内的并发安全工具同时执行,遇到非并发安全工具就切成新的串行批次。结果按接收顺序缓冲输出,不会乱序。
还有一个精细的设计:Bash 错误级联。当一个 Bash 命令出错时,siblingAbortController 会立刻终止所有同级工具进程——但只有 Bash 错误会触发,因为 Bash 命令经常有隐式依赖链(mkdir 失败了后面的 cd 也没意义)。而 Read/Grep 这类独立查询的失败不会级联。终止同级但不终止父级——主循环继续运行,因为模型需要看到错误并决定下一步。
投机执行:在用户打字时就开始干活
Claude Code 有一套完整的投机执行系统,它不只是”预测下一步”——真的在覆盖层文件系统上执行。流程如下:
- 用户还在打字时,系统用 Prompt Suggestion 预测下一步
- 启动一个受限的 fork Agent
- Agent 在覆盖层文件系统上执行:读操作先检查覆盖层再回退真实磁盘,写操作只写入覆盖目录
- 用户接受→覆盖层”提升”到真实磁盘;用户拒绝→覆盖层直接删掉
严格的工具白名单控制了投机能做什么:写操作只允许 Edit/Write/NotebookEdit 且必须在权限允许自动接受时,Bash 只在通过只读验证时。投机最多 20 轮或 100 条消息,transcript 不写入主会话。
投机执行的核心不是”预测准不准”,而是”预测错了零代价”。 覆盖层文件系统 + 工具白名单 + 主会话隔离,三层保证了做对了赚时间,做错了零成本。
多 Agent 的本质:能力分区,而非角色扮演
Claude Code 的多 Agent 不是”给不同 Agent 写不同的角色提示词”。核心是工具面(tool surface)的显式划分:
| Agent 类型 | 工具面 |
|---|---|
| 主 Agent | 完整工具集 |
| 子 Agent | 禁止递归创建 Agent、退出计划模式等 |
| Coordinator | 只有 3 个工具:创建/发消息/停止 worker |
| Async Agent | 只有文件读写、搜索、shell |
| In-process Teammate | Async + 任务管理 + cron |
Agent”是谁”不是由 system prompt 决定的,而是由它能做什么决定的。Capability-based 的身份定义比角色提示词更硬性、更可审计、更难绕过。角色提示词可以被模型”创造性地解读”,但工具禁用集合是硬约束——没有这个工具就是调不了。
子 Agent 加载 MCP 工具时还会检查信任边界:只有 isSourceAdminTrusted 的 Agent 才能使用 MCP 工具。这是信任边界在代码里的显式编码。
五、纠正:出错时怎么办
Agent 最怕的不是某个操作失败,而是在失败上无限重试、烧掉整个 token 预算。Claude Code 的错误恢复策略围绕一个核心原则:在确认无法恢复之前,不暴露中间态。
输出触顶的两步恢复
模型输出撞到 max_output_tokens 时,系统不是直接报错,而是两步恢复:
第一步:静默升级上限。 如果触顶时用的是默认 8K,系统直接用 64K 重发——不加 meta 消息,对用户完全透明,只火一次。
第二步:多轮接续。 如果 64K 还不够,才注入一条 meta 消息让模型继续。措辞很讲究:”Output token limit hit. Resume directly — no apology, no recap of what you were doing. Pick up mid-thought if that is where the cut happened.” 明确告诉模型不要道歉、不要复述、直接接着说。最多 3 次。
消息扣留:恢复循环的关键机制
整个恢复期间,错误消息被扣留(withheld),不发给外部消费者(桌面客户端、IDE 插件、远程会话)。因为那些客户端看到 error 字段就会终止会话——恢复循环还在跑但已经没人在听了。
恢复成功,消费者永远不知道出过错。所有恢复手段用尽了,扣留的错误才被释放。
这个设计的哲学是:错误处理的边界不是”单次 API 请求”,而是整个恢复循环。在循环结束前,消费者不应看到任何中间状态。
模型降级
主模型不可用时(过载、服务中断),系统自动切换到备用模型。关键难点是不同模型可能有不同的 tool_use 格式和签名机制,降级时需要清洗消息历史中的模型特定内容——丢弃旧 StreamingToolExecutor 中间结果,剥离旧模型特有的签名块,然后用备用模型重新发起请求。
熔断器无处不在
| 场景 | 阈值 | 说明 |
|---|---|---|
| 上下文压缩 | 连续 3 次失败 | 曾浪费 ~250K API 调用/天 |
| 权限分类 | 连续 3 次或累计 20 次拒绝 | 回退交互式提问 |
| 输出触顶 | 最多 3 次接续 | 先升级再接续 |
这些阈值不是拍脑袋定的。压缩熔断器的 3 次阈值来自真实产线数据:1279 个会话曾出现 50+ 次连续失败。消融实验基础设施让 Anthropic 可以量化每个熔断器的价值。
死亡螺旋防护
如果 API 调用出错 → 触发 stop hooks → stop hooks 也调 API → 也出错 → 又触发 stop hooks……这就是死亡螺旋。Claude Code 的规则很简单:API 错误时跳过所有 stop hooks。宁可丢失一次记忆提取和 prompt suggestion,也不能让系统陷入无限循环。
六、工程化实践:用科学方法做产品
消融实验基础设施
源码里有一个 ABLATION_BASELINE flag,启用后一次性关掉 7 个功能:thinking mode、上下文压缩、自动记忆、后台任务、简单模式、交织思考、第二个后台任务 flag。这让 Anthropic 可以跑对照实验:关掉某个功能后,用户的任务完成率、token 消耗、会话时长有没有显著变化。
做过 ML 研究的人都熟悉消融实验。但把这个方法论搬到产品工程上,在工业代码里是少见的。这意味着 Anthropic 不是”感觉有用就上”,而是”数据证明有用才上”。
一个有意思的细节:这段代码放在 cli.tsx(入口文件)而不是初始化函数里。原因是 BashTool、AgentTool 等模块在加载时就捕获了环境变量到模块级常量,init() 运行时已经来不及了。所以必须在任何 import 之前就把环境变量设好。
双层 Feature Flag
编译时 flag 用 Bun 的 feature() 宏实现,构建时被替换成 true/false,false 分支的代码被物理删除——不是运行时不执行,是从二进制文件里消失。安全研究员反编译也找不到。这用于未发布功能的门控。
运行时 flag 用 GrowthBook 实现,用于灰度发布和紧急 kill switch。所有 gate 名称以 tengu_ 开头(Claude Code 项目内部代号)。从磁盘缓存读取,接受脏读,不阻塞启动。
两层结合:编译时决定功能是否存在,运行时决定功能是否激活。
隐私工程:类型系统作为审计工具
埋点数据的类型叫 AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS。这个类型实际上是 never——不可能直接赋值。每次使用必须显式 as 转换,意味着每一处使用都是一个向代码审查者的声明:”我确认过这个字符串不包含敏感信息。”
MCP 工具名有专门的脱敏函数:用户自定义的 MCP 服务器名一律替换成 'mcp_tool',因为服务器名可能包含用户隐私。内置工具名直接放行。
SDK 客户端以 NDJSON 格式消费流式输出。Claude Code 在 process.stdout.write 上装了一个守卫:每一行输出都先 JSON.parse 验证,合法的转发 stdout,非法的转发 stderr 并打上 [stdout-guard] 标记。因为一个依赖库的 stray print 就能搞崩整个流式管道。
隐私保护不是事后加的”合规层”,而是编码进类型系统和运行时守卫里,让开发者在写代码时就被迫思考”这个字符串会不会泄露用户信息”。
防泄漏工程:Undercover Mode 与 Canary 检测
Claude Code 内部有一个叫 Undercover Mode 的机制,当检测到当前 repo 不在内部白名单时自动启用,剥除 commit message 和 PR 描述中的模型代号、内部项目名等信息。
这个开关的关键设计是:没有 force-OFF。注释写道:”There is NO force-OFF. This guards against model codename leaks — if we’re not confident we’re in an internal repo, we stay undercover.” 可以强制打开,但不能强制关闭。安全机制的非对称设计:因为泄露内部信息的代价远高于偶尔多一次保护的成本。
构建产物层面,Anthropic 维护了一份 excluded-strings.txt 黑名单,CI 会 grep 最终二进制检测内部代号。所有敏感字符串都用运行时构造绕过检查——比如 API key 前缀 sk-ant-api 写成 ['sk', 'ant', 'api'].join('-')。
最有趣的案例出现在 Buddy 宠物系统里:capybara(水豚)恰好是 Anthropic 某个模型的内部代号,直接写字符串会触发 canary 检测。为了不让它特别突出,所有 18 个物种名都统一用 hex 编码。安全检查能区分”宠物名”还是”模型代号”吗?不能。所以一视同仁。
七、六条核心设计原则
读完整份源码,可以提炼出六条贯穿始终的设计原则,前三条属于 Environment 层面,后三条属于约束/纠正层面。
Environment 原则
原则一:Cache 经济学是架构约束。 缓存命中率不是优化——它决定了消息怎么序列化、子 Agent 怎么分叉、工具结果怎么存储。第一天就画缓存边界图。
原则二:分层处理不同”保质期”的信息。 工具中间输出几轮后没价值(SNIP 删掉),对话结构需要压缩(COLLAPSE 归档),全局背景需持久保留。一种压缩搞不定所有场景,需要一条管线。
原则三:并行化一切可以并行的 LLM 调用。 sideQuery 把”调 LLM”变成到处撒的轻量操作。主模型推理期间,权限分类、记忆检索、摘要生成全部并行跑。
约束/纠正原则
原则四:熔断器要到处放。 Agent 最怕的不是失败,而是在失败上无限重试。每个恢复路径都有上限——压缩 3 次、权限 3 次、输出 3 次。
原则五:错误不要过早暴露。 在确认真的无法恢复之前,中间错误不应泄漏给消费者。静默升级、扣留错误、模型降级——“恢复循环”替代”单次重试”。
原则六:安全默认值必须保守。 工具默认不安全、默认有写操作。Undercover 默认开启、没有 force-OFF。不确定时,永远选更安全的选项。
前三条让 Agent 又快又聪明,后三条确保 Agent 又稳又安全。两者共同构成完整的 Harness。
结语:从 Demo 到生产的真正距离
很多 Agent 框架把精力花在”如何让模型调用工具”上。Claude Code 的 51 万行源码告诉我们,真正的工程量在模型、提示词和工具之外:
- 调了工具,权限怎么判?→ LLM 分类器 + 规则 + 熔断器 + 用户确认的四层体系
- 工具出错了怎么恢复?→ 扣留错误 + 静默升级 + 多轮接续 + 恢复耗尽才暴露
- 上下文太长怎么压缩?→ 五层管线,按信息保质期分别处理
- 多个工具同时跑怎么协调?→ 并发安全标记 + 错误级联 + 执行顺序保障
- 子 Agent 怎么分叉,缓存怎么共享?→ CacheSafeParams + 字节级一致 + 全局 slot
- 用户还没输入能不能先干着?→ 投机执行 + 覆盖层文件系统 + 主会话隔离
- 构建产物被分发出去了,内部信息怎么不泄漏?→ 字符串黑名单 + 编译时 DCE + Undercover Mode
这些”之后”的工程,才是从 Demo 到生产的真正距离。而这个距离,比大多数人想象的要远得多。
模型能力趋于商品化的今天,竞争优势正在转移到模型之外——正在转移到 Harness 上。Claude Code 的源码,为我们提供了一份迄今为止最详尽的 Harness Engineering 实战参考。