跳至内容
18/30第 18 章,共 30 章

Tool Calling 与结构化输出:守得住的契约

24 次调用、0 个破损 JSON、2 个可用日期。再看同一端点配上更好描述后,以及 schema 无法修复的问题。

本页内容

给模型一个航班搜索工具,让它找一班从马德里到柏林的航班。返回的是这样:

TEXT
<tool_call>
{"name": "search_flights",
 "arguments": {"from": "Madrid", "to": "Berlin", "date": "3rd October 2026"}}
</tool_call>

JSON 是有效的。工具名是对的。每个必填字段都在。而这次调用毫无用处:没有航班 API 会在需要机场代码时接受 "Madrid",也不会在需要日期时接受 "3rd October 2026"。

这个差距——语法完美、语义不可用——正是本章要讨论的内容,而首先要明确的是:这不是一个 JSON 问题。在使用这个工具的 24 次请求中,模型生成了 24 次有效的工具调用,0 个破损 JSON。它一次都没有在大家最常调试的部分失败。

在讲机制之前,先说一句最能避免混淆的话:工具调用是请求,不是动作。

模型发出一条结构化消息,意思是:我想用这些参数调用 search_flights。然后它就停止了。你的代码收到这条消息,决定是否接受它,调用该调用的东西,再把结果作为另一条消息发回去。模型从未碰过你的数据库,从未发起 HTTP 请求,也从未拥有凭据。

第 30 章里关于 agent 安全的一切都来自这个分工,第 23 章里关于 agent 设计的一切也是如此:模型提出方案,你的代码做决定,而所有保证都存在于代码里。

所以,去掉术语外衣,一个工具就是两件事:

一个 schema。 一份描述函数的 JSON Schema:它的名称、用途,以及它接受哪些参数、这些参数的类型和约束。这会进入 prompt,也是模型唯一能看到的东西。

一个端点。 你代码中的一个函数,接收这些参数并返回某些东西。模型永远看不到它,不知道它用什么语言写,也分不清数据库查询和硬编码字符串。

工具定义会进入 prompt,并序列化成模型训练时见过的某种格式。它们在每一次调用中都会消耗 token——这个事实会在本章后面带着数字回来。

响应不再是散文,而是包含一条结构化请求,API 也会报告一个表示这一点的结束原因。这个原因很重要:你的代码正是靠它知道该运行工具,而不是把答案展示给用户。

这一步里没有模型。根据 schema 验证参数,判断这个调用方是否被允许这么做,然后执行。

结果会成为对话中的又一轮,并使用一个专门保留的角色。模型会像读取任何其他上下文一样读取它。

这就是第 23 章的循环,也是一个请求能变成十几次往返的原因。

这一切都不是涌现出来的。正如第 11 章所说明的,tool calling 是一种训练出来的行为:1 在后训练期间,模型见过成千上万段正是这种形状的对话。这就是为什么格式会因模型而异,为什么大小相近的模型可靠性会差这么多,也解释了为什么模型能调用一个它从未见过的工具——形状是训练出来的,具体工具来自你的 prompt。

下面是大多数人最开始会写出的工具。注意,它没有任何地方是错误的;只是太薄:

tools/badFlights.tsTS
{
  name: "search_flights",
  description: "Search for flights.",
  parameters: {
    type: "object",
    properties: {
      from: { type: "string", description: "Airport." },   
      to:   { type: "string", description: "Airport." },   
      date: { type: "string", description: "The date." },  
    },
    required: ["from", "to", "date"],
  },
}

24 次请求,6 组城市配对,交叉 4 种日期表达方式("the 3rd of next month"、"next Friday"、"15 December"、"tomorrow"),使用贪心解码以便结果可复现:

调用了工具破损 JSONISO 日期机场为 IATA全部正确
上面的 schema24/2402/244/241/24

先看前两列,再看后三列。模型每次都调用了正确工具,每次都生成了格式良好的 JSON。失败完全出在值上,而这些值不可用:"Madrid" 而不是 MAD,"3rd October 2026" 而不是 2026-10-03。

这一点值得反复强调,因为它决定了出问题时你该去哪里找原因。本能反应是加一个带重试的 JSON 解析器,或者更强硬地要求模型给出有效 JSON。但这两者都没有处理这里实际发生的事。

新文章直达你的收件箱

AI 新闻、指南和产品更新——有值得你花时间阅读的内容时,我们会发一封简短邮件。

同一个端点。背后同一份代码。同一个模型,同样的 prompts,同样的解码方式。唯一改变的是 schema 里的文本:

tools/goodFlights.tsTS
{
  name: "search_flights",
  description: "Search scheduled flights between two airports on a given day.",
  parameters: {
    type: "object",
    properties: {
      from: {
        type: "string",
        description: "Departure airport as a three-letter IATA code, e.g. MAD for Madrid. Never a city name.",   
        pattern: "^[A-Z]{3}$",
      },
      to: { /* same */ },
      date: {
        type: "string",
        description: "Departure date as an ISO 8601 calendar date, YYYY-MM-DD. Resolve relative dates against today before calling.",   
        format: "date",
        pattern: "^\\d{4}-\\d{2}-\\d{2}$",
      },
    },
    required: ["from", "to", "date"],
  },
}
日期 FORMAT日期 VALUE机场 FORMAT机场 VALUE
薄 schema2/241/244/244/24
有描述的 schema24/2412/2416/248/24

日期格式从 24 次里的 2 次,变成了 24 次里的 24 次。只改一段文本,不碰代码,也没有重试逻辑,就做到了完美。如果你只从本章带走一个运行习惯,那就是:当一个工具被错误调用时,修复几乎总是在描述里,而且这是系统里成本最低的修复。

现在看第二列,那才是更重要的一半。

schema 约束形状。它无法提供知识。

链接到此部分:schema 约束形状。它无法提供知识。

日期 24 次里 24 次都是 ISO 格式。但它是正确那一天的次数只有 12/24。

也就是说,现在有一半调用携带了格式完美却日期错误的日期。描述告诉模型要产出什么形状,模型也毫无瑕疵地产出了——但把 "next Friday" 变成 2026-09-11 需要知道今天的日期并做日历运算,而再多描述也无法提供这一点。机场也是同样的故事:格式从 4 次提高到 16 次,但值只从 4 次提高到 8 次,因为写出 MAD 需要知道马德里的机场是 MAD。

这个区别是本章的承重概念:

schema 是一个关于形式的契约。它能让模型输出可解析、有类型且一致。它不能让输出变得真实,而所有在好 schema 之后仍然存在的失败模式,都是知识失败,不是格式失败。

这两者需要不同修复,把它们混在一起会浪费数周时间。格式失败要在描述里修,或用下面的约束解码修。知识失败要靠把知识放进 prompt 来修——把当前日期放进系统消息,让模型先调用一个作为第二工具的机场查询,或者在集合足够小时把 enum 放进 schema。注意这三者的共同点:它们都把问题从模型的记忆中移到模型的输入里,而这正是第 24 章的全部内容。

结构化输出,以及“约束解码”到底是什么

链接到此部分:结构化输出,以及“约束解码”到底是什么

上面的所有内容仍然依赖模型选择产出正确形状。还有一种更强的保证可用,也是第 17 章里最有价值的回报。

回想生成是如何工作的:在每一步,模型都会为词表中的每个 token 生成一个 logit,然后采样器选择一个。约束解码会在中间插入一步。给定一个语法——由你的 JSON Schema 推导而来——它会计算哪些 token 可以合法地接在后面,把所有其他 token 的 logit 设为负无穷,然后让采样器从剩下的 token 中选择。

如果 schema 说下一个东西必须是 {,那么每一个不是 { 的 token 概率都是零。不是“不太可能”:是零。模型无法发出无效 JSON,因为无效 token 在采样之前就已经从分布中移除了。

这就是“结构化输出”、“JSON 模式”和“引导生成”的底层原理,也解释了它们的两个属性。对于语法能表达的任何东西——类型、必填字段、enum、嵌套——保证都是完全的,因为它是机械强制执行的,而不是礼貌请求来的。它也不说明内容:语法可以强制 "date" 成为匹配日期模式的字符串,却不能强制它是正确那一天。这和上一节遇到的是同一堵墙,只是从另一侧抵达。

两个实践提示。它不是免费的:每一步都必须计算掩码,复杂语法会带来可测量的延迟。它也会改变模型正在做的事——当模型被引导离开自己偏好的 token 时,可能在产出完美结构的同时产出更差内容;所以对简单形状来说,“好好请求并验证”仍然是合理默认,而当形状复杂或消费方要求严格时,约束解码才值得付出成本。

第 14 章测过一次超时后重试,导致为了一个答案计费两次生成。有了工具,同样的失败会更糟,因为工具可以真的做事。

如果你的代码调用 charge_card,超时,然后重试,你就有两笔扣费。模型完全不知道这些发生过;它只看到一个工具结果。修复方式和任何分布式系统一样,而且这不是模型的问题:通过给调用一个 key,让操作具备幂等性,这样第二次执行会识别第一次并返回它的结果,而不是再次做同样的工作。

由此得出的设计规则值得直说。在你的工具目录里把读和写分开。 读操作可以自由重试、并行运行、缓存。写操作不行,而且应该带有 key、权限检查,以及——对于任何用户会想在发生前知情的事——一个 approval 步骤,把人放在请求和动作之间。这个 approval 步骤不是礼貌:它是 prompt injection 和真实后果之间少数几道防线之一——而且第 30 章测到,它是其中最弱的一道。

坊间说法是加载很多工具会让模型选错。与其重复这个说法,不如测量一下:同样的 24 次请求,航班工具加上一组不断增多的其他工具——其中包括 3 个刻意容易混淆的工具(火车时刻表、渡轮航线、公交路线)。

加载的工具数prompt token选择了 search_flightsISO 日期
135324/2424/24
573024/2424/24
101,19321/2421/24
202,11924/2424/24

选择没有退化。加载 20 个工具,其中 3 个有可信的混淆可能,一个 5 亿参数模型仍然 24 次里 24 次选对。10 个工具时的下滑来自 3 次调用命名了不同工具,但这个现象并没有延续到 20 个工具。

这是一个负结果,也应该按负结果报告:在这个任务、这些工具上,“工具太多”不是问题。 真正单调增长、并且增长到 6 倍的是 prompt:从 353 个 token 到 2,119 个 token,在对话中的每个请求上都要付费,永远如此,无论是否使用了任何工具。

所以,这个坊间说法的诚实版本谈的是成本和上下文,而不是准确率。20 个工具是每条消息上的永久税,而第 16 章已经展示过永久前缀会如何在 40 轮对话中推高账单。当人们报告很多工具损害质量时,其机制通常是工具定义挤掉了真正重要的上下文——这是披着第 18 章外衣的第 24 章问题。真正彼此接近重复的工具也是个真实问题,而它们的修复方式不是减少工具,而是更好的描述和命名空间:按系统加前缀(crm.search_customer、billing.search_customer),这样两个团队合并来的两个目录不会冲突,模型也有东西可区分。

三类工具,以及开启下一部分的那一种

链接到此部分:三类工具,以及开启下一部分的那一种

按工具会对世界做什么来分类会很有帮助,因为每一类所需的工程方式都不同。

数据工具负责读取:搜索、获取、查询。可重试、可并行、可缓存。它们的失败方式是返回不了有用内容,而主要风险在于把不受信任的文本带入上下文——这正是第 30 章的整个攻击面。

动作工具负责写入:发送、创建、扣费、删除。没有 key 就不能重试,不能安全并行,也是 approval 流程存在的原因。

编排工具会调用其他模型。它的实现是另一个 agent,拥有自己的 prompt、自己的工具和自己的循环——而对调用它的模型来说,它看起来和另外两类完全一样,因为一个 schema 加一个端点就是模型所能看到的全部。

第三类不是猎奇。它是第 25 章中“agent-as-a-tool”那一半背后的机制——另一种拓扑 handoff 会把对话交出去,并且再也拿不回来——而它之所以可行,正是因为本章的接口足够窄,窄到整个 agent 都能藏在后面。

现在你有了一个能请求事物的模型,也有了一个能让请求可解析的契约。你还没有的是:除了 prompt 里能装下的内容之外,它还有什么可请求的对象。

生产中最常见的工具,远远超过其他类型,是对一批模型训练时从未见过的文本进行搜索:你的文档、工单、合同。听起来像是一个已经解决的问题——embedding 它、找到最近邻、粘贴进去——但那些尚未解决的部分,才决定答案是否可信:文本在 embedding 之前如何切分,相似度阈值低到什么程度才意味着我不知道,以及 citation 如何附着到一个 claim 上,让读者可以检查它。

第 19 章讲的是检索,而在那一章里,错误答案不再只是一个有趣现象,而会开始成为责任。


本章的测量来自 Qwen/Qwen2.5-0.5B-Instruct,使用贪心解码,覆盖 24 个生成请求:6 组城市配对交叉 4 种日期表达方式,并使用模型自己的聊天模板来提供工具定义。它们可以精确复现,而且用的是小模型:请把 format/value 的分裂理解为机制演示,而不是当前模型能力的基准。前沿模型更常能正确解析 "next Friday"——但仍然无法被 schema 强制做到这一点,这才是可泛化的部分。

上文使用的 JSON Schema 词汇(type、properties、required、pattern、format、enum)由你的提供商文档所指向的 JSON Schema 草案规定;有用的子集很小,各提供商之间也相同,而确实存在的差异——哪些关键词会由约束解码强制执行,而哪些只是传给模型——值得去读提供商的结构化输出指南,而不是凭空假设。

至于约束解码这项技术,guidance 风格的库和 outlines 项目都以一种能直接映射到第 17 章采样器的方式,记录了从语法到 logit 掩码的构造。至于往返本身,最清晰的说明不是教程,而是协议:第 26 章会逐行阅读它。

  1. Ouyang, L. et al. Training language models to follow instructions with human feedback. arXiv:2203.02155 (2022)。这篇论文让后训练配方成为标准;tool calling 的形状正是在那里从示范中学到的,就像答案的形状一样。 ↩

新文章直达你的收件箱

AI 新闻、指南和产品更新——有值得你花时间阅读的内容时,我们会发一封简短邮件。

更喜欢用消息接收?同样的内容,也在这里:WhatsApp 社群 (在新标签页打开)Telegram 频道 (在新标签页打开)

作者

David Vicente Campos

NeuraLIA Labs 创始人、MyRealFood 联合创始人

我是莱昂大学毕业的计算机工程师。我共同创立了 MyRealFood,并在那里作为 CTO 打造了一款数百万人用来吃得更健康的应用;我还创立了 NeuraLIA Labs,在这里我打造 AI 产品。我在本站写下一路走来所必须理解的内容,就像我希望当初有人向我讲解的那样。

了解作者更多信息

由 NeuraLIA Labs 发布。

课程目录

Abstract desk scene with blank papers, geometric forms, and a glowing network suggesting AI-assisted mathematics.
openai10 分钟阅读

OpenAI 的数学声明暴露了 AI 的验证瓶颈

有关 OpenAI 模型解决重大数学问题的报道指向一个更深层的问题:生成证明越来越便宜,而验证、归属和发表仍以人类速度推进。

Abstract software decision engine with branching paths, probability nodes, and glowing gates.
jev10 分钟阅读

Jev AI 模型为决策而生,而非写作

TypeSafe AI 的 Jev 正受到关注,因为它把软件智能视为一个概率问题:选择正确分支,附上置信度,并避免在代码只需要决策时还花钱让 LLM 写文本。

让你的 AI 今天就开始工作

所有模型和工具都在一处。免费开始,无需信用卡。