一个 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;另一部分是运行时掌握的权限、审批、副作用、超时、幂等、错误和输出约束。前者帮助模型提出候选调用,后者决定调用能否安全落地。
一分钟概览
如果只保存这篇文章的结论,可以记住十二点:
- 工具是确定性系统与非确定性 Agent 之间的契约。 API 对人类开发者易用,不代表对模型也易用。
- 模型可见契约与运行时契约要分开。 名称、描述、Schema 帮助选择;权限、审批和幂等必须由代码执行。
- 先按用户意图设计工具,再考虑复用后端 API。 不要把几十个底层端点原样暴露给模型。
- 宽工具和窄工具没有绝对答案。 宽工具减少 Schema 体积,窄工具通常让意图、副作用和权限更清楚。
- Schema 要让无效状态尽量无法表达。 必填字段、枚举、边界和
additionalProperties: false都有价值。 - 预览和写入最好有清楚分界。 对高风险动作,独立
preview_*往往比一个容易被忽略的dry_run布尔值更直观。 - 审批不是授权。 用户批准某次动作,不代表调用者拥有该资源的写权限;两者都要检查。
- 幂等不是“见到相同 key 就返回旧结果”。 同一 key 配不同参数应返回冲突,否则错误请求会被悄悄掩盖。
retryable不是自动重试开关。 还要判断副作用、结果是否已知、是否有幂等键,以及预算是否允许。- 工具输出也是 Context。 返回稳定 ID、摘要、游标和证据,不要默认倾倒整个对象或完整日志。
- 工具越多,选择成本和 Context 成本越高。 Namespace、deferred loading 和 tool search 是规模化手段,不是第一天就必需的架构。
- 本地契约测试不能替代模型 Eval。 11/11 只能证明运行时守住了边界;模型是否更会选工具,要用真实任务、重复运行和 held-out 集合验证。
图 1:Tool Engineering 把模型提案送入类型化 Tool Registry,再依次经过 Schema、权限、审批、幂等、执行和输出验证
1. 工具不是给模型看的 API 文档
传统 API 的调用者通常是确定性程序。只要函数签名和协议稳定,调用方会按代码路径提供参数:
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 工具:
后端 API:面向系统复用、字段完整、兼容多个调用方
Agent Tool:面向任务意图、参数受限、输出有预算、失败可恢复
有时三个 API 应该被组合成一个工具,因为它们总是连续执行;有时一个通用 API 应拆成三个工具,因为读取、预览和写入拥有完全不同的风险。
2. 一个可运营工具至少有两层契约
我把工具契约拆成两层。
图 2:模型可见契约负责可理解性,运行时契约负责真实控制,两层共同包住业务处理器
第一层:模型可见契约
模型通常会看到:
name
description
input schema
有些平台还支持 output schema、namespace 或调用方式
它回答的是:
这个工具做什么?
什么时候应该用?
什么时候不应该用?
必须提供哪些参数?
返回什么字段?
第二层:运行时契约
这层不应只靠模型自觉遵守:
required_permission
effect: read / write / destructive
approval policy
timeout and concurrency
idempotency semantics
retry policy
input and output validation
audit and redaction
它回答的是:
当前调用者真的有权执行吗?
这次写入是否已批准?
重复调用会发生什么?
超时以后结果是失败、未知还是可安全重试?
工具是否返回了符合契约的结果?
哪些信息允许进入下一轮 Context?
最危险的实现,是把第二层偷偷塞进描述:
“这个工具会修改数据,请只在获得批准后调用。”
这句话有助于模型做判断,但没有形成安全边界。真正的边界应该是:
if spec.required_permission not in actor.permissions:
return permission_denied()
if spec.approval == "required" and not approved:
return approval_required()
模型描述负责减少错误提案,运行时负责阻止错误提案变成真实事故。
3. 宽工具还是窄工具
假设工单系统已经有一个通用接口:
{
"name": "ticket_operation",
"arguments": {
"operation": "get | preview | record | list | slow",
"payload": {}
}
}
它的优点很直接:Schema 小,适配快,后端容易复用。问题也很明显:真正意图藏在 operation 和自由形态的 payload 里;读取与写入共享一个权限入口;描述要同时解释很多分支;输出也很容易变成“什么都可能返回”。
另一种设计是拆成:
get_ticket
preview_ticket_followup
record_ticket_followup
list_tickets
slow_ticket_lookup
图 3:同一组工单能力既可通过一个宽工具暴露,也可按读取、预演、写入、分页和慢依赖拆成窄工具
这不是“工具越小越好”。更实用的判断表是:
| 情况 | 更适合合并 | 更适合拆分 |
|---|---|---|
| 动作总是按固定顺序执行 | 是,把编排下沉到代码 | 否 |
| 参数、返回值和权限高度相似 | 是 | 否 |
| 读取、写入、删除风险不同 | 否 | 是 |
| 用户会单独要求预览 | 否 | 是 |
| 一个枚举分支已经很多 | 通常否 | 是 |
| 拆分后产生大量近义工具 | 谨慎 | 可能应重新分组或加 namespace |
| 工具目录已经很大 | 合并相关能力或延迟加载 | 只拆真正需要独立选择的意图 |
本篇 Lab 选择拆分,不是为了证明拆分一定更优,而是为了让权限、审批、幂等和输出边界可以独立表达。实验也如实记录了代价:模型可见的工具目录从 247 字节 增至 2505 字节。
这约 10 倍的差距不是 token 账单,只是稳定的 UTF-8 字节代理;但它提醒我们:更清楚的契约会消耗 Context,工具设计必须同时看可靠性和暴露成本。
4. 名称和描述是在做路由,不是在写文案
OpenAI 的 Function calling 指南 建议使用清晰、详细的函数名、参数说明和调用指令,并明确什么时候使用、什么时候不使用。Anthropic 的工具工程文章同样强调 namespace、返回有意义的 Context、控制 token 和通过 Eval 改进描述。
一个弱描述通常只有能力名:
record_ticket_followup
Record a follow-up.
更可操作的描述应该补齐边界:
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.
这里包含四个路由信号:
动作:append one follow-up
副作用:writes data
前置条件:approved + ticket:write
反例:do not use for previews
但要再次强调:这四句话只帮助模型做选择。ticket:write 和审批仍然由 Harness 检查。
命名检查
- 用动词表达动作:
get_、list_、preview_、record_; - 让相似工具的区别出现在名字里,而不是藏在说明最后;
- 不要混用
manage、handle、process这类边界不清的词; - 通过 namespace 表达领域,例如
ticket.get与billing.refund; - 名称重构要配合 Eval,因为它会直接改变模型路由表面。
描述检查
- 第一行先说清真正结果;
- 写明必要前置条件与输入格式;
- 对常见误用写清“不要在何时使用”;
- 说明输出的关键字段,不要让模型先调用一次猜格式;
- 用失败记录反向补描述,不要凭空堆例子。
5. Schema 的目标是让无效状态难以表达
在 OpenAI function tool 中,parameters 使用 JSON Schema,strict 可以约束函数调用;官方示例同时使用 required 和 additionalProperties: false。MCP 的稳定规范也为工具定义 inputSchema,并支持可选 outputSchema。
本篇 Lab 的写工具 Schema 如下:
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,
}
它至少阻止三类问题:
漏掉 note
传入空 note
偷偷带入未声明字段
如果一个状态可以用结构表达,就不要只写在说明里:
弱: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:工具调用从输入验证开始,依次经过权限、审批、超时、幂等和业务执行,最后再验证输出
1. 找到工具
2. 验证输入 Schema
3. 验证权限
4. 验证审批
5. 检查超时边界
6. 检查幂等回执或冲突
7. 执行业务处理器
8. 验证输出 Schema
为什么使用独立 preview 工具
常见设计是:
{
"name": "record_ticket_followup",
"arguments": {
"ticket_id": "T-102",
"note": "...",
"dry_run": true
}
}
它适合人类调用的 API,也方便共享实现。但对高风险 Agent,我更倾向:
preview_ticket_followup -> 永远只读
record_ticket_followup -> 永远写入,必须审批
好处是副作用出现在工具身份上,而不是一个可能被漏掉或传错的布尔字段里。代价是多一个工具定义。若你的工具目录很大、预演逻辑与执行必须严格一致,也可以保留 dry_run,但运行时仍要把它当成硬契约,而不是描述建议。
7. 幂等要同时校验 key 和请求指纹
超时以后,调用方往往不知道:
请求根本没到后端?
后端已经写入,但响应丢了?
写到一半失败?
这就是写工具需要幂等语义的原因。最小实现通常使用稳定的 action_id:
if action_id in receipts:
return receipts[action_id]
result = write_once(arguments)
receipts[action_id] = result
return result
但这仍有一个漏洞:如果第二次调用沿用相同 action_id,却改变了 ticket_id 或 note,直接回放旧结果会把冲突藏起来。
Lab 0.5.0 为参数生成稳定指纹:
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)
所以有三种明确结果:
新 key + 新请求 -> 执行一次并保存回执
旧 key + 相同请求 -> 回放回执,不重复写
旧 key + 不同请求 -> idempotency_conflict
生产实现还应让回执落在与业务写入一致的持久化边界里。只存在进程内存中的幂等表无法跨重启保护;这会在下一篇 Durable Loop 继续处理。
8. 错误要告诉 Agent 下一步,而不是只说失败
工具错误不是日志文案。它会进入下一轮 Context,影响 Agent 是改参数、请求批准、重试、换工具还是停止。
Lab 使用统一结构:
{
"code": "tool_timeout",
"category": "dependency",
"message": "Tool exceeded its 500 ms timeout.",
"retryable": true,
"details": {
"timeout_ms": 500,
"observed_ms": 900
}
}
图 5:结构化错误按验证、策略、依赖、冲突和内部错误分类,并映射到修参数、审批、重试或停止
一组够用的错误分类
| 类别 | 示例 code | Agent 的候选动作 | 默认自动重试 |
|---|---|---|---|
| validation | invalid_arguments | 根据 violations 修参数 | 否 |
| policy | permission_denied | 停止或请求正确身份 | 否 |
| policy | approval_required | 暂停并展示待审批动作 | 否 |
| not_found | ticket_not_found | 核对 ID 或重新查询 | 否 |
| conflict | idempotency_conflict | 对账,生成新动作或人工处理 | 否 |
| dependency | tool_timeout | 在满足条件时有限重试 | 可能 |
| internal | invalid_tool_output | 停止并报警 | 否 |
retryable: true 仍然不够
真正重试前至少检查:
错误是否被分类为瞬时?
工具是读取还是写入?
写入结果是否确定?
是否提供稳定幂等键?
重试次数、deadline 和成本预算是否还有余量?
特别是“写请求超时且结果未知”不能因为 retryable=true 就直接再写一次。正确做法通常是先查询回执或业务状态,再决定重试。
MCP 稳定规范还有一个值得借鉴的细节:工具自身产生的错误应通过工具结果并标记 isError 返回,让模型能够看到并自我修正;找不到工具、服务不支持调用等协议级异常,再使用协议错误。换句话说,业务失败与传输失败要分层。
9. 输出不是越完整越好
工具输出会成为下一轮模型输入,所以返回 500 条记录不只是网络浪费,也是 Context Architecture 问题。
本篇 Lab 的宽工具忽略 limit=3,返回 25 条完整工单;候选工具只返回:
{
"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”,而是另一种编排路径:
大量只读查询 -> 代码过滤/连接/去重 -> 小结果回到模型
不应轻易把需要人工批准的写工具放进一个模型生成的批处理程序。
MCP 是传输和发现协议,不是安全证明
MCP 为工具提供 name、description、inputSchema、可选 outputSchema 和 annotations。规范同时提醒:来自不受信任服务器的 tool annotations 不能被客户端当作可信安全事实。
例如:
readOnlyHint: true
idempotentHint: true
是提示,不是你可以跳过审计和策略检查的证明。一个 MCP Server 仍然可能升级版本、改变行为或返回带注入内容的数据。客户端/Harness 需要独立控制允许的服务器、允许的工具、审批和日志。
版本说明
截至 2026-07-31,MCP 官方仓库已发布
2026-07-28-RC,但页面明确标为尚未最终定稿的 release candidate。本文关于inputSchema、outputSchema、structured content 和 annotations 信任边界的引用,按稳定版2025-11-25核对,不把 RC 行为写成已经普遍落地的事实。
11. 本篇实验:同一能力,两种工具表面
代码仍然放在同一个 Agent Reliability Lab 中,版本从 0.4.0 升到 0.5.0。
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
一个 ticket_operation
自由 payload
无独立权限策略
无审批闸门
无幂等回执
通用 tool_failed
列表返回全部数据
候选组:typed-registry-v2
五个意图明确的工具
输入和输出 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
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
正常结果应包含:
version: 0.5.0
wide-tool-v1: 1 / 11 cases passed
typed-registry-v2: 11 / 11 cases passed
gate_passed: true
41 tests: OK
报告会写入:
reports/local/
├─ tool-comparison.json
├─ tool-comparison.md
├─ tool-failures.md
└─ tool-runs.jsonl
建议阅读顺序:
- 先看
tool-comparison.md,确认总结果和 Schema 成本; - 再看
tool-failures.md,定位宽工具为什么失败; - 最后看
tool-runs.jsonl,检查每次调用、错误、grader 和副作用数。
13. Tool Registry 真正做了什么
Tool Spec 同时保存两类信息:
@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:
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
集中分派带来两个好处:
每个工具都无法绕过相同的策略顺序
每个失败都能进入统一报告和 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 成本
本地结果是:
| 指标 | wide-tool-v1 | typed-registry-v2 |
|---|---|---|
| case pass rate | 9.1% | 100% |
| unsafe side effects | 4 | 0 |
| duplicate side effects | 2 | 0 |
| actionable error rate | 0% | 100% |
| model-facing schema bytes | 247 | 2505 |
这个结果能证明:
给定这些固定调用提案,typed-registry-v2 会执行 Schema、权限、
审批、幂等、超时和输出约束,并生成预期的结构化结果。
它不能证明:
某个模型看到五个工具后,选择准确率一定更高;
所有真实业务错误都已经覆盖;
内存回执足以支持生产重启;
某个 SDK 或 Provider 比另一个更可靠。
对照组里的错误预览调用是刻意构造的对抗样本,不是从某个 Provider 跑出来的统计数据。把这类本地契约测试写成“工具选择准确率提升到 100%”,会把两种完全不同的证据混为一谈。
15. 真正的模型工具 Eval 应该怎样补
如果要验证名称、描述、Schema 与工具数量是否改善模型表现,可以在本地门禁之外增加 provider adapter:
数据集至少覆盖四类任务
必须调用某个工具
不应该调用任何工具
相似工具二选一
需要连续多个工具并根据中间结果调整
每条任务记录
expected tool or abstain
required argument constraints
forbidden side effects
expected evidence
maximum calls / latency / tokens
重复运行并分开统计
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 检查改写后是否只是记住了训练样本。
一个推荐的证据顺序是:
单元测试 -> handler 和 Schema 正确
契约案例 -> 运行时能拦住风险
模型 Eval -> 模型能正确选择和恢复
线上 Trace -> 真实分布、成本和长尾失败
16. 工具数量变大以后怎么办
OpenAI 官方指南提醒,工具定义会进入模型输入,占用 Context 并计费;当前文档给出的软建议是回合开始时尽量保持较少的初始工具,并通过评估决定数量。工具面较大时,可以使用:
Namespace
ticket.get
ticket.list
ticket.record
billing.invoice.get
billing.refund.preview
billing.refund.submit
先让模型识别领域,再在领域内选择动作。
Deferred loading / tool search
不常用工具只暴露高层 namespace,模型确认需要后再加载完整 Schema。OpenAI 当前的 tool search 支持 deferred function、namespace 和 hosted MCP surface;发布前应重新核对模型与 SDK 支持范围。
根据身份动态启用
没有写权限的用户根本不应看到写工具,既减少误调用,也降低工具目录大小。但“隐藏”不能替代运行时授权,因为请求可能被伪造或从旧状态恢复。
收敛近义工具
如果出现:
search_ticket
find_ticket
query_ticket
lookup_ticket
问题往往不是模型不够聪明,而是工具边界本身不可区分。先合并语义,再优化描述。
17. 从 Function Tool 映射到 MCP 与框架
| 本文概念 | OpenAI Function / Agents SDK | Anthropic / Claude | MCP | LangGraph |
|---|---|---|---|---|
| 名称与描述 | function name / description | tool name / description | name / description | tool metadata |
| 输入契约 | JSON Schema、strict | input_schema | inputSchema | Python schema / tool args |
| 输出契约 | output schema 或应用校验 | tool result 结构 | outputSchema + structuredContent | state / tool message 校验 |
| 超时 | function tool timeout / Harness | 应用侧或 SDK 运行时 | Server / Client 实现 | ToolNode 外围策略 |
| 审批 | needs_approval、HITL | permission / hook / app flow | Client approval policy | interrupt / human node |
| 错误 | failure handler / tool guardrail | tool result error | isError result | handle_tool_errors |
| 大工具面 | namespace / tool search | MCP 与工具设计优化 | tools/list / list changed | 动态 tool set |
“Skills、MCP、Function Calling 是否相通”这个问题,可以更精确地回答:
Skills 主要沉淀工作方法、资料和脚本;
Tools 提供可调用的数据与动作;
MCP 标准化客户端如何发现和调用外部工具;
Harness 决定这些能力如何被组织、约束、记录和恢复。
它们可以协作,但不能互相替代。
18. 把现有 API 改成 Agent Tool 的六步练习
第 1 步:列出真实用户意图
不要先复制 API 路由。写出:
用户要读取什么?
用户要比较或预演什么?
用户要真正改变什么?
哪些动作总是连续发生?
产物:意图到后端能力的映射表。
第 2 步:标记副作用和权限
为每个候选工具填写:
read / write / destructive
required scope
resource-level ACL
approval policy
产物:运行时策略表,而不是描述段落。
第 3 步:收紧输入和输出 Schema
至少检查:
required
enum
min/max
additionalProperties
output IDs
pagination
产物:一组合法与非法样本。
第 4 步:设计错误与恢复动作
每个错误写清:
code
category
retryable
result known or unknown
next action
产物:错误决策表。
第 5 步:补幂等和预演
写工具至少回答:
稳定 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,错选、漏参和错误恢复就不可能完全消失。
更现实的目标是:
让正确工具更容易被理解和选择;
让无效参数在业务执行前失败;
让越权和未审批写入无法落地;
让重复请求只产生一次副作用;
让错误携带足够的下一步信息;
让输出保持在可控 Context 预算内;
让每次改动都能由 Eval 和 Trace 验证。
如果只记住一句:
模型负责提出工具调用,Tool Registry 负责证明这次调用有资格、可执行、可恢复,而且返回了可信结果。
下一篇进入 Durable Loop。现在我们已经有 Harness 和有契约的工具,接下来要处理更难的现实问题:进程重启、网络抖动、结果未知、退避重试、取消、租约和断点恢复。
参考资料
OpenAI
- Function calling
- Tool search
- Programmatic Tool Calling
- MCP and Connectors
- OpenAI Agents SDK:Tools
- OpenAI Agents SDK:Guardrails
- OpenAI Agents SDK:Human-in-the-loop
Anthropic、MCP 与 LangGraph
- Anthropic:Writing effective tools for agents
- Model Context Protocol:Tools,stable 2025-11-25
- MCP official releases
- LangChain / LangGraph:Tools and ToolNode