做大模型应用做着做着,很多团队会从“纯 Chat”走到“工具调用/Function Calling”:

  • 查订单、查库存、查规则
  • 触发工单、发送通知、写入数据库
  • 调用搜索、调用知识库、调用内部服务

工具调用能显著提升可用性,但也会带来一组新的线上问题:

  • 高峰期工具超时 → LLM 一直等 → 延迟爆炸
  • 模型“编造参数” → 工具报错或查错数据
  • 误调用敏感工具 → 越权/数据泄露
  • 工具有副作用 → 重试导致重复扣款/重复下单

这篇不讲概念,直接给你一套工程可落地的做法:失败模式分类 + 重试/回退策略表 + 最小 Python 执行骨架


0)TL;DR(先给结论)

  • 工具调用要稳,先把失败分成 4 类:超时/不可用、参数幻觉、越权访问、重复副作用
  • 别把“重试”当银弹:对“可重试/不可重试”错误做分类,否则会把事故放大。
  • 最小可上线骨架:工具 Schema(强约束)+ 参数校验 + 权限门禁 + 超时/限流 + 幂等键 + 回退策略 + 全链路日志

1)失败模式 1:超时/不可用(最常见)

1.1 典型症状

  • P95 延迟突然变大,但模型本身没变慢
  • 工具调用数量上升时,错误率与重试率同步上升
  • LLM 输出“我正在查询,请稍等…”但实际上一直卡住

1.2 根因

  • 工具服务慢(下游依赖慢、DB 慢、第三方慢)
  • 没有超时与熔断,调用链被拖死
  • 高峰排队导致“超时→重试→更排队”的放大回路

1.3 工程解法(优先级)

  1. 工具超时写死(例如 1s/2s/5s 分级)
  2. 熔断/降级:连续失败或延迟超阈值直接进入降级路径
  3. 限流与队列:宁可快速失败,也不要无限排队
  4. 回退策略:工具失败时返回“可解释的降级结果”,不要让模型乱编

2)失败模式 2:参数幻觉(模型编造/拼错参数)

2.1 典型症状

  • 工具报 ValidationErrormissing required field
  • 订单号/日期格式错误、枚举值越界
  • 模型把自然语言“差不多”塞进严格字段(例如金额=“大概 300 左右”)

2.2 工程解法

  • 工具参数必须有 Schema(字段/类型/枚举/示例)
  • 服务端二次校验:永远不要信任模型参数
  • 参数修复循环:把校验错误返回给模型,让它“只修参数”

经验:把“修参数”做成一条明确的系统指令,成功率会明显提升。


3)失败模式 3:越权访问(安全与合规风险最高)

3.1 典型症状

  • 模型尝试调用不应该调用的工具(例如导出全量数据)
  • 模型把用户输入当成指令,越过权限边界
  • 工具返回敏感字段(手机号、身份证、地址等)被直接塞进回复

3.2 工程解法(必须做)

  • 工具白名单:按 tenant/user/role 控制可用工具集合
  • 最小权限:工具只返回必要字段(字段投影)
  • 审计日志:记录谁调用了什么工具、输入输出摘要
  • 敏感字段脱敏:在工具层处理,而不是靠模型“自觉”

4)失败模式 4:副作用与重复(重试放大器)

如果工具会“写入/扣款/下单/发送通知”,重试会导致重复副作用:

  • 超时重试导致重复扣款
  • 模型改写参数重试导致“重复创建工单”

工程上必须做:

  • 幂等键(idempotency key):同一业务请求只执行一次
  • 写操作与读操作分离:读可重试,写要谨慎
  • 先模拟/后提交:高风险动作先走“dry-run”验证,再确认执行

5)一张表:工具调用的重试/回退策略(可直接贴到评审会)

失败类型 是否重试 推荐策略 回退/降级
工具超时/临时网络 可重试(有限次) 指数退避 + 抖动;设置全局超时 返回缓存/返回“稍后重试”并记录工单
工具 4xx 参数错误 不重试 返回校验错误给模型“只修参数” 输出可读错误提示 + 引导用户补充信息
工具 5xx/不可用 可重试(更少次) 熔断后直接降级 走替代工具/走静态规则/走人工兜底
权限不足/越权 不重试 直接拒绝 + 记录审计 输出合规拒答
写操作超时 不要盲重试 幂等查询执行结果再决定 返回“处理中”并异步通知

6)最小 Python 骨架:Schema 校验 + 权限门禁 + 超时 + 幂等 + 回退

下面的骨架只表达“结构”,你可以把 call_llmtool_impl 换成自己的实现:

from dataclasses import dataclass
from typing import Any, Callable, Dict, Optional, Tuple
import time


@dataclass
class ToolSpec:
    name: str
    schema: Dict[str, Any]               # JSON Schema(或你自己的结构)
    impl: Callable[[Dict[str, Any]], Any]
    timeout_sec: float = 2.0
    side_effect: bool = False            # 是否写操作


class PermissionDenied(Exception):
    pass


class ToolTimeout(Exception):
    pass


def now_ms() -> int:
    return int(time.time() * 1000)


def validate_args(schema: Dict[str, Any], args: Dict[str, Any]) -> Tuple[bool, str]:
    # TODO: 用 jsonschema / pydantic 做严格校验
    return True, ""


def run_with_timeout(fn: Callable[[], Any], timeout_sec: float) -> Any:
    # 最小占位:真实实现建议用线程/协程/信号量等做超时控制
    start = time.time()
    out = fn()
    if time.time() - start > timeout_sec:
        raise ToolTimeout("tool_timeout")
    return out


def tool_gate(user_role: str, tool_name: str):
    # TODO: 按 role/tenant/user 做白名单
    allowed = {"search", "get_order", "get_policy"}
    if user_role != "admin" and tool_name not in allowed:
        raise PermissionDenied(f"tool_not_allowed: {tool_name}")


def call_llm(messages: list[dict]) -> dict:
    """
    TODO: 替换为真实大模型调用,返回结构示例:
    {"tool_name":"get_order","tool_args":{"order_id":"xxx"}}
    """
    return {"tool_name": "search", "tool_args": {"q": "example"}}


def handle_request(user_role: str, tool_registry: Dict[str, ToolSpec], idempotency_key: str):
    t0 = now_ms()

    # 1) 让模型决定是否要调用工具(或直接由业务决定)
    decision = call_llm(messages=[{"role": "user", "content": "..." }])
    tool_name = decision.get("tool_name")
    tool_args = decision.get("tool_args", {})

    # 2) 权限门禁
    tool_gate(user_role, tool_name)

    spec = tool_registry[tool_name]

    # 3) 参数校验(服务端必须做)
    ok, err = validate_args(spec.schema, tool_args)
    if not ok:
        # TODO: 把 err 反馈给模型“只修参数”,有限次数
        return {"ok": False, "error": f"bad_args: {err}"}

    # 4) 幂等(写操作必做)
    if spec.side_effect:
        # TODO: 用 idempotency_key 查重/落库,避免重复执行
        pass

    # 5) 工具执行 + 超时
    try:
        result = run_with_timeout(lambda: spec.impl(tool_args), spec.timeout_sec)
    except ToolTimeout:
        # TODO: 熔断/降级/走缓存
        return {"ok": False, "error": "tool_timeout"}
    except Exception as e:
        return {"ok": False, "error": f"tool_error: {e}"}

    # 6) 把工具结果交给模型生成最终回复(也可以由程序生成)
    latency = now_ms() - t0
    return {"ok": True, "tool": tool_name, "latency_ms": latency, "result": result}

这段骨架里,最关键的控制点是:

  • tool_gate:权限白名单
  • validate_args:参数校验与修复循环
  • timeout_sec:超时写死 + 降级
  • idempotency_key:写操作幂等

7)上线自检清单(建议打印)

  • 每个工具都有 Schema、示例与字段说明
  • 服务端二次校验参数(不信任模型)
  • 工具调用有超时、限流、熔断与降级
  • 写操作有幂等键与“查询执行结果再决定是否重试”
  • 工具白名单按 role/tenant 控制
  • 工具返回做字段投影与脱敏
  • 全链路日志:tool_name、args 摘要、latency、error、fallback_count

8)资源区:做多模型工具调用对比时,先把接入层统一

工具调用的“稳定性/成本/延迟”通常要做 A/B(不同模型、不同提示词版本)。
工程上更省事的做法是统一成 OpenAI 兼容调用方式(很多时候只改 base_urlapi_key)。

例如某些聚合入口提供 OpenAI 兼容端点(参数以其控制台/文档为准),我自己用的是大模型聚合平台147AI。便于你快速做对比评测与路由试验。

Logo

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

更多推荐