已发布资料基础官方资料 + 个人实践
·15 分钟0 次阅读AI Agent 工程进阶
文章/AI 与智能体

Tool Engineering:让 Agent 正确调用工具,也安全地失败

AI Agent 工程进阶第 5 篇:从名称、描述和 JSON Schema 讲到权限、审批、预演、幂等、结构化错误、分页与工具加载,并在同一个 GitHub 工程中用 11 个边界案例比较宽工具和类型化 Tool Registry。

#Agent#Agent Engineering#Tool Engineering#Function Calling#MCP
文章目录
  1. 一分钟概览
  2. 1. 工具不是给模型看的 API 文档
  3. 2. 一个可运营工具至少有两层契约
  4. 第一层:模型可见契约
  5. 第二层:运行时契约
  6. 3. 宽工具还是窄工具
  7. 4. 名称和描述是在做路由,不是在写文案
  8. 命名检查
  9. 描述检查
  10. 5. Schema 的目标是让无效状态难以表达
  11. 6. 权限、审批和预演是三个不同问题
  12. 为什么使用独立 preview 工具
  13. 7. 幂等要同时校验 key 和请求指纹
  14. 8. 错误要告诉 Agent 下一步,而不是只说失败
  15. 一组够用的错误分类
  16. retryable: true 仍然不够
  17. 9. 输出不是越完整越好
  18. 10. Direct、Programmatic 与 MCP 解决的不是同一层
  19. MCP 是传输和发现协议,不是安全证明
  20. 11. 本篇实验:同一能力,两种工具表面
  21. 对照组:wide-tool-v1
  22. 候选组:typed-registry-v2
  23. 十一个边界案例
  24. 12. 跟着运行 Lab 0.5.0
  25. 13. Tool Registry 真正做了什么
  26. 14. 怎样正确阅读 1/11 与 11/11
  27. 15. 真正的模型工具 Eval 应该怎样补
  28. 数据集至少覆盖四类任务
  29. 每条任务记录
  30. 重复运行并分开统计
  31. 保留 held-out 集合
  32. 16. 工具数量变大以后怎么办
  33. Namespace
  34. Deferred loading / tool search
  35. 根据身份动态启用
  36. 收敛近义工具
  37. 17. 从 Function Tool 映射到 MCP 与框架
  38. 18. 把现有 API 改成 Agent Tool 的六步练习
  39. 第 1 步:列出真实用户意图
  40. 第 2 步:标记副作用和权限
  41. 第 3 步:收紧输入和输出 Schema
  42. 第 4 步:设计错误与恢复动作
  43. 第 5 步:补幂等和预演
  44. 第 6 步:分两层评估
  45. 19. 接入生产前的 Tool Engineering 清单
  46. 可理解性
  47. 输入与输出
  48. 副作用
  49. 错误与恢复
  50. 工具规模
  51. 结语:好工具让错误提案停在边界上
  52. 参考资料
  53. OpenAI
  54. Anthropic、MCP 与 LangGraph
  55. 本文代码与证据

一个 Agent 能调用函数,并不意味着它已经拥有了“好用的工具”。

真实项目里,我更常见到另一种情况:API 本身没有问题,模型也确实发起了调用,但系统仍然在工具边界上失控。

  • 用户只想预览修改,Agent 却直接写入;
  • 模型漏了参数,工具用空字符串继续执行;
  • 超时以后盲目重试,把同一条记录写了两次;
  • 一个只读用户通过 Agent 调到了管理员动作;
  • 搜索工具一次返回几百条结果,把下一轮 Context 塞满;
  • 工具只返回一句 request failed,模型不知道该重试、改参数还是停止;
  • 工具越来越多,光 Schema 就占掉大量输入,选择反而更不稳定。

这些不是单纯的 Prompt 问题,而是 Tool Engineering 问题。

本文是 AI Agent 工程进阶 第 5 篇。上一篇 Harness Engineering 解决“谁负责组织模型、工具、状态和停止”;这一篇继续深入最容易产生真实副作用的接口:一个工具怎样既便于模型理解,又能被运行时可靠约束。

项目说明
内容类型工具契约设计、失败处理与可复现实验
适合读者已经实现 function calling / MCP,开始遇到参数错误、越权、重复写入或工具膨胀的开发者
阅读时间约 15-22 分钟
跟做时间45-60 分钟
环境要求Python 3.10+,零第三方依赖,不需要 API Key
代码检查点3e891ff
可带走产物Tool Registry、11 个边界案例、结构化错误、幂等回执、分页输出和对照报告
资料核对日期2026-07-31
实验边界重放固定、可检查的工具提案,验证运行时契约;不调用真实模型,不测模型选工具准确率

先说明本文所说的“工具”

这里的工具不是只有一个 Python 函数。它包含两部分:一部分是提供给模型的名称、描述和输入 Schema;另一部分是运行时掌握的权限、审批、副作用、超时、幂等、错误和输出约束。前者帮助模型提出候选调用,后者决定调用能否安全落地。

一分钟概览

如果只保存这篇文章的结论,可以记住十二点:

  1. 工具是确定性系统与非确定性 Agent 之间的契约。 API 对人类开发者易用,不代表对模型也易用。
  2. 模型可见契约与运行时契约要分开。 名称、描述、Schema 帮助选择;权限、审批和幂等必须由代码执行。
  3. 先按用户意图设计工具,再考虑复用后端 API。 不要把几十个底层端点原样暴露给模型。
  4. 宽工具和窄工具没有绝对答案。 宽工具减少 Schema 体积,窄工具通常让意图、副作用和权限更清楚。
  5. Schema 要让无效状态尽量无法表达。 必填字段、枚举、边界和 additionalProperties: false 都有价值。
  6. 预览和写入最好有清楚分界。 对高风险动作,独立 preview_* 往往比一个容易被忽略的 dry_run 布尔值更直观。
  7. 审批不是授权。 用户批准某次动作,不代表调用者拥有该资源的写权限;两者都要检查。
  8. 幂等不是“见到相同 key 就返回旧结果”。 同一 key 配不同参数应返回冲突,否则错误请求会被悄悄掩盖。
  9. retryable 不是自动重试开关。 还要判断副作用、结果是否已知、是否有幂等键,以及预算是否允许。
  10. 工具输出也是 Context。 返回稳定 ID、摘要、游标和证据,不要默认倾倒整个对象或完整日志。
  11. 工具越多,选择成本和 Context 成本越高。 Namespace、deferred loading 和 tool search 是规模化手段,不是第一天就必需的架构。
  12. 本地契约测试不能替代模型 Eval。 11/11 只能证明运行时守住了边界;模型是否更会选工具,要用真实任务、重复运行和 held-out 集合验证。

图 1:Tool Engineering 把模型提案送入类型化 Tool Registry,再依次经过 Schema、权限、审批、幂等、执行和输出验证图 1:Tool Engineering 把模型提案送入类型化 Tool Registry,再依次经过 Schema、权限、审批、幂等、执行和输出验证

1. 工具不是给模型看的 API 文档

传统 API 的调用者通常是确定性程序。只要函数签名和协议稳定,调用方会按代码路径提供参数:

python
record_followup(
    ticket_id="T-102",
    note="Customer asked for a callback.",
)

Agent 不同。模型面对的是自然语言目标、若干工具描述和一段会变化的 Context。它可能:

  • 不调用任何工具;
  • 选错相似工具;
  • 选对工具但漏参数;
  • 自行补出不存在的 ID;
  • 在用户只要求分析时调用写工具;
  • 根据工具结果继续、停止或换一条路线。

Anthropic 在 Writing effective tools for agents 里把工具描述为确定性系统与非确定性 Agent 之间的新型契约。这个判断很关键:Tool Engineering 不是替底层 API 补一份说明,而是设计一个适合 Agent 感知、选择、调用和恢复的动作表面。

因此,一个后端端点不一定对应一个 Agent 工具:

text
后端 API:面向系统复用、字段完整、兼容多个调用方
Agent Tool:面向任务意图、参数受限、输出有预算、失败可恢复

有时三个 API 应该被组合成一个工具,因为它们总是连续执行;有时一个通用 API 应拆成三个工具,因为读取、预览和写入拥有完全不同的风险。

2. 一个可运营工具至少有两层契约

我把工具契约拆成两层。

图 2:模型可见契约负责可理解性,运行时契约负责真实控制,两层共同包住业务处理器图 2:模型可见契约负责可理解性,运行时契约负责真实控制,两层共同包住业务处理器

第一层:模型可见契约

模型通常会看到:

text
name
description
input schema
有些平台还支持 output schema、namespace 或调用方式

它回答的是:

text
这个工具做什么?
什么时候应该用?
什么时候不应该用?
必须提供哪些参数?
返回什么字段?

第二层:运行时契约

这层不应只靠模型自觉遵守:

text
required_permission
effect: read / write / destructive
approval policy
timeout and concurrency
idempotency semantics
retry policy
input and output validation
audit and redaction

它回答的是:

text
当前调用者真的有权执行吗?
这次写入是否已批准?
重复调用会发生什么?
超时以后结果是失败、未知还是可安全重试?
工具是否返回了符合契约的结果?
哪些信息允许进入下一轮 Context?

最危险的实现,是把第二层偷偷塞进描述:

text
“这个工具会修改数据,请只在获得批准后调用。”

这句话有助于模型做判断,但没有形成安全边界。真正的边界应该是:

python
if spec.required_permission not in actor.permissions:
    return permission_denied()

if spec.approval == "required" and not approved:
    return approval_required()

模型描述负责减少错误提案,运行时负责阻止错误提案变成真实事故。

3. 宽工具还是窄工具

假设工单系统已经有一个通用接口:

json
{
  "name": "ticket_operation",
  "arguments": {
    "operation": "get | preview | record | list | slow",
    "payload": {}
  }
}

它的优点很直接:Schema 小,适配快,后端容易复用。问题也很明显:真正意图藏在 operation 和自由形态的 payload 里;读取与写入共享一个权限入口;描述要同时解释很多分支;输出也很容易变成“什么都可能返回”。

另一种设计是拆成:

text
get_ticket
preview_ticket_followup
record_ticket_followup
list_tickets
slow_ticket_lookup

图 3:同一组工单能力既可通过一个宽工具暴露,也可按读取、预演、写入、分页和慢依赖拆成窄工具图 3:同一组工单能力既可通过一个宽工具暴露,也可按读取、预演、写入、分页和慢依赖拆成窄工具

这不是“工具越小越好”。更实用的判断表是:

情况更适合合并更适合拆分
动作总是按固定顺序执行是,把编排下沉到代码
参数、返回值和权限高度相似
读取、写入、删除风险不同
用户会单独要求预览
一个枚举分支已经很多通常否
拆分后产生大量近义工具谨慎可能应重新分组或加 namespace
工具目录已经很大合并相关能力或延迟加载只拆真正需要独立选择的意图

本篇 Lab 选择拆分,不是为了证明拆分一定更优,而是为了让权限、审批、幂等和输出边界可以独立表达。实验也如实记录了代价:模型可见的工具目录从 247 字节 增至 2505 字节

这约 10 倍的差距不是 token 账单,只是稳定的 UTF-8 字节代理;但它提醒我们:更清楚的契约会消耗 Context,工具设计必须同时看可靠性和暴露成本。

4. 名称和描述是在做路由,不是在写文案

OpenAI 的 Function calling 指南 建议使用清晰、详细的函数名、参数说明和调用指令,并明确什么时候使用、什么时候不使用。Anthropic 的工具工程文章同样强调 namespace、返回有意义的 Context、控制 token 和通过 Eval 改进描述。

一个弱描述通常只有能力名:

text
record_ticket_followup
Record a follow-up.

更可操作的描述应该补齐边界:

text
Append one approved follow-up to a support ticket.
This tool writes data, requires ticket:write, and deduplicates
repeated calls by action_id. Do not use it for previews.

这里包含四个路由信号:

text
动作:append one follow-up
副作用:writes data
前置条件:approved + ticket:write
反例:do not use for previews

但要再次强调:这四句话只帮助模型做选择。ticket:write 和审批仍然由 Harness 检查。

命名检查

  • 用动词表达动作:get_list_preview_record_
  • 让相似工具的区别出现在名字里,而不是藏在说明最后;
  • 不要混用 managehandleprocess 这类边界不清的词;
  • 通过 namespace 表达领域,例如 ticket.getbilling.refund
  • 名称重构要配合 Eval,因为它会直接改变模型路由表面。

描述检查

  • 第一行先说清真正结果;
  • 写明必要前置条件与输入格式;
  • 对常见误用写清“不要在何时使用”;
  • 说明输出的关键字段,不要让模型先调用一次猜格式;
  • 用失败记录反向补描述,不要凭空堆例子。

5. Schema 的目标是让无效状态难以表达

在 OpenAI function tool 中,parameters 使用 JSON Schema,strict 可以约束函数调用;官方示例同时使用 requiredadditionalProperties: false。MCP 的稳定规范也为工具定义 inputSchema,并支持可选 outputSchema

本篇 Lab 的写工具 Schema 如下:

python
input_schema = {
    "type": "object",
    "properties": {
        "ticket_id": {
            "type": "string",
            "minLength": 5,
        },
        "note": {
            "type": "string",
            "minLength": 1,
        },
        "action_id": {
            "type": "string",
            "minLength": 6,
        },
    },
    "required": ["ticket_id", "note", "action_id"],
    "additionalProperties": False,
}

它至少阻止三类问题:

text
漏掉 note
传入空 note
偷偷带入未声明字段

如果一个状态可以用结构表达,就不要只写在说明里:

text
弱:status: string,描述里说只允许 open 或 closed
强:status: enum[open, closed]

弱:on: bool + off: bool
强:state: enum[on, off]

弱:amount + currency 都可选
强:两者都 required,或使用明确的联合结构

不过,Schema 也不能承担所有业务规则。ticket_id 是否存在、当前用户是否能访问该工单、金额是否超出额度,仍需要运行时查真实数据。

6. 权限、审批和预演是三个不同问题

这三个概念经常被压成一个 confirm=True,但它们回答的问题不同:

控制回答的问题典型证据
权限调用者是否有资格做这类事角色、scope、资源 ACL
审批这一次具体动作是否被确认call id、参数摘要、批准人、时间
预演如果执行,会改变什么diff、影响对象、预计副作用

一次用户点击“批准”,不应该把只读身份升级成写权限。反过来,拥有写权限也不表示每一笔高风险操作都无需确认。

本篇 Lab 的顺序是:

图 4:工具调用从输入验证开始,依次经过权限、审批、超时、幂等和业务执行,最后再验证输出图 4:工具调用从输入验证开始,依次经过权限、审批、超时、幂等和业务执行,最后再验证输出

text
1. 找到工具
2. 验证输入 Schema
3. 验证权限
4. 验证审批
5. 检查超时边界
6. 检查幂等回执或冲突
7. 执行业务处理器
8. 验证输出 Schema

为什么使用独立 preview 工具

常见设计是:

json
{
  "name": "record_ticket_followup",
  "arguments": {
    "ticket_id": "T-102",
    "note": "...",
    "dry_run": true
  }
}

它适合人类调用的 API,也方便共享实现。但对高风险 Agent,我更倾向:

text
preview_ticket_followup   -> 永远只读
record_ticket_followup    -> 永远写入,必须审批

好处是副作用出现在工具身份上,而不是一个可能被漏掉或传错的布尔字段里。代价是多一个工具定义。若你的工具目录很大、预演逻辑与执行必须严格一致,也可以保留 dry_run,但运行时仍要把它当成硬契约,而不是描述建议。

7. 幂等要同时校验 key 和请求指纹

超时以后,调用方往往不知道:

text
请求根本没到后端?
后端已经写入,但响应丢了?
写到一半失败?

这就是写工具需要幂等语义的原因。最小实现通常使用稳定的 action_id

python
if action_id in receipts:
    return receipts[action_id]

result = write_once(arguments)
receipts[action_id] = result
return result

但这仍有一个漏洞:如果第二次调用沿用相同 action_id,却改变了 ticket_idnote,直接回放旧结果会把冲突藏起来。

Lab 0.5.0 为参数生成稳定指纹:

python
fingerprint = json.dumps(
    arguments,
    sort_keys=True,
    separators=(",", ":"),
)

if action_id in receipts:
    old_fingerprint, old_output = receipts[action_id]
    if fingerprint != old_fingerprint:
        return idempotency_conflict(action_id)
    return replay(old_output)

所以有三种明确结果:

text
新 key + 新请求       -> 执行一次并保存回执
旧 key + 相同请求     -> 回放回执,不重复写
旧 key + 不同请求     -> idempotency_conflict

生产实现还应让回执落在与业务写入一致的持久化边界里。只存在进程内存中的幂等表无法跨重启保护;这会在下一篇 Durable Loop 继续处理。

8. 错误要告诉 Agent 下一步,而不是只说失败

工具错误不是日志文案。它会进入下一轮 Context,影响 Agent 是改参数、请求批准、重试、换工具还是停止。

Lab 使用统一结构:

json
{
  "code": "tool_timeout",
  "category": "dependency",
  "message": "Tool exceeded its 500 ms timeout.",
  "retryable": true,
  "details": {
    "timeout_ms": 500,
    "observed_ms": 900
  }
}

图 5:结构化错误按验证、策略、依赖、冲突和内部错误分类,并映射到修参数、审批、重试或停止图 5:结构化错误按验证、策略、依赖、冲突和内部错误分类,并映射到修参数、审批、重试或停止

一组够用的错误分类

类别示例 codeAgent 的候选动作默认自动重试
validationinvalid_arguments根据 violations 修参数
policypermission_denied停止或请求正确身份
policyapproval_required暂停并展示待审批动作
not_foundticket_not_found核对 ID 或重新查询
conflictidempotency_conflict对账,生成新动作或人工处理
dependencytool_timeout在满足条件时有限重试可能
internalinvalid_tool_output停止并报警

retryable: true 仍然不够

真正重试前至少检查:

text
错误是否被分类为瞬时?
工具是读取还是写入?
写入结果是否确定?
是否提供稳定幂等键?
重试次数、deadline 和成本预算是否还有余量?

特别是“写请求超时且结果未知”不能因为 retryable=true 就直接再写一次。正确做法通常是先查询回执或业务状态,再决定重试。

MCP 稳定规范还有一个值得借鉴的细节:工具自身产生的错误应通过工具结果并标记 isError 返回,让模型能够看到并自我修正;找不到工具、服务不支持调用等协议级异常,再使用协议错误。换句话说,业务失败与传输失败要分层。

9. 输出不是越完整越好

工具输出会成为下一轮模型输入,所以返回 500 条记录不只是网络浪费,也是 Context Architecture 问题。

本篇 Lab 的宽工具忽略 limit=3,返回 25 条完整工单;候选工具只返回:

json
{
  "items": [
    {"id": "T-101", "status": "open", "subject": "..."},
    {"id": "T-102", "status": "open", "subject": "..."},
    {"id": "T-103", "status": "waiting", "subject": "..."}
  ],
  "next_cursor": "3"
}

设计输出时可以逐项检查:

  • 是否有稳定 ID,便于下一次精确获取;
  • 是否返回摘要而不是完整对象;
  • 是否有 next_cursor,避免一次倾倒;
  • 是否标出截断、总数或剩余页;
  • 是否保留来源、时间和真实回执;
  • 错误是否与正常输出使用稳定结构;
  • 敏感字段是否在进入模型前脱敏;
  • 输出 Schema 是否也被运行时验证。

Anthropic 的文章把冗余调用和无效参数错误视为工具设计信号:重复查询可能意味着分页或 token 参数不合适,大量参数错误可能意味着描述和示例不清。工具输出不是一次设计完就结束,它应该和 Trace、Eval 一起迭代。

10. Direct、Programmatic 与 MCP 解决的不是同一层

工具工程容易把“工具怎么定义”和“工具通过什么通道执行”混在一起。可以用下面的表区分:

方式适合什么仍然需要什么
Direct function call每次结果都会影响模型下一步;写操作;审批;需要保留原生引用Schema、权限、审批、幂等、错误和输出预算
Programmatic Tool Calling有界的筛选、去重、聚合、并行读取,代码可在中间压缩结果明确输入输出、允许的 caller、资源和停止边界
MCP / Connector跨应用或跨服务标准化发现和调用工具信任判断、allowed tools、审批、数据边界和服务版本治理

OpenAI 当前的 Programmatic Tool Calling 允许受支持的 Responses 模型生成 JavaScript,在托管运行时内调用被允许的工具并汇总中间结果。官方部署建议也明确:当每个结果会改变下一步、动作需要批准,或最终回答必须保留引用与原生产物时,继续使用 direct call 更合适。

所以它不是“更先进的 function calling”,而是另一种编排路径:

text
大量只读查询 -> 代码过滤/连接/去重 -> 小结果回到模型

不应轻易把需要人工批准的写工具放进一个模型生成的批处理程序。

MCP 是传输和发现协议,不是安全证明

MCP 为工具提供 namedescriptioninputSchema、可选 outputSchema 和 annotations。规范同时提醒:来自不受信任服务器的 tool annotations 不能被客户端当作可信安全事实。

例如:

text
readOnlyHint: true
idempotentHint: true

是提示,不是你可以跳过审计和策略检查的证明。一个 MCP Server 仍然可能升级版本、改变行为或返回带注入内容的数据。客户端/Harness 需要独立控制允许的服务器、允许的工具、审批和日志。

版本说明

截至 2026-07-31,MCP 官方仓库已发布 2026-07-28-RC,但页面明确标为尚未最终定稿的 release candidate。本文关于 inputSchemaoutputSchema、structured content 和 annotations 信任边界的引用,按稳定版 2025-11-25 核对,不把 RC 行为写成已经普遍落地的事实。

11. 本篇实验:同一能力,两种工具表面

代码仍然放在同一个 Agent Reliability Lab 中,版本从 0.4.0 升到 0.5.0

text
agent-reliability-lab/
├─ agent_lab/
│  ├─ harness.py
│  ├─ tools.py
│  └─ tool_reporting.py
├─ datasets/
│  └─ tool-cases.jsonl
├─ reports/
│  ├─ tool-comparison.json
│  ├─ tool-comparison.md
│  ├─ tool-failures.md
│  └─ tool-runs.jsonl
├─ tests/
│  └─ test_tools.py
└─ run_lab.py

你也可以直接下载本站打包的 Tool Engineering Lab 0.5.0

实验控制变量是同一个内存工单后端,变化的是 Agent 面向的工具表面与运行时契约。

对照组:wide-tool-v1

text
一个 ticket_operation
自由 payload
无独立权限策略
无审批闸门
无幂等回执
通用 tool_failed
列表返回全部数据

候选组:typed-registry-v2

text
五个意图明确的工具
输入和输出 Schema
每工具权限与副作用
写操作审批
action_id + 参数指纹
结构化错误与 retryable
limit + next_cursor

十一个边界案例

case要验证的事情
read-ticket只读调用正常完成
preview-before-write预览不会产生真实写入
write-needs-approval未批准写操作被阻止
invalid-arguments缺少 note 时在 handler 前失败
permission-boundary只读身份无法写工单
duplicate-action相同 key 与参数只写一次
idempotency-conflict相同 key、不同参数返回冲突
transient-timeout超时返回可操作的重试信号
bounded-list只返回指定页并提供 cursor
ticket-not-found不存在的工单返回结构化错误
invalid-cursor非法分页游标不会让 handler 崩溃

为了保持零第三方依赖,Lab 只实现了这些案例所需的 JSON Schema 子集,用来展示边界顺序,而不是替代完整标准校验器。生产项目应使用平台 SDK 或成熟 JSON Schema 实现,并为实际使用的关键字增加兼容性测试。

12. 跟着运行 Lab 0.5.0

bash
git clone -b agent-engineering-series https://github.com/RalfNick/ai-agent-learn.git

cd ai-agent-learn/phase-7-agent-engineering/agent-reliability-lab

python run_lab.py tool-eval
python -m unittest discover -s tests -v

正常结果应包含:

text
version: 0.5.0
wide-tool-v1:       1 / 11 cases passed
typed-registry-v2:  11 / 11 cases passed
gate_passed: true
41 tests: OK

报告会写入:

text
reports/local/
├─ tool-comparison.json
├─ tool-comparison.md
├─ tool-failures.md
└─ tool-runs.jsonl

建议阅读顺序:

  1. 先看 tool-comparison.md,确认总结果和 Schema 成本;
  2. 再看 tool-failures.md,定位宽工具为什么失败;
  3. 最后看 tool-runs.jsonl,检查每次调用、错误、grader 和副作用数。

13. Tool Registry 真正做了什么

Tool Spec 同时保存两类信息:

python
@dataclass(frozen=True)
class ToolSpec:
    # Model-facing contract
    name: str
    description: str
    input_schema: dict
    output_schema: dict

    # Runtime contract
    required_permission: str
    effect: str = "read"
    approval: str = "never"
    idempotency_key: str | None = None
    retry_policy: str = "never"
    timeout_ms: int = 500

model_schema() 只导出模型需要看到的部分;权限和幂等规则留在受信任运行时。这一点很重要:不要为了让模型“知道一切”,把内部 ACL、密钥或策略实现全部塞进 Context。

调用路径集中在 Registry:

python
def invoke(call, actor_permissions, approved):
    spec = find_tool(call.name)
    validate(call.arguments, spec.input_schema)
    require_permission(spec, actor_permissions)
    require_approval(spec, approved)
    enforce_timeout(spec, call.arguments)
    replay_or_reject_idempotency_conflict(spec, call)

    result = handler(call.arguments)
    validate(result.output, spec.output_schema)
    save_receipt_if_needed(spec, call, result)
    return result

集中分派带来两个好处:

text
每个工具都无法绕过相同的策略顺序
每个失败都能进入统一报告和 Trace

生产系统不一定需要自研 Registry。OpenAI Agents SDK 的 function tools、timeouts、approval、tool guardrails,LangGraph 的 ToolNode,或其他框架都可以承载这些能力。Lab 的价值是让边界可见,便于你判断框架是否真的覆盖需求。

14. 怎样正确阅读 1/11 与 11/11

图 6:Lab 0.5.0 的契约结果与同时增加的模型 Schema 成本图 6:Lab 0.5.0 的契约结果与同时增加的模型 Schema 成本

本地结果是:

指标wide-tool-v1typed-registry-v2
case pass rate9.1%100%
unsafe side effects40
duplicate side effects20
actionable error rate0%100%
model-facing schema bytes2472505

这个结果能证明:

text
给定这些固定调用提案,typed-registry-v2 会执行 Schema、权限、
审批、幂等、超时和输出约束,并生成预期的结构化结果。

它不能证明:

text
某个模型看到五个工具后,选择准确率一定更高;
所有真实业务错误都已经覆盖;
内存回执足以支持生产重启;
某个 SDK 或 Provider 比另一个更可靠。

对照组里的错误预览调用是刻意构造的对抗样本,不是从某个 Provider 跑出来的统计数据。把这类本地契约测试写成“工具选择准确率提升到 100%”,会把两种完全不同的证据混为一谈。

15. 真正的模型工具 Eval 应该怎样补

如果要验证名称、描述、Schema 与工具数量是否改善模型表现,可以在本地门禁之外增加 provider adapter:

数据集至少覆盖四类任务

text
必须调用某个工具
不应该调用任何工具
相似工具二选一
需要连续多个工具并根据中间结果调整

每条任务记录

text
expected tool or abstain
required argument constraints
forbidden side effects
expected evidence
maximum calls / latency / tokens

重复运行并分开统计

text
tool selection accuracy
argument validity
task success
unnecessary calls
unsafe proposals
tool error recovery
input tokens from tool schemas
latency and cost

保留 held-out 集合

工具描述很容易针对已知失败样本过拟合。Anthropic 的实践建议使用贴近真实工作的任务,并用 held-out test set 检查改写后是否只是记住了训练样本。

一个推荐的证据顺序是:

text
单元测试       -> handler 和 Schema 正确
契约案例       -> 运行时能拦住风险
模型 Eval      -> 模型能正确选择和恢复
线上 Trace     -> 真实分布、成本和长尾失败

16. 工具数量变大以后怎么办

OpenAI 官方指南提醒,工具定义会进入模型输入,占用 Context 并计费;当前文档给出的软建议是回合开始时尽量保持较少的初始工具,并通过评估决定数量。工具面较大时,可以使用:

Namespace

text
ticket.get
ticket.list
ticket.record

billing.invoice.get
billing.refund.preview
billing.refund.submit

先让模型识别领域,再在领域内选择动作。

不常用工具只暴露高层 namespace,模型确认需要后再加载完整 Schema。OpenAI 当前的 tool search 支持 deferred function、namespace 和 hosted MCP surface;发布前应重新核对模型与 SDK 支持范围。

根据身份动态启用

没有写权限的用户根本不应看到写工具,既减少误调用,也降低工具目录大小。但“隐藏”不能替代运行时授权,因为请求可能被伪造或从旧状态恢复。

收敛近义工具

如果出现:

text
search_ticket
find_ticket
query_ticket
lookup_ticket

问题往往不是模型不够聪明,而是工具边界本身不可区分。先合并语义,再优化描述。

17. 从 Function Tool 映射到 MCP 与框架

本文概念OpenAI Function / Agents SDKAnthropic / ClaudeMCPLangGraph
名称与描述function name / descriptiontool name / descriptionname / descriptiontool metadata
输入契约JSON Schema、strictinput_schemainputSchemaPython schema / tool args
输出契约output schema 或应用校验tool result 结构outputSchema + structuredContentstate / tool message 校验
超时function tool timeout / Harness应用侧或 SDK 运行时Server / Client 实现ToolNode 外围策略
审批needs_approval、HITLpermission / hook / app flowClient approval policyinterrupt / human node
错误failure handler / tool guardrailtool result errorisError resulthandle_tool_errors
大工具面namespace / tool searchMCP 与工具设计优化tools/list / list changed动态 tool set

“Skills、MCP、Function Calling 是否相通”这个问题,可以更精确地回答:

text
Skills 主要沉淀工作方法、资料和脚本;
Tools 提供可调用的数据与动作;
MCP 标准化客户端如何发现和调用外部工具;
Harness 决定这些能力如何被组织、约束、记录和恢复。

它们可以协作,但不能互相替代。

18. 把现有 API 改成 Agent Tool 的六步练习

第 1 步:列出真实用户意图

不要先复制 API 路由。写出:

text
用户要读取什么?
用户要比较或预演什么?
用户要真正改变什么?
哪些动作总是连续发生?

产物:意图到后端能力的映射表。

第 2 步:标记副作用和权限

为每个候选工具填写:

text
read / write / destructive
required scope
resource-level ACL
approval policy

产物:运行时策略表,而不是描述段落。

第 3 步:收紧输入和输出 Schema

至少检查:

text
required
enum
min/max
additionalProperties
output IDs
pagination

产物:一组合法与非法样本。

第 4 步:设计错误与恢复动作

每个错误写清:

text
code
category
retryable
result known or unknown
next action

产物:错误决策表。

第 5 步:补幂等和预演

写工具至少回答:

text
稳定 action_id 从哪里来?
相同 key、不同参数怎么办?
重启后回执在哪里?
用户批准前怎样看到 diff?

产物:重复调用与冲突测试。

第 6 步:分两层评估

先跑确定性契约案例,再接真实模型做重复 Eval。不要用一层测试替另一层背书。

产物:契约报告 + 模型选择报告。

19. 接入生产前的 Tool Engineering 清单

可理解性

  • 工具名能区分读取、预演、写入和删除
  • 描述写明何时使用与何时不要使用
  • 参数有格式、边界和必要示例
  • 近义工具已经合并或放入 namespace

输入与输出

  • 输入 Schema 有 required、enum 和额外字段策略
  • 业务 ID、权限和资源状态在运行时重新验证
  • 输出有稳定 ID、摘要、来源和游标
  • 输出 Schema 也经过验证
  • 敏感字段在进入模型前脱敏

副作用

  • 工具明确标记 read / write / destructive
  • 权限和审批是两个独立检查
  • 高风险动作支持 preview 或 dry-run
  • 写入使用稳定幂等键和持久回执
  • 相同幂等键、不同参数返回冲突

错误与恢复

  • 错误有稳定 code、category 和 details
  • retryable 不会绕过副作用与预算判断
  • 超时能区分结果已知和结果未知
  • 协议错误与业务错误分层
  • 失败会进入 Trace 和 Eval 数据集

工具规模

  • 统计模型可见工具数和 Schema 输入成本
  • 不常用工具考虑延迟加载
  • 无权限工具在暴露层隐藏、运行时仍授权
  • 工具版本变化有回归集和灰度策略

结语:好工具让错误提案停在边界上

Tool Engineering 的目标,不是保证模型永远不会选错。只要调用者是非确定性 Agent,错选、漏参和错误恢复就不可能完全消失。

更现实的目标是:

text
让正确工具更容易被理解和选择;
让无效参数在业务执行前失败;
让越权和未审批写入无法落地;
让重复请求只产生一次副作用;
让错误携带足够的下一步信息;
让输出保持在可控 Context 预算内;
让每次改动都能由 Eval 和 Trace 验证。

如果只记住一句:

模型负责提出工具调用,Tool Registry 负责证明这次调用有资格、可执行、可恢复,而且返回了可信结果。

下一篇进入 Durable Loop。现在我们已经有 Harness 和有契约的工具,接下来要处理更难的现实问题:进程重启、网络抖动、结果未知、退避重试、取消、租约和断点恢复。

参考资料

OpenAI

Anthropic、MCP 与 LangGraph

本文代码与证据

继续阅读