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

LLM 输出 JSON Schema 不稳定时怎么设计重试

来源:17golang原创

时间:2026-09-10 18:08:15 480浏览 收藏

让大模型“只输出 JSON”并不等于下游一定拿得到可用对象。实际接入时,失败至少有三层:文本不是合法 JSON、JSON 结构不符合 Schema,以及结构正确但业务值不可用。稳妥的做法是把解析和 JSON Schema 校验放在模型调用之后,只针对前两类可修复的格式问题做有限重试;超时、拒答、鉴权失败和业务校验失败应走各自的处理路径。

要点速览
  • JSON Mode 主要解决“能解析”,Schema 校验才负责字段、类型和枚举约束。
  • 一次初始调用加两次修复重试通常足够,重试必须带上结构化错误而不是一句“再试试”。
  • 合法 JSON 也可能不满足业务规则,格式通过后仍要做独立的业务验收。
  • 生产日志至少保留尝试次数、错误类别、Schema 版本和最终处理结果。

先把 JSON Schema 当作应用契约

示例做一个“工单分类”小项目:模型返回分类、优先级和一句摘要。Schema 不只声明字段类型,还用 required 固定必填项,用 enum 限制可接受的优先级,并用 additionalProperties: false 拒绝模型随手增加的字段。

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "category": {"type": "string", "enum": ["bug", "question", "request"]},
    "priority": {"type": "string", "enum": ["low", "normal", "high"]},
    "summary": {"type": "string", "minLength": 8, "maxLength": 120}
  },
  "required": ["category", "priority", "summary"]
}

这一步解决的是“契约写清楚了吗”,不是“模型一定听话”。如果供应商支持 Structured Outputs,可以在请求层传入同一份 Schema;仍建议在应用侧再次校验,因为模型、供应商和业务规则并不总是完全一致。

LLM JSON Schema 工单分类的提示词、响应格式、解析器、Draft 2020-12 校验器与修复提示之间的静态依赖关系
图1:查看生成边界、应用校验边界和错误反馈边界,理解 JSON 文本如何对应到应用契约。

把解析与 Schema 校验放在模型调用之后

校验函数先处理语法,再处理结构。不要捕获所有异常后只返回“格式错误”,否则模型无法知道是缺少字段、类型不对还是枚举值超出范围。下面的代码用 Draft202012Validator.iter_errors 收集多个问题,并保留字段路径。

import json
from jsonschema import Draft202012Validator

SCHEMA = {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "additionalProperties": False,
    "properties": {
        "category": {"type": "string", "enum": ["bug", "question", "request"]},
        "priority": {"type": "string", "enum": ["low", "normal", "high"]},
        "summary": {"type": "string", "minLength": 8, "maxLength": 120},
    },
    "required": ["category", "priority", "summary"],
}

validator = Draft202012Validator(SCHEMA)

def parse_and_validate(raw: str):
    # 先判断文本是不是 JSON,再判断对象是否符合字段契约。
    try:
        payload = json.loads(raw)
    except json.JSONDecodeError as exc:
        return None, f"非法 JSON:{exc.msg}"

    # 按字段路径排序,让修复提示稳定、便于日志聚合。
    errors = sorted(validator.iter_errors(payload), key=lambda item: list(item.path))
    if errors:
        details = []
        for error in errors[:4]:
            path = ".".join(str(part) for part in error.path) or "$"
            details.append(f"{path}: {error.message}")
        return None, ";".join(details)
    return payload, None

只对可修复的格式错误做有限重试

重试不是把原问题重复发送,而是把“返回 JSON、不要新增字段、修复这些具体错误”变成下一次调用的明确约束。示例设置总尝试次数为 3,即初始请求加两次修复请求;同时截断上一轮输出,避免错误响应不断膨胀上下文。

def classify_ticket(call_model, ticket_text: str, max_attempts: int = 3):
    # call_model 只负责调用模型并返回文本,网络重试应在它自己的层处理。
    prompt = f"将下面工单整理为 JSON,只返回对象:{ticket_text}"
    raw = call_model(prompt)

    for attempt in range(max_attempts):
        value, error = parse_and_validate(raw)
        if value is not None:
            # Schema 通过后,再交给业务层检查,不在这里混合规则。
            return value

        if attempt == max_attempts - 1:
            raise ValueError(f"JSON 输出连续失败,attempts={max_attempts},error={error}")

        # 修复提示只携带必要上下文,避免让模型重复解释或输出 Markdown。
        repair_prompt = (
            "只返回符合给定 JSON Schema 的 JSON 对象,不要 Markdown 代码块。"
            f"\n校验错误:{error}\n上一次输出:{raw[-6000:]}"
        )
        raw = call_model(repair_prompt)

    raise AssertionError("不可达分支")

如果使用 Hugging Face Inference Providers,文档区分了 JSON Mode 和 Structured Outputs:前者强调可解析 JSON,后者把预定义 Schema 作为响应约束。即使上游有严格结构化输出,这段应用侧校验仍然适合做版本兼容、字段最小长度和业务前置检查。

把业务失败与格式失败分开验收

现象是否做 Schema 重试处理建议
缺少字段、类型错误、枚举值不对可以,最多两次回传字段路径和允许值
超时、鉴权失败、供应商 5xx不做格式重试交给网络层退避并记录请求 ID
JSON 合法但工单编号不存在不做 Schema 重试走业务校验或人工兜底
连续达到尝试上限停止保存错误类别、Schema 版本和原始响应摘要

验收时至少观察三项:成功率按“初次成功/修复后成功/最终失败”拆分;平均尝试次数;不同错误类型的占比。若大量请求都在同一字段失败,优先检查 Schema 与提示词是否冲突,而不是继续增加重试次数。

LLM JSON 输出中格式错误、业务错误、重试预算、修复提示和人工兜底的边界关系
图2:把可修复的格式层与不可由 Schema 解决的业务层分开,重试预算才有明确终点。

常见问题

JSON 能被 json.loads 解析,还需要 Schema 校验吗?

需要。解析只说明语法成立,不能证明字段存在、类型正确、枚举值合法或没有额外字段。

每次失败都把完整原文塞回提示词可以吗?

不建议。保留截断后的原文和精简错误即可,完整响应应进入受控日志,避免上下文和敏感数据无边界增长。

把最大重试次数调到 10 次是不是更稳?

通常不是。超过两三次仍失败往往意味着 Schema、提示词或模型能力不匹配,应转人工兜底或切换结构化输出能力更合适的模型。

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