跳到主内容
快讯直播
AI智模界
教程

Claude Opus 5.5 迁移大型代码库:分片与回滚

假设手上有一个 68 万行的 Python 单体仓库,要从 Python 2 迁到 Python 3,或者从旧框架写法迁到新写法。用大模型批量改代码,真正决定成败的不是模型本身有多强,而是流水线能不能做到三件事:切得开、验得住、退得回。模型的具体上下文长度、并发限制与计费方式,以官方文档当前版本为准,本文只讲流程。

下面按「报错 → 原因 → 排查 → 兜底 → 预防」展开,所有命令都可以直接照抄改路径。

报错现象

典型场景是这样的:写一个脚本遍历仓库,把每个文件丢给 Claude Opus 5.5,让它输出迁移后的完整文件,然后直接覆盖写回。脚本跑完,git diff --stat 显示 12000 多个文件变更。接着跑 CI,结果如下:

```

FAILED tests/test_order.py::test_calc_total - AttributeError:

'OrderService' object has no attribute '_calc'

FAILED tests/test_pricing.py::test_discount - TypeError: unsupported

operand type(s) for /: 'str' and 'int'

```

同时 python -m compileall app 直接报出语法级错误:

```

File "app/service/order.py", line 214

return self._calc(a,

^

SyntaxError: '(' was never closed

```

模型侧也会留下痕迹,比如返回内容里出现 ... (其余部分保持不变)、外层被 ``` `python ``` 包住、或者在文件末尾追加一段"以上是迁移后的代码,主要改动有……"的说明文字。落盘之后,这些文字就成了源码。

到了想回滚的时候,问题更明显:

```

error: Your local changes to the following files would be overwritten by merge

CONFLICT (content): Merge conflict in app/service/order.py

```

因为所有改动混在一个巨型提交里,git revert 一撤就是全部,没法只退掉出问题的那个模块。

影响范围:迁移分支无法合并,已迁好的部分也没法保留,整条流水线卡死。

可能原因

按出现概率从高到低:

1. 分片边界切错了。按文件或按固定行数切,把一个函数、一个类从中间劈开,模型看到的是残缺的语法结构,输出必然残缺。

2. 上下文给得不够。只喂单个文件,没给被调用方的签名、调用方的用法、类型定义和对应测试,模型只能猜,猜出来的方法名自然对不上。

3. 没有验证闸门。模型输出直接落盘,没有语法检查、没有静态检查、没有测试,错误一路带到 CI。

4. 输出格式不稳定。被截断、被 Markdown 包裹、加了散文解释、用了省略号偷懒。

5. 缺少幂等性。重跑一次,已经在迁移后的代码上又改一遍,产生二次改写甚至回退。

6. 提交粒度太粗。回滚单元和生成单元不是同一个,退无可退。

7. 并发冲突。多个任务同时改同一个文件,后写的覆盖先写的。

8. 环境差异造成的假失败。换行符 CRLF/LF、文件编码、可执行权限位变化,让 diff 看起来很大,其实代码没变。

逐条排查与解决

原因 1:分片边界切错

判断方法:统计所有 SyntaxError 报错的行号,如果集中在函数或类的中段,基本就是切片问题。用语法树按顶层定义切分:

```python

tools/list_units.py

import ast, pathlib, json, sys

def units(p):

try:

tree = ast.parse(p.read_text(encoding="utf-8"))

except SyntaxError as e:

return [{"file": str(p), "error": str(e)}]

out = []

for node in tree.body:

if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):

out.append({"file": str(p), "name": node.name,

"start": node.lineno, "end": node.end_lineno})

return out

root = pathlib.Path(sys.argv[1])

shards = []

for p in root.rglob("*.py"):

shards.extend(units(p))

print(json.dumps(shards, ensure_ascii=False, indent=2))

```

```bash

python tools/list_units.py app > migration/units.json

```

前端项目同理,用 TypeScript 编译器 API 或 tree-sitter 按顶层声明切。原则只有一条:分片边界必须落在完整的语法单元上。宁可多切几刀,也不要把函数劈成两半。单个分片喂给模型时还要留出上下文余量,别贴着上限切。

原因 2:上下文不足

判断方法:失败用例是否集中在跨模块调用?diff 里有没有出现仓库中根本不存在的方法名?

```bash

git grep -n "_calc" -- '*.py'

git grep -rn "from app.service.order import"

```

解决方式是给每个分片构造一个"依赖闭包":本分片的完整源码 + 被调用函数的签名 + 调用方的使用示例 + 相关测试文件。可以半自动化收集:

```bash

找出分片内引用的外部符号

grep -oE "from [a-zA-Z0-9_.]+ import [a-zA-Z0-9_, ]+" shard_001.py

把对应定义文件一起塞进 prompt

git grep -l "def _calc" -- '*.py'

```

原因 3:没有验证闸门

闸门要分层,任何一层不过就不允许合并:

```bash

L0 语法层

python -m compileall -q app

L1 静态层

ruff check app && mypy app

L2 相关单测(只跑受影响模块,速度快)

pytest -q tests/test_order.py tests/test_pricing.py

L3 全量(在合并批次上跑)

pytest -q tests/

```

前端对应 npx tsc --noEmitnpx eslintnpx vitest run

关键动作是闸门自检:故意在一个分片里注入一行语法错误,看 CI 是否真的拦住。拦不住,说明闸门形同虚设。

原因 4:输出格式不稳定

要求模型只输出 unified diff 或严格 JSON,不要散文。落盘前先校验:

```bash

git apply --check --3way shards/001.patch && git apply --3way shards/001.patch

jq -e . < out/shard_001.json > /dev/null

```

校验失败就重试,重试时把上一次的报错原文一起带回去,最多 3 次。3 次不过,扔进人工队列,不要无限重试烧时间。

原因 5:缺少幂等性

落盘前确认工作区干净,重跑前先回滚上一轮:

```bash

test -z "$(git status --porcelain)" || { echo "工作区不干净,先提交或清理"; exit 1; }

```

每个分片独立成一个 commit,message 里带上分片 ID:git commit -m "migrate: shard-001"。这样重跑某一分片时,先 git revert 掉它的旧 commit 再重新生成。

原因 6:回滚粒度太粗

回滚单元必须等于合并单元等于分片。按分片逆序撤回:

```bash

#!/usr/bin/env bash

set -euo pipefail

for sha in $(git log --format='%H %s' main..HEAD --grep='^migrate: shard-' \

| awk '{print $1}' | tac); do

git revert --no-edit "$sha"

done

```

也可以给每个分片开独立 worktree,互不干扰:

```bash

git worktree add -b shard/001 ../wt-001 main

```

出问题的分片直接 git worktree remove ../wt-001 丢掉,不影响主干。

原因 7:并发冲突

同一文件同一时刻只能被一个分片持有。用一个清单文件加文件锁:

```bash

flock /tmp/migration.lock -c "python tools/generate.py --shard 001"

```

或者更简单:把分片清单按文件路径做成互斥表,调度时先占位,占不到就排队。

原因 8:假失败

```bash

file app/service/order.py # 看编码

git diff --stat --ignore-all-space # 忽略空白后的真实改动量

git config core.fileMode false # 忽略权限位变化

```

在仓库根目录放 .gitattributes,统一换行符:

```

  • text=auto eol=lf

```

都不管用时的兜底方案

降级为人工审核模式。模型只产出 patch,不直接落盘,人工 git apply --check 确认后应用。速度慢,但一定能推进。

缩小迁移范围。先只迁没有外部依赖的叶子模块,主干保持原样。叶子模块跑通之后再逐层往上推,每层都有已验证的下层做基础。

影子运行。新旧实现并行存在,同一份输入分别跑,比对输出。差异率收敛到可接受范围再切流。

长期双栈 + 特性开关。不追求一次性全量替换,用开关按流量比例逐步切换,出问题直接关开关,比回滚代码更快。

标记为人工迁移。某个模块连续失败超过阈值,直接打上"人工迁移"标签移出自动队列。把力气花在能自动化的部分。

如何预防再次发生

迁移前先做一次体检:统计文件数、模块依赖图、测试覆盖率、语法单元分布、循环依赖数量。这份体检报告直接决定分片清单长什么样。

把分片清单入库,比如 migration/shards.json,每个分片记录:ID、包含的文件、依赖分片、当前状态、对应 commit sha、生成时使用的模型与提示模板版本。清单是唯一事实来源,回滚、重跑、统计进度都读它。

把验证闸门写进 CI,并且定期做闸门自检——注入一个已知错误,确认能被拦住。

每次生成都留痕:输入内容的哈希、输出内容的哈希、验证结果、重试次数。将来出现"为什么这个文件变成这样"的疑问,能一路查回去。

上线前在预发环境演练一次全量回滚,计时。回滚要多久、会不会冲突、冲突怎么解,这些必须提前知道,而不是出事当天现学。

最后,坚持小步合并。一次只合并一个分片批次,批次越小,出问题时定位越快、回滚代价越低。

68 万行一天迁完,靠的不是模型一口气吃下整个仓库,而是每个分片都能独立生成、独立验证、独立退回。流水线设计对了,规模就只是数字。

AI 生成本文由 AI 基于公开信息自动生成,仅供参考。