Pydantic-AI:用类型安全契约驱动AI智能体开发
1. 项目概述当Pydantic遇见AI数据验证与智能体开发的化学反应如果你最近在Python的AI应用开发圈子里混一定对“智能体”这个词不陌生。从简单的聊天机器人到复杂的自动化工作流智能体正在成为连接大语言模型与现实业务逻辑的关键桥梁。但在这个过程中一个老生常谈的问题又冒了出来如何可靠地处理AI那“不确定”的输出并将其安全、结构化地集成到你的确定性代码中你可能会想到用Pydantic来做数据验证这确实是Python生态里的黄金标准。但手动写一堆BaseModel再写胶水代码去调用API、解析响应、处理错误……这个过程既繁琐又容易出错。这就是pydantic-ai诞生的背景。它不是另一个AI框架而是Pydantic官方团队推出的一款“粘合剂”旨在将Pydantic强大的运行时类型验证与数据建模能力无缝地注入到AI驱动的应用开发流程中。简单来说它让你能用定义Pydantic模型一样熟悉、严谨的方式去定义和运行一个AI智能体。你不再需要操心HTTP请求、响应解析、重试逻辑这些底层细节而是可以专注于两件事用自然语言描述任务用Pydantic模型定义你期望的输出结构。pydantic-ai会帮你搞定中间的所有环节确保AI的输出最终能以你预期的、类型安全的Python对象形式交付。这解决了什么痛点想象一下你让AI从一段客户反馈中提取“产品名称”、“问题类型”和“情绪分数”。没有pydantic-ai你得到的可能是一段自由文本你需要自己写正则表达式或者复杂的逻辑去提取并且永远担心AI会不会突然用“非常生气”而不是数字“1”来表示情绪。有了pydantic-ai你只需要定义一个Feedback模型包含product_name: strissue_type: Literal[‘bug’ ‘feature’ ‘complaint’]sentiment: conint(ge1 le5)。然后告诉AI“请从以下文本中提取信息并填充到这个模型里。” 最终你拿到手的就是一个已经通过Pydantic验证的Feedback实例字段类型正确值在约束范围内可以直接用于数据库存储或下游业务逻辑。这不仅仅是方便更是从根本上提升了AI应用的可维护性和可靠性。2. 核心设计哲学以模型为中心驱动AI交互pydantic-ai的设计不是凭空而来的它深刻反映了当前AI工程化实践中的几个关键趋势并将Pydantic的核心优势发挥到了极致。2.1 契约优先而非字符串拼接传统调用AI API的方式本质上是“字符串工程”。你需要精心构造一个提示词Prompt字符串发送出去然后祈祷返回的文本是你想要的格式再手动解析。这个过程脆弱且难以测试。pydantic-ai翻转了这个范式提倡“契约优先”。这里的契约就是你的Pydantic模型。你首先定义好你希望AI返回的数据结构契约然后pydantic-ai会利用这个模型的信息自动优化发送给AI的提示词并强制要求AI的响应必须符合这个模型。这带来了几个根本性好处可测试性你可以像测试普通函数一样测试你的智能体。给定一个输入预期的输出类型是明确的你可以编写单元测试来验证智能体在各种情况下的行为。可维护性当业务逻辑变化需要增加或修改返回字段时你只需要更新Pydantic模型。pydantic-ai会自动处理提示词和解析逻辑的调整无需手动搜索和修改散落在各处的字符串模板和解析代码。开发者体验IDE的自动补全、类型检查、跳转到定义等功能全部生效。你操作的是熟悉的Python对象而不是一团模糊的JSON或文本。2.2 将AI视为一个不确定的函数在pydantic-ai的抽象里一个AI智能体Agent被视作一个特殊的、可能出错的函数。你调用它传入参数包括系统指令、用户消息、工具等它返回一个经过验证的结果对象。这个抽象巧妙地将AI的复杂性封装了起来。开发者无需关心底层是调用的OpenAI、Anthropic还是本地部署的模型也无需处理网络错误、速率限制、上下文窗口管理等。pydantic-ai提供了统一的接口和强大的中间件如重试、缓存、监控支持让开发者可以专注于业务逻辑。2.3 无缝集成Pydantic V2的全部能力这是pydantic-ai的杀手锏。它不仅仅是“支持”Pydantic模型而是深度集成。这意味着你可以使用Pydantic V2的所有高级特性来约束AI的行为字段类型与验证器使用EmailStr、HttpUrl等类型确保AI返回的是有效格式使用自定义验证器进行更复杂的业务规则校验。字段描述Pydantic字段的description参数会被pydantic-ai用作提示词的一部分清晰地告诉AI这个字段期望什么内容。递归模型与嵌套你可以定义非常复杂的、嵌套的数据结构pydantic-ai能够引导AI生成符合整个结构的数据。field_validator在数据被返回给调用者之前你还可以运行自定义的校验逻辑。这种深度集成使得约束AI输出变得异常强大和直观。你不需要学习一套新的模式定义语言用你已经掌握的Pydantic知识就足够了。3. 从零开始构建你的第一个智能体理论说得再多不如动手试一下。我们来构建一个简单的“会议纪要生成器”智能体体验pydantic-ai的工作流。3.1 环境准备与安装首先确保你的Python版本在3.8以上。然后使用pip安装pydantic-ai。由于它需要与AI模型后端通信我们同时安装OpenAI的SDK作为示例。pip install pydantic-ai openai接下来你需要设置你的AI模型API密钥。以OpenAI为例设置环境变量export OPENAI_API_KEY‘你的sk-xxx密钥’在代码中你也可以通过pydantic_ai.settings来配置。3.2 定义数据模型你的期望契约我们期望AI分析一段会议录音转录的文字并提取出结构化的信息。我们来定义这个结构。from pydantic import BaseModel Field from typing import List Optional from datetime import time class DecisionItem(BaseModel): 会议中做出的一个决策项。 topic: str Field(description“决策涉及的主题”) decision: str Field(description“达成的具体决策内容”) owner: Optional[str] Field(defaultNone description“决策负责人如果提及”) deadline: Optional[str] Field(defaultNone description“截止日期如‘下周前’”) class ActionItem(BaseModel): 会议中产生的一个行动项。 task: str Field(description“需要执行的具体任务”) assignee: str Field(description“任务负责人”) due_date: Optional[str] Field(defaultNone description“期望完成时间”) class MeetingMinutes(BaseModel): 会议纪要核心内容。 meeting_topic: str Field(description“会议的核心主题”) key_decisions: List[DecisionItem] Field(description“会议做出的关键决策列表”) action_items: List[ActionItem] Field(description“产生的行动项列表”) summary: str Field(description“会议内容的简要总结不超过200字”)注意看我们使用了Field(description...)。这个描述至关重要pydantic-ai会将这些描述注入给AI的提示词中指导它如何填充每个字段。Optional类型使得字段可为空更符合实际情况。3.3 创建并运行智能体有了模型创建智能体就非常简单了。我们使用pydantic-ai提供的Agent类。import asyncio from pydantic_ai import Agent from pydantic_ai.models.openai import OpenAIModel # 1. 选择模型。这里使用gpt-4o-mini你也可以换成gpt-4o或其它支持的模型。 model OpenAIModel(‘gpt-4o-mini’) # 2. 创建智能体并指定其返回的结果类型为我们的MeetingMinutes模型。 meeting_agent Agent(model result_typeMeetingMinutes) # 3. 准备一段模拟的会议转录文本 transcript “”“ 大家好我们开始本周的产品评审会。主要议题是Q3发布计划。 首先关于用户反馈的登录缓慢问题后端团队决定优化数据库查询索引王伟负责希望在本周五前完成。 其次新首页的A/B测试数据已经出来了V2版本转化率提升了15%。我们决定全量上线V2版本由李娜推动下周中完成。 另外市场部提出需要一个新的产品介绍视频张明牵头写脚本下个月初出初稿。 最后客服系统升级项目暂缓等Q3核心功能上线后再评估。 大体就是这样大家还有补充吗 “”” async def main(): # 4. 运行智能体 # 我们通过system_prompt告诉AI它的角色和任务。 # user_prompt提供具体的输入数据。 result await meeting_agent.run( system_prompt“你是一个专业的会议秘书擅长从混乱的对话文本中提取结构化信息。请根据提供的会议转录文本生成一份清晰的会议纪要。” user_promptf“请分析以下会议转录内容\n\n{transcript}” ) # 5. 获取结果 # result.data 就是已经验证好的MeetingMinutes实例 minutes: MeetingMinutes result.data print(f“会议主题: {minutes.meeting_topic}”) print(f“\n关键决策:”) for d in minutes.key_decisions: print(f“ - [{d.topic}] {d.decision} (负责人: {d.owner or ‘N/A’} 期限: {d.deadline or ‘N/A’})”) print(f“\n行动项:”) for a in minutes.action_items: print(f“ - {a.task} (负责人: {a.assignee} 截止: {a.due_date or ‘N/A’})”) print(f“\n会议总结: {minutes.summary}”) # 你还可以访问原始响应、使用次数等信息 print(f“\n本次调用消耗token: {result.usage.total_tokens}”) if __name__ ‘__main__’: asyncio.run(main())运行这段代码你会看到AI成功地将那段自由文本转换成了一个结构清晰的MeetingMinutes对象。所有的列表、嵌套模型都正确解析。最关键的是minutes.key_decisions[0].owner拿出来的就是一个字符串而不是需要你再手动解析的字典。实操心得一系统提示词system_prompt是智能体的灵魂在run方法中system_prompt定义了智能体的“角色”和“行为准则”而user_prompt是具体的“任务指令”。一个好的system_prompt能极大提升结果的准确性和稳定性。例如对于提取任务明确要求“如果信息未提及请将对应字段留空或设为None”可以避免AI胡编乱造。4. 进阶能力解析工具调用、流式响应与依赖注入基础的数据提取只是开始。pydantic-ai为构建复杂智能体提供了更多企业级特性。4.1 赋予智能体“手脚”工具Tools调用智能体不能只思考还得能行动。pydantic-ai允许你给智能体注册“工具”——本质上就是任何Python可调用对象函数、方法。AI可以根据对话上下文决定是否以及如何调用这些工具。假设我们给会议纪要智能体加一个工具查询某个负责人的当前任务负载。from pydantic_ai import Agent Tool from pydantic_ai.models.openai import OpenAIModel # 模拟一个数据库查询函数 def query_workload(assignee_name: str) - str: “”“查询负责人的当前任务数量。这是一个模拟函数。”“” workload_map {‘王伟’: ‘3个进行中任务’ ‘李娜’: ‘1个高优任务’ ‘张明’: ‘任务负载较轻’} return workload_map.get(assignee_name ‘未知人员无记录’) # 创建工具实例。description非常重要AI靠它来理解工具用途。 workload_tool Tool( query_workload description“根据负责人姓名查询其当前的任务负载情况。输入是姓名字符串。” ) model OpenAIModel(‘gpt-4o’) # 在创建Agent时通过tools参数注册工具 agent_with_tools Agent(model result_typeMeetingMinutes tools[workload_tool]) # 现在当你运行这个智能体时它在生成纪要过程中如果觉得有必要 # 可能会自动调用query_workload工具来获取信息并将其融入最终输出。 # 例如它可能在summary里加上一句“请注意王伟当前已有3个进行中任务需关注其负载。”工具调用的强大之处在于它将AI的推理能力与你现有的代码、API和服务连接了起来。智能体可以主动获取实时数据股票价格、天气、数据库记录或者执行操作发送邮件、创建工单、调用内部API。注意事项工具描述的精确性工具函数的description和参数类型注解是AI决定是否及如何调用它的唯一依据。描述必须清晰、无歧义。参数尽量使用简单类型strintbool或简单的Pydantic模型。过于复杂的参数类型可能导致AI无法正确生成调用参数。4.2 处理长篇内容流式响应Streaming当智能体需要生成较长文本如报告、文章时等待全部生成完毕再返回体验很差。pydantic-ai支持流式响应你可以实时接收到AI思考的“碎片”。async def stream_meeting_summary(transcript: str): agent Agent(OpenAIModel(‘gpt-4o’) result_typeMeetingMinutes) result_stream agent.run_stream( system_prompt“生成会议纪要...”, user_prompttranscript ) async for chunk in result_stream: if chunk.type ‘content’: # 收到内容增量 print(chunk.content end‘’ flushTrue) elif chunk.type ‘tool-call’: print(f“\n[AI正在调用工具: {chunk.tool_name}]...”) elif chunk.type ‘tool-result’: print(f“\n[工具返回结果]”) elif chunk.type ‘result’: # 流结束拿到最终验证好的对象 final_minutes: MeetingMinutes chunk.data print(f“\n\n流式接收完毕最终模型验证成功”)流式响应不仅提升了用户体验对于调试也极其有用。你可以看到AI的思考过程以及它是如何一步步调用工具的。4.3 构建可测试的智能体依赖Dependencies真实的智能体往往需要访问外部资源数据库会话、HTTP客户端、配置信息等。pydantic-ai通过“依赖注入”机制优雅地解决了这个问题。你可以将依赖声明在智能体的运行上下文中然后在工具函数或结果验证器中获取它们。from pydantic_ai import Agent Depends from sqlalchemy.orm import Session def get_db_session(): # 假设这是你的数据库会话获取函数 session ... # 初始化会话 try: yield session finally: session.close() class MeetingMinutesWithDB(BaseModel): meeting_topic: str action_items: List[ActionItem] # 假设我们想验证负责人是否存在于员工表 _db_session: Session Depends(get_db_session) # 声明依赖 model_validator(mode‘after’) def validate_assignees(self): for item in self.action_items: # 使用依赖的db_session进行查询 exists self._db_session.query(...).filter_by(nameitem.assignee).first() if not exists: raise ValueError(f“行动项负责人 ‘{item.assignee}’ 不在员工系统中。”) return self # 创建Agent时依赖会在每次run时被注入 agent Agent(model result_typeMeetingMinutesWithDB) # 运行时你需要提供依赖 async def run_agent_with_db(transcript: str db_session: Session): # 通过deps参数注入依赖 result await agent.run( system_prompt“...”, user_prompttranscript deps{Session: db_session} # 将具体的会话实例映射到依赖类型 ) return result.data依赖注入使得你的智能体逻辑与外部基础设施解耦单元测试时可以轻松注入Mock对象极大提高了代码的可测试性和可维护性。5. 实战构建一个支持多轮对话的客服分类机器人让我们用一个更复杂的例子串联起上述特性。我们要构建一个客服机器人它能够理解用户模糊的投诉或咨询。通过多轮对话澄清问题细节。调用内部工具查询知识库或订单状态。最终将问题分类并结构化传递给下游工单系统。5.1 定义对话状态与结果模型在多轮对话中我们需要维护状态。pydantic-ai的Agent本身是无状态的但我们可以通过模型和工具来模拟状态。from enum import Enum from pydantic import BaseModel Field from typing import List Optional class IssueCategory(Enum): BILLING “计费问题” TECHNICAL “技术故障” FEATURE_REQUEST “功能建议” ACCOUNT “账户管理” OTHER “其他” class StructuredTicket(BaseModel): “”“最终生成的工单结构。”“” category: IssueCategory title: str Field(description“工单摘要标题”) description: str Field(description“问题详细描述”) customer_id: Optional[str] Field(defaultNone description“客户ID如果用户提供”) priority: int Field(ge1 le5 default3 description“紧急程度1最高5最低”) tags: List[str] Field(default_factorylist description“相关标签如‘登录失败’ ‘iOS’”)5.2 创建具备工具和记忆的智能体我们将创建一个能主动提问、查信息的智能体。from pydantic_ai import Agent Tool from pydantic_ai.models.openai import OpenAIModel import asyncio # 工具1模拟查询订单状态 def lookup_order(order_id: str) - str: # 模拟数据库查询 orders {“ORD123”: “已发货 物流单号SF998877” “ORD456”: “待支付”} return orders.get(order_id “未找到该订单号请确认。”) # 工具2模拟查询知识库文章 def search_knowledge_base(keywords: List[str]) - str: # 模拟搜索 if “登录” in keywords: return “常见登录问题解决1. 检查网络2. 清除缓存3. 重置密码。详情见链接https://help.example.com/login” return “未找到完全匹配的文章请尝试其他关键词或联系人工客服。” # 工具3一个特殊的“提问”工具用于让AI主动向用户索要信息 async def ask_user(question: str) - str: “”“智能体通过此工具向用户提问。在实际应用中这里会连接你的前端对话界面。”“” print(f“[机器人提问]: {question}”) # 模拟用户输入。真实场景中这里应该等待并接收用户的回复。 user_answer input(“[您的回答]: “) return user_answer order_tool Tool(lookup_order description“根据订单号查询订单状态。输入是一个订单号字符串。”) kb_tool Tool(search_knowledge_base description“根据关键词列表搜索知识库。输入是字符串列表。”) ask_user_tool Tool(ask_user description“当需要更多信息来解决问题时向用户提问。输入是你想问的问题字符串。”) model OpenAIModel(‘gpt-4o’ temperature0.2) # 降低temperature使输出更确定 support_agent Agent( model result_typeStructuredTicket tools[order_tool kb_tool ask_user_tool] system_prompt“”“ 你是一个专业的客服助手。你的目标是通过与用户对话收集足够信息最终生成一个结构化工单。 你的流程是 1. 首先理解用户的问题。 2. 如果信息不足比如缺少订单号、错误详情使用‘ask_user’工具主动询问用户。 3. 如果用户提到订单使用‘lookup_order’工具查询状态。 4. 如果问题可能是常见问题使用‘search_knowledge_base’工具尝试直接提供解决方案。 5. 当你认为信息已足够或者用户要求转人工时生成最终的StructuredTicket对象。 在对话中请保持友好和专业。 “”” )5.3 实现多轮对话循环我们需要一个循环来驱动对话直到智能体生成最终结果。async def run_support_chat(): print(“客服机器人已上线请描述您的问题 (输入‘quit’退出)。”) conversation_history [] # 简易历史记录 while True: user_input input(“\n[您]: “) if user_input.lower() ‘quit’: break conversation_history.append({“role”: “user” “content”: user_input}) # 将历史记录作为上下文传入。这里简化处理实际可使用Agent的run方法多次调用。 # 更复杂的实现可以使用pydantic-ai的Message类型来管理历史。 context “\n”.join([f“{msg[‘role’]}: {msg[‘content’]}” for msg in conversation_history[-6:]]) # 保留最近6轮 try: result await support_agent.run( user_promptcontext # 注意我们将system_prompt已在创建Agent时定义。 ) except Exception as e: print(f“处理出错: {e}”) continue # 检查结果 if result.data: # 智能体生成了最终工单 ticket: StructuredTicket result.data print(f“\n[机器人]: 信息已收集完毕。已为您创建工单”) print(f“ 分类: {ticket.category.value}”) print(f“ 标题: {ticket.title}”) print(f“ 优先级: P{ticket.priority}”) print(f“ 标签: {‘ ‘.join(ticket.tags)}”) print(“\n工单即将提交给专人处理感谢您的反馈”) break # 对话结束 else: # 智能体可能调用了工具工具的输出会体现在result.messages里 # 这里我们简单地将AI的回复加入历史 ai_response result.message_content() # 获取AI最新的文本回复 if ai_response: print(f“[机器人]: {ai_response}”) conversation_history.append({“role”: “assistant” “content”: ai_response}) else: print(“[机器人]: (正在处理...)”) # 运行对话 if __name__ ‘__main__’: asyncio.run(run_support_chat())这个例子展示了如何将工具调用、状态管理和结构化输出结合。AI在对话中扮演了驱动者的角色决定何时提问、何时查资料、何时结束对话并输出结果。而你开发者只需要定义好工具、结果模型和系统指令。6. 部署与生产环境考量当你的智能体开发完毕准备投入生产时有几个关键点需要注意。6.1 性能与成本优化模型选择gpt-4系列精度高但成本也高gpt-3.5-turbo或gpt-4o-mini性价比更好。根据任务复杂度选择。pydantic-ai支持切换模型只需更改Model实例。提示词优化精心设计的system_prompt和模型字段的description能减少不必要的来回turns节省token。明确要求AI“如果信息缺失则留空字段”可以避免其编造。缓存对于相同输入期望相同输出的场景可以实现缓存层。pydantic-ai的中间件系统或外部缓存如Redis可以集成。异步与并发pydantic-ai原生支持异步。在生产Web服务中如FastAPI确保使用异步端点以高效处理多个并发请求。6.2 监控、日志与可观测性AI应用的不确定性要求更完善的监控。记录输入输出记录每一次agent.run的system_prompt、user_prompt、最终结果以及原始的AI响应消息(result.messages)。这对于调试和改善提示词至关重要。跟踪Token使用result.usage包含了prompt、completion和总的token数。将这些指标发送到你的监控系统如Prometheus以分析成本趋势和异常。性能指标记录每次调用的延迟。AI API的响应时间可能波动。错误处理除了网络错误、API限额错误还要处理AI输出无法通过Pydantic验证的情况。pydantic-ai会抛出验证错误你需要有相应的重试或降级策略例如让AI重试一次或转人工处理。6.3 与现有框架集成pydantic-ai智能体可以很容易地集成到流行的Web框架中。FastAPI 集成示例from fastapi import FastAPI HTTPException from pydantic_ai import Agent from pydantic_ai.models.openai import OpenAIModel from .schemas import MeetingMinutes MeetingRequest # 你的Pydantic模型 app FastAPI() model OpenAIModel(‘gpt-4o-mini’) meeting_agent Agent(model result_typeMeetingMinutes) app.post(“/api/generate-minutes” response_modelMeetingMinutes) async def generate_minutes(request: MeetingRequest): “”“接收会议文本返回结构化纪要。”“” try: result await meeting_agent.run( system_prompt“你是专业会议秘书...”, user_promptrequest.transcript ) return result.data except Exception as e: # 记录日志并返回用户友好的错误 app.logger.error(f“AI处理失败: {e}” exc_infoTrue) raise HTTPException(status_code500 detail“会议纪要生成失败请稍后重试。”)7. 避坑指南与常见问题在实际使用pydantic-ai的过程中我踩过一些坑也总结了一些经验。7.1 验证失败AI不听话怎么办这是最常见的问题。你定义了一个score: conint(ge0 le10)但AI偏偏返回了“非常高”。根本原因提示词约束力不足。AI尤其是早期版本更倾向于生成自然语言。解决方案强化系统提示词在system_prompt中明确强调“你必须返回一个严格的JSON对象且必须完全遵循提供的JSON Schema。所有字段值必须符合其类型和约束。”使用更强大的模型gpt-4o系列在遵循结构化输出方面远强于gpt-3.5-turbo。提供示例在system_prompt或user_prompt中给一个完整的输出示例。Few-shot learning效果显著。后处理与重试捕获pydantic.ValidationError将错误信息反馈给AI要求它修正。pydantic-ai的run方法可以包装在重试逻辑中。7.2 工具调用不准确或不被调用问题AI该调用工具时不调用或者调用时参数传错。检查点工具描述Tool(description...)是否清晰、无歧义是否说明了输入参数的类型和含义参数类型工具函数的参数是否使用了简单明了的类型注解避免使用复杂的Union或自定义类型。系统指令在system_prompt中你是否明确告知了AI在什么情况下应该使用哪个工具例如“当用户询问订单状态时你必须使用lookup_order工具。”7.3 处理长篇上下文与Token限制问题输入文本很长如长文档加上对话历史容易超出模型的上下文窗口。策略摘要与分块对于超长输入先使用另一个AI调用或传统方法进行摘要再将摘要送给主智能体。或者将长文本分块处理再合并结果。有选择地保留历史在多轮对话中不要无脑传送全部历史。可以只保留最近几轮或者总结之前的对话要点后再送入。使用支持长上下文的模型优先选择上下文窗口大的模型如gpt-4o128K。7.4 依赖管理在异步环境中的坑问题在异步Web服务器中数据库会话等依赖的生命周期管理需要小心。最佳实践使用FastAPI等框架的依赖注入系统来管理Session等资源的创建和销毁。在Agent.run(deps...)中注入的是请求级别的资源实例确保它们在请求结束后被正确清理。避免在工具函数或验证器中执行长时间阻塞的操作。pydantic-ai的出现标志着AI应用开发从“脚本小子”阶段向“软件工程”阶段迈进了一步。它通过引入强大的类型契约和熟悉的开发范式显著降低了将大语言模型集成到生产系统的复杂性和风险。它可能不是所有AI任务的万能钥匙但对于那些需要可靠、结构化输出的场景——数据提取、分类、标准化、流程自动化——它无疑是一把利器。开始尝试用它来替换你项目中那些脆弱的字符串解析和正则表达式吧你会发现和AI协作编程也可以如此稳健和愉悦。