这是《LLM 应用开发 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 自然语言输出适合人看,业务系统更需要结构化结果。本章讲如何让模型稳定输出 JSON,如何在服务端校验,以及如何设计失败修复流程。
5.1 为什么需要结构化
用户输入
-> LLM
-> JSON
-> schema 校验
-> 业务校验
-> 规则计算 / API / 数据库
-> 最终响应
结构化输出的价值:
- 让下游程序可解析;
- 限定字段和类型;
- 降低提示词注入影响面;
- 便于自动化测试;
- 便于统计错误分布;
- 让模型参与流程而非替代流程。
5.2 JSON Schema
先定义契约:
{
"type": "object",
"required": ["intent", "entities"],
"properties": {
"intent": {
"type": "string",
"enum": ["query_order", "refund", "complaint", "other"]
},
"entities": {
"type": "array",
"items": {
"type": "object",
"required": ["type", "value"],
"properties": {
"type": {"type": "string"},
"value": {"type": "string"}
}
}
},
"confidence": {"type": "number", "minimum": 0, "maximum": 1}
},
"additionalProperties": false
}
Schema 应与 Prompt 版本绑定。模型支持 JSON Schema 时可优先使用原生能力,不支持时用 Prompt 约束加服务端校验。
5.3 Python 校验
from pydantic import BaseModel, Field, ValidationError
from typing import Literal
class Entity(BaseModel):
type: str
value: str
class IntentResult(BaseModel):
intent: Literal["query_order", "refund", "complaint", "other"]
entities: list[Entity]
confidence: float = Field(ge=0, le=1)
def parse_intent(text: str) -> IntentResult:
try:
return IntentResult.model_validate_json(text)
except ValidationError as exc:
raise ValueError(f"invalid model output: {exc}") from exc
校验失败时不要直接把异常暴露给用户,应进入修复流程。
5.4 清洗与提取
模型可能输出代码块或解释文字:
```json
{"intent":"refund"}
常见处理:
```python
import json
import re
def extract_json(text: str) -> dict:
text = text.strip()
match = re.search(r"\{.*\}", text, re.S)
if not match:
raise ValueError("no json object")
return json.loads(match.group(0))
清洗只能处理包装问题,不能修复字段含义错误。业务语义仍需单独校验。
5.5 一次修复策略
def safe_generate(generate, parse, prompt, max_attempts=2):
last_error = None
for i in range(max_attempts):
raw = generate(prompt)
try:
return parse(raw)
except Exception as exc:
last_error = str(exc)
prompt = (
"上一次输出不合法。"
f"错误:{last_error}\n"
"请只输出符合 schema 的 JSON,不要解释。\n"
f"上一次输出:{raw}"
)
raise ValueError(f"parse failed: {last_error}")
建议最多重试一次,避免成本放大。若模型持续失败,应检查 Prompt、schema 和输入长度。
5.6 业务校验
Schema 只能保证“像 JSON”,不能保证“业务正确”。
| 校验层 | 示例 |
|---|---|
| 类型校验 | 金额是 number |
| 枚举校验 | 状态属于固定集合 |
| 引用校验 | 订单号真实存在 |
| 权限校验 | 用户能否查看该订单 |
| 范围校验 | 数量大于 0 且小于库存 |
| 关联校验 | 起始时间早于结束时间 |
| 唯一性校验 | 编号不重复 |
| 事务校验 | 写操作满足前置状态 |
示例:
def validate_order(order_id: str, user_id: str, db):
order = db.get_order(order_id)
if order is None:
raise ValueError("order not found")
if order.user_id != user_id:
raise PermissionError("not allowed")
return order
5.7 数据库写入
不推荐直接把模型输出写库。推荐流程:
模型抽取
-> schema 校验
-> 字段清洗
-> 业务校验
-> 生成变更预览
-> 用户或规则确认
-> 预参数化 SQL 写入
-> 审计日志
SQL 示例:
UPDATE orders
SET status = ?, updated_by = ?, updated_at = NOW()
WHERE id = ? AND version = ?;
表名、列名和排序条件不要从模型输出拼接。
5.8 不确定字段处理
{
"contract_no": null,
"sign_date": null,
"missing_reason": "扫描件首页缺失"
}
原则:
- 允许
null或空数组; - 要求说明缺失原因;
- 不要让模型编造默认值;
- 缺失关键字段时转人工;
- 保存原文位置便于回溯。
5.9 评测结构化质量
| 指标 | 说明 |
|---|---|
| parse success rate | 可解析比例 |
| schema pass rate | 结构合法比例 |
| field precision | 抽取字段正确率 |
| field recall | 应抽字段覆盖率 |
| enum accuracy | 分类准确率 |
| repair rate | 触发修复比例 |
| average tokens | 平均成本 |
| median latency | 中位延迟 |
评测时保存原始输出、解析结果、错误信息和最终人工标注。
5.10 常见故障
| 现象 | 原因 | 处理 |
|---|---|---|
| 输出带 Markdown | Prompt 未禁止 | 清洗并修改 Prompt |
| 字段缺失 | schema 未强制 required | 加校验 |
| 枚举外值 | Prompt 未列全 | 更新枚举和示例 |
| 数组变成字符串 | 类型说明不清 | 示例修正 |
| JSON 截断 | 输出上限 | 检查 finish_reason |
| 字段值幻觉 | 未要求来自原文 | 证据校验 |
| 修复失败率高 | schema 太复杂 | 拆分任务 |
本章小结
结构化输出是 LLM 与业务系统连接的关键。要用 JSON Schema 明确契约,用服务端解析和业务规则二次校验,用有限重试处理格式错误,并让关键动作经过确认和审计。模型负责抽取和判断,系统负责最终约束。
思考题
- Schema 校验和业务校验的区别是什么?
- 为什么要限制 JSON 修复重试次数?
- 哪些字段不适合由模型直接赋默认值?
- 模型生成 SQL 时如何避免注入和越权?
- 结构化输出上线后应该监控哪些指标?