深入理解 LLM 函数调用:模型不会"决定"用工具
深入理解 LLM 函数调用:模型不会”决定”用工具
一句话:函数调用不是模型内部”觉醒”的能力,而是推理层在 prompt 里塞了一套工具描述后,模型遵循协议输出的结构化 token。本质上是 prompt engineering 的延伸——只不过发生在 API 层而非用户输入层。
模型不会”觉得该用工具了”
关于函数调用最大的误解,就是以为模型内部有个”我该调工具了”的判断机制。没有。模型就是一个 autoregressive token 生成器——给定前文,预测下一个 token。仅此而已。
函数调用是推理层启用的能力,需要两个显式条件同时满足:
- 请求里必须带
tools参数——一个函数定义数组,每个函数包含名称、自然语言描述和 JSON Schema 参数规格。 tool_choice参数必须允许调用。默认值是"auto"——模型在它觉得合适时可能返回工具调用。你也可以设为"none"(绝不下手)、"required"(必须下手),或锁定到某个具体函数。
当 tool_choice 为 "auto",模型做的事是把每个函数的描述跟用户请求做匹配。请求超出了模型知识边界(实时数据、私有数据库、精确计算),且恰好有匹配的工具——模型就输出工具调用,不输出文本。没匹配上,就正常吐文本。
函数描述是模型判断何时调用的唯一信号。写个模糊的”获取数据”既会导致误触发,也会漏调用。写成”查询指定城市的当前天气。返回温度、湿度和天气状况字符串”——准确率能上去一大截。说白了,工具描述就是 prompt engineering,只是位置从用户消息挪到了 API 参数里。
content 和 tool_calls 永远不共存
这个设计让很多第一次接触的人愣住:在同一个 assistant 响应里,content 和 tool_calls 是互斥的。模型要么返回文本,要么返回工具调用——绝不同时返回。
| 情况 | content |
tool_calls |
|---|---|---|
| 模型直接回答 | 有值 | null |
| 模型需要工具 | null |
调用数组 |
ChatGPT 看起来能”边搜边说”,那是一种编排层制造的流畅假象。底层是多次往返:模型返回工具调用(没文本),你的系统执行它,把结果喂回去,模型再生成自然语言回复。你看到的连贯对话,是多次独立 API 调用拼接出来的。
这个互斥特性有实际的工程影响:如果你在流式传输响应,没法一边等工具结果一边先渲染部分文本——要么有内容能展示,要么有工具要执行,同一个响应对象里永远不存在两者并存。
messages[] 是唯一的真相来源
函数调用不是有状态的。发给模型的每一次请求,都必须在 messages 数组里带全整段对话历史。这个数组是模型唯一能看到的东西,它随每一轮对话不断膨胀。
一个完整的工具调用流转长这样:
1 | 第1步——用户提问 |
每一步都是对 messages[] 的一次追加。模型在两次 API 调用之间没有记忆——messages 数组就是记忆。如果你在最后一轮漏了 tool 结果消息,模型就会幻觉,因为它根本没看见那些数据。同理,你不能在中途删消息:模型需要看到 assistant(tool_call) 才能理解”刚才有过工具调用”,也需要看到 tool(result) 才能知道”返回了什么”。
工程落地:并行、错误、循环、流式
并行工具调用
模型可以在单个响应里同时返回多个 tool_calls——比如一次性查北京和上海的天气。这些调用彼此独立,你的系统应该并发执行它们,再把各自的结果以独立的 tool 消息全部追加进去,然后发回模型。串行执行等于平白无故把延迟翻倍。
错误处理
当工具执行失败,别把异常直接抛给用户。把错误当成一条 tool 消息追加到数组里,发回模型。模型通常会道歉、解释问题、或尝试替代方案。错误变成了对话的一部分,而不是走到了死胡同。
工具调用循环
这是最棘手的边缘情况。模型可能调用一个工具,拿到结果,然后基于结果再调用另一个工具——在给出最终答案之前串联多次调用。你的编排层必须能处理循环:只要模型还在返回 tool_calls 就继续迭代,同时设一个硬上限防死循环。每次迭代都在 messages[] 末尾追加内容,重新请求模型。
流式增量组装
接收流式响应(SSE)时,工具调用以碎片化的增量(delta)到达。一个 get_weather("北京") 可能拆成五个 SSE 事件传过来。你需要按 index 缓冲(模型可能交叉发送多个工具调用),累积 id、function.name 和 function.arguments 的碎片,直到 tool_calls 流发出完成信号。只有到那一刻你才能解析出完整的 JSON 参数并执行。
核心要点
- function calling 是外部触发的,不是内部决定的——
tools参数和tool_choice设定控制着是否及何时调用;模型只是按协议输出。 content与tool_calls互斥——单个响应绝不同时包含两者;多轮编排制造了文本与工具同步的假象。messages[]是唯一真相来源——每次 API 调用都带全对话历史,遗漏任何中间消息都会破坏模型上下文。- 并行工具调用必须并发执行——它们定义上就互相独立,串行是浪费延迟。
- 错误也是消息——工具执行失败作为
tool消息喂回对话,让模型优雅恢复。 - 流式工具调用需要小心的增量组装——按索引缓冲,累积碎片,完整后才解析。