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

发布时间:2026/9/20 6:26:49
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 # 场景2JSON 前后有模型自己的“解说” 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(modelgpt-4o-mini, temperature0) structured_llm llm.with_structured_output(Movie) result structured_llm.invoke(帮我提取《霸王别姬》的电影信息) print(result) # title霸王别姬 director陈凯歌 year1993 rating9.6 genres[剧情, 爱情]注意几个点一是temperature调到 0。结构化输出任务是确定性任务温度越高模型越容易“创作”哪怕只是换一种措辞字段缺失和格式偏离的概率都会上升。我在生产环境里一律把结构化输出链路的温度设为 0。二是字段描述非常关键。Pydantic 模型里的Field(description...)会被 LangChain 转成 JSON Schema 里的description模型就是靠这些描述理解“这个字段到底要填什么内容”。描述写得越具体模型输出就越准确。我见过不少团队在模型里只写字段名不写描述结果模型把rating识别的字段值格式五花八门。三是如果你开启了include_rawTrue返回的就不再是 Pydantic 对象而是一个包含raw、parsed、parsing_error三个键的字典response structured_llm.invoke(帮我提取《霸王别姬》的信息, include_rawTrue) # 注意实际是创建 new_llm structured_llm.with_structured_output(Movie, include_rawTrue) # 而不是在 invoke 时传参 new_llm llm.with_structured_output(Movie, include_rawTrue) response new_llm.invoke(帮我提取《霸王别姬》的信息) print(response[raw]) # 原始 BaseMessage带 token 信息 print(response[parsed]) # 解析后的 Movie 实例如果失败则为 None print(response[parsing_error]) # 异常对象成功则为 Noneinclude_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_factorylist, description商品标签) discount: Optional[float] Field(defaultNone, 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(modelgpt-4o-mini, temperature0) structured_llm llm.with_structured_output(MovieInfo) movie structured_llm.invoke(text) print(movie) # title流浪地球2 director郭帆 actors[吴京, 刘德华, 李雪健] # year2023 duration_minutes173 genres[科幻, 冒险, 灾难] rating8.3这段代码最迷人的地方在于你从头到尾没有写任何 JSON 解析代码没有正则清洗没有 try-except 包裹拿到的就是一个类型正确的MovieInfo实例。如果模型输出有问题LangChain 会在内部抛出异常或通过parsing_error反馈你不需要自己处理“字符串里多了一段话”这种脏活。如果某些字段缺失导致模型无法生成完整对象你也可以用一个宽松版本兜底class MovieInfoLoose(MovieInfo): actors: List[str] Field(default_factorylist, 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(defaultNone, description最低薪资单位K未知则为 null) salary_max: Optional[int] Field(defaultNone, description最高薪资单位K未知则为 null) requirements: List[str] Field(default_factorylist, description任职要求列表)JD 里经常只写“薪资面议”你要是把salary_min设为必填模型就只能编一个数字出来这就是妥妥的错误数据。设为可选之后模型能够诚实地输出None后续你在业务层再决定怎么处理至少不会产生虚假信息。4.3 温度与 token 限制对结构化输出的影响temperature对结构化输出的影响比大多数人想象的大。我在 3.3 节提到过温度要设 0这里解释一下原理。模型在生成 token 时是概率采样温度越高分布越平缓低概率 token 被选中的机会越大。在结构化输出场景里这意味着模型可能开始“自由发挥”字段名称、格式甚至内容。有一次我把温度误设成 0.8结果模型在 JSON 里加了一个“recommend”字段直接导致 Pydantic 校验报错虽然extraignore可以忽略但这类字段一多管理成本直线上升。现在但凡涉及结构化输出的调用我都在构建模型时写死temperature0宁可牺牲一点“文采”也要保住格式的确定性。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( stopstop_after_attempt(3), waitwait_exponential(multiplier1, min2, max10), reraiseTrue, ) 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 结构不完整框架在反序列化时直接炸了。排查思路很简单先把verboseTrue打开用httpx或curl手动复现请求看服务端到底返回了什么。不要一头扎进框架源码里绕大概率是模型服务商和 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直接报ValueError或NotImplementedError。解决办法是降级到“JSON Mode Pydantic 校验”的组合。llm ChatOpenAI(modellocal-model, temperature0) 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_keysk-...)把密钥提交到 Git 仓库然后发到公开平台。这在真实项目里等同于把保险箱钥匙贴在门外。正确的做法是用环境变量或.env文件。LangChain 的ChatOpenAI默认会读OPENAI_API_KEY环境变量所以这样写就足够安全export OPENAI_API_KEYsk-你的密钥如果你用.env文件管理可以配合python-dotenv或pydantic-settingsfrom 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_keysettings.openai_api_key)使用密钥的另一个隐患是日志泄露。LangChain 的回调系统会把请求的 raw body 打进日志万一你在 prompt 里放了不该出现的信息或者框架把认证 header 一并打出来那密钥就等于直接暴露了。我在生产环境里会单独配置一条redact逻辑在日志输出前用正则把形如Bearer sk-...、api_key...的内容替换成***再落盘。这条经验我多说一句哪怕你是纯个人项目也建议养成分离配置的习惯。因为你不确定哪天项目会被人拿去复用、开到公司里、或者发布成开源模板。等到密钥泄露变成事故再来补救成本和代价都远高于一开始就花 5 分钟做对。我个人在实际项目里的固定搭配是with_structured_output Pydantic 模型 temperature0 2 次重试 日志脱敏。这套组合让我从“每天跟 JSON 解析错误搏斗”变成“基本睡个安稳觉”。如果你刚开始做 LLM 落地建议直接从这个配置起步把踩坑的时间省下来去做业务本身。等你对某个模型的输出习惯足够熟悉之后再逐步按需调整。