升级 AI SDK 时,最危险的往往不是 resolver 报错,而是依赖安装成功,运行时才暴露类型不兼容。
核心判断:Anthropic SDK 1.0 切换到 httpx2 后,Pydantic AI 至少要升级到 v2.33.0;项目里的自定义 Client、APM 和 Mock 还要逐项复查。

一、安装成功为什么仍会炸在构造阶段
Anthropic Python SDK 1.0.0 于 2026-08-20 发布,HTTP 层从旧 httpx 迁移到 httpx2。Pydantic AI 官方说明:v2.32.2 及更早版本允许解析到 anthropic 1.0.0,却没有完成对应适配。
于是会出现一个反直觉组合:新环境安装成功,第一次构造 Anthropic Provider 才抛出 TypeError。
| 组合 | 构造结果 | 判断 |
|---|---|---|
2.32.2 + Anthropic 1.0.0 |
失败 | 版本解析不是运行门禁 |
2.33.0 + 默认 Client |
成功 | 最小可用组合 |
2.33.0 + httpx.AsyncClient |
失败 | 自定义 Client 未迁移 |
2.33.0 + httpx2.AsyncClient |
成功 | Client 类型正确 |
先看失败发生在哪一层
同一个升级问题,可能在四个位置露头。resolver 直接报版本冲突,属于安装层;Provider 构造时报 Client 类型不对,属于适配层;真正请求时代理、TLS 或 Transport 失败,属于网络层;请求正常但 APM 与 Mock 没有记录,属于观测和测试层。
这几层不能混着排。安装问题看锁文件,构造问题看对象来源,网络问题看代理与证书,观测问题看 instrumentation。把所有异常统称为"SDK 不兼容",常见结果是来回升级版本,却没有碰到真正的断点。
开发机没复现也不能马上否定问题。本地环境可能保留旧锁文件和已安装包,CI 与生产镜像却会从空环境重新解析。恰恰是这种"旧环境正常、新环境失败"的差异,说明只看安装命令退出码不够。
二、把版本配对写进锁文件
bash
uv add "pydantic-ai>=2.33.0" "anthropic>=1,<2"
暂时留在旧版 Pydantic AI 时,官方止血方案是固定 anthropic<1。此外,Anthropic SDK 1.0 的最低 Python 版本从 3.9 提高到了 3.10。
过关标准不是 uv sync 成功,而是全新环境完成安装后还能构造 Provider。
三、显式 Client 必须来自 httpx2
python
import httpx2
from pydantic_ai.providers.anthropic import AnthropicProvider
provider = AnthropicProvider(
api_key="test-key",
http_client=httpx2.AsyncClient(timeout=30.0),
)
继续传 httpx.AsyncClient() 会得到明确的类型错误。普通的浮点超时和重试次数大概率不受影响;显式 Client、Transport、Timeout、Limits 与 Proxy 类型才是迁移重点。
Client 背后的配置比 import 更重要
项目之所以自建 Client,通常是为了共享连接池、走企业代理、挂自签名证书,或者统一并发和超时。把 import 改成 httpx2 后,这些配置仍要在沙箱里逐项验证。
先确认生命周期:应用启动时创建的共享 Client,应在应用退出时关闭;不要在单次 Agent 调用后把整个连接池关掉,也不要把已经退出 async with 的 Client 留给 Provider。再确认代理和证书路径。配置对象"看起来一样"不代表请求真的经过了同一路由,最好让沙箱端回显来源,或者在代理侧看到请求证据。
超时也不要压成一个总数字。连接、读取、写入和等待连接池各自对应不同故障。流式输出更容易遇到读取超时,高并发更容易遇到连接池等待;如果升级后把它们都当成"模型慢",重试会进一步放大压力。
多个 Provider 共用 HTTP 工厂时,工厂最好显式区分旧 httpx 与 httpx2 返回类型。一个含糊的"通用 Client"虽然省了代码,类型错误也更容易跨服务传播。
四、观测和测试可能静默失效
OpenTelemetry HTTPX instrumentation、Sentry HTTPX integration、respx、pytest-httpx 或 vcrpy 可能还在 patch 旧 httpx,而 SDK 1.0 已经不再从那里发请求。
所以要主动验证:
- APM 能否看到请求与耗时;
- Mock 是否真正拦截,而不是测试意外走网;
isinstance和类型注解是否已改为httpx2对象;- 自定义 hook 是否收到预期类型。
应用确实需要兼容旧导入时,可以在入口最顶部、任何 httpx 导入之前执行:
python
import httpx2
httpx2.alias_httpx()
不要在库里替用户做全局 alias,也不要在 httpx 已导入后再调用。
为什么我更担心 Mock 没拦住,而不是测试报错
测试直接红灯,至少问题很诚实。Mock 没有拦住请求、测试却继续跑,才是迁移中更危险的状态。
一种情况是环境里没有真实 Key,外部请求返回鉴权错误,而测试只断言"出现异常",于是错把网络错误当成业务分支。另一种情况更糟:开发机或 CI 环境意外带着可用凭据,测试真的访问了外部服务,产生不稳定结果和额外调用。
迁移后的测试除了断言业务输出,还应断言替身接收到了预期次数、路径和请求体,并默认禁止外网。APM 也不能只看应用日志,要确认 Span 仍包含耗时、状态与 request id。
异常处理里的 isinstance 同样值得搜。旧代码可能只在 httpx.Response 分支提取状态码;收到 httpx2.Response 后落入宽泛兜底,接口仍返回错误提示,但关键诊断信息已经丢了。这类退化不会在安装或构造阶段提醒你。
五、四组本地构造实验
本次用 uv 0.8.3 创建隔离环境,只使用假的 API Key 构造 Client,不发送模型请求:
text
2.32.2 + anthropic 1.0.0 + default client → TypeError
2.33.0 + anthropic 1.0.0 + default client → constructed
2.33.0 + anthropic 1.0.0 + httpx.AsyncClient → TypeError
2.33.0 + anthropic 1.0.0 + httpx2.AsyncClient → constructed
这能证明构造边界,不足以证明真实模型调用、流式响应、重试、代理、Bedrock 或生产观测已经通过。
六、alias_httpx() 不是默认答案
迁移指南给出了 httpx2.alias_httpx(),它确实能让后续的 import httpx 指向 httpx2。但这个开关影响的是整个进程,不只是 Anthropic 调用。
如果服务里还有对象存储、支付、搜索、内部网关等 SDK,它们可能共享旧 httpx,也可能在启动时做严格的模块或类型判断。贸然 alias,表面上少改了几个 import,实际把回归范围扩到了所有 HTTP 调用。
我更建议先分三种情况:
- 没有自定义 Client:直接升级 Pydantic AI,先用默认实现;
- 只给 Anthropic 单独建 Client:显式改成
httpx2.AsyncClient; - 多个 SDK 共享 Client:先评估是否应该拆开连接池,再决定是否全局 alias。
所谓"drop-in continuation"说的是主要 API 兼容,不等于所有 instrumentation、monkey patch 和 isinstance 判断都天然兼容。迁移时真正要控制的是影响半径,而不是代码改动行数。
七、别只搜 Provider,HTTP 工厂更值得查
在一个维护时间稍长的 Python 项目里,Client 往往不在调用点创建,而是在公共工厂、依赖注入容器或启动钩子里统一生成。只搜 AnthropicProvider 很容易漏掉真正的风险点。
可以先跑三组只读搜索:
bash
rg -n "AnthropicProvider|AsyncAnthropic|http_client=" .
rg -n "import httpx|from httpx|HTTPXClientInstrumentor" .
rg -n "respx|pytest_httpx|vcrpy|isinstance\(.*httpx" tests src
第一组回答"谁调用 Anthropic",第二组回答"Client 从哪里来、谁观察它",第三组回答"测试到底拦截了什么"。不要把所有 import httpx 都批量替换:业务自己的普通 REST 调用可以继续留在旧包,只有进入 Anthropic SDK 的对象必须满足新类型契约。
还要搜 Dockerfile、CI Workflow 和部署脚本中的版本字符串。依赖升级最常见的分裂不是代码没改,而是本地锁文件已经更新,镜像构建却仍从另一条命令安装旧版本。
八、一个值得放进 CI 的构造探针
这段代码只构造 Provider,不读取真实 Key,也不会发送网络请求:
python
from importlib.metadata import version
import anyio
import httpx2
from pydantic_ai.providers.anthropic import AnthropicProvider
async def main() -> None:
async with httpx2.AsyncClient(timeout=30.0) as client:
AnthropicProvider(
api_key="construction-probe",
http_client=client,
)
print(
"provider-ready",
version("pydantic-ai"),
version("anthropic"),
type(client).__module__,
)
if __name__ == "__main__":
anyio.run(main)
它的价值很具体:CI 每次从锁文件重建环境后,都会验证一次真实解析结果,不再依赖"评审时大家都记得这个版本坑"。
不过它仍然只是第一层。下一层应该用测试账号或沙箱跑一条最小请求,确认代理、TLS、重试、流式消费和 APM 都工作。构造探针不该访问网络,集成测试也不该访问生产账号,两条边界都要守住。
这条探针必须失败关闭。版本读取失败、Client 类型错误或 Provider 无法构造,都应该让 CI 退出非零,而不是留下黄色警告继续发布。日志只打印版本与模块名,别把 Key、代理地址和完整 Client 配置打出来。
门禁写完后,可以在独立分支做一次反向验证:临时把 httpx2.AsyncClient 改回旧 httpx.AsyncClient,确认流水线真的会红,再恢复。很多"检查脚本"从没证明自己能抓住回归,这一步能把形式检查变成真实门禁。
如果项目有多套锁文件或镜像,别只跑开发环境那一套。在线服务、离线任务、不同 Python 版本都可能解析出不同依赖组合,探针应跟着实际交付单元走。
九、升级窗口里的回退,不要靠临时命令
如果线上出现构造错误,最小回退是恢复旧锁文件,或者在无法升级 Pydantic AI 的前提下固定 anthropic<1。仅在某台机器手工执行一次安装命令不算回退:自动扩容、滚动重启或下一次构建仍会再次解析到不兼容组合。
回退条件也别写成含糊的"有问题就回退"。Provider 构造失败可以直接回退;沙箱请求失败要先区分网络、鉴权还是 SDK;如果请求成功但 APM 完全看不到链路,应暂停继续扩容,因为可观测性缺口会让后续故障更难判断。
把解析版本、异常类型、Client 的模块来源和观测结果一起留下。下次再开升级窗口时,这些信息比一条"上次升级失败"的备注有用得多。
十、先确认交付物里到底装了什么
Python 项目里至少有四个"版本真相":依赖声明、锁文件、当前虚拟环境和最终容器镜像。它们经常不是同一个状态。pyproject.toml 写了新版本,不代表锁文件已经重算;开发机里的虚拟环境同步成功,也不代表 Docker 构建没有复用旧缓存;镜像里版本正确,还可能被启动脚本里的临时安装命令覆盖。
因此,升级验收不要只贴一段依赖文件差异。至少要从交付环境打印一次 pydantic-ai、anthropic、httpx2 和 Python 的实际版本,并把这段输出留在流水线产物里。这里说的是版本号与模块来源,不包括环境变量、代理地址或任何凭据。
如果项目使用 uv,可以先看依赖树,再在镜像内读已安装元数据。依赖树回答"为什么会选中这个版本",运行时元数据回答"进程最后加载了哪个版本"。两者不一致时,优先查构建层缓存、工作目录、可编辑安装和启动脚本,不要继续改版本区间碰运气。
多服务仓库还要注意锁文件边界。Web 服务、异步 Worker、定时任务和数据脚本可能共享源码,却使用不同镜像或不同 extras。只在主服务跑构造探针,不能证明后台任务也安全。最小做法是按实际部署单元列出矩阵:Python 版本、锁文件、镜像、是否调用 Anthropic、是否注入自定义 Client。没有调用 Anthropic 的单元不必强行升级,但真正调用的一项都不能漏。
版本证据最好由机器生成,而不是人工填写。人工表格容易在第二次升级后过期,CI 输出则与当次构建绑定。发布记录只保留关键摘要和构建链接,详细包清单留在构建产物中,既方便追溯,也不会把长日志塞进文章或变更说明。
十一、依赖注入容器里最容易藏着旧 Client
很多代码在业务层看不到 httpx.AsyncClient。Provider 只接收一个已经组装好的对象,而这个对象来自 FastAPI lifespan、依赖注入容器、公共 SDK 工厂或测试 fixture。审查者如果只看调用点,会以为项目使用默认 Client,真正运行时却仍然注入旧类型。
排查时可以沿对象所有权反向走一遍:谁创建 Client,谁把它交给 Provider,谁负责关闭,测试又在哪里替换它。每一跳都要能回答具体对象类型。仅把工厂函数的返回注解改成 Any 或协议类型,只会让静态检查安静下来,运行时契约仍然没有变化。
共享 Client 的生命周期尤其需要明确。应用级 Client 通常在进程启动时创建,在进程退出时关闭。请求级依赖如果每次都建新 Client,会失去连接池复用;相反,把短生命周期上下文中的 Client 放进单例 Provider,又会得到一个已经关闭的对象。升级前这些问题可能碰巧没暴露,修改工厂后时序一变,就会以"偶发连接失败"的形式出现。
比较稳妥的工厂接口不是"返回一个通用异步 HTTP 客户端",而是按消费者表达意图。例如,普通内部 REST 调用返回旧 httpx.AsyncClient,Anthropic Provider 返回 httpx2.AsyncClient。即使两个库的大部分 API 相似,也不要为了少写一个函数把类型揉在一起。明确的工厂边界能让类型检查、单元测试和关闭逻辑都更直接。
fixture 也要跟着真实工厂改。测试中如果直接 monkey patch Provider 构造函数,可能绕过了线上实际使用的 Client 工厂。更有效的测试是在工厂出口检查模块名和实例类型,然后把这个对象传入真实 Provider 构造路径。这样既不访问网络,也能抓住"业务代码改了、测试替身还停在旧实现"的分裂。
十二、重试、超时和取消要作为一组检查
HTTP Client 迁移后,最容易被忽略的不是"有没有 timeout",而是超时、重试与任务取消之间的组合。应用层可能有一次重试,SDK 自己也有重试,网关还有第三层重试。每层单看都很保守,叠加后却可能把一次用户请求放大成多次模型调用。
先画出实际重试链:谁触发第一次重试,哪些异常会被重试,退避多长时间,总预算是多少。不要只比较配置文件里的 max_retries,还要观察一次故障最终产生多少出站请求。迁移后异常类型或继承关系发生变化时,旧的捕获条件可能失效,也可能把原本不重试的错误纳入宽泛兜底。
超时同样需要从用户预算倒推。假设上游只愿意等待六十秒,连接、排队、首字节、流式读取和应用重试不能各自拿满六十秒。连接池等待过长会把容量问题伪装成模型延迟;流式读取预算过短,又会在模型仍正常输出时主动断开。建议把各阶段预算写成可观察配置,并在沙箱故意制造连接失败、慢响应和取消,确认异常落在预期层。
异步取消值得单独测。用户关闭页面、上游网关超时或任务组取消时,流式响应、Client context 和连接池都应被正确释放。只验证完整响应成功,无法发现取消路径泄漏连接。可以在测试里消费几个 chunk 后主动取消,再检查任务结束、连接关闭和下一次请求是否仍能正常获取连接。
重试还涉及副作用。模型生成请求通常不是数据库写入,但它会产生费用、配额占用和审计记录。不要默认"网络错误就可以无限安全重试"。如果调用链支持幂等标识,应按官方能力显式使用;如果不支持,就记录尝试次数并限制总预算。文章没有验证 Anthropic 1.0 在所有接口上的幂等行为,因此这里是工程门禁建议,不是 SDK 的官方保证。
十三、把离线门禁和联网门禁分开
一个测试既想验证类型,又想验证真实网络,最后往往两边都做不好。更清晰的做法是拆成离线构造门禁和联网沙箱门禁,它们有不同的失败含义,也有不同的凭据边界。
离线门禁不读取真实 Key,只验证锁文件解析、Client 类型、Provider 构造、生命周期和 Mock 接线。它应该快、稳定,并在每次提交运行。任何外网访问都应被默认禁止;如果测试替身没有拦住请求,测试要立即失败,而不是等待网络超时。
联网门禁使用独立测试账号或沙箱凭据,发送一条成本受控的最小请求。它验证 DNS、代理、TLS、鉴权、真实响应和 APM 链路,不承担业务质量评测。请求内容固定、输出上限明确、并发为一,失败时记录状态类别和请求标识,不能把完整凭据或敏感响应写进日志。
流式调用需要另一条用例。普通请求成功,不代表流式 context、chunk 消费、取消和连接归还都正确。最小流式门禁至少消费到首个有效片段,再正常结束;取消用例则在收到部分数据后主动停止,确认没有悬挂任务。两条都通过,才说明迁移覆盖了最常见的异步路径。
Mock 与真实请求不能在同一用例里模糊切换。测试配置如果缺少凭据就悄悄改走 Mock,会让 CI 绿灯却没有执行预期链路;配置有凭据就突然访问网络,也会破坏可重复性。应让测试名称、标记和启动参数明确说明当前是 offline 还是 sandbox,缺少沙箱条件时报告跳过原因,不能伪装成通过。
这两层门禁提供的是不同证据:离线层证明代码契约,联网层证明环境契约。生产可用性还需要灰度和运行指标,不能因为沙箱请求成功就跳过发布观察。
十四、观测链路要验证"内容",不只是有没有 Span
APM 页面里出现一条 Span 只是第一步。迁移前后的 Span 名称、服务归属、状态码、耗时、重试次数和请求标识都要能对应,否则链路虽然"有数据",排障价值已经下降。
建议在升级前保存一条去敏后的基线:一次正常请求会经过哪些服务,父子关系是什么,关键标签有哪些,错误如何标记。升级后用相同沙箱场景对比。重点不是追求字段完全不变,而是确认值班时真正需要的信息仍然存在,并且没有把 Key、完整提示词或响应正文意外采集进遥测。
错误场景至少覆盖三类:Provider 构造失败、网络或鉴权失败、流式过程中断。构造失败通常还没有 HTTP Span,需要应用层日志给出依赖版本和 Client 模块来源;网络失败应该在出站 Span 留下可分类状态;流式中断则要同时看到请求耗时和取消结果。如果所有错误最终只剩一个"调用失败",迁移就没有通过可观测性门禁。
指标也能发现单次测试看不到的问题。升级窗口可以关注出站请求数与业务请求数的比例、重试次数、连接池等待、首字节时间、完整响应时间、错误类型分布和取消数量。只看总体成功率,可能错过重试放大和观测缺失;只看平均延迟,又会把少量长尾藏起来。
日志关联要经过实际回读。应用日志里的 request id、APM trace id 和供应商返回的请求标识如果无法互相定位,出现故障时仍要靠时间范围猜测。沙箱门禁可以断言这些关联字段存在,但不要断言具体值,以免把一次运行的数据硬编码进测试。
这里同样要标边界:本文没有对具体 OpenTelemetry、Sentry 或 Mock 插件给出"已经兼容 httpx2"的结论。插件版本变化很快,发布当天应查看对应项目文档和 Release,再以真实回读为准。文章提供的是验证方法,不替插件维护者宣布兼容。
十五、灰度发布时先控制兼容半径
这类升级适合小步灰度,不适合把依赖、HTTP 工厂、全局 alias、重试策略和观测插件一次性全部换掉。改动混在一起后,即使指标异常,也很难判断是哪个层造成的。
第一步只更新锁文件与 Pydantic AI 适配,并在没有自定义 Client 的路径上跑构造门禁。第二步迁移 Anthropic 专用 Client 和生命周期。第三步修正 Mock、类型判断与 instrumentation。最后才评估是否真的需要全局 alias。每一步都保留独立回退点,失败时可以缩小定位范围。
如果服务支持按实例或流量开关选择 Provider 工厂,可以先让少量实例使用新 Client。灰度期间同时观察功能、请求数量、连接池、重试和 APM。不要只验证"有回答返回",因为最隐蔽的退化恰恰发生在观测和测试层。
回退动作要在发布前演练到命令和构建产物级别。恢复旧锁文件后重新构建镜像,比进入运行容器临时安装包可靠;前者能保证下一次扩容仍是一致版本,后者只修改了单个现场。若紧急固定 anthropic<1,也应提交到依赖声明和锁文件,并注明这是临时止血与解除条件。
全局 alias_httpx() 的灰度尤其困难,因为它改变整个进程的模块解析结果,无法只影响一小部分请求。除非已经列清同进程所有 HTTP 消费者并完成回归,否则更适合显式迁移 Anthropic 专用 Client。少改几行代码不等于更小风险,影响范围才是判断依据。
灰度结束的标准也要提前写明:新组合稳定运行一个约定观察窗口;构造、普通请求、流式和取消用例通过;错误率、重试、延迟和请求放大没有异常;APM 与日志关联可用;旧版本回退入口仍然存在。达到这些条件后再扩大流量,比"发布后十分钟没报警"更有意义。
十六、让迁移 PR 保留可审查的证据
一个容易审查的迁移 PR,最好把事实、代码和验证结果分开呈现。描述开头先写官方变化和受影响版本,再列仓库内真实命中点,最后给出验证矩阵。不要用"升级若干依赖、修复兼容问题"一句话概括全部风险。
代码差异可以按责任层拆分:依赖与锁文件、自定义 Client 工厂、测试与观测、发布门禁。是否真的拆成多个提交取决于团队习惯,但审查说明应保留这个结构。这样依赖维护者能看版本,平台工程师能看生命周期与代理,测试维护者能看 Mock,值班人员能看指标和回退。
验证证据至少包含旧组合失败、新组合构造成功、继续注入旧 Client 失败、注入 httpx2.AsyncClient 成功。它们共同证明门禁既能放行正确组合,也能拒绝错误组合。只有一张"流水线绿了"的截图,无法说明检查脚本是否抓得住回归。
如果联网沙箱还没跑,要明确写成待办和发布阻塞,不能把离线构造结果扩写成"完整兼容"。如果 APM 插件尚未确认,也应列出负责人、验证场景和回退条件。边界写得清楚不会削弱 PR,反而让审查者知道哪些结论可以依赖。
合并后把同样的探针保留在 CI,不要验证一次就删除。依赖解析会随新 Release 继续变化,今天的修复可能在下一个次版本被重新触发。长期门禁的维护成本很低,却能把"大家记得不要这么配"变成机器能够执行的契约。
十七、别只改两行依赖
更稳妥的 CI 门禁是:
- 拒绝旧 Pydantic AI 与 Anthropic 1.x 的错误组合;
- 同时构造默认 Client 和项目自定义 Client;
- 用沙箱请求确认 APM 与 Mock 没有断线。
没有自定义 HTTP Client、没有基于 httpx 的观测与 Mock,升级面会小很多;反过来,就不能把"安装成功"当作完成。
十八、升级检查表
-
pydantic-ai>=2.33.0与anthropic>=1,<2已锁定; - Python 版本至少为 3.10;
- 自定义 Client / Transport 全部来自
httpx2; - APM、Mock、VCR 与
isinstance判断已复查; - CI 会执行 Provider 构造;
- 沙箱请求覆盖重试、流式和观测链路。
官方来源:Pydantic AI v2.33.0 Release、Anthropic Python SDK v1 Migration Guide。
验证边界:事实核验于 2026-08-24;本地仅验证无网络 Client 构造,未发送真实模型请求。