Instructor 架构深度解析:patch、Retry 重试层与响应分发机制的完整执行链路
【免费下载链接】instructorstructured outputs for llms项目地址: https://gitcode.com/GitHub_Trending/in/instructor
本篇技术指南基于 docs/architecture.md,结合当前仓库的 v2 核心源码,系统讲解 Instructor 库的内部架构与设计决策:从patch()包装 provider 客户端开始,到 tenacity 重试层、Mode分发器、Provider Handler 的请求整形与 reask 纠错,再到最终 Pydantic 模型的解析与_raw_response挂载。读完本文,你将掌握 Instructor 的最小同步/异步调用路径、流式(Iterable)与部分对象(Partial)模式如何接入分发器、并行工具模式的约束、Hook 埋点与重试语义,以及如何为新的 provider 扩展注册一套完整的 handler。
整体架构与高层执行流程
Instructor 的核心思想并不复杂:把"请求整形 + 重试 + 响应解析"这一整套管道,透明地叠加在原生 LLM provider 客户端之上。开发者只需要传入一个response_model(Pydantic 模型),剩下的 schema 生成、工具参数注入、JSON 解析、校验失败重试全部由库内部完成。
原文给出了一个非常清晰的时序图(sequenceDiagram),完整描述了从用户代码到最终模型实例的全过程:
U->>I: chat.completions.create(response_model=..., **kwargs) Note over I: patch() 用 cache/templating 和 retry 包装 create() I->>R: retry_sync/async(func=create, max_retries, strict, mode, hooks) loop attempts R->>C: create(**prepared_kwargs) C-->>R: raw response(provider 特有) R->>D: process_response(_async)(response, response_model, mode, stream) alt Streaming/Partial D->>M: Iterable/Partial.from_streaming_response(_async) D-->>R: Iterable/Partial 模型(或条目列表) else Standard D->>H: provider mode handler(format/parse 选择) H-->>D: 调整后的 response_model/new_kwargs(如需) D->>M: response_model.from_response(...) M-->>D: 解析后的模型(挂载 _raw_response) D-->>R: model(或适配后的简单类型) end R-->>I: parsed model end I-->>U: final model(实例上附带 _raw_response) Note over R,H: 遇到 validation/JSON 错误 → 走 reask 路径 R->>H: handle_reask_kwargs(..., exception, failed_attempts) H-->>R: 用于下一次尝试的新 kwargs/messages在这个流程中,四个参与者各自承担明确的职责:
patch():包装 provider 的create,叠加 cache 查找/保存、templating、strict 模式、hooks 与 retry。- Retry 层(tenacity):执行 provider 调用、发射 hooks、累计 usage、处理 validation/JSON 错误并触发 reask,然后重新尝试。
- Dispatcher(process_response):按
Mode选择正确的解析路径,处理多模态消息转换,并把_raw_response挂载到返回的模型上。 - Provider Handlers:负责 provider/mode 特异的请求整形与 reask 准备。
源码中的"Dispatcher":从 process_response 到 ModeRegistry
值得说明的是,当前仓库的 v2 架构已经将原文中"Dispatcher(process_response)"的职责进一步细分:instructor/v2/core/registry.py中的ModeRegistry成为中央分发枢纽,它把(Provider, Mode)二元组映射到一组 handler:
# instructor/v2/core/registry.py @dataclass class ModeHandlers: request_handler: RequestHandler # 请求整形(tools/schema 注入) reask_handler: ReaskHandler # 校验失败后的纠错 response_parser: ResponseParser # 响应解析 stream_extractor: StreamExtractor | None = None stream_extractor_async: AsyncStreamExtractor | None = None message_converter: MessageConverter | None = None # 多模态消息转换 template_handler: TemplateHandler | None = NoneModeRegistry支持惰性加载与动态注册:handler 首次被查询时才 import 对应模块,并用threading.Lock保护并发首次调用场景(见 registry.py 的注释说明)。而retry_sync_v2在发起调用前会先执行RegistryValidationMixin.validate_mode_registration(provider, mode),若该模式未注册,会抛出带可用模式列表的RegistryError(见 exceptions.py),从源头上拦截"模式与 provider 不匹配"的误用。
patch():入口处的统一包装
patch()是整条管道的入口。在 v2 中,instructor/v2/core/patch.py的patch_v2接受原始 provider 调用函数(例如client.messages.create)、Provider枚举与Mode,通过is_async(func)判断后生成同步或异步包装器:
# instructor/v2/core/patch.py(节选) def patch_v2(func, provider, mode, default_model=None): RegistryValidationMixin.validate_mode_registration(provider, mode) func_is_async = is_async(func) if func_is_async: return _create_async_wrapper(func, provider, mode, default_model) return _create_sync_wrapper(func, provider, mode, default_model)包装后的create在instructor/v2/core/client.py的Instructor/AsyncInstructor中对外暴露。以同步create为例,它的职责链清晰可循:
- 通过
reject_async_validators(response_model)拒绝在同步路径中混入异步 validator; handle_kwargs将构造客户端时传入的默认 kwargs 与本次调用 kwargs 合并(调用侧优先,见 client.py);- 合并客户端级 hooks 与单次调用 hooks(
self.hooks + hooks); - 最终委托给
create_fn,即真正被包装过的重试管道。
# instructor/v2/core/client.py(create 核心逻辑节选) kwargs = self.handle_kwargs(kwargs) if token_budget is not None: kwargs["token_budget"] = token_budget combined_hooks = self.hooks if hooks is not None: combined_hooks = self.hooks + hooks return self.create_fn( response_model=response_model, messages=messages, max_retries=max_retries, context=context, strict=strict, hooks=combined_hooks, **kwargs, )客户端还通过__getattr__把未拦截的属性透传给底层原始 client(chat、messages、create除外),因此client.chat.completions.create(...)这类 OpenAI 原生链式调用依然可以无缝工作(见 client.py)。
Mode:分发器的路由键
Mode枚举定义了每种 provider 与解析策略的组合,是 Dispatcher 的路由键。instructor/v2/core/mode.py中按 provider 分组维护了全套模式:OpenAI 的TOOLS/TOOLS_STRICT/JSON/MD_JSON/JSON_SCHEMA/RESPONSES_TOOLS、Anthropic 的ANTHROPIC_TOOLS/ANTHROPIC_JSON/ANTHROPIC_PARALLEL_TOOLS、Vertex 与 Gemini 的VERTEXAI_TOOLS/GEMINI_TOOLS/GENAI_TOOLS等等,并提供了三个分类辅助方法:
Mode.tool_modes():所有基于工具调用的模式;Mode.json_modes():所有基于 JSON 输出的模式;Mode.parallel_modes():并行工具模式(PARALLEL_TOOLS、ANTHROPIC_PARALLEL_TOOLS、VERTEXAI_PARALLEL_TOOLS)。
仓库还维护了一张DEPRECATED_TO_CORE映射表,将历史遗留模式(如FUNCTIONS、TOOLS_STRICT、ANTHROPIC_TOOLS等)归一化到核心模式,并通过warn_deprecated_mode在会话内只警告一次,避免日志刷屏(见 mode.py)。从源码结构看,v2 的方向是"provider 由客户端决定,mode 只管解析策略",因此 Provider 枚举与 Mode 枚举在注册表中是解耦的。
最小调用路径:同步与异步
原文给出了两条最小代码路径,这也是理解全库的最短切入点。同步路径:
import openai import instructor from pydantic import BaseModel class User(BaseModel): name: str age: int client = instructor.from_provider("openai/gpt-5-nano") model = client.create( model="gpt-4o-mini", messages=[{"role": "user", "content": "{'name': 'Ada', 'age': 37}"}], response_model=User, # 触发 schema/tool 接线与解析 max_retries=3, # tenacity 支撑的校验重试 strict=True, # 若 provider 支持则启用严格 JSON 解析 ) # 需要时访问原始 provider 响应 raw = model._raw_response异步路径仅需换用async_client=True与await:
import asyncio import openai import instructor from pydantic import BaseModel class User(BaseModel): name: str age: int async def main(): aclient = instructor.from_provider("openai/gpt-5-nano", async_client=True) model = await aclient.create( model="gpt-4o-mini", messages=[{"role": "user", "content": "{\"name\": \"Ada\", \"age\": 37}"}], response_model=User, max_retries=3, strict=True, ) print(model) asyncio.run(main())from_provider工厂位于 instructor/v2/auto_client.py,通过"openai/gpt-5-nano"这样的provider/model字符串自动推导 Provider 与默认模型;其签名支持async_client布尔开关,据此返回Instructor或AsyncInstructor。Response._normalize_messages还允许直接传入字符串(自动包装为单条 user 消息),或在messages与input之间二选一,二者同时传入会抛TypeError(见 client.py)。
关于strict参数,结合 openai/handlers.py 的实现可以看到:prepare_request会从 kwargs 中弹出strict,为 true 时在生成的 OpenAI schema 中写入"strict": True,随后将 schema 作为唯一 function tool 注入tools,并把tool_choice固定为该 function。代码中还特意对generate_openai_schema的 lru_cache 返回值做了浅拷贝,避免把strict字段永久污染到后续所有针对同一模型的调用——这是一个值得注意的缓存安全细节。
流式、部分对象与并行工具
流式 Iterable:create_iterable
create_iterable(response_model=Model)内部会强制stream=True,并把response_model包装为Iterable[response_model],最终由IterableBase.from_streaming_response(_async)负责把流式 chunk 组装成条目的生成器(同步)或异步生成器(异步)。对应实现见 client.py:
kwargs["stream"] = True response_model = Iterable[response_model] # type: ignore return self.create_fn( messages=messages, response_model=response_model, max_retries=max_retries, context=context, strict=strict, hooks=combined_hooks, **kwargs, )使用方式:
for item in client.create_iterable(messages=..., response_model=MyModel): print(item)异步侧AsyncInstructor.create_iterable有额外一层判断:当create收到的response_model本身就是Iterable[T]类型且当前 mode 不属于Mode.parallel_modes()时,会自动转投到create_iterable(见 client.py),这让两种调用风格可以互相兼容。
部分对象:create_partial
create_partial(response_model=Model)用于在流式过程中拿到"逐字段填充"的部分模型:内部把模型包装为Partial[Model]并强制stream=True,每收到一个 chunk 就返回一次更新后的部分对象。实现同样位于 client.py,Partial类型定义在 instructor/v2/dsl/partial.py。
for partial in client.create_partial(messages=..., response_model=MyModel): # partial 中会包含已到达的字段 pass并行工具:Mode.PARALLEL_TOOLS
当需要一次请求中触发多个工具调用时,使用Mode.PARALLEL_TOOLS并将response_model声明为模型列表(如[PersonInfo, EventInfo])。注意:并行工具模式不支持流式。
from instructor.mode import Mode result = client.create( model="gpt-4o", messages=[{"role": "user", "content": "Extract person and event info."}], response_model=[PersonInfo, EventInfo], mode=Mode.PARALLEL_TOOLS, )从源码看,并行工具路径有两个关键点:请求侧由instructor/v2/dsl/parallel.py的handle_parallel_model把多个模型展开为多个 tools 并设置tool_choice="auto"(见 handlers.py);解析侧parse_response遍历response.choices[0].message.tool_calls,按工具名查找type_registry,用model_validate_json逐个解析并 yield(见 handlers.py)。之所以流式不支持,从该代码路径可以推断:并行工具需要拿到完整的tool_calls列表才能按工具名分发,流式 chunk 无法保证工具调用的完整性。
Hooks 与重试语义
Hook 事件一览
Hook 是观测与插桩整条调用链路的统一入口。HookName枚举定义在 instructor/v2/core/hooks.py,共六个事件:
| HookName | 触发时机 |
|---|---|
completion:kwargs | 每次 provider 调用之前 |
completion:response | 每次 provider 调用之后 |
completion:error | 非校验类的完成错误 |
completion:last_attempt | 重试序列即将停止时 |
completion:usage | 有可用 usage 时,携带累计用量快照 |
parse:error | 校验/JSON 解析错误 |
注册方式如下(client.on内部委托给Hooks.on,也支持传字符串形式的 hook 名):
from instructor.core.hooks import HookName client.on(HookName.COMPLETION_KWARGS, lambda **kw: print("KWARGS", kw)) client.on(HookName.PARSE_ERROR, lambda e: print("PARSE", e))Hooks类用defaultdict(list)按事件名维护处理器列表,每个事件可挂多个 handler;客户端级 hooks 与单次调用的hooks参数会通过+运算符合并(见 client.py)。completion:error/completion:last_attempt的 handler 协议签名固定为(error, *, attempt_number, max_attempts, is_last_attempt),方便做精确的重试状态观测(见 hooks.py)。
重试管道:tenacity 与 reask
重试逻辑的核心在instructor/v2/core/retry.py的retry_sync_v2(异步对应retry_async_v2)。关键机制:
- 停止条件:
max_retries为整数时,构造stop_after_attempt(max(max_retries, 0) + 1),若 kwargs 中带了数值型timeout,还会追加stop_after_delay(timeout)与之取并集;max_retries也可以直接传一个 tenacityRetrying实例做完全自定义(见 retry.py)。 - 可重试异常:仅
ValidationError、json.JSONDecodeError、AsyncValidationError、ResponseParsingError四类会被重试(_RETRYABLE_PARSE_ERRORS),其余异常直接向上抛出,并在completion:errorhook 中带出 attempt 元数据。 - 每次尝试:先 emit
completion:kwargs→ 调用 provider → emitcompletion:response→ 累计 usage(update_total_usage,与 openaiCompletionUsage结构对齐,Anthropic 模式则使用其自有 usage 初始化)→ 用注册表中的response_parser解析。 - 解析失败走 reask:把
FailedAttempt(attempt_number, exception, completion)记入failed_attempts,emitparse:error,然后调用handlers.reask_handler(kwargs, response, exception)生成新的 kwargs/messages(把错误反馈追加进对话),再进入下一轮尝试。 - 重试预算:
token_budget参数用于限制校验重试的累计 token 消耗;超出预算时(_budget_error)会提前终止并抛出TokenBudgetError,而不是继续盲目重试。
最终若所有尝试失败,抛出InstructorRetryException,其中包含failed_attempts(历次失败详情)、last_completion(最后一次原始响应)、total_usage(累计用量)、messages(用于复现的对话)以及create_kwargs(完整的调用参数),便于复现与排查(见 retry.py)。
以 OpenAI TOOLS 模式为例,reask 与解析由 instructor/v2/providers/openai/handlers.py 中注册到Mode.TOOLS的 handler 类实现:prepare_request注入 tools/tool_choice,handle_reask委托给reask_tools(kwargs, response, exception),parse_response则区分流式解析、finish_reason == "length"的不完整输出(抛IncompleteOutputException)、并行工具生成器与标准工具调用解析四条子路径。
多模态消息转换发生在哪里
对于需要多模态能力的模式,消息会经由processing.multimodal.convert_messages(v2 中为 instructor/v2/core/multimodal.py)转换。特定 handler/mode 可以启用 Image/Audio/PDF 自动检测,把字符串路径、URL、data URI 或字节流统一转成 provider 可消费的 payload:
Image.autodetect会依次识别 base64 字符串、http(s)://、gs://、本地文件路径(见 multimodal.py);- MIME 类型白名单覆盖了常见的图片(jpeg/png/gif/webp)、音频(wav/mp3/m4a/flac/opus 等)与 PDF(
application/pdf); - 远程内容通过
instructor/v2/core/remote.py的fetch_remote_content拉取,并附带MAX_IMAGE_BYTES/MAX_AUDIO_BYTES/MAX_PDF_BYTES大小上限约束。
消息转换器(message_converter)作为ModeHandlers的可选成员注册,因此在 registry 查询层面与请求/解析 handler 保持同一套生命周期。
错误处理一览
综合原文与源码,错误处理的完整图景如下:
- 校验或 JSON 解码错误(四类可重试异常)触发 reask 路径;
- Reask handler(
handle_reask/handle_reask_kwargs)在 kwargs 中追加/调整带错误反馈的 message,让下一次尝试能自我纠正; IncompleteOutputException在响应因finish_reason == "length"被截断时抛出(handlers.py),同样属于不可盲目重试的终止性信号,在 retry 管道中被原样透传;RegistryError在(Provider, Mode)组合未注册时于入口即失败,并给出可用模式列表;TokenBudgetError在token_budget被耗尽时提前终止重试;- 所有重试用尽则抛
InstructorRetryException,携带failed_attempts、最后一次 completion、usage 汇总与可复现的create_kwargs。
扩展性说明:新增 Provider 的正确姿势
原文最后给出了清晰的扩展约束,源码印证了这套约定的落地方式:
- 新 provider 需要提供响应解析与 reask 处理的 utils,并通过
mode_registry.register(...)注册到(Provider, Mode)键上(见 registry.py);也可用register_mode_handler装饰器按 mode 声明 handler 类(如 openai/handlers.py 中各 handler 类的用法)。 - 大部分 JSON/tool 模式是共享的:例如
DEPRECATED_TO_CORE把大量历史 provider 模式归一化到Mode.TOOLS/Mode.JSON/Mode.JSON_SCHEMA/Mode.MD_JSON/Mode.PARALLEL_TOOLS,新 provider 应优先复用这些核心模式的 handler。 - provider 特有逻辑留在 provider utils 中:中央 Dispatcher(registry + retry)只负责路由与编排,不内嵌任何 provider 细节。Provider 规格(
HANDLER_SPECS/PROVIDER_SPECS)集中在 instructor/v2/core/provider_specs.py,新 provider 主要补齐这一层声明与对应 handlers 即可。
小结
Instructor 的架构可以概括为一条"职责单一、层层委托"的管道:patch()负责入口包装,tenacity 重试层负责"调用 → 解析 → 失败则 reask"的循环,ModeRegistry负责按(Provider, Mode)分发到具体的 request/reask/response handler,最后统一把_raw_response挂到解析出的 Pydantic 模型上。理解这条链路后,无论是排查一次"重试了却仍解析失败"的问题、为内部调用接入completion:kwargs/parse:error观测,还是为一个新 provider 注册 handler,都能精准定位到对应的代码层次:入口看 instructor/v2/core/patch.py 与 instructor/v2/core/client.py,重试语义看 instructor/v2/core/retry.py,路由分发看 instructor/v2/core/registry.py,模式全集看 instructor/v2/core/mode.py。
【免费下载链接】instructorstructured outputs for llms项目地址: https://gitcode.com/GitHub_Trending/in/instructor
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考