一、从一次深夜调试说起
上周三凌晨两点,我被一个奇怪的bug卡住了:我的AI助手在回答“查一下北京明天天气”时,明明调用了天气API,返回的数据格式也正确,但最终回复给用户的却是一段混乱的JSON片段。控制台日志显示函数确实执行成功了,但结果没有被正确“翻译”成自然语言。
这个坑让我重新审视了所谓“Function Calling”这个听起来很简单的概念——它远不止是让AI调用函数那么简单,而是连接大语言模型与现实世界的关键桥梁。今天我们就来拆解这个核心模块。
二、Function Calling到底是什么?
很多人第一次接触这个概念,会简单理解为“AI调用外部函数”。这个理解只对了一半。更准确地说,Function Calling是一套让大模型理解工具能力、选择合适工具、格式化调用请求、解析工具返回结果的完整协议。
核心矛盾在于:大模型生活在文本世界里,而外部工具(API、数据库、本地函数)生活在结构化数据世界里。Function Calling就是这两个世界之间的翻译官。
三、底层原理:描述、决策与解析
3.1 工具描述:让AI知道你能做什么
大模型不是神仙,你得明确告诉它你有什么工具。OpenAI的function calling格式现在几乎是行业标准了:
tools = [
{
"type": "function",
"function": {
"name": "get_current_weather",
"description": "获取指定城市的当前天气情况", # 这里要写清楚,AI真的会读这个
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "城市名称,例如:北京、上海"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "温度单位"
}
},
"required": ["location"] # 必填参数必须标清楚
}
}
}
]
写description时有个经验:想象你在教一个完全不懂业务的新人。不要说“获取天气”,要说“获取指定城市的当前温度、湿度、天气状况和风力信息”。越具体,AI判断越准。
3.2 决策时刻:AI如何选择工具?
当你把用户问题“北京热吗?”和工具列表一起发给大模型时,内部发生了两件事:
第一,模型会评估是否需要调用工具。如果用户说“你好”,它肯定不会调用天气接口。这个判断基于对话上下文和工具描述。
第二,如果需要调用,模型会进行工具匹配。它会把用户模糊的自然语言转换成结构化的参数。比如“北京热吗?”可能被解析为{"location": "北京", "unit": "celsius"}。注意,这里的“热”是主观描述,但模型知道这对应温度查询。
3.3 关键环节:执行与结果解析
这是最容易出问题的地方。模型返回的只是一个调用请求,真正的执行发生在你的代码里:
# 模型返回的调用请求
tool_call = {
"name": "get_current_weather",
"arguments": '{"location": "北京", "unit": "celsius"}'
}
# 你的执行代码
import json
def handle_tool_call(tool_call):
func_name = tool_call["name"]
args = json.loads(tool_call["arguments"]) # 这里一定要做异常处理,我踩过坑
if func_name == "get_current_weather":
result = get_weather_from_api(args["location"], args.get("unit", "celsius"))
# 注意:result必须是JSON-serializable的
return json.dumps(result)
执行完后,你需要把结果再次塞回给大模型,让它“消化”成自然语言。这就是我开头遇到的bug的原因——我直接把API返回的原始数据给了模型,没有做格式整理。
四、实现细节:那些容易踩的坑
4.1 参数验证别偷懒
模型生成的参数不一定总是有效的。特别是用户输入模糊时:
# 坏例子:直接相信AI给的参数
city = args["location"]
call_api(city)
# 好例子:加一层清洗
city = args["location"].strip().replace("市", "") # 处理“北京市”这种情况
if city not in valid_cities:
return "抱歉,暂不支持该城市"
4.2 处理“不需要工具”的情况
不是每次对话都需要调用工具。你的代码要能处理两种响应:
response = client.chat.completions.create(
model="gpt-4",
messages=messages,
tools=tools,
tool_choice="auto" # 让模型自己决定
)
if response.choices[0].message.tool_calls:
# 处理工具调用
for tool_call in response.choices[0].message.tool_calls:
handle_tool_call(tool_call)
else:
# 直接返回文本回复
answer = response.choices[0].message.content
send_to_user(answer)
4.3 上下文管理是灵魂
工具调用不是一次性的。你需要把每次调用和结果都追加到对话历史中:
messages.append(response.choices[0].message) # 保存AI的请求
# 执行工具后,把结果也加进去
tool_result_message = {
"role": "tool",
"content": tool_execution_result,
"tool_call_id": tool_call.id # 这个id很重要,要对应上
}
messages.append(tool_result_message)
# 然后再次调用模型,让它基于结果生成回复
如果忘了加tool_call_id,模型就不知道哪个结果对应哪个请求,会直接混乱。
五、个人经验:几个非教科书建议
第一,工具描述要“过度注释”。不要假设模型知道常识。如果你有个“查询订单”工具,要在description里写清楚能查多久内的订单、需要哪些标识符。这比事后调参管用得多。
第二,实现时做好降级处理。网络调用可能超时,API可能返回意外格式。我的做法是给每个工具调用加一个fallback函数,当工具失败时,至少能返回一个友好的错误说明,而不是让整个对话崩溃。
第三,控制工具调用频率。有些用户会连续问几十个问题,每个都触发工具调用,成本扛不住。我通常会在session层面加个计数器,或者对简单问题直接走模型的内部知识。
第四,测试时多用边界案例。问“天气怎么样?”(没城市)、“查一下纽约伦敦东京的天气”(多城市)、“今天热还是昨天热”(比较查询)。这些边缘输入最能暴露设计问题。
最后记住,Function Calling不是终点。它只是让AI开始接触现实世界的第一步。真正的挑战在于如何设计一套工具生态,让AI能像人一样,灵活组合使用各种能力来解决复杂问题——这才是Agent架构的精髓。
(下一篇我们讲《工具编排与工作流引擎》,看看多个工具怎么串联起来完成复杂任务。)
432

被折叠的 条评论
为什么被折叠?



