乐于分享
好东西不私藏

从 Claude Code 源码解读高效工具使用是如何设计的

从 Claude Code 源码解读高效工具使用是如何设计的

这两年,大家聊 Agent,最容易掉进一个误区:总觉得工具调用效率,主要取决于模型够不够聪明。

模型更强,理解更准,推理更深,当然重要。但如果真把 Claude Code 的源码翻开看一遍,你会发现另一个更本质的事实:一个 Agent 能不能高效地使用工具,决定因素往往不在模型,而在工具系统本身是怎么设计的。

换句话说,真正拉开差距的,不是“模型会不会调工具”,而是“系统有没有把工具调用这件事设计成一个可治理、可调度、可扩展的工程系统”。

Claude Code 给我的最大启发,不是它有多少工具,而是它几乎把“工具使用”重新做成了一层操作系统。模型决定做什么,框架决定怎么做,工具定义决定能做到什么程度。这个分工一旦建立起来,Agent 才开始像一个系统,而不是一个偶尔能调命令的聊天机器人。

Claude Code 不是工具箱,而是工具操作系统

很多 Agent 框架在工具这层,做法都很直接:给模型暴露一堆函数,能调用就行。工具在系统中的身份,更像一组“外挂能力”。

Claude Code 不是这么干的。

从 src/Tool.ts 往下看,它对一个工具的定义,远不只是一个 call()。一个工具要进入系统,至少要回答几类问题:它叫什么,输入长什么样,怎么执行,是不是只读,能不能并发,需不需要权限,结果怎么渲染,进度怎么展示,什么时候该被折叠,什么时候会产生副作用。

这件事看起来很“工程化”,但恰恰是这一层工程化,让后面的所有能力有了基础。

如果工具只是函数,框架就很难治理。你没法统一校验输入,没法统一做权限控制,没法统一决定哪些调用能并发,没法统一控制上下文污染,也没法把 MCP 这样的外部能力无缝并进来。工具一旦被协议化,它就不再只是“可调用函数”,而变成了“可被系统调度和治理的能力单元”。

这是我看源码后的第一个“原来如此”时刻:很多 Agent 框架做不稳,不是因为模型不够强,而是因为工具从一开始就没有被建模成系统对象。

Claude Code 的 src/tools.ts 也很说明问题。它不是简单把工具列出来,而是在做装配、过滤、去重、权限裁剪、模式切换。内置工具、MCP 工具、延迟加载工具、不同模式下可见的工具,最后会被组装成一份“当前上下文真正可用的工具池”。这就意味着,模型看到的不是“系统里所有可能的工具”,而是“当前这轮真正应该看到的工具”。

这一步非常关键。很多 Agent 的失控,不是模型乱用工具,而是工具暴露策略本身就太粗糙。

所以如果非要给 Claude Code 的工具层下一个定义,我会说:它不是工具箱,而是一层工具操作系统。工具不是摆在那里等模型碰运气,而是被框架有组织地管理、筛选、调度和约束。

职责分类,决定了效率上限

Claude Code 源码里另一个特别值得借鉴的地方,是它虽然没有在文档里高调宣称“我们做了职责分层”,但整个工具体系的组织方式,实际上已经把职责分层做得非常清楚了。

如果从职责看,Claude Code 里的工具大致可以分成六类。

第一类是感知类工具,比如 Read、Grep、Glob、WebSearch、WebFetch、MCP Resource。它们本质上不是为了“执行动作”,而是为了“获取高质量上下文”。这类工具决定的不是行动能力,而是认知质量。Agent 能不能做出靠谱决策,往往不取决于它会不会写代码,而取决于它拿到的上下文是不是足够准、足够小、足够有用。

第二类是执行类工具,比如 Bash、PowerShell。这类工具决定的是行动范围。它们是真正把 Agent 从“会分析”带到“会做事”的关键接口。但同时,它们也是副作用、风险和不确定性最容易聚集的地方。

第三类是修改类工具,比如 Edit、Write、NotebookEdit。这类工具和执行类工具看起来都能改东西,但本质并不一样。执行类是开放动作空间,修改类是约束动作空间。一个好的修改类工具,通常会更强地绑定路径、内容、diff、权限和校验,因为它天然更适合被系统治理。

第四类是编排类工具,比如 Agent、Task、SendMessage、TodoWrite。它们不是在解决业务问题,而是在管理执行过程本身。复杂任务不是靠主模型一口气做完的,而是靠拆任务、分代理、控节奏、保状态完成的。这类工具决定的是吞吐量,而不是单步能力。

第五类是模式类工具,比如 Plan Mode、Worktree Mode。它们不是直接创造能力,而是在切换工作方式。Plan Mode 约束的是“先计划再执行”,Worktree Mode 管的是“隔离环境中执行修改”。这类工具的重要性经常被低估,但实际上它们决定了系统什么时候应该保守,什么时候可以放开。

第六类是扩展类工具,比如 MCP、Skill、ToolSearch。它们管理的是系统边界。一个 Agent 体系能不能长期演化,不在于内置能力堆得有多满,而在于它能不能把外部能力纳入统一治理。

为什么这套职责分类这么重要?

因为效率,从来不是“调用次数少”或者“响应时间短”这么简单。效率首先是结构问题。感知、执行、修改、编排、模式、扩展,如果全混在一起,系统就无法对不同工具施加不同策略。只读和写入会被一视同仁,搜索和执行会被塞进同一套上下文逻辑,子代理和主线程会抢同一个上下文预算,扩展能力也会变成野生插件。

很多 Agent 项目做到后面越来越不稳,问题往往不在模型,而在职责没分开。系统无法知道一个工具到底是在“看世界”、还是在“改世界”、还是在“组织别人改世界”。一旦这个边界不清楚,优化就无从下手。

这是第二个“原来如此”的地方:高效工具使用,首先不是调度问题,而是分类问题。没有职责分层,后面谈并发、权限、上下文优化,都是补锅。

模型决策,不等于模型统治

很多人看到 Agent 会用工具,脑子里默认的图景是:模型想调用什么,就调用什么;模型决定流程,框架只是跑一下。

Claude Code 不是这个逻辑。

它的设计更像一种很成熟的分层协作:模型负责决策,框架负责治理。

具体来说,模型负责的是这两件事:选哪个工具,填什么参数。也就是 assistant 产出一个 tool_use,给出工具名和输入。到这里为止,模型完成的是策略层工作。

但接下来是不是能执行、以什么顺序执行、是否需要用户确认、能不能并发、结果怎么回流上下文,这些事情都已经不再由模型说了算,而是进入了框架的编排链路。

从 src/services/tools/toolExecution.ts 和 src/services/tools/toolOrchestration.ts 看,这条链路很清晰。工具调用出来之后,先过 schema 校验,再过 validateInput(),再跑 pre-tool hooks,再进权限判定,再交给调度器决定串行还是并发,最后才真正 tool.call()。执行完之后,结果会被包装成 tool_result,再回流到上下文里。

这里最值得琢磨的一点是:Claude Code 并没有把“模型会调用工具”理解成“模型可以统治执行系统”。它把模型放在策略层,把执行层牢牢握在框架手里。

这件事非常重要。因为模型擅长的是在不完全信息下做近似决策,而框架擅长的是在已知规则下做稳定执行。把两者混在一起,系统会越来越脆。把两者拆开,能力反而更强。

一句话概括就是:模型是策略层,框架是执行治理层。

这是第三个“原来如此”的瞬间。Agent 的成熟度,不是看模型会不会调工具,而是看工具调用有没有被编排系统接住。

如果一个框架把模型的 tool call 直接透传到底层执行,那它本质上还是“LLM 外挂调用器”。如果一个框架能把模型的选择,放进权限、调度、上下文、模式和扩展体系里,那它才开始接近真正的 Agent Runtime。

真正拉开差距的,是框架编排

如果只看“工具数量”,Claude Code 并不构成真正的壁垒。真正让我觉得它设计成熟的地方,是它在工具编排上做了大量看起来不起眼、但极其关键的工作。

这些工作单独看都不算惊艳,但放在一起,就构成了“高效工具使用”的底层原因。

1. 并发不是默认能力,而是受控能力

在 toolOrchestration.ts 里,工具调用不是一股脑并发的。系统会看工具的 isConcurrencySafe(),把调用切成不同批次:能并发的读操作并发,不能并发的操作串行。

这件事看起来只是调度细节,实际上是在定义系统的安全边界。

很多 Agent 框架一说提效,第一反应就是并发化。但真正难的从来不是“能不能并发”,而是“哪些东西应该并发”。只读搜索和文件修改当然不能用同一种调度策略;一个系统如果连这一点都不区分,并发越多,事故越多。

Claude Code 的做法很克制,也很对路:先把并发安全建模成工具属性,再由调度器利用这个属性。并发不是技巧,而是协议的一部分。

2. 工具不是全量暴露,而是按需发现

ToolSearchTool 和 deferred tool 的设计,是我特别喜欢的一点。

很多系统会把所有工具 schema 一次性塞进 prompt,觉得这样模型知道得最全。但实际效果往往是:上下文变大了,注意力被稀释了,工具选择反而更乱。

Claude Code 的做法是,某些工具默认延迟加载,模型需要时先通过 ToolSearch 找,再调用。这其实是在做一件非常聪明的事:把“工具发现”从“上下文预加载”改成“按需检索”。

这背后不是 prompt 技巧,而是系统设计思路变了。不是假设模型应该永远知道全量工具,而是假设模型只需要在当前任务里知道最相关的那部分工具。

工具系统一大,按需发现几乎不是优化项,而是必选项。

3. 大结果不进上下文,是高效系统的基本修养

很多 Agent 用着用着就越来越笨,一个核心原因是:它们太容易把长输出重新喂给自己。

Claude Code 在工具层有明显的“大结果落盘”机制。超过一定大小的结果不会原样塞回上下文,而是存到文件里,只给模型摘要、预览或者路径。

这件事特别像数据库系统里的冷热分层。真正需要给模型保留的是决策信息,不是全部原始输出。长日志、整页网页内容、大块 shell 输出,如果直接回灌上下文,短期看像是信息更充分,长期看其实是在制造噪音债务。

这也是很多 Agent 项目会慢慢失真、失控、失忆的原因:不是模型记不住,而是系统没有做信息分级。

高效,不只是快,更是节制。

4. 只读和搜索操作要被特殊对待

Claude Code 对只读、搜索、列表类操作做了很多 UI 和上下文层面的特殊处理。比如 Bash 会识别 rgfindcatls 这类命令,将其归类为 search/read/list,用于折叠展示和降低噪音。

这背后的设计直觉非常好:搜索类工具经常高频调用,但单次价值密度不高。如果每次都完整展开,它们会淹没真正重要的信息。如果一味压缩,又会损失关键线索。

所以最好的策略不是简单保留或删除,而是做结构化折叠。保留“找过什么、看过哪里、结果规模多大”这样的决策信息,把低价值细节隐藏起来。

这其实是在做上下文的信噪比优化。很多 Agent 框架的问题不是上下文太少,而是上下文里的低价值信息太多。

5. 多代理不是炫技,而是上下文管理手段

Claude Code 的 AgentTool、Task 体系、SendMessage 机制,表面看是在做多代理协作,实际上更深一层是在做上下文管理。

一个复杂任务,如果全压在主线程里做,问题不只是推理慢,更重要的是上下文会越来越脏。调试信息、临时探索、长日志、中间判断都会堆积在主上下文里,最后让主模型越来越难保持判断力。

子代理的意义,不只是分工,更是隔离。把局部任务放进单独的执行空间,允许它自己探索、自己试错、自己产出结果,然后只把必要的信息回传给主线程。这和服务架构里的进程隔离、队列隔离,本质上是同一种工程思想。

所以,多代理不是为了看起来高级,而是为了让主上下文保持清醒。

这一点特别值得 Agent 开发者注意。很多人把“上下文窗口更大”当成复杂任务的解法,但更大的窗口并不等于更好的执行系统。真正有效的方法,很多时候是结构性隔离,而不是一味堆上下文。

对 Agent 开发者最值得抄的 7 条经验

如果把上面这些源码观察最后压缩成几条可借鉴的方法,我觉得至少有七条是非常值得带走的。

第一,先做职责分层,再做工具暴露。不要一上来就想“我有哪些 API 可以给模型用”,而要先想“哪些工具是在看世界,哪些是在改世界,哪些是在组织别人改世界”。

第二,把工具协议化,而不是只做函数封装。一个成熟的工具对象,至少要有输入契约、权限契约、并发契约、结果契约和展示契约。没有这些,工具永远进不了系统层。

第三,把并发安全当成一等公民。并发不是执行器的补充能力,而是工具定义的一部分。能不能并发,应该由工具自己声明,再由调度器利用。

第四,让模型负责选择,让框架负责约束。模型擅长出策略,不擅长守秩序。秩序必须由框架建立,否则系统迟早被概率性错误拖垮。

第五,大结果不要直接塞回上下文。上下文不是日志仓库,而是决策工作区。原始输出该落盘就落盘,该摘要就摘要,该引用路径就引用路径。

第六,把扩展能力纳入统一治理,不要做野生插件。MCP 之所以在 Claude Code 里成立,不是因为它能接很多外部工具,而是因为它接进来之后依旧服从统一的工具协议、权限系统和调度规则。

第七,多代理不是炫技,而是上下文管理手段。复杂任务最怕的不是模型做不出来,而是主上下文被探索过程污染。代理拆分,本质上是在管理注意力预算。

如果把这七条串起来,其实就是一句话:Agent 系统的质量,最终体现在它如何管理不确定性,而不是它暴露了多少能力。

写在最后

看 Claude Code 这套工具体系,最让我感慨的一点是,Agent 工程正在越来越像传统软件工程里那些成熟系统的演进路径。

最早大家讨论 Prompt Engineering,后来开始讨论 Tool Use,现在真正往前走的团队,讨论的已经是 Runtime、调度、权限、上下文治理、扩展协议和多代理协作。问题的重心,已经从“怎么让模型更会说”转向“怎么让系统更会做”。

这也是我觉得 Claude Code 源码最有价值的地方。它给出的不是一组神奇技巧,而是一种很清楚的工程判断:真正优秀的 Agent,不是一个会调用工具的大模型,而是一套让工具调用变得可靠、经济、可治理的系统设计。

如果你正在做 Agent,我很建议把思路从“怎么给模型接更多工具”,切到“怎么把工具设计成系统的一部分”。

很多问题,到这里会突然变得简单。

因为你会意识到,Agent 的核心难题,根本不是让模型学会调用工具,而是让工具调用这件事,终于像一个系统那样被设计出来。