
字节笔记本
2026年10月7日 · 约 6 分钟读完
工具调用中间那两趟不能并成一趟
问天气看起来像一次请求:用户说城市,模型回答温度。接上工具之后,中间一定有两次模型调用。第一次模型不回答,它点名要执行哪一个函数。你的程序真的去查,把结果塞回去,第二次模型才写成给人看的话。少一趟,要么没有数据,要么模型在编温度。

第一次调用只为了点名
程序先准备工具定义。名字、参数和一句说明告诉模型:有一个 get_weather,参数是城市。然后把用户的话和这份工具列表一起送出。
模型如果决定要用工具,返回的不是最终句子,而是一次工具调用:函数名加上参数,比如城市是北京。这一步不要把返回内容直接展示给用户。展示出来会是一截调用结构,或者一句还没拿到数据的空话。
本地函数按参数执行。样本里是一张写死的表,北京对应 18°C 和晴,用来把流程跑通,不是实时天气。真实项目把这一步换成 HTTP 或数据库,函数签名不用改。模型不负责发这个请求。它没有你的网络,也没有你的密钥。
第二次调用才生成回答
执行完,把工具结果按调用编号放进消息列表,再请求一次模型。这次消息里有三样东西:原来的用户问题、模型刚才发出的工具调用、你填回去的结果。模型据此写成“北京 18°C,晴”这种句子。这个数字来自你的函数,不是来自模型的记忆。
两次调用的角色不同。第一次看的是有没有 tool_calls。有,就执行,不要当最终答案。没有,才说明模型打算直接回答,比如用户问的根本不是天气。第二次看的是正文。如果第二次仍然返回工具调用,说明结果格式不对,模型还想再要一次。把第二次的调用又展示出来,界面会像卡住。
异步版本只是把这两次请求放进协程。顺序不能并成一次。你不可能在模型点名之前就知道它要哪个城市,除非你自己先解析用户的话,那就是不用工具、自己写路由了。
流程画乱,通常是把两次调用画成了一次
对话里要求流程图只突出中间这两次,后来仍然画乱。乱的来源很固定:把“本地执行函数”画成第三次模型请求,或者把工具定义画成一次调用。工具定义是第一次请求的输入,不是一次往返。本地执行发生在两次请求之间,不出网到模型。
用文字把四步固定下来,比用图更不容易错:
- 带上工具定义,发送用户消息
- 读返回,如果是工具调用,取出名字和参数
- 在进程里执行函数,得到字符串或 JSON
- 把结果追加进消息,再请求一次,这次的正文才给用户
日志也按这个顺序打。第一次的响应体、函数的返回值、第二次的正文,三份都留。缺第二份时,你会以为模型没调用工具。缺第三份时,你会把函数的原始 JSON 当成已经回答了用户。
模型点名,代码执行,模型再写
工具调用的重点不是天气接口长什么样,是这两次请求中间夹着你的代码。第一次只负责点名。函数在你这边跑完,第二次才有资格生成句子。样本里的温度是本地表,换真实数据时只换函数内部。来回两趟的形状不要并成一趟。

结果要能对上那一次调用
模型可以一次返回多个工具调用。天气例子里通常只有一个,但消息格式仍按列表处理。每个调用有自己的 id。把结果塞回去时带上这个 id,模型才知道这是哪一次点名的返回。不带 id,第二次请求里结果和调用对不上,模型会再要一次,或者忽略你查到的温度。
函数返回值用字符串或 JSON 文本即可。样本的天气是温度和天气状况两个字段。不要在函数里把句子写好,例如不要返回“今天北京很暖和”。措辞留给第二次的模型。函数只给事实。事实写错,模型会用错误事实写出很流畅的句子,这比没调用工具更难发现。
超时和失败也要作为结果返回,而不是让第二次请求缺少这条消息。查不到城市时返回一段明确的错误文本。模型可以告诉用户没有这个城市。如果你直接抛异常、不发第二次请求,用户看到的是界面报错,模型没有机会解释。
工具定义里的参数名要和函数参数一致。模型按定义里的名字填 JSON。你的函数如果读取的是 city,定义里却写 location,第一次调用会“成功”,函数拿到空城市,表里查不到,第二次就在解释一则空数据。流程图上这仍是完整的两趟,数据却是空的。
日志里不要只打“调用了工具”。打函数名、参数和结果的前一段。城市名错误时,能看出是模型抽错了,还是表里没有这个键。样本表里至少有北京。用这个城市做固定测试,两次请求的正文应该引用表里的温度,而不是另一个数字。数字对不上,说明第二次没有读到工具结果,模型在自由发挥。
协程可以把第一次等待、本地查询、第二次等待串起来。不要为了快,在第一次返回之前就并发去查“可能的城市”。那就不再是工具调用,而是你自己做了意图识别。对话要展示的就是模型点名之后你才执行。 第一次响应里如果同时有正文和工具调用,以工具调用为准,先执行再第二次请求。有的接口会在调用旁边带一句过渡文本。那句可以显示成“正在查询”,但不要把它当成最终天气。最终句子只来自第二次。
工具列表太长时,模型可能点错名字。本地执行前做一次白名单:只允许定义过的函数。点名之外的函数直接当成错误结果返回,不要用反射去调用任意函数。天气示例只有一个函数,白名单就是这一个名字。加上白名单,流程仍是两趟,只是非法点名不会跑出进程里别的能力。
把这个流程讲给别人时,用一个固定城市走完四步,把两次请求的消息数组打印出来。第一次的消息只有用户和工具定义。第二次的消息末尾是工具结果。数组形状对了,实现就没有并成一趟。



