05. 结构化输出(JSON/XML)怎么通过 Prompt 保证稳定性?

整理 JSON/XML 结构化输出的稳定性问题、Prompt 约束、API 约束与容错机制。

简单回答

结构化输出的稳定性是 LLM 工程化落地的常见难点——模型可能漏字段、加多余字段、格式错误、或者在某些输入下输出解析失败。保证稳定性的方法从低到高有四层:Prompt 里明确规定格式(给 schema 或示例)、要求模型先确认再输出、用支持结构化输出的 API(如 OpenAI 的 JSON Mode / Structured Outputs)、以及在应用层做解析容错和重试逻辑。工程上这四层最好都做,而不是只依赖其中一层。

详细解答

为什么结构化输出不稳定

大模型的训练目标是语言建模(预测下一个 token),不是"遵守 JSON 语法"。模型在生成 JSON 时,每个字符都是靠概率预测出来的,没有语法级别的约束。特别是在以下情况下容易出错:

字段值里包含引号(导致 JSON 转义问题);字段值很长,模型在生成中途出现截断或跑偏;输入内容触发了模型的对话倾向(比如输入是一个问题,模型想直接回答而不是填 JSON 字段);少见的字段名或嵌套层数深的结构,模型"没见过多少"。

随着模型能力提升,GPT-4、Claude 3.5 这类强模型的结构化输出可靠性已经大幅提升,但在生产环境里"几乎不出错"和"完全不出错"之间的差距仍然很大,工程上必须处理失败 case。

Prompt 层面的最佳实践

用 Schema 而非自然语言描述格式:与其写"请输出一个包含 name、age 和 city 字段的 JSON",不如直接给出 JSON Schema 或一个完整的示例。示例比描述更精确:

请严格按照以下 JSON 格式输出,不要输出任何其他内容:
{"name": "字符串", "age": 整数, "city": "字符串"}

示例输出:
{"name": "张伟", "age": 28, "city": "上海"}

把格式要求放在 Prompt 末尾:模型在生成时对最近的上下文权重更高(Lost in the Middle 效应)。把格式要求放在用户输入之后、临近生成的位置,比放在 System Prompt 开头遵从效果更好。典型结构:[任务描述] + [用户输入] + [格式约束]

用分隔符隔离指令和内容:用 XML 标签或 --- 这类分隔符把格式指令和要处理的内容清晰隔开,减少内容对格式指令的干扰:

<task>分析以下评论的情感,输出 JSON 格式:{"sentiment": "positive/negative/neutral", "confidence": 0到1的浮点数}</task>

<input>这个产品真的太好用了,超出预期!</input>

要求"只输出 JSON,不要其他内容":明确禁止模型在 JSON 前后加入解释性文本("当然,以下是分析结果:"……),否则解析时需要先找到 JSON 的边界。

API 层面:JSON Mode 和 Structured Outputs

JSON Mode(OpenAI、许多其他 API 支持):强制模型输出有效的 JSON 字符串,通过在 decoding 阶段做语法约束实现——在每个 token 生成时,过滤掉所有会导致 JSON 语法错误的 token,只保留合法 token。这保证了语法合法性(不会有格式错误),但不保证字段的完整性和内容的正确性。

Structured Outputs(OpenAI 2024 引入):在 JSON Mode 基础上进一步升级,可以指定完整的 JSON Schema(包括必须有的字段、字段类型、枚举值等),模型输出被约束为完全符合 Schema 的 JSON。这是目前工程上最可靠的结构化输出方案。

from openai import OpenAI
from pydantic import BaseModel

class UserProfile(BaseModel):
    name: str
    age: int
    city: str

client = OpenAI()
response = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=[{"role": "user", "content": "从下面的文本提取用户信息:张伟,28岁,上海人。"}],
    response_format=UserProfile,
)
profile = response.choices[0].message.parsed

对于没有原生 Structured Outputs 支持的模型,outlines、guidance、lm-format-enforcer 等开源库提供了类似的 grammar-constrained decoding 能力,可以在本地部署的模型上使用。

应用层的容错处理

即使做了上面所有工作,生产环境里仍然要有应用层的容错机制:

解析失败后重试:捕获 JSON 解析异常,重新请求模型。通常失败后重试一次的成功率就很高(很多失败是模型在某次请求里恰好出了问题)。但重试要有次数限制,避免无限循环。

宽松解析:用比严格 JSON 解析更宽容的方式处理输出——比如用正则表达式提取 JSON 子串(处理前后有多余文本的情况)、尝试修复常见错误(末尾多了一个逗号、引号未转义等)。json_repair 这类库专门做 LLM 输出的 JSON 修复。

Fallback 处理:如果所有重试都失败,有明确的 fallback 逻辑(返回默认值、触发人工审核、或者给用户友好的错误提示),而不是直接让整个系统崩溃。

面试时可以这样答

结构化输出不稳定是很常见的工程问题,工程上应该分四层来保证。

Prompt 层:给 Schema 或示例而不是用自然语言描述格式,更准确;格式要求放在 Prompt 末尾,紧靠生成位置,比放在开头遵从率更高;用 XML 标签或分隔符把指令和内容隔开;明确说"只输出 JSON,不要任何其他内容"。

API 层:OpenAI 的 Structured Outputs 可以指定完整 Schema 做语法级别的约束,是目前最可靠的方案。对于本地模型,outlines、lm-format-enforcer 这类库能实现类似效果。

应用层:解析失败要有重试逻辑(失败重试一次成功率很高);用宽松解析处理常见的小错误(末尾多逗号、多余文本);有明确 fallback,不能让整个系统因为一次解析失败就崩。

实际工程里这几层最好都做,不能只靠 Prompt 层,因为强模型在边界情况下也会偶尔输出格式错误。

常见追问

  1. Structured Outputs 的 grammar-constrained decoding 是怎么实现的?会不会影响生成质量?
  2. 如果 JSON 的某个字段值是自由文本(比如"用户的反馈"),该字段怎么防止特殊字符导致格式错误?
  3. XML 和 JSON 作为结构化输出格式,哪个对 LLM 更友好?