跳到主要内容

工具怎么接上去 — 一份说明书加一张调用单

这一章讲三件事: 你交给模型的到底是什么(不是代码); 它交回来的到底是什么(不是答案);以及一句话点三个工具时,编号为什么必须原样带回去。

它在全书链条里的位置: 第 03 章那一圈里,「点工具」是靠模型照格式写文字、 外面拿正则去拆。这一章讲的是同一件事的另一种做法:让接口本身支持它。 后面第 05、06、09、10 章的工具接入,底下都是这一套。

为什么这一章又换了输入。 前面那个鲜花定价问题一次只点一个工具, 看不出编号有什么用。所以这一章改用书里唯一一次 一句话点了三个工具的真实调用——三城库存查询。

1. 模型不会执行你的函数,它只写一张调用单

这一节的结论,书里用「再强调一遍」四个字领出来,可见作者知道这里最容易绕。

原话是:在把工具传递给模型之后,模型不会真的调用工具中的代码, 相反,它会生成一段 JSON 格式的字符串;开发者要先解析,然后拿它去调自己代码里的函数1

换成一句话:模型交回来的是一张调用单,不是结果。

你交给模型的: 一份说明书(这个函数叫什么、干什么、要哪几个参数)


模型交回来的: 一张调用单(要点哪一个、每个参数填什么值)


你的代码去做的: 照单子真的把函数跑一遍,拿到结果


再交给模型的: 那份结果,连同前面所有消息


模型这次交回来的: 一段人能读的话

图说:全程模型碰过两次,而它一次都没有执行过代码。
中间那一步(真的跑)永远是你的程序在做。

书里还给这件事起了两个名字,你在文档里都会撞见2: Functions 指的是那份接口定义(说明书), 而Function Calling 指的是「模型生成一张符合这份说明书的调用单」这个过程。

2. 说明文字就是它挑不挑你的依据

这一节讲一个很小、但会决定成败的细节。

学生在书里问了一个好问题:它是怎么知道什么时候该点哪个工具的? 作者的回答分两半。内置的那几件工具有一套内部逻辑; 而你自己定制的那些,你给出的说明文字特别重要—— 说明文字就是它用于判断是否应该调用这个工具的依据3

书举的例子朴素得刚好:一个叫 get_weather 的函数,说明写着「查我所在位置的天气」。 作者说,只要你交代一句「你的任务是根据今天的气温来安排鲜花的存储」, 它当然会自主地联想到这件工具4

请把这句话反过来读一遍:说明写歪了,它就挑错;说明写重了,它就在两个之间犹豫。 这一条在第 10 章会真的出事——那一章要同时挂两个长得很像的资料库, 全靠两句说明文字把它们分开。

3. 交给模型的不是代码,是一份说明书

这一节讲书里最费口舌纠正的一个误会。

书专门排了一段师生对话来演这个误会。学生照着函数的实现代码往界面里一贴, 作者当场喊停:非也,非也——这里要填的不是实现代码, 而是对函数及其参数的 JSON 格式的描述5

这份描述有个名字叫元数据——关于这个函数本身的信息:它叫什么、干什么、要哪几个参数; 书的原话是「这个说明只是函数的元数据,描述了如何使用这个函数; 具体的实现代码需要在主程序中编写」6

书里那个「鼓励生成器」的例子把两边摆得很清楚。左边是真正的实现,右边是交给模型的说明书:

左边(你的代码,模型看不到): 右边(交给模型的说明书,模型只看到这个):
──────────────────────────── ─────────────────────────────────────
def get_encouragement(name,mood): {
messages = { "name": "get_encouragement",
"happy": "继续保持…", "description": "根据用户的心情提供鼓励信息",
"sad": "记住,即使…", "parameters": {
... "type": "object",
} "properties": {
return f"亲爱的{name},{message}" "mood": {"type":"string",
"description":"用户当前的心情,例如:开心,难过,压力大,疲倦"},
"name": {"type":"string",
"description":"用户的名字,用来个性化鼓励信息"}
},
"required": ["mood"]
}
}

图说:右边一行可执行代码都没有。它全部的作用是让模型知道
「有这么个东西、什么时候该点它、点的时候要填哪几个格子」。

右边那种写法本身也有名字,叫 JSON Schema——一份说明 JSON 数据该长什么样的说明书: 每个格子叫什么、是什么类型、哪些必填。上面那份里 required 只列了 mood, 意思是心情必填、名字可以不给7

往后指一句(第 13 章第 4 节兑现): 这份「说明书」的写法, 在这本书里是某一家厂商的私有约定——每接一个工具,你都得为这一家写一遍。 书出版之后,这件事被做成了一份跨厂商的共同规矩;第 13 章第 4 节讲它是什么、解决了什么。

4. 主走查:一句话点三个工具

这一节是本章的主走查。下面每一个数字和字串,全部来自书里那次真实调用。

输入: 一条用户消息——「北京、上海和深圳的鲜花库存是多少?」 手上一件工具: get_flower_inventory,说明写着「获取指定城市的鲜花库存」, 只要一个参数 city,说明写着「城市名称,如北京、上海或深圳」8。 **这件工具内部是写死的:**北京返回「玫瑰: 100, 郁金香: 150」, 上海返回「百合: 80, 康乃馨: 120」,深圳返回「向日葵: 200, 玉兰: 90」。

第 1 步 · 第一次问模型。 把那条用户消息、连同那份说明书一起发过去。 回来的东西里,那个「为什么停下来」的字段写着 finish_reason='tool_calls'9—— 对照第 02 章那张表:这不是答案,这是「我要点工具」。

第 2 步 · 看那张调用单。 内容那一格是空的(content=None), 真正的东西在另一格里,一口气三张单子10:

[1] id = 'call_DhKVYSzAsqJ2DKkYLhmlhNYH' name = get_flower_inventory arguments = {"city": "北京"}
[2] id = 'call_EW6HrY9A0WUV40KsOifY7cfY' name = get_flower_inventory arguments = {"city": "上海"}
[3] id = 'call_g9yh7ebEzIU8vc1Cx0OkGYK8' name = get_flower_inventory arguments = {"city": "深圳"}

图说:三张单子指向的是同一个函数,只是参数不同。
三个编号互不相同 —— 下一步全靠它们对号入座。

这一次的账单:输入 95、输出 67、合计 16211给个参照:第 02 章那次普通对话是 89 进 254 出。 输入这边差不多 (95 比 89 多的那几个,就是那份说明书的开销); 输出这边只有 67,不到那次的三分之一——因为它这次只写了三行调用单,没写正文。

第 3 步 · 跑工具。 你的代码把三张单子逐一取出来:读出函数名、 把参数那段 JSON 解析成真值、调用真正的函数,拿到三段库存文字12

第 4 步 · 回填。 把三段结果作为三条新消息追加到消息数组末尾。 每条新消息必须带三样东西——下一节整节讲这一步。

第 5 步 · 第二次问模型。 把加长后的整个消息数组重新发一遍。 这一次它不点工具了,finish_reason 变回 stop,内容那一格里是一段人话13:

北京的鲜花库存为:玫瑰: 100, 郁金香: 150
上海的鲜花库存为:百合: 80, 康乃馨: 120
深圳的鲜花库存为:向日葵: 200, 玉兰: 90

这一次的账单:输入 235、输出 90、合计 32514输入从 95 涨到 235,涨了一倍半——涨的那 140 个 token, 就是第 2 步那三张单子加第 4 步那三条结果。这就是第 02 章那笔账在这里的具体样子。

5. 回填:必须带上编号和「工具」这个角色

这一节讲那三条新消息的格式,漏一样都跑不通。

书说得很直白:要响应这些函数调用请求,需要向对话中添加三条新消息, 其中每条消息包含一个函数调用的结果,并通过 tool_call_id 引用调用单里的编号15

每条新消息长这样:

这一格填什么为什么必须有
role固定写 tool第 02 章那三种角色之外的第四种,专门用来放工具结果
tool_call_id第 2 步那三个编号之一一次点了三个,靠它认领谁是谁的结果
name函数名告诉模型这是哪个工具回来的
content那段库存文字结果本身

为什么编号是硬要求? 请看第 4 节那张图:三张单子指向的是同一个函数、同一个名字, 唯一能把它们分开的就是那三个编号。 你要是不带编号回去,模型无从知道 「玫瑰: 100」说的是哪座城市。

书里还顺手记了一个小现象:打印出来的消息数组里,中文变成了一串 北京 这样的编码, 读起来很不舒服。作者的判断是这是内部做的编码转换,不影响使用16这条留着,是因为你第一次跑起来一定会被它吓一跳。

6. 第二次问模型,才有人话

这一节讲一个很容易被跳过、但省不掉的步骤。

第 4 步之后,你手里已经有三段库存文字了。为什么不能直接拿去展示?

书用一段师生对话回答了这个问题。学生自己想通的: 把这个包含库存查询结果的消息列表再次丢给模型,让它先整合、再生成人人都可以读懂的最终答案17

两个理由,一个比一个实在:

第一,你手里那三段是三份原始返回,格式是给机器看的 (比如 {"city": "北京", "inventory": "玫瑰: 100, 郁金香: 150"})。 用户要的是第 4 节那段整整齐齐的中文。

第二,更要紧的:你事先不知道模型拿到结果之后还想不想再点工具。 第二次问,它可能回 stop(像这次),也可能又回 tool_calls第 01 章那条「转到它不再派活为止」,在这里就是这么落地的。

所以一圈的固定开销是:两次模型调用 + N 次工具执行,N 由模型说了算。

7. 那三个调用是顺序跑的,能不能并行书里没答

这一节讲书里的一个空白,并给出今天的答案。

回看第 3 步。书里那段代码是一个普通的 for 循环:取一张单子、跑一次函数、 追加一条结果,再取下一张18三次库存查询是一个接一个跑完的。

为什么这值得单说? 因为这三次查询彼此没有依赖——北京的结果不影响上海的查法。 它们本来完全可以同时跑。 假设一次查询要 200 毫秒,顺序跑要 600 毫秒, 同时跑仍然是 200 毫秒左右;一次响应里的单子越多,这个差距越大。

书对这件事一个字都没说。它甚至在小结里把「提升并行调用多个工具的能力」 列成了一条冲着厂商去的指望19——也就是说,作者当时认为这还是个待解决的问题。

同时跑这件事有两个说法要分清。并发是指同一段时间里有好几件事都在推进; 而并行是指同一时刻真的有好几件事在跑。下面这些框架里说的都是前者(靠等待时切换)。

补充(不在书里,依据我们的前沿框架书架): 这个问题在今天已经不是问题了,而且三家给出的答案还各有分工。 依据: shelf=ai-frontier-reference/vercel-ai-sdk#02-generate-text-loop.md @2174f202c21f86a44124a11baea8baa29f5dd0e5 事实=同一步里的多个工具调用被 Promise.all 一起跑掉(generate-text.ts:1490), 并且文档明写:工具之间若有依赖,得靠模型分多步调用,框架不替你安排同一步内部的先后。20

补充(不在书里,依据我们的前沿框架书架): 另一家把「不能并发的工具」做成了一个显式开关。 依据: shelf=ai-frontier-reference/pydantic-ai#03-output-and-end-strategy.md @606b0e581272eb9f06ca989d88be5828047eed9c 事实=同一段里的工具默认并发跑;给某个工具标上 sequential=True, 它就变成一道栅栏——前面的先跑完、它单独跑、后面的等它跑完再开始 (_tool_execution.py:281);还有一个运行级开关能一刀切把每个工具都变成栅栏。21

这两条合起来给出的经验是:默认并发是对的,但你得有办法把有副作用的那一个单独拎出来。 书里那个查库存的函数只读不写,顺序跑还是同时跑都无所谓; 换成「下单」「扣库存」这种,就必须有第二家那个开关。

8. 那个 JSON 开关的四条注意

这一节把书里散在两处的四条注意收齐,它们全是会安静出事的那种。

书说,只要用到工具接入,那个 JSON 开关就会默认启用, 以确保参数和返回值都是能被正确解析的有效结构22。四条注意如下:

第一,上下文里必须真的出现「JSON」这四个字母,否则接口直接报错。 这条第 02 章第 5 节已经讲过,这里只是再强调:它是硬性检查,不是建议。

第二,不写会怎样? 书给的后果比报错更吓人: 模型可能生成无限多的空白字符,请求一直跑到用光输出上限23

第三,达到输出上限时返回的结构可能不完整。 所以解析之前要先看那个结束原因字段; 它如果是 length,你手里那段 JSON 多半是半截的。

第四,也是最要紧的一条:它保证输出是有效且无错误的 JSON 对象, 但并不保证输出结果能匹配任何特定的架构或格式24

第四条要划重点。 说明书里写着 city 必填、类型是字符串, 这只是给模型的提示,不是保证。它完全可能填一个空串、填一个数字、 或者填一个说明书里根本没有的字段名。你的代码必须自己校验。

判断(我们的,不是书里的): 这四条里,第四条才是真正的地雷。 前三条会当场报错或者当场卡住,你一跑就知道; 而第四条不报错——它给你一份格式合法、内容不对的东西,你的程序照收不误。 如果错,会错在: 如果某一天厂商把「严格照说明书」做成默认行为, 这条判断就只适用于这本书写作的那个时间点,不适用于之后的接口。 第 13 章第 4 节会讲这条路后来走到了哪一步。

9. 可带走的

  1. 模型永远不执行你的函数。 它只写一张调用单:要点哪个、参数填什么;真的去跑的是你的代码;
  2. 一圈的固定开销是两次模型调用 + N 次工具执行,N 由模型说了算;
  3. 交给模型的是说明书不是代码。 说明书里有三样:名字、说明、参数表;
  4. 说明文字就是它挑不挑你的唯一依据——写歪了它就挑错,写重了它就犹豫;
  5. 参数表用的是 JSON Schema 这种现成写法: 每个格子叫什么、什么类型、哪些必填;
  6. 一次响应里可以有多张调用单,每张各带一个编号;回填时必须原样带回去——它们指向同一个函数时,编号是唯一的区分;
  7. 回填的那几条消息用第四种角色 tool,而且要带函数名和结果本身;
  8. 第二次问模型才有人话,而且这一次它可能又点工具——这就是那一圈;
  9. 书里三个工具是顺序跑的,能不能并发它没答;今天的框架默认并发,并且给了「这一个必须单独跑」的开关;
  10. 那个 JSON 开关只保证合法,不保证是你要的形状——参数必须自己校验,这一条不报错、最容易出事。

10. 原文地图

主题原书章原文位置
模型不执行代码、只生成字符串5.1 OpenAI中的Functionstext/36-ch05-01-5-1-openai-functions.txt:77(搜「大模型不会真的调用工具中的代码」)
Functions 与 Function Calling 两个名字5.1 OpenAI中的Functionstext/36-ch05-01-5-1-openai-functions.txt:87(搜「Function Calling是GPT-3.5 Turbo和GPT-4等模型的新功能」) · text/36-ch05-01-5-1-openai-functions.txt:107(搜「有时我们看到的Functions指的是要调用的函数的接口定义部分」)
说明文字是判断依据5.1 OpenAI中的Functionstext/36-ch05-01-5-1-openai-functions.txt:35(搜「Description就是Assistants用于判断」) · text/36-ch05-01-5-1-openai-functions.txt:43(搜「根据今天的气温来安排鲜花的存储」)
不是代码是元数据5.1 OpenAI中的Functions 与 5.2 在Playground中定义Functiontext/36-ch05-01-5-1-openai-functions.txt:51(搜「不是代码段」) · text/36-ch05-01-5-1-openai-functions.txt:53(搜「元数据」) · text/37-ch05-02-5-2-playground-function.txt:42(搜「非也,非也」)
鼓励生成器的实现与说明书5.2 在Playground中定义Functiontext/37-ch05-02-5-2-playground-function.txt:10(搜「def get_encouragement」) · text/37-ch05-02-5-2-playground-function.txt:44(搜「JSON Schema 正确的示例」)
主走查:输入、工具、三张单子5.4 通过ChatCompletion API来实现Tool Callstext/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:66(搜「北京、上海和深圳的鲜花库存是多少」) · text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:38(搜「获取指定城市的鲜花库存」) · text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:86(搜「call_DhKVYSzAsqJ2DKkYLhmlhNYH」)
两次调用的账单5.4 通过ChatCompletion API来实现Tool Callstext/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:129(搜「completion_tokens=67」) · text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:131(搜「prompt_tokens=95」) · text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:267(搜「completion_tokens=90」) · text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:269(搜「prompt_tokens=235」)
回填的三条消息与编号5.4 通过ChatCompletion API来实现Tool Callstext/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:168(搜「tool_call_id」) · text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:183(搜「tool_call.id」)
第二次调用与最终回答5.4 通过ChatCompletion API来实现Tool Callstext/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:221(搜「生成人人都可以读懂的最终答案」) · text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:253(搜「玫瑰:100,郁金香:150」)
顺序 for 循环、UTF 编码5.4 通过ChatCompletion API来实现Tool Callstext/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:166(搜「循环执行tool_calls」) · text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:219(搜「UTF编码」)
对并行的期望5.5 小结text/40-ch05-05-5-5.txt:23(搜「提升Agent并行调用多个工具的能力」)
JSON 开关的四条注意5.4 通过ChatCompletion API来实现Tool Callstext/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:139(搜「就会默认启用之前提到的“JSON模式”」) · text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:154(搜「API将反馈出错信息」) · text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:156(搜「需要检查返回的 finish_reason」) · text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:158(搜「并不保证输出结果能匹配任何特定的架构」)

Footnotes

  1. 出处:「5.1 OpenAI中的Functions」第 77 段(text/36-ch05-01-5-1-openai-functions.txt:77,搜「大模型不会真的调用工具中的代码」)。原文最后还补了一句很坦白的话:「对于初学者,这一点特别『绕』」。

  2. 出处:「5.1 OpenAI中的Functions」第 87 段(text/36-ch05-01-5-1-openai-functions.txt:87,搜「Function Calling是GPT-3.5 Turbo和GPT-4等模型的新功能」)与第 107 段(text/36-ch05-01-5-1-openai-functions.txt:107,搜「有时我们看到的Functions指的是要调用的函数的接口定义部分」)。后一段是学生自己总结的,作者的评价是「说得真好,就是这么回事」。

  3. 出处:「5.1 OpenAI中的Functions」第 35 段(text/36-ch05-01-5-1-openai-functions.txt:35,搜「Description就是Assistants用于判断」)。原文把内置工具和自定义工具分开说:前者有厂商自己的一套逻辑,后者全靠你写的那句说明。

  4. 出处:「5.1 OpenAI中的Functions」第 43 段(text/36-ch05-01-5-1-openai-functions.txt:43,搜「根据今天的气温来安排鲜花的存储」)。这一段是个反问句,学生点头认可——书没有真的跑这个例子,所以这一条属于作者的推断而非实测。

  5. 出处:「5.2 在Playground中定义Function」第 42 段(text/37-ch05-02-5-2-playground-function.txt:42,搜「非也,非也」)。学生贴错的那一版在同节第 40 段有截图说明(text/37-ch05-02-5-2-playground-function.txt:40,搜「小雪在Function中添加了具体实现代码」)——书特意把这个错误的中间态印出来,可见作者遇到过很多人栽在这里。

  6. 出处:「5.1 OpenAI中的Functions」第 53 段(text/36-ch05-01-5-1-openai-functions.txt:53,搜「元数据」)与第 51 段(text/36-ch05-01-5-1-openai-functions.txt:51,搜「不是代码段」)。原文用的词是「对函数接口的 JSON 格式的描述」。

  7. 出处:「5.2 在Playground中定义Function」第 44 段(text/37-ch05-02-5-2-playground-function.txt:44,搜「JSON Schema 正确的示例」),实现代码在同节第 10 段起(text/37-ch05-02-5-2-playground-function.txt:10,搜「def get_encouragement」)。本节那张对照图两边都是书里的原文,只是把它们并排摆在了一起。

  8. 出处:「5.4 通过ChatCompletion API来实现Tool Calls」第 66 段(text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:66,搜「北京、上海和深圳的鲜花库存是多少」)与第 38 段(text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:38,搜「获取指定城市的鲜花库存」)。函数内部三座城市的返回值写死在第 24 段起(text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:24,搜「玫瑰: 100, 郁金香: 150」);作者自己说这个实现「较为粗糙」,真实应用里应该连数据库。

  9. 出处:「5.4 通过ChatCompletion API来实现Tool Calls」第 101 段(text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:101,搜「表示对话结束的原因是需要执行工具调用」)。原始返回对象在同节第 84 段(text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:84,搜「finish_reason='tool_calls'」)。

  10. 出处:「5.4 通过ChatCompletion API来实现Tool Calls」第 86 段起(text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:86,搜「call_DhKVYSzAsqJ2DKkYLhmlhNYH」)。书里对这三张单子的说明在第 115 段(text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:115,搜「分别针对“北京”“上海”和“深圳”」),并补了一句很重要的话:这三次调用也可以分别指向不同的函数

  11. 出处:「5.4 通过ChatCompletion API来实现Tool Calls」第 129 段(text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:129,搜「completion_tokens=67」);同节第 131 段(text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:131,搜「prompt_tokens=95」)。和第 02 章那次调用的对比是我们做的,书里没有把两次放在一起比过。

  12. 出处:「5.4 通过ChatCompletion API来实现Tool Calls」第 166 段(text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:166,搜「循环执行tool_calls」)。书里那段代码从每张单子上取三样:函数名、参数、编号——和第 06 章那套托管接口取的东西一模一样。

  13. 出处:「5.4 通过ChatCompletion API来实现Tool Calls」第 253 段(text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:253,搜「玫瑰:100,郁金香:150」)。原始返回对象在第 237 段(text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:237,搜「chatcmpl-8pp0cTdOj5aLn1Ny3t1ldRyXyoE9w」),那里的结束原因字段已经变回 stop

  14. 出处:「5.4 通过ChatCompletion API来实现Tool Calls」第 267 段(text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:267,搜「completion_tokens=90」);同节第 269 段(text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:269,搜「prompt_tokens=235」)。「涨了一倍半」是我们算的:235 ÷ 95 ≈ 2.5,多出来的 140 个 token 就是三张单子加三条结果。

  15. 出处:「5.4 通过ChatCompletion API来实现Tool Calls」第 168 段(text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:168,搜「tool_call_id」)与第 183 段(text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:183,搜「tool_call.id」)。那张表里四个格子的名字全部来自书里那段代码。

  16. 出处:「5.4 通过ChatCompletion API来实现Tool Calls」第 219 段(text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:219,搜「UTF编码」)。作者的原话带着不确定:「应该是……内部进行编码转换后的结果」「但是,应该有办法可以解决」——这属于书里说「不知道」的地方,照实记下来。

  17. 出处:「5.4 通过ChatCompletion API来实现Tool Calls」第 221 段(text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:221,搜「生成人人都可以读懂的最终答案」)。这句是学生说的,作者只回了两个字「聪明」。

  18. 出处:「5.4 通过ChatCompletion API来实现Tool Calls」第 166 段(text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:166,搜「循环执行tool_calls」)。「200 毫秒」那个数是我们为了说明差距编的,不是书里的数,也不是真实测量值。

  19. 出处:「5.5 小结」第 23 段(text/40-ch05-05-5-5.txt:23,搜「提升Agent并行调用多个工具的能力」)。作者把它和另一条(让程序能根据人的评价调整选工具的能力)并列,写成希望厂商去做的两件事。

  20. 补充(不在书里,依据我们的前沿框架书架):同一步里的多个工具调用被一次性并发跑掉。依据: shelf=ai-frontier-reference/vercel-ai-sdk#02-generate-text-loop.md @2174f202c21f86a44124a11baea8baa29f5dd0e5 事实=执行工具那一步内部用 Promise.all 把该步的多个调用一起跑(generate-text.ts:1490),文档同时写明:工具之间若有依赖,得靠模型分多步调用,框架不替你安排同一步内部的先后。

  21. 补充(不在书里,依据我们的前沿框架书架):有副作用的工具可以被显式标成「必须单独跑」。依据: shelf=ai-frontier-reference/pydantic-ai#03-output-and-end-strategy.md @606b0e581272eb9f06ca989d88be5828047eed9c 事实=同一段内的工具默认并发执行;标了 sequential=True 的工具变成一道栅栏,前面的先跑完、它自己单独跑、后面的等它完成才开始(_tool_execution.py:281),另有运行级开关可一刀切让每个工具都成为栅栏(:283)。

  22. 出处:「5.4 通过ChatCompletion API来实现Tool Calls」第 139 段(text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:139,搜「就会默认启用之前提到的“JSON模式”」)。原文强调:无论走哪一套接口,只要用到工具接入,这个开关都会默认打开。

  23. 出处:「5.4 通过ChatCompletion API来实现Tool Calls」第 154 段(text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:154,搜「API将反馈出错信息」)。原文的完整逻辑是:不明确指示的话,模型「可能会生成无限多的空白字符,并且请求可能会持续运行直至达到 Token 的上限」;为了防止这种情况,接口才加了那个硬性检查。

  24. 出处:「5.4 通过ChatCompletion API来实现Tool Calls」第 158 段(text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:158,搜「并不保证输出结果能匹配任何特定的架构」)与第 156 段(text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:156,搜「需要检查返回的 finish_reason」)。这两条是这本书对这个开关最清醒的两句话,可惜都放在一个「咖哥发言」的小框里,很容易被跳过。