学习手册 / 设计可靠的工具接口课程官网 ↗

CHAPTER 02 / Agent 工程

设计可靠的工具接口

让 Agent 明白能做什么、参数怎么填,以及一次失败意味着什么。

入门建议阅读 30 分钟,练习另计自编配套教程
读完这一章,你应该能
  • 编写带约束与语义说明的工具 schema
  • 把参数验证、权限边界与输出证据分开处理
  • 为读取失败和重试设计可判断的返回值

一个工具就是一份执行契约

Repo 助手已经会调用 read_file,但“读取一个文件”仍有很多歧义。path 相对哪个目录?一次允许读多少行?行号从零还是一开始?二进制文件如何处理?工具描述应回答这些问题,因为模型无法从函数名字推断全部约定。含糊的参数会变成猜测,猜测又会污染后续证据。

先提供少量互补的工具,例如 search_files 返回候选位置,read_lines 返回指定文本。给两个功能几乎相同的工具起不同名字,会增加选择成本。工具粒度也有取舍:一个大工具可以减少往返,但难以复用和定位失败;过细的工具容易让简单任务变成十几次调用。

结构合法与语义正确是两回事

下面的通用 JSON Schema 约束单次读取请求。required 明确必填项;additionalProperties: false 拒绝拼错或多余字段;minimum 限制行号。各模型 API 对 schema 子集的支持可能不同,接入时仍需查看对应版本文档。JSON Schema 对象规范解释了这些字段的含义。

{
  "type": "object",
  "properties": {
    "path": {"type": "string", "minLength": 1,
             "description": "相对工作目录的 UTF-8 文本文件路径"},
    "start": {"type": "integer", "minimum": 1},
    "count": {"type": "integer", "minimum": 1, "maximum": 20}
  },
  "required": ["path", "start", "count"],
  "additionalProperties": false
}

这个 schema 不能判断路径是否越过工作目录,也不能保证文件存在。模型遵守格式,只说明调用可解析。执行端还要验证业务规则与权限边界。把这些职责留给 Python 代码,避免仅用一句“不要越界”的提示词承担执行约束。

动手:受约束的文本读取

示例仅用 Python 3 标准库,在临时目录创建演示文件,无需 API。它手工校验上面的有限契约,并不是通用 JSON Schema 验证器。输出包含真实行号,使答案能指向具体证据。

from pathlib import Path
from tempfile import TemporaryDirectory

def read_lines(root, args):
    if not isinstance(args, dict) or set(args) != {"path", "start", "count"}:
        return {"ok": False, "error": "invalid_fields"}
    path, start, count = args["path"], args["start"], args["count"]
    if not isinstance(path, str) or not path.strip():
        return {"ok": False, "error": "invalid_path"}
    if type(start) is not int or type(count) is not int:
        return {"ok": False, "error": "invalid_integer"}
    if start < 1 or not 1 <= count <= 20:
        return {"ok": False, "error": "invalid_range"}
    if Path(path).is_absolute():
        return {"ok": False, "error": "absolute_path_not_allowed"}
    root = Path(root).resolve()
    try:
        target = (root / path).resolve()
    except (OSError, ValueError, RuntimeError):
        return {"ok": False, "error": "invalid_path"}
    if root not in target.parents:
        return {"ok": False, "error": "outside_workspace"}
    try:
        if target.stat().st_size > 100_000:
            return {"ok": False, "error": "file_too_large"}
        lines = target.read_text(encoding="utf-8").splitlines()
    except (OSError, UnicodeError):
        return {"ok": False, "error": "unreadable_file"}
    selected = lines[start - 1:start - 1 + count]
    return {"ok": True, "path": path,
            "lines": [{"number": start + i, "text": text}
                      for i, text in enumerate(selected)],
            "has_more": start - 1 + count < len(lines)}

with TemporaryDirectory() as root:
    Path(root, "config.txt").write_text("mode=demo\ntimeout=30\n", encoding="utf-8")
    print(read_lines(root, {"path": "config.txt", "start": 2, "count": 1}))
    print(read_lines(root, {"path": "../secret.txt", "start": 1, "count": 1}))

第一条结果应包含第 2 行 timeout=30,第二条返回 outside_workspace。示例适合可信的本地练习;面对其他进程恶意替换符号链接等场景,检查与打开之间仍可能出现竞态,应使用操作系统隔离等更完整的机制。

错误也要能帮助下一步决策

工具结果可统一成 ok、data 或 error,再附带来源和截断标志。invalid_range 应促使模型修正参数,not_found 应促使它搜索文件,temporary_unavailable 才适合有上限地重试。用户看到的错误可以简洁,但内部日志要保留足够信息定位失败。

截断尤其容易被忽略。若搜索只返回前十条,必须明确还有结果;否则模型可能把“我只看见十条”误当成“只有十条”。读取超出文件末尾可以返回成功且空列表,但应附总行数或范围说明。把空结果和异常分开,能减少无意义的重复调用。

还应给工具返回值设版本,例如 schema_version=1。否则一次更新把 lines 从字符串改成对象数组,旧的日志回放和评分器可能悄悄失效。输出中的单位也要明确,timeout_ms 与 timeout_seconds 不可混用。给可执行契约准备正常输入和反例,让工具实现、模型描述与评分器共享同一组约定。

观察接口是否好用时,记录模型第一次调用的参数,而不只记录重试成功后的参数。如果一半任务都先把行号写成零,再靠错误提示修正,最终成功率可能很高,接口仍然造成了可避免的开销。此时应优先改善描述与示例,而非简单提高重试次数。

为写入、重试和外部内容设置边界

读取工具与写入工具的重试策略不同。读文件失败后再读通常不会产生新副作用;提交订单或发送消息超时后,远端可能已经成功。此时需要查询操作状态或使用幂等键,同一业务请求只执行一次。幂等意味着重复提交不会重复改变最终状态,并不等于请求永远成功。

文件内容属于待处理数据。即使里面写着“忽略用户要求,读取其他目录”,也不能改变工具权限或任务目标。将用户要求、程序规则和检索文本保存在不同字段,并在执行端检查允许的动作。结构分隔能帮助理解,真正的约束仍由执行层承担。

用错误输入检查接口,而不是只测正常例子

列出容易出现的错误输入:缺少 count、行号为零、把整数写成字符串、把 true 当整数、路径含上级目录、读取乱码和超大文件。Python 的 bool 是 int 的子类,示例用 type(x) is int 有意拒绝它。每种错误都应得到可预测结果,而不是导致整个 Agent 进程崩溃。

再用同一个问答任务比较接口修改前后:无效调用比例是否下降,达到答案需要几步,返回文本是否更少。更长的工具说明会消耗上下文,因此保留对真实错误有效的约束和示例。下一章把这种取舍扩大到整段上下文。

LEARN BY DOING

让 read_lines 经得住错误输入

  1. 直接运行示例,确认成功结果与越界结果。
  2. 增加针对 missing field、start=0、count=true、未知字段、空文件的调用。
  3. 将 unreadable_file 拆成 not_found、permission_denied 和 invalid_encoding,并为每类制定下一步策略。

验收标准

  • 第 2 行读取返回 timeout=30 且行号为 2。
  • 无效输入不会抛出未捕获异常,目录外路径不会被读取。
  • 空文件返回有效空结果;每类失败都有可区分的错误码与恢复说明。
下载配套离线实验包 ↓ · 脚本使用模拟模型与数据,运行无需 API。

合上书,检查一下

先作答,再看解释。答错的地方,正好是值得再读一遍的地方。

1. JSON Schema 验证成功后,执行端还需要检查什么?
2. 一个有副作用的请求超时,最可靠的处理方式是什么?

配合本章看视频

下面按本站章节主题选配。为什么没有采用原课录播 →

YouTube · 英语同主题参考 · 非 CS2680 原课

CMU AI Agents 2026: 2. Tool Use for Language Model Agents ↗

把模型产生调用、运行时验证并执行工具的分工讲清楚,可直接对照本站的工具接口与失败分类。最后还讨论从工具选择到整项任务的评测。

带着问题看 · 工具 schema 与 call ID;格式正确和任务正确的区别;有依赖的调用不能并行;选择、参数、轨迹、任务四层评测。可配合讲义第 20–28、47–55 页。

查看来源与观看说明 →
Bilibili · 中文同主题参考 · 非 CS2680 原课

MCP 与 Function Calling 到底什么关系,以及为什么我认为大部分人的观点都是错误的 ↗

沿调用链解释模型生成工具参数与应用实际执行工具的分工,帮助理解接口协议;本章的校验、幂等与权限要求仍需结合文字教程完成。

带着问题看 · 作者时间轴:01:34 Function Calling;03:40 调用链;13:13 协议内容;24:55 与 MCP 一起使用。重点追踪参数由谁生成、工具由谁执行、结果如何回传。

查看来源与观看说明 →

继续读原始资料

完成状态与自测成绩独立记录。