第 4 章 工具设计与执行边界
设计工具契约,处理参数、资源范围、版本冲突与副作用
4.1 先设计可用接口,再要求模型选对工具
本章目标:能为一个工具写完整契约,区分参数格式与权限校验,处理超时、重试和不确定的副作用。
给运费 Agent 一个叫 do_everything(text) 的工具,模型可能把“读文件、改代码、运行测试、提交”全部写进字符串。运行时很难知道它究竟准备做什么,也难以单独限制写入或识别失败位置。
拆成 read_file(path)、replace_file(path,content)、run_tests(),每种动作的输入、产物和副作用都清楚。教学实验将测试命令固定在工具内部,不接收模型生成的任意 shell。这样可以检查代码而不给策略无限制命令接口;真实开发工具可能需要更广的能力,但也应明确执行范围。
4.2 一份工具契约要包含什么
| 契约项 | read_file 的例子 |
缺失后会发生什么 |
|---|---|---|
| 用途与不适用范围 | 读取允许的项目文件,不读取任意宿主路径 | 模型误选工具或越界 |
| 输入 | 非空相对路径、可选行范围 | 参数含糊或输出过大 |
| 输出 | 路径、版本、内容、是否截断 | 无法引用或判断完整性 |
| 错误 | 不存在、拒绝、编码失败 | 所有错误被误当“空文件” |
| 副作用 | 无写入;可能计费或留访问记录 | 重试安全性判断错误 |
| 资源限制 | 最大字节数、超时 | 占满上下文或长时间等待 |
工具描述也应该回答“何时该用它”。两个工具都叫“搜索”,一个找文件名,一个检索全文,模型容易混淆。更清楚的用途与返回示例可以减少误选,但真正执行时仍要由程序检查。
4.3 Schema 只通过了第一道门
{
"name": "read_file",
"description": "读取工作区内获准文件,返回内容和版本;不写文件",
"parameters": {
"type": "object",
"properties": {"path": {"type": "string", "minLength": 1}},
"required": ["path"],
"additionalProperties": false
}
}
{"path":"../secret.txt"} 完全可能满足这个 schema,但超出了工作区。可靠校验通常有四层:工具名是否允许、参数形状是否正确、业务语义是否合理、当前身份是否有权操作该资源。写文件还要检查容量、目标类型和预期版本。
# 教学片段:resolve 处理 .. 与已有符号链接,再检查目录包含关系。
from pathlib import Path
def resolve_in_workspace(root, raw_path):
if not isinstance(raw_path, str) or not raw_path:
raise ValueError("invalid path")
root = Path(root).resolve()
target = (root / raw_path).resolve()
if not target.is_relative_to(root):
raise PermissionError("outside workspace")
return target
这个片段说明路径检查的思路,并不是生产沙箱:并发的符号链接替换仍可能引入检查与使用之间的竞态。实际隔离还要结合 OS 能力和文件打开方式。repair_lab.py 在受控临时目录中采用更窄的白名单,只允许 pricing.py;它同样没有降低 Python 进程的宿主权限。
4.4 输出格式决定下一步能否正确判断
不要把所有结果都压成一个字符串。至少区分执行状态、业务结果、来源版本与完整性。
{
"status": "ok",
"path": "pricing.py",
"source_hash": "sha256:...",
"content": "def shipping_fee(...): ...",
"truncated": false
}
若日志过长,应返回开头/关键错误/结尾的结构化片段,并告诉策略还有多少内容、怎样获取完整结果。静默截断可能丢掉真正的失败原因。若工具报 NOT_FOUND,不应伪装成空内容;若测试命令退出 1,应提供失败检查而不只写“命令异常”。
错误信息要能支持决策:PERMISSION_DENIED 通常需要调整范围或交给用户,不应反复同参重试;临时网络 TIMEOUT 可以在有限预算内尝试恢复;格式错误应修参数。错误细分不是为了枚举漂亮的状态码,而是为了定义不同下一步。
4.5 超时的副作用为什么难处理
只读查询超时,通常可以再查;发送邮件超时,可能邮件已送出但响应丢失。盲目重试会重复发送。“客户端没有收到成功”并不等于“服务端没有执行”。即使是读接口,也可能计费或触发其他行为,因此重试依据应来自具体契约。
对有副作用操作可定义状态 prepared → dispatched → confirmed。若响应丢失,记录为 unknown,先查询操作状态,不能直接当作 failed 再发一次。这里的状态名是应用设计示例。
幂等键使同一个逻辑操作的重复请求能被服务识别;前提是服务端真的支持,并规定键的作用范围和有效期。文件修改可携带 expected_hash,只在目标版本仍一致时应用,避免覆盖并行更新。多工具中的幂等性、版本控制和事务不能互相替代。
4.6 并行工具:先画依赖,再节省时间
读取两个独立文件可以并行;先写补丁再测代码有依赖,不能同时开始。若并行测试与修改同一文件,结果可能对应混合状态,即使输出“通过”也不可靠。
日志应记录调用 ID、开始/结束、参数摘要、结果版本、字节数、错误码和重试序号。要比较并行收益,记录的是墙钟时间,不是把每个工具耗时简单相加;资源竞争和排队仍会影响实际速度。
4.7 动手练习与参考检查
运行 python3 agent/examples/repair_lab.py --scenario invalid,观察越界请求在哪里被拒绝。再在脚本副本中直接调用 Tools.execute,分别传入未知工具、缺少 path、多余字段、非字符串内容、超大内容。每个请求都应得到明确错误,不应修改文件。
增加一个带 expected_hash 的写入工具:先读取哈希;模拟另一执行者修改文件;再提交基于旧哈希的内容。参考验收是返回版本冲突并保留新文件。这个练习比仅增加一个 schema 字段更完整,因为它检验了冲突时的实际行为。
本章交付物:一份工具契约、一个错误到下一步的映射表、一次版本冲突实验。能说明副作用在超时后如何确认,才具备可靠工具集成的基础。