工具系统 — 让 Agent 有手有脚
Agent 不能只会说话——它得能读文件、写文件、跑命令
📝 本章目标
读完本章,你将:
- 用四个 Prompt 让 AI 帮你实现一套完整的工具系统——读文件、写文件、跑命令、搜代码
- 理解工具系统的三层架构——定义、注册、执行
- 掌握 Tool Schema 的设计原则——为什么 AI 能”知道”怎么调工具
- 了解 Claude Code 45+ 工具背后的并发执行和分类管理
上一章我们给 Agent 装上了”嘴巴”——它能跟你对话了,而且聊再久也不怕爆。但你很快就会发现一个尴尬的事实:它只会说,不会做。
你问它”帮我看看这个文件里写了什么”,它会说”好的,让我来查看——“然后就卡住了,因为它根本没有能力读你的文件。你让它”把这个 bug 修一下”,它会给你一段看起来不错的代码,但它改不了你的文件——你得自己手动复制粘贴。
这就像雇了一个顾问:什么都懂,但什么都不动手。你需要的不是顾问,是一个能撸起袖子干活的人。
工具系统就是给 Agent 装上手和脚。装完之后,它不仅能说”这个文件有问题”,还能直接帮你改掉。
动手:用四个 Prompt 给 Agent 装上手脚
确认你在 harness 项目根目录。如果你跟着第 2 章做了查询引擎,现在应该有 harness/engine.py 和 harness/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:工具系统的三层架构
- Tool Definition — JSON Schema、参数描述、能力声明 → 告诉 AI「我能做什么」
- Tool Registry — 自动发现、统一注册、名称映射 → 运行时找到工具
- 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 三件事:
- 这个工具叫什么——AI 通过名字来”点名”调用
- 这个工具能做什么——
description字段帮助 AI 判断什么时候该用它- 需要传什么参数——每个参数的类型、含义、是否必填
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 在多个工具之间做选择offset和limit不是必填的——大文件时可以分段读,小文件时直接全部读- 参数的
description解释了格式要求(“absolute or relative path”),减少 AI 传错参数的概率
这个 Schema 会在每次 API 调用时传给 Claude。Claude 看到 Schema,就知道”哦,我有一个能力叫 read_file,需要给一个 path”——然后它会在合适的时候主动调用。
工具执行的生命周期
从 AI 决定”我要调用一个工具”到结果回传,中间经历了完整的生命周期:
图 3-2:工具执行的生命周期
- AI 决策 → 返回 tool_use
- Schema 校验 → 参数合法?
- 执行工具 → 运行实际逻辑
- 结果回传 → tool_result
- AI 继续 → 下一步决策
具体来说,每次工具调用的流程是这样的:
- AI 决策——Claude 分析用户请求后,判断需要调用某个工具。它返回一个
tool_use内容块,包含工具名和参数(JSON 格式) - Schema 校验——查询引擎收到
tool_use块后,先检查:工具名存在吗?参数类型对吗?必填字段都有吗?不合法就直接返回错误 - 执行工具——校验通过后,调用工具的实际逻辑。读文件就真的读文件,跑命令就真的跑命令
- 结果回传——把执行结果(成功的内容或失败的错误信息)包装成
tool_result消息,追加到对话历史 - 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 个以上。它们可以分成几大类:
| 分类 | 我们的 Harness | Claude Code |
|---|---|---|
| 文件操作 | read_file, write_file | Read, Write, Edit, MultiEdit, NotebookEdit |
| 命令执行 | bash | Bash(沙箱 + 超时 + 后台模式) |
| 代码搜索 | grep | Grep, 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/ 目录,思考几个问题:
- 现在任何工具的执行结果都会完整传回给 AI——如果某个命令输出了 10 万行日志,会发生什么?你会怎么处理?(提示:第 2 章的微压缩做了什么?)
- 我们的命令执行工具可以执行任何命令,包括删除所有文件。在没有权限系统的情况下,你会怎么做初步的安全防护?
- 如果你要给 Agent 加一个”浏览网页”的工具,它的 Schema 应该怎么设计?需要哪些参数?
章节小结
- 四个 Prompt 构建了一个完整的工具系统:读文件 → 写文件 → 跑命令 → 搜代码
- 工具系统由三层组成:Tool Definition(Schema 描述)→ Tool Registry(注册发现)→ Tool Executor(执行回传)
- Tool Schema 是给 AI 看的”使用说明书”——
description写得越清楚,AI 用得越准确 - 工具执行的生命周期:AI 决策 → Schema 校验 → 执行 → 结果回传 → AI 继续(可能循环多次)
- Claude Code 有 45+ 工具,但核心机制和我们的 4 个工具完全一样——差别在于数量和打磨程度
- 工具系统让 Agent 从”只会说话”变成了”能干活”——下一章给它装上安全锁,防止它干坏事