想象一家生意火爆的餐厅:主厨(大模型)才华横溢但随性散漫。你问他“今天进的那批鱼什么情况”,他能眉飞色舞讲上十分钟——里面确实有产地、有斤两、有新鲜度,但全藏在流水账里。问题在于,后厨的库存系统不认流水账,它只认一张印好的标准小票。没有小票,传菜靠喊,记账靠猜,餐厅很快乱成一锅粥。 Instructor 就是这样一套“标准点单小票”系统:先印好模板(Pydantic 模型),规定每一栏填什么;主厨照着模板回答;传菜员(校验器)逐栏核对,填错了就端着盘子回厨房重做(自动重试);最后交到各个工位(Agent 工具)手里的,永远是一张格式统一的单子。本篇文章就带你走进这家“AI 餐厅”,把整套流程看明白:从印小票、传单、质检,到工位间传递和餐厅成本管控。

岗位关联

对 AI 从业者来说,掌握 Instructor 结构化输出,意味着你能把大模型的“自由发挥”变成工程上可靠的数据接口——这是 Agent 从 Demo 走向生产的基本功。

目录
  1. 可视化演示
  2. 结构化输出概述
  3. Pydantic模型定义
  4. 模式匹配机制
  5. 验证与重试
  6. Agent数据流转
  7. 工程实践要点
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示Instructor将LLM文本输出转化为结构化Python对象的全流程:Schema定义、模型调用、JSON解析、验证重试,以及Agent工具间的类型安全数据传递。

本节配有交互动效(进入视口后自动加载,约 3KB)。

结构化输出管线:文本到对象的转化 Schema:Pydantic模型定义输出结构 LLM:模型生成符合Schema的JSON instructor.create(model, response_model=Schema) 六阶段管线确保从自由文本到类型安全Python对象的可靠转化。

结构化输出概述

想象一家后厨:主厨(大模型)最讨厌填表格。你问他“这个客户什么情况”,他能眉飞色舞讲一大段,里面确实有姓名、有年龄、有爱好,但全都藏在流水账里。后厨的记账系统不认流水账,它只认小票上的固定栏目。Agent 世界也一样:Agent 调用工具(查数据库、发邮件、建工单)时,工具接口需要的是 name、age、order_id 这样的类型化参数,而不是一段口语。

过去大家靠手写正则、json.loads 从模型输出里“抠”字段,字段一多、说法一杂就崩,这正是所谓的“非结构化困境”。要让 Agent 真正自动化,就必须把大模型的自由文本“翻译”成严格的数据结构——这就是结构化需求。Instructor 就是为解决这个痛点而生的 Python 库。

它轻量地封装了大模型客户端(OpenAI、Anthropic 等),核心用法只有一招:调用时传入 response_model——一个 Pydantic 模型。Instructor 会自动把模型输出校验、纠错、解析成 Python 对象,开发者拿到的永远是对象而不是字符串。它的核心价值,是把“让模型输出合规数据”从手工作坊变成标准化流水线。

回到餐厅:主厨还是那个洒脱的主厨,但有了小票系统,后厨再也不用靠喊。你向 Agent 要数据,拿到的一定是规规整整的对象。

fInstructor:raw text↦O,O⊨Schemaf_{\text{Instructor}}: \text{raw text} \mapsto \mathcal{O}, \quad \mathcal{O} \models \text{Schema}
通俗说:Instructor 就是一个“自由文本 → 合规对象”的转换器,转换结果必须符合 Pydantic 定义的模板。
  1. 输入侧大模型输出的自由文本,好比主厨的随口描述
    raw text\text{raw text}
  2. 转换函数Instructor 封装了“生成 + 校验 + 解析”整条流水线
    fInstructor(⋅)f_{\text{Instructor}}(\cdot)
  3. 模式约束结果对象每一个属性都满足 Pydantic 定义
    O⊨Schema\mathcal{O} \models \text{Schema}

Pydantic模型定义

小票模板是整个系统的灵魂。印小票的模板必须严谨:哪栏填姓名、哪栏填数量、哪栏允许空白——全部定死,主厨才不会自由发挥。Instructor 的“小票模板”就是 Pydantic 模型:一个继承自 BaseModel 的类声明了输出结构的全部规则。

用法很直观:写字段名和类型即可。每个字段的 Field(description=...) 还会作为 schema 描述喂给大模型,相当于在模板旁边印一行小字“此栏填写客户姓名”,引导模型理解语义。字段的类型验证(str、int、list[str] 等)由 Pydantic 严格把关:类型不匹配立刻报 ValidationError,为后续重试提供依据。

Pydantic 还支持默认值和嵌套模型。有默认值的字段,模型偶尔漏填也能兜住,不用整单作废;嵌套模型用来表达“地址包含省、市、街道”这类复合结构,让输出对象具备真正的层次感。这些特性共同决定了“哪些字段必须填、填错会怎样”。

模板越严谨,返工越少。这也是为什么优秀的 Agent 工程总是把“数据结构设计”放在写提示词之前——小票印得清楚,主厨才不容易填错。

v⊨Schema  ⟺  ⋀i=1k(type(vi)=Ti)v \models \text{Schema} \iff \bigwedge_{i=1}^{k} \left( \text{type}(v_i) = T_i \right)
通俗说:一条数据只有所有字段类型都匹配,才算通过校验,就像小票每一栏都填对才收单。
  1. 数据 v待校验的一行数据,相当于填好的小票
    v={f1:v1,f2:v2,⋯ ,fk:vk}v = \{ f_1: v_1, f_2: v_2, \cdots, f_k: v_k \}
  2. 全字段合取任一字段不合法,整个校验就失败
    ⋀i=1k\bigwedge_{i=1}^{k}
  3. 类型匹配每个字段类型必须和模型声明一致
    type(vi)=Ti\text{type}(v_i) = T_i

模式匹配机制

同一张小票,传单方式可以不同:老练的服务员可以口头报给后厨(函数调用),求稳时也可以塞一张书面单(JSON 模式)。Instructor 会根据“后厨设备”——也就是大模型的能力——自动选择传单方式。

以 OpenAI 系模型为例,它们原生支持函数调用(tool calling)。Instructor 会把 Pydantic 模型编译成工具的 parameters schema,模型生成时以“工具参数”的形式返回对象,输出天然就是结构化 JSON,准确率最高。这就是函数调用机制的威力:模型不是在“写散文”,而是在“填参数”。

如果底层模型不支持工具调用(部分开源模型、较旧版本),Instructor 就退回 JSON 模式:把 JSON Schema 写进提示词,要求模型输出符合该 Schema 的 JSON 文本,再交给 json.loads + Pydantic 解析。两条路走哪条基本不用开发者操心,Instructor 自动判断、自动兜底。

就像一个熟练的服务员知道什么时候该口头传单、什么时候该写书面单,Instructor 把这条“判断”也替你做了。

Mengine={tool_calling,if tools supportedjson_schema,otherwise\text{M}_{\text{engine}} = \begin{cases} \text{tool\_calling}, & \text{if tools supported} \\ \text{json\_schema}, & \text{otherwise} \end{cases}
通俗说:Instructor 自动判断该用“电话点单”还是“书面点单”,总是选更稳的方式拿到结构化结果。
  1. 引擎决策根据模型是否支持工具调用,决定走哪条路
    Mengine\text{M}_{\text{engine}}
  2. 工具调用优先模型支持函数调用时,按工具参数约束输出
    tool_calling\text{tool\_calling}
  3. JSON 兜底不支持工具时,用 JSON Schema 约束输出格式
    json_schema\text{json\_schema}

验证与重试

小票填好不等于合格。传菜员发现“年龄”栏写了个“十八岁”,不会直接认了,而是把单子端回厨房:“这栏要填数字!”Instructor 的重试机制就是这个流程的自动化。

一次生成的完整链路是:模型输出 → Pydantic 校验 → 失败则把 ValidationError 的详细错误信息(哪个字段错、期望什么类型)拼进对话上下文 → 再让模型生成一次,如此循环,直到通过或达到 max_retries 上限。因为错误信息足够具体,模型第二次通常就能改对——这不是盲试,而是“带着点评改作业”。

用概率来看这件事:假设模型单次生成完全合法的概率是 p,那么重试 n 次后,最终成功率会显著提升。正是这个简单的概率逻辑,让“碰运气”变成了“稳成功”。

不过要提醒:每退一次单,都要重新付一次“烹饪成本”(token)。重试不是无限免单,如何设置上限,是工程实践要解决的问题。

Psuccess=1−(1−p)n+1P_{\text{success}} = 1 - (1-p)^{n+1}
通俗说:多试几次,总有一次能填对——重试把“单次碰运气”升级成“多次稳成功”。
  1. 单次成功率模型每生成一次,完全符合模式的概率为 p
    p∈[0,1]p \in [0, 1]
  2. 总尝试次数首次尝试加上 n 次重试,一共 n+1 次
    n+1n + 1
  3. 最终成功率全部尝试都失败才算失败,用 1 减去失败概率
    1−(1−p)n+11 - (1-p)^{n+1}

Agent数据流转

真正的餐厅后厨有很多工位:切配、掌勺、装盘……每个工位之间传递的,都是同一格式的小票。Agent 架构也一样:工具 A 的结果要喂给工具 B,中间传的如果是自由文本,下游根本不知道该怎么读。

让每个 Agent 步骤的输出都经过 Instructor 结构化,工具之间传递的就是类型化对象:切配工位(解析工具)产出结构体,掌勺工位(生成工具)直接读字段。多步对话时,上一步的结构化结果还能存进状态,作为下一步的上下文——这就是“字段补全 + 类型校验”在流程编排中的真正价值。

状态管理也因结构化而简化:对象比字符串更容易存储、比对、回滚。Agent 的每一步都带着一张标准小票进入下一工位,任何一步出了问题,都能精确定位是哪个字段、哪个环节,而不是在一大段文本里大海捞针。

后厨的协作本质就是“标准信息在工位间流动”。Agent 有了结构化输出,工具之间的协作才真正靠得住。

statet+1=Agent(statet,  fInstructor(outputt))\text{state}_{t+1} = \text{Agent}\left(\text{state}_t,\; f_{\text{Instructor}}(\text{output}_t)\right)
通俗说:Agent 的每一步输出都先被结构化,再作为下一步的输入,链条处处是小票。
  1. 原始输出Agent 上一步拿到的自然语言结果
    outputt\text{output}_t
  2. 结构化封装经过 Instructor 转成类型化对象再传递
    fInstructor(outputt)f_{\text{Instructor}}(\text{output}_t)
  3. 状态推进Agent 状态带着新对象进入下一步
    statet+1\text{state}_{t+1}

工程实践要点

小票系统上线前,餐厅还要定几条规矩:面没了可以写“售罄”,但不能让小票作废;传菜员复查不过的单子,最多退回两次。这些在 AI 工程里就是“输出兜底”和“重试上限”。

具体做法上:给非关键字段设置默认值,模型漏填也能拿到可用对象;max_retries 一般设 2~3 次,既保成功率又控延迟;字段描述写清楚,减少模型因歧义导致的无效重试。测试策略上,把历史上校验失败的输入收集成回归集,每次改提示词都跑一遍,防止“修好一个坑、踩塌一座桥”。

性能优化与成本控制是一体两面:每一次调用、每一次重试,都是真金白银的 token 消耗。用轻量模型做简单字段抽取、精简 description 减少输入长度,都是立竿见影的省钱手段。

就像后厨 SOP 决定餐厅能否稳定营业,这些工程细节决定了 Agent 能不能从 Demo 走到生产环境。

Cost=ctoken×∑i=1n+1ti\text{Cost} = c_{\text{token}} \times \sum_{i=1}^{n+1} t_i
通俗说:每次尝试都要花钱,所以要把重试次数和 token 消耗一起算进成本账。
  1. 单次消耗第 i 次调用消耗的 token 数
    tit_i
  2. 累计求和所有尝试的消耗加总
    ∑i=1n+1ti\sum_{i=1}^{n+1} t_i
  3. 单价换算乘以 token 单价得到总成本
    Cost=ctoken×∑i=1n+1ti\text{Cost} = c_{\text{token}} \times \sum_{i=1}^{n+1} t_i

AI 应用场景

智能客服工单自动分类

阶段:Agent 工具调用

解决的问题:传统客服机器人用关键词和正则匹配用户意图,用户换个说法就漏单,工单里的姓名、订单号、问题类型经常空缺,人工还得二次整理。接入 Instructor 后,投诉文本被直接抽成带类型校验的工单对象,路由系统按字段自动分发和分级,漏单和人工作业大幅减少。

具体案例:用户发来“我上周买的键盘坏了,订单号 8848,能快点处理吗?”客服 Agent 调用结构化抽取工具,一步得到 RepairTicket(order_id="8848", issue="键盘损坏", urgency="high"),随后自动创建高优工单并通知售后。

网页商品信息抽取入库

阶段:数据处理管道(ETL)

解决的问题:电商比价 Agent 每天要抓竞品页面,页面结构改版频繁,正则和 XPath 选择器说崩就崩,维护成本极高。把页面文本和 ProductInfo 模型交给 Instructor,抽出 name、price、stock 等字段,直接校验后入库,页面怎么改都不怕。

具体案例:价格监控 Agent 抓取某电商商品页 HTML 转成的文本,Instructor 依据 ProductInfo(name, price, stock, url) 模型完成抽取,产出对象直接写入数据库,字段缺失时自动重试补全,相比手工解析上线后零维护。

多 Agent 协作内容生产

阶段:Agent 编排与状态管理

解决的问题:调研 Agent 输出洋洋洒洒的自由文本,写稿 Agent 很难直接消费,中间的字符串解析又慢又容易错。约定一份研究对象(如 ResearchResult(claims, sources, confidence)),上游结构化输出,下游直接读取,整条链路清晰可回滚。

具体案例:券商研报辅助系统里,研究员 Agent 先读财报,输出带置信度的结论对象;写稿 Agent 遍历这些对象拼装章节;审计 Agent 再按 sources 字段回溯原文。三条 Agent 协作全程无字符串硬解析。

Python 演示

python37 行
from pydantic import BaseModel, Field, ValidationError  class UserProfile(BaseModel):    name: str = Field(description="user name")    age: int = Field(description="user age, non-negative int")    city: str = Field(default="Unknown", description="user city")    tags: list[str] = Field(default_factory=list, description="interest tags")  def mock_llm(text: str, attempt: int) -> dict:    # 模拟大模型自由输出, 前两次返回类型不合法的数据    if attempt == 0:        return {"name": "Alice", "age": "twenty eight"}   # age 是字符串, 类型错    if attempt == 1:        return {"name": 12345, "age": 28, "city": "HZ"}   # name 是整数, 类型错    return {"name": "Alice", "age": 28, "city": "HZ", "tags": ["AI", "dance"]}  def extract_profile(text: str, max_retries: int = 3) -> UserProfile:    last_error = None    for attempt in range(max_retries + 1):        data = mock_llm(text, attempt)        try:            profile = UserProfile(**data)            print("attempt {} ok: {}".format(attempt + 1, profile))            return profile        except ValidationError as e:            last_error = e            print("attempt {} failed: {}".format(attempt + 1, e))    raise last_error  if __name__ == "__main__":    raw_text = "Alice is 28 years old and lives in Hangzhou."    profile = extract_profile(raw_text)    print("name: {}, age: {}, city: {}".format(profile.name, profile.age, profile.city))
目的:用 Pydantic 定义输出结构,并模拟“大模型生成 → 校验失败 → 携带错误重试 → 最终成功”的完整闭环。
  1. class UserProfile(BaseModel):声明一个 Pydantic 模型,等于先画好一张“点单小票”
  2. age: int = Field(description=...)字段类型约束 + 给模型看的填写说明
  3. city: str = Field(default="Unknown", ...)默认值兜底,漏填也不作废
  4. def mock_llm(text, attempt)模拟大模型自由发挥,前两次故意返回类型错误的数据
  5. UserProfile(**data)把数据转成对象,像质检员按小票逐栏核对
  6. except ValidationError as e:捕获不合格结果,记录错误详情
  7. for attempt in range(max_retries + 1)重试循环,最多首次 + n 次重试

常见误区

误区我第一次用 Instructor 时,以为它像一个独立 API 那样是一个大模型,只要把文本传进去,它自己就能“想”出结构化数据。

正解Instructor 不是模型,而是“封装层 + 质检员”。底层还是要调用 GPT 等大模型,它负责把 Pydantic 模型翻译成函数 Schema 或 JSON Schema,把模型输出解析并校验,失败时把错误回传给模型重试。它的价值在“编排和校验”,不在“生成”。

为何常见:调用方式和传参 API 很像(传入 messages、拿回对象),很容易让人误以为它是一个独立服务。

误区反正有字段类型约束,大模型怎么也能填对,提示词随便写写就行。

正解Schema 约束的是“结构”,不一定约束“语义”。比如 description 写得太含糊,模型可能把“张三”填到 age 字段——即便类型校验最终拦住,也白白浪费一次重试。清晰的字段描述等于告诉主厨“红烧”是什么意思,小票格式再规范,语义不清照样返工。

为何常见:新手容易高估硬约束的作用,低估语义歧义带来的隐性成本(额外重试、更高延迟)。

误区反正支持自动重试,把 `max_retries` 调到 10,总能得到完美结果。

正解重试不是免费的:每次重试都重新消耗 token 并增加延迟,而且如果模型本身就不理解该字段的语义,重试 10 次和 3 次的成功率几乎一样。实践中 max_retries=2~3 是合理区间,真正要优化的还是提示词和 Field 描述。

为何常见:把概率公式线性外推,忽略了边际收益递减和成本曲线,以为“多试一定更好”。

误区我之前自己用正则从 JSON 字符串里抠字段,觉得 Instructor 也没什么大不了。

正解让模型“输出 JSON”只是第一步,真正的工程问题是:格式合法但字段缺失、类型不对、嵌套错误、多出无关键。Instructor 把“JSON 字符串”提升到“类型化对象”,并自动完成校验和重试,这是手写正则加 json.loads 很难覆盖完整的。

为何常见:从“能跑”到“可靠”之间的距离,只有踩过字段缺失的坑才能真正理解。

面试题

Q1. Instructor 库最核心的作用是什么?

单选题 · easy

  • A.训练出一个全新的大模型
  • B.把自由文本转为结构对象← 正确答案
  • C.替代 Pydantic 完成校验
  • D.为 Agent 提供长期记忆

答案:B。Instructor 不是模型,也不是存储或校验库的替代品。它把大模型自由生成的文本,通过 Pydantic 模式约束并转换为 Python 对象;校验与重试是它实现目标的自动化手段。就像传菜员负责把主厨的话变成标准小票,而不是自己上灶台。

Q2. Instructor 定义输出结构时依赖哪个库?

单选题 · easy

  • A.基于 NumPy 的科学计算库
  • B.基于 Pandas 的数据处理库
  • C.基于 Pydantic 的数据校验框架← 正确答案
  • D.基于 Requests 的协议请求库

答案:C。Instructor 把 Pydantic 模型作为输出结构模板,利用它的类型系统、默认值和 ValidationError 完成声明与校验。NumPy、Pandas、Requests 分别负责数值计算、数据分析与网络请求,与“定义输出结构”无关。

Q3. 一次校验失败后,Instructor 重试时会向大模型补充什么?

单选题 · mid

  • A.随机生成的噪声填充数据
  • B.上轮输出和校验错误详情← 正确答案
  • C.用户一整段的完整对话
  • D.模型训练集的原始样本

答案:B。重试不是清空重来,而是把上一轮输出和 ValidationError 的具体信息(哪个字段、期望什么类型)追加进对话,让模型“按点评改”。传菜员是把菜端回厨房说清楚哪里不对,而不是把顾客随便打发走。

Q4. 关于函数调用与 JSON 模式,下列说法正确的是?

单选题 · mid

  • A.函数调用适合不支持工具的模型
  • B.JSON 模式能保证输出一定合法
  • C.Instructor 按模型能力自动选择← 正确答案
  • D.两种模式可以完全随意互换

答案:C。Instructor 会先探测模型是否支持函数调用,支持就走工具参数约束,不支持就退回 JSON 模式。A 说反了;B 过于绝对——JSON 模式只能约束格式,不能保证语义合法;D 也不对,两种模式的稳定性有差异,不能无条件互换。

Q5. 字段 age 声明为 int,以下哪种输出会触发校验重试?

单选题 · mid

  • A.传入 age=28 的整数
  • B.传入 age=0 的整数
  • C.传入 age=“二十八”字符串← 正确答案
  • D.传入 age=100 的整数

答案:C。“二十八”是字符串,类型与 int 不匹配,Pydantic 校验直接失败,Instructor 会携带错误信息重试。0、28、100 都是合法整数,能够通过校验。

Q6. 某模型要求 name 为必填 str 字段,LLM 输出中缺少 name,Instructor 会?

单选题 · hard

  • A.自动填入 None 后正常返回
  • B.忽略缺失字段直接返回
  • C.携带错误信息重试生成← 正确答案
  • D.抛出异常终止整个流程

答案:C。必填字段缺失会触发 ValidationError,Instructor 默认行为是带着“name 字段缺失”的错误继续重试,直到模型补全或达到重试上限。它不会替模型随意填值,也不会因为一次失败就终止。

Q7. 嵌套模型外层通过、内层校验失败时,Instructor 的行为是?

单选题 · extreme

  • A.外层成功就直接整体返回
  • B.只回传内层错误并整体重试← 正确答案
  • C.丢弃内层字段仅返回外层
  • D.把内层错误静默忽略掉

答案:B。Pydantic 的 ValidationError 会记录完整的嵌套错误路径,Instructor 也会整体重试而非部分返回。也就是说,外层再“像样”,只要任何一个内层子模型不合法,整张“小票”都算不合格,退回重做。

Q8. 以下哪些是 Instructor 能带来的实际好处?

多选题 · easy

  • A.减少 JSON 手工解析代码← 正确答案
  • B.自动执行校验失败重试← 正确答案
  • C.保证大模型输出永远完美
  • D.让工具获得类型化参数← 正确答案

答案:ABD。A、B、D 分别是 Instructor 的“少解析、自重试、传对象”三大价值;C 是典型误解——它只能提高成功率,不能保证永远正确,模型能力不足时依然可能反复失败。

Q9. 关于 Pydantic 字段默认值,下列说法正确的有?

多选题 · mid

  • A.有默认值字段可缺省← 正确答案
  • B.必填字段缺失会报错← 正确答案
  • C.默认值降低返工概率← 正确答案
  • D.默认值覆盖生成内容

答案:ABC。有默认值的字段漏填时会用默认值兜底,能避免整单作废,降低重试次数;必填字段缺失则报 ValidationError。D 错:默认值只在字段缺失时生效,不会替换模型已经生成的有效内容。

Q10. 生产中控制结构化输出成本的有效手段包括?

多选题 · hard

  • A.限制模型重试的总次数← 正确答案
  • B.精简字段描述降低成本← 正确答案
  • C.无限增加重试轮次数
  • D.用轻量模型做简单抽取← 正确答案

答案:ABD。重试等于重复烧 token,上限设 2~3 次即可;字段描述越精炼,输入 token 越少,模型歧义也越少;简单场景换轻量模型直接省钱。C 说反了——无限重试既不提升成功率,还让成本失控。