我应该使用结构化输出吗?¶
OpenAI 最近宣布了结构化输出功能,它确保生成的响应匹配提供的任何任意 JSON Schema。在他们的公告文章中,他们承认该功能受到了诸如 instructor
等库的启发。
主要挑战¶
如果你正在构建复杂的 LLM 工作流程,你可能已经考虑过将 OpenAI 的结构化输出作为 instructor
的潜在替代方案。
但在你这样做之前,有三个主要挑战仍然存在
- 有限的验证和重试逻辑:结构化输出确保遵循 schema,但不保证内容有用。你可能会得到格式完美但毫无帮助的响应。
- 流式处理挑战:使用 SDK 从流式响应中解析原始 JSON 对象容易出错且效率低下。
- 不可预测的延迟问题:结构化输出存在随机延迟峰值,可能导致响应时间增加近 20 倍。
此外,采用结构化输出会将你锁定在 OpenAI 的生态系统中,限制你尝试可能更适合特定用例的不同模型或提供商的能力。
这种供应商锁定增加了对提供商中断的脆弱性,可能导致应用程序停机和违反 SLA,从而损害用户信任并影响您的商业声誉。
在本文中,我们将展示 instructor
如何通过在验证失败时自动重新提问、对已验证的流式数据提供自动支持等功能来解决其中许多挑战。
有限的验证和重试逻辑¶
验证对于构建可靠有效的应用程序至关重要。我们希望利用 Pydantic
验证器实时捕获错误,以便我们的 LLM 能够即时更正其响应。
下面我们看一个简单的验证器示例,它确保用户名始终为大写。
import openai
from pydantic import BaseModel, field_validator
class User(BaseModel):
name: str
age: int
@field_validator("name")
def ensure_uppercase(cls, v: str) -> str:
if not v.isupper():
raise ValueError("All letters must be uppercase. Got: " + v)
return v
client = openai.OpenAI()
try:
resp = client.beta.chat.completions.parse(
response_format=User,
messages=[
{
"role": "user",
"content": "Extract the following user: Jason is 25 years old.",
},
],
model="gpt-4o-mini",
)
except Exception as e:
print(e)
"""
1 validation error for User
name
Value error, All letters must be uppercase. Got: Jason [type=value_error, input_value='Jason', input_type=str]
For further information visit https://errors.pydantic.dev/2.9/v/value_error
"""
我们可以看到,当验证失败时,我们丢失了原始的补全结果。这使得开发者无法实现重试逻辑,从而让 LLM 能够提供有针对性的更正并重新生成响应。
没有强大的验证,应用程序可能会产生不一致的输出,并丢失宝贵的错误纠正上下文。这会导致用户体验下降,并错过在 LLM 响应中进行有针对性改进的机会。
流式处理挑战¶
使用结构化输出进行流式处理很复杂。它需要手动解析,缺乏部分验证,并且需要一个上下文管理器才能使用。使用 beta.chat.completions.stream
方法进行有效实现需要付出巨大的努力。
下面我们看一个示例。
import openai
from pydantic import BaseModel
class User(BaseModel):
name: str
age: int
client = openai.OpenAI()
with client.beta.chat.completions.stream(
response_format=User,
messages=[
{
"role": "user",
"content": "Extract the following user: Jason is 25 years old.",
},
],
model="gpt-4o-mini",
) as stream:
for event in stream:
if event.type == "content.delta":
print(event.snapshot, flush=True, end="\n")
#>
#> {"
#> {"name
#> {"name":"
#> {"name":"Jason
#> {"name":"Jason","
#> {"name":"Jason","age
#> {"name":"Jason","age":
#> {"name":"Jason","age":25
#> {"name":"Jason","age":25}
# >
#> {"
#> {"name
#> {"name":"
#> {"name":"Jason
#> {"name":"Jason","
#> {"name":"Jason","age
#> {"name":"Jason","age":
#> {"name":"Jason","age":25
#> {"name":"Jason","age":25}
不可预测的延迟峰值¶
为了对这两种模式进行基准测试,我们向 OpenAI 发送了 200 个相同的请求,并记录了每个请求完成所需的时间。结果汇总在下表中
模式 | 平均值 | 最小值 | 最大值 | 标准差 | 方差 |
---|---|---|---|---|---|
Tool Calling | 6.84 | 6.21 | 12.84 | 0.69 | 0.47 |
结构化输出 | 28.20 | 14.91 | 136.90 | 9.27 | 86.01 |
结构化输出存在不可预测的延迟峰值,而 Tool Calling 保持一致的性能。这可能导致用户偶尔经历响应时间的显著延迟,从而可能影响整体用户满意度和留存率。
为什么使用 instructor
¶
instructor
与结构化输出完全兼容,并为开发者提供了三个主要优势。
- 自动验证和重试:在 Pydantic 验证失败时重新生成 LLM 响应,确保数据完整性。
- 实时流式验证:针对 Pydantic 模型增量验证部分 JSON,从而可以立即使用已验证的属性。
- 提供商无关的 API:只需一行代码即可在 LLM 提供商和模型之间切换。
下面我们来看一个实际例子
自动验证和重试¶
使用 instructor
,只需一个简单的 Pydantic Schema 和一个验证器,即可将提取的名称作为大写值获取。
import instructor
import openai
from pydantic import BaseModel, field_validator
class User(BaseModel):
name: str
age: int
@field_validator("name")
def ensure_uppercase(cls, v: str) -> str:
if not v.isupper():
raise ValueError("All letters must be uppercase. Got: " + v)
return v
client = instructor.from_openai(openai.OpenAI(), mode=instructor.Mode.TOOLS_STRICT)
resp = client.chat.completions.create(
response_model=User,
messages=[
{
"role": "user",
"content": "Extract the following user: Jason is 25 years old.",
}
],
model="gpt-4o-mini",
)
print(resp)
#> name='JASON' age=25
这种内置的重试逻辑允许对生成的响应进行有针对性的更正,确保输出不仅与你的 schema 一致,而且也适合你的用例。这对于构建可靠的 LLM 系统非常有价值。
实时流式验证¶
一个常见的用例是定义一个 schema 并提取它的多个实例。使用 instructor
,通过使用 我们的 create_iterable
方法,可以相对容易地实现这一点。
client = instructor.from_openai(openai.OpenAI(), mode=instructor.Mode.TOOLS_STRICT)
class User(BaseModel):
name: str
age: int
users = client.chat.completions.create_iterable(
model="gpt-4o-mini",
response_model=User,
messages=[
{
"role": "system",
"content": "You are a perfect entity extraction system",
},
{
"role": "user",
"content": (f"Extract `Jason is 10 and John is 10`"),
},
],
)
for user in users:
print(user)
#> name='Jason' age=10
#> name='John' age=10
其他时候,我们可能还希望将动态生成的信息流式传输到某种前端组件中。使用 instructor
,你可以通过 使用 create_partial
方法 来做到这一点。
import instructor
from openai import OpenAI
from pydantic import BaseModel
from rich.console import Console
client = instructor.from_openai(OpenAI(), mode=instructor.Mode.TOOLS_STRICT)
text_block = """
In our recent online meeting, participants from various backgrounds joined to discuss the upcoming tech conference. The names and contact details of the participants were as follows:
- Name: John Doe, Email: johndoe@email.com, Twitter: @TechGuru44
- Name: Jane Smith, Email: janesmith@email.com, Twitter: @DigitalDiva88
- Name: Alex Johnson, Email: alexj@email.com, Twitter: @CodeMaster2023
During the meeting, we agreed on several key points. The conference will be held on March 15th, 2024, at the Grand Tech Arena located at 4521 Innovation Drive. Dr. Emily Johnson, a renowned AI researcher, will be our keynote speaker.
The budget for the event is set at $50,000, covering venue costs, speaker fees, and promotional activities. Each participant is expected to contribute an article to the conference blog by February 20th.
A follow-up meeting is scheduled for January 25th at 3 PM GMT to finalize the agenda and confirm the list of speakers.
"""
class User(BaseModel):
name: str
email: str
twitter: str
class MeetingInfo(BaseModel):
users: list[User]
date: str
location: str
budget: int
deadline: str
extraction_stream = client.chat.completions.create_partial(
model="gpt-4o-mini",
response_model=MeetingInfo,
messages=[
{
"role": "user",
"content": f"Get the information about the meeting and the users {text_block}",
},
],
stream=True,
)
console = Console()
for extraction in extraction_stream:
obj = extraction.model_dump()
console.clear()
console.print(obj)
这将输出以下内容
提供商无关的 API¶
使用 instructor
,由于我们统一的 API,在不同提供商之间切换非常容易。
例如,从 OpenAI 切换到 Anthropic 只需三个调整
- 导入 Anthropic 客户端
- 使用
from_anthropic
代替from_openai
- 更新模型名称(例如,从 gpt-4o-mini 到 claude-3-5-sonnet)
这使得对于希望迁移和测试不同提供商以满足其用例的用户来说,它变得非常灵活。下面我们来看一个实际例子。
import instructor
from openai import OpenAI
from pydantic import BaseModel
client = instructor.from_openai(OpenAI())
class User(BaseModel):
name: str
age: int
resp = client.chat.completions.create(
model="gpt-4o-mini",
response_model=User,
messages=[
{
"role": "user",
"content": "Extract the user from the string belo - Chris is a 27 year old engineer in San Francisco",
}
],
max_tokens=100,
)
print(resp)
#> name='Chris' age=27
现在我们来看如何使用 Anthropic 实现同样的效果。
import instructor
from anthropic import Anthropic # (1)!
from pydantic import BaseModel
client = instructor.from_anthropic(Anthropic()) # (2)!
class User(BaseModel):
name: str
age: int
resp = client.chat.completions.create(
model="claude-3-5-sonnet-20240620", # (3)!
response_model=User,
messages=[
{
"role": "user",
"content": "Extract the user from the string belo - Chris is a 27 year old engineer in San Francisco",
}
],
max_tokens=100,
)
print(resp)
#> name='Chris' age=27
- 导入 Anthropic 客户端
- 使用
from_anthropic
代替from_openai
- 将模型名称更新为
claude-3-5-sonnet-20240620
结论¶
虽然 OpenAI 的结构化输出显示出潜力,但它存在关键限制。该系统缺乏对额外 JSON 字段的支持,无法提供输出示例、默认值工厂以及定义 schema 中的模式匹配。这些限制限制了开发者表达复杂返回类型的能力,可能影响应用程序的性能和灵活性。
如果你对结构化输出感兴趣,instructor
解决了这些关键问题。它提供了自动重试、实时输入验证和多提供商集成,使开发者能够更有效地在其 AI 项目中实现结构化输出。
如果你还没有尝试过 instructor
,今天就试试吧!