从 Claude Code 源码看 Coding Agent 的架构演进之路


导语

2024 年底到 2025 年初,Coding Agent 赛道迎来了一次范式跳跃。

从最早的 LangChain 式”显式编排”,到 AutoGPT 式”自主循环”,再到如今以 Claude Code 为代表的 Harness 范式——AI 编程助手的架构设计在短短两年内经历了三次根本性的转变。

Claude Code 的源码曾短暂泄露,社区 fork 了一份后很快被删除。基于对这份源码的研究,本文试图回答一个核心问题:为什么同样是”让 LLM 写代码”,不同架构的体验差距如此之大?

这篇文章不是一篇源码导读,而是对 Coding Agent 架构演进的思考总结。会从以下几个维度展开:

  1. Agent 架构的三次范式转变
  2. Harness 范式的工程设计哲学
  3. 如何让 Agent 不丢失上下文
  4. 如何让模型减少幻觉
  5. 工具系统的精妙设计
  6. Edge Case 的工程化处理

一、Agent 架构的三次范式转变

1.1 第一代:显式编排时代(LangChain / LangGraph)

2023 年,LangChain 几乎是所有 LLM 应用的标准起点。它的核心思想是显式编排:开发者用 Chain、Pipe、DAG 把 LLM 的调用流程”画”出来,当时这个“高大上”的使用方式让不少人作为一个新的技术范式来研究它。

1
2
3
4
5
6
7
8
# LangChain 的典型模式:开发者预定义工作流
chain = (
prompt_template
| llm
| output_parser
| tool_selector
| tool_executor
)

这个模式的问题在于:开发者必须预先知道所有可能的执行路径。每当你想支持一个新场景,就必须回去修改 chain 定义。这在面对代码编辑这种高度动态的场景时,说实话很难去做穷举。

更致命的是状态管理。LangChain 把状态管理的责任完全交给了开发者——你需要手动管理对话历史、手动处理上下文窗口溢出、手动定义错误恢复路径。当你的 Agent 需要在一个 10 万行的项目里做跨文件重构时,这种手动管理也是个比较复杂的任务,尤其是遇到编码场景等。

1.2 第二代:自主循环时代(AutoGPT / BabyAGI)

2023 年中,AutoGPT 的出现让所有人兴奋了一阵子。它的思路很简单:让 LLM 自己决定下一步做什么,循环执行直到任务完成

1
2
3
4
5
while not done:
thought = llm.think(context)
action = llm.decide_action(thought)
result = execute(action)
context.append(result)

这个方向是对的,但当时的执行有严重缺陷:

  • 上下文快速爆炸:没有压缩策略,几轮循环就塞满了上下文窗口
  • 幻觉失控:缺乏环境反馈的 grounding,LLM 经常在想象中”完成”了任务
  • 没有安全边界rm -rf / 说执行就执行,没有任何防护
  • 工具原始:只有最基本的文件读写和 shell 执行

AutoGPT 的方向正确但时机不对——当时的模型能力和上下文限制不足以支撑自主决策,而工程基础设施也远远不够,比方说没有稳定的长期记忆结构,没有稳定的状态压缩机制以及planning abstraction等等。

1.3 第三代:Harness 范式(Claude Code / Cursor / Codex CLI)

到了 2025 年,一种新范式逐渐成型。社区将这种模式概括为 Harness 范式(Anthropic 官方文档中更多使用”agentic systems”的表述,但 Harness 这个词精准地捕捉了其核心特征)。它的核心理念用一句话概括就是:

不要编排 LLM,要为 LLM 构建一个丰富的运行时环境,然后让它自己决定怎么做。

“Harness” 这个词很有意思——它既有”驾驭、利用”的意思,也有”安全带、保护装置”的含义。一个好的 Harness 既要赋能(给 LLM 足够的工具和信息),又要约束(确保安全、防止失控)。

Claude Code 则是这个范式目前最极致的实现。它的 Agent Loop 核心逻辑其实非常简洁:

1
2
3
4
5
6
7
8
while (模型未停止):
response = callAPI(messages)
if response 包含 tool_use:
results = executeTools(tool_calls)
messages.append(results)
continue // 模型自己决定下一步
else:
break // 模型认为任务完成

就这么几行伪代码所覆盖的逻辑,支撑起了一个能做跨文件重构、多代理协作、无限对话的编程助手。所有的”智能”不是来自复杂的编排逻辑,而是来自三个方面:

  1. 模型本身的推理能力
  2. 精心设计的系统提示
  3. 丰富且高质量的工具反馈

1.4 三代对比

维度 显式编排 (LangChain) 自主循环 (AutoGPT) Harness (Claude Code)
控制流 开发者定义 DAG LLM 自主但粗糙 LLM 自主 + 精密环境
状态管理 手动管理 几乎没有 多层自动管理
安全性 开发者负责 几乎没有 多层权限 + 沙箱
扩展性 改 chain 定义 改代码 加工具/MCP 服务器
上下文 固定窗口 快速溢出 多层压缩 + 无限对话
核心瓶颈 开发者的想象力 模型的自控力 模型的推理能力

这个演进的本质是:随着模型能力的提升,控制权从开发者逐渐转移到模型本身。LangChain 时代,开发者不信任模型,所以要用 DAG 来”管”它;Harness 时代,模型已经足够强,开发者要做的是构建好环境,然后 get out of the way


二、Harness 范式的工程设计哲学

理解了范式差异之后,再来深入的看看 Harness 范式在Claude Code工程层面是怎么落地的。

2.1 “操作系统”而非”框架”

Claude Code 的设计定位就不是一个”调用 LLM 的应用”或者中台,而是一个为 LLM 构建的完整操作系统。就像 Linux 为用户程序提供文件系统、进程管理、网络栈一样,Claude Code 为 LLM 提供:

  • 文件系统访问:读/写/精确编辑任意文件
  • 进程执行:运行 Shell 命令,支持超时和中断
  • 代码搜索:文件名匹配 + 内容正则搜索
  • 网络访问:搜索互联网 + 抓取网页
  • 子进程(子代理):递归启动子代理处理复杂任务
  • 外部协议:通过 MCP 协议接入任意外部工具
  • 语言服务器:实时获取代码诊断、定义跳转

这些能力不是简单的 API wrapper,每一个都经过了精心的工程设计。比如文件编辑不是简单的”覆写文件”,而是支持精确的行级编辑,并且会自动检测冲突、处理并发。

2.2 Agent Loop 的极简之美

整个系统的心脏是一个流式的 Agent Loop。它的设计体现了一个深刻的工程洞察:复杂的行为应该涌现自简单的规则,而不是被硬编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
用户输入

┌──────────────────────────────────────────────────┐
│ Agent Loop │
│ │
│ 1. 构建请求 (系统提示 + 历史消息 + 上下文) │
│ 2. 调用 API (流式) │
│ 3. 处理流式响应: │
│ ├── 文本块 → 实时渲染给用户 │
│ ├── tool_use 块 → 立即执行工具 │
│ └── thinking 块 → 内部推理(不展示) │
│ 4. 收集工具结果 │
│ 5. 判断是否继续: │
│ ├── 有 tool_use → 工具结果追加,回到步骤 2 │
│ ├── end_turn → 结束 │
│ ├── max_output_tokens → 自动恢复 (最多3次) │
│ └── prompt_too_long → 自动压缩,回到步骤 2 │
│ 6. Token 预算检查 │
│ 7. Turn 计数检查 │
│ │
│ [循环直到模型决定停止或触发终止条件] │
└──────────────────────────────────────────────────┘

输出结果

注意这里没有任何”if 用户要求重构 then 走重构流程”这样的分支逻辑。所有的任务路由、工具选择、执行策略都由模型在运行时动态决定

值得一提的是这个 Agent Loop 在代码层面的实现——query.ts 中的核心函数是一个 async function* query(),即 async generator。这个选择并非偶然:

  • 流式事件分发:UI 层可以通过 for await (const event of query(...)) 实时接收每一个流式事件(文本块、工具调用、思考过程),无需缓存完整响应
  • 随时可中断:配合 AbortController,调用方可以在任意时刻中断 generator,实现用户 Ctrl+C 的即时响应
  • 内存友好:不需要将整个对话的所有中间状态堆积在内存中,yield 出去的事件可以被即时消费

“极简之美”不只是概念上的简洁——Agent Loop 的核心逻辑约 1730 行(query.ts),用一个 async generator 就承载了流式处理、工具执行、自动压缩、错误恢复等所有能力。

2.3 流式执行:与时间赛跑

传统的工具执行是”请求-响应”模式——等 API 返回完整响应,解析出工具调用,再逐个执行。Claude Code 的做法更激进:

在 API 流式返回的过程中就开始执行工具

第一个 tool_use block 的 JSON 一完成解析,就立刻开始执行。后续的 block 继续接收,继续排队。这意味着网络传输和工具执行是重叠进行的,整体延迟大幅降低。

这种设计在编程场景下尤其重要——当模型决定同时读取 5 个文件时,你不需要等 5 个 tool_use block 全部接收完才开始读文件。第一个文件名一确定,文件读取就开始了。


三、如何让 Agent 不丢失上下文

上下文管理是 Coding Agent 最核心也比较容易被忽视的问题。一个典型的编程会话可能持续数小时,涉及几十个文件、上百次工具调用。如果每隔 20 分钟就”忘了”之前在干什么,体验就会断崖式下跌。

3.1 问题的本质

LLM 的上下文窗口是有限的。即使是 200K token 的窗口,在一个复杂的编程会话中也会很快填满——每次文件读取、每次命令输出、每条对话消息都在消耗 token。

传统方案很粗暴:要么截断历史消息(丢失上下文),要么限制对话长度(限制能力)。Claude Code 的方案精妙得多:多层压缩 + 持久记忆

3.2 六层上下文压缩策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
┌──────────────────────────────────────────────┐
│ 上下文管理的六道防线 │
│ │
│ 第一层: Auto Compact (自动压缩) │
│ - 监控 token 使用量 │
│ - 达到阈值时自动触发 │
│ - 将历史对话压缩为简洁摘要 │
│ - 保留关键决策和当前任务上下文 │
│ │
│ 第二层: Micro Compact (微压缩) │
│ - 针对单次工具调用结果的即时压缩 │
│ - 比如一个 grep 返回了 500 行结果 │
│ - 自动提取关键信息,压缩为摘要 │
│ - 缓存压缩结果,避免重复计算 │
│ │
│ 第三层: Reactive Compact (响应式压缩) │
│ - API 返回 prompt_too_long 错误时触发 │
│ - 紧急压缩以恢复对话 │
│ - 这是"最后一道防线" │
│ │
│ 第四层: Snip Compact (裁剪压缩) │
│ - 对历史消息进行智能裁剪 │
│ - 保留开头和结尾,压缩中间部分 │
│ │
│ 第五层: Session Memory (会话记忆) │
│ - 跨压缩边界保持关键信息 │
│ - 即使历史消息被压缩,关键决策仍然保留 │
│ - 持久化到磁盘 │
│ │
│ 第六层: Context Collapse (上下文折叠) │
│ - 实验性的更激进压缩策略 │
│ - 将多轮工具调用折叠为单条摘要 │
└──────────────────────────────────────────────┘

这套系统的精妙之处在于层层递进、各有侧重

  • Auto Compact 是常规操作,温和地压缩历史
  • Micro Compact 解决单次工具返回结果过大的问题
  • Reactive Compact 是紧急兜底
  • Session Memory 确保即使压缩了,关键信息也不丢失

3.3 Session Memory:跨越压缩边界的记忆

这是我认为最精妙的设计之一。

想象一个场景:你和 Agent 讨论了一个小时,做了很多决策——“我们决定用 Strategy 模式重构这个模块”、”这个 API 的返回格式必须保持向后兼容”、”测试覆盖率要达到 80%”。然后上下文满了,触发了自动压缩。

如果只做简单的摘要压缩,这些关键决策很可能被淹没在”读取了 file_a.ts”这样的工具调用记录中。

Session Memory 的做法是:在压缩之前,提取并持久化关键的上下文信息——用户的偏好、重要决策、当前任务状态。即使历史消息被大幅压缩,这些”记忆”仍然会被注入到后续的对话中。

这就像人类的记忆系统:你可能忘了上周二下午 3 点读了哪些代码,但你记得”我们决定用 Redis 做缓存”。

3.4 System Prompt 缓存:被忽视的成本杀手

上下文管理不只是关于”不丢信息”,还关于成本

Claude Code 的系统提示词有近千行,每次 API 调用都要发送。如果不做优化,光系统提示就要消耗大量 token。

它的解决方案是将系统提示分为两部分:

  • 静态部分:工具定义、行为规范、安全规则——这些跨会话不变
  • 动态部分:当前 Git 状态、项目配置、用户自定义规则——这些每次可能不同

通过在两部分之间插入一个特殊的分隔标记,API 层可以对静态部分做全局缓存,Prompt Cache 命中率最大化。据分析,仅仅将代理列表从工具描述移到附件中,就减少了 10.2% 的 cache_creation tokens。

这种”抠 token”的精神贯穿了整个系统。


四、如何让模型减少幻觉

幻觉(Hallucination)是 LLM 诞生以来就一直存在的天敌——在编码情况下模型”幻觉”出一个不存在的 API、一个错误的文件路径,就可能浪费用户大量时间。

Claude Code 的防幻觉策略可以归纳为一个核心原则:用环境事实来 ground 模型的每一步决策

4.1 Tool-Grounded Reasoning:用工具结果锚定推理

这是最根本的防幻觉机制。模型不是在”想象”代码库长什么样,而是通过工具调用实际查看代码库。

当模型需要修改一个文件时,它不会凭记忆”猜”文件内容,而是:

  1. 先用 GrepTool 搜索相关代码
  2. FileReadTool 读取完整文件
  3. 基于实际读取的内容生成编辑
  4. FileEditTool 执行精确编辑(而不是覆写整个文件)

每一步都有真实的文件系统反馈。如果文件不存在,工具会返回明确的错误信息,而不是让模型在幻觉中继续。

4.2 环境上下文注入:让模型知道自己在哪

Claude Code 会在每次对话中自动注入丰富的环境上下文:

上下文类型 注入内容
Git 状态 当前分支、最近提交、未提交的修改
项目配置 CLAUDE.md 项目级规则、技术栈信息
LSP 诊断 实时的编译错误、类型警告
文件结构 项目目录树的概览

这些信息让模型在开始推理之前就对”当前环境的真实状态”有准确的认知,而不是基于训练数据中的通用知识来猜测。

4.3 精确编辑 vs 全文覆写

这是一个常被忽视但极其重要的设计决策。

很多 Coding Agent 的文件编辑方式是”生成新的完整文件内容然后覆写”。这有两个严重问题:

  1. 幻觉风险高:模型需要”记住”文件的所有内容,很容易遗漏或修改不该改的部分
  2. Token 浪费:一个 1000 行的文件,改了 3 行,却要重新生成 1000 行

Claude Code 的 FileEditTool 采用的是精确编辑模式——指定要替换的旧字符串和新字符串。模型只需要关注需要修改的部分,大幅降低了幻觉的概率。

4.4 LSP 集成:编译器级别的事实核查

当模型完成代码编辑后,LSP(Language Server Protocol)会立刻提供诊断信息——类型错误、语法错误、未解析的引用等。

这相当于给模型配了一个”编译器级别的事实核查员”。如果模型幻觉出了一个不存在的函数调用,LSP 会立刻报告 Cannot find name 'xxx',模型可以据此自我修正。


五、工具系统的精妙设计

工具系统是 Harness 的核心。决定了 LLM 能做什么、做得多好。Claude Code 的 tools/ 目录下有 42 个子目录,每个对应一个工具,整个工具系统有几个比较典型的设计。

5.1 智能并发编排

当模型在一次回复中调用了多个工具时,系统不是简单地顺序执行,而是根据工具的特性自动决定并行还是串行

1
2
3
4
5
6
7
8
9
10
11
模型一次性发出工具调用:
[ReadFile("a.ts"), GrepTool("TODO"), ReadFile("b.ts"), FileEdit("c.ts"), ReadFile("d.ts")]

系统自动分区执行:
┌─ ReadFile("a.ts") ─┐
├─ GrepTool("TODO") ├── 并行执行 (都是只读操作)
├─ ReadFile("b.ts") ┘

├─ FileEdit("c.ts") ←── 串行执行 (写操作,必须独占)

└─ ReadFile("d.ts") ←── 继续执行 (只读操作)

每个工具自己声明”我是否可以安全并发”,系统据此自动编排。读操作之间自由并行(最高 10 并发),遇到写操作立刻切换为串行,写操作完成后再恢复并行。

这种设计在大型项目中效果显著——当模型需要同时读取 5 个文件来理解一个功能的实现时,5 次 IO 操作是并行的,而不是串行等待。

5.2 元工具:按需发现能力

当 Agent 接入大量 MCP 服务器后,可用工具数量可能达到上百个。把所有工具的 Schema 都塞进每次 API 调用会浪费大量 token,还会降低模型的工具选择准确率。

Claude Code 的解决方案是引入 ToolSearchTool——一个**”元工具”**(工具的工具)。工具数量超过阈值时,系统只加载核心工具 + ToolSearchTool,模型需要特定能力时先通过它按语义搜索,再调用匹配到的具体工具。这是”延迟加载”思想在工具系统中的体现(详见第七章 7.4 节)。

5.3 子代理系统:分而治之

AgentTool 是整个工具系统中最复杂也最强大的工具。它允许主代理启动子代理来处理子任务,支持多种模式:

  • 同步子代理:阻塞主对话,等待子任务完成
  • 后台异步子代理:不阻塞主对话,子任务在后台执行
  • Fork 子代理:继承父代理的完整对话上下文,共享 Prompt Cache
  • Worktree 隔离:在独立的 Git worktree 中执行,避免冲突

其中 Fork 子代理的设计尤其精妙——子代理不从零开始,而是”站在父代理的肩膀上”。它继承了父代理积累的所有上下文(项目结构、已做的决策、当前任务状态),同时共享 Prompt Cache,几乎不产生额外的缓存成本。

5.4 Coordinator 模式:生产级多代理协作

在更复杂的场景下,Claude Code 可以进入 Coordinator 模式——一个主代理编排多个 Worker 的协作流程。

值得注意的是,Coordinator 模式的实现完全忠于 Harness 范式的核心理念——它并没有硬编码”Research → Synthesis → Implementation → Verification”这样的固定阶段。实际上,coordinatorMode.ts 的做法是:通过修改系统提示来引导模型行为,将工具集限制为 Agent、SendMessage、TaskStop 等协作工具,然后让模型自己决定如何编排 Worker。

一个典型的协作流程可能长这样(但不是强制的):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Coordinator (主代理,通过 prompt engineering 引导)

├── Research 阶段 (模型自行决定是否并行)
│ ├── Worker A: 分析代码结构
│ ├── Worker B: 分析测试覆盖
│ └── Worker C: 检查依赖关系

├── Synthesis 阶段 (Coordinator 自己做)
│ └── 综合所有 Worker 的研究结果

├── Implementation 阶段
│ └── Worker D: 基于综合分析实现修复

└── Verification 阶段
└── Worker E: 用全新视角验证修复

这恰好印证了文章的核心论点:即使在多代理协作这种复杂场景下,Claude Code 依然选择用 prompt 引导而非用代码编排。Coordinator 拿到的是一组协作工具和一份精心设计的系统提示,具体怎么安排 Worker、分几个阶段、是否并行——全部由模型在运行时决定。

一个关键的设计选择是:综合分析(Synthesis)由 Coordinator 自己完成,而不是委托给 Worker。这确保了主代理始终掌握全局视图,不会在信息传递中丢失关键细节。

Worker 之间通过消息路由系统通信,支持直接消息、广播、关闭请求等多种消息类型,并有完整的计划审批流程。


六、Edge Case 的工程化处理

一个 Coding Agent 从”能用”到”好用”,差距往往不在核心架构,而在 Edge Case 的处理深度

很多人可能会臆测 Claude Code 使用了某种特别高雅的处理方式——比如一个统一的错误恢复框架,或者什么高级的形式化验证。实际上不是的。翻看源码你会发现,它的做法非常朴素:面向体验,逐个击破。源码中藏着数百个 edge case 的专项处理,每一个都针对某个真实用户会遇到的具体问题。

这种”笨功夫”恰恰是产品质量的护城河。下面按类别展开。

6.1 会话恢复:让对话”看起来没断过”

用户随时可能 Ctrl+C、网络断开、程序崩溃。恢复会话时,必须让对话无缝继续。这听起来简单,实际上要处理至少 7-8 种”脏数据”:

① 未配对的工具调用

最常见的场景:模型正在调用工具,用户此时 Ctrl+C。此时对话历史中会留下只有 tool_use 但没有对应 tool_result 的消息。如果不处理,下次 API 调用会直接报错 “tool_use ids were found without tool_result blocks”,对话彻底卡死。

处理方式:恢复时自动扫描所有消息,过滤掉没有匹配 tool_resulttool_use 块。

② 孤儿消息清理

流式传输时,模型可能先输出了两个换行符 "\n\n",然后开始内部推理(thinking),用户此时取消——留下一条只有空白文本的 assistant 消息。如果不清理,API 会拒绝处理。

类似的还有”只含 thinking block 的孤儿消息”——流式传输产生了独立的 thinking 消息片段,但因为中途中断没有被正确合并。这些都需要在恢复时逐一过滤。

而且这些过滤操作之间还有顺序依赖:必须先处理 thinking 块,再处理空白消息。反过来做会导致部分脏数据漏网。

③ 中断类型的精确识别

中断不是一种状态,而是至少三种:

  • 正常完成:模型完整回复了
  • 用户输入中断:用户输入了新问题但模型还没来得及回复
  • 回复中断:模型回复到一半被打断

系统需要精确区分这三种状态,因为恢复策略完全不同。更复杂的是,有些消息看起来像中断但其实不是——比如系统进度消息、API 错误消息都不应该被计入中断判断。

④ 版本兼容

老版本保存的 session 数据可能使用了新版本不认识的字段值(比如某个权限模式在内部版本有但在公开版本没有)。恢复时需要透明地迁移这些数据,否则 API 调用会因为无效参数而报 400 错误。

这些看起来都是小问题,但任何一个没处理好,用户就会卡在”无法恢复会话”的死胡同里

6.2 API 错误的精细化处理

一个面向海量用户的产品,API 调用失败是家常便饭。Claude Code 对 30+ 种 API 错误做了逐一分类,每种都给出针对性的用户提示,而不是笼统的 “Something went wrong”。

① 重试策略的分级设计

不是所有错误都值得重试,也不是所有场景的重试策略都一样:

  • 前台任务(用户在等待的):529 过载 → 最多重试 3 次,然后降级到备用模型
  • 后台任务(摘要生成、分类器等):529 过载 → 直接放弃不重试,因为后台任务的重试会造成 3-10 倍的网关放大效应
  • 无人值守模式(CI/CD 场景):429/529 → 无限重试,每 30 秒发心跳防止宿主标记 session 为空闲

② 连接池的陈旧连接修复

HTTP keep-alive 连接池中的连接可能已经被服务端关闭了,但客户端不知道。每次请求都会先触发一次 ECONNRESET 错误。

处理方式:检测到 ECONNRESET/EPIPE → 关闭整个连接池 → 用新连接重试。不做这个处理,用户会感觉”每个请求都要失败一次才能成功”。

③ Context Window 溢出的自动调整

input_tokens + max_tokens > context_limit 时,不是简单报错,而是解析错误消息中的具体数字 → 计算可用空间 → 自动调低 max_tokens(但不低于 3000,也要保证 thinking 预算的最小需求)→ 重试。对用户完全透明。

④ 认证凭证的透明刷新

OAuth Token 过期、AWS Bedrock 凭证失效、GCP Vertex 认证失败——每种认证错误都有独立的检测和刷新路径。而且还要处理”另一个进程已经刷新了 token”的竞态场景。

⑤ 错误消息的极致精确

源码中的错误分类函数长达 600+ 行,针对每种错误给出极其具体的提示。举几个例子:

错误场景 提示内容
使用了 Pro 计划不支持的模型 “Opus not available on Pro plan, try /logout + /login”
模型名无效(内部用户) “Your org isn’t gated, try ANTHROPIC_MODEL=…”
PDF 超页数限制 交互模式: “esc esc to go back”;非交互: “try pdftotext”
余额不足 专门的计费错误提示

这种精确到场景的错误提示,让用户能自助解决问题,而不是去论坛发帖求助。

6.3 对话历史的”防腐蚀”设计

对话历史是整个系统最核心的状态。一旦被”腐蚀”(数据不一致),轻则某条消息丢失,重则对话永久损坏无法继续。源码中对此有多重防护:

① tool_use / tool_result 配对修复

恢复会话或跨设备迁移(teleport)时,tool_usetool_result 可能不匹配。处理方式:

  • 为孤儿 tool_use 插入合成的错误 tool_result(内容:”[Tool result missing due to internal error]”)
  • 剥离引用了不存在 tool_use 的孤儿 tool_result

更细致的是:合成的 placeholder 会被人类反馈(HFI)系统识别并拒绝提交,避免脏数据污染训练集

② 重复 tool_use ID 检测

理论上 tool_use 的 ID 是唯一的,但在某些异常路径下(比如 SDK 重连后发送了重复的权限响应),可能产生重复 ID。如果不检测,对话会进入死循环——每次重试都累积更多重复。

③ 权限响应的去重

VS Code 等 IDE 通过 WebSocket 发送权限响应时,如果网络断开重连,可能重复发送同一个工具的权限确认。如果不去重,同一个工具会被执行两次 → 产生重复 tool_use ID → API 返回 400 → 对话永久损坏。

6.4 上下文压缩的防御性处理

前面讲了六层压缩策略,但”压缩”本身也可能出问题:

① 压缩的熔断器

有时上下文已经不可恢复地超过了限制,autocompact 注定会失败。真实数据显示,有 1,279 个 session 出现了 50+ 次连续压缩失败(最高 3,272 次),全局浪费了约 25 万次 API 调用/天。

解决方案很直接:连续失败 3 次后停止尝试。数据驱动,不是拍脑袋。

② 压缩的递归防护

session_memorycompact 本身就是通过 forked agent 实现的。如果它们自己也触发了 autocompact,就会死锁。因此需要对这些特殊的查询来源直接跳过压缩检查。

③ 压缩请求本身太长

这是一个很微妙的问题:当对话太长时触发压缩,但压缩请求(需要把整个对话发给 API 来生成摘要)本身就超过了上下文限制。解决方案:从最老的对话回合开始逐步截断,利用错误消息中的 token 差值一次跳过多个回合(而不是逐个剥离),直到压缩请求能发出去。同时设置兜底——截断后仍然太长就抛出明确错误,而不是无限循环。

④ 图片剥离

频繁附加截图的用户,压缩时把图片也发给 API 很容易超限。处理方式:压缩前将所有图片替换为 “[image was shared]” 文本标记。

6.5 Shell 命令的多层安全验证

BashTool 是安全风险最高的工具——它可以执行任意 Shell 命令。Claude Code 为它构建了极其严格的多层验证:

  1. 路径验证:确保命令不操作项目目录之外的路径
  2. sed 命令解析:专门解析 sed 命令判断是否修改文件(因为 sed -i 是一种隐蔽的写操作)
  3. 破坏性命令检测:识别 rm -rfgit push --forcechmod 777 等危险模式
  4. 只读验证:在只读模式下拦截所有写操作
  5. 沙箱限制:在沙箱环境中限制网络访问和文件系统权限

仅这个安全验证模块就超过了 100KB 的代码量。配合四层权限体系(权限模式 → 工具级权限 → 钩子系统 → 沙箱隔离),安全策略可以根据场景灵活调整——本地开发可以宽松一些,CI/CD 环境严格锁定,企业环境需要审计日志。

6.6 Git 操作的多级 Fallback

Git 是编码场景的基础设施,但 Git 仓库的状态千奇百怪。源码中对此设计了三种退化模式,每种都有独立的恢复路径:

  1. Detached HEAD:无法获取分支名 → fallback 到用 merge-base 与默认分支比较
  2. No Remote:没有远端仓库 → 只用 HEAD-only 模式
  3. Shallow Clone:浅克隆没有完整历史 → 检测后 fallback 到 HEAD-only 模式
  4. Merge-base 失败:即使上面都通过了,merge-base 本身也可能失败 → 再次 fallback

原则很清楚:无论 Git 仓库处于什么奇怪的状态,Agent 都不能卡死

6.7 第三方依赖的 Workaround

现实世界的工程不只是写自己的代码,还要和各种有 bug 的第三方依赖共存:

  • GrowthBook SDK 的 Remote Eval bug:SDK 在远程评估模式下返回值格式不对、evalFeature() 忽略预计算值、setForcedFeatures 也不可靠——三层 workaround 层层兜底
  • Bun 运行时的文件监视器死锁:chokidar 4+ 放弃了 fsevents,Bun 用 kqueue 实现 fs.watch,git 操作触碰多个目录时会导致死锁——改用 stat() 轮询代替
  • Axios 代理兼容性 bug:手动创建 proxy agent 绕过 axios 的已知问题
  • WebSocket 重连的事件处理器泄漏:网络不稳时频繁重连,每次重连都注册新的事件监听器导致内存泄漏——重连前显式清除旧 WS 的所有事件处理器

这些 workaround 大多附带了详细的注释和 issue 编号(如 CC-1180、#21931、axios/axios#4531),说明了”为什么”而不仅仅是”怎么做”。

6.8 Edge Case 处理的设计哲学

回过头来看,这数百个 edge case 处理展现了几个核心原则:

  1. 永不卡死:无论发生什么错误,都有退化路径。从 fallback 模型到 HEAD-only git mode 到截断式压缩,用户永远能继续工作
  2. 透明降级:错误发生时用户甚至感知不到,系统在后台默默处理了一切
  3. 数据驱动:阈值和策略来自真实的用户数据(”1,279 个 session 有 50+ 次连续失败”),不是拍脑袋的猜测
  4. 防腐蚀设计:对话历史是最核心的状态,任何可能腐蚀它的操作都有多重防护
  5. 注释即文档:几乎每个 workaround 都有 issue 编号和详细的”为什么”注释

这种”笨功夫”是没有捷径的——你必须真的遇到了每一个问题,理解了它的根因,然后逐个修复。这也解释了为什么新的 Coding Agent 产品层出不穷,但体验上能接近 Claude Code 的寥寥无几:核心架构可以被借鉴,但数百个 edge case 的积累不能被速成


七、开放的扩展体系:MCP、Skill 与 Plugin

一个 Coding Agent 如果只能用内置工具,它的能力天花板就是开发者当初预设的那些场景。要突破这个天花板,需要开放的扩展体系

Claude Code 的源码中,扩展层 Harness 由五个机制组成:

扩展机制 代码位置 定位
MCP 协议 services/mcp/ 通过标准协议接入外部工具
Skill 系统 skills/, SkillTool 领域特定知识和工作流注入
Plugin 系统 plugins/, services/plugins/ 自定义插件加载
自定义代理 AgentTool/loadAgentsDir.ts 用户定义的代理类型
Feature Flag GrowthBook + feature() 编译时功能门控

下面逐个拆解前三个最核心的机制。

7.1 MCP 协议:标准化的工具扩展

MCP(Model Context Protocol) 是 Claude Code 扩展能力的核心通道。它的独特之处在于——Claude Code 同时扮演两个角色:

作为 MCP Client(services/mcp/:连接外部的 MCP Server,获取新的工具能力。每个连接的 MCP Server 会被动态生成对应的 MCPTool 实例,注册到工具系统中:

1
MCPTool  // 按连接的 MCP 服务器动态生成

这意味着:

  • 想让 Agent 操作数据库?接入一个数据库 MCP Server
  • 想让 Agent 管理 Kubernetes?接入一个 K8s MCP Server
  • 想让 Agent 操控浏览器?接入 Playwright MCP Server
  • 想让 Agent 调用内部 API?写一个自定义 MCP Server

无需修改 Claude Code 的任何代码,通过协议扩展能力

作为 MCP Server(entrypoints/mcp.ts:将自己的所有工具暴露出去,供其他程序调用。这使得 Claude Code 可以被嵌入到更大的编排系统中,成为其他 Agent 的一个”子能力”。

这种双向设计意味着 Claude Code 既是能力的消费者,也是能力的提供者——一个真正的”开放节点”。

7.2 Skill 系统:领域知识的热插拔

如果说 MCP 解决的是”能做什么“(新增工具),那 Skill 系统解决的是”怎么做“(注入领域知识和工作流)。

从源码来看,Skill 系统有独立的 skills/ 目录和 SkillTool,还有专门的 /skills 斜杠命令。它的设计非常精巧:

核心机制

  • 热加载:通过 skillChangeDetector.ts 监控技能文件变化,动态加载新技能(在 Bun 运行时下还做了 FSWatcher 死锁的 workaround,改用 stat() 轮询)
  • 状态持久化STATE.invokedSkills 跟踪已激活的技能,restoreSkillStateFromMessages() 在会话恢复时从消息附件中重建技能状态
  • 跨 compaction 保留:当上下文压缩(compaction)发生后再 resume 会话,如果不恢复技能状态,下一次 compaction 会因为 invokedSkills 为空而丢失技能信息。源码中做了专门的处理,并且抑制重复的技能列表公告(节省约 600 tokens)

使用场景:Skill 适合注入”做某类事情的标准流程”——比如代码审查的 checklist、特定框架的最佳实践、文档生成的模板等。它不是增加新的工具调用能力,而是增加”领域智慧”。

7.3 Plugin 系统:更深度的扩展点

Plugin 系统(plugins/, services/plugins/)是比 MCP 和 Skill 更重量级的扩展机制。它允许加载自定义插件,有专门的 /plugin 斜杠命令管理。

从架构图中可以看到,Plugin 系统位于 Services 服务层,与 MCP、compact、lsp 并列:

1
2
3
4
5
6
7
8
Services 服务层
┌────────────┬────────────┬──────────────┬─────────────────────┐
│ compact/ │ mcp/ │ analytics/ │ lsp/ │
│ 上下文压缩 │ MCP 协议 │ 遥测分析 │ 语言服务器 │
├────────────┼────────────┼──────────────┼─────────────────────┤
│ oauth/ │ voice/ │ plugins/ │ SessionMemory/ │
│ 认证 │ 语音输入 │ 插件系统 │ 会话记忆 │
└────────────┴────────────┴──────────────┴─────────────────────┘

需要诚实说明的是:Plugin 系统的公开文档较少,它与 Skill 系统的边界在源码中也没有非常清晰的划分。从目前可观察到的代码来看,Plugin 更偏向内部使用的深度扩展机制,而 MCP 和 Skill 是面向用户的主要扩展方式。

7.4 ToolSearchTool:当工具太多时怎么办

三层扩展机制带来了一个现实问题:工具数量爆炸

内置 30+ 工具,加上通过 MCP 接入的外部工具,工具总数可能达到数十甚至上百个。如果把所有工具的 Schema 都发送给模型,不仅消耗大量 token,还会降低模型的工具选择准确率。

Claude Code 的解决方案是 ToolSearchTool——一个 472 行的元工具(工具的工具):

1
2
3
4
5
6
7
8
9
工具数量 > 阈值?
├─ 否 → 正常发送所有工具 Schema
└─ 是 → 激活 ToolSearchTool

模型先调用 ToolSearchTool 描述需求

系统根据语义匹配返回相关工具

模型再调用匹配到的具体工具

这是”延迟加载”思想在工具系统中的体现——不预加载所有工具,而是按需发现、按需使用。它保证了:即使扩展了大量外部工具,Agent 的核心性能也不会退化。

7.5 扩展体系的设计哲学

三层扩展机制形成了一个清晰的分工:

层次 机制 解决的问题 类比
工具层 MCP 能做什么(新增能力) 安装新的 App
知识层 Skill 怎么做(注入经验) 雇一个领域专家
系统层 Plugin 如何运行(深度定制) 修改操作系统设置

再加上 ToolSearchTool 解决工具发现问题,loadAgentsDir.ts 允许用户定义全新的代理类型——整个扩展体系形成了完整的闭环。

这种分层设计的好处是:大多数扩展需求都可以在最轻量的 MCP 层解决,只有真正需要领域知识注入时才动用 Skill,只有需要深度定制时才编写 Plugin。用户不需要理解整个扩展体系就能获得价值。


八、成本工程:被忽视的竞争力

在讨论 Coding Agent 架构时,人们往往关注能力和体验,却忽视了一个现实问题:每一次 API 调用都是真金白银。Claude Code 源码中散落着大量的成本优化细节,它们不像核心架构那样引人注目,但积少成多形成了显著的竞争优势。

8.1 Prompt Cache 的极致利用

前面 3.4 节提到了系统提示的静态/动态分离,但 Cache 优化远不止于此:

  • Agent 列表位置优化:将 MCP Agent 列表从工具描述(tool description)移到附件(attachment)中,仅此一项就减少了 10.2% 的 cache_creation tokens
  • Fork 子代理共享 Cache:子代理继承父代理的完整对话上下文,共享 Prompt Cache,几乎不产生额外的缓存创建成本
  • 静态提示优先排列:确保跨会话不变的内容排在最前面,最大化 Cache 命中率

8.2 后台任务的成本控制

不是所有 API 调用的重要性都相同。Claude Code 对此做了精细的分级:

  • 前台任务(用户在等待的):529 过载 → 最多重试 3 次,然后降级到备用模型
  • 后台任务(摘要生成、分类器等):529 过载 → 直接放弃不重试。原因很现实——后台任务的重试会造成 3-10 倍的网关放大效应,一个后台摘要不值得用 3 倍成本去重试
  • 压缩熔断器:连续压缩失败 3 次后停止尝试。真实数据显示有 1,279 个 session 出现了 50+ 次连续压缩失败,全局浪费约 25 万次 API 调用/天。加上熔断器后这些浪费被完全消除

8.3 成本意识作为工程文化

这些优化单独看每一个都不起眼,但它们反映了一种工程文化:在 LLM 应用中,每一个 token 都有成本,每一次 API 调用都值得审视。对于一个日活数十万的产品,10% 的 token 节省意味着巨额成本差异。

这也是为什么 Claude Code 选择精确编辑(FileEditTool)而非全文覆写——除了降低幻觉风险,它还能将编辑操作的 token 消耗从”文件总行数”降到”修改行数”。


九、总结:好的 Agent 框架就是没有框架

写到这里,我想回到文章开头的问题:为什么同样是”让 LLM 写代码”,不同架构的体验差距如此之大?

答案是:差距不在 LLM 本身,而在 Harness 的深度

一个优秀的 Coding Agent 需要同时做好七件事:

  1. 丰富的工具调用:让 LLM 有足够的”手脚”
  2. 智能的上下文管理:让 LLM 不会”失忆”
  3. 充分的环境反馈:让 LLM 不会”幻觉”
  4. 高效的执行:让交互不会”卡顿”
  5. 严密的安全:让操作不会”失控”
  6. 开放的扩展:让能力不会”封顶”
  7. 精细的成本控制:让产品能”活下去”

LangChain 时代,我们试图用复杂的编排逻辑来弥补模型能力的不足。如今,模型已经足够强大,最好的策略是:

构建一个极其丰富的 Harness——工具、上下文、安全、协作——然后让 LLM 自己决定怎么做。

这就是 Harness 范式的核心,也是 Claude Code 的成功之处。它证明了一个反直觉的结论:最好的 Agent 框架,就是没有框架

不需要 DAG,不需要 Chain,不需要 Pipe。只需要一个简洁的 Agent Loop + 精心设计的环境,剩下的交给模型自己。


参考资料


这篇文章是基于 Claude Code 源码研究的思考总结。在 AI 编程助手快速演进的今天,理解架构设计背后的取舍,比知道某个具体功能怎么用更有长期价值。


文章作者: RickDamon
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 RickDamon !
 上一篇
用 Agent 分析源码的方法论:如何快速理解LLM生成的海量代码 用 Agent 分析源码的方法论:如何快速理解LLM生成的海量代码
在 AI Coding 爆发的时代,我们逐行读源码的方法论可能有些落后或者说成本过重了。本文记录一套经过实战验证的方法论——如何借助 AI Agent 快速理解大型项目的架构设计,并深度分析你想了解的项目内容。
下一篇 
基于Google最新Agents论文的学习文档 基于Google最新Agents论文的学习文档
导语原链接:Google AI Agents Technical Guide 这份白皮书是 Google 对AI Agent 从概念到生产部署的完整技术路线图,涵盖了: ✅ AI Agent 的定义和分类 ✅ Agent 开发框架(ADK
  目录