Function Calling 从原理到实现:模型并不会执行代码
最大的误解:模型不执行任何东西
第一次接触 Function Calling 的人常有一种错觉:给模型一堆函数,它就能自己去查数据库、发邮件。
不是的。 模型唯一能做的是输出一段结构化文本,声明「我想调用 query_order,参数是 {"order_id": "A123"}」。真正执行的是你的代码,执行完把结果再交回给模型,它才能继续往下说。
完整闭环是四步:
1. 你:带上 tools 定义 + 用户消息,请求模型
2. 模型:返回 tool_calls(意图),不返回自然语言答案
3. 你:按意图执行真实函数,拿到结果
4. 你:把结果以 role="tool" 追加进消息,再请求一次模型
5. 模型:基于工具结果生成最终回答
第 4 步最容易被漏掉——很多人拿到 tool_calls 执行完就直接把结果返回给用户,于是用户看到的是原始 JSON 而不是人话。
一、工具定义长什么样
以 OpenAI 兼容格式为例(国内多数厂商也用这套):
tools = [{
"type": "function",
"function": {
"name": "query_order",
"description": "查询订单状态与金额。当用户询问订单进度、是否发货、金额明细时使用。"
"不要用于查询他人订单。返回字段:order_id, status, amount, created_at。",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "订单号,形如 A123456,必填"
}
},
"required": ["order_id"]
}
}
}]
三个关键点:
description决定模型会不会正确选工具。写「查询订单」太短;要写清「做什么 / 什么时候用 / 什么时候不要用 / 返回什么」- 参数的
description同样重要,模型靠它判断该填什么格式 - 工具越少越准。超过 10 个高度相似的工具,误调用率会明显上升——按场景拆分不同的工具集
二、执行循环的代码骨架
import json
from openai import OpenAI
client = OpenAI(api_key=KEY, base_url=BASE_URL)
def run(messages, tools, handlers, max_steps=6):
for step in range(max_steps):
resp = client.chat.completions.create(
model=MODEL,
messages=messages,
tools=tools,
tool_choice="auto",
)
msg = resp.choices[0].message
# 没有工具调用 → 这就是最终答案
if not msg.tool_calls:
return msg.content
messages.append(msg) # 关键:把 assistant 的 tool_calls 原样放回
for call in msg.tool_calls:
fn = handlers.get(call.function.name)
try:
args = json.loads(call.function.arguments or "{}")
result = fn(**args) if fn else {"error": f"unknown tool {call.function.name}"}
except Exception as e:
result = {"error": str(e), "retryable": True}
messages.append({
"role": "tool",
"tool_call_id": call.id, # 必须对上,对不上会报错
"content": json.dumps(result, ensure_ascii=False),
})
return "(已达到最大步骤数,任务未完成)"
四个工程细节,漏一个就跑不通:
messages.append(msg):assistant 那条带tool_calls的消息必须原样回传,否则模型不知道自己在回应什么tool_call_id必须匹配:每条 tool 结果都要对应一个 call idcontent必须是字符串:不要直接塞 dict,要json.dumpsmax_steps必须有:模型可能反复调用同一工具陷入循环
三、并行工具调用
模型可以一次返回多个 tool_calls(比如同时查订单和物流)。这是性能优化点:不要串行执行,用并发。
import asyncio
async def run_parallel(calls, handlers):
async def one(call):
args = json.loads(call.function.arguments or "{}")
return call.id, await handlers[call.function.name](**args)
return await asyncio.gather(*[one(c) for c in calls])
注意:并行只在工具之间无依赖时安全。有依赖(先查订单号、再查物流)时,模型通常会自己分两轮调用。
四、tool_choice:控制调不调
| 取值 | 行为 | 用在哪 |
|---|---|---|
auto |
模型自己决定 | 默认 |
required |
必须调用至少一个工具 | 强制走工具链,防止它直接瞎答 |
none |
禁止调用 | 只想要纯文本回答时 |
{"type":"function","function":{"name":"x"}} |
强制调用指定工具 | 固定流程的第一步 |
实用场景:先 required 逼它查数据,再 auto 让它组织答案。可以显著减少"不查就编"。
五、strict 模式 / 结构化输出
即使给了 JSON Schema,模型仍可能少字段、类型不对。启用严格模式(OpenAI 的 strict: true 或 response_format 的 json_schema)后,模型输出会被约束到 schema 上。
限制要知道:
- 通常要求
additionalProperties: false,且所有字段列入required - 嵌套深度、枚举值数量有上限
- 不是所有厂商都支持,跨厂商时要有降级方案
六、常见故障对照表
| 现象 | 根因 | 修法 |
|---|---|---|
| 该调工具时不调 | description 太笼统 | 补「什么时候用」;或 tool_choice="required" |
| 调错工具(相近功能) | 工具太多/描述重叠 | 拆分工具集,在描述里写清与相邻工具的区别 |
| 参数缺字段 | 参数无 description、未设 required | 补描述 + strict 模式 + 执行前校验 |
报错 tool_call_id 不匹配 |
结果没带 id 或顺序错 | 严格按 call.id 回传 |
| 无限循环调用 | 结果没让模型满足 | 设 max_steps,并在 prompt 里写「拿到 X 就停止」 |
| 用户看到 JSON | 漏了最后一轮 | 执行完必须再请求一次模型生成自然语言 |
七、什么时候不该用 Function Calling
- 只有一种固定操作、参数简单 → 直接规则解析更快更便宜
- 需要严格事务一致性 → 让模型选工具可以,但执行与校验必须在你自己的代码里
- 高风险不可逆操作(转账、删除)→ 必须加人工确认环节,不能交给 Agent 自主决定
相关技能
- Agent 工具描述优化:直接生成上面那种高质量 tools 定义
- Agent 多步任务规划
- JSON 格式化:排查 tool_calls 的 arguments 解析失败