工具即能力:AI Agent 的工具系统设计哲学

上一篇文章我们聊了 Think-Act-Observe 循环——AI 编程助手的骨架。骨架本身不会干活,它需要肌肉。在 Claude Code 里,肌肉就是工具

没有工具,循环里 Think 和 Observe 之间缺了 Act,退化回普通的问答。有了工具,模型才能触碰真实世界:读你的代码、改你的配置、跑你的测试。

但工具系统的设计远比"给模型一堆函数调用"复杂。Claude Code 源码里藏着几个关键设计决策,每一个都指向同一个问题:

怎么让 AI 知道该用什么工具、怎么用、什么时候用?

答案是:描述比实现重要。

Claude Code 里的每个工具都有三层:

第一层:定义(Definition)——工具的名字、描述、参数 schema。这一层不执行任何操作,它只回答"我是什么、我能干什么、你需要给我什么"。

第二层:实现(Implementation)——工具实际做什么。读文件就调 fs.readFile,执行命令就调 child_process。这是工程师写的代码。

第三层:权限(Permission)——工具的风险等级。读文件是低风险的,直接执行;删文件是高风险的,需要用户确认。

这三层里,模型只能看到第一层。它不知道 read_file 内部调的是 fs.readFile 还是某种缓存机制,它只知道"这个工具能读取文件内容,需要传入 path 参数"。

这意味着工具的定义层是模型和真实世界之间的唯一接口。接口设计得好不好,直接决定了模型能不能正确使用工具。

大多数人在设计 AI 工具时,精力都放在实现上——功能对不对、性能好不好、边界条件有没有覆盖。这没错,但 Claude Code 的工具系统揭示了一个容易被忽略的事实:

工具描述(description)是一段微型 prompt。

它会被注入到 system prompt 里,模型靠它理解"这个工具是干什么的、什么时候该用、参数是什么意思"。一段好的工具描述和一段烂的描述,差距可能是一个工具被频繁使用 vs. 从未被调用。

看 Claude Code 里 grep_search 工具的描述:

Search file contents using regular expressions (powered by ripgrep). Returns matching lines with file paths and line numbers.

这段描述回答了三个问题:

  • 干什么:搜索文件内容
  • 用什么:正则表达式
  • 返回什么:匹配行、文件路径、行号

再看一个反例。假设描述只写 “Search files”——模型知道你要搜索,但不知道是搜索文件名还是文件内容,不知道支持什么语法,不知道返回什么格式。这种描述下,模型要么不敢用(信息不足),要么乱用(猜错了)。

工具描述的质量 = 模型使用工具的准确率。 这不是 Prompt Engineering 的分支,这就是 Prompt Engineering。

Claude Code 的工具不是随意堆砌的。源码里的工具可以按认知维度分成几类:

感知类(Perception)——让模型"看到"项目状态。read_fileglob(文件匹配)、grep(内容搜索)、ls(目录列表)。这类工具只读不改,是模型理解上下文的主要手段。

操作类(Action)——让模型"改变"项目状态。write_fileedit(精确替换)、bash(执行命令)。这类工具有副作用,是权限系统的重点管控对象。

外部类(External)——让模型"触达"项目之外的世界。web_searchweb_fetch。这类工具引入了不可控的外部信息源,风险和收益都更高。

元认知类(Meta)——让模型"了解自己"。mcp_statuscost。这类工具帮助模型监控自身状态和资源配置。

这种分类不是源码里的显式结构,但它在 system prompt 和权限系统里被隐式地体现出来。感知类工具通常无需确认即可调用,操作类工具需要分级确认,外部类工具有独立的开关。

分类的本质是风险分级。不是所有工具调用都同等危险,权限系统需要知道"这次调用该不该问用户"。

工具系统面临一个根本矛盾:

  • 工具越多,AI 能力越强。
  • 工具越多,工具描述占用的上下文越多。
  • 上下文被工具描述占满,留给实际对话的空间越少。
  • 空间越少,模型表现越差(所谓"lost in the middle"效应)。

Claude Code 的解法是渐进式披露(Progressive Disclosure)

不是把所有工具的定义一次性塞进 system prompt,而是在对话过程中按需加载。模型一开始只看到核心工具的描述(read、write、edit、bash),当它需要更专业的能力时,再通过某种机制"发现"并加载额外工具。

这个思路在源码里体现为 deferred tools(延迟工具)机制——某些工具的定义默认不注入 system prompt,只在特定条件下被激活。

渐进式披露的本质和操作系统里的懒加载一样:不用不着急加载,用了再加载也不迟。区别是操作系统的懒加载优化的是内存,AI Agent 的懒加载优化的是注意力。模型的注意力是有限的上下文窗口,每一 token 都应该花在刀刃上。

Claude Code 的工具调用是同步阻塞的——模型请求调用一个工具,循环暂停,等工具执行完毕,结果返回,循环继续。

这个选择值得咀嚼。异步并行显然更快:如果模型需要读三个文件,并行读比串行读快三倍。但 Claude Code 选择了串行。

原因是认知一致性。模型在 Think 阶段基于当前已知的信息做出判断,它请求调用工具 A 是因为它认为"我现在最需要知道 X"。如果三个工具并行执行、结果同时返回,模型看到的是一堆它没按顺序请求的信息——它可能已经基于工具 A 的结果做了判断,工具 B 的结果却推翻了那个判断。

串行调用慢,但它保证了模型的思考过程是线性可追踪的。每一步都有明确的前因后果,出了问题容易回溯。

当然,Claude Code 也在某些场景下支持并行工具调用——当模型明确判断多个工具调用之间没有依赖关系时。但默认策略是串行,这是安全优先的设计选择。

Claude Code 的工具系统背后有一条贯穿始终的设计哲学:

AI Agent 的能力不由模型决定,由工具决定。

同样的 GPT-4o 或 Claude Sonnet,给它不同的工具集,它就是不同的 Agent。只有 read/write 工具,它是文本编辑器。加上 bash,它是运维助手。加上 web_search,它是研究员。加上 MCP 协议连接外部服务,它的边界几乎无限。

模型是引擎,工具是变速箱和轮胎。引擎再好,没有合适的传动系统也跑不起来。

这也是为什么 Claude Code 的源码里,工具系统的代码量和 agent 循环的代码量几乎一样多。不是因为工具实现有多复杂——大多数工具几行到几十行就搞定了——而是因为工具的定义、调度、权限、错误处理、结果格式化这套基础设施需要精心设计。

BYF 项目里,工具系统不仅做了 Claude Code 做的事,还增加了一层抽象——Kaos(执行环境抽象)

BYF 的 Kaos 接口将"执行"与"位置"解耦:一个操作(读文件、跑命令)可以在本地执行,也可以通过 SSH 在远程执行,调用方不需要知道区别。代码调 readText()exec() 时,底层可能走 LocalKaos(本地文件系统)或未来的 SSHKaos(远程),AsyncLocalStorage 自动绑定到当前上下文。这是对 Claude Code 工具系统中"权限分类"的进一步深化——不仅分读/写/执行,还分在哪执行

BYF 还做过一个有意思的设计债清理。它的后台任务系统原本用一个 ManagedProcess 数据结构同时管理两种任务:真实 OS 进程和 JS Promise 子代理。为了让 Promise 任务适配,代码中有一处 as unknown as KaosProcess 的类型强制转换——BYF 源码中唯一一处 as unknown。通过引入 TaskEntry 判别联合ProcessTaskEntry | PromiseTaskEntry),BYF 彻底消除了这个类型逃生口,让每种任务有了自己的形态,编译器强制你在访问 entry.proc 前做 kind 守卫。

这个决策和 Claude Code 的"工具三层结构"是同一个思路:类型系统本身就是工具系统的一部分。 你用类型编码工具之间的差异,让错误在编译时被捕获,而不是在运行时被模型发现。

工具描述是注入到 system prompt 里的。但一个生产级 AI Agent 的 system prompt 不只是工具描述的堆砌——它是一个几百甚至上千行的精密工程作品,分层、占位符、延迟加载、动态注入……下一篇文章我们要拆的就是这个:800 行 system prompt 背后的设计决策


本系列基于 Claude Code 官方源码项目进行架构分析,聚焦设计思想而非代码实现。

相关内容