导语
2024 年底到 2025 年初,Coding Agent 赛道迎来了一次范式跳跃。
从最早的 LangChain 式”显式编排”,到 AutoGPT 式”自主循环”,再到如今以 Claude Code 为代表的 Harness 范式——AI 编程助手的架构设计在短短两年内经历了三次根本性的转变。
Claude Code 的源码曾短暂泄露,社区 fork 了一份后很快被删除。基于对这份源码的研究,本文试图回答一个核心问题:为什么同样是”让 LLM 写代码”,不同架构的体验差距如此之大?
这篇文章不是一篇源码导读,而是对 Coding Agent 架构演进的思考总结。会从以下几个维度展开:
- Agent 架构的三次范式转变
- Harness 范式的工程设计哲学
- 如何让 Agent 不丢失上下文
- 如何让模型减少幻觉
- 工具系统的精妙设计
- Edge Case 的工程化处理
一、Agent 架构的三次范式转变
1.1 第一代:显式编排时代(LangChain / LangGraph)
2023 年,LangChain 几乎是所有 LLM 应用的标准起点。它的核心思想是显式编排:开发者用 Chain、Pipe、DAG 把 LLM 的调用流程”画”出来,当时这个“高大上”的使用方式让不少人作为一个新的技术范式来研究它。
1 | # LangChain 的典型模式:开发者预定义工作流 |
这个模式的问题在于:开发者必须预先知道所有可能的执行路径。每当你想支持一个新场景,就必须回去修改 chain 定义。这在面对代码编辑这种高度动态的场景时,说实话很难去做穷举。
更致命的是状态管理。LangChain 把状态管理的责任完全交给了开发者——你需要手动管理对话历史、手动处理上下文窗口溢出、手动定义错误恢复路径。当你的 Agent 需要在一个 10 万行的项目里做跨文件重构时,这种手动管理也是个比较复杂的任务,尤其是遇到编码场景等。
1.2 第二代:自主循环时代(AutoGPT / BabyAGI)
2023 年中,AutoGPT 的出现让所有人兴奋了一阵子。它的思路很简单:让 LLM 自己决定下一步做什么,循环执行直到任务完成。
1 | while not done: |
这个方向是对的,但当时的执行有严重缺陷:
- 上下文快速爆炸:没有压缩策略,几轮循环就塞满了上下文窗口
- 幻觉失控:缺乏环境反馈的 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 | while (模型未停止): |
就这么几行伪代码所覆盖的逻辑,支撑起了一个能做跨文件重构、多代理协作、无限对话的编程助手。所有的”智能”不是来自复杂的编排逻辑,而是来自三个方面:
- 模型本身的推理能力
- 精心设计的系统提示
- 丰富且高质量的工具反馈
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 | 用户输入 |
注意这里没有任何”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 | ┌──────────────────────────────────────────────┐ |
这套系统的精妙之处在于层层递进、各有侧重:
- 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:用工具结果锚定推理
这是最根本的防幻觉机制。模型不是在”想象”代码库长什么样,而是通过工具调用实际查看代码库。
当模型需要修改一个文件时,它不会凭记忆”猜”文件内容,而是:
- 先用
GrepTool搜索相关代码 - 用
FileReadTool读取完整文件 - 基于实际读取的内容生成编辑
- 用
FileEditTool执行精确编辑(而不是覆写整个文件)
每一步都有真实的文件系统反馈。如果文件不存在,工具会返回明确的错误信息,而不是让模型在幻觉中继续。
4.2 环境上下文注入:让模型知道自己在哪
Claude Code 会在每次对话中自动注入丰富的环境上下文:
| 上下文类型 | 注入内容 |
|---|---|
| Git 状态 | 当前分支、最近提交、未提交的修改 |
| 项目配置 | CLAUDE.md 项目级规则、技术栈信息 |
| LSP 诊断 | 实时的编译错误、类型警告 |
| 文件结构 | 项目目录树的概览 |
这些信息让模型在开始推理之前就对”当前环境的真实状态”有准确的认知,而不是基于训练数据中的通用知识来猜测。
4.3 精确编辑 vs 全文覆写
这是一个常被忽视但极其重要的设计决策。
很多 Coding Agent 的文件编辑方式是”生成新的完整文件内容然后覆写”。这有两个严重问题:
- 幻觉风险高:模型需要”记住”文件的所有内容,很容易遗漏或修改不该改的部分
- 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 | 模型一次性发出工具调用: |
每个工具自己声明”我是否可以安全并发”,系统据此自动编排。读操作之间自由并行(最高 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 | Coordinator (主代理,通过 prompt engineering 引导) |
这恰好印证了文章的核心论点:即使在多代理协作这种复杂场景下,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_result 的 tool_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_use 和 tool_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_memory 和 compact 本身就是通过 forked agent 实现的。如果它们自己也触发了 autocompact,就会死锁。因此需要对这些特殊的查询来源直接跳过压缩检查。
③ 压缩请求本身太长
这是一个很微妙的问题:当对话太长时触发压缩,但压缩请求(需要把整个对话发给 API 来生成摘要)本身就超过了上下文限制。解决方案:从最老的对话回合开始逐步截断,利用错误消息中的 token 差值一次跳过多个回合(而不是逐个剥离),直到压缩请求能发出去。同时设置兜底——截断后仍然太长就抛出明确错误,而不是无限循环。
④ 图片剥离
频繁附加截图的用户,压缩时把图片也发给 API 很容易超限。处理方式:压缩前将所有图片替换为 “[image was shared]” 文本标记。
6.5 Shell 命令的多层安全验证
BashTool 是安全风险最高的工具——它可以执行任意 Shell 命令。Claude Code 为它构建了极其严格的多层验证:
- 路径验证:确保命令不操作项目目录之外的路径
- sed 命令解析:专门解析 sed 命令判断是否修改文件(因为
sed -i是一种隐蔽的写操作) - 破坏性命令检测:识别
rm -rf、git push --force、chmod 777等危险模式 - 只读验证:在只读模式下拦截所有写操作
- 沙箱限制:在沙箱环境中限制网络访问和文件系统权限
仅这个安全验证模块就超过了 100KB 的代码量。配合四层权限体系(权限模式 → 工具级权限 → 钩子系统 → 沙箱隔离),安全策略可以根据场景灵活调整——本地开发可以宽松一些,CI/CD 环境严格锁定,企业环境需要审计日志。
6.6 Git 操作的多级 Fallback
Git 是编码场景的基础设施,但 Git 仓库的状态千奇百怪。源码中对此设计了三种退化模式,每种都有独立的恢复路径:
- Detached HEAD:无法获取分支名 → fallback 到用 merge-base 与默认分支比较
- No Remote:没有远端仓库 → 只用 HEAD-only 模式
- Shallow Clone:浅克隆没有完整历史 → 检测后 fallback 到 HEAD-only 模式
- 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 处理展现了几个核心原则:
- 永不卡死:无论发生什么错误,都有退化路径。从 fallback 模型到 HEAD-only git mode 到截断式压缩,用户永远能继续工作
- 透明降级:错误发生时用户甚至感知不到,系统在后台默默处理了一切
- 数据驱动:阈值和策略来自真实的用户数据(”1,279 个 session 有 50+ 次连续失败”),不是拍脑袋的猜测
- 防腐蚀设计:对话历史是最核心的状态,任何可能腐蚀它的操作都有多重防护
- 注释即文档:几乎每个 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 | Services 服务层 |
需要诚实说明的是:Plugin 系统的公开文档较少,它与 Skill 系统的边界在源码中也没有非常清晰的划分。从目前可观察到的代码来看,Plugin 更偏向内部使用的深度扩展机制,而 MCP 和 Skill 是面向用户的主要扩展方式。
7.4 ToolSearchTool:当工具太多时怎么办
三层扩展机制带来了一个现实问题:工具数量爆炸。
内置 30+ 工具,加上通过 MCP 接入的外部工具,工具总数可能达到数十甚至上百个。如果把所有工具的 Schema 都发送给模型,不仅消耗大量 token,还会降低模型的工具选择准确率。
Claude Code 的解决方案是 ToolSearchTool——一个 472 行的元工具(工具的工具):
1 | 工具数量 > 阈值? |
这是”延迟加载”思想在工具系统中的体现——不预加载所有工具,而是按需发现、按需使用。它保证了:即使扩展了大量外部工具,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 需要同时做好七件事:
- 丰富的工具调用:让 LLM 有足够的”手脚”
- 智能的上下文管理:让 LLM 不会”失忆”
- 充分的环境反馈:让 LLM 不会”幻觉”
- 高效的执行:让交互不会”卡顿”
- 严密的安全:让操作不会”失控”
- 开放的扩展:让能力不会”封顶”
- 精细的成本控制:让产品能”活下去”
LangChain 时代,我们试图用复杂的编排逻辑来弥补模型能力的不足。如今,模型已经足够强大,最好的策略是:
构建一个极其丰富的 Harness——工具、上下文、安全、协作——然后让 LLM 自己决定怎么做。
这就是 Harness 范式的核心,也是 Claude Code 的成功之处。它证明了一个反直觉的结论:最好的 Agent 框架,就是没有框架。
不需要 DAG,不需要 Chain,不需要 Pipe。只需要一个简洁的 Agent Loop + 精心设计的环境,剩下的交给模型自己。
参考资料
- Claude Code 源码分析(基于 claude-code/src 目录)
- Anthropic: Building Effective Agents
- Model Context Protocol (MCP) 规范
- Google AI Agents Technical Guide
这篇文章是基于 Claude Code 源码研究的思考总结。在 AI 编程助手快速演进的今天,理解架构设计背后的取舍,比知道某个具体功能怎么用更有长期价值。