qwen-structured-json-output · ZH · 2026-10-08

让 Qwen 稳定吐 JSON:在 OpenAI 兼容接口上的 Schema 提示与重试循环

介绍在 OpenAI 兼容接口上使用 Qwen 产出结构化 JSON 的实用方法:从 schema 提示脚手架、结果校验到有界重试循环,帮助你把模型输出稳定接入下游程序。

为什么 Qwen 吐 JSON 有时不听话

结构化输出是很多下游流程的前提:字段抽取、分类打标、工具调用参数、批处理流水线。模型本身并不保证输出永远合法 JSON——它只是按概率续写文本。常见失败包括:

  • 前后包裹了自然语言说明,例如「好的,以下是结果:」
  • 使用了 markdown 代码块围栏(``json ... ``)
  • 尾随逗号、单引号、注释等非标准 JSON 语法
  • 字段名漂移或凭空新增字段
  • 该给数组时给了对象,或枚举值不在约定集合内

这些都不是「模型不行」,而是缺少脚手架与校验。下面这套方法在 OpenAI 兼容接口上通用,对 Qwen 同样适用。

第一层:Schema 提示脚手架

目标是在提示里把「输出形状」讲死,减少自由发挥空间。

明确输出契约

在 system 或首条 user 消息里固定一段契约,包含:

  • 只输出一个 JSON 对象(或数组),不要任何解释、前后缀、代码块围栏
  • 字段名、类型、是否必填
  • 枚举字段的合法取值
  • 缺失信息时的约定,例如用 null 而不是省略字段
  • 数字与字符串的边界,例如 ID 用字符串、金额用数字

给出迷你示例

一个极简的输入→输出示例,比大段文字描述更能锚定格式。示例要短,避免模型把它当作要复述的内容。

用 JSON Schema 描述

把字段结构写成 JSON Schema 文本放进提示,比自然语言更少歧义。例如:

{
  "type": "object",
  "required": ["title", "tags", "score"],
  "properties": {
    "title": { "type": "string" },
    "tags": { "type": "array", "items": { "type": "string" } },
    "score": { "type": "number", "minimum": 0, "maximum": 1 }
  },
  "additionalProperties": false
}

如果你的接入点支持 response_format 之类的结构化输出开关,优先启用;不支持时,Schema 文本提示就是主要手段。

把「不确定」也设计进 schema

字段如果可能拿不到,就给它一个显式的空值或 "unknown" 枚举。强迫模型在没有依据时硬编内容,往往比返回空值更糟。

第二层:结果校验

拿到文本后不要直接 JSON.parse 就完事,先做剥离,再做解析,最后做语义校验。

剥离外壳

按顺序尝试:

  1. 去掉首尾空白
  2. 若被代码块围栏包裹,取出围栏内的内容
  3. 从第一个 { 或 [ 到最后一个匹配的 } 或 ] 截取候选片段

这一步只做「定位候选 JSON」,不做修复。

解析与语义校验

  • 语法层:标准 JSON 解析,捕获异常
  • 结构层:必填字段是否存在、类型是否匹配、是否出现多余字段
  • 值域层:枚举是否合法、数值是否在范围内、数组长度是否符合预期

建议把校验写成一个小函数,返回「通过 / 失败原因」。失败原因要具体,例如「缺少字段 tags」「score 超出范围」,方便下一步喂回模型。

不要过度修复

自动补逗号、改单引号这类「宽容解析」在快速原型里能用,但会掩盖提示问题,长期看让输出更难预测。优先把它当作校验失败的信号。

第三层:有界重试循环

校验失败时不要盲目重发同一条请求,也不要不设上限地循环。

重试要把错误带回去

在重试的对话里追加一条反馈,说明上次输出哪里不合规、期望什么。比起单纯重复原提示,带有具体错误的纠正更有效。

设定明确边界

  • 最大尝试次数(例如 2–3 次),超过就向上游返回结构化错误
  • 每次尝试的记录,便于排查是哪类输入更容易失败
  • 重试时可考虑降低采样随机性,让输出更收敛

伪代码骨架

def call_with_schema(prompt, schema, max_attempts=3):
    messages = build_messages(prompt, schema)
    last_error = None
    for attempt in range(max_attempts):
        raw = chat_completion(messages)
        candidate = strip_wrapper(raw)
        ok, data, reason = validate(candidate, schema)
        if ok:
            return data
        last_error = reason
        messages.append({"role": "assistant", "content": raw})
        messages.append({
            "role": "user",
            "content": f"上次输出不合规:{reason}。请只输出符合 schema 的 JSON。"
        })
    raise StructuredOutputError(last_error)

失败要可观测

把「第几次尝试成功」「失败原因分布」记进日志。如果某类输入总是要重试,说明提示契约需要修,而不是无限加重试。

和聚合接入点配合的实践建议

当你在一个 API key 调多个模型的聚合站上跑结构化任务时,可以把这些点做成可切换的配置:

  • 不同模型对 schema 提示的敏感度不同,把提示模板参数化,方便按模型微调
  • 校验与重试逻辑放在你自己的代码里,不依赖某个模型的特有返回字段
  • 记录每次请求用的模型名与是否命中结构化输出开关,方便对比成功率

计费方面,聚合站通常按官方价乘一个系数扣费,贡献 key 侧按另一个系数返 USDC。重试会增加调用次数,因此把「最大尝试次数」控制得合理,也是控制成本的一部分。

小结

让 Qwen 稳定吐 JSON,不靠魔法,靠三层工程:

  1. 提示层:用清晰的契约、示例和 JSON Schema 把输出形状钉住
  2. 校验层:剥离外壳、标准解析、结构和值域校验,产出具��的失败原因
  3. 重试层:有界循环,把错误反馈回去,并把失败记录成可观测指标

把这三层写成可复用的包装函数,你的下游流程就不必再为偶尔出现的半截 JSON 做防御式编程。