引言
与大型语言模型(LLM)交互时,一个核心的挑战在于如何确保其输出的稳定性和格式一致性。尽管 LLM 在自然语言理解和生成方面表现出色,但其输出的非确定性常常导致下游应用程序难以处理。开发者们尝试了各种提示工程(Prompt Engineering)技巧,但效果并不稳定。
pydantic-ai
是一个旨在解决此问题的 Python 库,它通过深度整合 Pydantic 的数据验证和模式(Schema)生成能力,为 LLM 的输出提供了一套强大而可靠的结构化约束机制。本文将深入剖析 pydantic-ai
的内部实现,揭示其如何将 Python 类型定义转化为对 LLM 输出的精确控制。
核心设计思想:万物皆工具(Everything as a Tool)
pydantic-ai
最核心的设计思想是将期望的结构化输出抽象为一种特殊的、必须调用的“工具”。它没有将输出结构仅仅作为提示信息的一部分,而是利用了现代 LLM(如 GPT-4、Gemini)强大的工具调用(Tool Calling)或函数调用(Function Calling)API。
这种设计的优势在于:
1. 统一的抽象层:无论是执行外部 API 调用(例如查询天气),还是格式化最终的答案,其底层逻辑都被统一为“工具使用”,大大简化了 Agent
的内部状态管理。
2. 利用模型原生能力:它直接利用了模型厂商为工具调用所做的深度优化,这比简单的文本解析更为可靠和高效。
3. 强制性与可靠性:通过 API 参数,pydantic-ai
可以强制模型必须调用指定的输出工具,从而从根本上保证了返回数据的结构,避免了模型返回普通文本或其他无关内容。
实现流程深度剖析
下面,我们将以一个 Agent
的完整生命周期为例,分步解析其技术实现。
步骤一:模式定义与初始化
当用户创建一个 Agent
实例并指定 output_type
时,一系列的初始化工作便开始了。
from pydantic import BaseModel
from pydantic_ai import Agent
class CityLocation(BaseModel):
city: str
country: str
# 初始化 Agent 并指定输出类型
agent = Agent('google:gemini-1.5-flash', output_type=CityLocation)
1. Agent.__init__
: 在 agent.py
中,Agent
的构造函数接收 output_type
参数。它立即调用 _output.OutputSchema.build()
方法,将 CityLocation
这个 Python 类型转化为内部表示。
2. OutputSchema.build()
: 这个位于 _output.py
的类方法是模式转换的起点。它会为 CityLocation
类型创建一个 OutputTool
实例。这个 OutputTool
内部包含一个 OutputObjectSchema
。
3. 生成 JSON Schema: OutputObjectSchema
的核心职责是利用 Pydantic 的 TypeAdapter
对 CityLocation
进行自省,并生成一份完全兼容的 JSON Schema。这份 Schema 详细描述了 city
和 country
字段及其类型,它将作为与 LLM 沟通的“技术合同”。同时,它还会创建一个 Pydantic SchemaValidator
,用于后续的数据验证。
步骤二:构建面向模型的工具定义
当 agent.run_sync()
被调用时,pydantic-ai
会根据目标模型(如 Google Gemini)的 API 规范,将上一步生成的通用 JSON Schema 包装成特定格式。
1. 特定于模型的转换: 在 models/google.py
中,GoogleModel
类在其 _generate_content
方法中调用 _get_tools
。此方法会将 OutputTool
转换为 Google API 所要求的 FunctionDeclarationDict
格式,其中包含了工具的名称、描述以及最重要的 parameters
(即第一步生成的 JSON Schema)。
2. 强制工具调用: 这是确保结构化输出的关键。GoogleModel
调用 _get_tool_config
方法,生成一个 ToolConfigDict
。通过将 mode
设置为 ANY
并提供允许的函数名,它指示 Google API 必须调用这个工具,从而排除了模型返回非结构化文本的可能性。
步骤三:响应解析与数据验证
LLM 执行后,会返回一个包含工具调用结果的响应。
1. 解析响应: GoogleModel._process_response
方法解析来自 Google API 的响应,提取出 function_call
部分,并将其封装成一个通用的 ToolCallPart
消息。
2. 数据验证与实例化: 在 _agent_graph.py
的驱动下,这个 ToolCallPart
被传递给 OutputTool.process
方法。该方法进而调用 OutputObjectSchema.process
,并使用在初始化时创建的 SchemaValidator
来验证 LLM 返回的参数。如果验证通过,LLM 提供的 JSON 数据就会被成功实例化为一个 CityLocation
Python 对象。如果验证失败,则会触发重试机制。
高级特性与鲁棒性设计
a. 动态与复合输出
多模式输出 (Sequence
/Union
): output_type
支持接收一个类型列表(如 [Flight, Hotel]
)。pydantic-ai
会为每个类型创建独立的 OutputTool
,让 LLM 根据上下文选择最合适的一个进行“调用”,实现了动态输出格式。
函数即模式 (output_type=function
): 用户可以直接传入一个带有类型注解和文档字符串的 Python 函数。_function_schema.py
模块会对函数进行自省,自动生成对应的 JSON Schema,这遵循了 DRY(Don't Repeat Yourself)原则,极为便利。
b. 闭环重试机制
当 Pydantic 验证失败并抛出 ValidationError
时,pydantic-ai
不会立即失败。
它会捕获该错误,并将其中的详细信息(如哪个字段缺失或类型错误)包装成一个 RetryPromptPart
消息。
这个包含修正指令的消息会被添加到对话历史中,并重新发送给 LLM。
LLM 看到自己的错误和修正建议后,有很大几率在下一次尝试中生成正确的数据。这个“请求 → 验证 → 失败 → 修正 → 重试”的闭环大大增强了系统的鲁棒性。
c. Agent Graph 状态机
所有这些复杂的交互流程由 _agent_graph.py
中的一个内部图状态机进行调度。它定义了 UserPromptNode
(接收输入)、ModelRequestNode
(调用模型)、CallToolsNode
(处理工具调用)和 End
(结束)等状态节点。这个状态机确保了无论对话进行多少轮、工具被调用多少次、重试发生多少回,整个流程始终清晰、有序且可控。
结论
pydantic-ai
通过将结构化输出巧妙地抽象为一种强制性的工具调用,并深度整合 Pydantic 的模式生成与验证能力,实现了一套极为可靠的 LLM 输出约束机制。其设计不仅优雅地统一了多种交互逻辑,还通过闭环重试和精巧的状态机管理保证了系统的鲁棒性。这种从“提示”到“编程”的范式转变,使得构建可预测、生产级别的 AI 应用成为可能。
📍发表于:中国 北京