很多团队把大模型接进业务后,最先翻车的不是“回答不够聪明”,而是结构化输出不稳定

  • 你要 JSON,模型给你“看起来像 JSON”的东西(多了注释/少了引号/多了逗号)
  • 你要固定字段,模型临时加字段/漏字段/类型飘了
  • 你要数组,模型给你字符串;你要数字,模型给你“约 3.5k”

这类问题如果不工程化处理,最后会变成:

上游看似输出了 JSON,下游解析报错 → 重试放大 → 成本上升 → 延迟变差 → 用户体验崩。

这篇文章给一套可复用的工程方案:约束 → 解析 → 校验 → 修复重试 → 降级兜底,并附最小 Python 模板。


0)TL;DR(先给结论)

  • 不要只靠 Prompt“祈祷”:结构化输出需要“解析 + 校验 + 修复”的闭环。
  • 把失败当成正常路径:准备好 3 类失败的兜底:解析失败、校验失败、语义错误。
  • 工程上最稳的路线
    1. 明确 Schema(字段/类型/约束/示例)
    2. 解析时做“JSON 提取”(别直接 json.loads(text)
    3. 用 Pydantic/JSON Schema 校验
    4. 校验失败走“修复提示词”重试(有限次数)
    5. 仍失败就降级输出(返回可读文本 + 错误码)

1)为什么“看起来像 JSON”≠“可解析 JSON”(失败模式清单)

常见失败你大概率都见过:

1.1 语法层失败(parse fail)

  • 末尾多了逗号(trailing comma)
  • 用了单引号 '、或字段名没加引号
  • 混入了注释、Markdown 代码块、解释性文字
  • 字符串里有未转义的换行/引号

1.2 结构层失败(validate fail)

  • 缺字段、字段名拼错
  • 类型不对(数字变字符串/数组变对象)
  • 枚举值越界(你只允许 low/medium/high,它输出了 urgent
  • 约束不满足(items 不能为空,它给了空数组)

1.3 语义层失败(semantic fail)

JSON 语法正确、Schema 也过了,但内容不对:

  • “金额”字段填了描述性文本
  • “日期”字段不在范围内
  • “操作动作”与上下文矛盾

结论:结构化输出要稳,必须把 parse / validate / semantic 三类失败都纳入链路。


2)你应该选哪种“约束方式”?(三种常见路线)

2.1 纯 Prompt 约束(最脆弱)

写法:让模型“只输出 JSON,不要解释”。
问题:在复杂任务、长上下文或温度较高时非常容易漂。

适用:PoC、内部脚本、容错高的场景。

2.2 JSON Schema / Pydantic 约束(最通用)

写法:明确字段、类型、枚举、示例;输出后做程序校验。
优点:可落地、可测试、可回归
缺点:仍需处理“模型没按 Schema 输出”的情况(所以要有修复链路)。

适用:绝大多数生产业务。

2.3 工具调用/函数调用式约束(更强,但要配套)

写法:把输出变成“调用一个函数并填入参数”。
优点:约束更强。
缺点:对 SDK/平台能力、工具定义、权限控制要求更高;仍需要校验与兜底。

适用:工具链成熟、对稳定性要求极高的系统。


3)最小闭环:约束 → 解析 → 校验 → 修复 → 降级

我建议你把结构化输出链路固定成这样(顺序很重要):

LLM输出(text)
  -> 提取JSON片段(extract)
  -> 解析(parse)
  -> 结构校验(validate)
  -> 语义校验(optional)
  -> 修复重试(repair, <=N次)
  -> 降级兜底(fallback)

其中最关键的两个工程点:

  • JSON 提取:别让解释性文字污染解析(尤其是模型喜欢加“好的,以下是 JSON:”)
  • 有限修复重试:修复不是无限循环,必须有次数上限与错误分级

4)Python 最小模板:Pydantic 校验 + 修复重试(可复制)

4.1 定义你的输出 Schema(示例)

以“把一段用户反馈结构化”为例:

from pydantic import BaseModel, Field
from typing import Literal, List


class FeedbackItem(BaseModel):
    category: Literal["bug", "feature", "question"] = Field(..., description="反馈类型")
    severity: Literal["low", "medium", "high"] = Field(..., description="严重程度")
    summary: str = Field(..., min_length=1, description="一句话总结")
    evidence: List[str] = Field(default_factory=list, description="原文证据片段(可为空)")

4.2 JSON 提取:从文本里“捞出”第一个 JSON 对象

import json


def extract_first_json(text: str) -> str:
    """
    最简单可用版:从文本中找到第一个 '{' 到最后一个 '}'。
    生产环境可升级为:基于括号栈的提取,或只提取 ```json ... ```代码块。
    """
    start = text.find("{")
    end = text.rfind("}")
    if start == -1 or end == -1 or end <= start:
        raise ValueError("no_json_object_found")
    return text[start : end + 1]


def parse_json_object(text: str):
    raw = extract_first_json(text)
    return json.loads(raw)

提醒:这是“最小可用”。如果你的模型经常输出多个 JSON、或包含嵌套大括号,建议用“括号栈”更稳。

4.3 修复重试:让模型只做“修 JSON”这件事

修复提示词的关键是:输入原始错误 + 目标 Schema + 只输出 JSON

REPAIR_PROMPT = """你是一个严格的JSON修复器。
任务:把下面的输出修复为“可解析且符合Schema”的JSON。
要求:
1) 只输出JSON本体,不要解释,不要Markdown代码块
2) 字段必须与Schema一致:{schema}
3) 无法确定的字段用最合理的默认值(例如 evidence 为空数组)

原始输出:
{bad_output}

解析/校验错误:
{error}
"""

4.4 完整闭环:最多修复 N 次,仍失败就降级

下面用“伪调用函数”代表你的 LLM 调用(你替换成自己的 SDK 即可):

from pydantic import ValidationError


def call_llm(prompt: str) -> str:
    # TODO: 替换为你的真实大模型调用(OpenAI兼容接口/自建服务都行)
    #我自己用的是大模型API聚合平台147API
    return ""


def run_structured(text: str, max_repair: int = 2) -> FeedbackItem:
    schema = FeedbackItem.model_json_schema()

    # 第一次:正常生成
    prompt = f"""请把下面用户反馈结构化为JSON,字段遵循Schema:{schema}
只输出JSON,不要解释。
用户反馈:{text}
"""
    out = call_llm(prompt)

    last_error = None
    for _ in range(max_repair + 1):
        try:
            obj = parse_json_object(out)
            return FeedbackItem.model_validate(obj)
        except (json.JSONDecodeError, ValidationError, ValueError) as e:
            last_error = str(e)
            if _ >= max_repair:
                break
            out = call_llm(
                REPAIR_PROMPT.format(schema=schema, bad_output=out, error=last_error)
            )

    # 降级:返回一个“可用但不完美”的结构(或抛业务异常)
    raise RuntimeError(f"structured_output_failed: {last_error}")

5)工程落地 Checklist(发布前自检)

5.1 Prompt 与解码参数

  • 明确 Schema(字段/类型/枚举/示例)
  • 输出要求写死:只输出 JSON、本体无解释
  • 温度尽量低(例如 temperature=0~0.2,视模型而定)
  • 限制输出长度(max_tokens)避免截断

5.2 解析与校验

  • 不直接 json.loads(full_text),先做 JSON 提取
  • 用 Pydantic/JSON Schema 校验
  • 校验失败要区分错误类型(parse/validate/semantic)

5.3 修复与降级

  • 修复重试次数有限(例如 1~3 次),避免死循环
  • 失败要有降级:返回可读文本/错误码/兜底模板
  • 记录 repair_count 与错误分布,后续才能优化

6)资源区:当你要做多模型对比或路由时,先把接入层统一

结构化输出的稳定性与成本通常需要做对比评测(不同模型、不同提示词版本)。
工程上更省事的做法是统一成 OpenAI 兼容调用方式(很多时候只改 base_urlapi_key)。

例如某些聚合入口提供 OpenAI 兼容端点[我用的是大模型聚合平台147API](参数以其控制台/文档为准),便于你快速做 A/B 评测与路由试验。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐