Function Calling实战:从翻车到稳定调用
发布日期: 2026/07/20 阅读总量: 1
Function Calling实战:从翻车到稳定调用

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,比换模型更有效。