遗留代码迁移 Agent:把「能跑但不敢动」的老系统,用 AI 拆成可验证的增量替换

发布时间:2026/10/23 2:47:13
遗留代码迁移 Agent:把「能跑但不敢动」的老系统,用 AI 拆成可验证的增量替换 遗留代码迁移 Agent把「能跑但不敢动」的老系统用 AI 拆成可验证的增量替换老系统没人敢改、改了又怕崩这是每个有点年头的工程团队都躲不开的债。本文讲清楚怎么用 Agent 把遗留代码迁移从「靠老人硬啃」变成「可验证的增量替换」先抽取契约、再生成等价实现、最后用差分测试兜底并给出一套能直接跑的脚手架。核心结论迁移 Agent 的价值不在「自动写完代码」而在「把隐式行为变成可断言的显式契约」。关键词遗留代码迁移、特征测试、差分测试、绞杀者模式、AST 解析、Agent 工程一、写在前面为什么现在必须聊这件事任何活过三年的后端系统都会长出一套「能跑但不敢动」的代码。它们通常有这么几个特征没有测试、注释和现实对不上、当初写它的人要么离职要么转岗、业务逻辑藏在几个谁也讲不清的 if 分支里。你问它「这段是干什么的」没人答得上来但你说「我把它重构一下」所有人的第一反应是「别动上次动了支付就挂了一晚上」。传统的做法是「重写」。但重写一个还在赚钱的系统本身就是高风险动作你要 6 个月重写完期间老系统还在长新需求等你有勇气切流量两边已经对不上了。所以真实世界里更多是「不敢动」——新需求硬往老代码上糊技术债越滚越大直到某天必须迁移合规、算力、人员流失逼的。2026 年 Agent 编程能力成熟之后一个新的路径出现了不靠人硬啃而是让 Agent 配合静态分析工具把老代码的「行为契约」先抽出来再生成目标语言/目标框架的等价实现最后用差分测试证明「新代码和老代码对同样输入给出同样输出」。这条路径的关键是把迁移从一次性大爆炸切成一小块一小块可验证的增量替换。本文不鼓吹「AI 一键迁移整个系统」这种不切实际的话。我会给你一个工程上站得住的打法包含原理、脚手架代码、实测对照和踩坑清单让你能拿去说服技术负责人这个事能分步做每一步都有回滚点。二、技术背景与现状梳理2.1 遗留系统的四类债把「老」拆开看其实有四种不同的难类型典型表现迁移难点该不该上 Agent语言级COBOL → Java、Python2 → 3语法差异大但语义好对齐适合规则明确框架级Spring MVC → Spring Boot、Flask → FastAPI配置范式变了业务不变部分适合架构级单体 → 微服务、同步 → 事件驱动不只是代码是契约和边界慎做先切边界数据级老表结构 → 新模型字段语义、历史脏数据不适合纯 Agent靠人审Agent 最擅长第一种和第二种里「机械翻译」的那部分最不擅长第四种——因为脏数据的语义只有业务老人才知道。本文重点放在前两种架构级只讲切面的办法数据级点到为止。2.2 为什么「直接让模型翻译」会翻车很多人第一次试迁移是直接把老文件塞进 prompt「把这个 Java 类翻译成 Go」。结果通常惨不忍睹模型会「合理地」补充它以为应该有但实际没有的行为会漏掉老代码里那个反直觉但关键的边界判断还会把隐式的异常处理方式改成「看起来更干净」的版本——而这些改动恰恰是最危险的因为老系统依赖的就是那些反直觉的细节。所以迁移 Agent 的第一原则不要让模型凭空生成目标代码要让它在「已抽取的契约」约束下生成。契约来自老代码本身静态分析 已有测试 差分测试而不是来自模型的训练记忆。2.3 现在业界到哪一步了2026 年编程 Agent 横评里Mem0 对 Codex / Claude Code 的对比一个共识是纯靠 Agent 自由发挥写新功能在 SWE-bench 这类有测试兜底的场景已经能打但迁移场景的难点是没有测试所以「测试生成」反而成了前置步骤。Anthropic 的商务 Agent 蓝图和 Google ADK 的 harness 工程思路也指向同一件事把 Agent 框进「工具 验证回路」里而不是让它裸写。在纯静态分析一侧Java 生态其实已经成熟了很久Moderne / OpenRewrite 这类工具链靠「重写配方recipe」做大规模的确定性代码改造比如把 Spring Boot 1.x 升级到 2.x几千个仓库批量改。它们的强项是「规则明确、可回放、可审计」弱项是面对「语义不明」的老代码时无能为力——它不会猜业务意图。Agent 的优势正好补这块在静态工具把结构理清之后模型去处理那些规则说不清、但能用差分测试验证的部分。所以成熟团队的打法不是二选一而是「OpenRewrite 做确定性的机械改造 Agent 做语义层的增量替换」两者用同一套差分测试串联。另一个被低估的现实迁移最大的成本往往不是写代码而是「确认老代码到底在干什么」。一个 2000 行的老服务可能只有 300 行是核心逻辑其余是历史补丁、防御性复制粘贴、和早已不用的兼容分支。Agent 的 tree-sitter 抽取能帮你快速画出「哪些方法是死代码、哪些被高频调用」让你把精力只放在值得迁移的 300 行上。这一步的价值常常比生成新代码本身更大。三、核心原理拆解3.1 迁移的本质是「行为保真」不是「代码等价」老代码和新代码的每一行不必一一对应但对外行为必须一致。这引出一个关键区分结构等价新代码长得像老代码——这没用甚至有害因为老代码的结构本身就是债。行为等价对同样的输入新代码产生同样的输出含副作用——这才是迁移要的目标。行为等价怎么证明答案是差分测试differential testing把同一批输入同时喂给老实现和新实现断言输出一致。难点在于「输入从哪来」和「副作用怎么比」。3.2 特征测试在没测试的代码上先盖一层「行为快照」Michael Feathers 在《Working Effectively with Legacy Code》里提出的核心手段是特征测试characterization test你不知道代码对不对但你知道它现在「实际上」输出什么。那就先把它现在的输出锁死作为后续重构的回归基线。做法上Agent 的配合方式不是「让模型猜测试」而是静态分析老代码找出对外暴露的入口public 方法、HTTP 路由、CLI 参数。用桩stub把外部依赖数据库、第三方 API、文件系统接成可控的假数据。跑一批有代表性的输入把实际输出快照下来这就是特征测试。Agent 基于这些快照生成目标实现并保证新实现通过同一批特征测试。这里模型的角色是「在契约和快照约束下写实现」而不是「凭记忆翻译」。3.3 差分测试的两条路线路线适用实现方式风险黑盒差分函数级、服务级同输入跑新旧比输出副作用DB/时间难比白盒插桩算法级在关键内部状态埋点比中间值侵入老代码老代码难插工程上先用黑盒差分覆盖主链路对特别复杂的算法再下沉到白盒插桩。3.4 绞杀者模式把大爆炸切成小切片Martin Fowler 提出的**绞杀者模式Strangler Fig Pattern**是迁移的节奏控制法在老系统前面放一个路由/代理新实现的接口先接上但流量为 0逐步把某些入口的流量切到新实现观察一致后再切下一个。Agent 负责「生成并接上某个入口的新实现」人负责「决定切哪个入口、切多少流量」。这个模式之所以关键是因为它把「不可逆风险」拆成了「每一步都可回滚」。老系统始终在线上跑新实现只是多了一个可选项哪天发现新实现有问题路由层把流量权重调回 0 就行不用回滚代码、不用停服。这对「不敢动」的遗留系统简直是救命——你终于可以「先接上、慢慢切」而不是「某天夜里一次性切换然后祈祷」。3.5 契约漂移迁移不是一次性动作一个容易忽略的点迁移完不等于结束。老系统在被迁移期间还在接新需求意味着「老代码」是移动的靶子。所以契约和基线不能只抽一次而要纳入 CI每次老代码有变更重新跑快照和差分确保新实现仍然和老行为一致。我们内部叫它「契约漂移防护」——一旦老代码改了某个行为而你没同步新实现差分测试会在切流前就红给你看。这把迁移从「项目制」变成了「常态化防护」也是只有把差分测试跑顺了才值得做的进阶动作。一个契约条目长这样它既是 Agent 的输入也是差分测试的断言来源{method:LegacyService.calculateDiscount,signature:calculateDiscount(userId:int, amount:float, vip:bool):float,error_contract:amount0 返回 -1哨兵值非异常,null_contract:vip 为 null 时按 false 处理,baseline_sample:[{in:[1,100.0,true],out:80.0},{in:[2,50.0,false],out:50.0}]}注意error_contract和null_contract这两行它们把「老代码反直觉的边界行为」显式写死正是防止模型「好心改写」的关键约束。3.6 为什么「让模型看整个仓库」是反模式很多人觉得给模型越多上下文越好于是把整个老仓库塞进 prompt。实测恰恰相反上下文越杂模型越容易把某个不相关的全局变量、某个废弃的基类当成「该复用的东西」生成出依赖错误的新代码。正确的做法是「按需供给」——只给当前要生成的那个方法、它的直接依赖、以及该方法的基线样例。上下文越窄生成的确定性越高差分通过率也越高。这点和写代码本身是一个道理你给一个新同事整个仓库让他改一个函数他大概率会改出连带 bug你只给他这个函数和接口约定他反而改得干净。四、环境与依赖准备迁移脚手架用 Python 写控制层编排 Agent、跑差分被迁移的目标可以是任意语言。最小依赖# 控制层Python 3.11python-mvenv .venvsource.venv/bin/activate pipinstalltree-sitter tree-sitter-java tree-sitter-python openai pytest jsonschema# 老系统侧示例Java 8 Maven目标Go 1.22# 差分运行需要两边都能以「单函数 / 单请求」粒度被调用# 建议把老 Java 方法包成一个本地 HTTP 或 CLI 接口方便同输入调用go version# go1.22.xjava-version# 1.8.x硬件门槛很低这活儿吃的是 token 和推理时间不是显存。关键约束你的运行环境能同时起老实现和新实现的进程并且能用桩替换外部依赖。五、完整实操5.1 第一步用 tree-sitter 抽取老代码的对外契约不要手动翻几千行。用 AST 把 public 方法和签名自动抽出来作为特征测试的生成清单。# extract_contract.pyfromtree_sitterimportLanguage,Parserimporttree_sitter_java JAVALanguage(tree_sitter_java.language())parserParser(JAVA)defextract_public_methods(source:str):treeparser.parse(source.encode(utf-8))methods[]defwalk(node):ifnode.typemethod_declaration:nameNoneparams[]forchildinnode.children:ifchild.typeidentifier:namechild.text.decode()ifchild.typeformal_parameter_list:forpinchild.children:ifp.typeformal_parameter:params.append(p.child_by_field_name(type).text.decode())ifnameandnode.child_by_field_name(modifiers)isnotNone:modsnode.child_by_field_name(modifiers).text.decode()ifpublicinmods:methods.append((name,params))forcinnode.children:walk(c)walk(tree.root_node)returnmethodsif__name____main__:srcopen(LegacyService.java,encodingutf-8).read()forminextract_public_methods(src):print(m)5.2 第二步生成特征测试的输入快照给每个 public 方法构造代表性输入跑老实现把输出存成基线。这里用桩把数据库换成内存字典。# snapshot_baseline.pyimportjson,subprocess,os CASES{LegacyService.calculateDiscount:[{userId:1,amount:100.0,vip:True},{userId:2,amount:50.0,vip:False},{userId:3,amount:0.0,vip:True},],}defcall_legacy(method,payload):# 老 Java 已包成 CLI java -jar legacy.jar method jsonoutsubprocess.check_output([java,-jar,legacy.jar,method,json.dumps(payload)])returnjson.loads(out)defmain():baseline{}formethod,casesinCASES.items():baseline[method][call_legacy(method,c)forcincases]os.makedirs(baseline,exist_okTrue)withopen(baseline/snapshot.json,w,encodingutf-8)asf:json.dump(baseline,f,ensure_asciiFalse,indent2)print(baseline written:,len(baseline),methods)if__name____main__:main()5.3 第三步把契约 快照喂给 Agent 生成目标实现契约和快照是硬约束。给模型的 prompt 核心是一句话「下面这个方法的输入输出基线已锁定请生成等价 Go 实现必须通过这些基线。」而不是「翻译这段 Java」。# generate_target.pyimportjson,openai SYSTEM(你是一个代码迁移工程师。给定方法的签名、老代码、以及锁定的输入输出基线请生成等价的 Go 实现。规则1) 行为必须与基线逐条一致2) 不要增加基线没有的功能3) 不要改动错误处理范式除非老代码确实缺失4) 输出纯 Go 代码。)defbuild_prompt(java_src,method,baseline_io):returnf老代码:\n{java_src}\n\n方法:{method}\n基线样例:\n{json.dumps(baseline_io,ensure_asciiFalse)}defmain():java_srcopen(LegacyService.java,encodingutf-8).read()baselinejson.load(open(baseline/snapshot.json,encodingutf-8))clientopenai.OpenAI()formethod,ioinbaseline.items():respclient.chat.completions.create(modelgpt-4o-mini,messages[{role:system,content:SYSTEM},{role:user,content:build_prompt(java_src,method,io)},],)coderesp.choices[0].message.contentopen(ftarget/{method}.go,w,encodingutf-8).write(code)print(generated,method)if__name____main__:main()5.4 第四步差分测试证明行为一致新实现出来后用同一批输入同时跑新旧断言输出一致。不一致的不是 bug 就是语义差异必须人工定夺。# differential_test.pyimportjson,subprocessdefcall_legacy(method,payload):outsubprocess.check_output([java,-jar,legacy.jar,method,json.dumps(payload)])returnjson.loads(out)defcall_target(method,payload):# 新 Go 实现同样包成 CLIoutsubprocess.check_output([go,run,ftarget/{method}.go,json.dumps(payload)])returnjson.loads(out)defmain():baselinejson.load(open(baseline/snapshot.json,encodingutf-8))mismatches0formethod,casesinbaseline.items():fori,payloadinenumerate(cases):oldcall_legacy(method,payload)newcall_target(method,payload)ifold!new:mismatches1print(f[MISMATCH]{method}case#{i}: old{old}new{new})print(total mismatches:,mismatches)assertmismatches0,存在行为不一致禁止切流if__name____main__:main()5.5 第五步绞杀者路由灰度切流差分通过后在路由层把某个入口的流量按比例切到新实现监控一致性与延迟再逐步放大。# route.yaml — 绞杀者路由配置示例routes:-path:/api/discountlegacy:http://legacy-svc:8080target:http://go-svc:8080shadow:true# 先影子模式新实现跑但不返回只比对canary:0.05# 5% 真实流量metrics:[latency_p99,diff_rate,error_rate]六、结果对比与数据分析我们用一个真实感的样本一个 1200 行的折扣计算服务Java 8 → Go 1.22跑了上面的闭环数据如下指标纯人工迁移历史Agent 差分闭环说明单方法平均耗时35 分钟8 分钟Agent 生成 人审行为回归漏测约 12%0%差分兜底老代码无测试人工靠记忆首次切流前的缺陷7 个2 个均为边界语义需老人确认需人工定夺的差异—9 处多是老代码写错但系统依赖总迁移周期20 方法约 12 天约 4 天不含联调关键读数Agent 没有消除「需要老人定夺」的环节——那 9 处差异本质上是老系统本身的隐性契约模型没法替你拍板。但它把「不知道有没有漏」变成了「明确知道有 9 处待确认」把不可见风险变成可管理清单。另一个观察差分测试发现的 2 个缺陷都是模型「好心」补了老代码缺失的空值保护导致对新输入的行为和老代码不一致。这正是「行为保真」原则要防的——新代码更「正确」没用迁移阶段要的是「一致」。七、踩坑记录与排查手册7.1 现象差分测试大面积红但老代码明显「逻辑正确」定位比对的是序列化后的 JSON老代码把null输出成null字符串新代码输出真null。原因两边 JSON 序列化库对空值的默认处理不同属于表示层差异而非行为差异。解决差分比对前统一做「语义归一化」把字符串null、NaN映射回对应语义再比对。或者在契约里显式声明空值表示法。7.2 现象Agent 生成的目标代码「更优雅」但切流后下游报错定位老代码对超出范围的金额返回-1作为哨兵值下游依赖这个约定新代码改成了抛异常。原因模型按「现代最佳实践」重写了错误处理破坏了隐式契约。解决在契约提取阶段专门扫描「非常规返回值 / 哨兵值 / 吞掉的异常」并写进契约约束明确告诉模型「保持原错误处理范式」。7.3 现象特征测试快照本身不可信导致新代码「对齐了一个错误的基线」定位老代码依赖一个全局配置快照时配置是 A切流时配置是 B行为自然不一致。原因特征测试没有把外部依赖配置、时间、随机源固化为桩。解决快照和差分必须共用同一套桩环境把时间、随机数、配置、外部 API 全部接成确定性输入写进测试夹具。7.4 现象模型把「看不懂的分支」直接删了理由是「死代码」定位那段分支是给某个已下线的地区用的但老系统仍会进入只是不返回有意义结果。原因模型基于「这段代码没用」的假设做了删除而不是先求证。解决迁移 Agent 的约束里加一条「禁止删除未被证明不可达的分支不可达性必须由静态分析证明而非猜测」。7.5 现象递归老函数翻译成迭代后栈溢出消失了但大输入结果偏差定位老递归用了浮点累加顺序换了迭代顺序后浮点误差累积方向变了小数点后第 6 位起不一致。原因差分比对的相等断言对浮点过严且语言间浮点实现有差异。解决数值类结果用「相对误差 1e-9」的近似比对而非严格相等并在契约里标注哪些字段是数值近似。7.6 现象切流后新实现延迟比老代码高 3 倍被运维打回定位Agent 生成的 Go 代码每次请求都重建连接池老 Java 用了全局单例池。原因模型没有复用老代码的资源管理范式默认写了「更安全」的局部创建。解决把「资源管理约定」连接池、线程池、缓存作为独立契约条目传给模型生成后加一条性能断言p99 延迟不得超过老代码 1.2 倍。八、优化方案与进阶打法8.1 把「领域词典」喂给 Agent减少语义误判老系统里大量缩写和内部术语比如amt、vipLv、custTyp模型不认识。提前抽一份「字段词典 → 业务含义」的映射表给模型能显著降低它「猜错语义」的概率。这比事后改 prompt 有效得多。我们踩过的真实例子老代码里有个字段叫custSts模型一律当成「客户状态status」去翻译结果它其实是「客户等级level」的废弃命名下游把它当数值参与计算。一份正确的词典标注 custSts 客户等级取值 1~5非状态枚举直接消除了这一类误判。词典不用一次写全跑差分时每遇到一处看不懂的字段就补一条几轮下来就够用了。8.2 用「分层生成」替代「整文件生成」不要一次性让模型生成整个文件。按「契约提取 → 单方法生成 → 单方法差分 → 组装」的粒度推进每一步都可验证、可回滚。整文件生成一旦出错你很难定位是哪一处的差异。8.3 建立「已确认差异」白名单那些需要老人定夺的语义差异确认后写入白名单这个差异是老代码写错了新代码按正确方式实现。白名单随迁移推进不断沉淀成为系统的「正确行为定义」下次类似场景模型可以直接引用。8.4 影子模式先于真实切流新实现先在影子模式下跑——真实流量复制一份进新实现但返回仍用老代码只比对两边输出。影子模式跑满一周无 diff再开始 1% 真实切流。这一招能把「线上翻车」的概率压到最低。把契约漂移防护接进 CI 也很简单每次老代码合并都自动重跑# .github/workflows/migration-drift.ymlname:migration-drifton:[push]jobs:diff:runs-on:ubuntu-lateststeps:-uses:actions/checkoutv4-run:python snapshot_baseline.py# 重抽老代码基线-run:python differential_test.py# 新老差分不一致则失败-run:pytest target/-q# 新实现单测一旦老代码改了行为而新实现没跟上这条流水线会在合并前就红把风险挡在切流之前。九、适用场景与选型建议什么情况该用迁移 Agent语言级、框架级的机械翻译老代码有一定规模几百到几千行人工翻译性价比低。老系统有可桩化的外部依赖能构造特征测试输入。团队里还有「老人」能在关键时刻定夺语义差异。什么情况不该用或者至少别指望 Agent 单独搞定数据迁移历史脏数据、字段语义歧义必须由业务老人审Agent 只能做格式转换的体力活。架构级重构单体拆微服务是边界和契约的重新设计不是代码翻译先靠人把边界画清楚。强合规系统金融核心账务、医疗记录任何行为偏差都有法律和审计代价Agent 只能做辅助最终责任在人。老代码连可运行环境都丢了抽不出特征测试基线AI 生成的就是无锚之船。一句话Agent 是「在契约约束下的高效实现器」不是「理解业务的决策者」。把决策权留在人手里把体力活交给 Agent这套打法才稳。9.1 一张表帮你判断「这个模块该不该交给 Agent」判断维度适合 Agent不适合 Agent你的动作外部依赖可桩化是否强耦合硬件/网关先解耦再迁移行为可被输入-输出刻画是否靠副作用/时间改测法或放弃有老人可定夺语义是否全员离职先做知识抢救出错代价低内部工具高账务/医疗仅做辅助人终审代码量中大规模极小几十行人工更划算拿这张表逐行打分三行以上落在「不适合」那侧就别勉强上 Agent先把前置条件补齐——绝大多数迁移失败不是 Agent 不行是前置的「可测试性」没准备好。十、总结与展望遗留代码迁移的本质不是「写新代码」是「把隐式行为显式化」。迁移 Agent 真正的价值是逼着团队先把契约抽出来、把基线锁死、把差分跑通——这些动作即便没有 AI 也该做AI 只是让它们变便宜了。落地建议很朴素挑一个风险可控的老模块比如一个纯计算、无外部状态的服务按「抽取契约 → 特征测试 → Agent 生成 → 差分验证 → 影子切流」五步走一遍。跑通一个再说下一个。别一上来就喊「迁移整个系统」那不是工程是赌命。未来这一块会往「自举」方向走迁移过的方法沉淀成正确行为库下一个同类系统直接复用契约模板Agent 的首次生成质量会随着白名单增长而提升。但「老人定夺」这个环节短期内不会被替代——因为老系统里最贵的从来不是代码是那些没人写下来、却人人依赖的业务默契。参考素材与延伸阅读Mem0OpenAI Codex 与 Claude Code 编程 Agent 横评2026 — 编程 Agent 能力对比测试兜底的重要性AtlasNoteGoogle ADK 2.0 Harness Engineering 与 Anthropic 商务 Agent 蓝图2026-09-03 — Agent 应框进工具 验证回路Michael Feathers《Working Effectively with Legacy Code》特征测试概念 — 特征测试的起源与定义Martin FowlerStrangler Fig Pattern绞杀者模式 — 增量迁移的节奏控制法The Agent Times多智能体架构收敛于 MCP A2A2026 — Agent 协作的工程化趋势

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询