工具接口:为什么 Agent 需要结构化 I/O


文档摘要

工具接口:为什么 Agent 需要结构化 I/O 本节摘要:语言模型只会产出 token,而程序会采取行动。这两者之间的鸿沟,就是「工具接口(Tool Interface)」——一份契约,让模型请求一个动作,再让 host 去执行它。2026 年的每一条技术栈——OpenAI、Anthropic、Gemini 的函数调用,MCP 的 ,A2A 的 task parts——都是同一个「四步循环」的不同编码。本节给这个循环命名,并展示跑通它所需的最小机制:描述(describe)→ 决定(decide)→ 执行(execute)→ 观察(observe),谁拥有每一步,纯工具与有副作用工具的安全分野,以及为什么「让模型直接吐 JSON」是个坏主意。

工具接口:为什么 Agent 需要结构化 I/O

本节摘要:语言模型只会产出 token,而程序会采取行动。这两者之间的鸿沟,就是「工具接口(Tool Interface)」——一份契约,让模型请求一个动作,再让 host 去执行它。2026 年的每一条技术栈——OpenAI、Anthropic、Gemini 的函数调用,MCP 的 tools/call,A2A 的 task parts——都是同一个「四步循环」的不同编码。本节给这个循环命名,并展示跑通它所需的最小机制:描述(describe)→ 决定(decide)→ 执行(execute)→ 观察(observe),谁拥有每一步,纯工具与有副作用工具的安全分野,以及为什么「让模型直接吐 JSON」是个坏主意。

学习目标

阅读完本节,你应当能够:

  1. 解释为什么一个只能生成文本的 LLM,无法靠自己对真实世界采取行动。
  2. 画出四步工具调用循环(describe → decide → execute → observe),并说清每一步归谁所有。
  3. 把一个工具描述写成三部分:名字、JSON Schema 输入、确定性执行函数。
  4. 区分纯工具(Pure)有副作用工具(Consequential),并说明这一分野为何关乎安全。

一、问题与直觉

一个 LLM 输出的是「下一个 token 的概率分布」——这就是它全部的输出面。你问一个聊天模型「班加罗尔现在天气如何」,它能写出一串看起来合理的句子,但它拨不进天气 API。这句话可能恰好猜对,也可能滞后三天。

弥合这道鸿沟,正是工具接口的目的。host 程序(你的 Agent 运行时、Claude Desktop、ChatGPT、Cursor 或一段自定义脚本)向模型广播一份可调用工具的清单。模型在判定某个动作是必要的时候,产出一个结构化的 payload,点出工具名和参数。host 解析这个 payload,真正地运行工具,再把结果喂回去。循环往复,直到模型判定不再需要调用为止。

这份契约的第一版在 2023 年 6 月以 OpenAI 的 functions 参数问世;Anthropic 随后在 Claude 2.1 里跟进 tool_use 块;几个月后 Gemini 加入 functionDeclarations。如今每家厂商都暴露出相同的形状:输入是 JSON Schema 类型化的工具列表,输出是 JSON payload 形态的工具调用。模型上下文协议(2024 年 11 月)把它泛化,使一份工具注册表能服务所有模型;A2A(2026 年 4 月,v1.0)为 Agent 之间的委托叠加了同样的原语。

四步循环是这一切之下不变的东西。本章其余各节,都只是对它的展开。

二、从零实现

第一步:描述(describe)

host 用三个字段声明每个工具:

  • 名字(Name):稳定、机器可读的标识符。get_weather,而不是「weather thing」。
  • 描述(Description):一段自然语言的简报。「当用户询问某个城市当前状况时使用;不要用于历史数据。」
  • 输入 schema(Input schema):一个 JSON Schema(draft 2020-12)对象,描述工具的参数。

模型会收到这份清单。现代厂商用各自专有的模板把这些声明序列化进系统提示,所以作为调用方,你只需面对结构化形式。

第二步:决定(decide)

给定用户消息和可用工具,模型在三选一:

  1. 直接用文本作答,无工具调用。
  2. 调用一个或多个工具,产出结构化调用对象。在 parallel_tool_calls: true(OpenAI 与 Gemini 默认开,Anthropic 需显式 opt-in)下,模型能在一轮里发出多个调用。
  3. 拒绝(refuse)。strict 模式的结构化输出可以产出一个类型化的 refusal 块,而非一次调用。

一个工具调用 payload 有三个稳定字段:调用 id、工具 name、一个 JSON arguments 对象。id 的存在,是为了让 host 能把之后的结果与这一次特定调用关联起来——当并行调用乱序返回时,这点至关重要。

第三步:执行(execute)

host 收到调用,按声明的 schema 校验参数,再运行执行函数(executor)。参数非法,意味着模型幻觉了一个字段或用错了类型——这是弱模型上极常见的失败模式。生产级 host 遇到非法参数,有三条路可走:① fail fast,把错误抛回给模型;② 用受限解析器修复 JSON;③ 把校验错误塞进提示里重试模型。

执行函数本身是普通代码:Python、TypeScript、shell 命令、数据库查询。它产出一个结果——通常是字符串,但可以是任意 JSON 值或结构化内容块(MCP 里的文本、图像或资源引用)。结果必须可序列化。

# 最小工具注册表:每个工具持有四样东西 TOOL_REGISTRY = { "get_weather": { "description": "当用户询问某个城市当前天气时使用。不要用于历史数据。", "schema": { "type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"], }, "executor": lambda city: f"{city}: 28°C, 多云", # 确定性执行函数 "consequential": False, # 纯工具 vs 有副作用工具 }, # ... 其余工具同理 }

第四步:观察(observe)

host 把工具结果追加进对话(作为一条带匹配 idtool 角色消息),再次调用模型。此时模型在上下文里有了工具输出,可以给出最终答案或请求更多调用。循环持续,直到模型不再产出调用,或 host 触达迭代次数的安全上限。

信任的分野:纯工具 vs 有副作用工具

工具按安全性分成两类:

  • 纯工具(Pure):只读、确定性、无副作用。get_weathersearch_docsget_current_time。可以放心地试探性调用。
  • 有副作用工具(Consequential):改变状态、花钱、触碰用户数据。send_emaildelete_fileexecute_trade。必须加门控。

Meta 在 2026 年提出的 Agent 安全「二选一规则(Rule of Two)」:单轮里至多同时出现三者之二——不可信输入、敏感数据、有副作用的动作。工具接口正是你落实这条规则的地方:拒绝调用、要求用户确认、或提升权限范围。完整的安全章节见第 15 节,Agent 级权限策略见第 14 章 09 节。

为什么不直接「让模型吐 JSON」?

「要求模型用 JSON 回复」是函数调用诞生前的老套路。它在前沿模型上仍有约 5%~15% 的失败率,在更小的模型上失败率高得多。失败形态包括缺括号、尾逗号、幻觉字段、类型错误——你随后还需要一轮 JSON 修复、一次重试,或一个受限解码器(constrained decoder)。

原生函数调用更好,原因有三:

  1. 厂商在精确的调用形状上做了端到端训练,因此 strict 模式下合法 JSON 率能爬到 98%~99%。
  2. 调用 payload 待在自己的协议槽里,不在自由文本内——工具调用绝不会泄漏进用户能看到的回复。
  3. 厂商用受限解码强制 schema 合规(OpenAI 的 strict 模式、Anthropic 的 tool_use、Gemini 的 responseSchema),输出保证通过校验。

第 02 节会并排走查三家厂商 API;第 04 节深入结构化输出。

熔断器(Circuit Breaker)

循环在模型停止产出调用,或 host 触达最大轮次时终止。生产级 host 把它设在 5~20 轮之间;超过这个范围,你几乎肯定陷入了一个模型走不出的死循环。Claude Code 默认 20;OpenAI Assistants 默认 10;Cursor 的 Agent 模式 25。

替代方案——无界循环——每半年就会以「Agent 一夜烧掉 400 美元 API 费用」的事后复盘形式出现。绝不要在没有上限的情况下上线。

循环在不同上下文里归谁

上下文 谁描述工具 谁决定 谁执行
单轮函数调用(OpenAI/Anthropic/Gemini) 应用开发者 LLM 应用开发者
MCP MCP 服务端 LLM,经 MCP 客户端 MCP 服务端
A2A Agent Card 发布者 发起方 Agent 被调方 Agent
Web 浏览器(函数调用 Agent) 浏览器扩展 / WebMCP LLM 浏览器运行时

处处都是同样的四步。列名在变,结构不变。

三、框架对比

维度 让模型吐 JSON(前函数调用) 原生函数调用 MCP
合法 JSON 率 85%~95% strict 模式 98%~99% 同厂商原生调用
调用与回复分离 否(混在文本里) 是(独立协议槽)
schema 强制 受限解码 受限解码
跨 host 复用 N×M 重写 每家不同 一份注册表服务所有模型

💡 选型心法:能用原生函数调用就别让模型裸吐 JSON;能在 MCP 之上建模就别为每家 host 写定制胶水。本章其余各节都在为这两条背书。

四、可复用产物

本节产出 outputs/skill-tool-interface-reviewer.md——一个工具接口审查技能。给定一份工具定义草稿(名字 + 描述 + schema + 执行函数大纲),它按「循环适配性」逐项审计:名字是否机器稳定、描述是否是完整的用法简报、schema 是否正确使用 JSON Schema 2020-12、纯工具与有副作用工具的分类是否显式。

原课程 code/main.py 用纯标准库跑通四步循环(无 LLM):一个假的「decider」函数靠模式匹配模拟模型,执行器、schema 校验器、observe 步的脚手架都是真的。跑它能看到完整的请求/响应编排与可打印的中间状态,之后任何一节都能把这个假 decider 换成真实厂商。

五、练习

  1. 加第四个工具:给 code/main.py 加一个 get_stock_price(ticker)。描述写成「当用户按 ticker 询问当前股价时使用;不要用于历史价格或市场综述。」跑脚手架,确认假 decider 会把提到 ticker 的查询路由到新工具。

  2. 打破 schema 校验器:传入一个 arguments 缺了必填字段的调用,确认 host 在执行前就拒绝。再传入一个带多余未知字段的调用,自己决定:host 该拒绝还是忽略?用安全论据为自己的选择辩护。

  3. 分类与门控:把脚手架里每个工具标为纯或有副作用。给需要门控的注册条目加一个 consequential: true 标志,改循环,使它在选中这类工具时打印「将请求用户确认」一行。这就是每个生产级 host 都需要的确认门。

  4. 手画循环:在纸上画出四步循环,把上面的厂商列表填进去(选你最熟的客户端:Claude Desktop、Cursor、ChatGPT 或自定义栈),并与第 06 节里 MCP 的变体交叉对照。

  5. 读厂商文档:从上到下读一遍 OpenAI 的函数调用指南,找出一个在请求里、却不在本节四步循环里的字段。解释它加了什么,以及为什么它「方便」而非「必需」。

本节要点回顾

  1. token 与动作的鸿沟:LLM 只产出 token,无法对真实世界动手;工具接口是弥合这道鸿沟的契约。
  2. 四步循环不变量:describe(host 广播)→ decide(模型选择)→ execute(host 运行)→ observe(结果回灌),所有协议都是它的变体。
  3. 工具的三段式声明:名字(机器稳定)+ 描述(完整用法简报)+ 输入 schema(JSON Schema 2020-12)。
  4. 调用 payload 三字段:id(关联结果)、namearguments;并行调用乱序返回时 id 关键。
  5. 纯工具 vs 有副作用工具:前者只读可试探,后者必须加门控——这是落实安全「Rule of Two」的地方。
  6. 别裸吐 JSON:原生函数调用在 strict 模式合法率 98%~99%,调用与回复分离,且由受限解码强制 schema 合规。
  7. 熔断器必需:迭代上限 5~20 轮,无界循环等于一夜烧光预算。

下一节,我们把三家厂商(OpenAI、Anthropic、Gemini)的函数调用 API 并排走查,看清同一四步循环在不同协议里的编码差异。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U