news 2026/9/20 6:26:38

LangChain withStructuredOutput实战:让大模型输出稳定JSON

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LangChain withStructuredOutput实战:让大模型输出稳定JSON

1. 为什么必须做结构化输出:从“随便聊聊”到“能跑的系统”

在之前的模块里我们聊过 LangChain 怎么把大模型接进来,但真正做应用开发的朋友应该都有同感:调通聊天只是第一步,让模型输出的内容能被程序“接住”,才是从 demo 走向系统的分水岭。这篇我们专门解决这个问题——用 LangChain 的withStructuredOutput把模型的输出稳定地变成 JSON、Pydantic 模型,而不是一段飘忽不定的自然语言。

先说我自己的经历。早年间我做了一个信息抽取小工具,让 LLM 从一段商品介绍里提取价格、品牌、规格,当时用的是“在 Prompt 里写一句请返回 JSON”的土办法。单测的时候一切正常,上线之后各种翻车:有时候模型多解释了一句“以下是您需要的 JSON”,有时候把双引号写成了中文引号,有时候多个字段少个字段,甚至直接返回一个 Markdown 代码块。最崩溃的一次是模型把嵌套数组的括号丢了,json.loads 直接抛异常,线上接口 500 了十分钟,而模型还觉得自己挺无辜。后来我全面切到 LangChain 的结构化输出方案,类似问题基本绝迹。这篇文章就是把我这一路踩坑、换方案、做选型的经验完整记录下来,给同样在做 LLM 应用落地的人一个可以直接抄作业的参考。

这篇文章适合谁?如果你正在用 LangChain 写 agent、做数据提取、做表单自动填写、做 RAG 答案后处理,或者你只是被“大模型返回的 JSON 总是解析失败”折磨过,那这篇内容就是为你准备的。我会从最基础的 Prompt 约束讲起,再说为什么json.loads不是好出路,最后重点拆解withStructuredOutput的原理、参数、实战代码和常见坑。

1.1 非结构化输出的混乱现场

先还原一下没有做结构化约束的时候,模型到底会返回什么。

假设你让模型从一段商品文案里提取信息,Prompt 是“请提取商品名称、价格、库存状态”。模型可能会给你这些:

好的!根据您提供的信息,我提取到以下内容: - 商品名称:iPhone 15 Pro - 价格:7999元 - 库存状态:有货

也能给你这个:

{ "商品名称": "iPhone 15 Pro", "价格": "7999元", "库存状态": "有货" }

运气差一点还会遇到这样:

{ name: 'iPhone 15 Pro', price: 7999, stock: 'in_stock', }

三种格式长得完全不一样,如果你在代码里写死了某个解析规则,第二种和第三种都能让你崩溃。更别提模型偶尔还会在 JSON 前后夹带私货,比如“以下是提取结果:”这种废话,直接导致json.loads在开头就抛异常。

这里的核心问题在于:语言模型本质是一个“接龙游戏”,它预测的是下一个 token 的概率分布,它并不知道你的程序需要一个合法的 JSON 文档。你如果不把“只能输出 JSON”这个约束加进去,模型就会按照它“最自然”的方式回答,而这种方式恰恰是程序最不想要的。

1.2 结构化输出的三种实现路径

市面上的方案归纳起来有三类,我先用一个表格把它们的原理、优缺点说清楚,后面再逐个展开。

方案实现方式优点缺点适用场景
Prompt 约束 + 手动解析在 System Prompt 里要求“只输出 JSON”,然后用json.loads解析零依赖,所有模型都能用极不稳定,模型偶尔不遵守;解析错误需要大量兜底逻辑临时脚本、简单 demo、不支持工具调用的本地小模型
JSON Mode / JSON Schema Mode平台 API 提供一个“强制 JSON 输出”开关,模型生成时就按照 JSON 格式比纯 Prompt 稳定很多;不需要额外工具定义只能保证“是合法 JSON”,不能保证“字段齐全”;部分模型/平台不支持模型不支持 function calling,或输出结构相对简单
Function Calling / Tool Calling把输出 schema 定义成“工具参数”,模型在生成时就按参数格式输出稳定度最高,字段缺失极少;原生支持嵌套结构依赖模型能力;需要平台支持 tools 参数绝大多数场景的首选方案

LangChain 的withStructuredOutput之所以好用,是因为它在底层自动帮你完成了“定义工具参数”或“开启 JSON 模式”这些动作,还屏蔽了不同模型厂商之间的 API 差异。你在代码里只需要声明“我要什么结构”,剩下的交给框架。这也是为什么我现在强烈建议:只要条件允许,一律用withStructuredOutput,别再手写 Prompt 加解析了。

2. 被“JSON 解析”支配的恐惧:从裸奔到自救

在介绍终极方案之前,我先把基础方案里容易踩的坑讲透。因为你在实际项目里总会遇到“某个模型不支持工具调用”或者“临时调一下接口不想引入太多依赖”的情况,这时候你还是得回到手动解析。把这些坑搞清楚,你至少不会在基础方案上栽跟头。

2.1 写一个“靠谱”的输出指令

如果一定要走 Prompt 约束这条路,那指令也分三六九等。一句“请输出 JSON”是肯定不够的,模型会把“JSON 格式”理解成“一种像 JSON 的东西”,然后自由发挥。我实践下来,一份可靠的输出指令至少要包含四个要素:明确输出类型、给出完整 Schema、给一个示例、禁止任何解释。

下面是我常用的一个模板:

SYSTEM_PROMPT = """你是一个数据提取助手。请从用户提供的文本中提取商品信息,只输出JSON格式,不要输出任何其他文字、解释或Markdown代码块。 输出必须符合以下JSON Schema: { "type": "object", "properties": { "name": {"type": "string", "description": "商品名称"}, "price": {"type": "number", "description": "商品价格,单位元"}, "in_stock": {"type": "boolean", "description": "是否有库存"}, "tags": {"type": "array", "items": {"type": "string"}, "description": "商品标签列表"} }, "required": ["name", "price", "in_stock"] } 示例输出: {"name": "无线鼠标", "price": 99.9, "in_stock": true, "tags": ["办公", "无线"]} """

这里有个关键细节:把 Schema 写在 Prompt 里和直接在 API 层声明,效果完全不一样。Prompt 里的 Schema 对模型而言只是“参考”,它记住了这个形状,但没法保证绝对遵守;API 层的 Schema 对模型而言是“硬约束”,尤其是 function calling 模式下,模型输出的那个工具调用的参数就是按照 Schema 生成的,偏离的概率低得多。

2.2 json.loads 的雷区和兜底方案

即便你 Prompt 写得再详细,json.loads仍然可能失败。我收集过最常见的四类错误,每一类都在生产环境真实发生过:

import json # 场景1:模型把 JSON 包在 Markdown 代码块里 text = """```json {"name": "无线鼠标", "price": 99.9} ```""" json.loads(text) # JSONDecodeError # 场景2:JSON 前后有模型自己的“解说” text = """好的,以下是提取结果:{"name": "无线鼠标", "price": 99.9},希望能帮到你。""" json.loads(text) # JSONDecodeError # 场景3:模型用了单引号/宽松字符串 text = "{'name': '无线鼠标', 'price': 99.9}" json.loads(text) # JSONDecodeError # 场景4:输出被截断,末尾少了一个或多个括号 text = '{"name": "无线鼠标", "price": 99.9, "in_stock": tru' json.loads(text) # JSONDecodeError

针对场景 1 和场景 2,我一般会写一个清洗函数,把代码块标记和首尾的非 JSON 内容剥掉。针对场景 3 和场景 4,纯正则就搞不定了,需要更高级的修复手段。下面是我在项目里用过的清洗方案:

import json import re def clean_json_string(text: str) -> str: # 去掉 Markdown 代码块标记 text = re.sub(r"^```(?:json)?\\s*|\\s*```$", "", text.strip()) # 找到第一个 { 和最后一个 },把之外的文字全部丢弃 start = text.find("{") end = text.rfind("}") if start == -1 or end == -1: raise ValueError("找不到 JSON 对象") return text[start : end + 1]

这个函数能解决大部分“夹带私货”的问题,但解决不了“模型把布尔值输出成 tru”这种截断问题。要处理那种情况,就得把目光转向专门的修复工具。

2.3 用 json_repair 做“最后一道防线”

有一个开源库叫json-repair,专门用来修复各种不合法 JSON,实际效果比我手写正则强得多。安装方式很简单:

pip install json-repair

它的用法非常符合直觉:

from json_repair import repair_json, load_json broken = '{"name": "无线鼠标", "price": 99.9, "in_stock": tru' repaired = repair_json(broken) print(repaired) # {"name": "无线鼠标", "price": 99.9, "in_stock": true} data = load_json(broken) print(data) # {'name': '无线鼠标', 'price': 99.9, 'in_stock': True}

这个库能修复的问题包括:末尾多余逗号、单引号替代双引号、缺失的括号、裸单词布尔值、未加引号的 key 等。它的原理是把 JSON 解析拆成词法分析再重新组装,而不是简单正则替换,所以容错能力比手写方案强很多。

但我要提醒一句:json_repair是“事后补救”,不是“事前保证”。如果模型因为输出截断导致信息缺失,修复工具也只能保证“语法合法”,字段里的值可能已经被截掉一半,比如价格从7999变成79,这种错误修复工具是发现不了的。所以它的定位应该是最后一道防线,而不是你依赖的主力工具。

3. withStructuredOutput 完全解析:把“解析问题”消灭在源头

手动解析方案本质上还是在“猜模型的心思”。真正优雅的做法是让模型在生成阶段就只产生合规的结构化数据。LangChain 的with_structured_output就是干这个的。

3.1 它到底是什么,和 OutputParser 有什么区别

很多老手会把它和早期的PydanticOutputParser搞混。这里我做一个明确区分:

  • 早期方案:PydanticOutputParser会在你的 Prompt 里注入一大段 JSON 格式说明和示例,然后模型“尽力”按这个格式输出,你再手动调用parser.parse()去解析。模型还是自由生成文本,只是收到了一段格式引导。
  • with_structured_output:它会在请求层把输出结构“声明”给模型。如果是 function calling 模式,框架会把你的 schema 转换成工具的parameters,模型在生成时不是生成普通回复,而是生成一个“工具调用参数”,这本质上就是一份结构严密的 JSON。解析步骤从“猜文本”变成了“读取参数”,成功率天差地别。

如果你用过 LangChain 稍微早期的版本,应该记得bind_functions或者bind_tools之后再手动解析tool_call的写法。with_structured_output是把这套流程封装了:它在内部帮你绑定工具、调用模型、提取工具参数、转成 Pydantic 对象,最终你拿到的是一个干净、合法的 Python 数据类实例。我把这个演变理解为“把格式化输出的工程问题提升到了模型原生能力的层面”。

3.2 三种调用模式怎么选

with_structured_output最核心的参数是method,它决定了框架走哪条路拿结构化数据。不同模型厂商支持的方案不完全一样,我整理了一个选型表格:

method 值底层机制适合的模型稳定性
function_calling(默认)把 schema 作为工具参数,由模型生成工具调用OpenAI、Anthropic、Google、DeepSeek、Ollama 里的 Qwen 等大多数支持工具调用的模型
tool_calling本质同 function calling,部分新模型厂商用这个命名部分支持 tools 的国产模型
json_mode平台开启 JSON Mode,强制模型输出合法 JSONOpenAI 的response_format={"type": "json_object"}、部分兼容 JSON mode 的模型中高
json_schema平台强制按指定 JSON Schema 输出OpenAI 较新的结构化输出、支持 schema 约束的模型

实际项目中,function_calling对大多数模型来说都是最稳的选择。即便模型本身也支持json_mode,我还是优先用 function calling,因为工具调用的训练数据更充足,模型对“按工具参数输出”这件事的理解更深刻,字段缺失率明显更低。

method参数不传时,LangChain 会根据当前模型自动选择一个可用方案。但自动选择不一定是最优解,尤其是当你用了本地模型或者国内模型服务时,我很建议手动指定一次,免得框架选了一个模型不支持的方式,然后以很奇怪的报错收场。

3.3 参数详解与 include_raw 的有效用法

直接看代码。我们先定义一个 Pydantic 模型,再用with_structured_output让模型按这个结构输出:

from typing import List, Optional from pydantic import BaseModel, Field from langchain_openai import ChatOpenAI class Movie(BaseModel): title: str = Field(description="电影名称") director: str = Field(description="导演姓名") year: int = Field(description="上映年份") rating: float = Field(description="豆瓣评分,保留一位小数") genres: List[str] = Field(description="电影类型列表,如:剧情、喜剧") llm = ChatOpenAI(model="gpt-4o-mini", temperature=0) structured_llm = llm.with_structured_output(Movie) result = structured_llm.invoke("帮我提取《霸王别姬》的电影信息") print(result) # title='霸王别姬' director='陈凯歌' year=1993 rating=9.6 genres=['剧情', '爱情']

注意几个点:

一是temperature调到 0。结构化输出任务是确定性任务,温度越高,模型越容易“创作”,哪怕只是换一种措辞,字段缺失和格式偏离的概率都会上升。我在生产环境里一律把结构化输出链路的温度设为 0。

二是字段描述非常关键。Pydantic 模型里的Field(description=...)会被 LangChain 转成 JSON Schema 里的description,模型就是靠这些描述理解“这个字段到底要填什么内容”。描述写得越具体,模型输出就越准确。我见过不少团队在模型里只写字段名不写描述,结果模型把rating识别的字段值格式五花八门。

三是如果你开启了include_raw=True,返回的就不再是 Pydantic 对象,而是一个包含rawparsedparsing_error三个键的字典:

response = structured_llm.invoke("帮我提取《霸王别姬》的信息", include_raw=True) # 注意:实际是创建 new_llm = structured_llm.with_structured_output(Movie, include_raw=True) # 而不是在 invoke 时传参 new_llm = llm.with_structured_output(Movie, include_raw=True) response = new_llm.invoke("帮我提取《霸王别姬》的信息") print(response["raw"]) # 原始 BaseMessage,带 token 信息 print(response["parsed"]) # 解析后的 Movie 实例,如果失败则为 None print(response["parsing_error"]) # 异常对象,成功则为 None

include_raw在生产环境里很有价值。因为即便 with_structured_output 已经很稳,也架不住模型偶尔发疯,如果你拿不到raw,你连现场日志都没有,排查起来很被动。我建议在重要流程上把它打开,把raw存到日志系统里,方便事后复盘。

3.4 用 Pydantic 定义“高情商”输出模型

Pydantic 在这里承担两个职责:一是定义数据结构,二是通过类型和描述给模型提供“生成蓝图”。定义输出模型时,有几点经验值得分享。

第一,字段类型尽量精确。能用float就不要用str,能用枚举就不要用字符串。给你看一个更完整的设计:

from enum import Enum class StockStatus(str, Enum): in_stock = "in_stock" out_of_stock = "out_of_stock" pre_order = "pre_order" class Product(BaseModel): name: str = Field(description="商品名称") price: float = Field(description="商品价格,单位元,保留两位小数") stock_status: StockStatus = Field(description="库存状态") tags: List[str] = Field(default_factory=list, description="商品标签") discount: Optional[float] = Field(default=None, description="折扣价,没有则为 null")

第二,字段描述要写“模型听得懂的话”,不要写代码注释。例如description="库存状态,只能是 in_stock / out_of_stock / pre_order 之一"就比description="库存状态"好很多。这等于直接给模型圈定了候选答案,能显著减少模型自己发明新值的情况。

第三,必填字段要克制。Pydantic 里的字段不写默认值就是必填,写Optional或者default_factory就是可选。如果必填字段太多,模型在信息不足时为了满足 schema 会强行编造内容,这是“幻觉”的高发场景。信息不确定的字段尽量设为可选,让模型输出null而不是硬编一个值。

3.5 实战:从一段电影介绍里稳定提取 JSON

完整跑通一个案例,你就能理解withStructuredOutput如何替代手写 Prompt。假设你手上有一大段电影介绍文本,比如从某个电影网站复制下来的剧情简介加演职员表,现在要提取结构化信息。

from langchain_openai import ChatOpenAI from pydantic import BaseModel, Field from typing import List class MovieInfo(BaseModel): title: str = Field(description="电影名称") director: str = Field(description="导演姓名") actors: List[str] = Field(description="主演姓名列表") year: int = Field(description="上映年份") duration_minutes: int = Field(description="片长,单位分钟") genres: List[str] = Field(description="电影类型") rating: float = Field(description="某评分网站的评分,没有则为0") text = """ 《流浪地球2》是由郭帆执导,吴京、刘德华、李雪健主演的科幻电影, 于2023年1月22日在中国大陆上映。影片片长173分钟,类型为科幻、冒险、灾难。 豆瓣评分目前为8.3分。 """ llm = ChatOpenAI(model="gpt-4o-mini", temperature=0) structured_llm = llm.with_structured_output(MovieInfo) movie = structured_llm.invoke(text) print(movie) # title='流浪地球2' director='郭帆' actors=['吴京', '刘德华', '李雪健'] # year=2023 duration_minutes=173 genres=['科幻', '冒险', '灾难'] rating=8.3

这段代码最迷人的地方在于:你从头到尾没有写任何 JSON 解析代码,没有正则清洗,没有 try-except 包裹,拿到的就是一个类型正确的MovieInfo实例。如果模型输出有问题,LangChain 会在内部抛出异常或通过parsing_error反馈,你不需要自己处理“字符串里多了一段话”这种脏活。

如果某些字段缺失导致模型无法生成完整对象,你也可以用一个宽松版本兜底:

class MovieInfoLoose(MovieInfo): actors: List[str] = Field(default_factory=list, description="主演姓名列表") structured_llm_loose = llm.with_structured_output(MovieInfoLoose)

这样即使原文里没提主演,模型也会给一个空列表,而不是报错。这个技巧在应对爬虫拿回来的残缺页面文本时特别实用。

4. 进阶操作:复杂结构、温度控制与批量稳定

做到上一步,你已经能把“单层对象”稳定输出出来了。但实际业务往往更复杂:嵌套对象、枚举值、日期格式、大列表,甚至还要同时处理多条记录。这一节我挑几个实战中最高频的进阶场景展开。

4.1 嵌套模型与复杂类型转换

比如你要从一个网页里提取整个电影列表,每个元素包含片名、评分、导演等多个字段。定义嵌套 Pydantic 模型:

from typing import List from datetime import date from pydantic import BaseModel, Field class MovieItem(BaseModel): title: str = Field(description="电影名称") director: str = Field(description="导演") release_date: date = Field(description="上映日期,格式 YYYY-MM-DD") class MovieList(BaseModel): movies: List[MovieItem] = Field(description="电影列表") total: int = Field(description="电影总数")

这里的release_date: date值得注意。Pydantic v2 会自动尝试把模型的字符串输出转成date类型,如果模型输出“2023年1月22日”这种格式,Pydantic 解析就会失败。所以我在描述里一定写清楚“格式 YYYY-MM-DD”,让模型按标准格式输出。本质上,日期、枚举、布尔值这类带格式要求的字段,都要靠“描述”来给模型做一次格式引导,而不是指望 Pydantic 来兼容所有格式。

4.2 可选字段与默认值:给模型“减负”

结构化输出的失败率,很大程度上和必填字段数量正相关。字段越多、嵌套越深、必填越多,模型“猜错”或“编造”的概率就越高。为了降低失败率,我有三条原则:

  • 信息不明确时设Optional,让模型输出null
  • 列表字段给默认空列表,别让模型为了凑数硬编造。
  • 枚举字段描述里明确列出可选值,直接给模型塞“候选答案”。

举个例子,现在要提取招聘 JD 里的岗位信息:

class JobInfo(BaseModel): title: str = Field(description="岗位名称") company: str = Field(description="公司名称") salary_min: Optional[int] = Field(default=None, description="最低薪资,单位K,未知则为 null") salary_max: Optional[int] = Field(default=None, description="最高薪资,单位K,未知则为 null") requirements: List[str] = Field(default_factory=list, description="任职要求列表")

JD 里经常只写“薪资面议”,你要是把salary_min设为必填,模型就只能编一个数字出来,这就是妥妥的错误数据。设为可选之后,模型能够诚实地输出None,后续你在业务层再决定怎么处理,至少不会产生虚假信息。

4.3 温度与 token 限制对结构化输出的影响

temperature对结构化输出的影响比大多数人想象的大。我在 3.3 节提到过温度要设 0,这里解释一下原理。模型在生成 token 时是概率采样,温度越高,分布越平缓,低概率 token 被选中的机会越大。在结构化输出场景里,这意味着模型可能开始“自由发挥”字段名称、格式甚至内容。有一次我把温度误设成 0.8,结果模型在 JSON 里加了一个“recommend”字段,直接导致 Pydantic 校验报错(虽然extra="ignore"可以忽略,但这类字段一多,管理成本直线上升)。现在但凡涉及结构化输出的调用,我都在构建模型时写死temperature=0,宁可牺牲一点“文采”,也要保住格式的确定性。

max_tokens是另一个被忽略的坑。复杂嵌套结构需要很长的输出,如果max_tokens设的小,模型生成到一半被截断,最终返回的 JSON 缺了尾巴。前面说的json_repair能修复一部分语法问题,但字段值是残缺的,修复后也不可用。我的经验是:结构化输出链路的max_tokens至少要比你预估的输出长度多 20% 到 50%。如果你不知道输出多长,就先跑一次样例统计 token 数,再留足余量。

4.4 批量场景下的重试与补偿机制

单个请求稳了,批量请求还会遇到新问题。比如你让模型逐个处理 100 条商品记录,前面 95 条都成功了,第 68 条因为文本里信息残缺导致输出失败。如果整个流程直接报错,那前面的工作全都白费。我现在的做法是给批量处理加一层重试和补偿。

from tenacity import retry, stop_after_attempt, wait_exponential @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10), reraise=True, ) def safe_extract(text: str, structured_llm): return structured_llm.invoke(text) for i, doc in enumerate(docs): try: result = safe_extract(doc.text, structured_llm) results.append(result) except Exception: results.append(None) log_error(f"第 {i} 条记录提取失败,原文: {doc.text[:200]}")

这里有两个要点。第一,重试要带指数退避,避免对同一个热点模型服务造成瞬时峰值压垮限流。第二,重试次数要结合成本考虑,结构化输出调用的底层大模型是按 token 计费的,反复重试会烧钱,我一般设 2 到 3 次。第三,重试也失败时,把失败记录连带原文摘要写进日志,而不是悄悄跳过。有了日志,你才能复盘是不是某个文本类型的提取逻辑需要单独优化。

批量场景还有一个隐蔽问题:不同请求之间,模型输出的格式细节可能漂移。比如第一个请求里日期是2023-01-22,第二个请求里可能变成2023年1月22日。这就要靠 4.1 里说的类型转换来兜底,或者在后处理环节统一做一次数据清洗。不要把“模型一定输出同一种格式”当成假设,最好是先让 Pydantic 做类型转换校验,再对关键字段做业务规则校验。

5. 常见问题与排查技巧实录:从报错到密钥安全

最后这一章,我把这几年来被问得最多、踩得最实的坑集中列出来。有些来自我自己的生产事故,有些来自同行交流,内容偏“排查向”,建议直接收藏当速查表用。

5.1 “failed to deserialize the json body” 这类报错

很多人在调 LangChain 接口时遇到这个报错,尤其是使用的模型服务不完全兼容 OpenAI 协议时:

Failed to deserialize the JSON body into the target type: missing field `messages`

这个报错一般不是模型输出 JSON 失败,而是请求本身没有正确发送。常见原因有两个:一是模型服务商要求额外的messages字段而 LangChain 没带上,说明模型服务端点的协议兼容性有问题;二是你用的模型服务不支持某些参数(比如response_format),服务端照样返回 200 但 body 结构不完整,框架在反序列化时直接炸了。

排查思路很简单:先把verbose=True打开,用httpxcurl手动复现请求,看服务端到底返回了什么。不要一头扎进框架源码里绕,大概率是模型服务商和 LangChain 版本之间的兼容问题,换一个更通用的 endpoint 配置(比如 OpenAI 兼容端点)就好。如果你用的是某个经过封装的企业级模型网关,这个报错尤其常见,解决方案是在 LangChain 的api_base配置里指向网关的标准/chat/completions,而不是网关自己的特殊路径。

5.2 中文乱码与特殊字符问题

结构化输出里中文出现转义是正常现象,比如模型返回\\u5f20\\u827a\\u8c0b而不是“张艺谋”。这并不算错误,json.loads之后自然能转回中文。真正让新手懵的是两种情况。

第一种,输出里夹了不可见的控制字符,比如换行符写成了字面意义的\\n而不是真正的换行,导致 JSON 字符串解析后内容多了两个字符。排查时可以print(repr(json_string))来查看转义后的真实内容,不要直接 print 原串。

第二种,模型把"写成了中文全角或者。这种情况下json.loads会报Invalid control character或者Expecting ',' delimiter。解决办法是清洗时先做全角转半角,或者干脆依赖json_repair这种容错解析工具。我在项目里有一条固定的清洗顺序:去 Markdown 标记 -> 全角转半角 ->json_repair修复 ->json.loads

5.3 模型不支持 withStructuredOutput 怎么办

这是我在帮助读者排查问题时被问到最多的问题,尤其是用了本地模型或某些老模型时,with_structured_output直接报ValueErrorNotImplementedError。解决办法是降级到“JSON Mode + Pydantic 校验”的组合。

llm = ChatOpenAI(model="local-model", temperature=0) llm_json = llm.bind(response_format={"type": "json_object"}) prompt = ChatPromptTemplate.from_messages([ ("system", "你只输出JSON,不要输出任何其他内容。"), ("human", "{input}"), ]) chain = prompt | llm_json raw = chain.invoke({"input": "提取..."}).content # 再用 Pydantic 做校验和转换 try: parsed = MovieInfo.model_validate_json(raw) except ValidationError as e: print("字段校验失败:", e)

如果模型连 JSON Mode 都不支持,那只能回到第 2 章的 Prompt 方案,配合json_repair。这时候我的建议是:如果业务长期依赖这个模型做结构化输出,不如直接换一个支持工具调用的模型。花在清洗字符串上的时间和钱,通常比换模型省下的成本多得多。工具调用已经是当今大模型行业的标准能力了,新发布的模型几乎都支持,死守老模型只会让工程侧越来越吃力。

5.4 密钥管理:别把 API Key 写进代码里

最后一条虽然不是 JSON 解析问题,但它是 LLM 开发里最容易被忽略的安全隐患。我见过不少新手在示例代码里直接写ChatOpenAI(api_key="sk-..."),把密钥提交到 Git 仓库,然后发到公开平台。这在真实项目里等同于把保险箱钥匙贴在门外。

正确的做法是用环境变量或.env文件。LangChain 的ChatOpenAI默认会读OPENAI_API_KEY环境变量,所以这样写就足够安全:

export OPENAI_API_KEY="sk-你的密钥"

如果你用.env文件管理,可以配合python-dotenvpydantic-settings

from pydantic_settings import BaseSettings class Settings(BaseSettings): openai_api_key: str model_config = { "env_file": ".env", "env_prefix": "", "extra": "ignore", } settings = Settings() llm = ChatOpenAI(api_key=settings.openai_api_key)

使用密钥的另一个隐患是日志泄露。LangChain 的回调系统会把请求的 raw body 打进日志,万一你在 prompt 里放了不该出现的信息,或者框架把认证 header 一并打出来,那密钥就等于直接暴露了。我在生产环境里会单独配置一条redact逻辑:在日志输出前,用正则把形如Bearer sk-...api_key=...的内容替换成***,再落盘。

这条经验我多说一句:哪怕你是纯个人项目,也建议养成分离配置的习惯。因为你不确定哪天项目会被人拿去复用、开到公司里、或者发布成开源模板。等到密钥泄露变成事故再来补救,成本和代价都远高于一开始就花 5 分钟做对。

我个人在实际项目里的固定搭配是:with_structured_output+ Pydantic 模型 +temperature=0+ 2 次重试 + 日志脱敏。这套组合让我从“每天跟 JSON 解析错误搏斗”变成“基本睡个安稳觉”。如果你刚开始做 LLM 落地,建议直接从这个配置起步,把踩坑的时间省下来去做业务本身。等你对某个模型的输出习惯足够熟悉之后,再逐步按需调整。

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

4G 显存能跑 RVC 变声器吗?10 分钟录音训出专属音色

4G 显存能跑 RVC 变声器吗&#xff1f;10 分钟录音训出专属音色 【免费下载链接】Retrieval-based-Voice-Conversion-WebUI Easily train a good VC model with voice data < 10 mins! 项目地址: https://gitcode.com/GitHub_Trending/re/Retrieval-based-Voice-Conversio…

作者头像 李华
网站建设 2026/9/20 6:26:13

AI编程工具演进:从代码生成到架构设计

1. 大模型编程辅助工具的技术演进2023年AI编程领域迎来关键转折点&#xff0c;两大技术路线逐渐清晰&#xff1a;以GPT系列为代表的通用大模型正通过代码生成能力重塑开发者工作流&#xff0c;而Claude等专用模型则在代码理解与重构场景持续突破。作为从业者&#xff0c;我亲历…

作者头像 李华
网站建设 2026/9/20 6:26:03

程序员健康饮食:精米与糙米的科学对比与优化方案

1. 程序员饮食健康的核心矛盾作为长期与代码打交道的群体&#xff0c;程序员们普遍面临着久坐、用脑过度、作息不规律等职业健康挑战。在这种工作状态下&#xff0c;主食选择这个看似简单的问题&#xff0c;实际上直接影响着我们的工作效率和长期健康。精米和糙米作为亚洲饮食中…

作者头像 李华
网站建设 2026/9/20 6:23:51

从零部署LibreChat:自建AI对话聚合平台实战指南

1. 为什么我最终把日常AI对话工作流迁到了LibreChat用AI聊天工具的人大概都经历过这个阶段&#xff1a;一开始用某个网页版&#xff0c;觉得挺方便&#xff1b;后来想对比不同模型的回答&#xff0c;就得开好几个标签页来回切换&#xff1b;再后来想让AI帮忙查点资料&#xff0…

作者头像 李华