大模型稳定输出JSON的实战指南:从提示词到函数调用与结构化生成
发布时间:2026/9/5 5:31:22 作者:尧图编辑部 阅读量:1,286

大模型什么都会就是不会好好说人话。这可能是做AI应用开发最让人头疼的一件事你问它一个结构化的问题它给你回一大段散文你让它输出配置信息它给你写一首诗你辛辛苦苦部署好的服务就因为解析失败当场崩溃。这个问题我踩了无数坑今天把实战中验证过的方法完整梳理一遍——怎么用函数调用、响应格式限定、提示词约束、输出解析器给大模型套上缰绳让它老老实实返回干净的JSON数据。这篇文章适合正在做AI应用开发、API对接、自动化流程的工程师尤其是那些刚接触大模型、被不稳定输出折磨过的新手。1. 为什么大模型总是答非所问结构化输出的痛点解剖在动手写代码之前先花点时间搞清楚问题的本质。大模型本质上是一个预测下一个词的语言模型它并不知道自己正在和一段程序对话也不知道对方的期待是什么。你让它返回一个JSON它脑子里想的是好的我来描述一个JSON长什么样而不是我来生成一段可以被json.loads直接解析的字符串。1.1 非结构化输出的三种典型表现我在实际项目中总结了模型输出不稳定的几种常见形态每一种都有自己的成因和对策。第一种是散文式输出。模型收到请求后会用自然语言回复比如你问提取这篇文章的关键词它回答好的以下是这篇文章的关键词人工智能、深度学习、自然语言处理……。这种输出人类读起来很舒服但程序要处理它就得写一大堆正则表达式去匹配而且格式稍微一变化就全完蛋。最典型的是让模型输出一个代码片段它会在代码块外面加上markdown的标记甚至附上下面是代码这样的说明文字。第二种是格式漂移。模型可能上一轮返回的JSON带双引号下一轮就用了单引号上一轮字段名是驼峰式userName下一轮就变成了下划线式user_name更麻烦的是它可能把布尔值从true/false变成True/False这在Python的json.loads里直接报错因为Python严格区分大小写。我曾经在一套自动化流程里让模型提取合同金额同一份合同跑三次测试三次返回的键名都不一样调试到崩溃。第三种是截断和幻觉。尤其在使用开源模型比如LLaMA、Qwen的本地部署版本时模型生成到一半可能因为达到最大token长度而中断留下的JSON字符串只有半个对象花括号都不闭合。更常见的是模型在生成JSON时自己创造了一些不存在的字段或者把NULL值写成了字符串null而不是null。这时候解析器能跑但返回的数据结构完全不符合预期。这三种形态的共同根源在于默认情况下模型的空间里没有输出格式这个约束条件。要让它稳定输出JSON本质上是把这个约束从语感揣测变成技术强制。1.2 一次真实的生产事故解析失败的连锁反应去年我在做一个人力资源系统的智能问答模块大模型负责从简历库里筛选候选人信息。最初的实现方案非常天真——直接问模型给出张三的电话号码然后从响应的字符串里截取。结果上线第一天就出了事一个候选人的电话号码是18开头模型在电话前后各加了一段描述性文字导致正则匹配到一串错误号码系统把简历推荐给了完全不相关的岗位。修复过程中尝试了提示词方案在System Prompt里写你必须只输出JSON效果有改善但不稳定。后来换成函数调用Function Calling才最终解决了问题——因为函数的入参schema明确告诉模型电话这里应该放一个符合手机号格式的字符串模型的输出被约束在了这个结构内。这次事故让我意识到一个核心问题处理大模型输出不能靠人品不能靠概率要在技术架构层面设计一套可靠的约束机制。下面从工具选型、实现方式、常见问题和调优技巧四个维度展开讲清楚如何让大模型稳定返回JSON数据。2. 主流方案全景对比从提示词到结构化生成的路径选择解决结构化输出问题业界已经沉淀出几套主流方案它们各有优劣和使用场景。我踩过不少坑也对比过不少实现下面把这几种路径掰开揉碎讲清楚。2.1 从软约束到硬约束的技术谱系先看三种主流方法的定位差异方案实现难度约束强度适用场景提示词强约束解析器最低弱依赖模型能力快速原型、简单场景函数调用Function Calling中较强结构固定需要精确参数的API调用结构化生成受限解码高最强强制执行生产环境、对格式零容忍这个表格只是我自己的工作真实项目里通常不是只用一种而是组合使用。比如用提示词做兜底用函数调用做主路径再用结构化生成处理最关键的数据接口。2.2 方案一提示词强约束 输出解析器这可能是很多人第一个想到的办法也是我在小项目里最常用的一种。核心思路是在System Prompt里明文规定输出只能是JSON不许带任何多余文字然后借助一些开源解析库把模型输出清洗成可用的JSON。实际写好一个强约束提示词有几个细节容易被忽略给出目标JSON的完整示例不要只给字段列表。模型对实例的理解远强于对规则的理解你给它一个完美的JSON样例它返回的格式就差不到哪去。明确说明不能包含的字符比如不要使用花括号语言、不要带markdown代码块标记、不要解释你为什么这么输出。要求模型将不确定的字段置为null而不是编造。这个对数据清洗非常重要是防止幻觉的第一道防线。只写提示词还是不够稳因为模型无法100%遵守指令尤其是一些参数量较小的开源模型。所以我一般会在提示词后面再接一层解析器兜底。Python里常用的解析器有LangChain的PydanticOutputParser用Pydantic定义好数据结构解析器先把LLM输出尝试json.loads如果失败则自动尝试从文本中提取JSON部分再按Pydantic模型校验。json-repair专门修复不合法JSON的小库能把缺失引号的键名、单引号替代双引号、多余的尾逗号等常见问题修好。OpenAI的JSON Mode如果是OpenAI GPT-4o、GPT-4-turbo系列在API里直接设置response_format{ type: json_object }模型就只会生成合法的JSON文本不夹带任何多余内容。2.3 方案二函数调用Function Calling / Tool Use这是目前GPT-4、Claude、Qwen、GLM等主流大模型都支持的能力也是我生产环境中首选的方案。函数调用的底层机制是开发者给模型一组带JSON Schema描述的工具函数清单模型根据用户提问自动决定要调用哪个函数并生成符合该函数参数结构的JSON对象。举个招聘筛选的例子用户问题帮我找到张三的电话和最近的工作经历。 系统收到函数调用请求 调用 get_candidate_info(candidate_name: 张三, fields: [phone, experience])模型不是直接输出张三的电话号码是138xxxx而是生成一个结构化的调用请求。在OpenAI SDK里这一步表现为返回的tool_calls数组。拿到这个数组后你再决定是去查数据库、调外部API还是直接把参数里的JSON转给下游使用。函数调用的好处是格式强约束是模型原生支持的不需要提示词层级去碰运气。而且它天然适合让大模型做决策、让代码做执行的架构——模型负责理解意图、填参数你的程序负责真正的逻辑和存储。我在做一个代码审查机器人时就用了这套机制模型只负责决定要不要触发规则规则参数永远通过JSON Schema来传递实现零格式错误。2.4 方案三结构化生成受限解码 / Grammar-Constrained Decoding这是最硬核的手段。它的思路不是劝模型输出JSON而是从解码层面直接限制模型只能生成符合某个语法格式的token序列。开源模型生态里这套方案的火烧得很旺。比如在使用vLLM部署模型时可以传入一个guided_json参数传入一份JSON Schema那么模型在每一步生成时都会排除不符合该schema的token。在Llama.cpp里也内置了json_schema语法约束功能。实践下来条条大路通罗马结构化生成的准确率基本是100%因为不合法的token被直接屏蔽了。但这套方案有个明显的取舍需要占用额外的推理时间。因为每一步生成都要做一次语法校验推理吞吐会下降15%到40%不等。如果不是对输出格式零容忍的生产级接口下不了这个成本也没关系。3. 实战拆解从零构建一个稳定返回JSON的工具思路理清了是时候动手了。下面我从头演示一个完整的、可直接运行的解决方案用的技术栈是LangChain Pydantic OpenAI兼容API也可以用Ollama本地部署的Qwen等开源模型代替。最终效果是输入一段自然语言系统稳定返回一个符合预定义结构的JSON。3.1 环境准备与相关库的选型理由先装包pip install langchain langchain-openai pydantic如果你用的是本地部署的Ollama那么可以搭配pip install langchain-ollama选这几个库的原因很简单Pydantic负责定义数据结构和数据验证LangChain负责把Pydantic结构和LLM输出粘合起来LangChain-OpenAI或LangChain-Ollama则是模型接入的适配层。这套组合能让我从一个点子到一个可运行的原型只花十几分钟。有一个细节很容易踩坑如果要用OpenAI的JSON Mode功能必须确保在消息末尾加上JSON这个词否则接口会报错。LangChain的with_structured_output方法内部会帮你处理好这个逻辑但如果直接裸调OpenAI SDK就得自己拼提示词。3.2 定义一个不可变的数据结构Pydantic模型先想清楚你要的输出长什么样。比如我现在要做的是从一段招聘JD里提取岗位信息定义如下from typing import List, Optional from pydantic import BaseModel, Field class SkillRequirement(BaseModel): name: str Field(description技能名称) level: str Field(description技能要求等级初级/中级/高级/专家) class JobPosting(BaseModel): title: str Field(description岗位名称) salary_range: Optional[str] Field(defaultNone, description薪资范围如30k-50k) requirements: List[SkillRequirement] Field(description技能要求列表) years_of_experience: Optional[int] Field(description最低工作年限) remote_ok: bool Field(description是否支持远程办公)注意几点写作惯例每个字段都写description这句描述会传给模型作为生成依据越具体越好。不要写职位信息这种空话要写岗位名称如高级后端工程师。Optional字段给默认值避免模型因缺失字段而编造数据。如果给的是None模型找不到就直接置null不会卡住。嵌套结构的设计尽量扁平。模型在生成深层嵌套的JSON时错误率会成倍上涨能用两层的就不要搞五层。3.3 核心调用代码打通自然语言到JSON的管道接下来是最核心的管道部分代码不多但每一步都决定了最终稳定性。from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate # 1. 初始化模型 model ChatOpenAI(modelgpt-4o, temperature0) # 2. 绑定Pydantic结构 structured_model model.with_structured_output(JobPosting) # 3. 构造提示词 prompt ChatPromptTemplate.from_messages([ (system, 你是一个专业的招聘信息提取助手。只提取原文中明确提到的信息未提到的字段一律置为null不要猜测。), (human, {text}) ]) # 4. 组装Runnable链 chain prompt | structured_model # 5. 测试 result chain.invoke({ text: 我们正在招聘一名高级Python开发工程师负责AI平台的后端开发。 薪资25k-40k要求5年以上开发经验精通Python、FastAPI、PostgreSQL 熟悉大模型应用开发者优先。支持每周3天远程办公。 })执行完result就是一个JobPosting实例直接用 result.model_dump() { title: 高级Python开发工程师, salary_range: 25k-40k, requirements: [ {name: Python, level: 专家}, {name: FastAPI, level: 高级}, {name: PostgreSQL, level: 高级} ], years_of_experience: 5, remote_ok: true }漂不漂亮模型没有再输出一大段心灵鸡汤而是直接给出了干净的结果model_dump()的字典可以直接被json.dumps序列化扔给前端或数据库。3.4 解析失败的兜底策略与针对性修复虽然with_structured_output内置了格式校验但遇到模型输出没法匹配Pydantic结构的情况它仍可能抛Error。这时我一般会写一个兜底函数第一次解析失败对文本做一次清洗——去掉模型顺手加的markdown标记、截取第一个{到最后一个}之间的子串再喂给json.loads如果还失败就调用一次修正模型把原始输出和报错信息一块发回去让模型重新输出一遍正确写法。其中一种兜底实现的骨架参考import json import re def robust_parse(text: str): # 情况1文本中混入了额外说明文字尝试提取JSON区域 pattern r\{.*\} match re.search(pattern, text, re.DOTALL) if match: try: return json.loads(match.group()) except json.JSONDecodeError: pass # 情况2用json-repair库做修复 from json_repair import repair_json return json.loads(repair_json(text))兜底的价值在于它不会让主线流程因为一次意外中断而是把出错重试的成本放在最末端。4. 生产环境踩坑记录那些在官方文档里搜不到的教训工具流程能跑通只是第一步生产环境里真正考验人的是那些我称它为“大概率事件”的边界情况。下面几条经验每条都是流血换来的。4.1 提示词长度与上下文占位不可忽视的隐性Buff当你让模型输出结构化JSON时它也同时处理了用户输入。如果你要求它输出的JSON schema极其复杂、字段高达数十个那么模型在有限的上下文窗口里可能记不住完整schema尤其是在上下文很长的情况下。我来举个反例我做某个数据清洗任务时要求在系统提示词里塞入一张包含27个字段的表格schema然后把一份12000字的原始文档一并交给模型处理。结果模型的JSON输出开始频繁跳字段而且出现幻觉字段——它开始自己编造一些不在schema里的键名。把schema里所有字段名和描述精简到只留关键部分后大概900字符问题立刻缓解。常规经验保持总输入长度低于上下文长度的50%留足生成空间。如果schema过于庞大拆成多步完成而不是一次提取全部。尽可能把schema描述写在最近的、离输出最近的提示词区域模型对尾部指令的关注度更高。4.2 不同模型的兼容性差异与策略这里必须提醒一下OpenAI GPT-4系列和Claude 3.5 Sonnet在结构化输出方面是标杆级别的。但如果你用其他模型情况会完全不同模型with_structured_output支持情况实际体验GPT-4o / 4-turbo原生支持JSON固定生成最高Claude 3.5 Sonnet函数调用良好优秀偶尔需要提示兜底Qwen2.5本地方案支持Tools Calling不错但复杂嵌套结构较吃力LLaMA 3.1 8B勉强支持需要强提示词解析兜底一些小参数模型几乎不支持建议直接上结构化生成所以策略上要分层——生产环境如果核心链路是付费API优先选择原生支持好的模型如果你迫于成本使用本地部署的开源模型那么请务必上一套强提示词解析器兜底重试的链路同时把JSON Schema尽量简化字段用几十年跨度的老数据结构这会是省心不折腾的关键。4.3 关于结构化输出未来的展望结构化输出正在成为大模型应用层的标配能力从早期提示词求模型到现在的函数调用再到将来的受限解码技术演进方向非常明确把格式控制从概率事情变成必然事件。个人实操中如果你面临一个固定格式的高频任务我强烈建议投入时间学会一种SGLang/vLLM的结构化生成方案它能在保证速度的同时做到零格式错误特别适合批量处理、自动化脚本这些场景。我在本地的多个生产任务里用这套方案替代了纯提示词方案稳定性直接提升了一个数量级再也不用半夜爬起来看解析报错日志。5. 最终建议如何根据自身场景选择最适合的方案说了这么多核心原则其实非常朴素稳定性是从架构里设计出来的不是靠prompt驯化出来的。仅靠微调和写一段漂亮的提示词没法彻底解决格式输出的随机性但如果你在系统里加上schema约束、输出校验、异常重试和格式兜底即使是开源小模型也能输出一个相对稳固的JSON结构。换个角度说从提示词、函数调用到结构化生成稳定手段的强度在递增成本也在递增。做原型推荐提示词解析器很快能出活写正规服务或对接外部API优先上函数调用跑核心数据管道、容不得半点错时选择结构化生成。这套思路不管你用哪家模型、哪种语言框架基本都适用。最后结合我在这块踩过的大量坑再送你两条真心话第一永远不要相信模型第一次返回的JSON一定符合要求外加一层Pydantic校验比什么都管用第二输出的字段名一定要和下游数据库表的字段名完全对齐否则数据流会在最后一公里莫名其妙地断掉。结构化输出这块没有一劳永逸的银弹只有不断迭代和兜底才是稳如泰山的根本。