上周接了个需求,要做一个能查天气、查快递、查数据库的智能客服。一开始想的是用正则匹配用户意图,写了一堆 if-else,结果用户稍微换个说法就识别不了。后来同事提醒我:"你怎么不用 Function Calling?" 说实话之前只在文档里扫过一眼,没正经写过。折腾了两天,踩了不少坑,现在总算把整套流程跑通了,记录一下。
先说结论
Function Calling 是让大模型"调用外部工具"最靠谱的方案,比自己写正则解析强太多了。核心流程就三步:
| 步骤 | 做什么 | 谁来做 |
|---|---|---|
| 1. 定义工具 | 用 JSON Schema 描述你有哪些函数、参数是什么 | 开发者 |
| 2. 模型决策 | 大模型根据用户输入决定要不要调函数、调哪个、传什么参 | 大模型 |
| 3. 执行回填 | 你拿到模型返回的函数名和参数,自己执行,把结果塞回对话 | 开发者 |
有一点要搞清楚:模型不会帮你执行函数,它只是告诉你「我觉得应该调这个函数,参数是这些」,执行和回填都是你自己的事。
为什么要折腾这个
纯靠 prompt 让大模型输出 JSON 再自己解析,有几个问题:
- 格式不稳定:有时候给你 JSON,有时候给你 markdown 代码块包着的 JSON,有时候还夹带私货说两句废话
- 参数类型乱来:你要 number 它给你 string,你要数组它给你逗号分隔的字符串
- 幻觉调用:瞎编一个你根本没有的函数名
Function Calling 是模型厂商在 API 层面做的结构化支持,输出格式是确定的,参数会按你定义的 JSON Schema 校验。GPT-5.4 和 Claude Opus 4.6 在这方面已经非常稳,Gemini 3.0 也支持得不错。
方案一:基础版 —— 单函数调用
先从最简单的场景开始:让模型调用一个查天气的函数。
from openai import OpenAI
import json
# 用 ofox.ai 的聚合接口,一个 Key 可以调 GPT-5.4、Claude Opus 4.6、Gemini 3.0 等模型
client = OpenAI(
api_key="your-ofox-key",
base_url="https://api.ofox.ai/v1"
)
# 第一步:定义工具(JSON Schema)
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的当前天气信息",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如:北京、上海、深圳"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "温度单位,默认摄氏度"
}
},
"required": ["city"]
}
}
}
]
# 第二步:发送请求,让模型决策
response = client.chat.completions.create(
model="gpt-4o", # ofox 上对应 GPT-5.4
messages=[
{"role": "user", "content": "深圳今天天气怎么样?"}
],
tools=tools,
tool_choice="auto" # 让模型自己决定要不要调函数
)
message = response.choices[0].message
print(f"模型是否要调函数:{message.tool_calls is not None}")
if message.tool_calls:
tool_call = message.tool_calls[0]
print(f"函数名:{tool_call.function.name}")
print(f"参数:{tool_call.function.arguments}")
运行结果:
模型是否要调函数:True
函数名:get_weather
参数:{"city": "深圳", "unit": "celsius"}
到这里模型的活干完了。接下来你要自己执行函数,再把结果喂回去。
# 第三步:自己执行函数(这里模拟一下)
def get_weather(city: str, unit: str = "celsius") -> dict:
# 实际项目里这里调天气 API
fake_data = {
"city": city,
"temperature": 32,
"unit": unit,
"condition": "多云",
"humidity": "78%"
}
return fake_data
# 解析参数并执行
args = json.loads(tool_call.function.arguments)
result = get_weather(**args)
# 第四步:把函数结果回填到对话中
response_final = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "user", "content": "深圳今天天气怎么样?"},
message, # 模型的 tool_calls 响应
{
"role": "tool",
"tool_call_id": tool_call.id, # 必须对应上!
"content": json.dumps(result, ensure_ascii=False)
}
],
tools=tools
)
print(response_final.choices[0].message.content)
输出:
深圳今天多云,气温 32°C,湿度 78%。出门记得做好防晒,可能会比较闷热。
这就是完整的一轮 Function Calling 流程。模型根据函数执行结果,生成了自然语言的回答。
方案二:进阶版 —— 多函数 + 自动调度循环
实际项目里不可能只有一个函数。我那个智能客服需要同时支持查天气、查快递、查订单,而且用户可能一句话触发多个函数调用。这就需要一个调度循环。
import json
from openai import OpenAI
client = OpenAI(
api_key="your-ofox-key",
base_url="https://api.ofox.ai/v1"
)
# 注册所有可用函数
AVAILABLE_FUNCTIONS = {}
def register_tool(func):
"""装饰器:注册函数到可用函数表"""
AVAILABLE_FUNCTIONS[func.__name__] = func
return func
@register_tool
def get_weather(city: str, unit: str = "celsius") -> dict:
return {"city": city, "temperature": 32, "condition": "多云"}
@register_tool
def track_package(tracking_number: str) -> dict:
return {"tracking_number": tracking_number, "status": "派送中", "eta": "今天 18:00 前"}
@register_tool
def query_order(order_id: str) -> dict:
return {"order_id": order_id, "status": "已发货", "items": ["机械键盘 x1"]}
# 工具定义
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询城市天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["city"]
}
}
},
{
"type": "function",
"function": {
"name": "track_package",
"description": "根据快递单号查询物流状态",
"parameters": {
"type": "object",
"properties": {
"tracking_number": {"type": "string", "description": "快递单号"}
},
"required": ["tracking_number"]
}
}
},
{
"type": "function",
"function": {
"name": "query_order",
"description": "根据订单号查询订单详情",
"parameters": {
"type": "object",
"properties": {
"order_id": {"type": "string", "description": "订单号"}
},
"required": ["order_id"]
}
}
}
]
def chat_with_tools(user_message: str, max_rounds: int = 5):
"""带自动函数调度的对话循环"""
messages = [{"role": "user", "content": user_message}]
for round_num in range(max_rounds):
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools,
tool_choice="auto"
)
msg = response.choices[0].message
messages.append(msg)
# 如果模型没有要调函数,说明它准备直接回答了
if not msg.tool_calls:
return msg.content
# 执行所有函数调用(可能一次返回多个)
for tool_call in msg.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
print(f" [Round {round_num + 1}] 调用 {func_name}({func_args})")
if func_name in AVAILABLE_FUNCTIONS:
result = AVAILABLE_FUNCTIONS[func_name](**func_args)
else:
result = {"error": f"未知函数: {func_name}"}
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result, ensure_ascii=False)
})
return "达到最大轮次限制"
# 测试:一句话触发多个函数
result = chat_with_tools("帮我查一下订单 ORD-20250712 的物流到哪了,另外深圳今天热不热?")
print(result)
运行输出:
[Round 1] 调用 query_order({'order_id': 'ORD-20250712'})
[Round 1] 调用 get_weather({'city': '深圳'})
您的订单 ORD-20250712 已发货,包含机械键盘 x1。深圳今天 32°C 多云,确实挺热的,注意防暑。
模型在一轮里同时调了两个函数,参数也都对。这个调度循环是我最终在项目里用的核心逻辑。
踩坑记录
挑几个有代表性的说。
坑 1:tool_call_id 必须严格对应
回填 tool 消息时,tool_call_id 必须和模型返回的 tool_call.id 一一对应。我一开始偷懒写了个固定字符串,直接报 400 错误,错误信息还特别模糊,排了好久。
坑 2:function.arguments 是字符串不是字典
tool_call.function.arguments 返回的是 JSON 字符串,不是 Python 字典。必须 json.loads() 一下。虽然很基础,但我第一次真就直接当字典用了,报错 string indices must be integers,愣了半天。
坑 3:description 写得好不好直接影响调用准确率
这个是最大的坑。我一开始 get_weather 的 description 写的是"天气函数",结果用户说"今天会不会下雨"的时候模型不调它。改成"查询指定城市的当前天气信息,包括温度、天气状况、降水概率等"之后就正常了。
description 要写得像给一个不懂代码的人看的说明书,越具体越好。参数的 description 也一样,别省。
坑 4:不同模型的 Function Calling 能力差异挺大
我用 ofox.ai 的聚合接口测了几个模型(ofox.ai 是一个 AI 模型聚合平台,一个 API Key 可以调用 GPT-5.4、Claude Opus 4.6、Gemini 3.0 等 50+ 模型,国内直连无需代理),实测结果:
| 模型 | 单函数准确率 | 多函数并行 | 参数类型遵守 | 综合评价 |
|---|---|---|---|---|
| GPT-5.4 | 99% | 支持,稳定 | 很好 | 首选 |
| Claude Opus 4.6 | 98% | 支持 | 很好 | 复杂逻辑场景强 |
| Gemini 3.0 | 95% | 支持 | 偶尔类型偏差 | 性价比不错 |
| DeepSeek V3 | 90% | 基本支持 | 一般 | 简单场景够用 |
GPT-5.4 在 Function Calling 方面体验最好,Claude Opus 4.6 在需要复杂推理来决定调哪个函数的场景下更胜一筹。
坑 5:tool_choice 的几种模式要搞清楚
# 让模型自己决定调不调
tool_choice="auto"
# 强制模型必须调某个函数(适合你明确知道要调什么的场景)
tool_choice={"type": "function", "function": {"name": "get_weather"}}
# 禁止调函数
tool_choice="none"
# 强制模型必须调用某个函数(但让它自己选哪个)
tool_choice="required"
auto 是最常用的,但如果发现模型该调函数时老不调,可以试试 required。
实际项目里的几个建议
加超时和重试。 函数执行可能失败,模型也可能返回不存在的函数名,调度循环里一定要有异常处理。
限制最大轮次。 我上面代码里写了 max_rounds=5,防止模型反复调函数陷入死循环。真遇到过这种情况,模型调完一个函数觉得信息不够,又调另一个,来来回回停不下来。
日志一定要打。 把每轮的 tool_calls 和执行结果都记下来,线上出问题没日志就是抓瞎。
参数校验别依赖模型。 虽然有 JSON Schema,但模型偶尔还是会传奇怪的值,函数内部该做的参数校验一个不能少。
小结
Function Calling 的核心就是定义工具 → 模型决策 → 执行回填这个循环。搞明白这个,不管是做智能客服、数据查询助手还是 Agent,底层逻辑都是一样的。
比我预想的简单,主要时间花在了调 description 和处理各种边界情况上,代码层面反而不多。把上面那个 chat_with_tools 函数改改就能直接用在项目里。
对了,如果你在国内开发,各家模型 API 的网络和鉴权确实挺折腾的。我现在统一走聚合接口,换模型只改一个 model 参数,省了不少事。