去年帮一个客户做智能客服项目的时候,我第一次真正体会到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演进详解
我经历了整个演进过程,简单梳理一下关键节点:
- 2023年6月:首次发布,支持在请求中声明functions,模型返回function_call,开发者手动执行并回传结果
- 2023年11月:参数名从functions改为tools,格式从"function"对象改为"function"嵌套在"tool"对象中,同时支持多个工具并行调用
- 2024年7月:gpt-4o原生支持并行函数调用(Parallel Function Calling),一次响应中可以同时请求调用多个工具,大幅减少多轮请求延迟
- 2025年初:tool_choice参数全面成熟,新增"required"选项强制模型必须调用至少一个工具
- 2026年:structured outputs与function calling深度整合,工具参数可以严格按JSON Schema校验输出
关于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数组。
真实案例:电商智能客服的Function Calling实践
说个我亲身经历的项目。去年底给一家中型电商做智能客服,他们每天的客服工单大概3000多,人工处理压力很大。我们用Function Calling方案接入了三个内部API:订单查询、退款处理、物流追踪。
工具定义大概是这样的:
- query_order:根据订单号或手机号查询订单状态、商品信息、收货地址
- process_refund:发起退款申请,自动判断是否符合退款政策(购买时间、商品状态等)
- track_logistics:输入订单号或快递单号,返回物流节点信息
上线跑了三个月的数据: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的时候不考虑、上生产就翻车的地方。几个必须处理的异常:
- 函数执行超时:给每个函数调用加timeout,超过5秒直接返回错误信息给模型,让模型告诉用户"查询超时,请稍后重试"
- 参数校验失败:模型传过来的参数不一定合法,比如日期格式错误、城市名不存在。在函数入口做校验,返回友好的错误信息
- 外部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的解析比你想的严格。几个高频问题:
- enum值写了中文但没加引号:enum必须是字符串数组,["celsius", "fahrenheit"],不能写错类型
- required字段漏填:如果某个参数是必须的,一定要在required数组里声明,否则模型可能不传
- description太模糊:比如参数名叫"date",description写"日期",模型不知道要什么格式。应该写"出发日期,格式为YYYY-MM-DD"
- 嵌套对象结构错误:复杂参数用嵌套对象时,确保每一层都有正确的type声明
函数执行超时
如果你的工具函数涉及网络请求(调第三方API、查数据库),超时是迟早会遇到的问题。建议:
- 所有外部调用设置超时时间,建议3-5秒
- 对频繁调用的接口做本地缓存,比如天气数据缓存10分钟
- 考虑异步处理:对于耗时操作,先返回"正在查询",结果通过回调或轮询获取
循环调用防护
这个坑比较隐蔽。模型有时候会陷入循环:调工具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写得不好,导致模型频繁选错工具,最后开发团队还以为是模型能力不行。其实很多时候是输入端的问题。
有什么问题欢迎交流,这块踩过的坑确实不少,希望能帮大家少走一些弯路。