目录

08如何让模型“按格式输出”更稳定

让 LLM “按格式输出”更稳定:从随缘到可控的工程实战

“我让它输出 JSON,它给我回了带 Markdown 格式的 JSON,有时候还加一句‘如上所示’……” 这是每个 LLM 开发者都遇到过的“血压升高”时刻。


引言:格式不稳定的痛

你调用 API 时希望拿到这样的输出:

{"name": "张三", "age": 28, "city": "北京"}

但模型却返回:

如上所示,用户的信息如下:

```json
{
  "name": "张三",
  "age": 28,
  "city": "北京"
}
```

希望能帮到你!

你的 JSON 解析器直接报错。😤

让 LLM 稳定输出结构化格式,不是“写个 Prompt 让它尽量遵守”就能解决的。 它需要一套组合拳——Prompt 技巧 + 约束解码 + 后处理校验。

本文从简单到可靠,逐步介绍 6 种工程手段。


1. 纯 Prompt 手段(最轻量,但不够稳定)

策略 1:极简指令 + 示例

prompt = """
提取以下文本中的信息,输出 JSON 格式,不要有其他无关内容。

文本:张三今年28岁,住在北京。

输出:
{"name": "张三", "age": 28, "city": "北京"}
"""

策略 2:明确禁止额外内容

输出格式:纯 JSON 对象,不要有解释、不要有 Markdown 代码块标记、不要有其他文字。

策略 3:用分隔符锁定输出区域

### 输出开始(下一行开始必须是 JSON)###
[模型输出]
### 输出结束 ###

问题:纯 Prompt 手段在 70%~85% 的情况下有效,但总会有“不听话”的时候。 尤其当模型不确定时,更容易“多说话”。


2. 约束解码(Constrained Decoding)—— 最可靠的工程方案

这是目前最稳定的方法:在模型生成 Token 时,就限制它只能输出符合某种格式的内容。

2.1 JSON Mode(OpenAI / Anthropic 原生支持)

OpenAI 的 response_format 参数:

from openai import OpenAI
client = OpenAI()

response = client.chat.completions.create(
    model="gpt-4o-2024-08-06",
    messages=[
        {"role": "system", "content": "提取信息,输出 JSON 对象。"},
        {"role": "user", "content": "张三,28岁,北京"}
    ],
    response_format={"type": "json_object"}  # 👈 关键
)

效果:模型只会输出合法的 JSON,不会有多余文字。

Anthropic Claude 的类似功能:

response = client.messages.create(
    model="claude-3-haiku-20240307",
    system="输出 JSON 格式。",
    messages=[...],
    extra_headers={"anthropic-beta": "json-mode-2024-07-25"}
)

2.2 结构化输出(Structured Outputs)—— OpenAI 最新方案

直接提供 JSON Schema,模型严格按照 Schema 输出:

response = client.beta.chat.completions.parse(
    model="gpt-4o-2024-08-06",
    messages=[...],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "person_info",
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "age": {"type": "integer"},
                    "city": {"type": "string"}
                },
                "required": ["name", "age", "city"]
            }
        }
    }
)

优点

  • ✅ 100% 符合 Schema
  • ✅ 字段类型自动校验(age 一定是 int,不是 “28”)
  • ✅ 必填字段保证存在

2.3 开源替代:Outlines / Guidance / LMQL

如果你使用开源模型(LLaMA、Mistral),可以用这些库实现约束解码:

# Outlines 示例
import outlines

model = outlines.models.transformers("mistralai/Mistral-7B")
generator = outlines.generate.json(model, Person)

result = generator("张三,28岁,北京")
# 输出: {"name": "张三", "age": 28, "city": "北京"}

原理:在生成时,用正则 / 上下文无关文法约束下一个 Token 的候选集。

graph LR A["普通生成<br/>候选: 任意 Token"] --> B["约束解码<br/>候选: 只允许JSON中合法的Token"] B --> C["JSON Key"] --> D["下一个Token只能是 ':'"] D --> E["下一个Token只能是数字或字符串"]

3. 后处理:兜底方案

即使加了约束解码,有些场景(如用户直接调用通用模型)仍需要后处理。

3.1 提取 JSON 块

import re
import json

def extract_json(text):
    # 尝试匹配 ```json ... ``` 中的内容
    pattern = r"```json\s*(\{.*?\})\s*```"
    match = re.search(pattern, text, re.DOTALL)
    if match:
        return json.loads(match.group(1))
  
    # 尝试直接找第一个 { 到最后一个 }
    start = text.find("{")
    end = text.rfind("}")
    if start != -1 and end != -1:
        return json.loads(text[start:end+1])
  
    raise ValueError("No JSON found")

3.2 解析“修复”非标准 JSON

有些模型会输出:

  • 尾随逗号:{"name": "张三",}
  • 单引号:{'name': '张三'}
  • 缺少引号:{name: "张三"}

可以用 jsonlinesdemjsonjson_repair 库:

import json_repair

repaired = json_repair.repair_json(broken_json_string)
data = json.loads(repaired)

3.3 重试机制

def call_with_retry(prompt, max_retries=3):
    for i in range(max_retries):
        response = model.generate(prompt)
        try:
            return json.loads(response)
        except json.JSONDecodeError:
            if i == max_retries - 1:
                raise
            # 追加一条指令要求重新输出 JSON
            prompt += "\n之前的输出不是合法 JSON,请只输出 JSON。"
    return None

4. 输出非 JSON:YAML / 表格 / Markdown

4.1 YAML 输出

YAML 比 JSON 更“人性化”,但解析稍复杂。

Prompt 示例

以 YAML 格式输出,不要有任何额外文字:
---
name: 张三
age: 28
city: 北京

解析

import yaml

data = yaml.safe_load(response)

稳定性技巧:要求用 --- 包裹,便于截取。

4.2 Markdown 表格

模型输出表格很稳定,因为它是自然语言的一部分。

Prompt

输出为 Markdown 表格,三列:姓名、年龄、城市。
| 姓名 | 年龄 | 城市 |
|------|------|------|
| 张三 | 28   | 北京 |

解析:可直接保留给前端渲染,或用 pandas.read_html() 解析。

4.3 CSV / TSV

适合大量数据的批量输出。

Prompt

输出 CSV 格式(逗号分隔,不要表头):
张三,28,北京
李四,32,上海

5. 对比表:各方案选择指南

方案稳定性实现成本适用场景
纯 Prompt 指令⭐⭐ 60%~80%极低原型、非关键路径
JSON Mode(API)⭐⭐⭐⭐ 99%使用 OpenAI/Claude
结构化输出(Schema)⭐⭐⭐⭐⭐ 100%需要类型校验的场景
约束解码(开源自建)⭐⭐⭐⭐⭐ 100%自部署模型 / 离线环境
后处理 + 重试⭐⭐⭐ 80%~95%通用模型的兜底
换用 YAML/表格⭐⭐⭐⭐ 95%可接受非 JSON 的场景

6. 端到端最佳实践:一个推荐架构

flowchart TD A["用户请求"] --> B["使用 JSON Mode / 结构化输出 API"] B --> C{"输出是否合法?"} C -- 是 --> D["解析成功 ✅"] C -- 否 --> E["后处理提取 JSON 块"] E --> F{"提取成功?"} F -- 是 --> D F -- 否 --> G["调用 json_repair 修复"] G --> H{"修复成功?"} H -- 是 --> D H -- 否 --> I["重试请求(最多 3 次)"] I --> B

核心原则

优先用 API 级别的约束解码,后处理是兜底,重试是最后防线。


7. 常见错误与解决

错误现象原因解决方案
输出多了 “Here’s your JSON”模型习惯性解释用 JSON Mode / 结构化输出
JSON 结尾有尾随逗号某些开源模型行为后处理用 json_repair
age 输出成 “28” 字符串Schema 未定义类型用结构化输出强制 integer
多层嵌套 JSON 出错复杂 Schema 模型难遵守拆成多次调用
表格列数对不齐模型算错在示例中给出完整表格

8. 总结:三种层次,按需选择

层次方法什么时候用
L1:轻量写好 Prompt + 后处理提取个人项目、小流量、非关键
L2:靠谱JSON Mode / 结构化输出(API)生产环境、使用 OpenAI/Claude
L3:极致约束解码库(Outlines/Guidance)自部署模型、强合规要求

最终建议:能用 L2 就用 L2,不要把时间花在“写更复杂的 Prompt 逼它输出 JSON”上。 工具已经帮你解决了。