函数调用解决了什么问题
用LLM做个聊天机器人很简单,但你试试让它查个天气、算个账、查个库存,马上就露馅。模型会一本正经地编数据,或者干脆说”我无法访问外部数据”。Function Calling就是解决这个问题的。
原理不复杂。你提前告诉模型有哪些工具可以用,模型收到用户问题后判断要不要调工具,如果要调就返回函数名和参数。你的代码执行这个函数,把结果喂回给模型,模型再组织成自然语言回答。整个过程分两轮API调用。
OpenAI在2023年6月推出这个功能,现在Claude、Gemini、Qwen等主流模型都支持类似机制。这篇文章用OpenAI的API做演示,但思路通用于所有支持function calling的模型。
完整代码实现
下面是一个可以运行的最小实现。你只需要装openai库,配好API key就能跑。
先定义工具。这一步最关键,模型全靠你的描述来决定什么时候调什么工具:
import openai
import json
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的当前天气信息",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如:北京、上海、深圳"
}
},
"required": ["city"]
}
}
},
{
"type": "function",
"function": {
"name": "calculate",
"description": "执行数学计算,支持加减乘除和括号",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "数学表达式,如:125 * 8 + 30"
}
},
"required": ["expression"]
}
}
}
]
description字段别偷懒。写清楚函数做什么、什么时候该用,模型才能准确判断。很多人工具定义写得太模糊,模型该调的时候不调,不该调的时候乱调,问题基本都出在这。
然后是工具的实际实现:
def get_weather(city):
# 实际项目替换为真实天气API
weather_data = {
"北京": {"temp": 15, "condition": "晴", "humidity": 45},
"上海": {"temp": 22, "condition": "多云", "humidity": 68},
"深圳": {"temp": 28, "condition": "阵雨", "humidity": 82}
}
return json.dumps(
weather_data.get(city, {"error": "未找到该城市天气数据"}),
ensure_ascii=False
)
def calculate(expression):
try:
# 生产环境请用ast.literal_eval或专门的表达式解析库
result = eval(expression)
return json.dumps({"result": result})
except Exception as e:
return json.dumps({"error": str(e)})
最后是对话主循环。这是核心逻辑:
def run_conversation(user_input):
messages = [{"role": "user", "content": user_input}]
# 第一轮:模型决定是否调用工具
response = openai.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=tools,
tool_choice="auto"
)
message = response.choices[0].message
messages.append(message)
# 模型决定调用工具
if message.tool_calls:
for tool_call in message.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
# 执行对应的函数
if func_name == "get_weather":
result = get_weather(**func_args)
elif func_name == "calculate":
result = calculate(**func_args)
else:
result = json.dumps({"error": "未知函数"})
# 把结果返回给模型
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result
})
# 第二轮:模型根据工具结果生成最终回复
final_response = openai.chat.completions.create(
model="gpt-4o-mini",
messages=messages
)
return final_response.choices[0].message.content
return message.content
测试一下:
print(run_conversation("北京今天天气怎么样?"))
# 输出:北京今天晴,气温15度,湿度45%。
print(run_conversation("帮我算一下 125 * 8 + 30"))
# 输出:125 * 8 + 30 = 1030。
print(run_conversation("上海和深圳哪个更热?"))
# 输出:深圳更热。深圳当前28度有阵雨,上海22度多云。
第三个例子会自动调用两次get_weather,分别查上海和深圳的数据,然后对比给出结论。模型自己决定了调用顺序。
4个容易踩的坑
第一个坑:description写得太短。 之前说过,但值得再说一次。如果你的工具description只写了”查询天气”,模型可能在用户说”北京适合穿什么”的时候不知道该调天气工具。改成”获取指定城市的当前天气信息,包括温度、天气状况和湿度”就好多了。
第二个坑:参数类型不匹配。 模型偶尔会把数字传成字符串,或者把数组传成单个值。在函数实现里加类型检查和默认值处理。比如city参数如果不是字符串就str()一下。
第三个坑:忘记处理多工具调用。 模型可能在一个回复里同时调用多个工具(像第三个测试例子那样)。你的循环必须处理tool_calls列表里的每一个调用,不能只取第一个。
第四个坑:tool_call_id搞混。 每个tool_call都有唯一id,返回结果时必须对应正确的id。如果你在循环里把id搞串了,模型会把天气数据当成计算结果来解读,输出会很离谱。
成本控制建议
function calling的两轮调用意味着token消耗翻倍。第一轮发出去的消息(包括工具定义)会被算作输入token,第二轮又要带上完整对话历史加上工具返回的结果。工具定义本身也占token,一般200到500 token。
据OpenAI官方定价,gpt-4o-mini的输入token价格是$0.15/百万,输出$0.60/百万。一次普通的function calling对话大概消耗1500到3000 token,算下来不到$0.001。但如果你的应用有并发用户,成本会快速累积。
几个省钱的办法。工具数量控制在3到5个,别一次性塞20个工具进去。不需要工具的对话就不传tools参数,模型不会尝试调用。如果用户问题很明显不需要工具(比如”你好”),可以先做个简单分类跳过function calling流程。
用function calling做AI Agent是当前最实用的路径。比起写一堆prompt让模型输出特定格式再正则解析,function calling的结构化输出靠谱得多。把这个代码跑起来,加上你自己的业务工具,一个能用的AI助手就有了雏形。
