想象你家的后厨请来一位天才大厨——厨艺顶尖、见识广博,但有个毛病:你让他“报一下今晚的鱼香肉丝订单”,他给你来一段评书:“这道菜源自川渝,讲究急火快炒,我建议搭配米饭……”啰嗦半天,后厨的系统根本没法把他的话录入订单。于是你递上一张印好栏目的点菜单:菜名、辣度、份量、备注,每栏留空。大厨虽然话多,但看到表格,就乖乖往格子里填字。这就是结构化输出的本质——给LLM这张“点菜单”,让程序能直接“扫描”它的回答。 LLM天生是个“自由发挥的话痨”,而业务系统是个“只会读表格的机器人”。结构化输出(JSON/XML)就是二者之间的桥梁。JSON像一张通用点菜单,XML像一张带标签的档案卡,JSON Schema则像后厨的质检专员,负责检查每栏填得合不合规。本文会用点菜单的故事,带你把“提示词约束→格式控制→校验→纠错→Function Calling→业务落地”这条链路完整走一遍。
掌握结构化输出,等于掌握了LLM与业务系统“握手”的通用语言。几乎任何需要LLM对接数据库、API、工单系统的开发岗位,都绕不开这堂课。它能让你的AI输出从“仅供参考”升级为“即插即用”。
目录
可视化演示
通过格式约束生成、Schema校验修复、Function Calling调用三个模式,展示LLM输出JSON/XML的约束、校验与自动修复全流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
格式约束与JSON生成 System Prompt约束输出格式,提供字段定义和示例模板 LLM按约束生成结构化JSON,字段名和类型可控 Prompt + Schema → Structured JSON 提示词约束是结构化输出的基础方法
为什么需要结构化输出
后厨系统(业务程序)只认“点菜单”。可LLM大厨天生爱讲故事——你问“这道菜多少钱”,它从食材产地聊到烹饪哲学,最后才捎带一句价格。程序拿这段自由文本毫无办法:想提取价格?得靠正则匹配、关键词硬抠;想存数据库?字段对不上。这就像让收银员从大厨的评书里找菜品价格,效率低还总出错。
“结构化输出”就是给大厨的发挥套上格子:规定每句话的栏目、类型、层级,让输出符合某个固定格式。JSON(一种像购物清单一样的键值对文本)和XML(像带标签的档案卡)是两种最常见的“点菜单格式”。LLM在训练时见过海量此类数据,天然有能力模仿——你要做的,只是把“格子”画清楚。
技术上讲,结构化输出本质是把生成任务变成填空任务。模型仍是逐token生成,但提示词中的格式模板、字段名、示例,会引导它的注意力集中在“填哪个槽”。常见手段包括system指令、few-shot示例、后置约束,以及用代码块包裹输出内容。后端再用json.loads或XML解析器把它变成对象,程序就能像读数据库一样消费LLM的回答。
大厨不是不听话,是你没给表。结构化输出解决了“AI说人话、程序要数据”的根本矛盾,是LLM工程化的第一块敲门砖。
提示词约束技巧
一张合格的空白点菜单,光有栏目不够,还得告诉大厨“按什么规则填”。很多人只在对话末尾加一句“输出JSON”,效果时好时坏。为什么?因为大厨脑子里装了太多菜谱,一句轻飘飘的要求像在大饭堂里喊了一嗓子,很快被淹没。
有效的“点菜单设计”有四招。第一,system指令:系统角色里写死“你是一个点单助手,永远只输出JSON”。这相当于在菜单抬头印上大字“本单仅接受标准格式”。第二,few-shot示例:给一个“用户说X→模型输出Y”的完整例子。大厨是模仿型选手,给个样板比讲十遍规则管用。第三,后置约束:把“只输出JSON,不要任何解释”放在提示词最后。实验和经验都表明,LLM对上下文末尾的注意力权重更高,结尾的要求往往比开头更有效。第四,否定指令:明确说“禁止输出markdown、禁止注释、禁止客套话”,堵住它自由发挥的出口。
为什么“结尾强调”这么灵?可以用贝叶斯视角理解:格式提示相当于一个强先验,把“合法输出”的条件概率抬高了。
点菜单画得越细、示例越具体、结尾越强硬,大厨的自由发挥空间就越小。四招齐上,输出格式的稳定性会从“随缘”变成“八九不离十”。
- 后验概率
P(valid|hint)表示“在提示词里写了格式要求”之后,模型输出合格格式的概率,这是我们真正关心的最终成功率。 - 似然概率
P(hint|valid)表示“历史上那些合法输出里,本来就附带格式提示”的比例。few-shot示例和明确字段名能显著提高它。 - 先验概率
P(valid)是无任何提示时输出本来就合法的概率。LLM见过大量JSON,所以它天然不为零,只是不够高。 - 证据归一化分母
P(hint)是所有出现格式提示的情况(含合法与不合法)。约束越精确,这个分母中不合法的占比越小。
JSON输出控制
JSON是当下最流行的“点菜单格式”。它的语法像极了购物清单:一个花括号代表一张单子,里面是无数“名字:值”的配对。{"dish": "鱼香肉丝", "spicy": "微辣"}——简单、嵌套清晰、几乎所有语言都有现成解析器。LLM对JSON的训练语料极其充分,模仿起来最顺手。
但让大厨填JSON,光说“用JSON”不够,还得告诉他“把JSON装进代码块”。常用套路是要求模型输出时用三个反引号包裹:
{……}
代码块就像给点菜单加了一层保护信封,防止模型在JSON外面又写“好的,这是你要的结果”之类的废话。后端解析时先取代码块内容,再json.loads,稳妥得多。
另一个实际坑是中文编码。Python的json.dumps默认ensure_ascii=True,会把中文变成\u4e00这样的转义序列。如果是给人看的接口,通常会设ensure_ascii=False让中文直出;但如果你要传输到某些老系统,转义反而是安全的。指令里建议明确说“不要转义中文,原样输出”,否则模型可能自作主张。
还有两个隐蔽问题:一是模型可能在JSON前后添加“这里是我的输出:”之类的说明,需要禁止;二是花括号、引号可能不完全配对,所以输出后必须用解析器验证,不能迷信肉眼看起来“像JSON”。后厨拿到单子也要对一遍栏目,对吧?
JSON控制的核心口诀:结构清楚、代码块包裹、前后无杂质、中文约定好。这四点做到,解析成功率会大幅提升。
Schema定义与校验
点菜单填完了,谁验收?后厨需要一个质检专员——JSON Schema就是这份“验收规范”。它声明每个字段的名字、类型、是否必填、取值范围。比如dish必须是字符串、spicy只能从[“不辣”,“微辣”,“中辣”,“特辣”]里选、count必须是≥1的整数。
有了Schema,程序就能像质检员一样逐项核对:字段缺失?返回“缺少字段:dish”。类型不对?返回“count应是整数”。这个过程把LLM的概率输出变成了可验证的契约。Python生态最常用pydantic库——定义好数据类,一行model_validate(obj)就能完成校验,失败时自动抛出带详细原因的错误。
校验的数学本质是一个逻辑合取:
质检专员(Schema)把“大厨可能犯错”变成“程序能捕获的异常”。校验失败不是终点,而是下一节“退单重填”流程的起点。
- 合法性判定
valid(o)是一个布尔开关,代表输出对象o整体是否通过验收。 - 逻辑合取
∧符号类似“每一项都要打勾”,只要一项不满足,整个表达式为假。 - 字段存在性
f ∈ o检查必填字段f是否真的出现在输出对象中。 - 类型匹配
type(o[f]) ∈ allowed[f]检查该字段值的类型是否落在Schema允许的列表里。
XML输出控制
XML是另一种点菜单格式,长得像档案卡:每个值都被一对标签夹住。<dish>鱼香肉丝</dish>表示“dish栏的值为鱼香肉丝”。它比JSON更擅长表达“文档型”数据——有开标签、闭标签、属性、命名空间,适合带标注的文本结构(比如RSS、SVG、配置文件)。
让LLM输出XML时,最大风险是标签闭合不配对。大厨写<dish>鱼香肉丝</dish>没问题,但嵌套多了就容易漏掉某个</xxx>。因此提示词里一定要给出完整的XML模板,让模型“填空”而不是“自创结构”:
模板填空的本质是降低生成自由度——结构已经写死,模型只需往空位里填内容,漏标签的概率大大降低。这与人类填表时“照着栏目抄”是同一个心理机制。
XML还有一个额外福利:数据的层级关系一目了然。比如<customer><name>张三</name><level>VIP</level></customer>天然表达了“name和level是customer的子属性”,这在JSON里要靠缩进气表述说清楚。对需要保留“文档语义”的场景(如合同条款标注、图书目录),XML比JSON更贴切。
但要注意,LLM对XML的熟悉程度普遍低于JSON,首次成功率通常略逊。所以实际工程中,如果数据形态是“纯键值嵌套”,优先JSON;如果是“带标记的文档流”,选XML。选对了“表格样式”,大厨填写才会顺手。
<order> <dish>___</dish> <spicy>___</spicy> <count>___</count></order>
错误修复策略
再熟练的大厨也有填错栏的时候。后厨系统最不该做的事,就是把填错的单子直接扔进垃圾桶——正确的做法是退单并注明原因,让大厨重填。
结构化输出的错误修复,同样遵循“退单重填”流程。第一步,解析失败检测:用json.loads或XML解析器去解析输出,捕获异常或Schema校验失败。第二步,带错重试:把错误信息追加到对话里,告诉模型“你刚才输出缺少字段spicy,请重试”。这比静默重试有效得多,因为模型能根据具体错误调整自己。第三步,正则抢救:如果检测到模型在JSON外套了一段散文,用正则\{[\s\S]*\}把花括号部分抽取出来再解析,相当于从大厨的废纸上把能用的字剪下来。第四步,兜底机制:设置最大重试次数(比如3次),超过就返回默认值或标记人工处理,防止无限烧token。
从概率角度看,每次重试的成功率独立叠加,整体成功率会随次数显著上升:
退单重填不是不信任大厨,而是信任“大厨能在反馈中修正”。工程上把这条链路做成循环:生成→校验→失败则带错重试→成功或达上限。这是结构化输出稳定性的最后一道保险。
- 单次成功率
p_i是第i次重试该输出通过校验的概率。每次带回的错误信息越多,p_i通常越高。 - 全失败概率
∏(1-p_i)表示“每次都失败”的概率乘积,因为每次失败是独立事件,概率相乘。 - 至少一次成功用
1 -取补集,得到“n次里有至少一次成功”的总概率。
Function Calling结合
如果大厨不在纸上填表,而是直接对着传菜系统喊“上菜!”,是不是更高效?Function Calling(函数调用)就是这条“直连通道”——它允许LLM在回答中声明“我要调用某个函数”,并自动生成该函数所需的参数JSON。系统收到后执行函数,再把结果返回给LLM继续处理。
技术上讲,你在API请求里声明一组可用函数(函数名、参数描述、类型),模型会根据用户请求“选一个函数并填好参数”。比如用户说“帮我把明天的会议室订了”,模型不是写散文,而是输出:
后端拿到这个JSON,校验参数合法后真正执行订房操作。这本质上是把结构化输出从“数据交换”升级为“动作触发”——LLM输出的结构化内容直接变成系统API调用。Agent(智能体)工具调用的底层核心就是这个机制。
从建模视角看,函数选择也是一个概率最大化过程:
Function Calling让大厨和传菜系统“直接通电”,省去了中间填单环节。但它不是万能的——函数描述依然要遵循上一节的提示词约束技巧,参数描述含糊照样会选错函数。
p 和可用函数集合 F 的条件下,模型在所有“函数名+参数”组合里,挑一个概率最高的组合输出。- 目标函数
(f*, θ*)是模型最终选定的函数名和参数,是我们要拿到的结构。 - 穷举取最大
arg max表示在候选集合中找最大值对应的那组(f, θ)。 - 条件概率
P(f,θ|p,F)是给定用户话术和函数列表时,选这组方案的概率。参数描述写得越清楚,正确组合的概率越高。
{"name": "book_room", "arguments": {"room_id": "A201", "start_time": "2025-01-01T09:00:00"}}业务落地最佳实践
把点菜单系统真正开起来,要考虑的不是单张菜单,而是整条后厨流水线。结构化输出在真实业务中有三大落地方向。
第一,数据抽取。从自由文本(客服聊天、邮件、发票)里提取结构化字段,是LLM最成熟的应用。传统正则规则面对“换个说法就失效”的文本苦不堪言,LLM凭着语义理解直接吐出{"商品": "...", "金额": "..."},配合Schema校验,准确率大幅提升。第二,下游系统对接。ERP、CRM、工单系统都只认固定格式,LLM输出JSON后经中间层映射即可写入数据库,无需人工转录。第三,成本与延迟权衡。严格格式约束会消耗更多token(示例+Schema都算输入),校验失败重试也增加延迟。因此不是所有场景都需要“纯结构化”——内部对话可以用宽松格式,只有最终要入库或触发动作的输出才严格校验。
实践中还有一个铁律:永远不要盲信LLM输出。Schema只能保证“格式对”,不能保证“语义对”。大厨填了“微辣”,但后厨实际做的是“特辣”,这道菜还是得人工把关。安全敏感场景(如删除操作、转账)必须加权限审批,不能只靠结构化输出就自动执行。
点菜单设计得好,餐厅才能高效运转。结构化输出也一样——它不是单一技巧,而是格式约束+校验+纠错+工具绑定的组合工程。掌握这套方法论,LLM才能真正从“聊天玩具”变成“业务系统的一等公民”。
AI 应用场景
客服工单自动分类
阶段:数据抽取与结构化
解决的问题:客服每天收到大量自由文本工单,传统正则规则对“说法多样”的文本覆盖率低,人工录入字段耗时且易错。AI用结构化输出把口语工单转成固定字段JSON,直接写入工单系统。
具体案例:某售后系统在提示词中定义Schema要求LLM抽取“产品型号、故障描述、紧急程度、客户情绪”四个字段,输出纯JSON。后端用pydantic校验通过后自动创建CRM工单,录入耗时从3分钟降到10秒,字段完整率从78%提升到96%。
智能助手对接企业ERP
阶段:Function Calling / Agent执行
解决的问题:员工用自然语言让系统“订会议室”“查库存”“发审批”,系统无法直接理解口语,更不知道调哪个API。Function Calling让LLM自动选择函数并生成参数JSON,打通人话与系统操作。
具体案例:企业内部助理声明了book_room、query_stock等20个函数,用户在飞书上说“下周一下午订A201会议室,带投影”,LLM输出{"name":"book_room","arguments":{"room_id":"A201","start_time":"..."}},系统校验后自动执行并回复确认。
发票关键信息批量抽取
阶段:文档结构化处理
解决的问题:财务部门需从PDF发票中提取发票号、金额、税号等字段,传统OCR+正则对版式变化敏感的发票适配困难。多模态LLM结合XML模板可稳定抽取。
具体案例:某财税公司用多模态LLM要求输出XML模板:<invoice><number>...</number><amount>...</amount><tax>...</tax></invoice>,解析后自动灌入财务系统。新老版式发票均能适配,人工复核量下降70%。
Python 演示
import jsonimport re from openai import OpenAI SCHEMA = { "type": "object", "properties": { "dish": {"type": "string"}, "spicy": {"type": "string", "enum": ["不辣", "微辣", "中辣", "特辣"]}, "count": {"type": "integer", "minimum": 1}, "remark": {"type": "string"} }, "required": ["dish", "spicy", "count"]} def validate(obj, schema): if not isinstance(obj, dict): return False, "输出不是JSON对象" for field in schema["required"]: if field not in obj: return False, "缺少必填字段: " + field prop = schema["properties"][field] if field == "spicy" and obj[field] not in prop["enum"]: return False, "辣度不在枚举范围内" if field == "count" and not isinstance(obj[field], int): return False, "count不是整数" if field == "count" and obj[field] < prop["minimum"]: return False, "count必须大于等于1" return True, "校验通过" def extract_json(text): match = re.search(r"\{[\s\S]*\}", text) return match.group(0) if match else text def main(): client = OpenAI(api_key="sk-your-key") user_text = "帮我把订单信息提取成JSON:我要一份微辣的鱼香肉丝,两份米饭,不要葱。" history = [] for attempt in range(3): messages = [ {"role": "system", "content": "你是点单助手。只输出符合JSON Schema的JSON,不要任何解释。"}, {"role": "user", "content": user_text} ] + history resp = client.chat.completions.create( model="gpt-4o-mini", messages=messages, temperature=0 ) raw = resp.choices[0].message.content cleaned = extract_json(raw.strip().split("
SCHEMA = {...}定义点单验收规范:字段类型、枚举、必填项,相当于质检员的检查清单。def validate(obj, schema):逐栏核对字段是否存在、类型是否合法,返回通过/失败及原因。def extract_json(text):用正则从散文里捞出花括号JSON子串,救回被模型“加戏”的输出。messages = [...] + history把上次失败的错误信息追加进对话历史,让模型“带错改错”。resp.choices[0].message.content拿到模型原始回答,先假设它可能“不乖”。raw.strip().split("```json")[-1].split("```")[0]剥离代码块标记,只留JSON本体。json.loads(cleaned)真正的质检关卡:解析失败立即抛异常,进入错误修复流程。history.append(...)把错误信息作为用户消息回传,形成“退单重填”的闭环。for attempt in range(3)重试上限3次,避免无限烧token,超限转人工兜底。ensure_ascii=False输出中文原样显示,避免转义序列,方便日志与调试。
常见误区
误区“我明明在提示词里写了`请输出JSON`,结果还是一大段解释带个代码块,模板根本没用。”
正解一句轻飘飘的“请输出JSON”就像在餐厅大喊“做菜注意点”——太模糊。要让大厨照单填字,需要组合拳:system里声明角色与格式、给一个few-shot示例、结尾再强调“只输出JSON,不要任何解释”、要求用代码块包裹。缺一不可。
为何常见:人类之间沟通时一句话要求通常足够,于是误以为模型也有同样“听话”能力,忽视了模型对上下文位置的敏感度和自由生成惯性。
误区“XML有Schema、有规范,看起来更严谨,让LLM输出XML一定比JSON更可靠。”
正解LLM不是按规范推理,而是按训练数据的分布生成。互联网上JSON语料远多于XML,模型对JSON的“语感”更好,JSON首次成功率通常更高。XML的标签闭合反而容易出错。严谨的是“校验机制”,不是格式本身。
为何常见:XML从设计上确实有严格规范,人们容易把“规范严格”等同于“模型更容易遵守”,忽略了模型的学习来源是数据分布而非规范文档。
误区“校验没过?再调一次接口让它重新输出,多试几次总会好的。”
正解盲目重试浪费token,而且模型的错误往往是系统性的——上次漏了字段,这次可能还漏。正确做法是把校验错误信息作为反馈追加到对话里,告诉模型“你缺了spicy字段”,成功率大幅提升。好比退单时写下“辣度没填”,后厨才知道改哪里。
为何常见:人写错字重写一遍往往能好,但模型的自回归错误有惯性,不带着反馈纠正,容易在同一个坑里反复摔。
误区“用了Function Calling就不用写格式约束了,框架会帮我处理一切。”
正解Function Calling本身也是模型概率生成的结果,函数名和参数选错照样发生。函数参数描述写得含糊、没有示例,模型就会猜错参数类型。好的参数描述、system指令和必要的示例依旧是基础。
为何常见:因为Function Calling的交互由框架封装,看起来“很确定”,容易让人忘记底层依然是语言模型的概率输出。
误区“JSON Schema校验都过了,字段类型、必填都对,这数据肯定没问题。”
正解Schema只保证“格式合规”,不保证“语义正确”。模型可能输出一个格式完美但事实错误的字段——比如把“不要葱”提取成备注“no onion“没问题,但如果把”微辣“识别成”特辣”,代码可看不出来。涉及金额、权限、危险操作,必须叠加业务规则与人工审批。
为何常见:格式校验看起来是“机器自动判定的客观标准”,容易让人产生“机器说行就是行”的安全错觉,忽略了语义层的风险。
面试题
Q1. 结构化输出最核心的价值是什么?
- A.让模型输出更长的回答
- B.让结果可被程序解析← 正确答案
- C.提升模型的推理速度
- D.减少模型训练数据
答案:B。结构化输出的本质是让LLM的输出从“人读的自然语言”变成“程序可解析的数据格式”。B准确描述了它的核心价值。A是副作用非目标,C、D与结构化输出无直接关系。
Q2. 想让LLM只输出JSON,哪种提示词写法最有效?
- A.在开头说“请友善回答”
- B.结尾强调“只输出JSON”← 正确答案
- C.完全不写任何格式要求
- D.要求模型朗读输出内容
答案:B。后置约束利用了模型对上下文末尾注意力权重更高的特性,把“只输出JSON”放在提示词最后,比埋在中间更有效。A和格式无关,C不约束格式,D是干扰项。
Q3. JSON Schema的主要作用是什么?
- A.加密JSON数据防止泄露
- B.定义字段类型与必填约束← 正确答案
- C.压缩JSON减少传输体积
- D.自动将JSON转为XML
答案:B。JSON Schema是一份“字段验收规范”,声明每个字段的类型、枚举、必填等约束,程序据此校验LLM输出是否合格。它不加密、不压缩、也不做格式转换,因此选B。
Q4. LLM输出XML时最常见的问题是什么?
- A.标签闭合不配对← 正确答案
- B.输出合法但速度变快
- C.数据内容自动加密
- D.标签无法嵌套
答案:A。XML依赖开闭标签配对表达结构,模型生成嵌套层级深的内容时容易漏写闭合标签,导致整个文档不可解析。B、C不符合实际,D错误——XML天然支持嵌套。
Q5. 结构化输出校验失败后,最推荐的修复做法是?
- A.不做任何处理直接放弃
- B.把校验错误反馈给模型再重试← 正确答案
- C.只截取输出前半部分使用
- D.立刻更换一个语言模型
答案:B。带着具体错误信息(如“缺少字段spicy”)重试,相当于告诉后厨“哪里填错了”,模型能针对性修正,成功率显著高于盲目重试。A浪费机会,C破坏数据,D换模型成本高且未必更优。
Q6. 哪些技巧能提高LLM结构化输出的成功率?
- A.system中明确声明输出格式← 正确答案
- B.给一个完整的few-shot示例← 正确答案
- C.要求用代码块包裹JSON← 正确答案
- D.鼓励模型自由发挥
答案:ABC。system声明格式提供全局约束,few-shot给出模仿样板,代码块包裹防止前后缀污染,三项都能提升成功率。D自由发挥恰恰是结构化输出的反面。
Q7. 关于Function Calling与结构化输出的关系,正确的是?
- A.二者互斥不能同时使用
- B.是结构化输出的一种应用形态← 正确答案
- C.Function Calling只能输出XML
- D.Function Calling不需要任何提示词
答案:B。Function Calling要求模型输出“函数名+参数JSON”,本质上是把结构化输出用于触发动作,是它的进阶形态。A、C、D都不符合技术事实。
Q8. 以下哪些做法属于结构化输出的错误修复策略?
- A.用正则从文中抽取JSON子串← 正确答案
- B.把具体错误信息追加给模型重试← 正确答案
- C.设置最大重试次数兜底← 正确答案
- D.把所有标点统一替换成逗号
答案:ABC。正则提取是“抢救式”修复,带错重试是“反馈式”修复,重试上限是“成本控制”策略——三者都正确。D盲目替换标点会破坏JSON语法,是错误做法。
Q9. 纯键值嵌套的数据交换场景,优先推荐哪种格式?
- A.XML
- B.JSON← 正确答案
- C.Markdown
- D.CSV
答案:B。JSON语法简洁、嵌套直观、解析生态成熟,LLM对JSON语料最熟悉,纯数据交换场景它是首选。XML适合标记型文档,Markdown和CSV不适合复杂嵌套。
Q10. 关于结构化输出的成本与风险,哪些说法正确?
- A.严格格式约束会增加token消耗← 正确答案
- B.Schema校验可拦截垃圾输出浪费← 正确答案
- C.结构化输出永远不会失败
- D.流式解析可缓解响应延迟← 正确答案
答案:ABD。few-shot示例和Schema描述都计入输入token,A正确;校验能在早期拦截不合格输出,避免下游浪费,B正确;流式解析让程序边收边处理,缓解等待,D正确;模型是概率生成,结构化输出也可能失败,C错误。