依赖升级是每个团队都躲不掉的债。这次我用CodeArts开源升级子智能体,把一个Flask服务从2.2.5升级到3.1.3,跨两个大版本,自动发现4个不兼容点、逐条给出官方变更依据、修复后3/3测试通过。本文完整复盘这次升级。
一、引子:为什么依赖升级总被无限期搁置
打开任何一个跑了两年以上的项目,requirements.txt里大概率躺着一堆"远古版本"。不是团队不想升,而是升级的成本收益太难算:
- 升级收益:安全修复(CVE)、性能改进、新特性——看不见摸不着
- 升级成本:Breaking Changes埋在几百行CHANGELOG里,谁知道哪个会炸?升级→测试→回滚的循环能吃掉一周
- 最要命的:改版本号只要1分钟,但找出所有被移除的API并逐一改写,靠人肉翻文档,既慢又容易漏
于是升级被无限期搁置,直到某个CVE被公开利用、某个安全审计不通过、或者某个新特性必须用时,才被迫突击。
华为云码道(CodeArts)的开源升级子智能体(opensource-upgrade)专门解决这个问题:给定一个项目,它能自动调研目标版本的Breaking Changes、逐文件扫描不兼容API、生成修复代码、并编译验证。这次我用一个真实的Flask旧项目实测了全流程。
二、升级对象:一个"祖传"Flask服务
我构造了一个典型的存量项目 /workspace/upgrade-demo/:
upgrade-demo/
├── requirements.txt # flask==2.2.5 / werkzeug==2.2.3 / requests==2.28.2
├── app.py # 内容管理服务:4个接口
└── test_app.py # 功能测试:3个用例
这个项目用了4个Flask 2.x时代的典型API,它们在新版本中全部被移除或改变:
# app.py — 旧版API用法(升级前)
# 1. 自定义JSON编码器:Flask 2.3移除了JSONEncoder
from flask.json import JSONEncoder
class CustomJSONEncoder(JSONEncoder):
def default(self, obj):
if isinstance(obj, (datetime.date, datetime.datetime)):
return obj.isoformat()
if isinstance(obj, decimal.Decimal):
return float(obj)
return super().default(obj)
# 2. 挂载方式:app.json_encoder 属性在2.3被移除
app.json_encoder = CustomJSONEncoder
# 3. 首次请求前初始化:before_first_request 在2.3被移除
@app.before_first_request
def init_app():
print("[init] 应用首次请求前初始化完成")
# 4. HTML转义:flask.escape 在3.0被移除
from flask import escape
return "输入内容: {}".format(escape(text))
升级前基线:服务正常启动,3个功能测试全部通过:
[PASS] /api/order 返回自定义类型JSON
[PASS] /api/quote HTML转义正常
[PASS] /api/error 返回404
全部测试通过
有趣的是,Flask 2.2.5自己在启动时就已经发出警告:
DeprecationWarning: 'app.json_encoder' is deprecated and will be
removed in Flask 2.3. Customize 'app.json_provider_class' or
'app.json' instead.
——弃用警告就摆在那里,但没人有精力去处理。这就是存量项目的常态。
三、升级目标:跳到最新稳定版
| 组件 | 升级前 | 升级后 | 版本跨度 |
|---|---|---|---|
| flask | 2.2.5 | 3.1.3 | 跨2个大版本 |
| werkzeug | 2.2.3 | 3.1.9 | 跨2个大版本 |
| requests | 2.28.2 | 2.34.2 | 大版本内升级 |
任务交给CodeArts开源升级子智能体:
子智能体: opensource-upgrade
输入: /workspace/upgrade-demo/ 项目目录
任务: flask 2.2→3.x、werkzeug 2.2→3.x、requests 2.28→最新
要求: 修改版本锁定 + 适配代码 + 安装验证 + 功能测试全通过
四、子智能体做了什么
4.1 第一步:调研Breaking Changes
子智能体首先查阅了各库的官方CHANGELOG,整理出与本项目相关的移除清单:
| # | 被移除的API | 移除版本 | 官方CHANGELOG依据 |
|---|---|---|---|
| 1 | flask.json.JSONEncoder |
Flask 2.3.0 | "the corresponding json.JSONEncoder and JSONDecoder classes, are removed [#4995]" |
| 2 | app.json_encoder 属性 |
Flask 2.3.0 | "json_encoder and json_decoder attributes on app and blueprint... are removed [#4995]" |
| 3 | @app.before_first_request |
Flask 2.3.0 | "The app.before_first_request... decorators are removed [#4995]" |
| 4 | from flask import escape |
Flask 3.0.0 | "Import them directly from markupsafe instead [#4996]" |
注意这个细节:4个API里有3个在2.3.0就被移除了——也就是说,从2.2.5升到3.1.3,最陡的坎不在最后一步,而在中间。这种"跨多个大版本时Breaking Changes层层叠加"的情况,正是人肉升级最容易翻车的地方。
4.2 第二步:实测确认不兼容(不是猜的)
子智能体没有只靠文档判断,而是用三轮验证法实测:
| 轮次 | 环境 | 结果 |
|---|---|---|
| 第一轮 | 旧版本 + 原代码 | ✅ 导入成功(基线成立) |
| 第二轮 | 新版本 + 原代码 | ❌ ImportError: cannot import name 'JSONEncoder' from 'flask.json' |
| 第三轮 | 新版本 + 修复后代码 | ✅ 导入成功 → 测试通过 |
第二轮的失败是关键证据:先让旧代码在新版本上真实报错,确认每个不兼容点确实存在,再针对性修复。这比"照着文档改完就上"靠谱得多。
4.3 第三步:逐条修复
① JSON编码器 → JSON Provider(官方迁移路径)
Flask 2.3用Provider机制替代了Encoder机制:
# 修复后
from flask.json.provider import DefaultJSONProvider
class CustomJSONProvider(DefaultJSONProvider):
def default(self, obj):
if isinstance(obj, (datetime.date, datetime.datetime)):
return obj.isoformat()
if isinstance(obj, decimal.Decimal):
return float(obj)
return super().default(obj)
app.json = CustomJSONProvider(app)
子智能体还发现了一个文档里不会明说的行为差异:新版的DefaultJSONProvider默认把datetime序列化成HTTP日期格式(Mon, 15 Jan 2024 10:30:00 GMT)而非ISO格式——如果只做机械的类名替换不覆盖default方法,/api/order接口的返回格式会静默变化,前端拿到的日期字段直接错乱。这种"升级后测试还能过、但数据格式变了"的坑,是最难排查的一类。
② before_first_request → 应用创建时直接执行
# 修复后
def init_app():
print("[init] ...")
init_app() # 应用创建时直接调用
③ escape → 从markupsafe导入
# 修复后
from markupsafe import escape
return "输入内容: {}".format(escape(text))
4.4 第四步:全量验证
修复后重新安装新版本、启动服务、跑功能测试:
[PASS] /api/order 返回自定义类型JSON ← CustomJSONProvider生效
[PASS] /api/quote HTML转义正常 ← markupsafe.escape
[PASS] /api/error 返回404 ← werkzeug 3.1.9
全部测试通过
附带收获:requests 2.28→2.34的升级带入了3项安全修复(CVE-2023-32681、CVE-2024-35195、CVE-2024-47081)——这就是"升级收益看不见"的反例,CVE就实实在在地躺在版本号差里。
五、升级的完整账本
| 项目 | 内容 |
|---|---|
| 版本跨度 | flask +2个大版本、werkzeug +2个大版本、requests +6个小版本 |
| Breaking Changes | 4个API移除 + 1个行为差异(datetime序列化格式) |
| 代码修改 | app.py共4处,requirements.txt全量更新 |
| 新增依赖 | blinker 1.9.0(Flask 2.3+必需) |
| 安全收益 | 3个CVE修复(requests) |
| 验证方式 | 三轮验证法:基线✅ → 破坏确认❌ → 修复验证✅ |
| 功能回归 | 3/3测试通过 |
| 产出物 | 完整迁移指南(版本对比、修复方案、验证记录) |
六、人工升级 vs CodeArts自动升级
| 维度 | 人工升级 | CodeArts子智能体 |
|---|---|---|
| 调研成本 | 翻几百行CHANGELOG,逐条比对 | 自动整理与本代码相关的移除清单 |
| 不兼容定位 | 靠报错驱动,改一处试一次 | 先实测确认全部不兼容点,再统一修复 |
| 隐蔽坑 | datetime格式变化这类静默差异很难发现 | 主动指出行为差异及影响 |
| 官方依据 | 很少有人逐条查PR编号 | 每条都附CHANGELOG原文和issue编号 |
| 回归验证 | 挑几个接口手测 | 基线测试全量跑通 |
我的切身感受是:子智能体最大的价值不是"替你改代码"(改代码本身不难),而是把升级从"报错驱动的试错循环"变成"证据驱动的确定性任务"——先列全清单、再实测确认、再逐条修复、最后全量回归。每一步都有证据,每一步都可复现。
七、结语:升级债的解法不是勇气,是方法
升级Flask这件事,难点从来不是"敢不敢",而是"怎么确定改全了"。人肉升级的恐惧源于不确定性:不知道有没有漏掉的API、不知道有没有静默的行为变化、不知道回归测试够不够。
CodeArts开源升级子智能体把不确定性变成了确定性:4个Breaking Changes、1个行为差异、3轮验证、3/3测试通过——升级债的偿还,从一个需要"下决心"的突击任务,变成了一次可以随时执行的例行操作。
依赖升级最难的不是改代码,是确信没改漏。
华为云码道(CodeArts),让每一次升级都有完整证据链。
本文由CodeArts开源升级子智能体真实升级驱动。升级前后的项目代码与迁移指南在 /workspace/upgrade-demo/ 目录。
评论(0)