Function-Calling详解-LLM与外部世界的接口
LLM 本身是一个纯文本的输入输出系统,无法执行代码、查数据库、调 API。Function Calling(函数调用,也叫工具调用 Tool Use)是连接 LLM 和外部世界的机制:LLM 决定"需要调用哪个工具、传什么参数",开发者负责实际执行,然后把结果返回给 LLM。
Function Calling 详解:LLM 与外部世界的接口
LLM 本身是一个纯文本的输入输出系统,无法执行代码、查数据库、调 API。Function Calling(函数调用,也叫工具调用 Tool Use)是连接 LLM 和外部世界的机制:LLM 决定"需要调用哪个工具、传什么参数",开发者负责实际执行,然后把结果返回给 LLM。
1.1 为什么需要 Function Calling:一个具体场景
Function Calling 完整工作流程——用户请求经LLM判断后触发工具调用,结果整合后返回最终答案
核心要点:LLM不执行代码,只生成调用指令 | 应用层负责实际执行 | 结果回注LLM生成最终答案
假设你在构建一个客服机器人,用户问:"我的订单 ORD-20240315 现在到哪了?"
如果没有 Function Calling,LLM 只能回答:"您好,关于您的订单状态,建议您在订单详情页查询……"——它根本不知道这个订单的实际状态,只能说废话。
有了 Function Calling,流程变成:
- 用户问题发给 LLM,同时告诉它"你有一个查询订单状态的工具"
- LLM 判断:这个问题需要查实际数据,调用
get_order_status(order_id="ORD-20240315") - 开发者代码收到这个请求,真正查数据库
- 把查询结果返回给 LLM
- LLM 根据真实数据回答:"您的订单 ORD-20240315 已于今天 10:30 发货,预计明天送达。"
这就是 Function Calling 的价值:它把 LLM 的语言理解能力和真实系统的数据/执行能力连接起来。
1.2 Function Calling 的本质设计
在有 Function Calling 之前,开发者也会用一种土办法:在 Prompt 里告诉 LLM"输出 JSON 格式,然后解析"。两者的核心差异在于:
- Prompt 拼接方案:LLM 输出纯文本,开发者用正则或 JSON 解析,格式不稳定,模型容易在 JSON 前后加解释性文字导致解析失败
- Function Calling:API 层面强制规范输出格式,专门经过训练,解析成功率接近 100%
这个分工很重要:LLM 只负责"决策",执行权始终在开发者手里。这意味着工具调用的安全边界是可控的——你可以在执行前加权限检查、参数校验,任何危险操作都可以拦截。
整个流程是多轮的:工具调用不是一次性完成的,而是模型请求 → 执行 → 返回结果 → 模型继续生成的循环。
1.3 工具定义的 JSON Schema
每个工具用 JSON Schema(一种描述 JSON 数据结构的标准规范,规定有哪些字段、每个字段是什么类型、哪些是必填项)描述其名称、功能和参数。这个描述会被发给模型,模型根据它来决定何时调用以及传什么参数。
下面是一个天气查询工具和数据库查询工具的完整定义示例:
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
# description 是 LLM 的"使用说明书",写得越清晰,调用越准确
"description": "查询指定城市的当前天气情况,包括温度、湿度、天气状况。"
"适用于用户询问天气、出行建议等场景。",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,例如:北京、上海、广州。请使用中文城市名。",
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "温度单位,celsius=摄氏度(默认),fahrenheit=华氏度",
},
},
"required": ["city"], # city 必填,unit 可选
},
},
},
{
"type": "function",
"function": {
"name": "query_order",
"description": "查询订单状态和物流信息。用户提供订单号时调用此工具。",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "订单号,格式如 'ORD-20240315001'",
},
"include_logistics": {
"type": "boolean",
"description": "是否包含物流跟踪信息,默认 true",
},
},
"required": ["order_id"],
},
},
},
]
工具描述质量直接影响调用准确率。description 字段要说清楚:这个工具能做什么、什么时候该用它、参数的含义和格式要求。参数的 description 写得越具体,模型传错参数的概率越低。
1.4 完整代码示例:使用 DeepSeek API
DeepSeek 兼容 OpenAI 的接口格式,可以直接用 openai 库调用,只需修改 base_url。
import json
import os
from openai import OpenAI
# DeepSeek API 使用 OpenAI 兼容格式
client = OpenAI(
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)
# ---- 模拟的工具实现 ----
# 实际项目中,这里替换为真实的 API 调用、数据库查询等
def get_weather(city: str, unit: str = "celsius") -> dict:
"""模拟天气查询,实际项目调用真实天气 API"""
weather_data = {
"北京": {"temp": 22, "humidity": 45, "condition": "晴天"},
"上海": {"temp": 28, "humidity": 80, "condition": "多云"},
"广州": {"temp": 32, "humidity": 90, "condition": "阵雨"},
}
data = weather_data.get(city, {"temp": 20, "humidity": 60, "condition": "未知"})
temp = data["temp"] if unit == "celsius" else round(data["temp"] * 9 / 5 + 32, 1)
return {
"city": city,
"temp": temp,
"unit": "摄氏度" if unit == "celsius" else "华氏度",
"humidity": data["humidity"],
"condition": data["condition"],
}
def query_order(order_id: str, include_logistics: bool = True) -> dict:
"""模拟订单查询,实际项目查数据库"""
orders = {
"ORD-20240315001": {
"status": "已发货",
"product": "Python 进阶指南",
"shipped_at": "2024-03-15 10:30",
"logistics": "顺丰快递 SF1234567890,预计明天送达"
},
}
order = orders.get(order_id)
if not order:
return {"error": f"未找到订单 {order_id}"}
result = {"order_id": order_id, "status": order["status"], "product": order["product"]}
if include_logistics:
result["logistics"] = order.get("logistics", "暂无物流信息")
return result
# ---- 工具执行分发函数 ----
def execute_tool(tool_name: str, tool_args: dict) -> str:
"""
根据工具名调用对应函数,统一返回 JSON 字符串
这是 Function Calling 的执行层,实际工程中在此加权限检查、参数验证、日志记录
"""
if tool_name == "get_weather":
result = get_weather(**tool_args)
elif tool_name == "query_order":
result = query_order(**tool_args)
else:
result = {"error": f"未知工具:{tool_name}"}
return json.dumps(result, ensure_ascii=False)
# ---- 工具定义 ----
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的当前天气,包括温度、湿度和天气状况。",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称,如北京、上海"},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "温度单位,默认 celsius"
},
},
"required": ["city"],
},
},
},
{
"type": "function",
"function": {
"name": "query_order",
"description": "查询订单状态和物流信息,需要用户提供订单号。",
"parameters": {
"type": "object",
"properties": {
"order_id": {"type": "string", "description": "订单号,格式如 ORD-YYYYMMDDXXX"},
"include_logistics": {"type": "boolean", "description": "是否包含物流信息,默认 true"},
},
"required": ["order_id"],
},
},
},
]
# ---- 主对话循环 ----
def chat_with_tools(user_message: str) -> str:
"""
支持工具调用的对话函数
处理多轮工具调用:一次对话中模型可能连续调用多个工具
"""
messages = [{"role": "user", "content": user_message}]
print(f"\n用户:{user_message}")
while True:
response = client.chat.completions.create(
model="deepseek-chat",
messages=messages,
tools=tools,
tool_choice="auto", # auto=模型自己决定是否调用工具
)
message = response.choices[0].message
# 如果模型没有调用工具,说明它已经有了最终答案
if not message.tool_calls:
print(f"\nAI:{message.content}")
return message.content
# 模型请求调用工具,把工具调用请求记录到消息历史
messages.append(message)
# 执行模型请求的所有工具调用
for tool_call in message.tool_calls:
tool_name = tool_call.function.name
tool_args = json.loads(tool_call.function.arguments)
print(f"\n[调用工具] {tool_name}({tool_args})")
result = execute_tool(tool_name, tool_args)
print(f"[工具结果] {result}")
# 把工具执行结果加入消息历史(必须与 tool_call.id 对应)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id, # 这个 ID 将调用请求和结果关联起来
"content": result,
})
# 循环继续:把工具结果发回给模型,让它继续生成
# 测试
chat_with_tools("北京今天天气怎么样?")
print("-" * 50)
chat_with_tools("我的订单 ORD-20240315001 现在到哪了?")
print("-" * 50)
chat_with_tools("上海天气如何?另外查一下订单 ORD-20240315001 的状态。")
预期输出:
用户:北京今天天气怎么样?
[调用工具] get_weather({'city': '北京'})
[工具结果] {"city": "北京", "temp": 22, "unit": "摄氏度", "humidity": 45, "condition": "晴天"}
AI:北京今天天气晴朗,气温 22°C,湿度 45%,非常适合户外活动。
--------------------------------------------------
用户:我的订单 ORD-20240315001 现在到哪了?
[调用工具] query_order({'order_id': 'ORD-20240315001'})
[工具结果] {"order_id": "ORD-20240315001", "status": "已发货", "product": "Python 进阶指南", "logistics": "顺丰快递 SF1234567890,预计明天送达"}
AI:您的订单 ORD-20240315001(Python 进阶指南)已发货,正通过顺丰快递(SF1234567890)配送,预计明天送达。
1.5 消息格式详解
理解消息格式是掌握 Function Calling 的关键。一次完整的工具调用对话,消息历史是这样的:
# 完整的消息历史,展示工具调用的消息结构
messages = [
# 1. 用户提问
{
"role": "user",
"content": "北京今天天气如何?"
},
# 2. 模型返回工具调用请求(注意:content 可能为 None)
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_abc123", # 唯一 ID,用于关联工具结果
"type": "function",
"function": {
"name": "get_weather",
"arguments": '{"city": "北京"}' # 注意:这是 JSON 字符串,不是 dict
}
}
]
},
# 3. 工具执行结果(role 必须是 "tool")
{
"role": "tool",
"tool_call_id": "call_abc123", # 必须和上面的 id 对应
"content": '{"city": "北京", "temp": 22, "condition": "晴天"}'
},
# 4. 模型根据工具结果生成最终回答
{
"role": "assistant",
"content": "北京今天天气晴朗,气温 22°C..."
}
]
有几个细节容易出错:
message.tool_calls存在时,message.content通常为Nonetool_call.function.arguments是 JSON 字符串,不是 dict,需要json.loads()解析- 工具结果消息的
tool_call_id必须和对应的工具调用请求id完全匹配
1.6 并行工具调用
当用户的问题涉及多个独立工具时,模型可能在一次响应中返回多个 tool_calls,可以并行执行,大幅节省总耗时:
import asyncio
async def execute_tool_async(tool_call) -> dict:
"""异步执行单个工具调用"""
tool_name = tool_call.function.name
tool_args = json.loads(tool_call.function.arguments)
# 实际场景中,这里是真正的 async IO 操作(HTTP 请求、数据库查询)
result = execute_tool(tool_name, tool_args)
return {
"role": "tool",
"tool_call_id": tool_call.id,
"content": result,
}
async def chat_with_parallel_tools(user_message: str) -> str:
"""并行执行多个工具调用,而不是串行等待"""
messages = [{"role": "user", "content": user_message}]
response = client.chat.completions.create(
model="deepseek-chat",
messages=messages,
tools=tools,
)
message = response.choices[0].message
if not message.tool_calls:
return message.content
messages.append(message)
# 并行执行所有工具调用(比串行快 N 倍)
tool_results = await asyncio.gather(
*[execute_tool_async(tc) for tc in message.tool_calls]
)
messages.extend(tool_results)
final_response = client.chat.completions.create(
model="deepseek-chat",
messages=messages,
tools=tools,
)
return final_response.choices[0].message.content
# 这个查询会触发两个工具并行调用
result = asyncio.run(
chat_with_parallel_tools("上海天气怎样?同时查一下订单 ORD-20240315001 的状态。")
)
并行调用的前提:工具之间没有依赖关系。如果工具 B 的输入依赖工具 A 的输出(比如先查询天气,再根据天气决定是否启动某个流程),则必须串行执行。
1.7 强制工具调用(tool_choice)
有时候需要强制模型使用特定工具,而不是让它自由选择:
# 强制调用特定工具
# 适用场景:用 Function Calling 做结构化数据提取,比 JSON mode 更精确
response = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content": "分析这段文本的情感:今天天气真好!"}],
tools=[sentiment_tool],
# 强制调用 analyze_sentiment,模型不能选择直接回答
tool_choice={
"type": "function",
"function": {"name": "analyze_sentiment"}
},
)
# tool_choice 的三个值:
# "auto" - 默认,模型自己决定是否调用工具(推荐)
# "none" - 禁止调用任何工具,强制模型直接回答
# {"type": "function", "function": {"name": "xxx"}} - 强制调用指定工具
强制工具调用最有价值的场景:用 Function Calling 做结构化数据提取。定义一个"结构化提取"工具,强制模型调用它,就能稳定地从非结构化文本中提取结构化字段,比 JSON 模式或 Prompt 拼接更可靠。
1.8 多轮工具调用:解决复杂问题
某些复杂问题需要多轮工具调用才能解决。比如:
用户:帮我计算一下,如果我把 1000 美元换成人民币,能买几本售价 89.9 元的书?
这个问题需要三步:
- 查询美元兑人民币汇率
- 计算 1000 美元等于多少人民币(1000 × 汇率)
- 计算能买几本(总金额 ÷ 书价)
@tool
def get_exchange_rate(from_currency: str, to_currency: str) -> str:
"""查询实时汇率"""
# 实际调用汇率 API
return "1 USD = 7.23 CNY(2024-03-15)"
@tool
def calculate(expression: str) -> str:
"""安全地计算数学表达式"""
allowed_chars = set("0123456789+-*/.() ")
if not all(c in allowed_chars for c in expression):
return "不支持的表达式"
try:
return str(eval(expression))
except Exception as e:
return f"计算失败:{str(e)}"
# 模型会自动规划:
# 第一轮:调用 get_exchange_rate(from_currency="USD", to_currency="CNY")
# 第二轮:调用 calculate(expression="1000 * 7.23")
# 第三轮:调用 calculate(expression="7230 / 89.9")
# 最终:整合所有结果,给出自然语言回答
这就是为什么工具调用的代码要写成 while True 循环——直到模型不再请求工具调用,才返回最终答案。
1.9 Function Calling 的适用场景总结
| 场景 | 是否适合 Function Calling |
|---|---|
| 查询实时数据(天气、股价、汇率) | 非常适合 |
| 访问私有数据(内部数据库、文件系统) | 非常适合 |
| 执行操作(发邮件、创建工单、调用 API) | 非常适合 |
| 结构化数据提取(从非结构文本提取字段) | 非常适合 |
| 复杂计算(金融模型、数据分析) | 非常适合 |
| 纯文字问答(不需要外部数据) | 不需要 |
1.10 工具设计的注意事项
工具定义的质量直接决定模型调用的准确率:
描述要精准:description 告诉模型这个工具干什么,什么情况下用。描述模糊会导致模型乱用或不知道该用哪个。
参数说明要具体:每个参数的 description 要说清楚格式要求、可选值、默认值。模型会严格按照 schema 填参数。
必填参数要标清楚:required 数组里的参数必填,否则模型可能传 null 导致工具报错。
做好错误处理:工具执行失败时,返回有意义的错误信息字符串,让模型知道发生了什么,能够调整策略,而不是直接抛异常。
最小权限原则:查询工具只给只读权限,不能有写入操作;写入工具只能改特定的资源,不能访问其他数据。工具的权限边界要严格控制。
下一篇介绍 LangChain 的工具调用封装——@tool 装饰器、bind_tools、以及如何用更少的代码实现更完整的工具调用循环。