# LLM Function Calling 实战指南:让 AI 模型调用外部工具
## 背景介绍
大语言模型的能力边界在哪里?早期的 GPT 模型只能生成文本,无法获取实时信息,也无法执行具体操作。但现在,Function Calling 功能让 LLM 获得了”动手能力”——它可以根据用户需求调用外部 API、查询数据库、执行计算,甚至操作其他系统。
Function Calling 首次大规模出现在 GPT-4 Turbo 和 Claude 3 系列模型中,现在已经成为主流 LLM 的标准配置。这项技术的核心价值在于:模型不再是一个封闭的文本生成器,而是一个可以与真实世界交互的智能代理。
然而,实际实现 Function Calling 并不是一件简单的事情。开发者经常遇到以下问题:模型生成的函数调用参数格式错误、复杂参数类型(如嵌套对象、数组)处理失败、没有合适的调试工具、不知道如何设计有效的工具描述。这些问题极大地限制了 Function Calling 的实用性。
本文将通过一个完整的实战案例,带你从零实现 Function Calling 功能。我们会构建一个简化的”天气助手”,让用户询问天气时,模型自动调用外部天气 API 获取数据。
## 问题描述
在使用 LLM 的 Function Calling 功能时,开发者通常会遇到以下几类问题:
**第一类是参数格式问题。** 模型生成的 JSON 参数经常缺少必要的引号、逗号,或者使用了错误的数据类型。例如,本应该是字符串的字段被错误地识别为数字,或者数组被错误地拆分 为多个独立字段。
**第二类是复杂类型处理问题。** 当工具函数包含嵌套参数(如对象数组、可选字段、枚举类型)时,模型的表现会明显下降。它可能无法正确理解参数之间的层级关系,或者遗漏某些可选字段。
**第三类是工具描述设计问题。** 怎么样的描述才能让模型准确理解函数的用途和参数格式?描述太简单会导致模型乱调用,描述太复杂又会让模型无所适从。
**第四类是调试和监控问题。** Function Calling 涉及多个步骤的交互:用户输入 → 模型识别意图 → 生成调用参数 → 执行函数 → 返回结果 → 生成最终回复。如何追踪这个流程中的每一个环节?
## 详细步骤
### 第一步:设计工具函数
首先,我们需要定义一个外部工具。在这个例子中,我们设计一个简化的天气查询工具:
def get_weather(city: str, date: str = "today") -> dict:
"""
查询指定城市的天气信息
参数:
city: 城市名称,如 "北京"、"上海"
date: 日期,可选值为 "today"、"tomorrow"、或具体日期 "2024-01-15"
返回:
包含天气状况、温度、湿度等信息的字典
"""
# 模拟的天气数据
weather_data = {
"北京": {"today": {"condition": "晴", "temperature": "15°C", "humidity": "45%"}},
"上海": {"today": {"condition": "多云", "temperature": "18°C", "humidity": "62%"}}
}
city_data = weather_data.get(city, {})
date_data = city_data.get(date, city_data.get("today", {}))
return {
"city": city,
"date": date,
"condition": date_data.get("condition", "未知"),
"temperature": date_data.get("temperature", "N/A"),
"humidity": date_data.get("humidity", "N/A")
}
这个工具函数的设计遵循以下原则:参数类型尽量简单、使用默认值减少必填字段、返回结构化的字典对象。在实际开发中,你可能需要调用真实的天气 API(如 OpenWeatherMap),但核心模式是相同的。
### 第二步:定义工具描述
工具描述是模型理解函数用途的关键。以下是我们为天气工具定义的描述:
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市和日期的天气状况。当用户询问天气、温度、湿度等信息时使用。",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如 北京、上海、杭州"
},
"date": {
"type": "string",
"description": "查询的日期,可选 'today'、'tomorrow' 或具体日期如 '2024-01-15'",
"default": "today"
}
},
"required": ["city"]
}
}
}
描述设计的关键点包括:function 的 name 必须与实际函数名称一致;description 要说明函数的适用场景而不是仅仅重复参数名称;parameters 中的每个字段都需要有清晰的 description;必填字段放在 required 数组中。
### 第三步:实现调用逻辑
现在我们来实现完整的 Function Calling 流程。这里使用 OpenAI 的 API 格式,你可以根据使用的模型进行相应调整:
import json
import openai
from typing import Any, Dict, List, Optional
# 配置你的 API Key
openai.api_key = "your-api-key-here"
# 定义可用工具
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市和日期的天气状况。当用户询问天气、温度、湿度等信息时使用。",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如 北京、上海、杭州"
},
"date": {
"type": "string",
"description": "查询的日期,可选 'today'、'tomorrow' 或具体日期",
"default": "today"
}
},
"required": ["city"]
}
}
}
]
# 函数实现映射
function_map = {
"get_weather": get_weather
}
def chat_with_functions(messages: List[Dict]) -> str:
"""
处理带有函数调用的对话
"""
# 第一次调用:让模型决定是否需要调用函数
response = openai.ChatCompletion.create(
model="gpt-4-turbo",
messages=messages,
tools=tools,
tool_choice="auto"
)
response_message = response["choices"][0]["message"]
messages.append(response_message)
# 检查模型是否请求了函数调用
tool_calls = response_message.get("tool_calls")
if tool_calls:
# 处理每个函数调用
for tool_call in tool_calls:
function_name = tool_call["function"]["name"]
function_args = json.loads(tool_call["function"]["arguments"])
# 执行实际的函数
function_response = function_map[function_name](**function_args)
# 将函数结果添加到对话中
messages.append({
"tool_call_id": tool_call["id"],
"role": "tool",
"name": function_name,
"content": json.dumps(function_response, ensure_ascii=False)
})
# 第二次调用:让模型根据函数结果生成最终回复
final_response = openai.ChatCompletion.create(
model="gpt-4-turbo",
messages=messages
)
return final_response["choices"][0]["message"]["content"]
# 如果没有函数调用,直接返回模型回复
return response_message["content"]
这段代码展示了 Function Calling 的完整流程:第一次调用时,模型分析用户输入,决定是否需要调用工具;如果需要,生成调用参数;执行函数后,将结果返回给模型;第二次调用时,模型结合函数结果生成最终回复。
### 第四步:测试与调试
让我们测试几个典型场景:
# 测试用例
test_cases = [
"北京明天天气怎么样?",
"上海今天冷吗?",
"告诉我杭州的天气"
]
for user_input in test_cases:
print(f"\n用户: {user_input}")
messages = [{"role": "user", "content": user_input}]
result = chat_with_functions(messages)
print(f"助手: {result}")
## 运行结果
执行测试后,我们得到以下输出:
用户: 北京明天天气怎么样?
助手: 根据查询结果,明天北京天气为晴,气温 15°C,湿度 45%。
用户: 上海今天冷吗?
助手: 上海今天多云,气温 18°C,湿度 62%。总体来说体感应该比较舒适,不算冷。
用户: 告诉我杭州的天气
助手: 抱歉,我没有杭州市的天气数据。不过可以使用真实的天气 API(如 OpenWeatherMap)来获取更多城市的数据。
从结果可以看出几个要点:模型成功识别了”明天”这个日期参数,正确调用了 get_weather(city=”北京”, date=”tomorrow”);模型不仅返回了原始数据,还理解了”冷吗”这个问法,给出了更人性化的回复��最��一个测试用例展示了工具的边界——当城市不在预设数据中时,模型能够给出合理的回复而不是胡乱回答。
## 总结
Function Calling 这项技术为 LLM 打开了一扇通往外部世界的大门。通过本文的实战案例,我们总结了以下关键点:
**在工具设计层面**,函数签名要尽量简单,避免过于复杂的参数类型;必填参数尽量少,给可选参数设置合理的默认值;返回结构化的 JSON 数据,方便模型解析。
**在描述设计层面**,description 要说明函数的适用场景,而不是简单重复参数名称;每个参数的 description 都要写清楚具体的值格式和示例;required 数组只包含真正的必填参数。
**在实现层面**,Function Calling 通常需要两次 API 调用:第一次让模型决定是否调用函数,第二次让模型结合函数结果生成回复;要注意处理函数执行失败的情况,返回有意义的错误信息;保持函数名称和工具描述中的名称一致。
**在调试层面**,保存完整的 message 历史可以帮助你追踪问题;打印 intermediate steps(中间步骤)可以帮助你理解模型的决策过程;在生产环境中,要做好日志记录和监控。
Function Calling 还在快速发展中。现在已经有一些框架(如 LangChain、LlamaIndex)提供了更抽象的 Function Calling 能力封装。对于复杂的应用场景,建议直接使用这些框架,它们处理好了很多边界情况。随着模型的进一步改进,Function Calling 将会更加智能和可靠,成为 AI Agent 系统的核心组件。