程云来 / 杭州
返回文章·Agent 与 Harness
·约 6 分钟

思想|Tool Use,多一个工具,循环要重写吗?

从先读 main.py、再运行它出发,理解工具说明、名称分发、参数校验与调用 ID;工具变多,循环仍按同一套规则工作。


它会运行文件,却还不会读文件

第一课给 Harness 一个工具:运行 main.py。现在把要求改一点:

先读 main.py,再运行它,报告文件内容和运行结果。

文件仍然很短:

python
print(6 * 7)

运行它会得到 42。但只知道输出,不能证明文件里写了什么:print(42)、print(40 + 2) 都能给出相同结果。用户要求的“读文件”,需要一次独立的读取。

可以再给 Harness 一个 read_file。随之而来的问题是:工具从一个变成两个,循环中是否也得多写一套“读文件 → 回传 → 再问模型”的流程?

Learn Claude Code 第二课提出的方向是把工具实现放进分发表。沿着这个方向,我们只增加一个读取工具,用同一个例子看看循环和工具怎样分开。

工具有两面:模型看说明,Harness 调函数

先把两个工具的用途写清楚:

名称接受什么返回什么
read_file字符串 path,例如 main.py文件文本,或读取错误
run_main空对象 {}main.py 的退出码与输出

这张表里包含两种读者。模型需要名称、用途和参数形状,才能提出合适的请求;Harness 需要可执行的函数,才能真正操作文件或启动进程。

因此,提供工具说明时,不会把 Python 函数对象发给模型。以本册使用的 Claude 客户端工具协议为例,说明通过 tools 传入,模型返回带 name、input、id 的 tool_use,应用负责执行,并提交关联的 tool_result。官方工具调用文档给出了这次往返的格式。

假设模型先选择读取文件,返回的内容相当于:

json
{
  "type": "tool_use",
  "id": "call_1",
  "name": "read_file",
  "input": {"path": "main.py"}
}

name 负责找到实现,input 负责提供参数,id 负责把结果认回这一次请求。三个字段各有用途;只看见一段像函数调用的普通文字,还不能触发执行。

名称怎样找到真正的函数

Harness 保存一张注册表,键是公开的工具名称,值包含参数约定和实现入口。收到 read_file,就取出读取函数;收到 run_main,就取出运行函数。

下面把一次工具请求拆开。先走完“读文件”,再切换到“运行程序”,观察变化集中在哪里。

TOOL_USEread_fileid: call_1read_filerun_mainNAME → INPUT → RUNFILE READERpath → textTOOL_RESULTtool_use_id请求分发与校验实现函数关联结果
模型先看到可用工具

Harness 提供两项说明:read_file(path) 读取文件,run_main() 运行固定程序。实现函数留在本地。

  1. 模型先看到可用工具

    Harness 提供两项说明:read_file(path) 读取文件,run_main() 运行固定程序。实现函数留在本地。

  2. 模型返回名称和参数

    这是一份结构化请求。Harness 读取 name,再到自己的注册表中查找。

    id=call_1 · name=read_file input={"path": "main.py"}
  3. 检查该工具接受什么参数

    read_file 要求一个字符串 path。字段缺失、多出或类型错误,都不能进入实现函数。

  4. 调用选中的实现函数

    read_file 解析路径、检查目录范围并读取文件。模型不需要把读文件翻译成 Shell 命令。

    read_file(path="main.py") → print(6 * 7)
  5. 结果带原 ID 返回

    分发层为输出加上 tool_use_id。循环保存结果,再次调用模型;下一次选哪个工具,要结合用户问题和已有结果判断。

    type=tool_result · tool_use_id=call_1

两次请求经过的是同一个分发函数:检查名称、检查参数、调用实现、封装结果。变化的是注册项和传入参数。循环只拿到一份工具结果,仍然负责保存记录并再次调用模型。

工具名和参数形状也不能替代检查。即使说明写了 path 必须是字符串,执行入口仍应检查真实输入;本册还会拒绝多出的字段。run_main 收到 {"command": "..."} 时,不能顺手当作另一种用法执行。

在短代码里看一次“查表”

先把真实文件换成一个内存字典,留下分发的最小结构。下面的 Python 可以运行,也可以点行号设断点;它没有调用模型或访问文件。

PyPython · 浏览器本地运行
未运行
files = {"main.py": "print(6 * 7)"}


def read_file(path):
    return files[path]


def run_main():
    return "42(教学固定输出)"


handlers = {"read_file": read_file, "run_main": run_main}
calls = [
    {"id": "read_1", "name": "read_file", "input": {"path": "main.py"}},
    {"id": "run_1", "name": "run_main", "input": {}},
]
for call in calls:
    handler = handlers[call["name"]]
    output = handler(**call["input"])
    result = {"tool_use_id": call["id"], "content": output}
    print(result["tool_use_id"], "→", result["content"])
输出

点击行号设置断点,再点“调试”;也可以直接运行。

代码只读;执行发生在浏览器中,不会发送到博客服务器。

把断点放在 handler = handlers[call["name"]],用“继续”让它命中两次。调用方式相同,选中的函数却不同:第一次把 path 展开给 read_file;第二次把空参数展开给 run_main。输出分别是:

text
read_1 → print(6 * 7)
run_1 → 42(教学固定输出)

这个短例子刻意只留下查表。真实入口还要处理未知名称、错误参数、文件不存在和执行异常;配对实践篇会把这些检查放回正式函数,并用断点观察拒绝发生在执行之前。

谁决定“读完以后再运行”?

注册表只回答“这个名称对应哪个实现”。它不会自动安排先读后运行,也不会因为两个工具都存在,就把它们各调用一遍。

在这次任务中,一条可能的路径是:

  1. 模型请求 read_file(path="main.py"),Harness 保存读取结果。
  2. 模型结合原问题和文件内容,请求 run_main(),Harness 保存运行结果。
  3. 模型看到两份证据,回答文件内容和运行结果。

如果模型在一次响应里提出多个请求,本课实现按它们出现的顺序执行,再统一回传结果。已有输入足以确定的调用可以这样提交;如果第二个调用的参数依赖第一个结果,就要先把结果交回模型,才能作下一次选择。

两份结果必须各带自己的 tool_use_id。工具名相同也可能调用多次,所以名称无法替代调用 ID。第一课保存完整请求和关联结果的规则,在这里继续适用。

加上工具以后,什么还没改变?

模型请求工具,Harness 分发执行,保存结果,再问模型;收到可接受的最终回答就停。这条循环没有因工具数量增加而换一套控制方式。调用次数上限也仍然需要:读取错误可以作为反馈交回去,但不能保证模型下一次一定选对。

在本册的实现中,新增工具需要注册用途、参数与函数,并验证实际执行边界。仅向模型公布一个新名称,本地没有相应实现,会得到未知工具错误;仅写好一个本地函数,模型没有看见说明,也无法据此选择它。

回到开头的要求,现在 42 只负责证明运行结果,读取工具负责提供文件文本。工具扩展了 Harness 能取得的证据;如何用这些证据满足用户要求,仍要回到循环中的下一次判断。