大模型稳定输出JSON的实战指南:从提示词到函数调用与结构化生成
2026/9/5 5:31:21 网站建设 项目流程

大模型什么都会,就是不会好好说人话。这可能是做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。

实际写好一个强约束提示词,有几个细节容易被忽略:

  1. 给出目标JSON的完整示例,不要只给字段列表。模型对"实例"的理解远强于对"规则"的理解,你给它一个完美的JSON样例,它返回的格式就差不到哪去。
  2. 明确说明不能包含的字符,比如"不要使用花括号语言、不要带markdown代码块标记、不要解释你为什么这么输出"。
  3. 要求模型将不确定的字段置为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(default=None, 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(model="gpt-4o", temperature=0) # 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校验,比什么都管用;第二,输出的字段名一定要和下游数据库表的字段名完全对齐,否则数据流会在最后一公里莫名其妙地断掉。结构化输出这块,没有一劳永逸的银弹,只有不断迭代和兜底,才是稳如泰山的根本。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询