3、Function Calling 机制:原理、Schema 定义与多工具并行调用
Function Calling 这个词,听起来挺唬人的。其实说白了,就是让大模型能调用你写好的函数。
我刚开始接触这个概念时,也觉得它很神秘。后来发现,它本质上就是一个「翻译」过程——把自然语言翻译成结构化的函数调用指令。嗯,今天我们就把它彻底讲透。
3.1 Function Calling 的核心原理
传统的 LLM 对话,模型只能输出文本。你问它「帮我查一下北京的天气」,它只能回复「好的,我帮你查一下」,然后就没有然后了。它没法真的去调 API。
Function Calling 解决了这个问题。它的工作流程是这样的:
- 你预先定义好一批工具函数,告诉模型这些函数叫什么、有什么参数
- 用户提问后,模型判断是否需要调用某个函数
- 如果需要,模型返回一个结构化的函数调用请求(不是真的执行)
- 你拿到这个请求,自己去执行函数,把结果返回给模型
- 模型基于函数结果,生成最终的自然语言回复
关键点:模型不执行代码,它只负责「决定该调用哪个函数、传什么参数」。执行权始终在你手里。
我在项目中遇到过不少新手,以为 Function Calling 是模型直接跑 Python 脚本。不是的。模型输出的是 JSON,你拿到 JSON 后自己去调函数。这个设计很安全,也很灵活。
3.2 定义工具函数 Schema
要让模型知道你的函数长什么样,你得给它一份「说明书」。这份说明书就是 Function Schema。
不同的模型厂商,Schema 格式略有差异。但核心结构是相通的。我以 OpenAI 的格式为例,这是目前最主流的写法:
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市在指定日期的天气信息",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如北京、上海"
},
"date": {
"type": "string",
"description": "日期,格式为 YYYY-MM-DD"
}
},
"required": ["city", "date"]
}
}
}
你看,每个字段都有明确的含义:
| 字段 | 说明 | 我的建议 |
|---|---|---|
| name | 函数名,模型靠它来匹配 | 用下划线命名法,别用驼峰 |
| description | 函数描述,越详细越好 | 写清楚什么场景下调用 |
| parameters | 参数定义,遵循 JSON Schema | 每个参数都要写 description |
| required | 必填参数列表 | 能设默认值的就别放 required |
小技巧:description 字段非常关键。模型靠它来判断「这个函数适不适合当前场景」。我习惯在 description 里写清楚边界条件,比如「仅支持中国城市,国外城市返回错误」。这样能减少模型乱调用的概率。
3.3 多工具并行调用
早期的 Function Calling 一次只能调一个函数。但现在主流模型都支持 并行调用——一次回复里包含多个函数调用请求。
举个例子。用户说:「帮我查一下北京和上海的天气,然后给张三发邮件汇报结果。」
模型可能会返回这样的结构:
{
"choices": [{
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\": \"北京\", \"date\": \"2024-01-15\"}"
}
},
{
"id": "call_2",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\": \"上海\", \"date\": \"2024-01-15\"}"
}
},
{
"id": "call_3",
"type": "function",
"function": {
"name": "send_email",
"arguments": "{\"to\": \"zhangsan@example.com\", \"subject\": \"天气汇报\", \"body\": \"详见附件\"}"
}
}
]
}
}]
}
三个调用之间没有依赖关系,你可以并行执行它们。这大大提升了效率。
注意:并行调用虽然快,但要注意 API 限流。我曾经在一个项目里,模型一次返回了 20 个并行调用请求,直接把第三方天气 API 打爆了。建议你在代码里加个并发数控制,比如最多同时执行 5 个。
3.4 实际项目中的避坑指南
做了几个项目后,我总结了一些血泪教训:
- 参数校验不能省:模型生成的参数不一定合法。比如日期格式可能不对,城市名可能拼错。我习惯在函数入口做一层校验。
- 超时处理要到位:有些函数执行时间很长(比如调用第三方 API),模型可能等不及。建议设置超时时间,超时后返回一个友好的错误信息。
- 错误信息要结构化:函数执行失败时,返回的错误信息最好也带个 code 和 message,方便模型理解。比如
{"error": {"code": "RATE_LIMIT", "message": "请求过于频繁"}}。 - 上下文管理:每次函数调用的结果都会塞回对话里。如果函数返回的数据很大(比如几万行表格),对话上下文会迅速膨胀。我一般会做摘要,只把关键信息返回给模型。
3.5 一个完整的调用示例
最后,给你看一个完整的 Python 实现片段。这是我常用的模式:
import json
from openai import OpenAI
client = OpenAI(api_key="your-key")
# 1. 定义工具
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取中国城市的天气信息",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名"},
"date": {"type": "string", "description": "日期 YYYY-MM-DD"}
},
"required": ["city", "date"]
}
}
}
]
# 2. 调用模型
response = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": "北京今天天气怎么样?"}],
tools=tools,
tool_choice="auto"
)
# 3. 处理函数调用
message = response.choices[0].message
if message.tool_calls:
for tool_call in message.tool_calls:
func_name = tool_call.function.name
args = json.loads(tool_call.function.arguments)
# 执行你的函数
result = your_function(func_name, args)
# 把结果返回给模型
messages.append({
"tool_call_id": tool_call.id,
"role": "tool",
"content": json.dumps(result)
})
# 再次调用模型生成最终回复
final_response = client.chat.completions.create(
model="gpt-4",
messages=messages
)
print(final_response.choices[0].message.content)
这个模式很通用。你只需要替换 your_function 的实现,就能接入任意工具。
核心要点回顾:
- Function Calling 是模型输出结构化调用请求,不是模型执行代码
- Schema 定义要详细,description 是关键
- 并行调用能提升效率,但要注意限流
- 参数校验、超时处理、错误结构化,一个都不能少