Pydantic 与结构化输出

Pydantic and structured outputs

大语言模型(LLM)善于生成接近人类表达的文本,但默认输出形式自由,难以预测。在数据提取、报告生成及更一般的智能体系统等生产应用中,我们需要 LLM 按严格格式输出,例如包含指定字段的 JSON,让下游系统能够可靠解析和使用。寄希望于模型恰好生成格式完美的 JSON,存在风险。即使提示词要求输出 JSON,模型也可能添加额外文字、遗漏字段或使用错误类型,导致流水线故障和验证错误。例如,同一个提示词执行 10 次,可能有 9 次格式正确,第 10 次却无法正常解析。

结构化输出技术通过预先定义的 schema 约束或验证模型输出,是构建可靠 AI 系统的重要手段。例如,基于 LLM 的数据录入工具应以一致格式输出 name、email 等字段;执行任务的智能体可能输出结构化的动作说明;问答系统可能返回答案及来源引用列表。如果流水线需要 { id: string, intent: bool },结构化输出便可施加相应格式要求,从而提高可预测性,让数据库、API 和其他代码的集成更加安全。

实现 LLM 结构化输出的主要方法包括:

  • 使用 Pydantic 模型作为 schema,在 Python 中定义并验证输出结构。
  • 使用 OpenAI function calling 和 Structured Outputs 模式,引导模型返回符合 schema 的 JSON。
  • 使用 LangChain output parser,将模型文本格式化并解析成结构化数据。
  • 使用 Instructor 等库封装上述技术,通过 Pydantic 验证输出,并在出错时重试。根据我的经验,Instructor 很适合较简单、无需复杂链式编排的系统。

本课介绍如何使用 Pydantic 定义结构化 schema,以及如何通过 Instructor、OpenAI function calling 和 LangChain output parser 约束并验证 LLM 输出。

使用 Pydantic 定义结构化输出 schema

Pydantic 是利用 Python 类型提示进行数据验证的库。你可以通过类,也就是模型,定义预期的数据结构与类型;Pydantic 会自动验证、解析输入,使其符合 schema。在这里,Pydantic 模型用作 LLM 输出的 schema,描述我们希望模型返回什么。假设需要返回 RAG 流水线的最终答案。除了答案,我们还希望返回检索任务取得、并且在生成过程中实际使用的上下文 ID 列表。

可以定义以下 Pydantic 模型:

from pydantic import BaseModel


class RAGUsedContext(BaseModel):
    id: int
    description: str

class RAGGenerationResponse(BaseModel):
    answer: str
    retrieved_context_ids: list[RAGUsedContext]

有效的 RAGGenerationResponse 必须包含字符串 answer,以及 retrieved_context_ids。后者是对象列表,每个对象均为 RAGUsedContext Pydantic 模型,并遵循自己的结构定义。Pydantic 模型具有多方面优势:

  • 自动类型转换:Pydantic 可以根据字段类型和配置转换输入。例如,将 "answer": 3547 中的整数转换为字符串 "3547",在 Pydantic v2 中需要明确启用 coerce_numbers_to_str,并使用允许该转换的非严格验证模式;默认配置会对此抛出 ValidationError。参见Pydantic 配置说明。
  • 必填与可选字段:默认情况下,没有默认值的字段都是必填字段。模型输出缺少必填字段时,验证失败。
  • 嵌套模型与列表:Pydantic 支持嵌套。如上例,RAGGenerationResponse 中的 retrieved_context_ids: list[RAGUsedContext] 引用了另一个 Pydantic 模型,便于定义含列表和子对象的复杂 JSON 结构。
  • 自定义验证规则:可以添加验证器进行额外检查,例如在 Pydantic v2 中使用 @field_validator,确保答案不超过 X 个字符,否则抛出错误。

Pydantic 模型还可以生成自身的 JSON Schema 定义:v2 使用 model_json_schema(),v1 使用 schema()。可以将 JSON Schema 提供给 LLM,说明应遵循的格式,这是获得正确结构化输出的重要能力。

使用 Pydantic 定义 schema,再据此验证 LLM 输出,可以增加一道保护。即使输出部分错误或格式不符,也能识别问题,在修复前不将其视为有效数据。

为输出设计 Pydantic schema,是实现结构化输出的第一步。它以声明方式表达对模型结果的要求。接下来讨论如何让模型遵循 schema,并在实际应用中验证输出。

约束 LLM 生成结构化输出

建立 RAGGenerationResponse 这样的 schema 后,可以通过多种方式让 LLM 生成符合要求的输出。每种方式都有优势与限制。

使用 Instructor:Pydantic 与重试

这是我最喜欢的方法。Instructor 是专为从 LLM 获取可靠 JSON 而设计的开源库,以 Pydantic 模型作为接口。调用 LLM 时传入 response_model,也就是 Pydantic schema;Instructor 负责组织提示词、解析 JSON、验证结果,并在第一次输出不合要求时重试。它通过一个高级 API 封装“提示词 → JSON 输出 → 验证 → 错误处理”全过程。以下示例将 Instructor 与 OpenAI API 结合:

from openai import OpenAI
import instructor
from pydantic import BaseModel

class RAGUsedContext(BaseModel):
    id: int
    description: str

class RAGGenerationResponse(BaseModel):
    answer: str
    retrieved_context_ids: list[RAGUsedContext]

client = instructor.from_openai(OpenAI())
response, raw_response = client.chat.completions.create_with_completion(
    model="gpt-4.1",
    response_model=RAGGenerationResponse,
    messages=[{"role": "user", "content": prompt}],
    temperature=0.5,
)
print(response)

Instructor 承担了原本需要手动完成的主要工作:

  • 通常通过指令或 function calling,让 LLM 知道应输出 JSON。
  • 解析 LLM 响应,并执行 RAGGenerationResponse.model_validate_json(...)。
  • 如果验证失败,例如 JSON 无效或缺少字段,会携带错误信息或纠正提示重新请求模型。通过 max_retries 控制重试次数,尝试获得有效输出。
  • 成功时返回 Pydantic 对象,本例为 RAGGenerationResponse 实例,可以直接使用具有正确类型的数据。

Instructor 处理了结构化输出中许多繁琐的环节:

  • 根据 Pydantic 模型生成 schema,无需手写 JSON Schema。实际使用中,我仍常通过 model_json_schema() 将预期 JSON schema 加入提示词。我遇到过部分字段描述没有正确传递给模型的情况,加入 schema 会有帮助;我相信这种情况会继续改善。
  • 集中处理错误,减少 JSON 解析与手动修复所需的多层异常处理。
  • 以统一接口支持 OpenAI、Anthropic、Azure、Gemini、本地模型等多个提供方,使结构化输出逻辑不依赖某一家服务,便于灵活选择。
  • 具有适合生产环境的功能,例如流式返回模型生成过程中的部分对象,以及正确组装 JSON 中的嵌套 Pydantic 模型。
  • 轻量且专注于结构化提取,与更大型的框架相比,更轻便、快速,也更容易调试。

在底层,Instructor 使用 OpenAI API 时,会利用 OpenAI function calling 格式或类似机制,让模型生成 JSON。

Instructor 诞生于 OpenAI 较新的 Structured Outputs 功能之前,最初通过 function calling 和提示词设计完成任务。下节介绍的 OpenAI 严格结构化输出已覆盖其中部分功能;对于非 OpenAI 模型,以及多提供方支持、错误纠正等额外需求,Instructor 仍然很有价值。

使用 OpenAI function calling 与 Structured Outputs

OpenAI API 于 2023 年 6 月引入 function calling,之后在 2024 年 8 月进一步推出 Structured Outputs 相关能力。二者都提供了直接从模型获得结构化数据的内置方式。

Function calling 基础:定义一个或多个可供模型调用的函数,每个函数都具有名称,以及描述参数的 JSON schema。在聊天生成过程中,模型可以选择输出 function_call,其中参数遵循 schema。例如,为 run_rag 定义形如 {"question": {"type": "string"}} 的参数结构。用户要求运行 RAG 流水线时,模型可以调用 run_rag,参数为 {"question": "Your Question"}。API 返回结构化数据后,可以使用 json.loads 解析成字典,再验证或实例化 Pydantic 模型。

Function calling 引导模型生成符合 schema 的 JSON,是重要进展。但在某些情况下,模型仍可能出错或忽略 schema。未启用严格约束时,模型得到的是输出指定 JSON 的指令,解码过程并未受到相应约束,因此不能保证总是符合要求。

Structured Outputs:这项功能在解码阶段施加约束。在函数定义中设置 strict: true,或通过 OpenAI SDK 的 response_format 指定严格结构化输出格式,就可以要求 API 按 schema 生成结果。其内部使用受约束解码或语法规则,排除无效 token。

Schema 必须属于受支持的 JSON Schema 子集,模型及请求配置也必须兼容。完整且非拒绝的响应遵循指定结构;应用仍须处理拒绝回答、达到输出 token 上限而产生的不完整响应等情况。格式保证也不保证内容事实正确。参见OpenAI Structured Outputs 指南。

代码用法与 Instructor 相似。以下示例采用 2024 年 8 月更新后的 OpenAI Python SDK 接口:

from openai import OpenAI
from pydantic import BaseModel

class RAGUsedContext(BaseModel):
    id: int
    description: str

class RAGGenerationResponse(BaseModel):
    answer: str
    retrieved_context_ids: list[RAGUsedContext]

client = OpenAI()
response = client.beta.chat.completions.parse(
    model="gpt-4.1",
    response_format=RAGGenerationResponse,
    messages=[{"role": "user", "content": prompt}],
    temperature=0.5,
)
result_json = response.choices[0].message.content
rag_response = RAGGenerationResponse.parse_raw(result_json)

这种方式旨在获得能够解析为 RAGGenerationResponse 的输出,代码形式与第三方库接近。SDK 根据 Pydantic 模型传递对应 schema,由 API 将其用于生成约束。

Schema 与成本:OpenAI function calling 的函数定义占用上下文窗口,并按输入 token 计费。应结合所用接口返回的 usage 与适用价格核算,复杂 schema 的成本也应纳入预算。参见OpenAI function calling 的 token 使用说明。

OpenAI Structured Outputs 的优势:

  • 格式一致性:在受支持的配置与完整、非拒绝响应条件下,输出遵循 schema。
  • 减少解析工作:直接得到 JSON;为了方便使用,仍可验证或解析为对象。
  • 成本评估:可以结合 schema 大小、输入输出用量和重试次数,核算具体应用的费用。
  • 速度:受约束解码可能使生成略快。
  • 简单:使用 OpenAI 模型时较方便,基础场景可能无需外部库,也无需等待框架支持新的高级能力。

限制:

  • 提供方范围:这里介绍的是 OpenAI 原生接口。如果需要其他 LLM 提供方或离线模型,需要采用相应方案。
  • Schema 子集:仅支持部分 JSON Schema 功能。minLength、maxLength 等约束的支持范围取决于模型配置;当前官方指南对微调模型列有额外限制。Instructor 等工具可以承担额外验证。
  • 首次调用延迟:微调模型首次使用新 schema 时,可能产生额外处理延迟;当前官方指南说明其他模型没有这一限制。
  • 模型支持:需要支持 function calling 和严格模式的模型,使用前应检查所选模型与接口的兼容性。

如果只使用 OpenAI,并希望获得一致 JSON,原生 Structured Outputs 是合适选择。涉及其他提供方或高级功能时,Instructor 等库仍很重要。

使用 LangChain output parser

如果使用 LangChain 或类似框架构建应用,output parser 是另一种选择。LangChain 的输出解析器主要承担两项工作:

  • 帮助组织提示词,告诉 LLM 预期格式。
  • 将 LLM 响应解析成结构化结果。

PydanticOutputParser 是其中一种工具。以 Pydantic 模型初始化后,它可以自动根据模型的 JSON Schema 生成格式指令。

示例:

from langchain.output_parsers import PydanticOutputParser
from langchain.prompts import PromptTemplate
from pydantic import BaseModel

class RAGUsedContext(BaseModel):
    id: int
    description: str

class RAGGenerationResponse(BaseModel):
    answer: str
    retrieved_context_ids: list[RAGUsedContext]

parser = PydanticOutputParser(pydantic_object=RAGGenerationResponse)
format_instructions = parser.get_format_instructions()
prompt = PromptTemplate(
    template="Your prompt text ...\n{format_instructions}",
    input_variables=[],
    partial_variables={"format_instructions": format_instructions}
)
# 随后将 prompt 与 LLM(例如 LangChain 的 ChatOpenAI)结合生成输出

parser.get_format_instructions() 会加入类似“输出应为符合以下 JSON schema 的 JSON 实例”的指令,以及 Pydantic 模型的 JSON Schema 字符串。无需这些抽象也可以自行完成;解析器只是自动向模型提供明确的结构要求。

模型生成响应时,理想情况下会遵循 schema,返回 JSON 字符串。LangChain 取得原始输出,聊天模型中通常位于 message["content"],之后可调用 parser.parse(output) 得到 Python 字典或 Pydantic 对象。其内部进行 JSON 解析和 Pydantic 验证。

优势:

  • 可用于不同 LLM。它通过提示词引导,只要模型善于遵循指令,开源模型也可使用。
  • 利用 Pydantic schema,无需模型提供特殊模式。
  • 如果已经使用 LangChain 编排,可以方便地与链或智能体组合。

限制:

  • Schema 放入提示词后会消耗 token;较长的 schema 会占用上下文窗口与预算。不同方案的费用需要根据实际用量比较。
  • 模型没有受到强制格式约束。能力较强的模型通常能够遵循,尤其在较低 temperature 下,但仍可能出错。解析失败时通常需要自行实现重试,Instructor 已包含此机制。
  • 复杂 schema,尤其要求大量结构化文本时,可能处理不佳;提示词长度和模型能力都会成为限制。

LangChain 方法将可靠提示词与解析工作通用化,过程与手动实现类似:给出明确指令,必要时提供示例,再解析结果。它能够自动从 Pydantic 模型生成指令,比在提示词中手写 schema 更方便,也较少出现人工错误。

方法比较

自定义提示词(LangChain 或手动实现)、OpenAI 原生 Structured Outputs、Instructor 等库各有取舍。

Schema 复杂程度:字段较少、结构平坦时,各种方式都可胜任。嵌套对象较多、字段数量增加时,原生严格模式可通过生成约束帮助保持结构一致。基于提示词的方法可能因内容过长而遇到遗漏或错误。Instructor 等库可以通过分解任务或多次尝试处理嵌套 schema,但最终仍受模型能力限制。某些 JSON Schema 功能,例如正则表达式或长度约束,可能不在所用配置的支持范围内,因此仍需 Pydantic 等外部验证器检查,Instructor 也能发现这些问题。

模型遵循程度与保证:OpenAI 严格模式在受支持的配置下,对完整且非拒绝响应提供 schema 一致性保证。未启用 strict 的 function calling,或 LangChain 式提示词,通常能够遵循指令,但仍可能漏掉括号或字段。Instructor 在首次结果无效时携带错误反馈重试,显著提高可靠性;不过,模型持续不配合或 schema 约束过严时,重试后仍可能失败。OpenAI 严格模式通过生成约束提供格式保证,Instructor 与 LangChain 则还依赖模型遵循指令及验证循环。

性能与成本:需要考虑多个方面。

  • Token 与费用:需要计入 schema、提示词、错误反馈及重试。LangChain 的提示词 schema 可能达到数百个 token;Instructor 也需传递 schema 或错误信息,具体方式可能比完整展开 JSON Schema 更简洁。每次重试都是额外 API 调用,因此应尽量减少重试,并依据实际 usage 比较。
  • 延迟:Strict 模式每个 token 的生成可能更快;微调模型还需考虑首次 schema 处理开销。Instructor 出错后第二次、第三次请求会使最坏情况下的延迟增加到原来的两倍或三倍,虽然我在实践中很少遇到。LangChain 如果自行实现失败重试,也可能增加调用。Instructor 可以逐步返回解析对象,有利于长输出;OpenAI 也支持流式接收函数参数增量,参见官方流式调用说明。
  • 集成工作:已经使用 OpenAI 时,原生模式最简单;引入 Instructor 或 LangChain 通常也只需增加一项依赖。

模型幻觉与内容有效性:结构化输出只能保证格式,不能自行防止内容幻觉。模型可以输出结构完美、内容却毫无意义或凭空编造的 JSON。例如,要求从文档提取不存在的事实时,模型仍可能猜测并填写字段。如果 schema 不允许“不知道”或 null,强制格式甚至可能增加幻觉机会,因为模型可能认为必须填写所有必填字段。可以采取以下措施:

  • 在适当情况下,让 schema 能表达不确定性,例如设置可选字段或 unknown 选项。
  • 对内容进行后续验证,例如检查提取 ID 的校验和,或确认引用来源确实存在。
  • 在 RAG 中,要求答案与来源基于检索文档;模型返回未检索到的来源,应视为幻觉。

生态与灵活性:OpenAI 原生方案依赖 OpenAI API,示例使用 Python SDK。Instructor 和 LangChain 是开源工具,支持 OpenAI、Anthropic、本地模型等多个后端。重视模型选择或自行托管时,这些工具较合适。还需考虑社区支持与成熟程度。Instructor 有较大的用户群体,经过较多实际使用;将工具用于生产前,应评估其成熟度。

选择取决于具体场景:

  • 仅使用 OpenAI,并重视格式可靠性与简单使用方式时,可以采用启用 strict 的 function calling。
  • 需要多个提供方或统一方式时,Instructor 提供一致接口与便利功能,也可能在底层持续采用新的结构化输出能力。
  • 已经使用 LangChain,或希望自行实现时,基于提示词的解析器具有灵活性,同时需要更多手动错误处理。

搜索课程

正在加载课程目录…