让大模型稳定吐出 JSON:结构化输出的五种姿势与工程兜底

让大模型稳定吐出 JSON:结构化输出的五种姿势与工程兜底

脱敏说明:本文为大模型 API 教学项目的知识点提炼,所有示例采用 OpenAI 兼容的通用接口写法,不绑定任何模型厂商。

一、为什么"让模型输出 JSON"是个真问题

业务需要的从来不是一段散文,而是程序可以直接消费的结构:提取出的销售线索(姓名、意向度、预算)、切片清单(编号、起止时间、标题、分数)、考核评分(题号、分数、评语)。直接在提示词里写"请输出 JSON"会遇到三类失败:JSON 外裹 markdown 代码块、字段缺失或类型错误、遇到复杂 schema 干脆输出散文。

本文从最原始的"提示词约定"讲到厂商原生结构化输出,并给出生产环境的解析兜底模板。

二、姿势一:提示词约定 + 手工解析

python 复制代码
prompt = "请严格输出 JSON 数组,每项包含 name、intent、budget 三个字段,不要输出其他内容"
content = llm.call(prompt)
data = json.loads(content)   # 现实中会在这里不断抛异常

能用,但脆弱。只适合原型验证。

三、姿势二:Pydantic 定义 Schema

用数据模型表达"我要什么",字段描述同时写给模型和程序看:

python 复制代码
from pydantic import BaseModel, Field
from typing import Literal

class SalesLead(BaseModel):
    name: str = Field(description="客户称呼,未提及时为空字符串")
    intent: Literal["high", "medium", "low"] = Field(description="意向程度")
    budget: int | None = Field(description="预算,单位元,未提及为 null")

class LeadList(BaseModel):
    leads: list[SalesLead]

Pydantic 的价值有两层:运行时校验(类型不符直接报错并可触发重试)、自动生成 JSON Schema 供工具调用或原生结构化输出使用。所有需要结构化结果的地方都应该先定义 Pydantic 模型------它是模型契约和程序契约的同一份真源。

四、姿势三:Function Calling / Tool Calling 间接结构化

很多模型早期没有原生 JSON 模式,工程上的通用技巧是把目标 schema 伪造成一个工具:

python 复制代码
tools = [{
    "type": "function",
    "function": {
        "name": "submit_leads",
        "description": "提交提取到的销售线索",
        "parameters": LeadList.model_json_schema(),
    },
}]

模型为了"调用工具"会严格按 schema 产出参数,再从工具调用参数里取出对象。教学项目里的"结构化输出"在很长一段时间都是这个套路,优点是兼容所有支持工具调用的模型。

五、姿势四:厂商原生结构化输出

主流 API 已普遍支持 response_format 直接约束 schema,框架层负责把 Pydantic 模型传下去并自动解析:

python 复制代码
# 通用写法示意:模型保证返回符合 schema 的对象
result = llm.with_structured_output(LeadList).invoke(messages)
# result 直接是 LeadList 实例

这是目前最推荐的常规姿势:开发体验好、稳定性高。代价是与厂商实现质量绑定,个别小模型对复杂嵌套 schema 遵从度一般。

六、姿势五:Agent 主循环内集成(2025 年后的新形态)

LangChain 1.0 把结构化输出直接集成进 Agent 主循环:模型在一次调用中同时完成"回答/调工具/产出结构",消除了过去"先工具调用再二次解析"的额外模型往返,延迟和成本双降。可以精细选择生成策略:优先用厂商原生结构化,模型不支持时回退工具调用。

七、生产环境的解析兜底模板

无论用哪种姿势,真实项目里解析函数都要像下面这样设防(切片项目的真实做法):

python 复制代码
def parse_json_array(content: str):
    # 1. 剥掉 markdown 代码块围栏
    if "```" in content:
        content = content.strip().strip("`")
        content = content.split("\n", 1)[-1] if content.startswith("json") else content

    # 2. 直接解析
    try:
        data = json.loads(content)
        if isinstance(data, list):
            return data
    except json.JSONDecodeError:
        pass

    # 3. 正则兜底:提取第一个数组片段
    match = re.search(r"\[.*\]", content, re.DOTALL)
    if match:
        try:
            return json.loads(match.group(0))
        except json.JSONDecodeError:
            pass

    # 4. 全部失败:返回安全空值/触发重试,而不是抛给用户
    return []

配合两条纪律:单批最多重试 3 次 (避免坏请求无限烧钱)、解析失败降级而不是崩溃(例如该批片段跳过、整体流程继续)。

八、温度控制:不同任务用不同创造性

同一个模型在结构化任务里的参数应当随任务性质切换,教学项目给出了清晰对照:

任务 温度 理由
出题/文案创作 0.7 需要多样性
内容打分、评分评判 0.3 需要稳定一致
结构化抽取 0~0.2 要的是确定性

九、技术演进与最新差异(2025---2026)

  1. 原生结构化输出全面普及。 主流国产与海外模型均支持 JSON schema 约束,"伪工具调用"技巧主要用于兼容旧模型,新项目优先原生接口。
  2. 标准内容块。 LangChain 1.0 的 content blocks 把各家模型的私有返回字段统一为跨厂商抽象,结构化输出、推理痕迹、引用都有标准访问方式,迁移模型时改动面显著缩小。
  3. 2026 年 MCP 新规范采用完整 JSON Schema 2020-12。 工具参数定义能力增强(条件 schema、更丰富的校验关键字),工具的"输入契约"表达力更强,工具侧的结构化校验与本文的输出侧校验正在共用同一套 schema 体系。

十、小结

结构化输出的方法论:用 Pydantic 写一份同时给模型和程序看的契约,优先走原生结构化输出,兼容场景用工具调用伪装,永远保留剥围栏、正则提取、重试与降级四道防线。再配合"抽取低温、创作高温"的参数纪律,JSON 解析失败就从日常 bug 变成了可观测的小概率事件。

相关推荐
桃西西呀2 小时前
跟风把 Jev 当成便宜版 GPT 接进系统,三天后我默默拆了重做
人工智能·llm·ai编程
杨杨杨大侠2 小时前
聊几句就能让 AI 写代码,Spec 还有必要吗?
java·openai·ai编程
程序员老刘2 小时前
本来没抱期望的 Seed-2.1-pro-0915,居然能当主力模型用了
llm·ai编程
ServBay2 小时前
哑巴模型Jev到底要怎么用?一篇文章告诉你
aigc·openai·ai编程
flash俊杰2 小时前
让知识库自我进化:智能客服的"转人工—审核—回流"自纠错闭环
aigc
codigger2 小时前
记一次给 Claude Code 装护栏的全过程
ai·ai编程·开发工具·claude code
小和尚同志2 小时前
给你的 Claude Code\Codex 等 AI Coding Agent 装个桌宠吧
人工智能·aigc
tingke2 小时前
前端团队 Review 指南(open-code-review 版)
ai编程
OpsEye2 小时前
怎么判断企业采购的大模型,实际投入产出值不值得?
javascript·ai编程