它会运行文件,却还不会读文件
第一课给 Harness 一个工具:运行 main.py。现在把要求改一点:
先读
main.py,再运行它,报告文件内容和运行结果。
文件仍然很短:
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。官方工具调用文档给出了这次往返的格式。
假设模型先选择读取文件,返回的内容相当于:
{
"type": "tool_use",
"id": "call_1",
"name": "read_file",
"input": {"path": "main.py"}
}name 负责找到实现,input 负责提供参数,id 负责把结果认回这一次请求。三个字段各有用途;只看见一段像函数调用的普通文字,还不能触发执行。
名称怎样找到真正的函数
Harness 保存一张注册表,键是公开的工具名称,值包含参数约定和实现入口。收到 read_file,就取出读取函数;收到 run_main,就取出运行函数。
下面把一次工具请求拆开。先走完“读文件”,再切换到“运行程序”,观察变化集中在哪里。
Harness 提供两项说明:read_file(path) 读取文件,run_main() 运行固定程序。实现函数留在本地。
- 模型先看到可用工具
Harness 提供两项说明:read_file(path) 读取文件,run_main() 运行固定程序。实现函数留在本地。
- 模型返回名称和参数
这是一份结构化请求。Harness 读取 name,再到自己的注册表中查找。
id=call_1 · name=read_file input={"path": "main.py"} - 检查该工具接受什么参数
read_file 要求一个字符串 path。字段缺失、多出或类型错误,都不能进入实现函数。
- 调用选中的实现函数
read_file 解析路径、检查目录范围并读取文件。模型不需要把读文件翻译成 Shell 命令。
read_file(path="main.py") → print(6 * 7) - 结果带原 ID 返回
分发层为输出加上 tool_use_id。循环保存结果,再次调用模型;下一次选哪个工具,要结合用户问题和已有结果判断。
type=tool_result · tool_use_id=call_1
两次请求经过的是同一个分发函数:检查名称、检查参数、调用实现、封装结果。变化的是注册项和传入参数。循环只拿到一份工具结果,仍然负责保存记录并再次调用模型。
工具名和参数形状也不能替代检查。即使说明写了 path 必须是字符串,执行入口仍应检查真实输入;本册还会拒绝多出的字段。run_main 收到 {"command": "..."} 时,不能顺手当作另一种用法执行。
在短代码里看一次“查表”
先把真实文件换成一个内存字典,留下分发的最小结构。下面的 Python 可以运行,也可以点行号设断点;它没有调用模型或访问文件。
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。输出分别是:
read_1 → print(6 * 7)
run_1 → 42(教学固定输出)这个短例子刻意只留下查表。真实入口还要处理未知名称、错误参数、文件不存在和执行异常;配对实践篇会把这些检查放回正式函数,并用断点观察拒绝发生在执行之前。
谁决定“读完以后再运行”?
注册表只回答“这个名称对应哪个实现”。它不会自动安排先读后运行,也不会因为两个工具都存在,就把它们各调用一遍。
在这次任务中,一条可能的路径是:
- 模型请求
read_file(path="main.py"),Harness 保存读取结果。 - 模型结合原问题和文件内容,请求
run_main(),Harness 保存运行结果。 - 模型看到两份证据,回答文件内容和运行结果。
如果模型在一次响应里提出多个请求,本课实现按它们出现的顺序执行,再统一回传结果。已有输入足以确定的调用可以这样提交;如果第二个调用的参数依赖第一个结果,就要先把结果交回模型,才能作下一次选择。
两份结果必须各带自己的 tool_use_id。工具名相同也可能调用多次,所以名称无法替代调用 ID。第一课保存完整请求和关联结果的规则,在这里继续适用。
加上工具以后,什么还没改变?
模型请求工具,Harness 分发执行,保存结果,再问模型;收到可接受的最终回答就停。这条循环没有因工具数量增加而换一套控制方式。调用次数上限也仍然需要:读取错误可以作为反馈交回去,但不能保证模型下一次一定选对。
在本册的实现中,新增工具需要注册用途、参数与函数,并验证实际执行边界。仅向模型公布一个新名称,本地没有相应实现,会得到未知工具错误;仅写好一个本地函数,模型没有看见说明,也无法据此选择它。
回到开头的要求,现在 42 只负责证明运行结果,读取工具负责提供文件文本。工具扩展了 Harness 能取得的证据;如何用这些证据满足用户要求,仍要回到循环中的下一次判断。