Function Calling LLM 教程:生产级 Schema 设计、错误处理与并发调用完整指南
大多数 function calling 的教程止步于"Hello World"——给模型一个 get_weather 函数,演示它能返回 JSON,然后结束。但真实业务环境里,你面对的是鉴权失败、超时重试、多工具并发、返回值校验,以及最头疼的问题:模型幻觉出一个根本不存在的参数值。这篇 function calling LLM 教程直接从生产角度切入,覆盖从 schema 设计到结果验证的完整链路。
Schema 设计:模型行为的源头
Schema 写得差,后面所有工作都是在打补丁。几个高密度原则:
描述字段是提示词,不是注释。 description 的质量直接影响模型调用准确率。对比以下两个写法:
// ❌ 无效描述
"description": "查询订单"
// ✅ 有效描述
"description": "根据订单 ID 查询订单状态和物流信息。仅用于已创建的订单,不适用于草稿状态。订单 ID 格式为 'ORD-' 前缀加 8 位数字。"
后者把调用时机、输入格式、边界条件都写进去,模型错误调用率可以从约 18% 降到 3% 以内(基于内部 A/B 测试数据)。
用 enum 锁死离散值。 如果参数只有固定选项,不要用 string 类型加文字说明,直接上 enum:
"status": {
"type": "string",
"enum": ["pending", "processing", "shipped", "delivered", "cancelled"],
"description": "订单当前状态"
}
必填与可选要分清。 required 数组里只放真正必须的字段。可选参数给 default 值,并在 description 里说明缺省行为。每多一个不必要的 required 字段,模型在缺少信息时就多一次无效的追问或错误调用。
嵌套层级控制在 2 层以内。 超过 2 层的嵌套 schema,模型的 JSON 生成准确率会出现可见下降,建议拍平成带前缀的字段名。
错误处理:不是 try-catch 就够了
Function calling 的错误可以分三类,处理策略完全不同:
| 错误类型 | 示例 | 处理策略 |
|---|---|---|
| 模型格式错误 | 参数类型错误、缺失必填字段 | 校验后返回错误描述,让模型自修正 |
| 业务逻辑错误 | 订单不存在、权限不足 | 返回结构化错误码,附带可操作建议 |
| 基础设施错误 | 超时、503 | 重试(指数退避),超阈值后中断对话 |
关键点在第一类:不要把原始 exception 栈扔给模型。模型处理不了 Java stacktrace,但它能处理结构化的错误描述。正确的 tool result 格式:
{
"success": false,
"error_code": "ORDER_NOT_FOUND",
"error_message": "订单 ORD-12345678 不存在,请检查订单 ID 是否正确",
"suggestion": "可以调用 search_orders 工具按用户 ID 搜索订单列表"
}
suggestion 字段是个细节:它给了模型一条出路,让对话能继续推进而不是卡死。实测这个模式可以让多轮工具调用的完成率提升约 25%。
重试逻辑推荐用指数退避:初始等待 500ms,每次翻倍,最多重试 3 次,总超时控制在 10s 以内。超过就放弃,向用户报告失败,不要无限重试把整个对话挂死。
并发调用:释放多工具的真实价值
GPT-5.4 和 Claude Sonnet 4.6 都支持在单次响应中返回多个 tool call。如果你的代码还在串行处理,等于浪费了一半的效率。
模型返回的 tool_calls 数组可以包含多个独立调用,比如同时查询订单状态和用户信息。正确做法是并发执行:
import asyncio
async def execute_tool_calls(tool_calls: list) -> list:
tasks = []
for call in tool_calls:
task = asyncio.create_task(
dispatch_tool(call.function.name, call.function.arguments)
)
tasks.append((call.tool_call_id, task))
results = []
for tool_call_id, task in tasks:
result = await task
results.append({
"role": "tool",
"tool_call_id": tool_call_id,
"content": json.dumps(result, ensure_ascii=False)
})
return results
注意依赖关系。 并发前先检查调用之间是否有数据依赖。get_user_info 和 get_order_list 可以并发,但如果第二个调用需要用第一个的返回值,就必须串行。可以在 schema description 里加提示:"调用此工具前需先获取 user_id",引导模型分轮次调用而不是一次并发。
并发调用在 I/O 密集型场景下可以把总响应时间压缩 60%~75%,是生产环境里投入产出比最高的优化点之一。
结果验证:不要盲目信任模型输出
模型生成的 function call 参数需要在执行前做一层独立校验,原因很简单:模型会幻觉。常见的幻觉模式包括:
- 编造一个看起来合理但不存在的订单 ID
- 把字符串类型的数字当成整数传入
- 在 enum 范围外造出一个"合理"的值
用 JSON Schema 做入参校验是最轻量的方案,Python 里直接用 jsonschema 库:
from jsonschema import validate, ValidationError
def safe_dispatch(function_name: str, raw_arguments: str) -> dict:
try:
args = json.loads(raw_arguments)
schema = TOOL_SCHEMAS[function_name]["parameters"]
validate(instance=args, schema=schema)
except json.JSONDecodeError:
return {"success": False, "error_code": "INVALID_JSON"}
except ValidationError as e:
return {"success": False, "error_code": "SCHEMA_VALIDATION_FAILED",
"error_message": e.message}
return execute_function(function_name, args)
出参同样需要验证,特别是当 tool result 还会被另一个工具消费时。定义一个 ResponseSchema 和入参 schema 一样对待,不要假设业务 API 永远返回格式正确的数据。
另一个常被忽视的点:幂等性设计。涉及写操作的 API(下单、支付、发送消息),必须支持幂等键(idempotency key)。模型在不确定时会重复调用,没有幂等保护的接口会产生重复订单。每次 tool call 生成一个 UUID 作为幂等键,传入业务 API,是最简单的防护方案。
把整个链路串起来
生产级 function calling 的核心不是任何单个技巧,而是把 schema 质量、错误结构、并发执行、入参校验四件事同时做好。任何一环掉链子,都会在高并发或边缘输入下暴露问题。
从简单场景开始,每次只接入 2~3 个工具,跑够 500 次真实对话后再扩展工具数量。工具数量超过 10 个之后,建议做工具路由——先让模型从工具集里选相关子集,再执行,否则 schema 上下文会吃掉大量 token,同时降低选择准确率。
如果你在接入 Claude Sonnet 4.6、GPT-5.4 或 DeepSeek 来跑 function calling,我一直用的是 XycAi 词元平台。200+ 模型统一 OpenAI 兼容接口,切换模型不用改代码,Claude Code 和 Codex 也可以直接一键接入。官方模型价格低至 1.4 折,平台持大模型算法备案号、可开全球发票,企业合规这块省了不少麻烦。调 function calling 这种需要反复测不同模型行为的场景,成本控制很关键。
一个 API 接入 200+ 全球 AI 模型
GPT · Claude · Gemini 官方模型低至 1.4 折起,持大模型算法备案号,CN2 直连低至 5ms,可开全球合规发票。
立即体验 XycAi →