生产级 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!!!"。