让 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 就完事,先做剥离,再做解析,最后做语义校验。
剥离外壳
按顺序尝试:
- 去掉首尾空白
- 若被代码块围栏包裹,取出围栏内的内容
- 从第一个
{或[到最后一个匹配的}或]截取候选片段
这一步只做「定位候选 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,不靠魔法,靠三层工程:
- 提示层:用清晰的契约、示例和 JSON Schema 把输出形状钉住
- 校验层:剥离外壳、标准解析、结构和值域校验,产出具��的失败原因
- 重试层:有界循环,把错误反馈回去,并把失败记录成可观测指标
把这三层写成可复用的包装函数,你的下游流程就不必再为偶尔出现的半截 JSON 做防御式编程。