资讯详情

基于LangChain与Pydantic的结构化输出问答器实战

📅 2026/10/7 6:03:55 | 华诺云谱 👁 阅读
基于LangChain与Pydantic的结构化输出问答器实战
1. 为什么我要做结构化输出问答器大模型问答最让人头疼的地方不是它答不上来而是它答得太自由。你问它帮我分析一下这段用户反馈的情感倾向和涉及的产品模块它给你洋洋洒洒写三百字里面夹杂着整体来看比较正面可能涉及支付环节这种模棱两可的表述。你想把结果直接塞进下游系统做统计、做告警、做入库对不起还得再写一层解析逻辑而解析逻辑本身又是个无底洞。这个项目要解决的就是这件事让 Agent 的输出从一段话变成一个对象。具体来说我基于 LangChain 的 Agent 能力配合 Pydantic 定义输出结构做了一个结构化输出问答器。你给它一个问题它返回的不是散文而是一个严格符合预定义 Schema 的 JSON 对象字段类型、取值范围、必填项全部受控。适合谁来参考如果你正在做 Agent 应用开发尤其是需要把模型输出对接数据库、对接前端表单、对接自动化流程的场景这套思路可以直接抄。如果你只是想让聊天机器人回答得更整齐一点也能用但可能有点杀鸡用牛刀。前置知识要求不高会写 Python、大致知道 LangChain 是什么、听说过 Pydantic 就够了剩下的我在正文里会补。我踩过的最大一个坑是一开始我以为只要在 Prompt 里写请以 JSON 格式输出模型就会乖乖听话。实测下来十次里有那么两三次它会给你加个好的以下是结果的前缀或者把某个字段写成字符串3而不是数字 3。这种不确定性在生产环境里是致命的。所以这个项目的核心不是让模型输出 JSON而是让模型输出可被程序信任的结构化数据。2. 整体设计思路与方案选型2.1 三种结构化输出方案对比在动手之前我把当时能想到的几条路都捋了一遍最后才确定用 Pydantic LangChain 的组合。这里把对比过程摊开讲方便你判断自己的场景该选哪条。方案实现方式优点缺点适用场景Prompt 约束法在提示词里描述 JSON 格式零依赖改起来快不稳定字段类型易错无校验一次性脚本、原型验证函数调用法用模型的 function calling 能力模型原生支持结构较稳不同模型支持度不一Schema 表达力有限简单字段抽取Pydantic 解析法定义模型类解析并校验输出类型强校验可嵌套可复用需要处理解析失败重试生产级 Agent 应用我最终选的是第三条路但不是单纯用 Pydantic而是把它和 LangChain 的with_structured_output以及输出解析器结合起来用。原因很直接Pydantic 负责定义什么是合法的LangChain 负责让模型按这个定义去生成两者分工明确。提示不要迷信某一种方案。我实际项目里是混合用的——简单抽取用函数调用复杂嵌套结构用 PydanticPrompt 约束只作为兜底提示。2.2 为什么是 Pydantic 而不是 dataclass 或 TypedDict有人会问Python 自带的 dataclass 也能定义结构为什么要用 Pydantic关键在于校验。dataclass 只声明类型不校验运行时数据TypedDict 更是纯类型提示运行时形同虚设。而 Pydantic 在实例化时就会做类型转换和校验比如你声明age: int传进来字符串25它会自动转成 25传进来abc直接抛 ValidationError。这个特性在 Agent 场景里价值巨大。模型输出的东西本质上是不可信输入你必须假设它随时可能给你惊喜。Pydantic 就是那道防线把脏数据挡在业务逻辑之外。另外 Pydantic 支持嵌套模型、字段别名、自定义校验器、字段描述description这些描述还能被 LangChain 拿去做 Schema 提示一举两得。2.3 问答器的整体架构整个问答器的数据流是这样的用户输入问题 → LangChain Agent 接收 → Agent 根据问题决定是否需要调用工具比如查资料→ 模型生成结构化输出 → Pydantic 校验 → 返回对象。这里有个设计决策值得说我把结构化输出做成了 Agent 的最终输出格式而不是中间步骤。也就是说Agent 可以自由地思考、调用工具、多轮推理但最后一步必须收敛到一个 Pydantic 对象。这样做的好处是既保留了 Agent 的灵活性又保证了输出的确定性。架构上分三层输入层负责接收问题并做基础清洗推理层是 LangChain Agent 加工具集输出层是 Pydantic 模型加解析器加重试逻辑。三层之间通过明确的接口通信任何一层出问题都不会污染其他层。3. 核心细节解析与实操要点3.1 Pydantic 模型怎么设计才合理设计输出模型是整个项目的地基地基歪了后面全白搭。我的经验是遵循三个原则字段尽量扁平、枚举优于自由文本、必填与选填分清。先看一个反面例子。我最初设计的模型是这样的class Answer(BaseModel): content: str metadata: dict看起来简单实际上是个灾难。content是啥都能塞metadata是个无底洞下游根本不知道怎么用。后来我改成from pydantic import BaseModel, Field from enum import Enum from typing import List, Optional class ConfidenceLevel(str, Enum): HIGH high MEDIUM medium LOW low class Source(BaseModel): title: str Field(description来源标题) url: Optional[str] Field(defaultNone, description来源链接没有则留空) class StructuredAnswer(BaseModel): question: str Field(description用户原始问题) answer: str Field(description核心回答控制在200字以内) confidence: ConfidenceLevel Field(description回答置信度) sources: List[Source] Field(default_factorylist, description参考来源列表) follow_up: Optional[str] Field(defaultNone, description建议的追问)这个模型好在哪confidence用枚举模型只能在三个值里选不会给你编出比较确定这种词sources是列表天然支持零到多个来源follow_up选填没有就不填不会硬凑。注意Field里的description不是写给人看的是写给模型看的。LangChain 会把这些描述拼进 Schema 提示里描述写得越清楚模型输出越准。我一般会把字段的格式要求、字数限制、取值范围都写进去。3.2 LangChain 结构化输出的接入方式LangChain 提供了with_structured_output方法可以直接把 Pydantic 模型绑到模型上。用法大致是这样from langchain_openai import ChatOpenAI llm ChatOpenAI(modelgpt-4o-mini, temperature0) structured_llm llm.with_structured_output(StructuredAnswer) result structured_llm.invoke(LangChain 的 Agent 和 Chain 有什么区别)result直接就是StructuredAnswer实例不用手动解析 JSON。这背后 LangChain 做了两件事一是把 Pydantic Schema 转成模型能理解的格式不同模型走不同路径有的用 function calling有的用 JSON mode二是把模型返回的结果反序列化成对象。但这里有个坑with_structured_output默认不处理解析失败。如果模型抽风返回了不合法的内容它会直接抛异常。所以生产环境必须包一层重试。from tenacity import retry, stop_after_attempt, wait_fixed retry(stopstop_after_attempt(3), waitwait_fixed(1)) def safe_invoke(question: str) - StructuredAnswer: return structured_llm.invoke(question)temperature0也是关键。结构化输出场景下创造性是敌人我们要的是稳定复现。我实测过temperature 从 0 调到 0.7解析失败率大概会翻三倍。3.3 Agent 与结构化输出的结合点单纯用with_structured_output只是结构化输出加上 Agent 才是结构化输出问答器。区别在于 Agent 能调用工具。我的做法是Agent 负责推理和工具调用最后一步用一个专门的格式化节点把结果转成 Pydantic 对象。在 LangGraph 里这很自然就是一个节点的事如果用传统 AgentExecutor可以在return_only_outputs之后加一层解析。工具集我配了三个一个网页检索工具、一个本地知识库查询工具、一个计算器。前两个负责补信息第三个负责算数。为什么要计算器因为模型做算术经常出错尤其是多步计算交给工具稳得多。提示工具返回的内容也要结构化。我一开始让检索工具返回一大段文本结果模型在格式化时经常把无关内容也塞进answer字段。后来改成工具返回List[Source]模型只需要挑选和引用输出质量立刻上来了。4. 实操过程与核心环节实现4.1 环境准备与依赖安装先把环境搭起来。我用的是 Python 3.11太老的版本 Pydantic v2 支持不好。python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install langchain langchain-openai langchain-community pydantic tenacity版本上有个细节LangChain 生态迭代很快langchain和langchain-core的版本要匹配否则会出现with_structured_output找不到的诡异问题。我建议锁版本比如langchain0.3.x配langchain-core0.3.x。装完之后跑一句python -c import langchain; print(langchain.__version__)确认一下。API Key 通过环境变量注入不要写死在代码里。这是基本安全习惯我就不多说了。4.2 定义输出模型与校验器模型定义我在 3.1 已经给了一版这里补充两个进阶技巧。第一个是自定义校验器。比如我想限制answer字段不能为空且不超过 500 字from pydantic import field_validator class StructuredAnswer(BaseModel): # ... 其他字段 answer: str field_validator(answer) classmethod def check_answer(cls, v: str) - str: v v.strip() if not v: raise ValueError(answer 不能为空) if len(v) 500: raise ValueError(answer 超过 500 字) return v第二个是字段别名。有时候模型倾向于输出中文键名但你希望 Python 侧用英文可以用aliasanswer: str Field(alias回答, description核心回答)不过我个人不太推荐用别名容易在序列化时踩坑除非有明确的对接需求。4.3 构建 Agent 与工具链工具用tool装饰器定义最省事from langchain_core.tools import tool tool def search_knowledge(query: str) - str: 根据关键词检索本地知识库返回最相关的三条内容。 # 实际实现省略返回拼接后的文本 return ...注意 docstring 必须写清楚Agent 靠它判断什么时候调用这个工具。我见过有人 docstring 写搜索结果 Agent 啥问题都去搜效率极低。写清楚根据关键词检索本地知识库Agent 就知道只在需要查资料时用。Agent 的组装用 LangGraph 会更清晰但如果你只想快速跑通用create_react_agent也行from langgraph.prebuilt import create_react_agent agent create_react_agent(llm, tools[search_knowledge, calculator])4.4 完整调用流程与参数选择把上面几块拼起来完整流程是这样的def ask(question: str) - StructuredAnswer: # 第一步Agent 推理可能调用工具 agent_result agent.invoke({messages: [(user, question)]}) raw agent_result[messages][-1].content # 第二步结构化格式化 prompt f请把以下内容整理成结构化回答\n{raw}\n\n原始问题{question} return safe_invoke(prompt)这里有个参数选择值得展开要不要把 Agent 推理和结构化格式化分成两次模型调用我试过合并成一次让 Agent 直接输出结构化结果但效果不好——Agent 在推理时如果还要兼顾格式容易顾此失彼。分开之后第一次调用专注推理第二次调用专注格式化各司其职成功率明显更高。代价是多一次 API 调用成本大概增加 30%但换来的是稳定性我觉得值。如果你对成本敏感可以只在第一次调用失败时才走第二次正常情况用with_structured_output一步到位。5. 常见问题与排查技巧实录5.1 解析失败的五种典型情况结构化输出最常遇到的问题就是解析失败。我把踩过的坑整理成一张速查表现象根本原因解决方法返回带 Markdown 代码块包裹模型习惯性加 json解析前先 strip 掉代码块标记字段类型不符数字变字符串模型对类型不敏感Pydantic 开启宽松模式或加 validator 转换枚举值超出范围模型自创了枚举项用Literal或枚举 校验器兜底必填字段缺失模型偷懒省略在 description 里强调必填加重试嵌套对象层级错乱Schema 太深模型理解不了拆分成多次调用逐层构建其中字段类型不符是最常见的。Pydantic v2 默认是严格模式字符串3不会自动转成整数 3。你可以在模型配置里开model_config ConfigDict(coerce_numbers_to_strFalse)之类的选项或者干脆在 validator 里手动转。5.2 重试策略怎么设计才不浪费钱重试不是无脑重试。我的策略是分级重试第一次失败原样重试第二次失败在 Prompt 里加上失败原因比如上次输出中 confidence 字段值不合法请从 high/medium/low 中选择第三次失败降级到纯文本输出并记录日志。这样设计的原因是很多失败是偶发的模型随机性原样重试就能过有些失败是系统性的Schema 描述不清需要给模型额外提示如果三次都过不了说明这个 Schema 或这个模型有问题继续重试只是烧钱。注意重试要加退避等待wait_fixed(1)或wait_exponential都行。我见过有人不加等待直接重试结果触发限流反而更慢。5.3 几个独家避坑心得第一个心得Schema 别设计太复杂。我一开始设计了一个五层嵌套的模型结果解析成功率不到 60%。后来拆成三个扁平模型分三次调用成功率上到 95% 以上。模型对嵌套的理解能力有限层级越深越容易出错。第二个心得给模型看例子。在 Prompt 里放一两个输入输出示例比写十行描述都管用。这叫 few-shot在结构化输出场景下效果尤其明显。第三个心得日志要记原始输出。解析失败时你看到的只是 ValidationError但真正的问题在模型返回的原始文本里。我一开始没记原始输出排查问题时两眼一抹黑。后来每次调用都把原始返回存下来排查效率提升一大截。第四个心得不同模型表现差异巨大。同一个 Schema我用过几个主流模型解析成功率能差出 20 个百分点。选模型时不要只看价格和通用能力结构化输出能力要单独测。我的做法是准备 50 条测试问题跑一遍看成功率再决定用哪个。6. 这套方案还能怎么扩展跑通基础版本之后我做了几个扩展效果都不错分享给你。第一个扩展是多 Schema 路由。用户的问题类型不同需要的输出结构也不同。比如问事实类问题输出FactAnswer问对比类问题输出ComparisonAnswer。做法是先让模型判断问题类型再选对应的 Schema。这样每个 Schema 都能设计得更贴合场景不用为了兼容所有情况而妥协。第二个扩展是流式结构化输出。传统结构化输出要等模型全部生成完才能解析用户等待时间长。可以改成流式生成边生成边解析部分字段先把answer字段吐给用户看sources等字段后到。LangChain 的astream_events配合 Pydantic 的部分解析能做到但实现有点绕适合对体验要求高的场景。第三个扩展是输出缓存。相同或相似的问题结构化结果可以直接缓存复用。我用的是问题 embedding 加相似度阈值超过 0.95 就命中缓存。这在问答量大的场景下能省不少钱但要注意缓存失效策略知识库更新后要及时清理。第四个扩展是人工反馈闭环。把用户对结构化输出的修正比如改了confidence值收集起来定期用来微调模型或优化 Prompt。这个闭环跑起来之后系统的准确率会持续爬升而不是停在原地。我个人在实际操作中的体会是结构化输出这件事技术方案本身不复杂难的是对不确定性的管理。模型永远会给你惊喜你要做的是用 Pydantic 把惊喜挡在门外用重试把偶发失败消化掉用日志把系统性问题暴露出来。把这三点做好这套问答器就能稳稳地跑在生产环境里。最后再分享一个小技巧每次改完 Schema别急着上线先拿那 50 条测试问题跑一遍成功率没到 90% 就别发版这个习惯帮我省了无数次半夜救火。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑