登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  科技周边 >  人工智能

Tool calling校验工具参数并区分模型与业务错误的实现方法

来源:17golang原创

时间:2026-09-15 20:23:53 380浏览 收藏

我在第一次把工单查询接入 tool calling 时,最容易混淆的是“模型给错参数”和“工具执行失败”。前者属于调用契约问题,应该拒绝或让模型修正;后者属于业务结果,应该把可解释的失败回传给模型,而不是让模型继续猜参数。比较稳妥的实现是:工具声明负责约束形状,应用服务负责二次校验和白名单路由,业务函数只返回稳定的成功或失败结果。

官方文档:https://developers.openai.com/api/docs/guides/function-calling

要点速览
  • strict: true 能收紧函数参数契约,但不替代库存、权限、状态等业务校验。
  • 模型侧错误包括未知工具、JSON 解码失败和 schema 不匹配;业务侧错误包括订单不存在、无权访问和状态不允许。
  • 回传 function_call_output 时保留 call_id,让日志、重试和最终回答能对应同一次调用。

先把工具调用拆成两条责任边界

OpenAI 的 function calling 本质上是一个多轮交互:应用带着工具定义请求模型,模型返回工具调用,应用执行函数,再把工具结果发回模型。对我来说,最重要的设计决定不是“要不要加重试”,而是先给这条数据流划线:模型产生的调用对象到达业务函数之前,全部按模型错误处理;业务函数已经拿到合法参数后发生的失败,才算业务错误。

Tool calling 从模型调用对象到参数校验、业务工具和结果回传的边界结构图
图1:Tool calling 边界说明图,展示调用对象、schema 校验、业务工具与结果回传之间的责任关系,不是运行截图。

工具声明也要尽量具体。官方文档把函数输入定义为 JSON Schema,并建议开启严格模式;严格模式下,对象需要设置 additionalProperties: false,属性通常都应列入 required,可选值可以用可空类型表达。这些规则解决的是“字段长什么样”,不是“这个订单此刻能不能查”。

用 schema 拦截形状错误,再做一次应用层判断

下面这个示例把两层错误显式分开。示例使用 Python 标准库解析 JSON;生产项目可以把同一份契约交给 JSON Schema 校验器,但不要因为模型开启了 strict 就跳过应用侧校验。模型输出仍需要经过工具名白名单、解码、字段范围和权限上下文检查。

import json

def classify_tool_call(tool_call):
    # 先限制工具名,防止模型请求未注册的业务能力。
    if tool_call.get("name") != "lookup_order":
        return {"kind": "model_error", "code": "unknown_tool"}

    try:
        args = json.loads(tool_call.get("arguments", "{}"))
    except json.JSONDecodeError:
        # JSON 无法解析时,业务函数还没有被调用。
        return {"kind": "model_error", "code": "invalid_arguments_json"}

    if not isinstance(args, dict) or not isinstance(args.get("order_id"), str):
        # 这是参数形状错误,不应进入订单查询重试。
        return {"kind": "model_error", "code": "schema_mismatch"}

    if not args["order_id"].startswith("ORD-"):
        # 形状合法但业务字段格式不接受,仍在边界层拦截。
        return {"kind": "business_error", "code": "invalid_order_id"}

    return {"kind": "validated", "args": args}

这里的 invalid_order_id 也可以归入领域校验,关键是团队要固定分类并写进日志。不要有时把它当模型错误,有时又当订单服务错误,否则重试器很难判断是否应该再次请求模型。

现象归属处理动作
工具名不在注册表模型错误拒绝调用并记录原始 call_id
arguments 不是合法 JSON模型错误让模型修正调用,避免触发业务重试
订单不存在或无权限业务错误返回稳定错误码,由模型组织用户可读答复
订单已关闭,不能执行动作业务错误回传状态和下一步建议,不伪装成成功

业务函数只接收已验证参数,结果也要可回传

验证通过后再走白名单路由。业务函数不要把 Python 异常、数据库堆栈或内部字段直接塞进模型上下文;可观测信息写日志,给模型的结果保持短小、结构稳定。成功返回订单摘要,失败返回 ok=false、错误码和可行动的提示即可。

def execute_lookup(validated, current_user):
    # 通过验证后才读取业务数据,权限仍以当前用户为准。
    order_id = validated["args"]["order_id"]
    order = find_order(order_id)
    if order is None:
        return {"ok": False, "error_code": "ORDER_NOT_FOUND",
                "message": "订单不存在,请核对订单号"}
    if order["owner_id"] != current_user["id"]:
        # 不泄露订单是否存在,权限失败统一返回可解释结果。
        return {"ok": False, "error_code": "ORDER_FORBIDDEN",
                "message": "当前账号无权查看该订单"}
    return {"ok": True, "order_id": order_id,
            "status": order["status"]}

def to_function_output(call_id, result):
    # call_id 必须原样保留,便于把结果对应回这次工具调用。
    return {"type": "function_call_output", "call_id": call_id,
            "output": json.dumps(result, ensure_ascii=False)}
Tool calling 错误分类表展示模型错误、边界校验错误与业务错误的结果对象关系
图2:错误结果结构说明图,比较模型侧错误与业务侧错误的返回字段和后续处理边界,不是执行证据。

官方文档允许把工具结果作为字符串传回,字符串内部可以是 JSON、错误码或普通文本。我的取舍是统一使用 JSON,因为监控可以按 okerror_code 聚合,模型也能稳定理解;但内部异常细节不放进去,避免把实现信息暴露给下一轮上下文。

重试只对症下药,别把业务失败变成参数修复

模型错误可以有限次重试,例如只针对 JSON 解码或 schema 不匹配重新请求;未知工具应直接报警,因为它更像注册表漂移。业务错误通常不该自动重试:订单不存在不是换个参数就一定能解决,无权限也不能靠模型多调用几次绕过。若同时开启并行工具调用,日志还要按每个 call_id 独立记录结果,不能把一条失败污染同轮其他合法调用。

我会把以下字段放进结构化日志:调用名、call_id、schema 校验结果、业务错误码、用户上下文摘要、耗时和最终是否重试。这样排查时先看错误层级,再决定修提示词、修工具契约还是修业务服务,不会从一条模糊的“tool failed”开始猜。

常见问题

开启 strict 后还需要应用层校验吗?

需要。strict 主要约束模型生成的参数形状,订单存在性、权限、状态和数据一致性仍必须由应用服务判断。

模型错误要不要把原始异常回传?

不建议。给模型稳定的错误码和修正方向即可;原始 JSON、堆栈和内部字段留在受控日志中。

业务错误应该让模型重新调用工具吗?

只有错误信息明确提示可修正输入时才考虑一次重试,例如订单号格式不对;不存在、无权限和状态冲突通常直接生成解释性答复。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>