pydantic-ai 于 2026 年 9 月 25 日发布 v2.51.0:Agent.run() 改为按名称缓存执行图、capability 派发不再重建 RunContext,本文实测单次运行从 2.165 毫秒降到 1.189 毫秒,RunContext 拷贝从 65 次降到 23 次
pydantic-ai 于 2026 年 9 月 25 日发布 v2.51.0,单次 Agent.run() 从 2.165 毫秒降到 1.189 毫秒。
## 前置部署 Forward Deployed Engineer(FDE)为什么要在意这一版
前置部署 Forward Deployed Engineer(FDE)交付企业 RAG enterprise RAG 与 LLM 评估 LLM evaluation 时,最常见的口径分歧是「模型很快,为什么端到端还是慢」。答案往往不在推理,而在 agent 循环每轮都要重付的固定开销。
前置部署 Forward Deployed Engineer(FDE)把 agent 挂在低延迟模型上时,这部分开销的占比会被放大:pydantic-ai 维护者在 issue 里给出的对照是,一个中位数约 150 毫秒的低延迟模型服务上,裸 SDK 调用只占 0.32 毫秒,而 agent 循环本身要 2.94 毫秒。这一版 v2.51.0 动的正是这 2.94 毫秒。
前置部署 Forward Deployed Engineer(FDE)的第三个理由是可回归:这四处改动都在官方 PR 里给了改动前后的秒级与微秒级数字,可以用 FunctionModel 在本地重跑一遍再决定要不要升级。
## 一、这一版改了什么:四处重复开销、一个上下文接口、三个行为变更
pydantic-ai v2.51.0 的 release 页标注日期为 2026-09-25(页面 datetime 属性为 2026-09-25T23:19:04Z,仓库 star 数 20211)。这一版没有大功能,主体是四条互不相关的运行时开销修复。
四条修复分别是:PR 8658 让执行图按名称缓存而不是每次 run 重建;PR 8775 让 capability 派发不再为每个 hook 重建整个 RunContext;PR 8774 让只有一个子项的并发 fan-out 直接 await 而不是新开 task group;PR 8799 让回放已完成文本流时不再反复重拷增长中的前缀。
另一条是接口补齐:PR 8803 让实时(realtime)会话第一次能上报模型上下文的占用比例,对应新字段 context_window_used。
三条是行为变更,会打断现有代码:PR 8755 对 OpenAI、Azure OpenAI 与 xAI 实时模型上的强制 tool_choice 改为连接前抛 UserError;PR 8761 把 Gemini Live 的 WebSocket close code 从 ModelHTTPError.status_code 里挪走、改抛 RealtimeError,并修正 gemini-3.8-live 的 id 匹配;PR 8390 新增 OpenAILiveModel 支持 OpenAI GPT-Live 协议。
## 二、修复一:执行图不再每次 Agent.run() 重建
前置部署 Forward Deployed Engineer(FDE)在任何 agent 框架上做性能定位时,第一步都应该是把「框架自身开销」和「模型耗时」分开量,这一条修复正好是框架自身开销里最典型的一块。根因是 Agent.run() 每一轮都重建整个执行图。PR 8658 的描述指出,图的构建器只读 name 这一个参数,deps_type 与 output_type 只是绑定类型参数,也就是说图其实只依赖 name,却按每次 run 重算了一遍。
做法是在 pydantic_ai/_agent_graph.py 里新增一个私有的 _build_agent_graph(name),用 functools.lru_cache(maxsize=128) 装饰,公开的 build_agent_graph 保留原有泛型签名后只透传给缓存版本。本文解包 2.51.0 的 wheel 核对过这段源码:缓存函数只接收 name 一个参数,因此「图只依赖 name」这条不变量是由签名结构性保证的,不靠约定。
官方给出的数字是:单个图约占 7 KiB,缓存不持有 agent 的任何引用;一次 trivial 单请求 run 从 1.95 毫秒降到 1.62 毫秒,约快 17%,口径为 FunctionModel 加 Python 3.14。官方声明对外可见的新接口为零,行为不变。
验证方式值得抄:把构建出的图做 mermaid 渲染,比对节点 id 与边,与 main 逐字节一致;再确认 run 从不写已构建的图 —— Agent 传 infer_name=False,name 回退为字符串 Agent,_GraphIterator 也只读 graph.nodes 与 parent_forks。本文解包 wheel 的对照是 _agent_graph.py 里 lru_cache 的出现次数从 0 变成 2。
局限由作者自己写明:这个基准跑在假模型 FunctionModel 上,测的是图构建与循环开销,不含任何真实推理时间;缓存上限 128 意味着大量不同 name 的 agent 会互相挤出。
## 三、修复二:capability 派发不再重建 RunContext
根因由维护者 DouweM 在 2026-09-21 开的 issue 8587 里量化:一次完全没有 capability 的 Agent.run() 会执行 79 次 dataclasses.replace(RunContext, ...),其中 46 次来自 capability hook 的 fan-out;RunContext 有 43 个 dataclass 字段,一次 replace 约 5.7 微秒,79 次合计约 0.45 毫秒每次运行。
为什么现在才值得修:这条 issue 的论据是低延迟模型把固定开销放大到了可见量级。对照表为裸 typesafe_sdk.system_one 0.32 毫秒(占 150 毫秒请求的 0.2%)、TypeSafeModel.request() 0.41 毫秒(0.3%)、复用 agent 的 Agent.run() 2.94 毫秒(2.0%)、再加 logfire.instrument_pydantic_ai() 插桩 3.90 毫秒(2.6%)。模型层不是瓶颈,模型适配器只比裸 SDK 多 0.09 至 0.14 毫秒。
做法是按类型分叉:在 capabilities/combined.py 的 _replace_capability_context 里,如果 ctx 的精确类型就是 RunContext,改用 copy() 浅拷贝再赋 capability_active 与 _capability 两个字段;如果是子类则保留 dataclasses.replace 重建,理由是子类可能依赖重建,例如 TemporalRunContext 的构造函数维护字段可用性守卫。
官方数字是托管 CodSpeed 模拟从 17.1 毫秒降到 13.4 毫秒,相对 main 报告为效率提升 27.36%。
计量方法同样值得抄:用 httpx2.MockTransport 配一个固定的 asyncio.sleep(0.150),开销取墙钟减去真正花在 mock handler 里的时间,n=60 加 5 次预热,重复运行之间吻合到约 0.1 毫秒;replace 的调用次数来自 cProfile,样本是 300 次运行里 873900 次 getattr 调用。
局限是这次改动没有新增公开接口,也没有改 hook 的可观察语义(hook 仍然拿到独立上下文与共享的可变 run 状态),但同一作者的实现要求在 PR 里被明确写了出来:跳过 fan-out 会改变 hook 看到的东西,因为今天 hook 拿到的是一个 capability 为空的上下文,可能有代码依赖 ctx._capability 被这样设置。作者选择的路线是变便宜地拷贝,而不是跳过。
## 四、修复三:单子项 fan-out 不再新开 task group 与子任务
根因是一句具体的分布统计:作者在一个 5 次工具调用的 run 上统计到 33 次 _utils.gather 调用里有 32 次只带一个 awaitable。来源拆分为 AbstractToolset._collect_child_instruction_contributions 18 次、CombinedToolset.for_run_step 6 次、CombinedToolset.get_tools 6 次、CombinedToolset.for_run 1 次、Agent._resolve_run_capabilities 1 次。一个 awaitable 没有兄弟可以取消,却仍然被包进 anyio task group 并新开一个子任务。
做法是在 pydantic_ai/_utils.py 的 gather() 开头加一个分支:长度为 1 时直接 return [await coros[0]],在调用任务里 await。本文解包 2.51.0 的 wheel 核对了这段源码,docstring 里的原文是 A single awaitable has nothing to run alongside, so it is awaited directly in the calling task。多子项路径不变,仍然走 anyio task group 并在失败时取消其余子项。
官方数字口径为 FunctionModel 加 Python 3.14,3 轮各 300 次运行取最好:纯文本回复 1.75 毫秒降到 1.25 毫秒,1 次工具调用 3.20 毫秒降到 2.40 毫秒,5 次工具调用 7.90 毫秒降到 5.30 毫秒,流式文本 2.10 毫秒降到 1.80 毫秒。
这一条必须重点读的是它承认的兼容性影响:单子项 fan-out 的 ContextVar 隔离消失了。以前子任务里对 ContextVar 的修改对调用方不可见,现在子项设的 ContextVar 在返回后对调用方可见。作者同时说明,通过 Agent 使用时用户 toolset 与 _AgentFunctionToolset 相邻、用户 capability 与内置的 ToolSearch 与 PendingMessageDrainCapability 相邻,这些 fan-out 都多于一个子项,因此用户自己的 hook 仍然跑在各自的任务里。PR 描述里这条影响仍标注为 Maintainer decision pending,作者为此专门征询了另一位维护者的签字。
验证方式是作者给一个 toolset 的 get_tools() 和一个 capability 的 for_run() 各加了一个 ContextVar 探针,然后分别在 Agent.run()、iter() 与 run_stream() 三条路径上比对,main 与分支输出一致。已有测试覆盖 gather 的两个分支,包括一个失败时取消其余子项的行为;这个 PR 没有新增测试。
局限:基准同样跑在假模型上;这个改动只影响单子项路径,多子项的成本没有变化。前置部署 Forward Deployed Engineer(FDE)如果自己直接调用 CombinedToolset 或 CombinedCapability 且只传一个子项,就需要重新审计对子项设置过的 ContextVar,这是本次改动里唯一必须逐行读 PR 而不是读 release note 才能发现的点。
## 五、修复四:回放已完成文本流不再反复重拷增长中的前缀
根因由 Kludex 在 2026-09-25 开的 issue 8796 里说明:CompletedStreamedResponse(replay_events=...) 会为每一个回放的 PartDeltaEvent 重建增长中的文本前缀,路上 ModelResponsePartsManager.apply_event() 会 materialize 当前 part 再调用 TextPartDelta.apply(),而实时流路径本来就已经有一个字符串缓冲可用。
做法是在 pydantic_ai/_parts_manager.py 里新增 _buffer_string_delta(),只对精确的 TextPart 与 TextPartDelta 复用缓冲、只在真正读取时才 materialize;同时新增 _apply_metadata_or_copy_provider_details() 急切地应用元数据,以保留相互独立的快照。其他 part、自定义子类和非法事件顺序走原路径。
官方数字口径为 Graviton 上三组独立的匹配 baseline 与 head 对比:1000 个分片从 30.7 至 30.8 毫秒降到 19.0 至 19.3 毫秒,约少 37% 至 38%;5000 个分片从 609.8 至 695.9 毫秒降到 92.4 至 93.7 毫秒,约少 85% 至 87%。基准输入是 FunctionModel 的真实流事件在计时之外捕获,再以公开的已完成流接口回放 1000 或 5000 个 256 字符的分片。
作者主动写明了这条数字的弱点:大基线本身波动很大,1.25 MiB 那个场景即使拉长窗口仍会跨 job 变化,所以要用重复的匹配对比来验证改进,而不是相信单个百分比。这也是本次唯一一条把「测量方法的不确定度」写进结论的改动。
验证方式是 10 个新的兼容用例对两套实现都通过,覆盖中间读取、保留快照、元数据更新、稀疏索引、自定义 part 构造器与自定义 delta 方法、缺失 start 与取消;另有一个变异探针专门验证「条件克隆会从被保留的 start 事件里泄漏元数据」这条反例。作者明确排除了思考内容与工具参数的回放以及实时流路径。
## 六、修复五:实时会话终于能报上下文占用比例
改动前的问题是实时会话根本没法知道模型上下文有多满:RunContext.context_window_used 在会话内部是按这次 run 的模型与消息算的,而 GPT-Live 自己上报的 session.usage.updated 里的 context_window.usage_ratio 被直接丢掉了。
现在的接口有三层:codec.SessionUsage.context_window_used 是提供方上报的比例,属快照语义、绝不累加;RealtimeSession.context_window_used 取最新上报值,没有上报时才回落到标准公式,即最新一条 ModelResponse.usage.total_tokens 除以 profile 里的 context_window;RunContext.context_window_used 在设置了 realtime_session 时返回会话的值,普通 run 不受影响。
一个可复算的推导细节值得记下来:GPT-Live 的 profile 里 context_window 取 128000。OpenAI 文档没有写这个数,但每一条记录下来的 usage_ratio 都能换算成 128000 上的整数 token,例如 0.01021875 对应 1308、0.0103046875 对应 1319,因此上限是 128000 的整数倍。
新增的开关是 RealtimeModelProfile.response_usage_covers_context,默认 True。它在响应 token 并不能度量整个上下文时关掉派生比例,两个已知例外都是实测出来的:xAI 的 Grok Voice 的 input_tokens 是增量式的,作者在 grok-voice-latest 上跑了 5 个文本轮,第一个长轮报 908 个输入 token,之后分别是 10、5、5、10;GPT-Live 的 token 属于被委派的后端,它只回自己的比例。
局限由文档写明:这个比例会在服务端压缩或截断之后下降,而没有任何提供方会公告这件事,要用 openai_truncation 或 google_context_compression 去控制。本文未做实时链路的端到端验证,原因是这部分需要 openai 与 websockets 等可选依赖。
## 七、三个会打断现有代码的行为变更
第一个是强制 tool_choice 现在被拒绝。OpenAI、Azure OpenAI 与 xAI 的实时会话如果传 tool_choice 的 'required' 或一个工具名列表,现在会在连接之前抛 UserError。理由是这三家把 tool_choice 作用在整个会话上,强制选择会对每一个响应都生效,包括工具结果之后的那一个,因此模型永远没有机会回答。作者实测 gpt-realtime-mini 在 tool_choice 为 'required' 时会每次拿到工具结果后继续调用工具,直到 request_limit 等于 6 结束会话。迁移办法是改用 ToolOrOutput(function_tools=[...]),需要按运行上下文限制时用过滤过的 toolset 或 prepare_tools。'auto'、'none'、显式 None 与非强制值完全不变且不告警;Gemini Live 没有 tool_choice 配置,行为不变。
第二个是 Gemini Live 的错误类型与模型匹配。WebSocket 的 close code 以前会落进 ModelHTTPError.status_code,现在 1000 及以上的码改抛 RealtimeError("Gemini Live connection closed: …"),携带真实 HTTP 状态码的 APIError 仍然映射到 ModelHTTPError。迁移办法是改 catch RealtimeError,或者 catch ModelAPIError 一次覆盖两种。同时 gemini-3.8-live 从精确 id 匹配改为前缀匹配:此前日期版、-preview 或 @ 快照会拿到 supports_thinking=True,于是把 thinking 设置作为模型不接受的等级发出去并收到 1007,也拿不到 3.8 的工具调用标志。另外 google_affective_dialog=True 在 Gemini 3.1 Flash Live 与 3.8 Live 上改为建连前抛 UserError,实测 gemini-3.1-flash-live-preview 的握手直接被 1007 Request contains an invalid argument 拒绝,而 gemini-3.8-live 与 -extended-thinking 会先打开会话再在第一次发送时以同一个 1007 关闭。
第三个是新增 OpenAILiveModel。GPT-Live 是与 Realtime API 并列的另一套协议,因此 openai:gpt-live-1 会在 openai 前缀内按模型名路由到新的 OpenAILiveModel。本文解包 2.51.0 的 wheel 确认了这一点:pydantic_ai/realtime/openai_live.py 在 2.50.0 里不存在,2.51.0 里是 66417 字节的新文件。协议层有三个实测出来的硬约束:没有 end-of-turn 帧,连接在模型静音到 openai_live_turn_silence_ms 之后自己合成 ResponseDone;输出音频轨在两次回复之间是空闲的,只有高于 -54 dBFS 才算作说话;文本超过 Live 的 500 token 上限(o200k_base,实测)会在发送之前报错。后端模型默认取 auto 对应的 gpt-6-sol。客户端委派模式在这个版本里没有接线。
## 八、本文的本地复现:口径与官方数字不同
本节数字由本文在 2026-09-27 于本机(macOS,Python 3.13.12)用同一个脚本分别在 pydantic-ai-slim 2.50.0(即 v2.50.0)与 2.51.0(即 v2.51.0)上实测,模型用 FunctionModel,口径为 3 轮各 300 次运行取最好值。
前置部署 Forward Deployed Engineer(FDE)可以把这套脚本原样抄到客户环境里再跑一遍:它不依赖任何真实模型凭据,FunctionModel 只回一个固定的文本或工具调用,因此可以在物理隔离环境里安全复现。
三次升级前后对照:纯文本回复 2.165 毫秒降到 1.189 毫秒,少 45.1%;1 次工具调用 3.594 毫秒降到 2.038 毫秒,少 43.3%;5 次工具调用 8.894 毫秒降到 5.440 毫秒,少 38.8%。
cProfile 数 dataclasses.replace 的调用:2.50.0 上每次运行 65.0 次、每次运行耗时 0.0402 毫秒;2.51.0 上每次运行 23.0 次、每次运行 0.0155 毫秒。调用次数少 64.6%,耗时少 61.4%。
这里必须提醒口径差异:官方的 17%、27.36%、1.75 降到 1.25 这几个数,是每个 PR 单独合入时各测各的;本文的 45.1% 是四处改动一起生效之后的累积效应,两个口径不能直接相减或互相校验。另外本文在 65 次与官方 issue 里的 79 次之间有差额,原因是两次统计的 agent 配置与 hook 数量不同,本文用的是最简 agent 加一个 tool_plain 工具。
一处能对上的交叉验证:本文实测 RunContext.__dataclass_fields__ 的长度正好是 43,与 issue 8587 里给出的 43 个 dataclass 字段一致。
## 九、局限与未覆盖的场景
前置部署 Forward Deployed Engineer(FDE)在向客户承诺延迟指标之前,应把下面这几条边界写进 SOW 而不是口头说明,因为其中任何一条被忽略都会让复现结果对不上。
三类官方基准都跑在假模型 FunctionModel 上,测的是 agent 循环的固定开销,不含任何真实推理时间。真实模型动辄几百毫秒到几秒,这四处改动带来的绝对收益会被完全淹没,不要把这些数字当成端到端延迟承诺。
本文的本地复现只覆盖 macOS 加 Python 3.13.12 加单机单进程,没有覆盖官方的 Python 3.14、没有覆盖 Graviton 之类的服务器 CPU、也没有覆盖 Temporal、DBOS、Prefect 这些持久化执行后端;官方说明里 2.51.0 在 Python 3.13 上的 tests/durable_exec 与 DBOS 用例有失败,但与 main 完全相同、属本地环境问题。
PR 8774 的 ContextVar 隔离变化在 PR 描述里仍标为维护者决定待定,这是本次唯一一条可能改变你现有 hook 语义的改动,升级前应自行审计。
实时相关的改动需要 openai 与 websockets 等可选依赖,本文没有做实时链路的端到端验证,第二、六、七节里的实时数字全部来自官方 PR 描述与 issue,未经本文复跑。
作者自己承认的未覆盖项还有:GPT-Live 的客户端委派模式未接线;PR 8799 明确把思考内容回放、工具参数回放与实时流路径排除在改动之外;PR 8775 只对精确类型 RunContext 做浅拷贝,子类仍然付重建成本。
## 十、实用结论:谁该行动、怎么做
前置部署 Forward Deployed Engineer(FDE)先用一句话自测该不该升级:把你的 agent 换成 FunctionModel 跑 300 次,如果这个数字在你的端到端延迟里连 1% 都占不到,就跳过这一版;如果它占到几个百分点,下面的清单才值得执行。
该行动的第一类人是在客户环境里把 agent 挂在低延迟模型上的工程师。如果你的端到端延迟里模型只占几十到一两百毫秒,这四处改动可能直接砍掉其中相当一部分;如果模型本身要几秒,先别动这里。
该行动的第二类人是做 LLM 评估与流水线重放的工程师。PR 8799 的收益随分片数放大,本稿引用的口径是 5000 个分片少 85% 至 87%,回放型评测脚本是直接受益者。
该行动的第三类人是使用实时 API 的团队。升级前先做三件事:在自己的代码里搜 tool_choice,确认没有对 OpenAI、Azure OpenAI、xAI 传强制值;搜 ModelHTTPError 的 catch,确认没有依赖从 status_code 读 WebSocket close code;确认没有依赖工具子任务对 ContextVar 的隔离。
具体升级命令是 pip install -U "pydantic-ai-slim==2.51.0"。回归方法照抄官方口径即可:用 FunctionModel 跑 3 轮各 300 次取最好值对比升级前后的 Agent.run() 墙钟,再用 cProfile 数 dataclasses.replace 的次数。
回滚方式很干净:固定回 pydantic-ai-slim==2.50.0 即回到旧行为,本文实测 2.50.0 与 2.51.0 都能在 Python 3.13.12 上正常安装与运行,因此回滚不需要改代码。
适用条件与不适用条件要一起写进交付文档:这版值得升级的前提是循环开销在你的延迟预算里可见,且你没有依赖上面三个行为变更里的旧行为;如果你的交付环境已经冻结依赖版本,把这条记进升级清单即可,不必为它单独做一次发版。
## 十一、与本站历史条目的差异
这是同领域不同平台的条目,与本站 2026-09-26 发布的 Google ADK Python 2.10.0 那条是两件事:两者日期接近但平台不同,ADK 那条讲的是 Google 自家 agent 框架的版本内容,本条讲的是 pydantic-ai 这个独立框架的运行时开销与服务端接口收紧。
与本站 2026-09-24 发布的 LangGraph CLI 0.4.32 同样不重合,那条讲的是自托管部署参数。三条的内容面没有交集,本条也不是任何一条的性能回退补丁。
## 原始来源
原始来源:
https://github.com/pydantic/pydantic-ai/releases/tag/v2.51.0
https://github.com/pydantic/pydantic-ai/pull/8658
https://github.com/pydantic/pydantic-ai/pull/8775
https://github.com/pydantic/pydantic-ai/issues/8587
https://github.com/pydantic/pydantic-ai/pull/8774
https://github.com/pydantic/pydantic-ai/pull/8799
https://github.com/pydantic/pydantic-ai/issues/8796
https://github.com/pydantic/pydantic-ai/pull/8803
https://github.com/pydantic/pydantic-ai/pull/8761
https://github.com/pydantic/pydantic-ai/pull/8755
https://github.com/pydantic/pydantic-ai/pull/8390
评论(0)
- 暂无评论。