跳到主要内容

生产环境中的工具使用:真正有效的函数调用模式

· 阅读需 10 分钟
Tian Pan
Software Engineer

LLM 在生产环境中函数调用失败最令人惊讶的地方在于它们的来源。不是幻觉推理。也不是模型选错了工具。代理不稳定的首要原因在于参数构造:错误的类型、缺少必填字段、格式错误的 JSON、幻觉出的额外字段。模型本身没问题。你的 schema 才是问题所在。

这是个好消息,因为 schema 修复成本很低。

你实际运行的六阶段生命周期

在探讨模式之前,先建立一个心智模型:LLM 从不直接执行代码。它只是提出建议,由你来执行。工具调用是一种协议:

  1. 上下文准备 — 系统提示词 + 工具定义注入到上下文窗口
  2. 决策阶段 — 模型决定是否以及调用哪个工具
  3. 参数构造 — LLM 生成与你的 schema 匹配的结构化 JSON
  4. 执行 — 你的应用程序代码运行实际函数
  5. 观察结果注入 — 结果被附加到对话历史中
  6. 继续 — 模型要么回答问题,要么进行更多工具调用

这个管道中的每一个生产 bug 都是一个交接 bug。要么 schema 定义不明确(步骤 3),要么执行没有验证输出(步骤 4),要么结果在未进行净化处理的情况下被注入(步骤 5)。模型很少自行失败——它在提议者和执行者之间的契约模糊不清时才会失败。

Schema 设计是你最具杠杆作用的投资

在生产环境分析中,没有强制执行输出 schema 的 GPT-4,其 schema 符合率低于 40%。强制执行 OpenAI 结构化输出后:结构符合率达到 100%。多步骤工作流中的错误率从约 5% 下降到 0.3% 以下。复杂多步骤的准确性从 10% 提高到 70%。Schema 不是形式,它是接口契约。

以意图命名工具

search_customer_orders 优于 search。工具名称是模型决定调用哪个工具时使用的路由信号的一部分。模糊的名称会导致模糊的路由。

在修改状态的工具上使用 additionalProperties: false

对于只读工具,这是可选的。对于写入数据库或调用具有副作用的外部 API 的工具,请锁定 schema。一个幻觉出的额外字段如果悄无声息地传递给下游 API,可能会以难以调试的方式破坏状态。

{
"name": "create_order",
"description": "创建新客户订单。仅在确认商品可用性后调用。",
"input_schema": {
"type": "object",
"properties": {
"customer_id": { "type": "string", "description": "已认证客户的 UUID" },
"item_sku": { "type": "string", "description": "来自商品目录的产品 SKU" },
"quantity": { "type": "integer", "minimum": 1, "maximum": 100 }
},
"required": ["customer_id", "item_sku", "quantity"],
"additionalProperties": false
}
}

对分类参数使用 enum

在存在有界集合的情况下,绝不要使用开放字符串。如果模型可以用它编造的任何字符串调用 update_ticket_status,它就会这样做。如果你将其限制为 ["open", "in_progress", "resolved", "closed"],选项空间就会收敛到正确的值。

优先使用语义化的参数名称

user_email 优于 user_id。模型可以对电子邮件地址进行推理,并在其不正确时进行检测。它无法对不透明的 UUID 进行推理。当参数不明确时,模型会猜测——而且会猜错。

直接在描述中添加具体示例

Anthropic 的内部测试表明,仅通过在 schema 描述中添加具体示例,复杂参数处理的准确性就从 72% 提高到了 90%。不要只说明参数是什么。展示一个有效值应该是什么样子:

"order_date": {
"type": "string",
"description": "ISO 8601 格式的订单日期。示例:'2025-10-12'。不包含时间。"
}

Schema 门控模式

将 JSON Schema 验证作为一个强制性门控,放置在 LLM 输出和工具执行之间。无效参数绝不会通过。它们会返回一个结构化错误,模型可以据此进行处理。

import jsonschema

def execute_tool(tool_name: str, arguments: dict, schema: dict):
try:
jsonschema.validate(instance=arguments, schema=schema)
except jsonschema.ValidationError as e:
# 将错误作为工具结果返回给 LLM,而不是崩溃
return {
"error": "invalid_arguments",
"message": str(e.message),
"path": list(e.absolute_path)
}

return TOOL_REGISTRY[tool_name](**arguments)

模型收到此错误作为工具结果后,会进行自我纠正。将自我纠正尝试次数限制在 3 次。此后,转交给失败处理器或升级给人。无限制的纠正循环是一个等待发生的无限循环。

并行与顺序:明确架构

大多数 LLM 默认使用顺序工具调用,因为顺序推理在训练数据中占主导地位。除非你专门设计,否则它们不会并行化。

规则很简单:如果工具 B 不依赖工具 A 的输出,就让它们并行运行。只读查找几乎总是可以安全地并行化。状态修改则几乎从不安全。

实际影响:MiniMax-M2.5 报告称,通过更好地利用并行工具调用,端到端运行时从 31.3 分钟减少到 22.8 分钟——提高了 37%。这种收益是真实的,但你必须明确地指示何时可以安全地并行。

在你的系统提示词中告诉模型:

当检索独立记录(例如,用户资料、产品详情、外部数据)时,
你可以同时调用多个工具。不要对独立的查找进行序列化。

对于更复杂的编排,请考虑程序化工具调用——让模型发出一个代码块来表达循环和并行调度,而不是为每个操作进行一次 API 往返。Anthropic 的研究发现,这在复杂的研发任务中将 token 消耗降低了 37%,同时自然地表达了并行性。

错误处理:两个不同的问题

大多数工程师对所有故障都采用单一的重试模式。这是错误的。故障有两种根本不同的类型,需要不同的应对方式。

网络/基础设施故障(速率限制、5xx 错误、超时):采用指数退避和抖动进行重试。基础延迟 1-2 秒,每次尝试加倍,限制在 5-7 次重试。添加抖动以防止雷鸣般的群集效应(thundering herd)。

工具参数故障(验证错误、业务逻辑拒绝):不要重试相同的调用。将错误作为工具结果发送回 LLM。模型需要重新构思,而不是重复。

def handle_tool_result(tool_name: str, result: dict, reformulation_count: int):
if "error" in result:
if reformulation_count >= 3:
raise MaxReformulationsExceeded(tool_name, result)
# Inject error as tool result message; the LLM will self-correct
return {"type": "tool_result", "content": json.dumps(result), "is_error": True}
return {"type": "tool_result", "content": json.dumps(result)}

对于持续的下游故障,添加熔断器。随时间监控故障率。当下游服务持续失败时,应快速失败而不是排队请求。熔断器位于重试层之上,防止其无限期地触发。

你没有计算的 token 成本

工具定义在静止状态下(即在任何用户消息之前)就会消耗上下文 token。预先加载一个大型工具目录意味着你在每个请求中都要为每个未使用的工具定义付费。

具体数字令人震惊:通过 MCP 连接三个服务(GitHub、Slack 和 Sentry)可以在 200,000 token 的上下文窗口中消耗 143,000 个 token —— 在对话开始前就产生了 72% 的开销。如果每天有 1,000 个请求,并且工具定义数量庞大,仅仅是模式开销每月就可能达到 5,000 美元以上。

三种缓解模式:

领域分组加载:不要预先加载所有工具。首先识别用户的意图,然后只加载相关的工具组。客户支持代理不需要代码分析工具。

工具搜索而非工具注入:Anthropic 关于动态工具发现的研究表明,在 Opus 4 上,通过让模型在运行时搜索正确的工具,而不是预先加载所有定义,任务准确率从 49% 提高到 74%。权衡是多一次往返,但好处是显著降低了上下文开销。

结果摘要:在长代理链中,中间工具结果会累积并与当前推理争夺上下文。在注入工具结果后,如果结果很大,请对其进行摘要。完整结果在你的后端日志中;模型需要的是信号,而不是每个 token。

安全:每个工具都是攻击面

将工具执行视为由随机模型控制的远程代码执行。这绝非夸大其词。

通过工具结果进行提示注入:恶意内容可能出现在工具输出中——例如,包含“忽略先前指令”的客户名称,或包含对抗性文本的文档。在将工具结果注入上下文之前对其进行净化。将工具结果内容视为不受信任的用户输入,而非受信任的系统输出。

参数级授权:验证工具调用参数中的 user_id 是否与已认证用户匹配。不要假设模型只会请求其有权查看的内容——在执行层验证授权。

最小权限:工具应只暴露任务所需的能力。面向客户的代理不需要 delete_all_records 工具。如果工具存在于注册表中,模型就可以调用它。

全面的审计日志记录:记录每次工具调用,包括调用者身份、参数、结果和时间戳。这是调试生产故障的基本要求,也是许多领域中的监管要求。

用于调试的故障分类

当代理在生产环境中失败时,故障类型决定了修复方案:

故障类型示例修复方案
结构性故障JSON 格式错误,缺少必填字段模式强制执行 + 验证关卡
语义性故障工具正确,参数值错误在模式中提供更好的描述 + 示例
选择性故障针对任务选择了错误的工具更清晰的工具名称 + 在描述中进行明确消歧
链式故障工具 A 的输出被错误地用作工具 B 的输入添加转换步骤或指定数据契约
循环故障工具失败 → LLM 永久重试相同的调用限制重新构思尝试次数,实现故障处理程序
上下文溢出10 次工具调用后 200K 窗口耗尽总结中间结果,延迟加载工具定义

大多数生产调试时间都花在语义性故障上,因为它们通过了模式验证但产生了错误的结果。修复方案是在描述中提供更好的示例,并使用真实的生产流量模式进行行为测试。

现在的技术栈是什么样子

工具使用已从新颖之处发展为基础设施。OpenAI 的结构化输出在模型层面强制执行模式合规性。Anthropic 的 MCP 现已成为工具发现的标准协议,已被 OpenAI(2025 年 3 月)和 Google DeepMind(2025 年 4 月)采用,每月 SDK 下载量超过 9700 万,服务器数量超过 5,800 台。LangChain 旧的文本解析方法已过时;原生函数调用和直接提供商 API 成为标准。

前沿在于管理上下文开销和实现真正的并行性。当你进行架构设计时,模型可以并行化工具调用。如果你不管理它们,上下文成本会在代理循环中复合增长。这些是可解决的工程问题,而非模型能力问题。

模式是你拥有的最便宜的修复方案。从那里开始。

Let's stay in touch and Follow me for more thoughts and updates