LLMNotes

第 05 章:结构化输出

zjc 于 2026-01-05 发布

这是《LLM 应用开发 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 自然语言输出适合人看,业务系统更需要结构化结果。本章讲如何让模型稳定输出 JSON,如何在服务端校验,以及如何设计失败修复流程。

5.1 为什么需要结构化

用户输入
  -> LLM
     -> JSON
        -> schema 校验
           -> 业务校验
              -> 规则计算 / API / 数据库
                 -> 最终响应

结构化输出的价值:

  1. 让下游程序可解析;
  2. 限定字段和类型;
  3. 降低提示词注入影响面;
  4. 便于自动化测试;
  5. 便于统计错误分布;
  6. 让模型参与流程而非替代流程。

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": "扫描件首页缺失"
}

原则:

  1. 允许 null 或空数组;
  2. 要求说明缺失原因;
  3. 不要让模型编造默认值;
  4. 缺失关键字段时转人工;
  5. 保存原文位置便于回溯。

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 明确契约,用服务端解析和业务规则二次校验,用有限重试处理格式错误,并让关键动作经过确认和审计。模型负责抽取和判断,系统负责最终约束。

思考题

  1. Schema 校验和业务校验的区别是什么?
  2. 为什么要限制 JSON 修复重试次数?
  3. 哪些字段不适合由模型直接赋默认值?
  4. 模型生成 SQL 时如何避免注入和越权?
  5. 结构化输出上线后应该监控哪些指标?