Function Calling 实战教程:从踩坑到封装,附完整可运行代码

8 阅读1分钟

上周接了个需求,要做一个能查天气、查快递、查数据库的智能客服。一开始想的是用正则匹配用户意图,写了一堆 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.499%支持,稳定很好首选
Claude Opus 4.698%支持很好复杂逻辑场景强
Gemini 3.095%支持偶尔类型偏差性价比不错
DeepSeek V390%基本支持一般简单场景够用

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 参数,省了不少事。