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

OpenAI Responses API 工具调用工具参数校验失败时怎样保留可诊断信息怎样完成一个可复用的最小示例

来源:17golang原创

时间:2026-11-10 02:57:03 106浏览 收藏

工具调用最难排查的时刻,不是模型没有返回函数,而是参数校验失败后只剩一句“invalid arguments”。我的处理方式是把失败拆成三层:请求 schema 被拒绝、arguments 不是合法 JSON、JSON 合法但不符合业务 schema。每层保存不同字段,最终给模型的 function_call_output 只返回短错误码,完整诊断留在受控日志里。

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

要点速览
  • Responses API 的工具调用参数是 JSON 字符串,必须用 call_id 对应回传结果。
  • strict=true 不是业务校验的替代品;对象要关闭额外属性,所有属性要列入 required。
  • 日志保存 request_id、call_id、错误路径和脱敏参数,工具回传只保留可重试的错误码。

一、先把严格 schema 写成稳定边界

先定义工具能接受的最小输入。严格模式下,不能把“可选”理解成省略字段;更稳妥的写法是把字段放进 required,再用 ["string", "null"] 表示它允许为空。这样 schema 错误会在请求阶段暴露,而不是等到业务函数里才变成难追的异常。

Responses API function tool 的 strict schema、required 和 additionalProperties 静态边界结构图
图1:工具 schema 的静态边界说明图,展示 strict、required 与可空字段的关系,不是运行截图。
# 安装 SDK 和本地 JSON Schema 校验器
python -m pip install openai jsonschema
import json
import os

# 每个字段都 required;允许缺省语义时,用 null 表达而不是省略字段
ORDER_SCHEMA = {
    "type": "object",
    "properties": {
        "order_id": {"type": "string", "minLength": 1},
        "locale": {"type": ["string", "null"]},
    },
    "required": ["order_id", "locale"],
    "additionalProperties": False,
}

TOOLS = [{
    "type": "function",
    "name": "lookup_order",
    "description": "读取订单的非敏感状态",
    "strict": True,
    "parameters": ORDER_SCHEMA,
}]

# 密钥只从环境变量读取,不把它拼进日志或文章输出
assert os.environ.get("OPENAI_API_KEY"), "请先配置 OPENAI_API_KEY"
print(json.dumps(TOOLS, ensure_ascii=False))

这里的 additionalProperties=false 会阻止未声明字段混入;locale 虽然允许为空,仍然必须出现在 required 中。若直接把严格 schema 写成“只要求 order_id、其他字段随便来”,请求本身就可能被拒绝,日志应该把这类问题归为 schema_request,不要误判为模型生成错误。

二、把失败分层并保留可诊断字段

下面的最小示例故意保留三份信息:请求异常的 request_id,每次函数调用的 call_id,以及截断并脱敏后的原始参数。原始参数不要直接写入生产日志,因为工具参数可能包含手机号、地址或内部编号。

Responses API 工具调用从 request_id、call_id 到参数校验错误回传的分层诊断关系图
图2:工具参数失败的分层诊断说明图,展示 request_id 到 function_call_output 的对应关系,不是运行截图。
import hashlib
import json
import logging
import os
from jsonschema import Draft202012Validator
from openai import OpenAI

logger = logging.getLogger("tool_call")
client = OpenAI()

def lookup_order(order_id, locale):
    # 这里替换成真实查询;示例只返回可供模型继续理解的 JSON 字符串
    return json.dumps({"ok": True, "order_id": order_id, "locale": locale}, ensure_ascii=False)

def redact(raw: str, limit: int = 240) -> str:
    # 只保留有限长度,并用摘要帮助关联同一次失败
    clipped = raw[:limit]
    digest = hashlib.sha256(raw.encode("utf-8")).hexdigest()[:12]
    return f"{clipped}... sha256={digest}" if len(raw) > limit else f"{clipped} sha256={digest}"

def diagnostic(stage: str, *, request_id=None, call_id=None, raw=None, errors=None):
    # 完整诊断进日志,敏感参数经过截断;对模型只返回 error_code
    item = {"stage": stage, "request_id": request_id, "call_id": call_id}
    if raw is not None:
        item["raw_arguments"] = redact(raw)
    if errors:
        item["validation_errors"] = errors[:5]
    logger.warning("tool_call_diagnostic=%s", json.dumps(item, ensure_ascii=False))
    return item

try:
    response = client.responses.create(
        model=os.environ.get("OPENAI_MODEL", "gpt-4.1-mini"),
        input="查询订单 A-100 的状态",
        tools=TOOLS,
    )
except Exception as exc:
    # 请求阶段失败:记录 SDK 能提供的 request_id 和异常类型
    diagnostic("schema_request", request_id=getattr(exc, "request_id", None))
    raise

for item in response.output:
    if item.type != "function_call":
        continue

    raw_args = item.arguments
    try:
        args = json.loads(raw_args)
    except json.JSONDecodeError as exc:
        diagnostic("arguments_json", request_id=response.id, call_id=item.call_id,
                   raw=raw_args, errors=[str(exc)])
        continue

    problems = sorted(Draft202012Validator(ORDER_SCHEMA).iter_errors(args),
                      key=lambda error: list(error.path))
    if problems:
        paths = [".".join(map(str, error.path)) or "$" for error in problems]
        diagnostic("arguments_schema", request_id=response.id,
                   call_id=item.call_id, raw=raw_args, errors=paths)
        # 用 call_id 对回去;模型只看到短错误,不看到内部字段值
        print(json.dumps({"type": "function_call_output",
                          "call_id": item.call_id,
                          "output": json.dumps({"ok": False,
                                                 "error_code": "invalid_tool_arguments"})},
                         ensure_ascii=False))
        continue

    # 只有通过本地校验才进入实际业务函数
    print(lookup_order(args["order_id"], args["locale"]))

示例里的 response.id 用来关联本轮 Responses 请求,item.call_id 用来关联具体工具调用;一次响应可能有多个 function_call,不能只用“最后一个调用”做日志键。生产系统还应把诊断写到有访问控制的日志流,避免把完整参数回传给模型。

三、校验通过后再回传工具结果

校验失败时,业务函数没有被调用,模型收到的是可识别的 invalid_tool_arguments。校验通过时才执行查询,并把结果作为字符串放进 function_call_output。这个结果可以是 JSON、错误码或普通文本,但必须带原来的 call_id;否则多工具并行时容易把结果串错。

失败层应保存给模型的内容
请求 schema异常类型、request_id、schema 版本不重试原请求,先修 schema
JSON 解析call_id、参数摘要、解析错误arguments_json_error
业务 schema字段路径、参数摘要、规则版本invalid_tool_arguments

这里“保留可诊断信息”不等于把所有上下文都塞给模型。对外回传短错误码,给人工和监控保留关联字段,既方便重放,也降低敏感数据扩散。

四、发布前的工具调用检查清单

  • 确认严格 schema 的对象都设置了 additionalProperties=false,每个 property 都在 required 中。
  • 确认日志能关联 request_id、call_id 和 schema 版本,并对原始参数截断、脱敏。
  • 确认 JSON 解析、schema 校验、业务执行分别有独立错误码,成功结果不会覆盖失败诊断。
  • 确认重试只针对可重试的网络或服务错误;参数错误先修调用方或 schema,不要盲目重发。

相关问题

strict=true 能替代本地参数校验吗?

不能。它约束模型输出是否贴合工具 schema,本地校验仍负责业务范围、敏感字段和版本兼容。

为什么日志里必须同时保存 request_id 和 call_id?

request_id 关联整次 Responses 请求,call_id 关联其中某一个工具调用;批量工具调用时二者缺一不可。

校验失败后要不要把完整错误堆栈返回模型?

不建议。模型只需得到稳定的错误码和可修正提示,堆栈、字段路径和脱敏参数留在受控日志中。

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