Skip to content

Latest commit

 

History

History
 
 

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

README.md

03 — 工具系统架构

Claude Code 之所以“能做事”,不是因为模型会魔法,而是因为它被接上了一套统一的工具系统

先建立一个关键认识

如果你第一次接触 AI Agent 系统,很容易产生一个误解:模型既然这么聪明,应该可以直接操作电脑吧?

事实恰恰相反。

模型本身只是一个”文本生成器”。它可以理解你的需求,可以规划步骤,可以生成代码,但它完全无法直接操作你的电脑

  • 不能读文件(它看不到你的文件系统)
  • 不能写代码(它生成的代码只是文本,不会自动保存)
  • 不能跑命令(它没有权限访问你的 Shell)
  • 不能访问外部服务(它被隔离在 API 调用的边界内)

那它是怎么”帮你做事”的?

答案是:通过一套精心设计的工具系统

模型不能直接做事,但它可以”请求做事”。它会输出一个结构化的请求:”我想调用 FileReadTool,参数是 src/app.ts”。然后 Claude Code 收到这个请求,在本地执行真正的文件读取,再把结果返回给模型。

这就是为什么工具系统不是”附属功能”,而是”产品主体的一部分”。

没有工具系统,Claude Code 就只是一个”会聊天的终端程序”。有了工具系统,它才能真正成为”会做事的编程助手”。

本章关键源码入口

文件 作用
src/Tool.ts 定义工具契约、工具上下文、工具构建基础能力
src/tools.ts 统一注册内置工具
src/services/tools/* 工具执行与调度相关逻辑
src/tools/BashTool/* 最复杂、最有代表性的工具实现

一个工具最少要定义什么

从源码设计上看,一个工具至少要回答 4 个问题:

  1. 我叫什么:模型如何引用我
  2. 我接收什么参数:输入 Schema 是什么
  3. 我什么时候能执行:权限和模式限制是什么
  4. 我执行后返回什么:结果如何回传给模型

这说明 Claude Code 的工具不是“随便暴露一个函数”,而是一个规范化接口。

Tool 契约为什么重要

对于模型来说,工具不是代码实现,而是一个“可调用能力描述”。

Claude Code 需要把工具变成模型能理解的形式,所以每个工具都不只是一个 call()

  • 有名称
  • 有描述
  • 有输入 Schema
  • 有结果格式
  • 有权限语义
  • 有 UI 渲染方式

这也是为什么工具系统会单独抽象成 src/Tool.ts,而不是散落在各个功能目录里。

src/tools.ts 到底在做什么

src/tools.ts 是内置工具注册中心。

它负责把各种工具按规则收集起来,例如:

  • 始终可用的核心工具
  • 由 Feature Flag 控制的实验性工具
  • 用懒加载打破循环依赖的工具

这一步非常关键,因为模型看到的并不是“仓库里有哪些工具文件”,而是当前这次会话里最终被注册出来、最终被开放出来的工具集合。

这里要补一个很重要的纠正

getTools() 很重要,但它不等于本轮真正可执行的全部工具池。

更准确地说:

  • getTools():给出经过 mode / deny / enable 过滤后的 built-in tools
  • assembleToolPool():把 built-in 和 MCP tools 合并、去重
  • toolUseContext.options.tools:本轮主链真正会被执行层使用的工具池

三种常见的工具加载方式

1. 静态导入

适合核心能力,例如:

  • BashTool
  • FileReadTool
  • FileWriteTool
  • FileEditTool
  • GlobTool
  • GrepTool

这些能力足够基础,几乎可以视为 Claude Code 的标配。

2. Feature Flag 条件导入

源码中很多工具要看 feature('...') 是否开启,例如:

  • 主动模式相关工具
  • Cron/Trigger 相关工具
  • KAIROS 相关工具
  • 监控、推送、简报类工具

这说明工具系统不仅服务当前产品,也服务未来产品实验。

3. 懒加载

有些工具不是因为“不重要”才延迟加载,而是因为:

  • 启动时不想拉起太多重量级模块
  • 要规避循环依赖

例如 TeamCreateToolTeamDeleteTool 这类和多 Agent 体系耦合更深的工具,就用了懒加载思路。

工具查找不是“随便搜一下名字”

Tool.ts 和执行层的调用链看,工具查找至少有一条明确规则:

  • 优先在当前会话真实可用的工具池里查
  • 名字匹配支持主名和 alias
  • 如果当前工具池里找不到,只会做很有限的旧 alias 兼容回退
  • 再找不到,就直接生成错误 tool_result

从模型视角看,工具描述本身就是 Prompt

对于模型来说,工具最重要的不是 TypeScript 实现,而是“工具说明文本”。

工具说明会告诉模型:

  • 这个工具适合什么时候用
  • 参数该怎么填
  • 有哪些常见失败方式
  • 什么时候不该用它

所以工具描述本质上是一种非常具体的 Prompt Engineering。

为什么这很关键

因为 Claude Code 并没有一个独立的工具选择器替模型做路由。模型能不能选对工具,很大程度取决于工具描述写得够不够清楚。

工具系统的核心价值,不只是提供能力,还要约束调用

Claude Code 的工具设计不是把能力裸露给模型,而是把能力做成一层有边界的 API。

举个典型例子:

  • FileReadToolFileEditTool 明确分开
  • 读文件和改文件是两个不同权限等级的动作
  • 工具层先把边界切清,权限系统才有可能做细粒度控制

编排层和执行层不是一回事

这是读工具系统时最值得看清的一层分工。

编排层做什么

  • 这一轮有哪些 tool_use
  • 哪些调用能并发,哪些必须串行
  • 并发结果按什么顺序回放

这里要特别注意:

单个 loop 的默认语义仍然是偏顺序推进。
编排层会判断并发安全性,但这不等于“Claude Code 默认把一轮里所有工具都并发跑掉”。

如果你前面已经读过 02 — Agent Loop,可以把它理解成:

  • 02 讲的是主链默认语义偏顺序
  • 03 讲的是执行层在少数安全前提下如何做更细的调度

执行层做什么

  • 找到具体工具定义
  • 校验输入 schema
  • 调用工具自己的权限检查
  • 真正执行 tool.call()
  • 把结果整理成统一结构

核心工具可以先分成哪几类

1. 文件与代码操作

  • 读取文件
  • 写入文件
  • 精准编辑文件
  • 搜索文件名
  • 搜索文件内容
  • 编辑 Notebook
  • 走 LSP 获取代码诊断与跳转

这是 Claude Code 最核心的一组工具,因为编码任务大多围绕它们展开。

2. Shell 执行

BashTool 是最重要也最敏感的工具之一。

因为一旦能执行命令,Claude Code 的能力会非常强,但风险也会骤增。所以这类工具的实现通常最厚、最严。

3. 网络与外部信息

  • WebFetchTool
  • WebSearchTool
  • 各类 MCP 工具

这组工具让模型不只会看本地代码,还能查外部资料、调用外部系统。

4. 协作与编排

  • AgentTool
  • TeamCreateTool
  • SendMessageTool
  • TaskCreateTool
  • TodoWriteTool

这组工具把 Claude Code 从单体 CLI 助手往多 Agent 协作系统推进了一步。

BashTool 为什么是工具系统里最复杂的一类

如果你只能挑一个工具深入看,优先看 BashTool

原因不是它最常用,而是它最能体现 Claude Code 的工程思路:

  • 命令语义需要分析
  • 路径需要验证
  • 破坏性动作要识别
  • 只读模式要强约束
  • 沙箱和权限模式都要兼容

换句话说,Claude Code 不是“把 shell 开给模型”,而是“把一层受控 shell 能力开放给模型”。

这里也能反向看出工具系统的设计边界:

  • 工具系统不是只负责“路由”
  • 权限系统也不是完全脱离工具系统独立运行
  • 两者是在执行层内部咬合起来的

为什么工具系统不能只靠函数签名

如果只是 TypeScript 世界里的函数,接口够用就行。

但 Claude Code 里的工具面对的是模型,所以还必须考虑:

  • 描述是否足够清楚
  • 输入是否足够结构化
  • 返回是否足够稳定
  • 权限语义是否足够明确
  • UI 层是否知道怎么展示工具调用过程

这也是为什么工具系统往往会同时涉及:

  • 类型系统
  • JSON Schema
  • Prompt 描述
  • 权限控制
  • 渲染逻辑

Claude Code 为什么没有单独做一个工具规划器

它没有设计一个外部模块去先判断:

  • 应该用哪个工具
  • 调用顺序是什么
  • 要不要先搜索再编辑

这些决策大多直接交给模型。

工具系统做的事情是:

  • 把能力清晰暴露出来
  • 把边界和说明写清楚
  • 在执行时严格检查

也就是说,Claude Code 选择的是:模型负责决策,工具系统负责提供清晰、稳定、安全的执行接口。

tool_result 不是一个简单返回值

源码里的边界至少有三层:

  1. 工具原始输出
    • ToolResult<T>
  2. 协议层结果块
    • tool_result
  3. 外层消息包络
    • 最终会落进 user-side message

所以“工具返回什么” 和 “模型最后看到什么” 不是同一个层级的问题。

动态信息为什么会影响工具体系

工具不只是代码模块,也会进入模型上下文。

这就带来一个重要问题:

  • 如果工具描述会跟着环境频繁变化
  • 那模型侧看到的工具 Schema 也会变化
  • 进而影响缓存和稳定性

这也是源码里把某些动态信息从工具描述中迁走的原因,比如动态 agent 列表。

内置工具和 MCP 工具在执行层是“同池异味”

更准确的说法是:

  • built-in 和 MCP tools 会进入同一个执行池
  • 两者共用主链上的查找、调用和结果装配框架
  • 但在结果后处理和元数据透传上,MCP 会保留一些协议特例

新手读工具系统时建议重点抓什么

不要一上来逐个看 40 多个工具。优先抓下面 4 件事:

  1. 工具契约怎么定义
  2. 工具是怎么注册进系统的
  3. 工具执行前后有哪些钩子和权限检查
  4. 为什么某些工具需要独立的安全子系统
  5. getTools()、真实执行池、tool_result 这三个概念千万不要混

本章小结

工具系统是 Claude Code 的执行层核心,它做的不只是“提供功能”,而是:

  • 用统一契约暴露能力
  • 用 Prompt 描述引导模型调用
  • 用 Schema 限制输入形状
  • 用权限系统决定是否执行
  • 用渲染层把工具过程反馈给用户
  • 把“工具池构建”“工具调度”“单次工具执行”明确拆层
  • 让 built-in 与 MCP 工具共享一套主链,只在少数协议点分叉

下一步