工具调用与函数调用

Tool Use / Function Calling

工具调用是让 LLM 超越纯文本生成、通过结构化参数去调用外部能力(天气 API、计算器、知识库查询、代码执行)的能力,是智能体的核心基础模块。

详细解释

工具调用(Tool Use / Function Calling,FC) 一句话定义:让 LLM 在生成文本的过程中,遇到”自己没法直接回答”的事情时,主动停下来,生成一段严格符合 JSON Schema 的”工具参数”,然后调用外部函数/HTTP API,把返回结果继续喂回模型上下文,接着生成最终答案。没有工具调用的 LLM 是”只活在训练数据里的书呆子”——问它今天上海的天气,它只能根据训练数据里的上海 1 月偏冷瞎编。有了工具调用,它可以生成:

{
  "name": "get_weather",
  "arguments": {"city": "上海", "date": "2026-08-24"}
}

→ 服务端用这段参数调第三方天气 API → 把返回 {temperature: 31, condition: "多云"} 塞回对话 history → 模型再根据真实天气生成回答,就是所谓的” Grounded Generation(落地生成)”。

一次典型工具调用的 6 步消息轮次

以用户问 “2025 年 Q1 唯元智创的平均 RPM 是多少?按同比和环比算一下增长率”为场景:

  1. Step 1 用户消息 User:上面那句话。
  2. Step 2 模型第一轮返回 Assistant(content=null, tool_calls=[…]):模型决定要调用”取内部指标数据库 query”和”数学计算”两个工具,生成两个 tool_call JSON,带每个 tool_call 的 id。
  3. Step 3 服务端并发调两次函数internal_kpi(query='weimeta RPM Q1 2024/2025') 返回数据行;math_calculator(formula='(y25-y24)/y24*100') 等待数据。
  4. Step 4 把工具结果以 Role=tool 的消息回塞:每个 tool_call 对应一条 role=tool, tool_call_id=xxx, content='实际返回的 JSON 字符串'
  5. Step 5 模型第二轮生成 Assistant(content=文本回答):拿到真实数据后开始做解读、生成回答文本。
  6. Step 6 流式返回给用户:把 Step 5 生成的回答通过 Streaming SSE 流到前端。

中间可能迭代多轮(上一步工具返回说”参数不够,还需要时间范围”→ 模型再生成第二条 Tool Call 补充参数),直到模型觉得已经足够回答,就停止调用工具。

主流 API 协议版本差异

2023 年 OpenAI 第一次推出 Function Calling 之后,协议经历了三代演进:

版本(时间)参数组织方式支持多工具并行调用流式 tool_calls 支持
v1 初代(2023 Q2)functions=[{name, description, parameters}] 顶层字段 + function_call 返回 role=assistant❌ 一次只能调一个❌ 只能非流式
v2 现代(2023 Q4)tools=[{type:'function', function:{...}}] 顶层字段 + 返回 tool_calls[] 数组,每条带 id,回复时 role=tool + tool_call_id✅ 并行调用任意多个✅ 流式:先吐 delta.tool_calls[i].index/.id/.function.name/.arguments
v3 严格结构化(2024 Q3)tools 字段相同,但新增 strict=true 强制模型”parameters JSON Schema 字段一个不能漏/一个不能加”,FC 准确率从 92% 提升到 99.5%+

唯元智创 兼容的 /chat/completions 端点完全支持 v2/v3 协议strict=true 下函数调用漏参率 < 0.4%(我们自己的内部测试集),适合生产级代码工具调用流水线不用写大量 if-else 兜底。

写好工具定义(Tool Schema)的 4 条黄金法则

90% 的 FC 失败是因为你 Schema 写得太烂,不是模型能力不行。记住以下四条能把你的 Tool Call 成功率从 70% 拉到 98%:

  1. Description 要用”何时调、何时 NOT 调”的负例写法,不要写”这个函数取天气”。
    • ❌ Bad:description: "获取指定城市天气"
    • ✅ Good:description: "当用户询问未来 7 天内某个具体城市的实时或预测天气,且问题中出现明确城市名时调用。如果用户只是泛泛说今天天气真好/聊气候话题/或没有给出城市名时,不要调用此工具。"
  2. 参数 Enum 能写尽写:对 city 字段如果你的 API 只支持 31 个省辖市,就把 31 个都列成 Enum,模型就不会瞎编 “浦东市” 这种不存在的城市。
  3. Required 字段必须和真实后端校验一致:后端实际需要 city + date 两个参数,Schema 里 Required 只写了 city → 模型 40% 会漏 date,你就会查不出来。
  4. 每个参数都要有独立的 description:对 date 参数写 ISO 8601 格式 YYYY-MM-DD,仅支持今天之后 7 天内,模型就不会给你传 date='2026年9月1号' 这种自由格式。

与 Agent / Computer Use 的关系

  • 单工具调用(Function Calling) = 原子能力(一次调用一个 HTTP API)。是积木块。
  • Agent(智能体) = 多工具 + 多轮 + 规划(ReAct / ToT / Self-Consistency 等)。是积木拼出来的乐高机器人。
  • Computer Use(计算机使用) = Agent 的特殊形态,工具集合变成”鼠标点击 / 键盘输入 / 截图”,本质上就是 3 个通用工具(mouse_click / type_text / take_screenshot)。

唯元智创 的 Agentic SDK 把这三个层级做了统一:先用 Function Calling 包每个原子 API,再套 ReAct Agent 规划层,最后 Computer Use 也是复用同一套 SDK 的 BaseTool 类,不用写两套实现。

常见问题

模型经常”该调用工具时不调用,不该调用时乱调”怎么办?
按顺序做三条,90% 的 case 都能治:(1)把 tool_choice 从 auto 改成显式 required,或者直接传一个包含 type 字段等于 function、function.name 字段等于具体工具名的对象来强制调用——对「你明确知道这一步必须调某个工具」的单步场景,强制要求调,模型就不会逃;(2)加两条 System Prompt 示例(One-Shot),一条”用户问了 XX → 我调了 YY 工具 → 返回 ZZ → 我回答 WW”,一条”用户只是闲聊 → 我不调任何工具 → 直接回答”——few-shot 例子能把 FC 准确率 +10%;(3)开启 strict=true(v3 协议)+ 后端参数校验失败时,把具体错误以 role=system error message 格式原样回塞 context 让模型自己改参数,一般 2 轮内就能改对。
一个请求里最多能并行调用多少个工具?
协议上不设上限,但工程实战建议:单次 tool_calls 不要超过 3 到 5 个。原因:(1)并行函数越多,每个的 JSON 参数出错概率乘起来就高——5 个每个 98% 成功率 → 整体 90%,还得全改;(2)你服务端线程/连接池也容易被打满;(3)模型在长上下文里塞了 10 个 tool results,反而会”注意力分散”,最终回答质量会下降。真的要调用十几个数据源,就写外层多 Agent 编排:先一个 Coordinator 把任务拆成 3 个子任务,每个子任务独立一条对话链,最后再合成结果。
结构化输出(response_format=json_object)和 Function Calling 是什么关系?
是同一件事的两个子集。Structured Output 回答的本质:模型直接生成 JSON,不再生成自然语言——这和 Function Calling 里模型生成 {"name":"xxx", "arguments":{...}} 是同一能力,只是 Structured Output 把 “arguments 部分” 直接当成最终答案返回给你,没有后续 role=tool 的下一轮调用。所以如果你的业务是纯”抽取 / 分类 / 返回结构化数据给前端渲染”,不需要让模型”再调用外部函数”,直接开 response_format=json_schema 最省事;如果需要”先调外部 API 拿结果,再生成最终回答”的多步链路 → 上 Function Calling / Tool Use。