别只靠一句「请输出 JSON」
让模型稳定吐出可解析的结构,要叠三层:提示词里给 schema 和一个完整示例;推理时开模型的 JSON mode 或 grammar 约束解码;出参再做一次 schema 校验,失败带错误信息重试。只做第一层,本地 7B 模型十次里总有两三次给你裹一层 Markdown 代码块、加个尾随逗号,或者把数字写成字符串。三层都做,失败率能压到千分位以下。
服务端与本地各自怎么开
兼容 OpenAI 的接口用 response_format 指定 json_schema 并开 strict,注意 strict 模式要求所有字段都进 required、对象加 additionalProperties: false,枚举用 enum 列死比让模型自由发挥可靠得多。
{
"model": "gpt-4o-mini",
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "extract",
"strict": true,
"schema": {
"type": "object",
"properties": {
"title": {"type": "string"},
"tags": {"type": "array", "items": {"type": "string"}}
},
"required": ["title", "tags"],
"additionalProperties": false
}
}
},
"messages": [{"role": "user", "content": "抽取标题和标签:Ollama 用 num_ctx 控制上下文"}]
}
Ollama 这边 format 可以传 "json",也可以直接传一份 JSON Schema,底层由 llama.cpp 的 GBNF 语法在解码时逐 token 约束,能从根本上保证语法合法。再往下想完全自定义格式,就用 llama.cpp 的 --grammar-file,或者 Outlines、Xgrammar 这类约束解码库,用正则甚至上下文无关文法限定输出形状。
curl http://localhost:11434/api/generate -d '{
"model": "qwen2.5:7b",
"format": {
"type": "object",
"properties": {
"title": {"type": "string"},
"tags": {"type": "array", "items": {"type": "string"}}
},
"required": ["title", "tags"]
},
"prompt": "从下面文本抽取标题和标签,只输出 JSON:Ollama 用 num_ctx 控制上下文长度。",
"stream": false,
"options": {"temperature": 0}
}'
校验、剥壳与重试
即便开了约束,也要做防御性解析:剥掉可能残留的 Markdown 代码围栏、用 json.loads、再用 Pydantic 或 jsonschema 做类型校验;失败时把具体错误拼回提示词重试,最多三次,仍失败就返回默认值并告警,别无限循环。
import json
from pydantic import BaseModel, ValidationError
class Extract(BaseModel):
title: str
tags: list[str]
def parse(raw):
s = raw.strip()
if s.startswith("```"):
s = s.split("
", 1)[1].rsplit("```", 1)[0].strip()
return Extract.model_validate(json.loads(s))
def run(prompt, call_model):
for attempt in range(3):
raw = call_model(prompt)
try:
return parse(raw)
except (json.JSONDecodeError, ValidationError) as e:
prompt += "
上次输出不合法:" + str(e) + ",请严格只输出 JSON 对象。"
raise RuntimeError("结构化输出连续三次校验失败")
踩坑清单
- 类型漂移:模型爱把 "12" 写成字符串、把单个标签写成字符串而不是数组,schema 里把类型和 items 写死。
- 编造字段:冒出个没定义的 confidence 字段,strict 模式下靠 additionalProperties: false 挡。
- 中文转义:部分实现会输出 u 转义,json.loads 能还原,别用字符串截取去抠字段。
- strict 的表达力代价:不支持复杂的 anyOf 组合和可选字段,schema 设计要迁就约束,必要时拆成两次调用。
- 温度仍要设 0:grammar 只管语法不管选词,采样参数不收敛,字段值照样会飘。