Instructor实战:让LLM老老实实返回JSON
你让GPT返回个用户信息JSON,它给你包了三层markdown:
json
{
"name": "张三"
}
你要个评分数字,它给你返回"高"。你要个列表,它少了俩字段。
这不是模型不行,是你没用对工具。Instructor就是专门治这个病的------3行代码,保证LLM输出跟你的Pydantic模型一模一样。
为什么需要Instructor
Raw JSON mode有三个硬伤:
1. 格式不可控
python
# 你期望的
{"score": 0.85}
# GPT给你的
```json
{"score": "很高", "confidence": "非常确定"}
\```
2. 字段缺失/多余 模型经常自己发挥,加字段或者漏字段,下游代码直接炸。
3. 没有重试机制 验证失败了就失败了,你得手写重试逻辑。
Instructor的解法很直接:用Pydantic定义schema,自动验证,失败了自动重试,直到拿到合法数据或者超过重试次数。
5分钟上手
python
pip install instructor openai pydantic
最简单的例子------提取人物信息:
python
import instructor
from openai import OpenAI
from pydantic import BaseModel
class Person(BaseModel):
name: str
age: int
occupation: str
client = instructor.from_openai(OpenAI())
person = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "user", "content": "Extract: John is 30, works as an engineer"}
],
response_model=Person,
max_retries=3
)
print(person.name) # John
print(person.age) # 30
工作流程:
- 定义Pydantic模型
from_openai()包装客户端- 传
response_model参数 - 拿到的直接是验证过的Python对象
验证失败?自动重试。字段类型错了?自动重试。直到成功或者达到max_retries。
复杂场景:嵌套结构+自定义验证
真实业务不会这么简单。来个工单分类系统:
python
from pydantic import BaseModel, Field, field_validator
from typing import List, Optional
from enum import Enum
class Priority(str, Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
class Tag(BaseModel):
name: str = Field(..., min_length=2, max_length=20)
confidence: float = Field(..., ge=0.0, le=1.0)
class Ticket(BaseModel):
title: str = Field(..., min_length=10)
priority: Priority
tags: List[Tag] = Field(..., min_items=1, max_items=5)
estimated_hours: Optional[float] = Field(None, gt=0, le=100)
@field_validator('estimated_hours')
@classmethod
def validate_hours(cls, v):
if v is not None and v % 0.5 != 0:
raise ValueError('工时必须是0.5的倍数')
return v
ticket = client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "user",
"content": "分类工单:生产环境Redis崩了,影响所有用户登录"
}],
response_model=Ticket,
max_retries=3
)
print(ticket.priority) # Priority.CRITICAL
print(ticket.tags[0].name) # redis
print(ticket.estimated_hours) # 2.0 (符合0.5倍数规则)
这里用了:
- 枚举类型(Priority)
- 嵌套对象(List[Tag])
- 字段约束(min_length、ge/le范围)
- 自定义验证器(工时必须0.5倍数)
任何一个不符合,Instructor会把验证错误信息塞回prompt,让模型重新生成。
流式输出:边生成边处理
大列表或者复杂对象,等模型全生成完太慢。Instructor支持Partial streaming:
python
from instructor import Partial
class UserList(BaseModel):
users: List[Person]
stream = client.chat.completions.create_partial(
model="gpt-4o",
messages=[{
"role": "user",
"content": "提取:张三30岁工程师,李四25岁设计师,王五35岁产品"
}],
response_model=Partial[UserList],
stream=True
)
for partial_result in stream:
if partial_result.users:
print(f"已提取 {len(partial_result.users)} 个用户")
for user in partial_result.users:
if user.name: # Partial对象字段可能None
print(f" - {user.name}")
输出:
markdown
已提取 1 个用户
- 张三
已提取 2 个用户
- 张三
- 李四
已提取 3 个用户
- 张三
- 李四
- 王五
Partial[T]会把模型里的所有字段标记为Optional,你可以实时处理部分结果,不用等全部生成完。
注意 :streaming模式下不能用@field_validator,因为中间状态可能不满足验证规则。
多provider支持:一套代码跑遍所有模型
Instructor 2026版支持15+家provider,API完全一样:
python
# OpenAI
client = instructor.from_openai(OpenAI())
# Anthropic Claude
from anthropic import Anthropic
client = instructor.from_anthropic(Anthropic())
# Google Gemini
import google.generativeai as genai
client = instructor.from_gemini(genai.GenerativeModel("gemini-2.0-flash-001"))
# 本地模型 (Ollama)
from openai import OpenAI
client = instructor.from_openai(
OpenAI(base_url="http://localhost:11434/v1", api_key="ollama"),
mode=instructor.Mode.JSON
)
切provider只需要改初始化代码,response_model和业务逻辑完全不变。
本地模型建议用Mode.JSON而不是Mode.TOOLS,因为开源模型的function calling能力参差不齐。
三个踩坑记录
1. 重试次数别设太高
python
# ❌ 这样写token消耗爆炸
response = client.create(
response_model=ComplexModel,
max_retries=10 # 验证失败会重试10次,每次都扣token
)
# ✅ 3次够了,验证失败说明prompt有问题
max_retries=3
每次重试都会把错误信息加到prompt里再调一次API。复杂模型验证失败10次,token消耗是正常的3-5倍。
2. 嵌套太深会超context
python
class DeepNested(BaseModel):
level1: List['DeepNested'] # 递归嵌套
# 模型生成超深结构时,验证错误信息会非常长
# 重试时prompt可能超出context window
解决:限制嵌套深度,或者用max_items约束列表长度。
3. streaming不支持同步validators
python
class User(BaseModel):
email: str
@field_validator('email')
@classmethod
def validate_email(cls, v):
# ❌ 在 create_partial(stream=True) 里不会执行
if '@' not in v:
raise ValueError('Invalid email')
return v
streaming模式返回的是Partial对象,验证器会被跳过。需要在最终结果上再验证一次。
Instructor vs PydanticAI:什么时候该换
Instructor适合单次提取任务:
- 从文本提取结构化数据
- 分类、打标签
- 格式转换
PydanticAI适合多轮agent交互:
- 需要调用工具(搜索、数据库查询)
- 多步骤推理
- 需要记录执行历史和可观测性
两者都是Pydantic团队官方产品,Instructor专注schema-first提取,PydanticAI是完整的agent runtime。
简单记:只要JSON就用Instructor,要干活就上PydanticAI。
一张清单
生产环境用Instructor之前,确认这5点:
- max_retries设成2-3,不要超过5
- 嵌套层级 ≤ 3层
- List字段都加了max_items限制
- streaming场景没用field_validator
- 算过token成本(重试会double消耗)