工具系统 — 让 Agent 有手有脚

Agent 不能只会说话——它得能读文件、写文件、跑命令

📝 本章目标

读完本章,你将:

  1. 用四个 Prompt 让 AI 帮你实现一套完整的工具系统——读文件、写文件、跑命令、搜代码
  2. 理解工具系统的三层架构——定义、注册、执行
  3. 掌握 Tool Schema 的设计原则——为什么 AI 能”知道”怎么调工具
  4. 了解 Claude Code 45+ 工具背后的并发执行和分类管理

上一章我们给 Agent 装上了”嘴巴”——它能跟你对话了,而且聊再久也不怕爆。但你很快就会发现一个尴尬的事实:它只会,不会

你问它”帮我看看这个文件里写了什么”,它会说”好的,让我来查看——“然后就卡住了,因为它根本没有能力读你的文件。你让它”把这个 bug 修一下”,它会给你一段看起来不错的代码,但它改不了你的文件——你得自己手动复制粘贴。

这就像雇了一个顾问:什么都懂,但什么都不动手。你需要的不是顾问,是一个能撸起袖子干活的人。

工具系统就是给 Agent 装上手和脚。装完之后,它不仅能说”这个文件有问题”,还能直接帮你改掉。

动手:用四个 Prompt 给 Agent 装上手脚

确认你在 harness 项目根目录。如果你跟着第 2 章做了查询引擎,现在应该有 harness/engine.pyharness/cli.py。如果没有,去 GitHub 仓库 git checkout ch02-query-engine 获取起点。

打开 Claude Code,跟着走。

Prompt 1:让 AI 能看文件

现在 Agent 只会说话。我们先给它最基本的能力——看你电脑上的文件。

帮我做一套工具机制,先实现一个读文件的工具。

大概的思路是这样的:
1. 定义一套描述工具的格式,
   让 AI 知道"有一个工具叫读文件,需要告诉我文件路径,
   我就把内容读出来给你"
2. 在查询引擎里,把这些工具描述传给 AI,
   AI 想用哪个就告诉我工具名和参数,
   我来执行然后把结果传回去
3. 工具要放在单独的目录里,方便以后加新的
4. 如果文件不存在或者读不了,
   别崩溃,把错误信息告诉 AI 让它自己想办法

先做一个读文件的,后面再加别的。

等 AI 跑完,试一下:

$ harness
You > 帮我看看 pyproject.toml 里写了什么
Assistant > 我来读一下这个文件。
[tool: read_file("pyproject.toml")]
这个文件是你的项目配置,里面定义了...

You > 读一下 不存在的文件.txt
Assistant > 我来看看...
[tool: read_file("不存在的文件.txt")]
这个文件不存在,你是不是路径写错了?

Agent 能读文件了!注意第二个测试——文件不存在时,AI 会收到错误信息并自己解释,而不是程序崩溃。这就是工具系统的容错设计。

Prompt 2:让 AI 能改文件

能读了但还不能改——Agent 现在是个”只读模式”。

帮我再加一个写文件的工具。

写文件之前要先告诉我它打算写什么内容,
我确认了再写。
如果目标路径的文件夹不存在,自动创建。

放到跟读文件工具一样的目录里,
注册方式也跟读文件一样。
$ harness
You > 帮我创建一个 hello.txt,里面写"你好世界"
Assistant > 我来创建这个文件。
[tool: write_file("hello.txt", "你好世界")]
文件已创建!

You > 读一下 hello.txt 确认内容对不对
Assistant > 我来确认一下。
[tool: read_file("hello.txt")]
内容是"你好世界",跟你要求的一样。

现在 Agent 能读也能写了。但很多任务不只是操作文件——你可能需要安装依赖、运行测试、查看 git 状态。这些都需要跑终端命令。

Prompt 3:让 AI 能跑命令

帮我加一个执行终端命令的工具,要注意:
1. 要有超时限制,别让一个命令卡死整个程序
2. 命令的输出(包括正常输出和错误输出)都要传回给 AI
3. 也要把命令的退出码告诉 AI,
   这样它就知道命令是成功了还是失败了

放到跟前面工具一样的目录里。
$ harness
You > 帮我看看当前目录有哪些文件
Assistant > 我来查看一下。
[tool: bash("ls -la")]
当前目录下有这些文件...

You > 帮我看看 git 最近的提交记录
Assistant >
[tool: bash("git log --oneline -5")]
最近 5 次提交是...

You > 跑一下 python -c "print(1/0)"
Assistant >
[tool: bash("python -c \"print(1/0)\"")]
命令执行失败了,报了一个除零错误...

Agent 现在能做事了。但随着项目变大,一个个文件读太慢。我们再加最后一个能力。

Prompt 4:让 AI 能搜代码

帮我加一个搜索功能,能在代码里搜关键词,
告诉我哪些文件的哪些行包含这个词。

最好能支持:
1. 按关键词搜索文件内容
2. 可以限定只搜某种类型的文件,比如只搜 .py 文件
3. 搜索结果要包含文件名、行号和那一行的内容
4. 结果太多的话自动截断,别一次返回几万行

放到跟前面工具一样的目录里。
$ harness
You > 帮我搜一下代码里哪里用到了 "query_loop"
Assistant > 我来搜索一下。
[tool: grep("query_loop", "*.py")]
找到 3 个文件包含 "query_loop":
- engine.py:42 — def query_loop(state):
- engine.py:98 — def query_loop_stream(state):
- cli.py:15 — from .engine import query_loop_stream

💡 四个 Prompt 做了什么

  • Prompt 1 建立了工具机制——注册 + 执行框架,附带第一个读文件工具
  • Prompt 2 加上了写能力——Agent 能创建和修改文件了
  • Prompt 3 加上了手脚——能跑任何终端命令
  • Prompt 4 加上了眼睛——能在代码库里快速搜索

现在你的 Agent 能读、能写、能跑命令、能搜代码——跟一个实习生差不多了。完整代码在 GitHub 仓库,对应 tag ch03-tool-system

接下来我们回过头,理解你刚刚构建的东西。


深入理解

理解工具系统:三层架构

打开你刚生成的 harness/tools/ 目录,你会发现工具系统并不复杂——它由三层组成,各司其职:

图 3-1:工具系统的三层架构

  1. Tool Definition — JSON Schema、参数描述、能力声明 → 告诉 AI「我能做什么」
  2. Tool Registry — 自动发现、统一注册、名称映射 → 运行时找到工具
  3. Tool Executor — 参数校验、执行逻辑、结果回传
  • Tool Definition(工具定义)——用 JSON Schema 描述每个工具的名称、用途和参数。这是给 AI 看的”使用说明书”
  • Tool Registry(工具注册)——把所有工具收集到一个地方,通过工具名就能找到对应的实现。这是一张”工具名 → 执行函数”的映射表
  • Tool Executor(工具执行)——拿到 AI 传来的参数,校验合法性,执行逻辑,把结果传回 AI

三层合在一起就是 20 行核心代码:

# harness/tools/ — 核心骨架

# ── Layer 1: Tool Definition ──
TOOL_SCHEMA = {
    "name": "read_file",
    "description": "Read the contents of a file",
    "input_schema": {
        "type": "object",
        "properties": {
            "path": {"type": "string", "description": "File path"}
        },
        "required": ["path"],
    },
}

# ── Layer 2: Tool Registry ──
TOOL_REGISTRY = {}           # name → (schema, executor)

def register(schema, executor):
    TOOL_REGISTRY[schema["name"]] = (schema, executor)

# ── Layer 3: Tool Executor ──
def execute_tool(name, params):
    schema, executor = TOOL_REGISTRY[name]
    return executor(**params)  # 调用实际逻辑

新建的 harness/tools/ 目录结构像这样:

harness/tools/
├── __init__.py        ← 导出注册表和执行函数
├── registry.py        ← Tool Registry:注册 + 查找
├── read_file.py       ← 读文件工具
├── write_file.py      ← 写文件工具
├── bash.py            ← 执行命令工具
└── grep.py            ← 搜索代码工具

每个工具文件的结构都一样:一个 Schema 字典 + 一个执行函数 + 一行注册调用。加新工具就是复制这个模式,改改参数和逻辑。

Tool Schema 的秘密

AI 怎么知道有哪些工具可以用,以及每个工具需要什么参数?答案是 Tool Schema——一段 JSON 格式的描述。

💡 核心概念:Tool Schema

Tool Schema 是工具的”使用说明书”,它告诉 AI 三件事:

  1. 这个工具叫什么——AI 通过名字来”点名”调用
  2. 这个工具能做什么——description 字段帮助 AI 判断什么时候该用它
  3. 需要传什么参数——每个参数的类型、含义、是否必填

Schema 不是给人看的——它是给 AI 看的。AI 根据 Schema 决定要不要调这个工具、传什么参数。所以 description 写得越清楚,AI 用得越准确。

看一个真实的例子——你生成的读文件工具的完整 Schema:

{
  "name": "read_file",
  "description": "Read the contents of a file at the given path. Returns the file content as text. Use this when you need to examine existing files.",
  "input_schema": {
    "type": "object",
    "properties": {
      "path": {
        "type": "string",
        "description": "The absolute or relative path to the file"
      },
      "offset": {
        "type": "integer",
        "description": "Line number to start reading from (0-based)"
      },
      "limit": {
        "type": "integer",
        "description": "Maximum number of lines to read"
      }
    },
    "required": ["path"]
  }
}

注意几个设计细节:

  • description 不只说”读文件”——它还说了什么时候该用(“when you need to examine existing files”)。这帮助 AI 在多个工具之间做选择
  • offsetlimit 不是必填的——大文件时可以分段读,小文件时直接全部读
  • 参数的 description 解释了格式要求(“absolute or relative path”),减少 AI 传错参数的概率

这个 Schema 会在每次 API 调用时传给 Claude。Claude 看到 Schema,就知道”哦,我有一个能力叫 read_file,需要给一个 path”——然后它会在合适的时候主动调用。

工具执行的生命周期

从 AI 决定”我要调用一个工具”到结果回传,中间经历了完整的生命周期:

图 3-2:工具执行的生命周期

  1. AI 决策 → 返回 tool_use
  2. Schema 校验 → 参数合法?
  3. 执行工具 → 运行实际逻辑
  4. 结果回传 → tool_result
  5. AI 继续 → 下一步决策

具体来说,每次工具调用的流程是这样的:

  1. AI 决策——Claude 分析用户请求后,判断需要调用某个工具。它返回一个 tool_use 内容块,包含工具名和参数(JSON 格式)
  2. Schema 校验——查询引擎收到 tool_use 块后,先检查:工具名存在吗?参数类型对吗?必填字段都有吗?不合法就直接返回错误
  3. 执行工具——校验通过后,调用工具的实际逻辑。读文件就真的读文件,跑命令就真的跑命令
  4. 结果回传——把执行结果(成功的内容或失败的错误信息)包装成 tool_result 消息,追加到对话历史
  5. AI 继续——Claude 看到工具结果后,决定下一步:还需要再调工具?还是可以回答用户了?如果要调工具,回到步骤 1 继续循环

这个流程最关键的一点是:步骤 5 可能回到步骤 1。AI 读了一个文件后可能发现需要再读另一个文件,或者需要跑一个命令来验证——一个用户请求可能触发十几次工具调用,全部自动完成。

这就是第 2 章查询循环的威力——工具系统只负责”执行一次”,查询循环负责”循环到完”。

并发工具执行

你可能注意到一个有趣的现象:有时候 AI 会在一次回复里同时调用多个工具。比如它想同时读三个文件来比较内容——这时候 Claude 会在一个响应里返回三个 tool_use 块。

Claude Code 对此的处理方式是并发执行:三个读文件操作同时进行,而不是一个接一个。这在工具调用频繁的复杂任务中能显著减少等待时间。

💡 我们的简化版

你生成的代码大概率是逐个执行工具的——遍历所有 tool_use 块,一个执行完再执行下一个。这完全没问题。

Claude Code 用了线程池来并发执行多个工具。优化思路很简单:读文件、搜代码这类互不影响的工具可以并行;写文件、跑命令这类可能有副作用的需要注意顺序。

等你的 Agent 需要处理复杂任务时再考虑并发优化——过早优化是万恶之源。

Claude Code 的 45+ 工具

我们做了 4 个工具,Claude Code 有 45 个以上。它们可以分成几大类:

分类我们的 HarnessClaude Code
文件操作read_file, write_fileRead, Write, Edit, MultiEdit, NotebookEdit
命令执行bashBash(沙箱 + 超时 + 后台模式)
代码搜索grepGrep, Glob, LSP(语义搜索)
Agent 管理——Task, TodoRead, TodoWrite
UI 交互——WebFetch, UrlScreenshot, UserInput
版本控制——专用 Git 操作工具
MCP——动态注册的外部工具

表 3-1:我们的 4 个工具 vs Claude Code 的 45+

差距看起来很大,但核心机制完全一样——Schema 定义 + Registry 注册 + Executor 执行。Claude Code 多出来的那些工具不是用了什么黑魔法,而是在同一个框架里多注册了几十个工具。

每个类别的设计取舍值得一提:

文件操作:全量写入 vs 精确替换

我们只有一个写文件工具,整个覆盖。Claude Code 区分了全量写入和精确替换(只改某几行)。为什么?因为修改一个 1000 行文件里的 3 行代码时,全量写入意味着 AI 要输出 1000 行——浪费 token 且容易出错。精确替换只传需要改的那几行,省钱又精准。

命令执行:沙箱与后台

我们的命令执行工具是直接执行。Claude Code 的版本多了三个关键能力:沙箱(限制文件系统访问范围)、超时控制(防止命令卡死)、后台模式(长时间运行的命令不阻塞对话)。这些是从”能用”到”安全好用”的差距。

代码搜索:文本 vs 语义

我们的搜索工具是文本搜索——只能找精确匹配的关键词。Claude Code 额外有按文件名模式找文件的能力,以及借助语言服务器做语义搜索——比如”找到这个函数的所有调用方”。文本搜索是 80% 场景的最佳选择,但语义搜索在大型代码库里不可替代。


延伸思考

在进入下一章之前,回头看看你的 harness/tools/ 目录,思考几个问题:

  1. 现在任何工具的执行结果都会完整传回给 AI——如果某个命令输出了 10 万行日志,会发生什么?你会怎么处理?(提示:第 2 章的微压缩做了什么?)
  2. 我们的命令执行工具可以执行任何命令,包括删除所有文件。在没有权限系统的情况下,你会怎么做初步的安全防护?
  3. 如果你要给 Agent 加一个”浏览网页”的工具,它的 Schema 应该怎么设计?需要哪些参数?

章节小结

  • 四个 Prompt 构建了一个完整的工具系统:读文件 → 写文件 → 跑命令 → 搜代码
  • 工具系统由三层组成:Tool Definition(Schema 描述)→ Tool Registry(注册发现)→ Tool Executor(执行回传)
  • Tool Schema 是给 AI 看的”使用说明书”——description 写得越清楚,AI 用得越准确
  • 工具执行的生命周期:AI 决策 → Schema 校验 → 执行 → 结果回传 → AI 继续(可能循环多次)
  • Claude Code 有 45+ 工具,但核心机制和我们的 4 个工具完全一样——差别在于数量和打磨程度
  • 工具系统让 Agent 从”只会说话”变成了”能干活”——下一章给它装上安全锁,防止它干坏事
English EN 简体中文 ZH