Instructor 结构化输出核心:用 Pydantic Response Model 定义 LLM 输出模式
2026/9/15 11:22:09 网站建设 项目流程

Instructor 结构化输出核心:用 Pydantic Response Model 定义 LLM 输出模式

【免费下载链接】instructorstructured outputs for llms项目地址: https://gitcode.com/GitHub_Trending/in/instructor

本文以 Instructor 的 Models 概念文档 为核心骨架,结合仓库源码(instructor/v2/core/schema.py、instructor/v2/providers/openai/schema.py、instructor/v2/core/client.py)与配套教程(Response Models 教程、Fields 概念、Optional Fields 教程)展开。读者将掌握:如何用pydantic.BaseModel定义 LLM 输出结构、如何通过response_model让模型自动校验并返回类型化实例、如何利用 docstring 与字段注解进行提示词工程、如何处理可选字段、如何在运行时动态创建模型,以及如何给模型挂载业务方法。

什么是 Response Model

Instructor 的核心理念是"用 Pydantic 模型定义你要什么,让语言模型照着输出"。在 Instructor 中,这个用于描述输出结构的 Pydantic 模型就被称为Response Model

定义一个 Response Model 极其简单——它就是一个普通的pydantic.BaseModel子类:

from pydantic import BaseModel, Field class User(BaseModel): name: str = Field(description="The name of the user.") age: int = Field(description="The age of the user.")

定义完成后,把它作为response_model参数传给客户端(例如client.create(...)),Instructor 会在背后完成三件事:

  • 定义 schema 与提示词:把你的模型编译成 JSON Schema 并注入 prompt / 工具定义,告诉语言模型"应该输出什么形状的数据";
  • 校验 API 返回:对语言模型的原始输出进行解析与 Pydantic 校验,类型不符或缺失字段都会触发重试(详见 Retrying 概念);
  • 返回模型实例:最终交付的是一个已经通过校验的User实例,而不是一坨需要手动解析的 JSON 字符串。

对应到仓库源码,响应处理的核心管线位于 instructor/v2/core/client.py,其中create的签名里response_model: type[T](或None)直接决定了本次调用是否走结构化输出分支;而"把模型变成发给 LLM 的 schema"这一步,在 v2 架构中按供应商拆分,统一由 instructor/v2/core/schema.py 导出generate_openai_schemagenerate_anthropic_schemagenerate_gemini_schema三个兼容入口,实际实现在各供应商目录下(如 instructor/v2/providers/openai/schema.py)。

一个最小可用示例:

import instructor from pydantic import BaseModel, Field class User(BaseModel): name: str = Field(description="The name of the user.") age: int = Field(description="The age of the user.") client = instructor.from_provider("openai/gpt-4o-mini") user = client.create( response_model=User, messages=[{"role": "user", "content": "Extract: John is 30 years old"}], ) print(user.name) #> John print(user.age) #> 30

from_provider("openai/gpt-4o-mini")是 Instructor 提供的统一客户端创建接口,支持 OpenAI、Anthropic、Gemini、Mistral、Cohere、Groq 等大量供应商(完整列表见 Integrations 索引 与 from_provider 概念)。

用 docstring 与字段注解驱动提示词

Response Model 不仅定义结构,它本身还承载着提示词工程。Instructor 约定:类的 docstring 就是发给语言模型的指令,每个字段的类型注解与Field(description=...)就是对该字段的说明

from pydantic import BaseModel, Field import instructor class User(BaseModel): """ This is the prompt that will be used to generate the response. Any instructions here will be passed to the language model. """ name: str = Field(description="The name of the user.") age: int = Field(description="The age of the user.") client = instructor.from_provider("openai/gpt-4o-mini") user = client.create( response_model=User, messages=[{"role": "user", "content": "Extract: John is 30 years old"}], )

源码层面的证据在 instructor/v2/providers/openai/schema.py:generate_openai_schemadocstring_parser.parse(model.__doc__)解析类 docstring,把其中的参数说明(:param xxx: ...)补进 JSON Schema 对应字段的description;若整个模型没有 description,则自动生成"Correctly extracted \{model.name}` with all the required parameters with correct types"` 作为工具描述。也就是说,你写下的 docstring 与字段描述会被直接编译进发给 LLM 的 tool/function schema,这就是"用 Python 类型系统做提示词工程"的原理。

此外,Pydantic 的Field还提供了更多可用于提示词工程的元数据(详见 Fields 概念):

  • description:字段语义说明;
  • title:字段标题;
  • examples:字段示例值,可显著提升抽取准确性;
  • json_schema_extra:向 JSON Schema 追加任意额外属性。

这些都会进入model.model_json_schema()生成的 schema,进而影响 LLM 的输出质量。

让字段可选:Optional 与默认值

现实中的抽取任务经常遇到"原文里没有这个信息"的情况。此时可以把字段声明为Optional并给出默认值:

from pydantic import BaseModel, Field from typing import Optional import instructor class User(BaseModel): name: str = Field(description="The name of the user.") age: int = Field(description="The age of the user.") email: Optional[str] = Field(description="The email of the user.", default=None) client = instructor.from_provider("openai/gpt-4o-mini") user = client.create( response_model=User, messages=[{"role": "user", "content": "Extract: John is 30 years old"}], ) # user.email == None

需要注意两个关键点:

  1. Optional[str]本身不产生默认值:即使类型写成Optional[str],字段依然会被视为必填(required)。必须显式给出default=None(或default_factory),字段才会在发给 LLM 的 schema 中标记为可选。这一点在 Fields 概念 中也有明确提示。
  2. JSON Schema 层面:可选字段意味着"该字段允许为null",同时Optional不改变类型的描述,LLM 在信息缺失时倾向于返回null而不是凭空编造。

关于"可选值"还有两套进阶工具:

  • Maybe[T]类型:用于表达"模型也不确定"的字段,返回值包裹在Maybe容器中,可通过is_uncertain判断置信度,详见 Maybe 概念 与 Optional Fields 教程;
  • SkipJsonSchema注解:如果某个字段(例如private_fieldscratch_pad不想让语言模型看到,可以用 Pydantic 的SkipJsonSchema[...]把它从发给 LLM 的 schema 中剔除,并配合默认值使用,见 Fields 概念中的对应小节。

从源码看,"可选字段不进入 required 集合"的行为是有意为之:在 instructor/v2/providers/openai/schema.py 的注释中明确说明,parameters["required"]直接复用 Pydantic 自己计算出的schema.get("required", []),而 Pydantic 的 required 集合天然排除了带默认值(无论是default=还是default_factory=)的字段——这也解释了为什么只写Optional而不给默认值不生效。

运行时动态创建模型

当输出结构在编码期无法预知(例如由数据库配置、用户配置或动态业务规则决定)时,可以使用 Pydantic 的create_model在运行时构造模型。

基础用法

from pydantic import BaseModel, create_model class FooModel(BaseModel): foo: str bar: int = 123 BarModel = create_model( 'BarModel', apple=(str, 'russet'), banana=(str, 'yellow'), __base__=FooModel, ) print(BarModel) #> <class '__main__.BarModel'> print(BarModel.model_fields.keys()) #> dict_keys(['foo', 'bar', 'apple', 'banana'])

create_model的字段参数形式为(类型, 默认值或 Field),同时可以用__base__继承已有模型,实现字段的合并与复用。

典型场景:从数据库配置构建模型

文档给出的典型场景是:模型的结构保存在数据库中,例如一张prompt表存有每个model_name对应的property_name / property_type / description

SELECT property_name, property_type, description FROM prompt WHERE model_name = {model_name}

拿到查询结果后,用create_model一行代码完成模型构建:

from pydantic import BaseModel, create_model, Field from typing import List types = { 'string': str, 'integer': int, 'boolean': bool, 'number': float, 'List[str]': List[str], } # Mocked cursor.fetchall() cursor = [ ('name', 'string', 'The name of the user.'), ('age', 'integer', 'The age of the user.'), ('email', 'string', 'The email of the user.'), ] BarModel = create_model( 'User', **{ property_name: (types[property_type], Field(description=description)) for property_name, property_type, description in cursor }, __base__=BaseModel, ) print(BarModel.model_json_schema())

输出正是标准 JSON Schema,可作为response_model直接使用:

{ "properties": { "name": {"description": "The name of the user.", "title": "Name", "type": "string"}, "age": {"description": "The age of the user.", "title": "Age", "type": "integer"}, "email": {"description": "The email of the user.", "title": "Email", "type": "string"} }, "required": ["name", "age", "email"], "title": "User", "type": "object" }

这套模式的价值在于:同一个代码库可以为不同用户/场景生成"字段相同、描述不同"的模型——字段描述即提示词,因此等于实现了"同一结构、个性化 prompt"。关于 JSON Schema 生成的更多细节(Optional 允许 null、Decimal 序列化为字符串、子模型进入$defs等),见 Fields 概念文档的附录。

给模型添加行为:让抽取结果"会做事"

Pydantic 模型本质是 Python 类,因此可以像普通类一样定义方法,为抽取结果附加业务逻辑:

from pydantic import BaseModel from typing import Literal import instructor client = instructor.from_provider("openai/gpt-4.1-mini") class SearchQuery(BaseModel): query: str query_type: Literal["web", "image", "video"] def execute(self): print(f"Searching for {self.query} of type {self.query_type}") #> Searching for cat of type image return "Results for cat" query = client.create( model="gpt-4.1-mini", messages=[{"role": "user", "content": "Search for a picture of a cat"}], response_model=SearchQuery, ) results = query.execute() print(results) #> Results for cat

在这里,Literal["web", "image", "video"]让 LLM 的输出被约束到枚举取值内(详见 Enums 概念),而execute()方法则在抽取完成后原地执行后续动作。这种"结构 + 行为一体"的模式非常适合将 RAG 检索、SQL 执行、API 调用等副作用封装在模型内部——官方博客 RAG is more than embeddings 中有更多此模式的实际案例。

类似地,Pydantic 还支持用@field_validator/@model_validator挂载自定义校验逻辑,使"模型内部即可校验、失败则自动重试",参考 Validation 概念 与 Custom Validators 教程。

组合进阶:从简单模型到复杂结构

Response Model 的能力可以自由组合,覆盖从简单到复杂的各类抽取需求(详见 Response Models 教程):

  • 嵌套模型addresses: List[Address],实现分层数据结构抽取;
  • 列表字段tags: List[str],一次抽取多个同类条目;
  • 字段校验price: float = Field(gt=0)name: str = Field(min_length=3),让 Pydantic 在解析时完成边界校验;
  • 文档即提示:为模型写 docstring、为字段写 description,让 LLM 与同事都能理解模型语义。

一个综合示例(结合 Simple Object Extraction 与 Nested Structure 教程):

from typing import List, Optional from pydantic import BaseModel, Field class Address(BaseModel): street: str city: str country: str class User(BaseModel): """A user record extracted from unstructured text.""" name: str = Field(description="Full name of the user.") age: Optional[int] = Field(default=None, description="Age if mentioned.") addresses: List[Address] = Field(description="All known addresses.") # 作为 response_model 使用: # user = client.create(response_model=User, messages=[...])

常见问题与排查要点

  • 字段声明了Optional却仍被要求必填Optional不自动产生默认值,请补上= Nonedefault_factory
  • 不想让 LLM 看到/生成某字段:用SkipJsonSchema[...]从 schema 中剔除,并给出默认值,避免校验失败。
  • 抽取结果字段总是错误或缺失:优先检查 docstring 与Field(description=...)是否准确——它们直接编译进发给 LLM 的 schema(源码见 instructor/v2/providers/openai/schema.py)。
  • 结构在编码期未知:用create_model从配置/数据库动态构建,字段描述按需生成。
  • 希望失败自动重试:Instructor 默认在响应校验失败时携带错误信息向 LLM 重试,可参考 Retrying 概念 调整max_retries

参考与延伸阅读

  • Response Models 教程:创建响应模型的分步指南
  • Simple Object Extraction:基础抽取模式
  • Nested Structures:复杂层级模型
  • Optional Fields:可选数据的处理
  • Types:各类数据类型的使用
  • Fields:字段高级配置与 JSON Schema 定制
  • Maybe 概念:表达"不确定"的字段
  • Fields 文档中的相关小节:SkipJsonSchema用法

【免费下载链接】instructorstructured outputs for llms项目地址: https://gitcode.com/GitHub_Trending/in/instructor

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询