Administrator
发布于 2024-01-29 / 3457 阅读
33

LLM 结构化输出:JSON Schema 约束的可靠方案

模型返回的 JSON 总在变花样

让 LLM 抽取工单里的"订单号、问题类型、紧急度"并输出 JSON,结果它时而多回一句解释,时而把字段名写成 order_num 而非 orderId,解析直接炸,下游拿到 null 又是一堆空指针。要做可靠的系统,结构化输出必须可控。我把"约束加重试加校验"几层手段叠起来,把解析失败率从 8% 压到 0.6%。

第一层:用 JSON Schema 约束

OpenAI 等支持 response_format 传 JSON Schema,让模型严格按结构输出:

{
  "response_format": {
    "type": "json_schema",
    "json_schema": {
      "name": "ticket",
      "schema": {
        "type": "object",
        "properties": {
          "orderId": {"type": "string"},
          "type": {"type": "string", "enum": ["退款","物流","其他"]},
          "urgency": {"type": "integer", "minimum": 1, "maximum": 3}
        },
        "required": ["orderId","type","urgency"]
      }
    }
  }
}

这一层叫"约束解码"的思路(模型在生成时就被限制只能吐合法 token)。但它仍可能偶发不守约——我们实测 92% 能严格遵循,剩下 8% 会多输出或漏字段,不能全信,必须再加防御。

第二层:解析失败就重试

约束之外加防御。解析异常时带"错误反馈"重试一次,让模型知道哪错了,通常第二次就能修正:

for (int i = 0; i < 2; i++) {
    String raw = llm.call(prompt + (i>0 ? "上次的JSON解析失败,请严格按格式" : ""));
    try { return objectMapper.readValue(extractJson(raw), Ticket.class); }
    catch (JsonProcessingException e) { lastErr = e; }
}
throw new StructuredOutputException(lastErr);

实测加一次重试后,解析成功率从 92% 升到 99.4%,剩下 0.6% 走人工队列,不影响主流程。重试要控次数,避免模型死循环浪费 token。

第三层:Java 侧类型安全封装

把模型输出映射成强类型,并用 Bean Validation 兜底字段合法性,防止脏数据进下游:

public record Ticket(
    @NotBlank String orderId,
    @Pattern(regexp="退款|物流|其他") String type,
    @Min(1) @Max(3) int urgency) {}

// 校验
validator.validate(ticket).forEach(v -> log.warn("字段非法:{}", v));

record 不可变且天然适配 JSON 绑定,比手写 getter 清爽。校验不通过则进人工队列,不污染下游,也不会因为 orderId 为空导致后续查库出错。

小结

可靠的结构化输出是"约束加重试加校验"三层防御:Schema 把概率问题变成大概率,重试兜偶发失约,Java 类型与校验守最后一道门。别指望模型永远听话,把不确定性的成本用工程手段吸收掉,系统才稳。这层封装我们已经抽成公共组件,所有 LLM 抽取任务都复用。

参考