LLM结构化输出实战:用PydanticAI与Instructor构建可校验的JSON生成链路
1. 模型返回的 JSON 为什么总在关键时刻掉链子你让模型从一段合同文本里抽甲方、乙方、金额、签署日期它回你一段“看起来像 JSON”的东西开头带一句“好的以下是提取结果”结尾少个右括号金额字段一会儿是38一会儿是38日期写成“去年三月”。你写正则补了半小时上线后遇到新样本又崩了。这不是模型不行而是你让它“自由发挥”了——自然语言的输出空间太大而业务系统要的是确定性的、可校验的结构化数据。结构化输出Structured Output要解决的就是这件事把模型的输出约束到一个预先定义好的 Schema 上字段名、类型、取值范围全部可验证验证不过就自动重试直到拿到合法数据或明确失败。它适合谁适合所有要把 LLM 接进真实业务链路的开发者——做信息抽取、表单填充、工单分类、Agent 工具调用参数生成只要下游代码需要obj.field而不是text.split()你就需要它。这篇聚焦三个主流方案PydanticAI、Instructor、LangChain 的with_structured_output。我会用同一个“招聘信息抽取”任务把三者的 Schema 定义、校验重试配置、端到端验证脚本都写成可复制的代码并且统一通过 TaoToken 的 API 通道接入模型这样你不用在多个厂商的 Key 之间来回切换。实测下来真正决定成败的不是框架本身而是 Schema 的约束粒度和重试时错误信息怎么回传给模型——这两点我会重点展开。先说结论方向Instructor 的 retry 反馈最精准PydanticAI 的类型系统最严谨LangChain 的生态整合最省事。但三者都能通过同一个 OpenAI 兼容入口工作下面一步步来。2. 用 TaoToken 统一 API 通道接入三个框架在写 Schema 之前先把“模型从哪来”这件事固定下来。三个框架默认都各自对接 OpenAI、Anthropic 等厂商如果你要对比测试就得维护三套 Key、三套 Base URL切换模型时改到崩溃。TaoToken 提供的是 OpenAI 兼容的统一入口一个 Key、一个 Base URL就能调用不同模型三个框架都能直接指过去。你需要先拿到 API Key打开 https://taotoken.net/api-keys 创建复制那串sk-开头的密钥。注意不要把它硬编码进代码提交到仓库用环境变量管理。# Linux / macOS export TAOTOKEN_API_KEYsk-你的密钥 export TAOTOKEN_BASE_URLhttps://taotoken.net/api # Windows PowerShell $env:TAOTOKEN_API_KEYsk-你的密钥 $env:TAOTOKEN_BASE_URLhttps://taotoken.net/apiBase URL 是https://taotoken.net/api注意结尾不要多加/v1OpenAI SDK 会自己拼/chat/completions。如果你用的是某些需要显式/v1的客户端写成https://taotoken.net/api/v1也可以但三个框架里统一用不带/v1的形式最省心。安装依赖三个框架放一起方便对比pip install pydantic-ai-slim[openai] instructor openai langchain-openai pydantic版本上PydanticAI 用 0.1.x 及以上Instructor 用 1.xLangChain 用 0.3.x。装完后先做一次最小连通性验证确认 Key 和 Base URL 没问题再往下写业务代码import os from openai import OpenAI client OpenAI( api_keyos.environ[TAOTOKEN_API_KEY], base_urlos.environ[TAOTOKEN_BASE_URL], ) resp client.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: 只回复两个字连通}], ) print(resp.choices[0].message.content)如果这一步报401八成是 Key 没读到或者复制时带了空格报Connection error或local proxy failed检查你的网络环境是否把taotoken.net拦了或者环境变量里残留了旧的代理配置。连通之后三个框架的接入方式分别是PydanticAI 用OpenAIModel指定 base_urlInstructor 用instructor.from_openai(OpenAI(...))包装LangChain 用ChatOpenAI(base_url...)。具体代码在下一节展开。提示把模型名也放进环境变量比如TAOTOKEN_MODELgpt-4o-mini这样换模型不用改代码。对比测试时建议固定temperature0减少随机性对结构化输出的干扰。3. 可复制的 Schema 定义与校验重试配置这一节是核心。我先把三个框架的完整配置片段给出来路径和参数都按可直接运行的标准写。任务统一为从招聘文本中抽取职位、公司、地点、薪资范围含月数、技能列表、经验年限、学历、联系方式。先定义共享的 Pydantic 模型三个框架复用同一套 Schema这样对比才公平from typing import Optional from pydantic import BaseModel, Field, field_validator class SalaryRange(BaseModel): min_salary: float Field(description最低月薪单位K只填数字) max_salary: float Field(description最高月薪单位K只填数字) months: int Field(default12, description年薪月数如15薪就填15) class JobPosting(BaseModel): title: str Field(description职位名称) company: str Field(description公司名称) location: str Field(description工作地点) salary: SalaryRange Field(description薪资范围) skills: list[str] Field(min_length1, description技能要求列表) experience_years: int Field(ge0, description最低工作年限) education: str Field(description学历要求) contact: Optional[str] Field(defaultNone, description联系方式没有则null) field_validator(title) classmethod def title_not_empty(cls, v: str) - str: if not v.strip(): raise ValueError(职位名称不能为空) return v.strip()3.1 PydanticAI 配置PydanticAI 通过OpenAIModel指定 base_url把 TaoToken 作为模型提供方import os from pydantic_ai import Agent from pydantic_ai.models.openai import OpenAIModel model OpenAIModel( model_nameos.environ.get(TAOTOKEN_MODEL, gpt-4o-mini), base_urlos.environ[TAOTOKEN_BASE_URL], api_keyos.environ[TAOTOKEN_API_KEY], ) job_agent Agent( model, result_typeJobPosting, system_prompt( 你是招聘信息解析专家。15薪表示年薪月薪×15K表示千元。 薪资只填数字不要带单位。不确定的字段填null不要猜测。 ), )PydanticAI 的校验失败会自动触发重试重试时把 Pydantic 的验证错误作为上下文回传给模型。你不需要手动写 retry 循环。3.2 Instructor 配置Instructor 包装 OpenAI 客户端max_retries控制重试次数import os import instructor from openai import OpenAI raw_client OpenAI( api_keyos.environ[TAOTOKEN_API_KEY], base_urlos.environ[TAOTOKEN_BASE_URL], ) client instructor.from_openai(raw_client) def extract_with_instructor(text: str) - JobPosting: return client.chat.completions.create( modelos.environ.get(TAOTOKEN_MODEL, gpt-4o-mini), response_modelJobPosting, max_retries2, temperature0, messages[ {role: system, content: 从文本中提取招聘信息薪资只填数字。}, {role: user, content: text}, ], )max_retries2是经验值。Instructor 每次重试会把上一次的 validation error 原文喂回模型比如salary.max_salary: Input should be a valid number, received 上不封顶模型看到具体错在哪第二次通常就能修正。设成 5 次以上意义不大反而烧 token——如果两次都修不对说明 Schema 或 prompt 本身有歧义。3.3 LangChain 配置LangChain 用ChatOpenAI指定 base_url再with_structured_output绑定 Schemaimport os from langchain_openai import ChatOpenAI llm ChatOpenAI( modelos.environ.get(TAOTOKEN_MODEL, gpt-4o-mini), api_keyos.environ[TAOTOKEN_API_KEY], base_urlos.environ[TAOTOKEN_BASE_URL], temperature0, ) structured_llm llm.with_structured_output(JobPosting)LangChain 的with_structured_output在 OpenAI 模型下走 function callingSchema 会作为工具定义传给模型。它的重试需要你自己包一层框架本身不自动重试。三件套对照Base URL 统一是https://taotoken.net/apiKey 统一用TAOTOKEN_API_KEYModel ID 统一从TAOTOKEN_MODEL读。任何框架出问题先确认这三个值没写错。4. 端到端验证请求与成功结果配置写完跑一个真实样本把三个框架的输出并排看。测试文本test_input 急招高级Python开发工程师 公司某科技有限公司 地点北京·海淀 薪资35K-50K·15薪 要求 - 3年以上Python开发经验 - 熟悉FastAPI/Django框架 - 有大模型应用开发经验优先 - 计算机相关专业本科及以上学历 联系hrexample.com PydanticAI 调用result job_agent.run_sync(test_input) job result.data print(job.model_dump_json(indent2))Instructor 调用job extract_with_instructor(test_input) print(job.model_dump_json(indent2))LangChain 调用job structured_llm.invoke(test_input) print(job.model_dump_json(indent2))三者理想输出一致{ title: 高级Python开发工程师, company: 某科技有限公司, location: 北京·海淀, salary: {min_salary: 35.0, max_salary: 50.0, months: 15}, skills: [Python, FastAPI, Django, 大模型应用开发], experience_years: 3, education: 本科及以上, contact: hrexample.com }注意salary是嵌套对象months从“15薪”正确解析成 15skills是列表且至少一项。拿到这个对象后下游代码可以直接job.salary.max_salary * job.salary.months算年薪不用再做任何字符串处理。再跑一个批量验证脚本统计成功率和重试情况import time def benchmark(call_fn, cases, name): ok, fail, total_latency 0, 0, 0.0 for text in cases: start time.time() try: call_fn(text) ok 1 total_latency time.time() - start except Exception as e: fail 1 print(f[{name}] 失败: {str(e)[:120]}) print(f{name}: 成功 {ok}/{len(cases)}, 平均延迟 {total_latency/max(ok,1):.2f}s) cases [test_input, test_input.replace(35K-50K, 面议), test_input.replace(本科, 硕士)] benchmark(lambda t: job_agent.run_sync(t), cases, PydanticAI) benchmark(extract_with_instructor, cases, Instructor) benchmark(lambda t: structured_llm.invoke(t), cases, LangChain)“面议”这种无法提取数字的样本是检验重试机制的好材料。Instructor 会把min_salary验证失败的原文回传模型第二次可能返回null或触发你的兜底逻辑PydanticAI 同样自动重试LangChain 则直接抛ValidationError需要你自己 catch。5. 本篇常见报错排查结构化输出踩的坑八成集中在这几类报错上逐个对照。401 Unauthorized或invalid_api_keyKey 没读到或写错。检查echo $TAOTOKEN_API_KEY是否有值代码里是否用了os.environ[TAOTOKEN_API_KEY]而不是硬编码。如果 Key 是从网页复制的注意别把首尾空格带进去。Connection error/local proxy failed/Failed to establish a new connection网络层没通。先确认https://taotoken.net/api能访问再检查环境变量里有没有残留的HTTP_PROXY、HTTPS_PROXY指向一个已经失效的地址。清掉它们unset HTTP_PROXY HTTPS_PROXY。ValidationError: 1 validation error for JobPosting后面跟着reading choices或NoneType模型返回了空内容或非预期结构。常见于模型名写错比如写了个不存在的 model id或者temperature太高导致输出跑偏。把 model id 换成TAOTOKEN_MODEL里确认可用的值temperature 设 0。OAuth相关报错比如OAuth token exchange failed说明客户端在尝试走 OAuth 流程而不是 API Key。检查你是不是误用了某个需要 OAuth 的 SDK 配置或者 base_url 被改成了非 API 端点。统一用https://taotoken.net/api加 API Key 即可。pydantic_ai.exceptions.UnexpectedModelBehavior或 Instructor 重试耗尽后抛InstructorRetryException模型连续多次没通过校验。先看错误信息里具体是哪个字段通常是 Schema 描述不清。比如months字段没写“15薪填15”模型可能填成 1.5。把Field(description...)写具体重试次数保持 2-3 次。with_structured_output返回NoneLangChain 在模型没触发 function call 时会返回 None。确认模型支持 function calling且 prompt 里没有让模型“用自然语言回答”的指令。加一句“必须调用工具返回结构化数据”通常能解决。还有一个隐蔽的坑嵌套层级太深。Anthropic 系模型走 tool_use 时超过 3 层嵌套容易丢字段。把list[dict[str, list[dict]]]这种结构拍平成list[str]加一个描述字段可靠性立刻上来。6. 把结构化输出接进你的业务链路三个框架跑通后选型其实取决于你现有的技术栈。如果你在快速验证一个抽取功能Instructor 的from_openai包装最省事三行代码就能用retry 反馈也最到位。如果你要构建带依赖注入、多步骤的 AgentPydanticAI 的类型系统和流式支持更合适。如果项目已经在 LangChain 生态里with_structured_output不用引入新依赖。真正让结构化输出稳定的不是框架而是两件事Schema 的约束够不够具体以及验证失败时错误信息有没有精准回传给模型。Field(description...)写得越像给新人的注释模型一次通过率越高max_retries保持在 2-3 次配合清晰的错误反馈比盲目加大重试次数有效得多。下一步你可以把JobPosting换成自己业务的模型比如订单、工单、病历摘要然后跑一遍批量验证脚本看成功率。如果要在生产里长期跑建议把模型调用统一走 TaoToken 的 Coding Plan省去多厂商 Key 管理验证阶段想快速试不同模型直接用模型对话页面切换对比就行。接入文档在 https://taotoken.net/doc 有完整的参数说明遇到报错先对照第 5 节排查基本能覆盖九成问题。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →