BAML 与 Instructor:结构化大语言模型输出

使用 BAML 和 Instructor 实现类型安全的 LLM 输出

目录

在生产环境中使用大型语言模型时,获取结构化、类型安全的输出至关重要。
两个流行的框架——BAML 和 Instructor——采用不同的方法来解决这个问题。

此比较帮助您为 Python LLM 应用选择合适的工具。

circular-flow

理解结构化输出的挑战

大型语言模型自然生成非结构化文本,但现代应用需要可预测、可解析的数据。无论您是在构建聊天机器人、数据提取流水线还是 AI 代理,您都需要 JSON 对象、验证过的数据类型和错误处理,而不是自由形式的响应。

BAML 和 Instructor 都解决了这个问题,但它们的基本哲学截然不同:BAML 使用合同优先的方法结合代码生成,而 Instructor 借助 Python 的类型系统进行运行时验证。如果您对 不同 LLM 提供商的结构化输出方法 感兴趣,那么理解这些框架将变得更加有价值。

BAML:LLM 的领域专用语言

BAML(BoundaryML 的语言)引入了一种专门的 DSL 来定义 LLM 交互。您编写 .baml 文件来声明提示、类型和函数,然后 BAML 为多种语言(包括 Python)生成类型安全的客户端代码。

BAML 的关键功能

跨语言的类型安全性:BAML 从相同的 .baml 定义中为 Python、TypeScript 和 Ruby 生成客户端,确保在您的技术栈中保持一致性。

提示的版本控制:您的提示存储在 .baml 文件中,使它们易于跟踪、审查和独立于应用程序代码进行测试。

内置的测试框架:BAML 包含测试工具,可在部署前验证提示行为,从而在开发早期发现问题。

游乐场界面:BAML 的游乐场允许您通过可视化方式迭代提示并立即获得反馈,从而加快开发周期。

BAML 示例实现

# 首先,在 .baml 文件中定义您的模式:
# persona.baml

class Person {
  name string
  age int
  occupation string
  skills string[]
}

function ExtractPerson(text: string) -> Person {
  client GPT4
  prompt #"
    从以下内容中提取人物信息{{ text }}
    返回结构化数据
  "#
}

生成的 Python 客户端提供了类型安全的访问:

from baml_client import b
from baml_client.types import Person

# 使用生成的客户端
text = "John Smith, 34, software engineer skilled in Python and Go"
result: Person = b.ExtractPerson(text)

print(f"{result.name}{result.age} 岁")
print(f"技能:{', '.join(result.skills)}")

当您有多个服务使用相同的 LLM 合约,或者需要在语言边界上对数据形状有强保证时,BAML 的方法尤为突出。

Instructor:Pydantic 本地的 Python 框架

Instructor 采用 Python 优先的方法,通过扩展 Pydantic 模型来添加 LLM 功能。对于已经使用 Pydantic 进行验证和类型提示的 Python 开发者来说,这感觉很自然。

Instructor 的关键功能

零样板代码:Instructor 直接使用您现有的 Pydantic 模型,通过简单的装饰器即可工作。不需要代码生成或构建步骤。

丰富的验证功能:利用 Pydantic 的完整验证生态系统——自定义验证器、字段约束、计算字段和复杂的嵌套结构。

多提供者支持:通过统一的接口与 OpenAI、Anthropic、Google 和 Ollama 无缝协作。

流式支持:对流式响应提供一流的原生支持,支持增量 Pydantic 模型更新。

重试逻辑:内置重试机制,包括指数退避和基于验证器的错误恢复。

Instructor 示例实现

from pydantic import BaseModel, Field
from instructor import from_openai
from openai import OpenAI

# 定义您的 Pydantic 模型
class Person(BaseModel):
    name: str = Field(description="人物的全名")
    age: int = Field(ge=0, le=120, description="年龄,单位为年")
    occupation: str
    skills: list[str] = Field(description="专业技能列表")

# 修补 OpenAI 客户端
client = from_openai(OpenAI())

# 提取结构化数据
text = "John Smith, 34, software engineer skilled in Python and Go"
result = client.chat.completions.create(
    model="gpt-4",
    response_model=Person,
    messages=[
        {"role": "user", "content": f"提取人物信息:{text}"}
    ]
)

print(f"{result.name}{result.age} 岁")
print(f"技能:{', '.join(result.skills)}")

Instructor 的优势在于其简单性和与 Python 生态系统的集成。如果您已经在使用 Pydantic,学习曲线非常小。对于 Python 新手或需要快速参考 Python 特定模式的开发者,我们的 Python 速查表 提供了这些框架的有用语法提醒。

详细比较:BAML vs Instructor

开发体验

BAML 需要额外的构建步骤和工具设置。您编写 .baml 文件,运行生成器,然后导入生成的代码。这在提示工程和应用逻辑之间创建了清晰的分离,这对大型团队可能有益。

Instructor 没有设置摩擦——只需 pip 安装即可。您的提示与代码并存,使小型项目或原型的快速迭代更容易。

类型安全和验证

BAML 在生成的代码中提供编译时类型检查。您的 IDE 在运行任何内容之前就知道确切的字段可用。由于相同的 .baml 文件为所有支持的语言生成客户端,跨语言一致性得到保证。

Instructor 通过 Pydantic 提供运行时验证。虽然 Python 类型提示提供 IDE 支持,但错误在执行期间出现。这在 Python 中是标准做法,但意味着比 BAML 的生成代码少一些静态保证。

使用本地 LLM

两个框架都支持本地模型,这对于隐私、成本控制和离线开发至关重要。使用 Ollama 或其他本地 LLM 提供商时,您可以保持结构化输出的好处,而无需依赖外部 API。对于深入了解 使用 Ollama、Qwen3 和 Python 或 Go 通过结构化输出约束 LLM,这些框架提供了对底层 API 的生产就绪抽象。

BAML 通过在 .baml 文件中配置客户端连接到 Ollama:

# 在您的 .baml 文件中:
client OllamaLocal {
  provider ollama
  options {
    model "llama2"
    base_url "http://localhost:11434"
  }
}

Instructor 通过 OpenAI 兼容 API 与 Ollama 一起工作:

from openai import OpenAI
from instructor import from_openai

client = from_openai(OpenAI(
    base_url="http://localhost:11434/v1",
    api_key="ollama"  # 虚拟密钥
))

请注意,当使用本地模型时,您需要了解 Ollama 和 GPT-OSS 模型的结构化输出问题,因为并非所有模型都能以相同可靠性处理结构化输出。

错误处理和重试

BAML 在框架级别处理重试,具有可配置的策略。模式验证错误会触发带有错误上下文的自动重新提示。

Instructor 提供装饰器重试逻辑,并带有自定义行为的钩子。您可以定义触发重试的验证器,并使用修改后的提示:

from instructor import patch
from tenacity import retry, stop_after_attempt

@retry(stop=stop_after_attempt(3))
def extract_with_retry(text: str) -> Person:
    return client.chat.completions.create(
        model="gpt-4",
        response_model=Person,
        messages=[{"role": "user", "content": text}]
    )

测试和可观测性

BAML 包含一个测试框架,您可以在 .baml 文件中直接编写测试用例,验证不同输入的提示行为。游乐场提供可视化调试。

Instructor 与标准 Python 测试框架集成。您可以像任何 Python 代码一样使用 pytest 固件、模拟库和断言助手。

性能考虑

运行时性能相当——这两个框架最终都进行相同的 LLM API 调用。验证和解析的开销与网络延迟和模型推理时间相比可以忽略不计。

开发速度 差异显著:

  • BAML 的代码生成意味着更好的自动补全和更早的错误检测,但需要构建步骤
  • Instructor 的装饰器方法意味着更快的迭代,但运行时发现错误

对于处理数百万请求的生产系统,两个框架都能很好地处理负载。您的选择更多取决于开发工作流的偏好,而不是性能特征。

何时选择 BAML

当您需要以下功能时,请选择 BAML:

  • 多语言支持:从 Python、TypeScript 和 Ruby 服务访问相同的 LLM 合约
  • 合同优先开发:在实现之前设计 LLM 接口的 API 风格开发
  • 团队协作:将提示工程工作流与应用开发分离
  • 强类型保证:在您的整个技术栈中进行编译时检查
  • 可视化提示开发:通过游乐场进行提示的迭代开发

何时选择 Instructor

当您想要以下功能时,请选择 Instructor:

  • 仅限 Python 的项目:不需要跨语言一致性
  • 快速原型设计:最少的设置即可使结构化输出正常工作
  • Pydantic 集成:利用现有的 Pydantic 模型和验证器
  • 简单部署:无需构建步骤或生成代码
  • 丰富的 Python 生态系统:使用 Python 特定的库和模式

结合方法

一些项目从使用这两个框架中获益。例如,您可能会使用 BAML 为需要跨语言客户端的客户面向 API,而使用 Instructor 为需要快速迭代的内部 Python 服务。

您也可以在项目成熟过程中在框架之间切换——从 Instructor 开始进行快速验证,然后在需要更广泛语言支持或更严格的合约时迁移到 BAML。

实际应用场景

数据提取流水线(BAML)

一个文档处理系统使用 BAML 从发票、合同和收据中提取结构化数据。.baml 定义作为 ML 团队和后端服务之间的合约,Web 仪表板使用 TypeScript 客户端,批处理使用 Python 客户端。

客户支持机器人(Instructor)

一个支持机器人使用 Instructor 对票证进行分类,提取用户意图并生成响应。团队使用 Pydantic 模型快速迭代提示,验证器确保提取的电话号码、电子邮件和票证 ID 符合格式要求。

多模态 AI 代理(两者)

一个 AI 代理系统使用 BAML 为代理之间的通信合约确保分布式系统中的类型安全,而单个代理内部使用 Instructor 进行灵活的、Python 本地处理用户输入。类似模式适用于 使用 Python 构建 MCP 服务器,其中结构化输出使 AI 助手的工具集成变得可靠。

迁移和集成路径

如果您已经在使用 LLM 的基本 JSON 解析,两个框架都提供简单的迁移路径:

从 JSON 到 BAML:将您的 JSON 模式转换为 BAML 类型定义,将提示移动到 .baml 文件中,生成客户端,并用生成的类型替换手动解析。

从 JSON 到 Instructor:添加匹配您的 JSON 结构的 Pydantic 模型,安装 instructor,修补您的 OpenAI 客户端,并用 response_model 参数替换 JSON 解析。

这两种迁移都可以逐步进行——您不需要一次性转换整个代码库。

未来展望和社区

这两个框架都在积极开发中,并拥有强大的社区:

BAML(BoundaryML)专注于扩展语言支持、改进游乐场和增强测试功能。商业支持表明其长期稳定性。

Instructor 保持强大的开源存在,频繁更新,文档详尽,采用率不断增长。该项目由 Jason Liu 和贡献者良好维护。

结论

BAML 和 Instructor 代表了两种优秀但截然不同的结构化 LLM 输出方法。BAML 的合同优先、多语言哲学适合构建具有严格类型要求的分布式系统的团队。Instructor 的 Python 本地、基于 Pydantic 的方法适合快速开发和 Python 为中心的堆栈。

没有一种方法是普遍更好的——您的选择取决于团队规模、语言偏好、开发工作流和类型安全性要求。许多团队会发现从 Instructor 开始进行原型设计,然后在生产多服务架构中采用 BAML,这能获得最佳效果。

有用的链接

本站相关文章

外部参考资料