【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 的"决策 + 你的代码执行",工具定义的质量直接决定调用成功率。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐