【AI应用开发】技术答疑篇(三):Function Calling 的执行流程是什么?什么时候会出现调用失败?
·
【AI应用开发】技术答疑篇(三):Function Calling 的执行流程是什么?什么时候会出现调用失败?
1. 一句话理解 Function Calling
Function Calling 是 LLM 输出结构化指令告诉程序"调用哪个工具、传什么参数"的能力。 LLM 不执行函数,只负责决策"选哪个工具"和"参数值是什么"。
Function Calling 不是 LLM 在调用函数
而是 LLM 说:"我想调用这个函数,参数是这些"
你的代码负责真正执行
2. Function Calling 全流程拆解
2.1 完整流程图
┌────────────────────────────────────────────────────────────────────┐
│ Function Calling 全流程 │
│ │
│ ① 用户输入 │
│ "北京今天天气怎么样?" │
│ │ │
│ ▼ │
│ ② 发送给 LLM(附带工具定义) │
│ messages + tools 定义 │
│ │ │
│ ▼ │
│ ③ LLM 推理决策 │
│ ├─ 需要工具 → 返回 tool_calls [name, arguments] │
│ └─ 不需要 → 返回纯文本 content │
│ │ │
│ ▼ │
│ ④ 你的代码解析 LLM 返回 │
│ 提取 function_name 和 arguments │
│ │ │
│ ▼ │
│ ⑤ 你的代码执行函数 │
│ result = get_weather(city="北京") │
│ │ │
│ ▼ │
│ ⑥ 结果回填 LLM │
│ 将 tool_result 作为新消息追加 │
│ │ │
│ ▼ │
│ ⑦ LLM 基于结果推理 │
│ ├─ 需要更多工具 → 回到③ │
│ └─ 可以回答 → 输出最终文本 │
│ │ │
│ ▼ │
│ ⑧ 返回给用户 │
│ "北京今天多云,23°C" │
└────────────────────────────────────────────────────────────────────┘
2.2 每一步的代码对应
用 OpenAI SDK 演示每一步发生了什么:
import json
from openai import OpenAI
client = OpenAI()
# ===== 步骤 ①: 用户输入 =====
user_message = "北京今天天气怎么样?"
# ===== 步骤 ②: 发送给 LLM(附带工具定义)=====
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的天气信息",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,例如'北京'",
}
},
"required": ["city"],
},
},
}
]
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": user_message}],
tools=tools,
)
# ===== 步骤 ③: 查看 LLM 返回了什么 =====
message = response.choices[0].message
print(f"content: {message.content}") # → None(因为要调工具)
print(f"tool_calls: {message.tool_calls}") # → 工具调用指令
# tool_calls 结构:
# [
# {
# "id": "call_abc123",
# "type": "function",
# "function": {
# "name": "get_weather",
# "arguments": '{"city": "北京"}'
# }
# }
# ]
# ===== 步骤 ④: 解析工具调用 =====
tool_call = message.tool_calls[0]
function_name = tool_call.function.name # "get_weather"
arguments = json.loads(tool_call.function.arguments) # {"city": "北京"}
# ===== 步骤 ⑤: 你的代码执行工具 =====
def get_weather(city):
return f"{city}今天多云,23°C"
result = get_weather(**arguments) # "北京今天多云,23°C"
# ===== 步骤 ⑥: 结果回填 =====
messages = [
{"role": "user", "content": user_message},
{
"role": "assistant",
"content": None,
"tool_calls": [tool_call],
},
{
"role": "tool",
"tool_call_id": tool_call.id,
"content": result,
},
]
# ===== 步骤 ⑦: LLM 基于工具结果推理 =====
final_response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
)
# ===== 步骤 ⑧: 返回给用户 =====
print(final_response.choices[0].message.content)
# → "北京今天多云,23°C"
3. 工具定义的规范与最佳实践
3.1 一个"好"的工具定义长什么样
# ✅ 好的工具定义:名称明确、描述清晰、参数类型精确
{
"type": "function",
"function": {
"name": "search_customer_orders", # snake_case 命名
"description": (
"根据客户姓名和日期范围搜索订单。"
"返回订单列表,包含订单号、金额、状态和创建时间。"
"当用户询问某个客户的订单信息时使用此工具。"
),
"parameters": {
"type": "object",
"properties": {
"customer_name": {
"type": "string",
"description": "客户的全名或部分名称",
},
"start_date": {
"type": "string",
"description": "搜索起始日期,格式 YYYY-MM-DD",
},
"end_date": {
"type": "string",
"description": "搜索结束日期,格式 YYYY-MM-DD",
},
"status": {
"type": "string",
"enum": ["pending", "shipped", "delivered", "cancelled"],
"description": "订单状态筛选,不传则返回全部状态",
},
},
"required": ["customer_name"],
},
},
}
3.2 工具定义的四个核心原则
| 原则 | 说明 | 错误示例 | 正确示例 |
|---|---|---|---|
| 名称要唯一且精确 | 函数名要见名知义 | search |
search_customer_orders |
| 描述要告诉 LLM"什么时候用" | LLM 通过描述判断该调用哪个工具 | "搜索订单" |
"当用户询问客户订单时使用..." |
| 参数类型要严格 | JSON Schema 类型约束 | 所有参数用 string | 该用 integer 用 integer |
| 枚举优先于自由文本 | 能用 enum 就别用 string | 状态用 string | 状态用 enum: [“pending”,“done”] |
3.3 为什么参数描述如此重要
LLM 看到的工具定义就像一个人看 API 文档。如果描述模糊,LLM 就会乱填参数:
# ❌ 坏的参数定义
"price": {
"type": "number",
"description": "价格" # 太模糊!什么价格?什么单位?什么货币?
}
# ✅ 好的参数定义
"price": {
"type": "number",
"description": "订单金额,单位:元(人民币),不含税"
}
4. JSON Schema 参数约束详解
4.1 类型约束
# 基础类型
{"type": "string"} # 字符串
{"type": "integer"} # 整数
{"type": "number"} # 数字(含小数)
{"type": "boolean"} # 布尔值
{"type": "array", "items": {"type": "string"}} # 字符串数组
{"type": "object", "properties": {...}} # 嵌套对象
4.2 枚举约束
# ✅ 用 enum 限制可选值
"order_by": {
"type": "string",
"enum": ["date", "amount", "status"],
"description": "排序字段"
}
# ❌ 不用 enum,LLM 可能生成五花八门的值
"order_by": {
"type": "string",
"description": "排序字段,可选 date/amount/status"
}
# LLM 可能输出:"按时间排序"、"排序依据:日期" 等
4.3 数值约束
"page_size": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"description": "每页返回的记录数,1-100"
}
4.4 复杂嵌套结构
# 支持嵌套对象
"order_items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"product_id": {"type": "string"},
"quantity": {"type": "integer", "minimum": 1},
},
"required": ["product_id", "quantity"],
},
"description": "订单商品列表"
}
5. 调用失败的 6 种场景与对策
场景一:LLM 编造不存在的工具
原因:LLM 的"幻觉"也体现在工具名上,尤其当 Prompt 中同时存在工具定义和自由对话时。
# 症状
LLM 返回: tool_calls=[{"function": {"name": "send_whatsapp", ...}}]
实际工具: ["send_email", "search", "calculate"] ← 根本没有 send_whatsapp
# 对策
if function_name not in available_tools:
error_msg = f"错误:工具 '{function_name}' 不存在。可用工具:{list(available_tools.keys())}"
messages.append({"role": "tool", "content": error_msg})
continue # 让 LLM 重新选择
场景二:参数缺失或格式错误
# 症状:required 参数没填
LLM 返回: arguments='{}' # 或 arguments='{"city": "北京", "date": null}'
# 对策
try:
args = json.loads(tool_call.function.arguments)
# 验证必填参数
for required_param in ["city"]:
if required_param not in args:
raise ValueError(f"缺少必填参数: {required_param}")
except json.JSONDecodeError:
error_msg = "参数 JSON 格式错误,请重新生成"
场景三:参数值语义错误
# 症状:类型对但值不对
LLM 返回: arguments='{"date": "2024-13-45"}' # 13月45号?不存在
# 对策:在业务层加校验
def search_orders(date):
try:
datetime.strptime(date, "%Y-%m-%d")
except ValueError:
return "错误:日期格式无效,请使用 YYYY-MM-DD 格式"
场景四:工具自身执行异常
# 症状:API 超时、数据库报错、网络问题
def call_external_api(url):
try:
response = requests.get(url, timeout=5)
response.raise_for_status()
return response.json()
except requests.Timeout:
return "错误:API 请求超时,请尝试缩小查询范围"
except requests.RequestException as e:
return f"错误:API 请求失败 ({e}),请稍后重试"
场景五:LLM 同时返回 content 和 tool_calls
# 症状:LLM 既说了话又调了工具
message.content = "好的,我帮你查一下天气" ← 有文本
message.tool_calls = [...] ← 又有工具调用
# 对策:优先处理 tool_calls,content 仅做日志
if message.tool_calls:
# 执行工具调用
...
else:
# 纯文本,直接返回
return message.content
场景六:无限循环调用
# 症状:LLM 不断调用同一个工具,永远不收手
# 原因:Observation 格式不清晰、LLM 判断不了任务已完成
# 对策:设置最大步数 + 检测重复调用
max_steps = 10
recent_calls = [] # 记录最近的调用
if len(recent_calls) >= 3 and all(
c == tool_call for c in recent_calls[-3:]
):
messages.append({
"role": "user",
"content": "你已经连续三次调用了相同的工具。请基于已有信息给出最终答案,或尝试其他方法。"
})
6. 并行调用与链式调用
6.1 并行调用(Parallel Calls)
当多个工具互不依赖时,LLM 可以一次返回多个 tool_calls:
# 用户:"查查北京和上海今天的天气"
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "查查北京和上海今天的天气"}],
tools=[weather_tool],
)
# LLM 返回两个 tool_calls(互不依赖,可并行)
tool_calls = response.choices[0].message.tool_calls
# [
# {"function": {"name": "get_weather", "arguments": '{"city":"北京"}'}},
# {"function": {"name": "get_weather", "arguments": '{"city":"上海"}'}}
# ]
# 并行执行
import concurrent.futures
def execute_tool(tc):
name = tc.function.name
args = json.loads(tc.function.arguments)
return tools[name](**args)
with concurrent.futures.ThreadPoolExecutor() as executor:
results = list(executor.map(execute_tool, tool_calls))
6.2 链式调用(Chained Calls)
当工具 B 的输入依赖工具 A 的输出时:
# 用户:"查一下我上一个订单的状态"
# Round 1: 获取最近订单
Action: get_recent_orders(limit=1)
Observation: [{"order_id": "ORD12345"}]
# Round 2: 用订单 ID 查状态
Action: get_order_status(order_id="ORD12345")
Observation: {"status": "shipped"}
# Round 3: 回答
Final Answer: "您的订单 ORD12345 状态为:已发货"
链式调用是天然的串行——每步依赖上一步的结果,无法并行。
7. 错误处理与重试策略
7.1 三层错误处理架构
Layer 1: JSON 解析层
↓ 捕获:JSON 格式错误、参数类型不匹配
↓ 策略:解析失败 → 重新请求 LLM 生成
Layer 2: 参数验证层
↓ 捕获:缺少必填参数、枚举值无效、数值超出范围
↓ 策略:返回具体错误信息 → LLM 读错误后修正重试
Layer 3: 业务执行层
↓ 捕获:API 超时、数据库错误、网络异常
↓ 策略:返回结构化错误 → LLM 决定重试/换工具/告知用户
7.2 完整重试逻辑
def execute_with_retry(agent, tool_call, max_retries=3):
"""带智能重试的工具执行"""
for attempt in range(max_retries):
try:
# 解析参数
args = json.loads(tool_call.function.arguments)
# 验证参数
validate_params(tool_call.function.name, args)
# 执行工具
result = execute_tool(tool_call.function.name, args)
# 检查结果是否包含错误
if isinstance(result, dict) and result.get("error"):
if attempt < max_retries - 1:
# 让 LLM 知道错误并重试
return {
"status": "retry",
"error": result["error"],
"attempt": attempt + 1,
}
return {"status": "success", "result": result}
except json.JSONDecodeError:
if attempt < max_retries - 1:
continue
return {"status": "error", "message": "参数 JSON 格式持续错误"}
except Exception as e:
if attempt < max_retries - 1:
time.sleep(2 ** attempt) # 指数退避
continue
return {"status": "error", "message": str(e)}
return {"status": "error", "message": "超过最大重试次数"}
8. 手写完整 Function Calling 引擎
"""
完整 Function Calling 引擎实现
支持:工具注册、JSON Schema 验证、并行调用、智能重试
"""
import json
import time
import concurrent.futures
from typing import Any, Callable
from openai import OpenAI
client = OpenAI()
class ToolRegistry:
"""工具注册中心"""
def __init__(self):
self._tools: dict[str, dict] = {}
def register(self, name: str, func: Callable, description: str,
parameters: dict) -> None:
self._tools[name] = {
"function": func,
"schema": {
"type": "function",
"function": {
"name": name,
"description": description,
"parameters": parameters,
}
}
}
def get_schemas(self) -> list:
return [t["schema"] for t in self._tools.values()]
def execute(self, name: str, args: dict) -> str:
if name not in self._tools:
return f"错误:工具 '{name}' 未注册"
try:
result = self._tools[name]["function"](**args)
return str(result)
except TypeError as e:
return f"参数错误:{e}"
except Exception as e:
return f"执行错误:{e}"
class FunctionCallingAgent:
"""带完整 Function Calling 能力的 Agent"""
def __init__(self, registry: ToolRegistry, max_steps: int = 10):
self.registry = registry
self.max_steps = max_steps
def run(self, user_input: str, system_prompt: str = "") -> str:
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
messages.append({"role": "user", "content": user_input})
return self._loop(messages)
def _loop(self, messages: list) -> str:
step = 0
while step < self.max_steps:
step += 1
# 调用 LLM
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=self.registry.get_schemas(),
temperature=0.0,
)
msg = response.choices[0].message
# 没有工具调用 → 最终回复
if not msg.tool_calls:
return msg.content or "任务完成"
# 添加 assistant 消息
messages.append({
"role": "assistant",
"content": msg.content,
"tool_calls": [
{
"id": tc.id,
"type": "function",
"function": {
"name": tc.function.name,
"arguments": tc.function.arguments,
}
}
for tc in msg.tool_calls
]
})
# 执行工具调用
for tc in msg.tool_calls:
name = tc.function.name
try:
args = json.loads(tc.function.arguments)
except json.JSONDecodeError:
result = f"参数 JSON 格式错误: {tc.function.arguments}"
else:
result = self.registry.execute(name, args)
# 工具结果回填
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": result,
})
return "达到最大执行步骤,未能完成任务"
9. 常见踩坑与避坑指南
坑 1:工具描述里写了"必填"但 schema 里没标 required
# ❌ 错误:描述说必填,但 schema 没约束
"parameters": {
"properties": {
"city": {"type": "string", "description": "城市名称,必填"}
}
# 缺少 "required": ["city"]
}
# LLM 有时不传 city,造成工具执行失败
坑 2:忘记处理 content 和 tool_calls 同时出现
# LLM 可能同时返回
msg.content = "我来帮你查一下" # 有内容
msg.tool_calls = [get_weather_call] # 有工具调用
# 正确处理
if msg.tool_calls:
# 优先执行工具,内容可忽略或记日志
...
坑 3:工具结果太长撑爆上下文
# 搜索结果可能返回几万字
def search(query):
result = api.search(query)
# ❌ 直接返回全部
return json.dumps(result)
# ✅ 截断并告知
truncated = result[:2000]
return json.dumps({
"results": truncated,
"total": len(result),
"note": f"已截断,共{len(result)}条结果,仅展示前若干条"
})
坑 4:模型不调用工具就直接回答
# 有时 LLM 明明该用工具却直接回答
# 原因:工具描述不够明确、temperature 太高、模型版本能力不足
# 对策 1:给 system prompt 加上"强制要求"
"如果问题需要实时数据或外部信息,必须使用工具,禁止凭空回答"
# 对策 2:使用 tool_choice 参数强制调用
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools,
tool_choice="required", # 强制 LLM 必须选一个工具
)
10. 本篇总结
| 问题 | 答案 |
|---|---|
| Function Calling 是什么? | LLM 输出结构化工具调用指令的能力 |
| LLM 真正执行函数吗? | 不,LLM 只输出调用哪个和参数,你的代码执行 |
| 为什么调用会失败? | 6 种原因:工具不存在、参数缺失、格式错误、语义错误、执行异常、无限循环 |
| 如何防范? | JSON Schema 精确约束 + 三层错误处理 + 最大步数限制 |
| 并行还是串行? | 互不依赖的并行,有依赖的串行 |
一句话记住:Function Calling 的核心不是 LLM 执行函数,而是 LLM 的"决策 + 你的代码执行",工具定义的质量直接决定调用成功率。
更多推荐



所有评论(0)