这次我们不看模型跑分,而是看一个更贴近日常开发的问题:Migration fatigue,迁移疲劳。
这不是一个新鲜术语,但经历过一次大版本升级、代码库迁移或技术栈替换的开发者应该都有直觉:项目越老,迁移越累。查旧 API、改调用、调参数、跑测试、看报错、再改,这种循环连续几周出现,消耗的不仅是时间,还有注意力。很多人开始问:LLM 到底能不能把迁移这件事接过去?
这篇文章不推导概念,重点回答三个问题:
- LLM 能在迁移工程里承担哪些环节;
- 怎么设计一套可复用的 LLM 迁移工作流;
- 在批量处理代码库时要面对哪些工程约束,以及怎么验证质量。
适合正在评估技术栈升级、负责存量代码维护、想用 LLM 做 codebase 级改动的开发者。看完可以直接照搬工作流和 Prompt 模板,先跑通一个小模块再决定要不要铺开。
1. 迁移疲劳是什么,为什么值得单独讨论
先给一个可操作的定义:迁移疲劳(Migration fatigue)指的是在技术栈升级、框架换代、服务化改造、数据库迁移、语言版本升级等长期迁移项目中,因为重复劳动密集、上下文切换频繁、验证链路长,导致开发效率下降、出错率上升、团队动力下降的一种累积性损耗。
它不是单次改代码的辛苦,而是整个迁移周期里的系统性消耗。举例来说,一次 Python 2 到 Python 3 的迁移,可能有几百个文件、几千处调用点。每一处改动单独看都不难,但连续处理几百个文件以后,人的判断力会下降,最容易出错的反而是最机械的批量替换。
1.1 迁移疲劳的三种典型来源
第一种是重复劳动密集。同一个 API 用法在代码库里出现几十次,每次都要手工调整参数顺序、改返回值处理、换异常类型。这种改动没有智力含量,但量大且不能出错。
第二种是上下文切换频繁。开发者刚搞清楚模块 A 的业务逻辑,又要切到模块 B 看它的依赖关系。迁移常常跨模块进行,每次切换都要重新加载一批上下文,这种成本很难被量化,但恰恰是疲劳感的主要来源。
第三种是验证链路长。改完一个文件,要先编译、再跑测试、再对比行为,出现问题还要回溯。改 100 个文件,意味着验证成本也要乘以 100。很多迁移项目工期延误,不是改不动,而是验证不完。
1.2 迁移成本模型
可以把迁移成本简化为:
迁移总成本 = 文件数 × 单文件改动点 × 单改动点验证成本迁移疲劳产生于三项同时很大的情况。传统自动化工具能降低第一项,但对第二和第三项的帮助有限。而 LLM 的价值在于:用语义理解处理单文件改动点,用测试生成和代码解释降低验证成本,用统一 Prompt 减少上下文切换。这个成本模型是后面所有工作流设计的基础。
2. LLM 在迁移工程里的五个落点
LLM 不是万能迁移器,但在迁移工程里至少有五个明确落点。
2.1 代码理解与语义映射
迁移首先要搞懂旧代码在做什么。LLM 可以读取一个文件或一个函数,输出它的逻辑摘要、依赖关系、潜在行为变化点。这比人工逐行读代码快很多,尤其适合接手不熟悉的存量项目。
2.2 批量代码改写
这是最核心的能力。给定旧的 API 用法和新的 API 用法,LLM 可以批量生成替换逻辑。与正则脚本不同,LLM 能处理需要理解的改动,比如参数顺序调整、回调函数改异步、配置项改名后的值映射。
2.3 测试用例迁移与生成
迁移完成后,旧测试不一定能直接运行。LLM 可以帮忙把旧测试迁移到新框架,或者根据旧代码的行为生成新的单元测试,用来锁定迁移前后的行为一致性。
2.4 配置与文档迁移
技术栈升级往往伴随配置文件、README、部署文档的同步更新。LLM 可以把旧配置映射到新格式,并生成对应的迁移说明文档。这类工作常常被忽略,但少了它们,项目很难长期维护。
2.5 代码评审辅助
批量迁移后需要人工评审。LLM 可以对比源文件和迁移后文件,标注行为可能变化的点,辅助 reviewer 把注意力放在高风险区域,而不是逐行看 diff。
2.6 和传统工具的对比
| 能力 | 正则脚本 | AST 工具 | LLM |
|---|---|---|---|
| 语法级替换 | 支持 | 支持 | 支持 |
| 语义理解 | 不支持 | 有限 | 支持 |
| 跨文件上下文 | 不支持 | 有限 | 支持 |
| 输出确定性 | 高 | 高 | 中 |
| 验证成本 | 低 | 中 | 高 |
| 长期维护迁移脚本 | 一般 | 较难 | 简单 |
从这张表能看出,LLM 的定位不是替代脚本和 AST 工具,而是补足它们不擅长的“语义级改动”。实际项目里,应该先跑工具做语法级替换,再用 LLM 处理工具覆盖不到的部分。
3. 一套可复用的 LLM 迁移工作流
迁移项目无论大小,都可以按固定流程推进。这里给出一套适合 LLM 介入的工作流,六个阶段。
3.1 盘点:先知道代码库里有什么
先用脚本统计文件数、语言版本、依赖清单、框架 API 使用频次。目标是把迁移范围量化。LLM 在这个阶段可以做依赖分析,输出各个模块之间的调用关系和风险分级。
3.2 分析:定义迁移规则
这是最关键的一步。先人工梳理旧 API 到新 API 的映射关系,形成一张迁移规则表。规则表包含:旧写法、新写法、需要注意的行为变化、不能自动处理的情况。这些规则会用来构造 Prompt,所以写得越具体,LLM 输出越稳定。
3.3 试点:用 5 到 10 个文件验证 Prompt
不要一开始就全量跑。挑几个有代表性的文件,覆盖不同复杂度,让 LLM 迁移后人工审查。这个阶段的目的不是提交代码,而是调 Prompt,找到稳定输出格式。
3.4 批量:按规则跑完整个代码库
跑批之前先设计任务队列、输出目录、失败重试机制。建议按文件粒度切分任务,每个文件独立调用 LLM,结果落盘到独立目录,方便后续逐个处理失败项。
3.5 验证:编译、测试、diff 三重检查
编译检查语法,测试检查行为,diff 检查是否有意外改动。三者都通过才考虑合入。
3.6 复盘:沉淀迁移知识
把迁移过程中发现的新规则、LLM 犯过的错、人工修正的 diff 整理成文档。下一次迁移直接复用这些材料,不需要从零开始。
4. 实操案例一:Python 2 到 Python 3 的 LLM 辅助迁移
Python 2 到 Python 3 已经是一个被反复讨论过的迁移场景,刚好适合用来演示 LLM 的工作方式。常见的改动点包括:
- print 语句改 print() 函数;
- except 语法变化;
- dict.iteritems() 改 items();
- unicode 和 str 的处理方式变化;
- 整数除法行为变化;
- 自定义类需要显式继承 object;
- 第三方库 API 变化。
4.1 先用工具处理语法层
Python 官方提供的 2to3 和老牌的 future 库可以处理大部分纯语法改动。先跑一遍工具,把能机器处理的部分处理掉,剩下的 diff 和报错就是 LLM 的输入。
# 先生成迁移后的代码目录 # 实际命令和参数需要按项目环境调整 python -m lib2to3 -w -n ./src工具处理完之后,收集编译错误和测试失败信息,这些都是喂给 LLM 的上下文。
4.2 迁移 Prompt 示例
下面是一个基于常见迁移场景的 Prompt 模板,你需要把自己的代码片段和具体报错信息填进去。
你是资深 Python 迁移工程师。 任务:把下面的代码从 Python 2 迁移到 Python 3。 要求: 1. 保持代码逻辑完全不变,不要顺手重构; 2. 修复所有与 Python 3 不兼容的语法和 API 调用; 3. 只输出迁移后的完整代码,不要输出解释; 4. 如果有无法确定的行为变化点,在代码末尾用 # MIGRATE_NOTE 标注。 待迁移代码: ```python # 这里粘贴待迁移的代码片段已知报错信息:
# 这里粘贴编译错误或测试失败信息这个 Prompt 的两个关键点:明确要求“不要顺手重构”,避免 LLM 输出和原逻辑不一致;要求“只输出迁移后的完整代码”,方便做后续的文本解析和 diff 对比。 ### 4.3 验证步骤 迁移后先跑编译器和测试。如果测试没有覆盖到某些函数,就人工把旧行为和新行为做对比。常见做法是把源文件和迁移后文件分别跑同一组输入,对比输出。 ```bash # 以 unittest 为例,实际命令按项目调整 python -m unittest discover tests从中能看出,Python 2 到 3 的迁移瓶颈不在语法替换,而在语义验证。LLM 负责把剩余的不兼容点改掉,人工负责确认这些改动没有改变业务行为。
5. 实操案例二:Vue 2 到 Vue 3 的前端升级
前端框架升级是另一个典型的迁移疲劳来源。以 Vue 2 到 Vue 3 为例,核心变化不止是语法,还有 API 设计思路的整体切换。
5.1 迁移难点
Vue 2 到 Vue 3 的常见改动点包括:
- 全局 API 注册方式变化,例如 Vue.prototype 改为 app.config.globalProperties;
- v-model 绑定机制变化;
- 过滤器 filter 被移除;
- $children 属性被移除;
- 生命周期钩子改名,例如 beforeDestroy 改为 beforeUnmount;
- 选项式 API 到组合式 API 的渐进迁移;
- 事件总线 EventBus 需要寻找替代方案。
这些改动分散在 .vue 文件、JS 文件、配置文件里,而且很多是跨文件联动,一次改错会影响整个组件树。
5.2 基于目录的批量改写思路
前端迁移不宜按文件逐个盲改,更稳妥的做法是先让一张“迁移清单”明确每个文件涉及哪些 API。可以使用静态分析工具扫描出 Vue API 的使用位置,再把这些位置交给 LLM 做定向改写。
你是一名前端开发工程师,负责 Vue 2 项目迁移到 Vue 3。 请根据下面的改动规则修改代码: 1. 把 Vue.prototype.$xxx 改为 app.config.globalProperties.$xxx; 2. 把 beforeDestroy 改为 beforeUnmount,把 destroyed 改为 unmounted; 3. 删除多余的 filter 定义,并替换模板里对 filter 的调用; 4. 保持组件 props、data、computed、methods 等选项的语义不变。 改动规则: {这里粘贴你的迁移规则表} 原代码: {这里粘贴待迁移的 .vue 或 .js 文件片段}注意 Prompt 里明确让模型“根据改动规则修改”,而不是让模型自己决定怎么改。迁移规则先由人工梳理,这样 llm 的输出更可控。
5.3 前端迁移的验证方式
前端迁移的验证比较复杂,因为很多问题在编译阶段不会暴露。验证顺序建议如下:
- 先跑构建工具,确认没有编译报错;
- 再用自动化测试覆盖关键组件流程;
- 最后人工走查页面,重点看交互逻辑和样式差异。
# 构建验证,实际命令按项目框架调整 npm run build从前端案例能看出,迁移规则表的价值比 Prompt 本身更大。规则表就是团队对迁移的理解,LLM 只是执行者。
6. 批量任务设计:让 LLM 按目录跑完整个代码库
迁移的最终目的是全量完成,不是手工改几个文件。LLM 产出不稳定,批量任务设计要围绕“可控”展开。这里给出一套通用的批量迁移脚本思路,适合代码库级任务。
6.1 任务拆分的三个原则
第一,按文件粒度切分任务。一个文件是一次独立调用,成功失败互不影响。第二,任何文件超过一定行数就分块处理,避免超出上下文窗口。第三,输出结果落盘到独立目录,不要直接覆盖源文件。这三个原则能保证批量跑完后有完整的回溯能力。
6.2 一个可用的批量迁移脚本
下面是一个基于常见 API 风格的批量迁移脚本示例。实际使用需要把接口地址、模型名、密钥替换成自己项目对应的配置。
import os import time from pathlib import Path import requests API_URL = os.getenv("LLM_API_URL", "http://127.0.0.1:8000/v1/chat/completions") API_KEY = os.getenv("LLM_API_KEY", "") MODEL = os.getenv("LLM_MODEL", "your-model-name") INPUT_DIR = Path("./src") OUTPUT_DIR = Path("./migrated") OUTPUT_DIR.mkdir(exist_ok=True) def split_file(text, max_lines=400): lines = text.splitlines(keepends=True) return ["".join(lines[i:i + max_lines]) for i in range(0, len(lines), max_lines)] def call_llm(messages, max_retries=3): for attempt in range(max_retries): try: resp = requests.post( API_URL, headers={"Authorization": f"Bearer {API_KEY}"}, json={"model": MODEL, "messages": messages, "temperature": 0.0}, timeout=120, ) resp.raise_for_status() return resp.json()["choices"][0]["message"]["content"] except Exception as e: if attempt == max_retries - 1: raise time.sleep(2 ** attempt) for src_file in INPUT_DIR.rglob("*"): if src_file.suffix not in {".py", ".js", ".ts", ".vue", ".java"}: continue if any(part in src_file.parts for part in {"venv", "node_modules", "dist", "build"}): continue content = src_file.read_text(encoding="utf-8") chunks = split_file(content) migrated_chunks = [] for idx, chunk in enumerate(chunks): prompt = ( "请根据迁移规则把下面的代码迁移到新版本技术栈。" "只输出完整代码,不要解释。\n\n" f"代码片段({idx + 1}/{len(chunks)}):\n{chunk}" ) result = call_llm( [ {"role": "system", "content": "你是资深代码迁移助手,只输出迁移后的代码。"}, {"role": "user", "content": prompt}, ] ) migrated_chunks.append(result) relative = src_file.relative_to(INPUT_DIR) out_file = OUTPUT_DIR / relative out_file.parent.mkdir(parents=True, exist_ok=True) out_file.write_text("\n".join(migrated_chunks), encoding="utf-8") print(f"完成: {relative}")6.3 失败重试与断点续跑
批量任务最怕跑到一半失败,又不知道从哪里继续。这个脚本的核心是输出目录和源目录一一对应。跑完一批后,检查 OUTPUT_DIR 里哪些文件缺失或为空,就是失败清单,补跑这些文件即可。
# 查看迁移后目录里是否有缺失,实际命令按目录名调整 find src -type f | sort > /tmp/src_files.txt find migrated -type f | sort > /tmp/migrated_files.txt comm -23 /tmp/src_files.txt /tmp/migrated_files.txt如果接口有速率限制,脚本里的重试机制不能省。最稳妥的做法是把每次调用的结果和原始请求都落盘,方便定位是哪一次调用出了问题。
7. 迁移场景的 Prompt 设计
Prompt 设计直接决定 LLM 迁移的质量。迁移场景和通用聊天不一样,核心要求是“稳定”,不是“有创意”。
7.1 统一 Prompt 模板
一份可复用的迁移 Prompt 应该包含四个部分:角色定义、任务目标、约束条件、输入输出格式。下面是一个完整模板。
System: 你是 {语言/框架} 迁移专家。你的任务是把用户提供的代码从 {旧版本/旧框架} 迁移到 {新版本/新框架}。 你的输出必须遵守以下约束: 1. 只输出迁移后的完整代码,不要输出任何解释; 2. 不得改变代码的业务逻辑; 3. 不得引入源文件中不存在的依赖; 4. 如果存在无法确定的语义变化,在代码中用 {约定标记} 标注; 5. 不要在代码中追加测试用例。 User: 迁移规则: {迁移规则表} 源文件: ```{language} {源文件内容}如果源文件过长,要提前拆分。
### 7.2 Few-shot 示例的作用 只给规则不给示例,LLM 容易发挥不稳定。建议在 Prompt 里给出 1 到 2 组“旧写法 → 新写法”的对照示例,特别要覆盖那些规则不容易描述的情况。 ```text 示例1: 旧写法: ```javascript Vue.prototype.$http = http新写法:
app.config.globalProperties.$http = http示例2: 旧写法:
for k, v in data.iteritems(): print(k, v)新写法:
for k, v in data.items(): print(k, v)### 7.3 输出稳定性处理 LLM 即使被要求“只输出代码”,也可能偶尔输出解释文字或 Markdown 代码块标记。批量处理时,最好在后处理脚本里做一道清洗:剥离代码块标记,只保留 ` {代码} ` 之间的内容;用标签匹配提取;对空输出和非预期输出打标记,交给人工处理。 ```python def clean_code_output(text: str) -> str: # 去掉接缝处的 Markdown 代码块标记,实际处理需要按输出格式调整 if text.startswith("```"): lines = text.splitlines() lines = [line for line in lines if not line.strip().startswith("```")] return "\n".join(lines) return text8. 验证与质量保障
LLM 迁移输出的代码只是草稿,验证才是迁移质量的核心。验证分三层,缺一不可。
8.1 编译与类型检查
第一层是语法和类型。后端代码跑编译器,前端代码跑构建工具。这一步能过滤掉大量低级错误。如果项目使用了 TypeScript、mypy 或类似工具,要主动开启严格模式,LLM 经常忽略类型注解的迁移。
# 后端示例,实际命令按语言和框架调整 python -m compileall migrated/ # 前端示例 npm run build8.2 测试基线
第二层是行为验证。迁移前,先跑一遍旧代码的测试,记录测试结果作为基线。迁移后,把同一套测试跑到新代码上,对比通过率和失败用例。
如果旧项目没有测试,那就需要先补测试。最务实的做法是挑核心业务函数,写少量冒烟测试,确保 LLM 迁移没有把主流程改坏。测试覆盖不到的位置,用源文件和迁移后文件的对比 diff 来人工检查。
8.3 diff 评审
第三层是 diff 评审。用 diff 工具对比源文件和迁移后文件,逐项确认每处改动是否符合迁移规则。这里建议重点看三类 diff:
- 超出迁移规则范围的改动,说明 LLM 顺手改了逻辑;
- 涉及配置和数据结构的改动,这类最容易引发线上问题;
- 带有 TODO 或注释的改动,说明 LLM 自己也不确定。
# 对比源文件和迁移后文件,实际命令按目录调整 diff -u src/user.py migrated/user.py三层验证都通过,才允许把迁移后的代码合入主分支。从工程实践看,验证环节消耗的时间通常会超过 LLM 生成代码的时间。
9. 成本、性能与工程约束
用 LLM 做代码迁移不是免费的,成本要提前估算。
9.1 Token 预算
Token 消耗的估算公式很简单:文件总字符数除以 4,约等于 token 数。一次“输入源文件 + 输出迁移结果”的调用,token 消耗约等于源文件的 2 到 3 倍。一个 10 万 token 的代码库,迁移一次可能要消耗几十万 token,费用和模型定价强相关。
想控制成本,就先把无需 LLM 参与的文件排除掉。能被脚本处理的部分,不要让 LLM 碰。
9.2 并发与限流
调用公开模型服务时,通常有速率限制。批量任务不要一次性发大量并发请求,建议做简单的限流:每秒钟限制请求数,失败后做指数退避重试。对大批量任务,更推荐分批跑,每批完成后再进行验证。
9.3 数据安全与合规
这一点单独强调。代码库往往包含内部业务逻辑、密钥配置、客户信息,直接提交到公网模型服务存在数据泄露风险。如果要使用 LLM 辅助迁移,建议按下面顺序处理:
- 优先使用私有化部署的代码模型;
- 使用公网服务前,先对代码做脱敏处理,把变量名、字符串常量、内部路径替换成无意义符号;
- 迁移后的代码必须走正常代码评审流程;
- 涉及版权代码、第三方库的迁移,要确认许可证和授权边界。
10. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| LLM 输出格式不稳定,夹带解释文字 | Prompt 约束不够强 | 查看原始返回内容 | 在后处理中剥离解释,增加输出格式约束 |
| 长文件超出上下文窗口 | 文件未分块或分块策略不合理 | 检查请求长度和模型上下文限制 | 按行数分块,补充上下文摘要 |
| 迁移后的代码编译不过 | LLM 对语法理解有误 | 先跑编译器收集错误 | 把错误信息回传 Prompt 做二次修复 |
| 迁移后行为不一致 | LLM 误解了业务语义 | 对比测试用例输出 | 补充测试基线,对高风险文件人工审查 |
| 批量任务大量失败 | 接口限流或网络超时 | 查看错误日志和 HTTP 状态码 | 增加重试、指数退避、断点续跑 |
| 代码库太大,token 成本失控 | 未做范围裁剪 | 统计文件数 token 消耗 | 先用脚本排除非关键文件,分批执行 |
| 公司代码使用了公网模型服务 | 数据安全没有管控 | 检查访问的网络出口 | 改用私有化部署或先脱敏 |
11. 最佳实践与使用建议
LLM 迁移已经有不少项目在用了,但跑得顺的团队通常遵循下面几条原则。
先小批量试点,不要直接全量跑。挑 10 到 20 个有代表性的文件,覆盖简单、中等、高风险三种类型。跑完以后看效率和错误率,再决定是否扩大范围。
迁移规则表要当作代码来维护。每次迁移发现新规则,就补充到表里;发现 LLM 反复出错,就调整规则描述。规则表越完善,后续批量迁移的质量越高。
输出目录和源目录分离。永远不要用 LLM 生成的代码直接覆盖源文件。保留源文件,输出到新目录,验证通过后再通过正式流程合入。这样能随时对比差异,也能避免误操作。
把验证写进自动化流程。哪怕只是简单的编译检查,也要做成脚本。迁移工作量大的时候,人工验证跟不上,自动化检查能兜住大部分低级错误。
批量任务要加日志。记录每个文件调用 LLM 的请求参数、返回结果、耗时和状态码。迁移项目周期越长,日志越重要。
12. 总结
迁移疲劳不会因为模型变强就自动消失。但 LLM 把问题的重心从“写迁移代码”转移到了“定义迁移规则和验证结果”。这个转变的价值在于,团队可以把最稀缺的经验集中在判断上:哪些行为必须保持,哪些可以顺手重构,哪些边界条件不能漏。
如果要做一件事,建议从一个小模块开始:挑一个改动风险低的代码目录,按前面这套工作流跑一遍,把 LLM 输出的代码和人工修改后的差异保存下来。这份差异就是团队自己的迁移知识库,也是下一次迁移最有效的 Prompt 素材。