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

结构化输出如何处理模型返回的枚举值错误

来源:17golang原创

时间:2026-09-12 11:40:22 345浏览 收藏

我在做工单自动分类时,最容易被“模型返回了一个不在枚举里的值”带偏:日志里看见 enum mismatch,第一反应往往是再试一次。更稳的处理顺序是先分三层:响应被拒答、输出被截断,还是确实拿到了 JSON 但字段值不在业务集合里。只有第三种才是枚举校验问题。

官方地址:https://developers.openai.com/api/docs/guides/structured-outputs

要点速览
  • 用 JSON Schema 的 enum 写死机器可接受的值,不要只在提示词里描述“请使用这几个词”。
  • 解析顺序是状态判断、结构解析、业务枚举校验;拒答和不完整不能伪装成默认分类。
  • 重试必须改变可恢复条件,并保存脱敏原始响应、schema 版本、请求 ID 和错误类型。

枚举报错通常来自三种不同现场

我现在排查这类问题,会先看响应状态和原始载荷,再看字段值。一个严格的结构化输出接口,在成功时应匹配提供的 JSON Schema;但模型也可能因为安全原因拒答,或者因为达到输出上限而不完整。把这两类情况直接送进业务反序列化,最后看到的往往只是一个模糊的“枚举错误”。

现场典型证据正确动作
契约不匹配JSON 可解析,但 label 不在允许集合检查 enum、模型能力和服务端适配层
拒答响应带 refusal 或没有可用结构化内容转人工或安全兜底,不当作分类结果
不完整状态为 incomplete,原因可能是 max tokens增加预算或缩短输入后重试
结构化输出从模型响应状态到 JSON Schema 枚举校验的静态分层关系图
图1:操作示意图把响应状态、结构化内容、枚举契约和业务结果分成四个边界,帮助定位错误发生在哪一层。

先在 JSON Schema 中固定真正允许的值

提示词里的“只能返回 bug、question、feature”只是自然语言约定,不能代替机器契约。以支持结构化输出的 Responses API 为例,字段应声明为字符串并给出 enum。同时把必填字段写进 required,关闭额外字段,避免下游悄悄接收另一种形状。

{
  "type": "object",
  "properties": {
    "label": {
      "type": "string",
      "enum": ["bug", "question", "feature"]
    },
    "confidence": {"type": "number"}
  },
  "required": ["label", "confidence"],
  "additionalProperties": false
}

请求层再将这个 schema 作为 JSON Schema 格式传入,并启用严格模式(如果当前模型和接口支持)。这里要记住一个边界:严格模式约束的是成功生成的结构,不能保证拒答一定有 label,也不能替你定义“未知”是否是合法业务值。如果业务确实允许无法判断,就把 unknown 明确写进枚举,而不是收到异常后临时改写。

解析前先判断 refusal 和 incomplete

我更愿意把解析器写成一个小的状态机:先判断响应是否完成,再取结构化文本,最后才检查枚举。这样日志会告诉你“为什么没有结果”,而不是把所有失败都记成同一类。

import json

ALLOWED_LABELS = {"bug", "question", "feature"}

def parse_classification(response: dict) -> dict:
    # 拒答不能转换为默认分类,否则会污染业务统计。
    if response.get("status") == "incomplete":
        reason = response.get("incomplete_details", {}).get("reason", "unknown")
        raise RuntimeError(f"incomplete response: {reason}")
    if response.get("refusal"):
        raise RuntimeError("model refusal: keep for review")

    raw = response.get("output_text", "")
    try:
        data = json.loads(raw)
    except json.JSONDecodeError as exc:
        # 原文只进入诊断记录,不直接进入业务表。
        raise ValueError("structured output is not valid JSON") from exc

    label = data.get("label")
    if label not in ALLOWED_LABELS:
        # 业务集合是最后一道防线,防止适配层放宽 schema 后漏检。
        raise ValueError(f"enum mismatch: {label!r}")
    return data

不同 SDK 对拒答和结构化内容的字段封装可能不同,所以不要照抄字段路径就上线;先在适配层把供应商响应归一成 statusrefusalincomplete_detailsoutput_text 四个内部字段,再让业务只依赖这四个字段。

JSON Schema 中 label enum、required 和业务分类集合之间的静态约束关系图
图2:结果示意图展示 label、enum、required、unknown 业务值和人工回退之间的静态边界;unknown 是否存在必须由产品契约决定。

重试必须改变条件,原始响应要能追查

枚举不匹配时不要无限重试同一条请求。若是旧模型或非严格模式,先修正 schema 适配;若是输入含糊,补充分类定义和一个反例;若是 incomplete,缩短上下文或增加输出预算。拒答则应走安全兜底,不能用重试把拒答“洗”成业务结果。

每次失败至少记录 request_id、模型标识、schema_version、错误类型、响应状态和脱敏后的原始响应。原始响应里可能含用户输入、密钥回显或个人信息,落盘前要按字段脱敏,并设置访问权限和保留期限。重试成功后也保留第一次失败的分类,后续才能判断是契约修复有效,还是偶然采样成功。

错误类型是否重试重试改变什么
enum mismatch有条件修 schema、模型适配或输入分类说明
incomplete可以缩短上下文、提高预算或拆分任务
refusal不按普通重试处理执行安全兜底、人工审核或改写业务流程

上线前看五个指标,而不是只看成功率

我会把 enum_mismatchrefusalincomplete、重试次数和人工回退分别统计,并按模型、schema 版本、业务场景切分。这样一次 schema 发布后,能看出是某个枚举被删除、某类输入变长,还是模型适配层没有把状态传出来。

  • 枚举值是否来自同一份版本化契约,前后端和数据表没有各写一套。
  • 严格模式不可用时,客户端是否仍保留本地 JSON 与枚举校验。
  • 拒答和不完整是否有独立状态,不会写入默认标签。
  • 原始响应是否脱敏、可按请求 ID 找回,并有保留期限。
  • 重试是否有次数上限,最终失败是否进入人工或明确的业务兜底。

常见问题

JSON 合法为什么还会枚举错误?

JSON 语法正确只说明文本能被解析,不能说明字段值属于业务允许集合。还要检查 enum、本地类型定义和适配层是否使用了同一份契约。

把未知值加入 enum 就能解决吗?

只有当“无法判断”本身是产品认可的结果才可以加入。否则它会把模型不确定性藏起来,应该保留原始响应并进入人工或业务兜底。

为什么不建议看到枚举错误就重试?

相同输入、相同契约和相同模型可能反复得到同类结果,重试只增加延迟和成本。先确认错误层,再改变 schema、输入、预算或处理路径。

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