结构化输出(进阶):复杂 schema、性能调优、失败兜底
谁该读这一篇? 已经用过 json_schema 但想把复杂场景 / 性能边界吃透的开发者。 前置阅读:
02-core-concepts/04-compressed-fsm.md、06-frontend-language/03-json-schema.md。 耗时: 35 分钟 学完能: 1. 写复杂深嵌套 schema(带 oneOf、$ref、自定义 pattern); 2. 优化 schema 编译耗时; 3. 配合 prompt engineering 让模型"愿意"遵守 schema; 4. 处理 schema 失败兜底; 5. 选对 grammar backend。
1. 复杂 schema 的工程问题
1.1 编译耗时
复杂度 → FSM 状态数 → 编译时间:
| Schema | 状态数 | 编译 ms |
|---|---|---|
| 简单 JSON (5 字段) | < 100 | 5-10 |
| 嵌套 JSON (3 层) | 500-1000 | 50-100 |
| oneOf × 5 分支 | 2000-5000 | 200-500 |
| 大型 $ref 复用 | 5000+ | 1-3 s |
第一次见到的 schema 会编译,后续 cache 命中。
优化:
- schema 字符串 deterministic(
json.dumps(s, sort_keys=True))。 - 业务上线 warmup 把所有用到的 schema 预编译。
- 避免大型 oneOf;改用 enum + 后处理。
1.2 Vocab mask 缓存
每个 FSM 状态有一个 vocab mask(vocab_size 大的 bool 张量)。
状态多 → 缓存吃显存。
SGLang 用 LRU 控制 cache 大小,但要监控 grammar_mask_cache_size。
2. 高级 schema 写法
2.1 $ref 复用
schema = {
"$defs": {
"Address": {
"type": "object",
"properties": {
"city": {"type": "string"},
"country": {"type": "string"}
}
}
},
"type": "object",
"properties": {
"home": {"$ref": "#/$defs/Address"},
"work": {"$ref": "#/$defs/Address"}
}
}
xgrammar 和 llguidance 支持 $ref;outlines 部分支持。
2.2 Const 字段
{"type": "object", "properties": {
"version": {"const": "v1"}
}}
强制特定值。等价于 enum 单值。 runtime 直接 jump 整个 "v1"。
2.3 Conditional schema
{
"type": "object",
"properties": {"type": {"enum": ["circle", "square"]}, "radius": {"type": "number"}},
"if": {"properties": {"type": {"const": "circle"}}},
"then": {"required": ["radius"]}
}
⚠️ if/then/else 语义复杂,多数 grammar backend 部分支持或忽略。
推荐改写为 oneOf。
2.4 Discriminated union (oneOf with const)
{
"oneOf": [
{"type": "object", "properties": {"kind": {"const": "user"}, "name": {"type": "string"}}, "required": ["kind", "name"]},
{"type": "object", "properties": {"kind": {"const": "admin"}, "perms": {"type": "array"}}, "required": ["kind", "perms"]}
]
}
效率比一般 oneOf 高(const 让 backend 快速分支)。
2.5 Pattern 进阶
{
"type": "string",
"pattern": r"^\d{4}-\d{2}-\d{2}$" # 日期
}
{
"type": "string",
"pattern": r"^[a-z][a-z0-9_]{2,30}$" # 用户名
}
直接编译进 FSM,速度极快。但慎用 .*(无限分支)。
3. 字段顺序
LLM 生成时按 schema 字段顺序输出(grammar 强制)。 但模型对字段顺序敏感:把"内容核心"放前面会让模型更易生成。
# ❌ 把答案放最后,模型先写一堆 metadata
{
"metadata": {...},
"reasoning": "...",
"answer": "..."
}
# ✓ 答案放前面(reasoning 在 chain-of-thought 上下文里)
{
"answer": "...",
"reasoning": "..."
}
4. Prompt 配合 schema
光开 schema 还不够,prompt 里也要提示:
prompt = """
Extract person info from text. Output JSON with:
- name (string)
- age (integer 0-150)
- email (string in valid email format)
Text: ...
Output:
"""
效果:
- 模型 logit 倾向于"接下来该写 JSON 了"。
- Grammar 在背后强制,但模型自己更易吐对。
避免:
- 让 schema 和 prompt 字段不一致(模型困惑)。
- 让 schema 太严格但 prompt 没说明(生成内容质量低)。
5. 失败兜底
理论上 grammar 100% 保证语法正确。但:
- 模型可能在 schema 之外卡死(一直生成空格直到 max_tokens)。
- 字段语义错(schema 允许的"正确"JSON 但内容胡说)。
兜底:
import json
from jsonschema import validate, ValidationError
try:
output = response_text
parsed = json.loads(output)
validate(instance=parsed, schema=schema)
# 二次业务校验
if not is_business_valid(parsed):
raise ValueError("business check failed")
except (json.JSONDecodeError, ValidationError, ValueError):
# fallback: 重试 / 用默认值 / 告诉用户
...
业务 SLA 高时配 retry 1-2 次。
6. 流式 + schema
stream = client.chat.completions.create(
..., response_format={"type": "json_schema", "json_schema": {...}},
stream=True
)
buf = ""
for chunk in stream:
delta = chunk.choices[0].delta.content or ""
buf += delta
# 增量 partial parse
try:
parsed = partial_json.loads(buf)
# 用 partial parsed UI 更新(如已知 name 字段就先展示)
except:
pass
partial-json 这类库做容错 parse。
7. 性能微调
7.1 关 jump-ahead 看差异
sgl.gen(name, json_schema=..., disable_jump_forward=True)
对比开关下的 TPOT。差距大 → jump-ahead 有用,schema 里多字面常量。 差距小 → schema 主要是自由字段。
7.2 提前 warmup
# 上线脚本
for schema in BUSINESS_SCHEMAS:
requests.post(URL, json={
"model": ..., "messages": [...],
"response_format": {"type": "json_schema", "json_schema": {"schema": schema}},
"max_tokens": 1
})
把所有热 schema 编译进 cache。
7.3 监控 grammar cache
curl /metrics | grep grammar
grammar_cache_hit_rate 应该 > 0.95。
不到 → schema 字符串变化太多。
8. Backend 选型再次回顾
| Schema 特征 | 推荐 |
|---|---|
| 简单 JSON,3-10 字段 | outlines |
| 复杂深嵌套 | xgrammar |
| Discriminated union | xgrammar |
| 大量 const literal | outlines (jump-ahead 强) |
| CFG(不只 JSON) | llguidance |
| Reasoning 模型 | reasoner_grammar |
不确定就跑 benchmark:
for backend in outlines xgrammar llguidance; do
SGLANG_GRAMMAR_BACKEND=$backend python -m sglang.bench_serving \
--dataset-name custom \
--custom-output-format json_schema \
...
done
9. 小结
- 复杂 schema 编译有开销,要 cache + warmup。
- $ref / const / discriminated union 是高效写法。
- if/then 慎用,改 oneOf。
- 字段顺序 + prompt 配合,让模型主动配合。
- 业务侧做 schema 校验 + 业务校验 + 失败兜底。
- 流式 + partial parse 让 UI 更早更新。
10. 自检
- oneOf 慢的根本原因?怎么改成 discriminated union?
答案
`oneOf` 编译为"并行候选 FSM":N 个子 schema 同时存活,每步都要算所有分支的合法 token 并集,状态空间组合爆炸。 Discriminated union 在首字段加 `{"const": "type_value"}` 让 FSM 一开始就能根据这个字段 jump 到具体分支: ```json { "oneOf": [ {"properties": {"kind": {"const": "user"}, "name": {"type": "string"}}, "required": ["kind"]}, {"properties": {"kind": {"const": "admin"}, "perms": {"type": "array"}}, "required": ["kind"]} ] } ``` 编译时 FSM 在看到 `"kind":"u` 就 jump 到第一个分支,看到 `"kind":"a"` 就 jump 到第二个,**没有并行候选**。 速度:discriminated union 与单分支 schema 几乎同速;纯 oneOf 慢 N×。- Schema 字符串变化导致 cache miss 怎么办?
答案
GrammarManager 按 schema 字符串做 cache key,字段顺序 / 空格 / 数值 0 vs 0.0 都会 miss。 解决: (1) **`json.dumps(schema, sort_keys=True)`**:字段按字母序,固定输出。 (2) **业务侧常量化**:把 schema 字符串作为 Python 模块常量定义一次,不要每请求 `json.dumps`。 (3) **Warmup**:业务上线前批量发请求覆盖所有热 schema,让 cache 沉淀。 (4) **监控**:`grammar_cache_miss_rate` 应该 < 5%;高于此值就是字符串不规范。 (5) **避免动态拼接**:不要在 schema 里嵌入 `f"{user_input}"`——每用户独立 schema 就是 cache 灾难。- 字段顺序对模型质量的影响?
答案
Grammar 强制按 schema 顺序输出,但模型质量**对字段顺序敏感**。 原因:模型生成是顺序的,前面已生成内容是后面生成的 condition。 - ❌ `{"metadata": {...}, "reasoning": "...", "answer": "..."}`:模型先编 metadata,再"找理由",再凑答案;答案被"为前文圆话"影响。 - ✅ `{"answer": "...", "reasoning": "..."}` 或 chain-of-thought 模式 `{"reasoning": "...", "answer": "..."}`:先 reasoning 再 answer,逻辑更通顺。 - 数字类字段尽量靠前(让模型"想清楚再说话")。 实战:业务上线前对比"字段顺序 A" vs "字段顺序 B"的业务 eval,挑高分的。同 schema 不同顺序质量能差 10%+。- Grammar 强制语法正确,为什么还要业务校验?
答案
Grammar 保证:JSON **结构合法**(括号配对、类型正确、required 字段都有)。 Grammar 不保证:**业务语义正确**。 例:schema `{"age": {"type": "integer"}}`,模型可以输出 `"age": 9999` 完全合法但显然错。 `{"email": {"type": "string"}}`,模型可以输出 `"email": "not-an-email"`。 `{"date": {"type": "string", "pattern": "..."}}`,模型可以输出语法正确但不可能存在的日期 `"2026-02-30"`。 业务校验补充: - 用 jsonschema 做严格 type / range / pattern 验证(比 grammar 严)。 - 业务字段范围(age ∈ [0, 150])。 - 跨字段一致性(start_date < end_date)。 - 外部 fact-check(email 可触达?地址存在?)。 规则:grammar 是协议层、业务校验是语义层,缺一不可。- Reasoning 模型用 reasoner_grammar 的原因?
答案
Reasoning 模型(DeepSeek-R1 / QwQ / OpenAI o1 类)在生成过程中先吐 `11. 下一步
03-multimodal-vlm.md— VLM 进阶。04-lora.md— LoRA。- 源码:
srt/constrained/。