乐于分享
好东西不私藏

Claude Code 源码解析(一):从工具调用到 Agent Harness

Claude Code 源码解析(一):从工具调用到 Agent Harness

AI应用实录 · Claude Code 源码拆解 · 第一章

Claude Code 51 万行代码拆解第一章——它不是 API 封装,而是一个完整的 Agent 运行时。

从 Chatbot 到 Agent:发生了什么?

2025 年以前,我们跟 AI 的交互方式基本就一件事:打字 → 等回复 → 手动操作

想让 AI 帮你改代码?它只能告诉你”你可以在第 42 行加一个 if 判断”——真正动手改的还是你自己。想让它帮你整理文件、调试报错?它只能给建议,真正去做的是你。

LLM 能”说话”,但不能”做事”。 无法读取文件系统,无法执行命令,无法创建 Git 分支,更无法在遇到错误时自主调整策略。每一次与外部世界的交互,都需要人类充当中间人。不管理解力多强、推理多精准,它的能力止步于文本输出。

2025 年之后,事情变了。LLM 开始有了”工具调用”(Tool Use / Function Calling)能力——它不仅能理解你的需求,还能自己决定调用哪个工具、传什么参数、怎么处理返回结果

举个具体例子:你说”帮我把项目里所有的 console.log 都删掉”。以前 AI 会回复”你可以用正则搜索替换……”。现在它会:

1.调用文件搜索工具,找到所有包含 console.log 的文件

2.逐个调用文件编辑工具,删除对应行

3.调用终端工具运行测试,确认没有破坏功能

4.汇报结果:”已删除 23 个文件中的 47 处 console.log,测试全部通过。”

LLM 的角色从”对话伙伴”变成了”任务编排器”——从回答问题,变成了解决问题。

▲ LLM 角色演进:从 Chatbot 到 Agent

但这也意味着,我们需要一套全新的工程体系来支撑这种”行动能力”。

但”能调用工具”只是开始

一旦 LLM 能操作真实世界,工程问题就来了。实际生产环境中,至少有六个硬骨头要啃:

🔧 工具管理:几十上百个工具,怎么注册、怎么动态添加?Token 是有限的,工具越多,模型选错的概率越大。需要一套智能的工具发现和过滤机制。

🛡️ 参数校验:LLM 生成的参数是不可预测的——类型错误、字段缺失、数值越界。你让它传文件路径,它可能传成 URL;让它传数字,它可能传字符串 “42”。

🔐 权限管控:模型可能要求执行 rm -rf /,你总不能真的执行吧?更微妙的是——rm -rf node_modules 是安全操作,rm -rf ~/.ssh 是灾难。权限不是简单的”允许/禁止”,要根据命令、参数、上下文做细粒度判断。

🔄 错误恢复:API 超时、工具崩溃、输出格式乱了——每种错误都需要不同的恢复策略,否则 Agent 会陷入死循环直到 Token 用完。

📦 状态一致性:多轮工具调用操作同一份资源,如何保证不出现”改了一半”的脏状态?

⚡ 并发调度:读 5 个文件可以同时进行,但”先建目录再写文件”必须串行——怎么智能地判断?

这六个挑战催生了一个新概念——Agent Harness

▲ Agent 生产级六大工程挑战

Agent Harness ≠ API 封装

一个常见误解:Agent Harness 不就是对 LLM API 包了一层吗?

这种理解停留在理想世界里——假设 API 永不超时,模型输出永远正确,上下文窗口无限大,所有操作都安全。真实环境会逐一打破这些假设。来看看差距到底有多大:

维度
“简单封装”的做法
Agent Harness 的做法
API 调用
发请求、收响应、完事
流式接收、超时重试、降级策略
工具执行
直接调用函数
参数校验 → 权限检查 → 沙箱执行 → 结果验证 → 错误恢复
上下文
把所有消息全塞进去
智能压缩、摘要替换、关键信息保留、缓存优化
错误处理
try/catch 打印日志
分级错误体系、自动重试、优雅降级
状态管理
全局变量
不可变状态、事务性更新、跨工具一致性
安全
白名单
四层纵深防御、上下文感知的动态权限决策

一句话总结:Agent Harness 是围绕 LLM 构建的运行时框架,它把 LLM 从文本生成器提升为能够安全、可靠、高效地与外部世界交互的自主智能体。

类比:LLM 是发动机,Agent Harness 是整辆车——包括变速箱、刹车、悬挂、仪表盘、安全气囊。你不能只有发动机就上路。

▲ Agent Harness 架构总览

Claude Code:51 万行代码在做什么?

项目规模

Claude Code 的 src 目录包含 1,884 个 TypeScript 文件,总计 512,664 行代码

做几个对比:VS Code 核心约 50 万行(体量相当)、React 约 20 万行(Claude Code 是它的 2.5 倍)、Express.js 约 1 万行(50 倍)。说明一个成熟的 Agent Harness,工程复杂度不亚于一个完整的编辑器框架。

但代码多不等于臃肿。Claude Code 的组织表现出高度模块化特征:每个工具是独立模块,每个子系统有清晰边界,职责划分遵循单一职责原则。

技术选型:为什么选这些?

技术组件
选择
一句话理由
为什么不选其他
运行时
Bun
原生 TypeScript、启动更快
Node.js 需编译,Deno 生态不够成熟
终端 UI
React + Ink
组件化声明式渲染
blessed 太底层,console.log 搞不定复杂布局
CLI 框架
Commander.js
成熟、轻量
yargs 更重,oclif 面向大型项目
Schema 验证
Zod v4
类型定义+运行时校验+API 文档三合一
Joi 不支持类型推导,io-ts 学习曲线陡
LLM SDK
@anthropic-ai/sdk
官方出品,原生流式支持
裸 fetch 缺类型安全和重试

React + Ink —— 用写网页的方式写终端 UI。工具执行进度条、权限确认对话框、多列搜索结果展示,背后都是 React 组件。Web 领域验证过的组件化思维,直接搬到终端里来。

Zod v4 —— LLM 生成的参数完全不可预测。你定义了 { path: string, line: number },模型可能传回 { filepath: "src/index.ts", lineNumber: "42" }。Zod 写一份 Schema,同时获得 TypeScript 类型、运行时校验、API 文档三种能力,而且参数错了能给模型精确的错误信息让它自我修正。

五个核心模块

Claude Code 的核心架构可以拆成五个模块,严格的层次调用关系:

入口 → 查询引擎 → 对话主循环 → 工具类型系统 → 工具注册中心

上层依赖下层,下层不感知上层。替换任何一层的实现,不影响其他层。

▲ 五大核心模块层次架构

① 入口点模块

三个职责:启动优化、CLI 解析、React/Ink 初始化。

启动优化值得单独说——性能探针最先执行,MDM 配置读取与 Keychain 预取并行展开,把原本串行的 I/O 操作叠在一起。传统做法每一步等上一步完成;Claude Code 让配置、密钥、UI 初始化同时进行。

工程原则很明确:启动路径是用户体验的第一印象。 打开等 3 秒和打开就能用,是两种完全不同的体验。用户不会因为”启动慢但功能强”而原谅你——他们会直接关掉。

② LLM 查询引擎(QueryEngine)

对话的”生命周期管理者”——管理消息历史、文件缓存、用量统计、权限记录。你可以把它想象成一个”数据库管理员”:所有跟对话状态相关的数据都由它统一管理。

关键设计:同时服务于 交互式 REPL 和 无头 SDK 两种运行模式。一个核心、多种入口,避免两套逻辑跑不同步。如果 REPL 和 SDK 各维护一套查询逻辑,bug 修复就要改两个地方,时间一长两套代码一定会跑偏。

③ 异步生成器对话主循环

整个系统最核心的模块。 一个 AsyncGenerator,每轮迭代依次执行:

构建请求 → 流式接收响应 → 解析工具调用 → 权限校验 → 执行工具 → 注入结果 → 判断是否继续

这个循环会一直运行,直到模型认为任务完成(不再调用工具)或用户手动终止。一次”帮我重构这个模块”的请求,可能在这个循环里转几十轮。

为什么用 AsyncGenerator

增量输出:通过 yield 实时接收中间状态,每一步即时推送给 UI

可中断性:用户按 Ctrl+C 随时取消,不留半完成的操作

背压控制:消费者处理不过来时,生成器自动暂停

为什么不用其他方案?回调 → 回调地狱;Promise 链 → 无法取消;EventEmitter → 内存泄漏风险。AsyncGenerator 是唯一同时满足”流式”+”可取消”+”类型安全”的方案。

跨迭代状态用不可变的 State 对象封装,整体替换而非逐字段修改。

④ 工具类型系统

定义所有工具必须遵循的类型契约。几个关键设计:

 权限检查内嵌在工具执行流程中,不可绕过

isConcurrencySafe 声明影响调度策略——安全的可以并行

isDestructive 标记是权限系统的重要输入

userFacingName 决定 UI 展示——用户看到的是”读取文件”而不是 ReadFileTool.execute()

这些约束通过 TypeScript 编译器强制执行——开发者物理上无法”忘记”实现某个方法

⑤ 工具注册中心(getAllBaseTools)

工具系统的”单一事实来源”。三个设计亮点:

条件注册:通过 Feature Flag 控制,内部版本和外部版本可以包含不同工具

延迟加载:工具定义通过动态 import() 按需加载,避免循环依赖,也减少启动时间——你可能注册了 50 个工具,但这次对话只用到 3 个

工具过滤:发送给 LLM 前过滤禁止的工具——模型甚至看不到它不应该使用的工具。这比”模型看到了但告诉它不能用”安全得多

五大设计原则

这五个原则不是孤立的技巧,而是相互支撑的架构决策网络。

▲ 五大设计原则互联关系

原则一:异步流式优先

Agent 可能在单次请求中执行多轮工具调用,持续数分钟。如果用户在这段时间什么都看不到,体验非常糟糕。AsyncGenerator 的增量输出、可中断性和背压控制,完美匹配了这个需求。

原则二:安全边界内嵌

权限系统不是”加上去的安全层”,而是内嵌在核心管线中。很多系统的做法是:核心功能先做好,然后在外面”包一层”安全检查。问题是——这种”外挂式”安全很容易被绕过,或者在重构时不小心移除。Claude Code 的做法是:安全检查就是工具执行管线的组成部分。工具调用从 LLM 提出到执行,经过四个检查点:

1.可见性过滤:模型连看到都看不到禁止的工具

2.输入校验:参数格式不对直接拒绝

3.权限决策:综合权限模式、工具危险等级、用户历史做判断

4.运行时防护:沙箱限制、超时控制、输出大小限制

▲ 安全纵深防御四层检查点

这是纵深防御——每一层独立短路,即使某层被绕过,下一层仍然挡着。

为什么白名单不够?rm -rf node_modules 是安全操作,rm -rf /etc 是灾难。git push 在普通分支是安全的,git push --force origin main 是危险的。同一个命令,不同参数,风险等级完全不同——静态白名单无法做出这种精细判断。

原则三:缓存感知设计

Claude API 的 Prompt Caching 命中时,输入 Token 成本降低 90%,延迟降低约 80%。Claude Code 从三个层面保证缓存命中率:

系统 Prompt 稳定性:构建方式保证字节内容一致

子智能体缓存共享:Fork 模式继承父智能体的 Prompt

消息历史不可变:只追加不修改,缓存键永远稳定

最容易踩坑的是系统 Prompt 稳定性。你的 Prompt 里包含”当前可用工具列表”,每次对话前动态生成。如果工具列表的排序不稳定(比如用了 Object.keys()),那么每次生成的 Prompt 字节内容都不一样——缓存永远命中不了。

微小的配置变化(如工具列表排序变了)导致的缓存失效,会迅速累积成可观的额外成本。

原则四:渐进式能力扩展

四级扩展模型,从内建到外部、从简单到复杂:

扩展级别
机制
适用场景
谁来扩展
工具
实现 Tool 接口
添加原子操作
核心开发者
技能
Markdown + 脚本
封装任务模板
高级用户
插件
带生命周期的工具包
组织相关工具
生态开发者
MCP 服务器
标准化协议
第三方工具集成
第三方开发者

设计哲学是”渐进增强”:让每一类贡献者都能在最适合自己的抽象层次上工作。 两种极端都不可取——只有工具一级扩展,第三方必须修改核心代码;只提供 MCP 一级扩展,简单自定义需求也要搭建完整工具服务器。VS Code 有扩展 API、浏览器有 WebExtension、Kubernetes 有 Operator——都是同一个思路。

原则五:不可变状态流转

Agent 系统天然高并发:模型可能同时调用 3 个工具,流式输出在持续产生事件,用户可能随时交互。可变状态会导致经典竞态条件:工具 A 改到一半,工具 B 读到了不一致的数据。 在子智能体场景下更危险——子智能体可能意外修改父智能体的状态。这类 bug 极难复现,因为它取决于异步操作的具体调度顺序。

不可变状态的三个设计要素:

Updater 函数(prev) => next,确保基于前一状态计算,避免竞态

引用相等性:只有真正变化才触发通知,避免无效重渲染

订阅/取消:监听器返回清理函数,防止内存泄漏

不可变状态从架构层面消除了并发竞态问题。

关键结论

1. 范式转移已经发生。 LLM 从”对话伙伴”变成”自主智能体”,核心驱动力是工具调用的标准化。简单 API 封装应对不了生产级挑战。

2. 工程复杂度不会因模型更强而消失。 51 万行代码告诉你:流式输出、权限管控、上下文管理、错误恢复、状态持久化——每一个都是独立的系统工程挑战。随着 Agent 执行更复杂的任务,这些挑战会愈发尖锐。

3. 五个模块定义架构骨架。 入口 → 查询引擎 → 对话主循环 → 工具类型 → 工具注册——这是整个系统的基本拓扑。

4. 五大原则贯穿全局。 异步流式优先、安全边界内嵌、缓存感知设计、渐进式能力扩展、不可变状态流转——几乎所有设计决策都能追溯到这五个原则。

实战练习:安装与初次诊断

步骤一:安装 Claude Code

确保系统已安装 Node.js 18+ 或 Bun:

BASH

npm install -g @anthropic-ai/claude-code claude --version

步骤二:运行 /doctor 诊断

BASH

cd your-project claude > /doctor

/doctor 会执行一系列系统自检,包括:

1.环境检查:Node.js 版本、Bun 可用性、Git 安装状态

2.权限配置检查:当前权限模式、工具白名单/黑名单状态

3.MCP 服务器连接状态:已配置的 MCP 服务器是否正常响应

4.API 连接性检查:与 Anthropic API 的连接是否正常

5.文件系统访问检查:工作目录的读写权限

每一项检查背后都对应着前面讲的某个模块。

步骤三:观察流式输出

注意 /doctor 的输出方式——每完成一项检查就即时显示,不等全部跑完。这正是”异步流式优先”原则的日常体现。

步骤四:观察一次完整的工具调用

发送一个简单请求:”列出当前目录下的所有 TypeScript 文件”。观察三个阶段:

1.思考 → 展示打算用哪个工具

2.工具调用 → 显示 GlobTool("*.ts") 和结果

3.回复 → 基于结果给出最终回答

这个简单交互的背后,第一章讲的五个模块全部参与了。

AI应用实录|用大白话拆解 AI 应用,让普通人也能上手。