Function Calling实战:从翻车到稳定调用
2024年7月,我在一个智能客服项目里踩了个大坑。用户问“帮我查下北京明天天气”,模型调了查纽约天气的API。查了日志,发现模型把“北京”解析成了“New York”。这不是模型笨,是Function Calling的prompt设计有问题。本文用OpenAI GPT-4o、Claude 3.5 Sonnet、本地Qwen2.5-7B三个模型,对比三种方案,给出完整代码和压测数据。
问题:模型乱调API,准确率只有62%
项目背景:一个智能助手,需要调用天气查询、股票查询、日历查询三个API。用户自然语言输入,模型解析出参数并调用对应函数。
初始方案:直接给模型一个JSON schema,让模型输出函数调用。测试100个样本,准确率62%。翻车场景:
- 用户说“北京明天天气”,模型调了纽约天气API
- 用户说“帮我查下苹果股票”,模型调了天气API
- 用户说“明天下午3点开会”,模型没调日历API,直接回复“好的”
根本原因:模型对函数意图理解不准确,参数解析错误,甚至漏调函数。
方案对比:三种Function Calling实现
我对比了三种主流方案:
- 方案A:OpenAI原生Function Calling(GPT-4o,2024-08-06版本)
- 方案B:Claude Tool Use(Claude 3.5 Sonnet,2024-10-22版本)
- 方案C:本地模型+自定义解析(Qwen2.5-7B-Instruct,vLLM部署)
测试环境:Python 3.11.4,OpenAI SDK 1.30.0,Anthropic SDK 0.34.0,vLLM 0.6.0。硬件:单卡A100 80GB。测试集:100条真实用户query,覆盖天气、股票、日历三类。
方案A:OpenAI原生Function Calling
OpenAI从2023年6月就支持Function Calling,是最成熟的方案。核心是给模型一个functions列表,模型输出JSON格式的函数调用。
# 方案A:OpenAI原生Function Calling
import openai
from openai import OpenAI
client = OpenAI(api_key="your-api-key")
functions = [
{
"name": "get_weather",
"description": "获取指定城市和日期的天气信息",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如北京、上海、纽约"
},
"date": {
"type": "string",
"description": "日期,格式YYYY-MM-DD,如2024-12-01"
}
},
"required": ["city", "date"]
}
},
{
"name": "get_stock_price",
"description": "获取指定股票代码的当前价格",
"parameters": {
"type": "object",
"properties": {
"symbol": {
"type": "string",
"description": "股票代码,如AAPL、GOOGL、TSLA"
}
},
"required": ["symbol"]
}
},
{
"name": "create_calendar_event",
"description": "创建日历事件",
"parameters": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "事件标题"
},
"start_time": {
"type": "string",
"description": "开始时间,格式YYYY-MM-DD HH:MM"
},
"end_time": {
"type": "string",
"description": "结束时间,格式YYYY-MM-DD HH:MM"
}
},
"required": ["title", "start_time", "end_time"]
}
}
]
def call_openai_function_calling(query):
response = client.chat.completions.create(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "你是一个智能助手,根据用户输入调用合适的函数。不要自己编造信息,必须使用提供的函数。"},
{"role": "user", "content": query}
],
functions=functions,
function_call="auto",
temperature=0.1,
max_tokens=500
)
return response.choices[0].message
# 测试
query = "北京明天天气怎么样?"
result = call_openai_function_calling(query)
print(result.function_call.name) # 应输出 get_weather
print(result.function_call.arguments) # 应输出 {"city": "北京", "date": "2024-12-02"}
方案B:Claude Tool Use
Claude的Tool Use类似OpenAI,但需要手动处理tool_use块。Claude 3.5 Sonnet在复杂参数解析上表现更好。
# 方案B:Claude Tool Use
import anthropic
client = anthropic.Anthropic(api_key="your-api-key")
tools = [
{
"name": "get_weather",
"description": "获取指定城市和日期的天气信息",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称"},
"date": {"type": "string", "description": "日期,格式YYYY-MM-DD"}
},
"required": ["city", "date"]
}
},
{
"name": "get_stock_price",
"description": "获取指定股票代码的当前价格",
"input_schema": {
"type": "object",
"properties": {
"symbol": {"type": "string", "description": "股票代码"}
},
"required": ["symbol"]
}
},
{
"name": "create_calendar_event",
"description": "创建日历事件",
"input_schema": {
"type": "object",
"properties": {
"title": {"type": "string", "description": "事件标题"},
"start_time": {"type": "string", "description": "开始时间"},
"end_time": {"type": "string", "description": "结束时间"}
},
"required": ["title", "start_time", "end_time"]
}
}
]
def call_claude_tool_use(query):
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=500,
temperature=0.1,
system="你是一个智能助手,根据用户输入调用合适的工具。不要自己编造信息,必须使用提供的工具。",
messages=[
{"role": "user", "content": query}
],
tools=tools
)
# 解析tool_use块
for content in response.content:
if content.type == "tool_use":
return content.name, content.input
return None, None
# 测试
query = "帮我查下苹果股票价格"
tool_name, tool_input = call_claude_tool_use(query)
print(tool_name) # 应输出 get_stock_price
print(tool_input) # 应输出 {"symbol": "AAPL"}
方案C:本地模型+自定义解析
本地模型Qwen2.5-7B不支持原生Function Calling,需要自己写prompt模板和解析逻辑。我用vLLM部署,用system prompt让模型输出JSON格式。
# 方案C:本地模型+自定义解析
from openai import OpenAI # 兼容OpenAI接口
client = OpenAI(
api_key="EMPTY",
base_url="http://localhost:8000/v1" # vLLM服务地址
)
def call_local_model(query):
system_prompt = """你是一个智能助手,根据用户输入调用合适的函数。你必须输出一个JSON对象,格式如下:
{
"function": "函数名",
"arguments": {
"参数名": "参数值"
}
}
可用的函数:
1. get_weather: 获取天气,参数:city(城市名),date(日期YYYY-MM-DD)
2. get_stock_price: 获取股票价格,参数:symbol(股票代码)
3. create_calendar_event: 创建日历事件,参数:title(标题),start_time(开始时间),end_time(结束时间)
注意:不要输出任何其他内容,只输出JSON。"""
response = client.chat.completions.create(
model="Qwen/Qwen2.5-7B-Instruct",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": query}
],
temperature=0.1,
max_tokens=200
)
content = response.choices[0].message.content
# 解析JSON
import json
try:
result = json.loads(content)
return result.get("function"), result.get("arguments")
except json.JSONDecodeError:
return None, None
# 测试
query = "明天下午3点开会,持续1小时"
func, args = call_local_model(query)
print(func) # 应输出 create_calendar_event
print(args) # 应输出 {"title": "会议", "start_time": "2024-12-02 15:00", "end_time": "2024-12-02 16:00"}
效果数据:准确率、延迟、成本对比
测试100条query,每条人工标注正确函数和参数。结果如下:
| 指标 | 方案A(GPT-4o) | 方案B(Claude 3.5) | 方案C(Qwen2.5-7B) |
|---|---|---|---|
| 函数选择准确率 | 95% | 98% | 82% |
| 参数解析准确率 | 92% | 96% | 75% |
| 平均延迟(秒) | 1.2 | 1.5 | 0.8 |
| 每100次调用成本(美元) | $3.0 | $2.5 | $0.1(电费) |
| 翻车率(严重错误) | 5% | 2% | 18% |
关键发现:
- Claude 3.5在参数解析上最准,尤其是日期和时间解析。用户说“明天下午3点”,Claude能正确解析为“2024-12-02 15:00”,GPT-4o偶尔会解析成“2024-12-02 03:00 PM”。
- 本地模型Qwen2.5-7B在简单场景(如“北京天气”)表现不错,但复杂参数(如时间范围)容易出错。成本优势明显,适合高并发场景。
- 所有模型在“多意图”场景(如“查下北京天气和苹果股票”)表现差,准确率降到50%以下。需要拆分成多个单意图请求。
避坑指南:我踩过的5个坑
以下是我在项目中实际遇到的坑,每个都花了至少一天排查。
坑1:模型编造参数
用户说“查下北京天气”,模型输出{"city": "Beijing", "date": "2024-12-02"}。但北京是中文,模型用了英文。更坑的是,模型有时会编造不存在的参数,比如加个"unit": "celsius"。解决方案:在function description里明确参数格式,加上示例值。
{
"name": "get_weather",
"description": "获取指定城市和日期的天气信息",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,必须使用中文,如北京、上海、纽约。不要使用英文。"
},
"date": {
"type": "string",
"description": "日期,格式YYYY-MM-DD,如2024-12-01。不要使用其他格式。"
}
},
"required": ["city", "date"]
}
}
坑2:模型不调函数,直接回复
用户说“帮我查下苹果股票”,模型回复“好的,我查一下”而不是调函数。原因是temperature太高(0.7),模型倾向于聊天而不是执行。解决方案:temperature设为0.1,system prompt加上“必须使用提供的函数,不要自己回复”。
坑3:多轮对话中的上下文污染
第一轮用户说“北京天气”,模型调了get_weather。第二轮用户说“那上海呢?”,模型没调函数,直接回复“上海天气也不错”。因为模型把“上海”当成了闲聊。解决方案:每轮对话都重新传入functions列表,并在system prompt里强调“每次用户输入都必须判断是否需要调函数”。
坑4:本地模型的JSON解析失败
Qwen2.5-7B有时输出不完整的JSON,比如{"function": "get_weather", "arguments": {"city": "北京", "date": "2024-12-02"}(缺少闭合括号)。解决方案:用正则或json.loads加try-catch,失败时重试一次。重试prompt加上“请确保输出完整的JSON”。
import json
import re
def safe_parse_json(content):
# 尝试直接解析
try:
return json.loads(content)
except json.JSONDecodeError:
pass
# 尝试提取JSON块
match = re.search(r'\{.*\}', content, re.DOTALL)
if match:
try:
return json.loads(match.group())
except json.JSONDecodeError:
pass
# 尝试修复常见错误
fixed = content.replace("'", '"')
try:
return json.loads(fixed)
except json.JSONDecodeError:
return None
坑5:并发调用时的函数冲突
用户说“查下北京天气和苹果股票”,模型只调了一个函数。解决方案:限制单次请求只处理一个意图。如果检测到多个意图,拆分成多个请求。或者用parallel_function_calling(OpenAI支持,但需要模型版本支持)。
# 多意图检测和拆分
def detect_multiple_intents(query):
# 简单规则:如果包含“和”、“以及”、“、”等连接词,可能有多意图
connectors = ["和", "以及", "、", "与"]
for c in connectors:
if c in query:
return True
return False
def split_intents(query):
# 简单拆分,实际项目需要更复杂的NLP
parts = query.replace("和", "|").replace("以及", "|").replace("、", "|").replace("与", "|")
return [p.strip() for p in parts.split("|") if p.strip()]
query = "查下北京天气和苹果股票"
if detect_multiple_intents(query):
intents = split_intents(query)
for intent in intents:
result = call_openai_function_calling(intent)
print(result)
进阶:Function Calling的prompt优化
经过多次迭代,我发现prompt设计比模型选择更重要。以下是我总结的最佳实践:
- description要具体:不要写“获取天气”,要写“获取指定城市和日期的天气信息,城市必须用中文,日期格式YYYY-MM-DD”。
- 参数示例:在description里加示例,如“如北京、上海、纽约”。
- 强制约束:在system prompt里加“必须使用提供的函数,不要自己编造信息”。
- temperature=0.1:减少随机性,提高确定性。
- max_tokens=500:防止模型输出过长内容,导致解析失败。
# 优化后的system prompt
optimized_system_prompt = """你是一个函数调用助手。你的唯一任务是:根据用户输入,选择合适的函数并输出参数。
规则:
1. 必须使用提供的函数,不要自己回复用户。
2. 不要编造参数,所有参数必须从用户输入中提取。
3. 如果用户输入不明确,选择最可能的函数。
4. 输出格式必须是JSON,不要包含任何其他文字。
5. 日期格式必须是YYYY-MM-DD,时间格式必须是HH:MM。
6. 城市名称必须使用中文。
可用函数:
- get_weather: 获取天气
- get_stock_price: 获取股票价格
- create_calendar_event: 创建日历事件"""
总结:选型建议
根据我的实战经验:
- 预算充足、追求准确率:选Claude 3.5 Sonnet,参数解析最准,翻车率最低。
- 需要高并发、低成本:选本地Qwen2.5-7B,配合vLLM部署,延迟低,成本几乎为零。但需要做好JSON解析和重试逻辑。
- 快速原型、生态成熟:选OpenAI GPT-4o,SDK最完善,文档最全。
- 多意图场景:所有模型都表现差,建议拆分成单意图请求。
最后,无论选哪个模型,prompt设计都是关键。花时间优化description和system prompt,比换模型更有效。