为什么只靠 Prompt 让 LLM 输出 JSON 不可靠

生产级 LLM 的结构化输出,不应该依赖"模型自觉遵守格式",而应该通过"生成阶段约束 + Schema 设计 + 应用层验证 + Retry + 监控"把不确定性一层层封住。

1. 为什么只靠 Prompt 让 LLM 输出 JSON 不可靠

假设你写:

请严格输出 JSON,不要输出任何解释。

看起来很明确,但对 LLM 来说,这仍然只是自然语言指令

模型本质上在做的是:

复制代码
根据前面的 token
预测下一个 token 的概率分布
↓
选择一个 token
↓
继续预测

它并没有一个天然的"JSON 编译器"在内部检查:

json 复制代码
{
  "name": "Alice",
  "age": 20
}

是不是严格合法。

所以模型可能输出:

csharp 复制代码
Sure! Here is the JSON:

{
  "name": "Alice",
  "age": 20,
}

人类一看就知道什么意思,但程序执行:

scss 复制代码
json.loads(response)

会直接失败。

一个很重要的思维方式:

JSON 正确性不是一个单次判断,而是很多 token 决策共同组成的结果。

一个长 JSON 里,只要一个位置生成错:

复制代码
"
,
}
[
null
true

就可能使整个结果失效。文章因此把"只靠 Prompt"视为概率保证,而不是工程保证。 所以:

makefile 复制代码
Prompt:
"Please output valid JSON"

本质是:
模型"尽量"遵守

而不是:
系统"保证"它遵守

这两者是生产系统里非常大的区别。


2. 三类典型失败

把失败分成三层,非常值得记住:

复制代码
Syntax
   ↓
Schema
   ↓
Semantics

先看前两层以及 Hallucinated Structure。

Syntax Failure:语法坏了

例如:

arduino 复制代码
{
  'name': 'Alice'
}

这是 Python dict 风格,不是合法 JSON,因为 JSON 必须使用双引号。

或者:

json 复制代码
{
  "name": "Alice",
}

尾随逗号。

或者:

csharp 复制代码
Here is your JSON:

{
  "name": "Alice"
}

如果你的 parser 期待整个 response 都是 JSON,也会失败。

所以 Syntax Failure 的问题是:

javascript 复制代码
连 JSON parser 都过不了。

也就是:

scss 复制代码
json.loads(...)

直接报错。

常见问题包括混用引号、尾随逗号、未加引号的 key、JSON 前的解释文本,以及输出被 token limit 截断等。


Schema Compliance Failure:JSON 合法,但结构不符合要求

比如你要求:

json 复制代码
{
  "user_id": 123,
  "tags": ["ai", "llm"]
}

Schema:

json 复制代码
{
  "type": "object",
  "properties": {
    "user_id": {
      "type": "integer"
    },
    "tags": {
      "type": "array"
    }
  },
  "required": ["user_id", "tags"]
}

LLM 返回:

json 复制代码
{
  "user_id": "123",
  "tags": "ai"
}

这个 JSON:

javascript 复制代码
Syntax           ✅
JSON parse       ✅
Schema           ❌

问题在于:

php 复制代码
user_id 应该 integer
却返回 string

tags 应该 array
却返回 string

或者直接漏字段:

json 复制代码
{
  "user_id": 123
}

仍然:

javascript 复制代码
JSON合法 ✅
Schema ❌

尤其是层级很深的 Schema,模型保持结构一致性会越来越困难,因此嵌套过深会明显提高出错概率。


Hallucinated Structure:结构"看着很合理",但不是你的结构

这是很危险的一类。

你要求:

json 复制代码
{
  "analysis": "...",
  "score": 0.9
}

模型输出:

json 复制代码
{
  "analysis_result": "...",
  "score": 0.9
}

人看:

analysis_result 和 analysis 差不多嘛。

但代码:

css 复制代码
result["analysis"]

可能直接失败。

更麻烦的是某些语言/框架会:

复制代码
忽略未知字段

于是系统继续运行。

最后表现成:

javascript 复制代码
LLM 调用成功
JSON parse 成功
程序没报错

但是数据丢了

这就是所谓:

silent failure(静默失败)

这类问题特别危险,因为它不会马上爆炸,而可能在下游几步之后才出现。


3. Constrained Decoding 到底是怎么工作的

这是整篇文章最重要的技术概念之一。

普通生成:

erlang 复制代码
LLM
 ↓
预测所有 token 的概率

"{"       0.15
"Hello"   0.10
"Sure"    0.08
"["       0.07
"name"    0.05
...

模型理论上可以选任何 token。

而 Constrained Decoding 会增加一个:

css 复制代码
Grammar / Schema Filter

变成:

markdown 复制代码
LLM probabilities
        ↓
Schema / Grammar 检查
        ↓
把非法 token 概率设为 0
        ↓
只从合法 token 中选择

比如现在已经生成:

json 复制代码
{
  "sentiment":

Schema 是:

json 复制代码
{
  "sentiment": {
    "enum": [
      "positive",
      "negative",
      "neutral"
    ]
  }
}

此时:

arduino 复制代码
"positive"    ✅
"negative"    ✅
"neutral"     ✅

"banana"      ❌
123           ❌
{             ❌

那么 decoder 会直接屏蔽非法选择。

概念上类似:

css 复制代码
for token in vocabulary:

    if grammar.allows(token):
        keep_probability(token)

    else:
        probability[token] = 0

然后重新归一化概率:

复制代码
合法 token
↓
继续 sampling

所以最关键的一点是:

不是生成完再检查,而是在生成的时候就不允许错误发生。

OpenAI 对 Structured Outputs 的公开说明也明确描述了这种动态 constrained decoding:系统根据"已经生成的 token + Schema"不断计算下一步允许的 token,并把非法 token mask 掉。

可以把它理解成:

sql 复制代码
Prompt only

LLM
 ↓
"拜托你遵守语法"

vs

Constrained decoding

LLM
 ↓
语法门卫
 ↓
非法 token 根本出不来

因此可靠性是完全不同的。


4. JSON Mode 和 Structured Outputs 的区别

这是最容易混淆的地方。

JSON Mode

JSON Mode 保证的主要是:

javascript 复制代码
输出是 valid JSON

例如:

json 复制代码
{
  "foo": "bar"
}

但是你本来要求:

json 复制代码
{
  "sentiment": "positive",
  "confidence": 0.95
}

模型依然可能给:

json 复制代码
{
  "foo": "bar"
}

JSON 是合法的,所以:

javascript 复制代码
JSON Mode:

Syntax ✅

Schema ???

OpenAI 官方目前仍明确说明:JSON mode 确保有效 JSON,但并不保证匹配某个特定 Schema


Structured Outputs

Structured Outputs 的目标是:

diff 复制代码
JSON合法
+
符合指定 Schema

比如 Schema:

json 复制代码
{
  "type": "object",
  "properties": {
    "sentiment": {
      "type": "string",
      "enum": [
        "positive",
        "negative",
        "neutral"
      ]
    },
    "confidence": {
      "type": "number"
    }
  },
  "required": [
    "sentiment",
    "confidence"
  ],
  "additionalProperties": false
}

那么结构约束会禁止:

json 复制代码
{
  "foo": "bar"
}

也禁止:

json 复制代码
{
  "sentiment": 123
}

以及:

json 复制代码
{
  "sentiment": "happy"
}

所以可以记成:

模式 JSON合法 Schema合规
Prompt only 不保证 不保证
JSON Mode ❌ 不保证
Structured Outputs

这是整个结构化输出体系里最重要的区别之一。


5. 为什么 Schema Design 本身就是 Reliability Engineering

很多人认为:

ini 复制代码
Schema = 数据格式

其实对于 LLM 更准确的是:

diff 复制代码
Schema
=
数据格式
+
模型提示
+
输出空间定义
+
业务规则的一部分

举个例子。

Schema A:

makefile 复制代码
sentiment: str

实际上允许:

php 复制代码
positive
negative
neutral
happy
angry
good
bad
mixed
unclear
...

模型选择空间非常大。

Schema B:

makefile 复制代码
sentiment: Literal[
    "positive",
    "negative",
    "neutral"
]

选择空间直接缩小成:

复制代码
3 个

更进一步:

css 复制代码
sentiment: Literal[
    "positive",
    "negative",
    "neutral"
] = Field(
    description=
    "Customer sentiment based only on explicit tone."
)

Schema 甚至开始承担 Prompt 的作用。

所以:

ruby 复制代码
好的 Schema
↓
减少模型自由度
↓
减少歧义
↓
减少 retry
↓
减少边缘错误
↓
提高 downstream reliability

Schema Design is Reliability Engineering。

Google 当前的 Structured Outputs 文档也明确建议使用清晰的 description、强类型和 enum,同时仍在应用层验证值。


6. description、required、嵌套层级怎么设计

description

不要只写:

makefile 复制代码
sentiment: str

更好:

ini 复制代码
sentiment: str = Field(
    description=
    "Customer sentiment based on explicit wording "
    "in the message, not inferred intent."
)

因为 Schema 通常会被模型看到。

于是:

复制代码
description
≈
字段级 Prompt

把它称为一种"嵌入类型系统中的 Prompt Engineering"。


required

假设:

json 复制代码
"properties": {
  "email": {"type": "string"}
}

但没有:

json 复制代码
"required": ["email"]

模型可能认为:

复制代码
不知道 email
→ 那我就不输出好了

如果业务逻辑实际上依赖 email:

scss 复制代码
send_email(result.email)

问题就被推迟到了下游。

所以重要业务字段应该明确 required。


限制 nesting

不推荐:

markdown 复制代码
customer
 └── profile
      └── preferences
           └── communication
                └── email

这种 5 层结构。

文章建议通常控制在:

复制代码
2--3 层

如果不断出现:

css 复制代码
A
 └ B
    └ C
       └ D
          └ E

通常应该考虑:

复制代码
flatten

或者:

复制代码
拆成多个 LLM calls

例如:

json 复制代码
{
  "customer_name": "...",
  "preferred_language": "...",
  "email_opt_in": true
}

比复杂树状结构容易生成、验证、调试。


7. Pydantic Validators + Instructor + Retry

现在进入第二道防线。

Structured Outputs 能做到:

复制代码
结构正确

但不一定做到:

复制代码
业务正确

所以你可以定义:

python 复制代码
class ExtractedData(BaseModel):
    entity_name: str
    confidence: float

然后 Validator:

python 复制代码
@field_validator("confidence")
def valid_confidence(cls, v):
    if not 0 <= v <= 1:
        raise ValueError(
            "confidence must be between 0 and 1"
        )
    return v

LLM 返回:

json 复制代码
{
  "entity_name": "OpenAI",
  "confidence": 4.7
}

结构上:

javascript 复制代码
JSON     ✅
Schema   ✅

因为:

ini 复制代码
confidence = number

但业务规则:

复制代码
0 <= confidence <= 1

不满足。

于是 Pydantic:

复制代码
ValidationError

Instructor 则可以把这个错误重新喂给 LLM:

vbscript 复制代码
Your previous response failed validation:

confidence must be between 0 and 1.

Please correct the response.

模型第二次:

json 复制代码
{
  "entity_name": "OpenAI",
  "confidence": 0.92
}

于是:

erlang 复制代码
Pass.

完整流程:

go 复制代码
LLM
 ↓
Structured Output
 ↓
Pydantic
 ↓
Validator
 ↓
PASS ─────────→ downstream

FAIL
 ↓
validation error
 ↓
Instructor
 ↓
Retry LLM
 ↓
重新验证

这就是所谓:

Validation → Feedback → Retry Loop。

retry 应该有限,一般作为处理长尾错误的机制,而不是用 retry 去掩盖糟糕 Prompt 或糟糕 Schema。


8. 为什么 structural correctness ≠ semantic correctness

这是最重要的一层。

例如输入:

复制代码
这个产品太垃圾了,我再也不会买。

输出:

json 复制代码
{
  "sentiment": "positive"
}

Schema:

json 复制代码
{
  "sentiment": {
    "type": "string",
    "enum": [
      "positive",
      "negative",
      "neutral"
    ]
  }
}

检查结果:

javascript 复制代码
JSON合法                  ✅

Schema Validation        ✅

Semantic Correctness     ❌

为什么?

Schema 只知道:

复制代码
positive 是允许值之一

它不知道:

复制代码
原文表达的是愤怒/负面情绪

也就是说:

yaml 复制代码
Schema:
"这个答案能不能长这样?"

Semantic validation:
"这个答案到底对不对?"

这是两个完全不同的问题。


9. Schema Validation 与 Semantic Validation 的边界

可以把系统理解成四层。

javascript 复制代码
Layer 1
Syntax validation

是不是合法 JSON?

例如:

scss 复制代码
json.loads(...)

arduino 复制代码
Layer 2
Schema validation

字段、类型、enum 是否正确?

例如:

javascript 复制代码
Pydantic
JSON Schema
Zod

复制代码
Layer 3
Business validation

业务约束是否满足?

比如:

css 复制代码
confidence ∈ [0,1]

end_date >= start_date

price >= 0

currency 必须和账户一致

复制代码
Layer 4
Semantic validation

内容跟原始信息一致吗?

例如:

makefile 复制代码
原文:
"这个产品太垃圾"

sentiment:
positive

→ semantic failure

最后一层最难。

它可能需要:

sql 复制代码
规则
ground truth
另一个模型
cross-check
RAG
数据库
人工审核

才能确认。

因此:

javascript 复制代码
JSON Schema
不能证明
LLM 是对的

它只能证明
LLM 的答案"长得符合规定"

即使有 Schema enforcement,应用层验证仍然必要,因为结构约束无法捕获所有语义错误。

Google 的官方文档同样提醒,即使 Structured Outputs 产生符合格式的 JSON,也应该继续在应用层验证 schema-compliant 但语义错误的值。


10. 生产环境应该监控什么

文章重点给出三个指标。

Schema validation failure rate

定义可以理解成:

复制代码
第一次输出 Schema validation 失败的请求数
──────────────────────────────
总 LLM 请求数

例如:

ini 复制代码
100,000 calls

1,500 schema failures

= 1.5%

这个指标主要反映:

复制代码
结构可靠性

如果用了严格 structured output 后还大量失败,就应该调查 Schema、模型调用方式、截断、供应商限制等问题。


Retry Rate

例如:

ini 复制代码
100,000 calls

12,000 至少 retry 一次

Retry rate = 12%

这通常是一个非常好的"系统健康度"信号。

假设原来:

shell 复制代码
2%

突然:

shell 复制代码
14%

你就需要调查:

复制代码
模型版本更新?
Prompt 更新?
Schema 更新?
用户输入分布改变?
新语言进入流量?
长输入变多?

特别强调:

Retry rate 上升往往是 drift 的早期信号。


Downstream Data Quality

这是最容易被忽略的。

因为:

复制代码
Schema Validation
只能捕获结构错误

真正危险的是:

复制代码
结构完全正确
但内容错了

例如信用审核:

json 复制代码
{
  "risk": "low",
  "reason": "..."
}

结构完美。

但正确答案其实:

ini 复制代码
risk = high

这种错误只能通过:

复制代码
下游异常
人工审核
业务指标
ground truth

发现。

所以成熟系统监控的是:

diff 复制代码
LLM boundary metrics
+
business outcome metrics

而不仅仅是:

go 复制代码
API error rate

建议前两个指标做告警,对 downstream quality 做持续抽样和异常分析。


11. 自托管模型和云端 API 应该怎么选技术栈

这部分可以总结成两个典型架构。

自托管模型

例如:

复制代码
Llama
Qwen
Mistral
DeepSeek

部署:

复制代码
vLLM
TGI
llama.cpp

推荐思路:

javascript 复制代码
Application
     ↓
Pydantic / JSON Schema
     ↓
Outlines / grammar engine
     ↓
vLLM / TGI / llama.cpp
     ↓
LLM

Outlines 可以从:

javascript 复制代码
Pydantic
JSON Schema
function signature

构造结构约束,用于结构化生成。它的官方文档也展示了直接从 Pydantic model 或 JSON Schema 生成符合结构的 JSON。

因此典型技术栈:

diff 复制代码
vLLM
+
Outlines
+
Pydantic
+
application validators
+
observability

你拥有:

复制代码
模型
推理服务器
decoding
schema
validator

整个栈的控制权。

优点:

复制代码
可控
可定制
可能降低大规模成本
数据不需要发送给第三方

代价:

复制代码
GPU
部署
扩缩容
模型升级
grammar integration
observability

都需要自己承担。


云端 API

例如使用 OpenAI / Google 等提供商时,推荐尽量使用供应商原生 Structured Outputs,而不是自己写:

arduino 复制代码
"Please return JSON"

架构:

javascript 复制代码
Application
     ↓
Pydantic / Zod
     ↓
JSON Schema
     ↓
Cloud API
     ↓
Provider constrained decoding
     ↓
Structured response
     ↓
Pydantic Validators
     ↓
Retry / fallback

例如 OpenAI 的 Structured Outputs 可以通过严格 Schema 约束来保证输出匹配受支持的 JSON Schema;其官方说明明确区别了它与 JSON mode。

Google Gemini 当前也支持 JSON Schema Structured Outputs,并可以在 Python 使用 Pydantic、JavaScript 使用 Zod 定义 Schema。

所以文章推荐的云端思路本质上是:

diff 复制代码
Native Structured Outputs
+
Pydantic
+
Instructor / retry
+
semantic validators
+
monitoring

而不是:

diff 复制代码
Prompt
+
regex
+
json.loads()
+
祈祷

对此的结论很明确。


把整套生产架构画出来

最终,一个比较成熟的系统大概是:

typescript 复制代码
                User Input
                    │
                    ▼
              Prompt / Context
                    │
                    ▼
             JSON Schema
        description / enum
        required / shallow nesting
                    │
                    ▼
               LLM inference
                    │
           Constrained Decoding
                    │
                    ▼
          Structurally valid JSON
                    │
                    ▼
           Pydantic / Zod
            Schema validation
                    │
          ┌─────────┴─────────┐
          │                   │
        FAIL                 PASS
          │                   │
          ▼                   ▼
      Retry loop        Business Validator
                              │
                       ┌──────┴──────┐
                       │             │
                     FAIL           PASS
                       │             │
                       ▼             ▼
                  Retry / Review   Semantic Check
                                      │
                                      ▼
                                  Downstream
                                      │
                                      ▼
                                  Monitoring

而监控:

复制代码
Schema failure rate
Retry rate
Semantic failure rate
Downstream quality
Latency
Token cost

共同构成你的 reliability loop。

所以这篇文章真正想改变的,不只是"怎么让 LLM 输出 JSON",而是一种工程思维:

sql 复制代码
错误思维:

LLM
↓
Prompt 写严格一点
↓
希望输出正确


生产思维:

LLM
↓
Constrained Decoding
↓
Schema
↓
Validation
↓
Retry
↓
Semantic Check
↓
Monitoring

前者依赖模型行为,后者依赖系统机制。结构化输出可靠性问题,主要应该通过架构解决,而不是不停更换模型或者往 Prompt 里增加"IMPORTANT: STRICTLY RETURN JSON!!!"。

相关推荐
Ai-_Man18 分钟前
千问能否电脑批量导出?深度拆解「AI导出鸭」如何让这件事从“反人性”变成“优雅”
人工智能·ai·小程序
深圳市益普科技有限公司20 分钟前
半导体智造升级大势所趋!深圳益普科技:以自研工业软件,助力制造业国产化转型
人工智能
Zzz 小生31 分钟前
Lazy Theta*:把昂贵的视线检测留到“真正需要”时再做
人工智能·算法·贪心算法·推荐算法
阿图灵32 分钟前
OpenCV 绘图五件套:画圆、文本、线段、矩形与椭圆
图像处理·人工智能·python·opencv·计算机视觉·绘图
水如烟35 分钟前
孤能子视角:EIS下的宏观、微观时空,以及光速常数与虚空背景
人工智能
slacker-kian40 分钟前
HuggingFace API加载模型超时:用 ModelScopeAPI 替代
人工智能·python·transformer·huggingface·blip·modelscope·blipprocessor
mmsx42 分钟前
用正则把数百个文件抽成知识图谱:三版迭代,从“能用“到“可移植“
人工智能·知识图谱
罗西的思考1 小时前
【OpenClaw具身硬件】ZeroClaw 源码阅读笔记(1)— 总体
人工智能·笔记·深度学习·机器学习
cuguanren1 小时前
Agent 框架的会话记忆管理机制
人工智能·ai·大模型·llm·agent·上下文管理·会话记忆管理