news 2026/9/28 13:18:18

大模型结构化输出实战:五种让模型稳定吐JSON的工程方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
大模型结构化输出实战:五种让模型稳定吐JSON的工程方案

大模型写代码、写文案、做总结都是一把好手,但一让它"按格式返回",很多人的第一反应就是头大。你明明在提示词里写了"请返回 JSON",结果它给你来一段"好的,以下是您需要的 JSON 数据:",后面还贴心地加了三个反引号和一句"希望对您有帮助"。程序那边json.loads()一跑,直接抛异常,整条链路崩掉。这不是模型不听话,而是我们没搞明白一件事:大模型的本质是"下一个 token 预测器",它天生倾向于输出自然语言,而不是机器可解析的严格结构。

这篇内容就是围绕"让大模型稳定吐出 JSON"这件事展开的。我会把目前工程上真正能落地的五种结构化输出姿势拆开讲清楚——从最原始的提示词约束,到 JSON Mode、Function Calling、Pydantic 校验,再到本地小模型配合语法约束解码。每一种都会说清楚它解决什么问题、在什么场景下用、坑在哪里。最后再补一层工程兜底:当模型就是不听话的时候,代码层面怎么补救。适合正在做大模型应用开发、Agent 编排、数据抽取、接口对接的工程师,也适合刚入门想搞清楚"结构化输出到底怎么回事"的朋友。

1. 为什么大模型默认不爱吐 JSON

1.1 从"下一个 token 预测"说起

要理解为什么模型不听话,得先理解它是怎么工作的。大模型在推理时,做的事情非常朴素:给定前面所有的 token,预测下一个最可能出现的 token,然后把这个 token 拼到序列后面,再预测下一个,如此循环。它没有"格式"这个概念,它只有"概率分布"。

当你在提示词里写"返回 JSON"时,模型确实会提高输出{的概率,但它同时也会提高输出"好的,以下是 JSON:"这类客套话的概率——因为在它的训练语料里,人类回答"请给我 JSON"时,往往前面就是会带一句寒暄。这就是为什么你经常收到带 markdown 代码块围栏、带解释文字、甚至带注释的"伪 JSON"。

更麻烦的是,JSON 对语法极其严格:键必须双引号、不能有尾逗号、不能有注释、字符串里的引号要转义。而模型是逐 token 生成的,它在生成第 50 个 token 的时候,并不能"回头看"整个结构是否合法。一旦前面某个地方漏了个引号,后面就会一路错下去,最后你拿到一个语法错误的字符串。

1.2 三种典型的"翻车现场"

我把实际项目里遇到过的翻车情况归了三类,你可以对照看看自己中过哪一枪。

第一类是包裹污染。模型返回的内容长这样:

好的,这是您要的数据: ```json {"name": "张三", "age": 28}

希望对您有帮助。

你要的是中间那一段,但前后全是废话。用正则去抠虽然能抠出来,但一旦模型换了个说法,正则就失效了。 第二类是**语法漂移**。模型返回了看似 JSON 的东西,但里面用了单引号、尾逗号、或者把 `null` 写成了 `None`(Python 习惯)、`undefined`(JS 习惯)。这种最坑,因为肉眼看没问题,程序一解析就炸。 第三类是**结构幻觉**。你要求返回 `{"name": ..., "age": ...}`,结果模型自作主张加了个 `"hobbies"` 字段,或者把 `age` 写成了字符串 `"28"`。字段类型对不上,下游的 Pydantic 或 Java Bean 直接反序列化失败。 > 提示:这三种翻车里,第二类和第三类是最难用提示词根治的,因为它们涉及 token 级别的语法约束和 schema 一致性,必须靠工程手段兜底。 ### 1.3 结构化输出的本质:把"概率生成"约束成"合法生成" 理解了上面这些,你就会明白:所谓"结构化输出",本质上是在做一件事——**把模型自由的概率生成过程,约束到合法结构的子空间里**。约束得越早、越硬,输出就越稳。 按约束的"硬度"从软到硬排,大致是这么一条光谱:纯提示词约束(最软)→ JSON Mode → Function Calling / Tool Use → Schema 校验 + 重试 → 语法约束解码(最硬)。下面五种姿势,基本就是沿着这条光谱展开的。你不需要每种都用,但需要知道每种的能力边界在哪,才能在对的场景选对的工具。 ## 2. 姿势一:提示词约束,最软但最通用 ### 2.1 提示词到底能约束到什么程度 提示词约束是所有方法里门槛最低的,任何模型、任何 API 都能用,不需要特殊支持。它的核心思路是:通过精心设计的指令和示例,把模型"引导"到输出 JSON 的路径上。 一个能用的提示词模板大概长这样:

你是一个数据抽取引擎。请从用户提供的文本中抽取信息,并严格按以下 JSON Schema 输出,不要输出任何解释、前后缀或 markdown 代码块。

Schema: { "name": "string, 人名", "age": "integer, 年龄", "city": "string, 城市" }

规则:

  1. 只输出 JSON 对象本身,第一个字符必须是 {,最后一个字符必须是 }
  2. 所有字符串用双引号
  3. 缺失字段填 null,不要省略字段
  4. 不要输出注释、不要输出尾逗号

用户文本:{input}

注意几个关键点:**明确首尾字符**、**明确引号类型**、**明确缺失值处理**、**明确禁止项**。这四条是提示词约束里性价比最高的。 ### 2.2 少样本示例比长篇规则更管用 实测下来,与其写一大段规则,不如给两三个"输入-输出"示例。模型对示例的模仿能力远强于对抽象规则的理解。比如:

示例1: 输入:李四今年35岁,住在杭州 输出:{"name": "李四", "age": 35, "city": "杭州"}

示例2: 输入:王五,北京,未提供年龄 输出:{"name": "王五", "age": null, "city": "北京"}

两个示例就把"缺失填 null""字段顺序""类型"全交代清楚了,比写十条规则都直观。这也是提示词工程里常说的"show, don't tell"。 ### 2.3 提示词约束的天花板在哪 提示词约束最大的问题是**不稳定**。同一个提示词,换个模型、换个温度参数、甚至同一模型多跑几次,结果都可能不一样。温度越高越发散,越容易跑偏。而且它对复杂嵌套结构几乎无能为力——你让它输出三层嵌套的 JSON,它大概率会在第二层就开始漏字段。 所以我的经验是:**提示词约束适合做"第一道引导",但绝不能作为唯一保障**。它应该和其他姿势叠加使用,而不是单打独斗。如果你的场景对稳定性要求高,光靠提示词就是在赌运气。 ## 3. 姿势二:JSON Mode,让模型"闭嘴只输出 JSON" ### 3.1 JSON Mode 解决了什么 JSON Mode 是很多大模型 API 提供的一个开关。打开之后,模型被强制只输出合法的 JSON 字符串,不会再有"好的,以下是……"这种寒暄,也不会再有 markdown 代码块围栏。它相当于在解码阶段加了一层约束:**只允许生成能构成合法 JSON 的 token 序列**。 用起来很简单,以常见的对话接口为例,请求体里加一个参数: ```python response = client.chat.completions.create( model="your-model", messages=[ {"role": "system", "content": "你是一个数据抽取引擎,只输出 JSON。"}, {"role": "user", "content": "抽取:张三,28岁,上海"} ], response_format={"type": "json_object"} )

拿到response.choices[0].message.content之后,直接json.loads()就能用,不用再抠代码块。

3.2 JSON Mode 的边界:合法 ≠ 符合你的 schema

这里有个特别容易踩的坑:JSON Mode 只保证"是合法 JSON",不保证"是你想要的 JSON"。也就是说,它可能返回{"result": "张三,28岁,上海"}——语法完全合法,但结构完全不是你要的。

所以用 JSON Mode 时,提示词里必须把 schema 写清楚,JSON Mode 负责"语法合法",提示词负责"结构正确",两者是配合关系,不是替代关系。很多人以为开了 JSON Mode 就万事大吉,结果拿到一个合法但没用的 JSON,白白浪费一轮调用。

3.3 什么时候该用 JSON Mode

JSON Mode 最适合的场景是:结构相对简单、字段固定、对稳定性有要求但不想引入复杂工具链。比如做文本分类、情感分析、简单的信息抽取。它的优点是接入成本极低,改一个参数就行;缺点是它不管 schema,复杂结构还是得靠后面的姿势。

另外要注意,不同厂商对 JSON Mode 的支持程度不一样,有的叫json_object,有的叫json_mode,有的干脆没有。上线前一定要在目标模型上实测,别照着文档写完发现参数不生效。

4. 姿势三:Function Calling,把 schema 交给模型"填表"

4.1 Function Calling 的工作机制

Function Calling(也叫 Tool Use)是目前工程上最靠谱的结构化输出方案之一。它的思路和前面完全不同:你不是让模型"写 JSON",而是给它一张"表格"(工具定义),让它"填表"。

你定义一个函数,把参数用 JSON Schema 描述清楚:

tools = [{ "type": "function", "function": { "name": "extract_person", "description": "从文本中抽取人物信息", "parameters": { "type": "object", "properties": { "name": {"type": "string", "description": "人名"}, "age": {"type": "integer", "description": "年龄"}, "city": {"type": "string", "description": "城市"} }, "required": ["name", "age", "city"] } } }]

模型收到之后,如果判断需要调用这个函数,就会返回一个结构化的tool_calls,里面的arguments就是符合你 schema 的 JSON 字符串。你解析这个字符串就行,不用再担心它加寒暄或者漏字段。

4.2 为什么它比 JSON Mode 更稳

关键在于:Function Calling 的 schema 是"强约束"。模型在生成参数时,是被引导着按 schema 的字段和类型来的,而不是自由发挥。字段名、类型、必填项都在 schema 里定义好了,模型填错的概率大幅降低。

而且它天然解决了"结构幻觉"问题——模型不会给你加 schema 里没有的字段,因为它的输出空间被限制在 schema 定义的属性里。这一点是纯提示词和 JSON Mode 都做不到的。

4.3 实战中的几个坑

第一个坑是模型可能不调用函数。如果你给的文本里没有可抽取的信息,模型可能直接回一句自然语言"文本中没有人物信息",而不是调用函数。这时候你得在提示词里明确"无论如何都要调用函数,没有信息就填 null"。

第二个坑是arguments 是字符串不是对象。很多接口返回的arguments是一个 JSON 字符串,需要你自己json.loads()一次。新手经常直接当字典用,结果报TypeError。

第三个坑是并行调用。有些模型会一次返回多个tool_calls,如果你的业务逻辑只处理第一个,就会漏数据。要么在提示词里限制只调用一次,要么在代码里遍历处理。

注意:Function Calling 的 schema 描述(description 字段)非常关键,它相当于给模型的"填写说明"。描述写得越清楚,模型填得越准。别偷懒只写字段名。

5. 姿势四:Pydantic 校验,把"事后检查"做成"自动重试"

5.1 Pydantic 在链路里的位置

前面三种姿势都是在"生成端"做文章,Pydantic 则是在"接收端"做文章。它的角色是校验器 + 类型转换器:模型返回的 JSON 先过一遍 Pydantic 模型,字段类型对不对、必填项有没有、取值范围合不合法,全都检查一遍。

定义一个 Pydantic 模型:

from pydantic import BaseModel, Field, ValidationError from typing import Optional class Person(BaseModel): name: str = Field(description="人名") age: int = Field(ge=0, le=150, description="年龄") city: Optional[str] = None

拿到模型输出后:

try: person = Person.model_validate_json(raw_output) except ValidationError as e: print("校验失败:", e)

Pydantic 的好处是它不只是"检查",还会做类型强制转换。比如模型返回"age": "28"(字符串),Pydantic 会自动转成整数 28。这能救回不少"类型漂移"的翻车。

5.2 校验失败之后怎么办:重试闭环

光校验不够,关键是校验失败之后要有补救动作。最实用的做法是"带错误信息重试":把 Pydantic 报的错原样塞回给模型,让它自己修。

def extract_with_retry(text, max_retries=3): prompt = build_prompt(text) for i in range(max_retries): raw = call_llm(prompt) try: return Person.model_validate_json(raw) except ValidationError as e: prompt = f"{build_prompt(text)}\n\n上次输出有误:{e}\n请修正后重新输出。" raise RuntimeError("重试多次仍失败")

这个闭环的威力在于:模型看到具体的错误信息(比如"age 字段期望整数,收到字符串"),修正的准确率非常高。实测下来,第一次失败后重试的成功率能到 80% 以上,两次重试基本能覆盖绝大多数情况。

5.3 用 Pydantic 反向生成 schema

Pydantic 还有个隐藏用法:用模型类自动生成 JSON Schema,喂给 Function Calling 或提示词。这样你只需要维护一份 Pydantic 定义,schema 和校验逻辑就统一了,不会出现"schema 写了一套、校验写了一套、两边还对不上"的尴尬。

schema = Person.model_json_schema() # 直接把这个 schema 塞进 tools 定义或提示词

这是我在项目里最推荐的组合拳:Pydantic 定义单一数据源 → 生成 schema 给模型 → 模型输出 → Pydantic 校验 → 失败重试。整条链路闭环,维护成本还低。

6. 姿势五:语法约束解码,本地部署的硬核方案

6.1 什么是语法约束解码

如果你在本地部署模型(比如用 llama.cpp 这类推理框架),还有一个更硬核的选项:语法约束解码(Grammar-Constrained Decoding)。它的原理是在解码的每一步,根据一个预定义的语法(比如 GBNF 语法或 JSON Schema),把不符合语法的 token 概率直接置零。也就是说,模型在物理上无法生成非法 JSON。

这比前面所有方法都硬,因为它不是"引导"或"校验",而是"禁止"。模型想输出一个单引号?对不起,这个 token 的概率是 0,采样不到。

6.2 怎么用起来

以 llama.cpp 为例,它支持传入 GBNF 语法文件。你可以手写一个 JSON 语法,也可以用工具从 JSON Schema 自动转换。跑起来大概是这样:

./main -m model.gguf -p "抽取人物信息:张三,28岁" --grammar-file json.gbnf

输出的内容会被严格约束成合法 JSON。对于本地部署、对稳定性要求极高的场景,这是终极方案。

6.3 代价与适用边界

语法约束解码不是没有代价。第一,它会拖慢推理速度,因为每一步都要做语法状态检查。第二,它对嵌套复杂结构的语法文件编写有一定门槛,写错了会导致模型"卡死"或者输出奇怪的东西。第三,它主要适用于本地部署,云端 API 一般不给这个能力。

所以我的建议是:云端 API 场景优先用 Function Calling + Pydantic;本地部署且对稳定性有极致要求时,再上语法约束解码。不要为了用而用。

7. 工程兜底:当模型就是不听话时怎么办

7.1 分层防御的整体思路

前面五种姿势不是互斥的,真正稳的系统是分层叠加的。我一般会这么搭:

层级手段作用
第一层提示词 + 少样本示例引导模型走向正确结构
第二层JSON Mode / Function Calling约束生成端,保证语法合法
第三层Pydantic 校验检查结构、类型、取值范围
第四层带错误重试失败后自动修复
第五层兜底默认值 / 降级多次失败后返回安全结果

这五层下来,结构化输出的成功率能做到 99% 以上。单靠任何一层都不行,组合起来才稳。

7.2 清洗层的几个实用技巧

即使有前面几层,偶尔还是会收到带污染的字符串。这时候一个健壮的清洗函数能救命:

import json import re def robust_json_parse(raw: str): # 1. 去掉 markdown 代码块围栏 raw = re.sub(r"^```(?:json)?\s*", "", raw.strip()) raw = re.sub(r"\s*```$", "", raw) # 2. 截取第一个 { 到最后一个 } start = raw.find("{") end = raw.rfind("}") if start != -1 and end != -1: raw = raw[start:end+1] # 3. 尝试解析 return json.loads(raw)

这个函数能处理掉 90% 的"包裹污染"。注意第 2 步用rfind而不是find,因为嵌套 JSON 里会有多个},取最后一个才能包住整个对象。

7.3 流式输出下的结构化难题

如果你的场景是流式输出(streaming),结构化会更麻烦,因为 token 是一个个来的,你没法等全部生成完再解析。这时候有两个思路:一是先流式展示、后结构化落库,把展示和解析分开;二是用增量解析,边收边尝试解析,遇到完整对象就吐出来。后者实现复杂,一般用在 Agent 场景里需要实时响应的场合。

7.4 几个容易被忽略的细节

第一,温度参数。做结构化抽取时,温度建议调到 0 或接近 0,减少随机性。温度越高,模型越"有创意",越容易跑偏。

第二,max_tokens 要留够。如果 max_tokens 设太小,JSON 还没输出完就被截断了,你拿到的是半个对象。宁可设大一点。

第三,字段顺序。有些模型对字段顺序敏感,schema 里把必填字段放前面,能提高一次成功率。

第四,中文和特殊字符。如果字段值里有中文、换行、引号,一定要确保模型正确转义。Pydantic 校验能帮你发现这类问题。

8. 五种姿势怎么选:一张决策表

说了这么多,最后落到"我到底该用哪个"这个问题上。我整理了一张决策表,按场景对号入座:

场景推荐方案理由
快速原型、结构简单提示词 + JSON Mode接入成本最低,够用
生产环境、结构固定Function Calling + Pydantic稳定性最高,维护成本可控
复杂嵌套结构Function Calling + Pydantic + 重试单层扛不住,必须叠加
本地部署、极致稳定语法约束解码 + Pydantic物理层面杜绝非法输出
流式 Agent 场景增量解析 + 校验兼顾实时性和正确性

选型的核心原则就一条:约束越硬越好,但要在成本和收益之间找平衡。不是所有场景都值得上语法约束解码,也不是所有场景都能靠提示词糊弄过去。搞清楚你的稳定性要求、模型能力、部署方式,答案自然就出来了。

我在实际项目里踩过最深的坑,是早期太迷信提示词,觉得"写清楚就行了",结果上线后各种边界 case 把服务打挂。后来老老实实把 Function Calling 和 Pydantic 加上,又补了重试闭环,才真正稳下来。结构化输出这件事,没有银弹,只有分层防御。把每一层都做扎实,比指望某一层做到完美要靠谱得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 13:18:15

联想拯救者Y7000电池充不进电?6种实用修复方案详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 13:17:46

MySQL主从复制原理与Docker部署实战:从binlog到GTID排错指南

很多人对“MySQL主从复制”的第一印象是:这是DBA的活儿,开发不用管。可真当自己负责一个系统、一台服务器甚至一个课程项目时,数据库挂了没人帮你切,读流量大了也没人帮你分担,这时候才意识到主从集群的基础知识绕不过…

作者头像 李华
网站建设 2026/9/28 13:17:28

数据库变慢如何排查?五大必看环节助你快速定位性能瓶颈

1. 先弄清楚“数据库变慢”到底慢在哪一环?先说个很真实的场景。半夜两点收到告警,说数据库响应时间从 20ms 飙到了 800ms,链路监控上整个接口的耗时长了一大截。这个时候如果直接冲到服务器上看 CPU、看内存、看磁盘,大概率会被一…

作者头像 李华
网站建设 2026/9/28 13:17:09

芯片稳态热分析五步法:从SOLIDWORKS Simulation建模到精准预测

1. 为什么芯片散热分析不能只靠经验估算——从“热得发烫”到精准预测的思维跃迁SOLIDWORKS Simulation、芯片散热、稳态热分析、参数设置——这四个词凑在一起,不是教科书里的抽象概念,而是我去年在做一款工业边缘计算模块热设计时,被硬件同…

作者头像 李华
网站建设 2026/9/28 13:15:31

天邑TY1612/TY1613免拆线刷教程:晶晨S905L3盒子刷机全流程

1. 刷机前的认知准备:先搞懂你要折腾的是什么1.1 天邑TY1612/TY1613到底是什么来头天邑TY1612和TY1613这两个型号,经常出现在各种IPTV盒子、运营商定制盒子堆里,长相普通、配置不高,但胜在价格便宜、量又足,是很多刷机…

作者头像 李华
网站建设 2026/9/28 13:15:03

JavaWeb物流系统集成遗传算法路径优化实战

简介:这是一套面向计算机相关专业学生与初学者的毕业设计级JavaWeb物流管理系统,融合遗传算法实现路径优化等核心物流调度功能,适用于课程设计、毕设立项及企业级项目原型开发。资源包共995个文件,涵盖435个JavaScript前端交互脚本…

作者头像 李华