广告位预留 (728x90)

AI API Function Calling与工具调用完全指南:从入门到实战

去年帮一个客户做智能客服项目的时候,我第一次真正体会到Function Calling的威力。之前用纯提示词工程搞了两个月,效果始终上不去,换了Function Calling方案后,准确率直接从60%拉到了92%。今天就把这段时间踩过的坑和积累的经验整理出来,希望能帮到正在做AI Agent开发的朋友。

什么是Function Calling,为什么它是AI Agent的基础能力

说白了,Function Calling(也叫Tool Use)就是让大模型能够"调用外部工具"。光靠模型自身的知识是不够的,它不知道你数据库里有什么订单、不知道今天的天气、不知道航班动态。Function Calling解决的就是这个问题——模型判断需要调用哪个工具,你的代码负责实际执行,然后把结果喂回给模型,模型再基于真实数据生成回答。

这就是AI Agent的核心循环:感知 - 决策 - 行动 - 观察。模型感知用户意图,决策要调用什么工具,代码执行行动,结果作为观察反馈给模型。没有Function Calling,AI Agent就是个只会说话的聊天机器人,没法跟真实世界交互。

OpenAI在2023年6月首次推出Function Calling功能,当时只支持gpt-3.5-turbo和gpt-4-0613两个模型。到2024年7月,随着gpt-4o的发布,并行函数调用成为标配。到2026年,tool_choice参数已经非常成熟,支持"auto"、"required"、"none"以及指定具体函数等多种模式,基本上覆盖了所有业务场景。

OpenAI Function Calling演进详解

我经历了整个演进过程,简单梳理一下关键节点:

关于tool_choice参数,这是很多人容易搞混的地方,我单独说一下:

# tool_choice 各选项含义
# "auto"(默认):模型自行判断是否需要调用工具
# "none":强制不调用任何工具,纯文本回复
# "required":强制必须调用至少一个工具
# {"type": "function", "function": {"name": "get_weather"}}:强制调用指定函数

实际开发中,"auto"用得最多。"required"适合你确定用户请求必须走工具链的场景,比如"查一下北京明天的天气",这种不查就没法回答的情况。"none"则适合你想让模型基于已有上下文做总结或推理的时候。

Anthropic Claude的Tool Use实现方式

Anthropic那边叫Tool Use,思路跟OpenAI类似但API设计上有一些差异。我两个平台都用过,说说实际体验上的区别:

对比维度 OpenAI Function Calling Anthropic Claude Tool Use
参数定义 tools数组,每个tool包含type和function tools数组,每个tool包含name、description、input_schema
Schema格式 JSON Schema放在function.parameters中 JSON Schema直接作为input_schema
控制选项 tool_choice参数(auto/none/required/指定函数) tool_choice参数(auto/any/tool/指定名称)
结果回传 role="tool",content为字符串 role="user",content为tool_result对象
并行调用 gpt-4o原生支持,返回多个tool_calls Claude 3.5+支持,返回多个tool_use content block
强制调用 tool_choice="required" tool_choice="any"

说实话,两个平台的差异主要在API格式上,核心逻辑是一样的。如果你要做跨平台适配,建议写一层抽象,把工具定义和调用流程统一封装。后面我会给一个双版本的代码示例。

实战案例:构建"天气查询+航班搜索"智能助手

光说理论没意思,直接上代码。我们构建一个能查天气和搜航班的助手,这是Function Calling最经典的入门场景。

第一步:定义Tools Schema

不管用哪个平台,第一步都是告诉模型你有哪些工具可以用。JSON Schema定义要尽量清晰,description写得好不好直接影响模型能不能正确选择工具。

# tools_schema.json
{
  "openai_tools": [
    {
      "type": "function",
      "function": {
        "name": "get_weather",
        "description": "查询指定城市的实时天气信息,包括温度、湿度、天气状况和未来3天预报",
        "parameters": {
          "type": "object",
          "properties": {
            "city": {
              "type": "string",
              "description": "城市名称,如'北京'、'上海'"
            },
            "unit": {
              "type": "string",
              "enum": ["celsius", "fahrenheit"],
              "description": "温度单位,默认摄氏度"
            }
          },
          "required": ["city"]
        }
      }
    },
    {
      "type": "function",
      "function": {
        "name": "search_flights",
        "description": "搜索两个城市之间的航班信息,包括航班号、出发时间、价格和余票数量",
        "parameters": {
          "type": "object",
          "properties": {
            "origin": {
              "type": "string",
              "description": "出发城市"
            },
            "destination": {
              "type": "string",
              "description": "目的城市"
            },
            "date": {
              "type": "string",
              "description": "出发日期,格式YYYY-MM-DD"
            },
            "passengers": {
              "type": "integer",
              "description": "乘客人数,默认1"
            }
          },
          "required": ["origin", "destination", "date"]
        }
      }
    }
  ]
}
经验分享

description字段是关键。模型靠这个判断什么时候该用哪个工具。我见过很多人写得特别简略,比如"查天气",结果模型在用户说"今天心情不好"的时候也去调天气接口。写得越具体、越明确,误调用率越低。

第二步:OpenAI版本Python代码

import openai
import json
from openai import OpenAI

client = OpenAI(api_key="your-api-key")

# 定义工具
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": "search_flights",
            "description": "搜索航班信息",
            "parameters": {
                "type": "object",
                "properties": {
                    "origin": {"type": "string", "description": "出发城市"},
                    "destination": {"type": "string", "description": "目的城市"},
                    "date": {"type": "string", "description": "出发日期 YYYY-MM-DD"},
                    "passengers": {"type": "integer", "description": "乘客人数"}
                },
                "required": ["origin", "destination", "date"]
            }
        }
    }
]

# 模拟工具函数
def get_weather(city, unit="celsius"):
    # 实际项目中这里调用真实天气API
    return json.dumps({
        "city": city,
        "temperature": 25 if unit == "celsius" else 77,
        "humidity": 65,
        "condition": "晴转多云",
        "forecast": ["晴", "多云", "小雨"]
    })

def search_flights(origin, destination, date, passengers=1):
    # 实际项目中这里调用真实航班API
    return json.dumps({
        "flights": [
            {"flight_no": "CA1234", "time": "08:00-10:30", "price": 890, "seats": 12},
            {"flight_no": "MU5678", "time": "14:00-16:20", "price": 650, "seats": 5}
        ]
    })

# 工具路由映射
available_functions = {
    "get_weather": get_weather,
    "search_flights": search_flights,
}

def run_assistant(user_message):
    messages = [
        {"role": "system", "content": "你是一个智能助手,可以查询天气和搜索航班。请用中文回答。"},
        {"role": "user", "content": user_message}
    ]

    # 第一轮:模型判断是否需要调用工具
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=messages,
        tools=tools,
        tool_choice="auto"
    )

    message = response.choices[0].message

    # 如果模型决定调用工具
    if message.tool_calls:
        messages.append(message)

        for tool_call in message.tool_calls:
            func_name = tool_call.function.name
            func_args = json.loads(tool_call.function.arguments)

            # 执行对应的函数
            func = available_functions[func_name]
            result = func(**func_args)

            # 把结果回传给模型
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": result
            })

        # 第二轮:模型基于工具结果生成最终回答
        final = client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            tools=tools
        )
        return final.choices[0].message.content

    return message.content

# 测试
print(run_assistant("帮我查一下上海明天的天气,再看看上海到北京5月20号有什么航班"))

第三步:Claude版本Python代码

import anthropic
import json

client = anthropic.Anthropic(api_key="your-api-key")

# Claude的工具定义格式不同
tools = [
    {
        "name": "get_weather",
        "description": "查询指定城市的实时天气信息",
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {"type": "string", "description": "城市名称"},
                "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
            },
            "required": ["city"]
        }
    },
    {
        "name": "search_flights",
        "description": "搜索航班信息",
        "input_schema": {
            "type": "object",
            "properties": {
                "origin": {"type": "string", "description": "出发城市"},
                "destination": {"type": "string", "description": "目的城市"},
                "date": {"type": "string", "description": "出发日期 YYYY-MM-DD"},
                "passengers": {"type": "integer", "description": "乘客人数"}
            },
            "required": ["origin", "destination", "date"]
        }
    }
]

def get_weather(city, unit="celsius"):
    return json.dumps({
        "city": city, "temperature": 25,
        "humidity": 65, "condition": "晴转多云"
    })

def search_flights(origin, destination, date, passengers=1):
    return json.dumps({
        "flights": [
            {"flight_no": "CA1234", "time": "08:00-10:30", "price": 890},
            {"flight_no": "MU5678", "time": "14:00-16:20", "price": 650}
        ]
    })

available_functions = {
    "get_weather": get_weather,
    "search_flights": search_flights,
}

def run_assistant(user_message):
    messages = [{"role": "user", "content": user_message}]

    response = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=1024,
        system="你是一个智能助手,可以查询天气和搜索航班。请用中文回答。",
        tools=tools,
        tool_choice={"type": "auto"},
        messages=messages
    )

    # 处理工具调用
    while response.stop_reason == "tool_use":
        # 收集所有工具调用结果
        tool_results = []
        for content_block in response.content:
            if content_block.type == "tool_use":
                func = available_functions[content_block.name]
                result = func(**content_block.input)
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": content_block.id,
                    "content": result
                })

        # 构建新的messages
        messages.append({"role": "assistant", "content": response.content})
        messages.append({"role": "user", "content": tool_results})

        response = client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=1024,
            system="你是一个智能助手,可以查询天气和搜索航班。请用中文回答。",
            tools=tools,
            tool_choice={"type": "auto"},
            messages=messages
        )

    return response.content[0].text

# 测试
print(run_assistant("帮我查一下上海明天的天气,再看看上海到北京5月20号有什么航班"))

两个版本的核心流程是一样的:定义工具 - 发送请求 - 检查是否有工具调用 - 执行函数 - 回传结果 - 获取最终回答。区别主要在API格式上,Claude用content block的方式组织工具调用,OpenAI用tool_calls数组。

广告位预留 (336x280)

真实案例:电商智能客服的Function Calling实践

说个我亲身经历的项目。去年底给一家中型电商做智能客服,他们每天的客服工单大概3000多,人工处理压力很大。我们用Function Calling方案接入了三个内部API:订单查询、退款处理、物流追踪。

工具定义大概是这样的:

上线跑了三个月的数据:67%的常见咨询(查快递到哪了、退款进度、订单信息确认)被AI直接处理,人工客服只需要处理那些涉及投诉、特殊售后和需要人工判断的复杂case。客服团队从原来的40人缩减到28人,但服务质量评分反而从4.2提升到了4.6——因为AI不会带着情绪上班,也不会因为忙了一天就敷衍了事。

这里有个关键设计:退款接口我们做了权限分级。AI只能处理金额在500元以下、购买不超过30天的标准退款申请。超出范围的自动转人工。这个"知道什么时候该放手"的能力,比"能处理多少问题"更重要。

高级技巧:并行调用、工具管理与错误处理

并行函数调用

这是gpt-4o和Claude 3.5之后才有的能力,效果非常明显。比如用户问"北京和上海的天气分别怎么样",模型会同时返回两个get_weather调用,你的代码并行执行,然后把两个结果一起回传。比串行调用快了一倍。

代码层面的处理也不复杂,关键是检查返回中是否有多个tool_calls:

# OpenAI并行调用处理
if message.tool_calls:
    messages.append(message)
    for tool_call in message.tool_calls:
        # 可以用线程池并行执行
        func = available_functions[tool_call.function.name]
        args = json.loads(tool_call.function.arguments)
        result = func(**args)
        messages.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": result
        })

多轮对话中的工具管理

多轮对话场景下有个容易踩的坑:工具定义要每轮都传,但不是每轮都需要调用工具。如果用户在第二轮只是说"好的谢谢",模型不应该再去调任何接口。这时候tool_choice="auto"就很重要,让模型自己判断。

另外,多轮对话中messages会越来越长,要注意token消耗。我的做法是保留最近5轮对话,更早的做摘要压缩。工具调用的中间结果(function返回值)也可以在最终回答生成后清理掉,只保留最终回答。

错误处理

这是很多人写demo的时候不考虑、上生产就翻车的地方。几个必须处理的异常:

  1. 函数执行超时:给每个函数调用加timeout,超过5秒直接返回错误信息给模型,让模型告诉用户"查询超时,请稍后重试"
  2. 参数校验失败:模型传过来的参数不一定合法,比如日期格式错误、城市名不存在。在函数入口做校验,返回友好的错误信息
  3. 外部API故障:try-catch包住所有外部调用,异常时返回降级信息而不是让整个流程崩溃
import time

def safe_call(func, args, timeout=5):
    try:
        start = time.time()
        result = func(**args)
        if time.time() - start > timeout:
            return json.dumps({"error": "查询超时,请稍后重试"})
        return result
    except Exception as e:
        return json.dumps({"error": f"服务暂时不可用:{str(e)}"})
重要提醒

永远不要把函数执行错误直接暴露给用户。模型收到错误信息后可以生成友好的回复,但原始的stack trace或API错误码不应该出现在最终输出中。

Function Calling vs 提示词工程:什么时候用哪个

这两个不是替代关系,而是互补的。我总结了一个简单的判断标准:

场景特征 推荐方案 原因
需要实时数据(天气、股价、库存) Function Calling 模型没有实时数据,必须通过工具获取
需要操作外部系统(下单、退款、发邮件) Function Calling 纯提示词无法执行操作
文本处理(翻译、摘要、改写) 提示词工程 模型本身能力足够,不需要外部工具
需要严格格式化输出(JSON、表格) 两者结合 提示词定义格式,Function Calling获取数据
多步推理(数学题、逻辑分析) 提示词工程 + Chain of Thought 核心是推理能力,不是数据获取
跨系统数据整合 Function Calling 需要从多个数据源获取并整合信息

简单记:凡是需要"真实数据"或"执行操作"的,用Function Calling;纯文本层面的理解和生成,用提示词工程就够了。

常见问题与排查指南

JSON Schema定义错误

这是最常见的坑。模型对JSON Schema的解析比你想的严格。几个高频问题:

函数执行超时

如果你的工具函数涉及网络请求(调第三方API、查数据库),超时是迟早会遇到的问题。建议:

  1. 所有外部调用设置超时时间,建议3-5秒
  2. 对频繁调用的接口做本地缓存,比如天气数据缓存10分钟
  3. 考虑异步处理:对于耗时操作,先返回"正在查询",结果通过回调或轮询获取

循环调用防护

这个坑比较隐蔽。模型有时候会陷入循环:调工具A -> 得到结果 -> 又调工具A -> 又得到类似结果...无限循环下去,token烧完为止。

解决方案很简单:设置最大工具调用轮次。一般2-3轮就足够了,超过就直接让模型基于已有信息回答。

MAX_TOOL_ROUNDS = 3

def run_assistant_safe(user_message):
    messages = [{"role": "user", "content": user_message}]
    rounds = 0

    while rounds < MAX_TOOL_ROUNDS:
        response = client.chat.completions.create(
            model="gpt-4o", messages=messages, tools=tools
        )
        message = response.choices[0].message

        if not message.tool_calls:
            return message.content

        messages.append(message)
        for tool_call in message.tool_calls:
            # ... 执行并回传结果 ...
            pass
        rounds += 1

    # 超过轮次限制,强制文本回复
    messages.append({"role": "user", "content": "请基于已有信息回答用户问题。"})
    final = client.chat.completions.create(
        model="gpt-4o", messages=messages
    )
    return final.choices[0].message.content
补充建议

除了最大轮次限制,还可以在system prompt里明确告诉模型"每个工具最多调用一次,不要重复调用相同工具"。这能从源头上减少大部分循环调用的情况。

写在最后

Function Calling是AI从"聊天机器人"进化到"智能助手"的关键一步。如果你在做AI Agent开发或者想给产品加上AI能力,这项技术基本是绕不过去的。

我的建议是先从一个简单的场景开始——比如查天气或者查数据库——把整个流程跑通,然后再逐步加工具、加复杂度。不要一上来就搞十几个工具的复杂系统,调试起来会非常痛苦。

另外,工具的description真的值得花时间打磨。我见过太多项目因为description写得不好,导致模型频繁选错工具,最后开发团队还以为是模型能力不行。其实很多时候是输入端的问题。

有什么问题欢迎交流,这块踩过的坑确实不少,希望能帮大家少走一些弯路。

广告位预留 (728x90)