はじめに:作るのは速くなったのに、「作り替える」はなぜ怖いままなのか
正直に言うと、AIが来てから、新しいコードを書くのはほんとに速くなりました。
でも、不思議なことに、もうひとつの仕事だけはあんまり楽になってない気がするんです。
それが 「移行(マイグレーション)」 。
たとえばこういうやつです。
-
React 17を18に上げたら、テストが数百ファイル落ちた - テスト基盤を
EnzymeからReact Testing Libraryに乗り換えたい。対象、3千ファイル -
Spring Boot 2を3に上げたい。javaxをjakartaに全部書き換え - 古い社内ライブラリ
legacy_fetch()を、新しいhttpClient.get()に全置換したい
ガイドはある。やり方も分かる。なのに着手できない。理由はだいたい一緒で、 対象が「1ファイル」じゃなくて「数百〜数千ファイル」だから なんですよね。
ここで多くの人が、こう考えます。
「もうこれ、AIに全部投げちゃえばよくない?」
その気持ち、めっちゃ分かります。僕も最初そう思いました。
でも、 移行を“全部AIに丸投げ”すると、たいてい静かに事故るんです。 一見うまくいったように見えて、後からじわじわ壊れていく。かといって全部手作業は、終わらない。
今日お伝えしたいのは、この 二択の「外側」に道がある という話です。
具体的には、移行を 「決定的なcodemod」「LLM」「人間」の3つのレイヤーに仕分ける という設計。これは僕の思いつきではなくて、2026年時点でAirbnbやGoogleが実際に大規模移行をやり切ったときの「勝ち筋」を、個人やチームの規模に縮小して移植できる形にしたものです。
AI開発にまだ慣れていない方でも追えるように、用語はぜんぶ噛み砕いて進めます。コード例とプロンプト例も、そのまま試せる粒度で置いておきますね。
そもそも「移行」って、技術的には何をしてるのか
まず言葉から、ゆっくりいきましょう。専門用語って、意味が分かると一気に怖くなくなるので。
移行(マイグレーション / migration) は、ざっくり言うと 「引っ越し」 です。
家の引っ越しを想像してみてください。荷物(=コードの中身)はだいたい同じなのに、新しい家(=新しいライブラリやフレームワーク)のルールに合わせて、置き場所や形を変えないといけない。コンセントの形が違う、間取りが違う。だから「中身は同じなのに、全部触らないといけない」。これが移行のしんどさの正体です。
ここで、引っ越しに出てくる登場人物(用語)を3つだけ先に紹介します。
-
codemod(コードモッド) … 「定型的な書き換えの型紙」。たとえば「
legacy_fetch(x)という形を見つけたら、全部httpClient.get(x)に置き換える」みたいな機械的な変換を、人間じゃなくてプログラムにやらせる仕組みのこと。「code(コード)+ modification(修正)」の略です。 - AST(抽象構文木 / Abstract Syntax Tree) … コードを「文字列」じゃなくて「文の骨組み(木構造)」として捉えたもの。人間が「主語・述語・目的語」で文を理解するのに近いです。codemodは、この骨組みを見て書き換えるので、ただの文字列置換よりずっと正確。
- long tail(ロングテール / 長い裾野) … 「型紙にハマらない例外だらけの少数派」。移行対象の8割は型紙でサクッといける。でも残り2割が、一個一個ちがう顔をしていて、機械的な型紙じゃ処理できない。この「裾野」が、移行の本当の難所です。
この3つを覚えておくと、後の話が一気に立体的になります。とくに 「型紙でいける8割」と「型紙にハマらない2割」をどう分けるか 。ここが今日の主役です。
2026年の事実:Airbnbの「6週間」を、丸投げ成功談として読まない
「AIで移行」と聞くと、まずこの2つの事例が出てきます。どちらも一次情報で確認できる、信頼できる事例です。両方とも、すごい数字が出ます。でも、 数字の読み方を間違えると、真逆の教訓を持って帰ることになる ので、そこを一緒に見ていきましょう。
事例1:Airbnb — Enzyme→RTL、約3,500ファイルを6週間で
Airbnbは、テスト基盤を Enzyme から React Testing Library へ移行しました(Airbnb Tech Blog / InfoQ)。
- 対象:約 3,500 のテストファイル
- 手作業の見積り: 1.5年
- 実際: 6週間 で完了
- 自動移行の成功率: 97% 。残り3%は、LLMが出した結果を「下書き」として人間が手で仕上げた
「ほら、やっぱりAIに任せれば一瞬じゃん」と思いますよね。でも、Airbnbがやったことの中身を見ると、印象が変わります。彼らは、こういう手順を踏んでいます。
- 移行を 1ファイルごとの離散したステップに分割 した
- それを 並列化 した
- 失敗したら 設定可能なリトライループ で何度も投げ直した
- プロンプトに 文脈(周辺コード・移行ルール)を厚く追加 した
- 型紙にハマらない複雑なファイル(=long tail)に、 個別のプロンプトチューニング を重ねた
つまり、 「LLMにポンと投げたら終わった」わけじゃない んです。むしろ、LLMの周りに「分割・並列・リトライ・検証・文脈追加」という頑丈なパイプラインを組んだから、6週間で済んだ。LLMは主役というより、 頑丈な工場のラインで動く一工程 でした。
事例2:Google — int32→int64の一括移行
Googleは、社内の大規模移行(たとえば32bit整数を64bitに変える、みたいなやつ)にLLMを使いました(arXiv:2504.09691, FSE 2025)。
- 仕組み: 変更すべき箇所を自動で特定(change location discovery) → LLMが編集を生成 → 複数の検証(validation) → 最後は 開発者が確認して受け入れ(accept)
- 規模:39の移行、開発者3名、12か月で595件の変更・93,574箇所の編集
- そのうち 74.45% の変更、 69.46% の編集がLLM生成
- 手作業比で 約50% の時間削減
ここでも構造は同じです。 「箇所の特定」「検証」「人間の受け入れ」 という枠があって、その中の「編集の生成」をLLMが担当している。LLMが7割を生成しても、 最終的にOKを出すのは人間 なんですよね。
この2つから持って帰るべき教訓
ここが一番大事なところです。
AirbnbもGoogleも、勝因は「優秀なLLM」じゃなくて「LLMを安全に動かすパイプライン」だった。
「移行をAIに丸投げして成功した話」ではなく、 「移行を仕分けて、機械にできる検証は機械にやらせて、判断だけ人間が握った話」 なんです。
だから僕らがやるべきは「もっと賢いモデルを待つ」ことじゃない。 手元の移行を、ちゃんと仕分けること 。次の章でその仕分けをやります。
移行を「3つのレイヤー」に仕分ける
移行のタスクを、性質ごとに3つに分けます。これが今日の背骨です。
| レイヤー | 中身の性質 | 誰に任せる | 具体例 |
|---|---|---|---|
| ① 機械的・規則的 | 「この形→この形」が明確。例外が少ない | 決定的なcodemod | importパスの一括変更、関数名のリネーム、引数の並べ替え |
| ② 文脈依存・判断が要る | 形が一定じゃない。周辺の意図を読む必要がある | LLM(検証つき) | テストの書き換え、非推奨APIの意味を保った置換、エラーハンドリングの作り替え |
| ③ 設計判断・不可逆 | 間違うと戻せない。方針そのものを決める | 人間 | 移行の方針決定、破壊的変更の承認、本番データに触る操作 |
ポイントは、 「全部AI」でも「全部手作業」でもなく、性質で切り分ける ということ。
そして大事な順番があります。 ①の機械的な部分を、先に決定的codemodで“確定”させてしまう 。そうすると、②でLLMに任せる対象が「型紙にハマらない裾野」だけにギュッと絞られる。LLMの仕事が減れば、非決定性のリスクもコストも検証の手間も全部減ります。
ここで、当然こういう疑問が出ますよね。
「機械的な部分も、別にAIにやらせればよくない? なんでわざわざ別のツールを使うの?」
すごく良い問いです。次の章で正面から答えます。
なぜ「機械的な部分」を、わざわざ決定的ツールでやるのか
①の機械的な変換をLLMに丸投げすると、一見うまくいくのに、 静かに事故る 。その理由は、大きく4つあります。
1. 非決定的だから、同じ入力でも結果がブレる
LLMは、同じコードを渡しても、毎回まったく同じ書き換えを返すとは限りません。3,500ファイルを処理して、3,480個は同じルールで変換されたのに、20個だけ微妙に違う書き方になる。この「揺らぎ」が、後でレビューを地獄にします。決定的なcodemodなら、 同じ入力には必ず同じ出力 。これは大規模になればなるほど効いてきます。
2. 差分(diff)が読めなくなる
codemodの変換は「ルールが1個」なので、レビューも「そのルールが正しいか」を1回確認すればいい。でもLLMが3,500ファイルを個別に書き換えると、3,500通りの差分が生まれて、人間が全部目で追えなくなる。 「読めない差分は、レビューしていないのと同じ」 です。
3. コストとレイテンシが爆発する
「importを1行変える」だけのために、毎ファイルLLMを呼ぶ。3,500回。これはお金も時間ももったいない。機械的な変換は、ローカルで一瞬で終わる決定的ツールのほうが、速くて安い。
4. 検証できない(テストできない)
codemodのルールは「入力コード → 期待する出力コード」という形でユニットテストが書けます。つまり 変換そのものをテストで守れる 。LLMの一回限りの出力は、そういう形では守りにくい。
まとめると、こうです。
機械的にできることは、機械的にやる。 AIは“魔法”じゃなくて“高い道具” だから、安い道具で済むところに使わない。
これ、移行に限らずAI活用全体に効く考え方だと思っていて。 「AIにできるか」じゃなくて「AIにやらせる必要があるか」で判断する 。能力ではなく、必要性で切る。ここがブレないと、移行はだいぶ安全になります。
決定的codemodの実物:ast-grep / jscodeshift / OpenRewrite
では、機械的な①レイヤーを担う「決定的codemod」の代表を、実物で見ていきましょう。3つ紹介しますが、 まず1つ(ast-grep)だけ覚えれば十分 です。
① ast-grep — 多言語で速い、最初の一本
ast-grep は、コードを「文字列」じゃなくて「AST(文の骨組み)」として検索・置換するツールです。Rust製でめちゃくちゃ速くて、JS/TS/Python/Java/Goなど多言語に対応しています。
たとえば「古い legacy_fetch(url) を、新しい httpClient.get(url) に全置換したい」とします。文字列置換(sed)だと、コメントの中の legacy_fetch まで誤爆したり、my_legacy_fetch みたいな別物まで巻き込んだりする。ast-grepはASTで見るので、 「本当に関数呼び出しになっているやつ」だけ を正確に捕まえます。
まずはインストールして、書き換えずに「どこがヒットするか」だけ見てみる(ここ超大事。いきなり書き換えない)。
# インストール(npm の例)
npm install --global @ast-grep/cli
# まず「検索だけ」して、対象がどこにあるか目で確認する(破壊しない)
ast-grep --pattern 'legacy_fetch($URL)' --lang ts src/
# 問題なさそうなら --rewrite で置換プレビュー、最後に --update-all で適用
ast-grep --pattern 'legacy_fetch($URL)' --rewrite 'httpClient.get($URL)' --lang ts src/
$URL は「ここに何が入ってもいい」というプレースホルダ(メタ変数)です。legacy_fetch("/users") でも legacy_fetch(buildUrl(id)) でも、中身を保ったまま httpClient.get(...) に移してくれます。
もう少し複雑なルールは、YAMLファイルに書けます。これだと「テスト・レビュー・再利用」がしやすい。
# rules/migrate-legacy-fetch.yml
id: migrate-legacy-fetch
language: typescript
rule:
pattern: legacy_fetch($URL)
fix: httpClient.get($URL)
# このルール自体に「入力→期待出力」のテストを付けられるのが決定的ツールの強み
# ルールを使って一括スキャン → 適用
ast-grep scan --rule rules/migrate-legacy-fetch.yml --update-all
ここがLLMとの決定的な違い です。このYAMLルールは、レビューするのが「3,500個の差分」じゃなくて「ルール1個」で済む。そしてルール自体をテストで守れる。 人間が確認すべき面積が、桁で小さくなる んですよね。
② jscodeshift — JS/TSで込み入った変換をしたいとき
jscodeshift(Meta製)は、JS/TS特化で、変換ロジックをJavaScriptでガッツリ書けるツールです。「単純なパターン置換じゃなくて、条件分岐や周辺コードの状態を見て書き換えたい」ときに向いています。
// transforms/rename-prop.js
// <OldButton type="..."> を <NewButton variant="..."> に変える例
module.exports = function (fileInfo, api) {
const j = api.jscodeshift;
const root = j(fileInfo.source);
// JSXの <OldButton> 要素を探す
root.findJSXElements('OldButton').forEach((path) => {
// タグ名を NewButton に
path.node.openingElement.name.name = 'NewButton';
if (path.node.closingElement) {
path.node.closingElement.name.name = 'NewButton';
}
// 属性 type を variant にリネーム
path.node.openingElement.attributes.forEach((attr) => {
if (attr.type === 'JSXAttribute' && attr.name.name === 'type') {
attr.name.name = 'variant';
}
});
});
return root.toSource();
};
npx jscodeshift -t transforms/rename-prop.js src/ --extensions=tsx
ast-grepの宣言的なYAMLでは表現しきれない「手続き的な判断」を入れたいときの選択肢、くらいに覚えておけば大丈夫です。
③ OpenRewrite — Java/Springなどの「フレームワーク移行レシピ」
OpenRewrite(Moderne)は、主にJava/Kotlin界隈で強い移行エンジンです。特徴は LST(Lossless Semantic Tree) という、ASTより詳しい木(型情報や空白まで保持する)を使うこと。だから「型を見て安全に書き換える」ような、フレームワーク移行(Spring Boot 2→3、JUnit 4→5など)の 既製レシピ が充実しています。
# rewrite.yml — 既製レシピを組み合わせる「宣言的レシピ」の例
type: specs.openrewrite.org/v1beta/recipe
name: com.example.MigrateToSpringBoot3
displayName: Migrate to Spring Boot 3
recipeList:
- org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_0
# javax → jakarta の名前空間移行なども既製レシピでまとまっている
ポイントは、 「よくある移行は、もう誰かがレシピにしてくれている」 こと。自分でLLMに考えさせる前に、 既製の決定的レシピがないか探す 。これが移行を速く・安全にする近道です。
使い分けの早見表
| ツール | 得意 | こんなときに |
|---|---|---|
| ast-grep | 多言語・高速・宣言的 | まず最初の一本。パターン置換の8割はこれで足りる |
| jscodeshift | JS/TSで手続き的な複雑変換 | 周辺の状態を見て条件分岐したい変換 |
| OpenRewrite | Java/Springの型を見た移行 | フレームワークのバージョン移行(既製レシピがある) |
迷ったら、 「自分の移行ルールを既製レシピや1個のパターンで書けないか?」を先に考える 。書けるなら、そこはLLMの出番じゃありません。
曖昧な「裾野」はLLMに:per-fileパイプラインを組む
さて、決定的codemodで①の8割を片付けると、残るのは「型紙にハマらない裾野(long tail)」です。ここは、形が一定じゃないので、 意味を読んで書き換える必要がある 。ここでようやくLLMの出番です。
ただし、 LLMにフォルダごと丸投げしない 。Airbnbがやったように、 1ファイルずつの小さなジョブに分割して、検証とリトライで囲う 。これが鉄則です。
考え方を擬似コードで書くと、こんな感じになります(TypeScript風)。
// per-fileの移行パイプライン(要点を抜き出した擬似コード)
type FileResult =
| { file: string; status: "done" }
| { file: string; status: "manual"; reason: string };
async function migrateFile(file: string): Promise<FileResult> {
const MAX_RETRY = 2;
const original = await readFile(file);
for (let attempt = 0; attempt <= MAX_RETRY; attempt++) {
// 1. LLMに「このファイルだけ」を移行させる。周辺文脈とルールをプロンプトに厚く渡す
const migrated = await llmMigrate({
code: original,
rules: MIGRATION_RULES, // 移行のルール(人間が定義)
examples: GOLDEN_EXAMPLES, // 正解例(before/after)を数個
feedback: lastError, // 前回の失敗内容を次の試行に渡す(ここがリトライの肝)
});
// 2. 機械でできる検証は、機械にやらせる(人間を待たせない)
await writeFile(file, migrated);
const lint = await run(`eslint ${file}`);
const types = await run(`tsc --noEmit`);
const test = await run(`vitest run ${testFor(file)}`);
if (lint.ok && types.ok && test.ok) {
return { file, status: "done" }; // 3点とも緑なら合格
}
lastError = summarizeFailures(lint, types, test); // 失敗を要約して次の試行へ
}
// 3. リトライしても緑にならなければ、無理させない。人間の山に積む
await restoreFile(file, original); // ← 壊れたまま放置せず、元に戻すのが大事
return { file, status: "manual", reason: lastError };
}
このパイプラインの肝は3つです。
- per-file(1ファイル単位) … 失敗の影響が1ファイルに閉じる。並列化もしやすい。
- retry with feedback(失敗を次に渡すリトライ) … 「型エラーが出た」という事実を次の試行のプロンプトに入れると、成功率がぐっと上がる。Airbnbの「設定可能なリトライループ」はこれです。
-
諦めたら人間に積む … 何度やっても緑にならないファイルは、無理に通さない。
manualとして人間のレビュー待ちに積む 。Airbnbの「残り3%は手動」がまさにこれ。
「全部を自動で通そう」としないこと。 8割をcodemodで、残りの大半をLLMで、最後の数%を人間で。 この比率を最初から織り込んでおくと、移行は止まりません。
「測れる移行」にする:テストの緑だけを信じない検証ゲート
移行で一番危ないのは、 「動いてるように見えるけど、意味がズレている」 という静かな事故です。だから移行は、 「測れる状態」 にしてから進めます。
検証は、 3点セット で見ます。1つだけだと必ず穴が空くので。
# 検証ゲート(最低この3つを緑にしてから次へ進む)
set -e
# 1. テスト:振る舞いが壊れていないか
vitest run
# 2. 型・lint:構造的に壊れていないか
tsc --noEmit
eslint . --max-warnings=0
# 3. 差分の量を機械的にチェック:意図より大きく変わっていないか
# 1ファイルの変更が異常に大きい=LLMが余計なことをしたサイン
git diff --stat | awk '$3 > 200 { print "⚠ 変更が大きすぎる: " $1 }'
ここで強く言いたいのは、 「テストが緑=移行が正しい」ではない ということ。理由は2つあります。
- カバレッジの穴 … テストが無い部分は、壊れても緑のまま。
-
意図の劣化 … LLMが「テストを通すために、テストの中身を骨抜きにする」ことがある。たとえば、本当は中身を検証していたテストを、
expect(true).toBe(true)みたいな空っぽのテストに書き換えてしまう。これは緑になるけど、 テストとして死んでいる 。
だから、 最後は人間が差分を読む 。ただし全部じゃなくていい。「変更が大きいファイル」「テスト本体が変わったファイル」など、 リスクの高いものから読む 。ここの優先順位付けは、人間が握るべき判断です。
そして、移行全体の進捗は 「移行台帳」 で1枚に可視化します。これが、明日の自分(と仲間)への置き手紙になります。
# migration-ledger.yml — 移行の進捗を1枚で見える化する台帳
migration: enzyme-to-rtl
owner: frontend-team # ダミー。固有名は書かない
started: 2026-06-21
total_files: 3500
status:
done_by_codemod: 1200 # 決定的codemodで確定した分
done_by_llm: 2150 # LLM+検証ゲートを通った分
manual_queue: 150 # 人間レビュー待ち(裾野)
gates:
- tests_green
- types_pass
- diff_reviewed_for_high_risk
irreversible_hold: # ここは絶対に自動化しない(人間ゲート)
- production_schema_change
- public_api_break
「いま何件終わって、何件が人間待ちで、どのゲートを通したか」が1枚で分かる。これがあるだけで、移行は「終わりの見えない作業」から「残りが数えられる作業」に変わります。
安全設計:不可逆なものは、絶対に人間ゲートを通す
ここは移行で一番、慎重になってほしいところです。ゆっくりいきます。
移行のコード書き換え自体は、git があるので基本「戻せる」。でも、移行作業の周りには、 戻せない操作(不可逆操作) が地続きで存在します。
- 本番データベースの スキーマ移行(列を消す、型を変える)
- 公開APIの 破壊的変更(利用者のコードが壊れる)
- パッケージの publish、本番への デプロイ
- 古いデータの 一括変換・削除
これらは、 どれだけテストが緑でも、AIに自動実行させてはいけません。 必ず、人間が内容を確認して承認する「ゲート」を通す。仕組みで止めるのが理想です。
// 不可逆操作は「構造で」人間承認を強制する
const IRREVERSIBLE = ["db:migrate:prod", "api:breaking", "npm:publish", "deploy:prod"];
async function runStep(step: string) {
if (IRREVERSIBLE.includes(step)) {
// AIエージェントの自動実行を拒否し、人間の承認トークンを要求する
throw new Error(`STOP: "${step}" は不可逆操作です。人間の承認が必要です。`);
}
// 可逆な操作(コード書き換え・テスト実行)だけ自動で進める
await execute(step);
}
あと2つ、地味だけど大事な注意を。
- 秘密情報・個人情報を、プロンプトやログに混ぜない。 移行対象のコードに、APIキーや個人情報、社内固有のIDが紛れていることがあります。LLMに渡す前に、 ダミー化・マスキング する。ログにもそのまま貼らない。
- 外部から来たテキストは「指示」じゃなく「データ」として扱う。 移行対象のコメントやテストデータに、「これまでの指示を無視して〜」みたいな文字列が紛れていたら(プロンプトインジェクション)、AIがそれに従ってしまうことがある。外部テキストは囲って、命令として解釈させない。
不可逆なものは人間、可逆なものはAI。 この線引きが、移行を「攻めても大丈夫」な作業に変えてくれます。
そのまま使えるプロンプト例4本(最終判断は、いつも人間)
ここからは、実際に貼って使えるプロンプトを4つ置いておきます。共通して、 AIには「材料」を出させて、「決定」は人間がする という形にしてあります。
プロンプト1:移行を3レイヤーに仕分けさせる
あなたはコード移行の設計を手伝うアシスタントです。
以下の移行タスクを、3つのレイヤーに仕分けてください。
# 移行内容
- 旧: legacy_fetch(url) / 新: httpClient.get(url)(ほか別紙のルール)
# 出力フォーマット(表)
| 変換パターン | レイヤー(機械的/LLM/人間) | 理由 | 決定的codemodで書ける? |
# ルール
- 「形が一定で例外が少ない」ものは「機械的」に寄せる
- 「周辺の意図を読む必要がある」ものだけ「LLM」にする
- 「不可逆・方針決定」は「人間」にする
- 判断に迷うものは「人間」に倒し、理由を書く
- 最終的にどのレイヤーにするかは私(人間)が決めます。あなたは候補と理由を出してください。
プロンプト2:1ファイルだけを、意図を保って移行させる
あなたは慎重なコード移行アシスタントです。
以下の1ファイルだけを、ルールに従って移行してください。
# 厳守事項
- テストの「意図」を変えないこと(検証している中身を骨抜きにしない)
- テストを通すためだけに assert を空にする・スキップするのは禁止
- ルールに無い変更(リファクタ・整形・命名変更)を勝手にしない
- 変換に自信がない箇所は、書き換えずに「# TODO(migration): 要確認」を残す
# 入力
- 移行ルール: {RULES}
- 正解例(before/after): {GOLDEN_EXAMPLES}
- 対象ファイル: {FILE}
# 出力
- 移行後のコード全文
- 末尾に「自信のない箇所」を箇条書きで(無ければ「なし」)
プロンプト3:移行の差分をレビューさせる(ドリフト検知)
以下は、ある移行の before / after の差分です。
「意図しない振る舞いの変化(ドリフト)」が無いかをレビューしてください。
# 観点
- 振る舞いが変わっていないか(条件分岐・境界値・エラー処理)
- テストの検証内容が弱くなっていないか(空の assert 化、skip化)
- ルールの範囲外の変更が混ざっていないか
- 隠れた副作用(順序依存・非同期の待ち漏れ)はないか
# 出力
- リスク: High / Medium / Low
- 具体的な懸念点(該当行を引用して)
- 「人間が必ず目視すべき箇所」のリスト
※ 通す/戻すの最終判断は私がします。あなたは懸念の洗い出しに徹してください。
プロンプト4:失敗ファイルの原因を分類させる
移行パイプラインで「manual(人間送り)」になったファイル群のエラー要約です。
原因をグループに分類し、それぞれに「決定的codemodで吸収できそうか」を判定してください。
# 入力
- 失敗ファイルとエラー要約のリスト: {FAILURES}
# 出力(表)
| 原因グループ | 件数 | codemod化できる? | 次の一手 |
- 「同じ原因が大量にある」グループは、新しいcodemodルールを作る候補として強調してください。
- 個別対応が必要なものと、ルール化で一掃できるものを、はっきり分けてください。
このプロンプト4が地味に効きます。 「人間送りになった裾野」をもう一度分類すると、その中に“実は機械化できる新しいパターン”が眠っている ことがよくある。そこを見つけてcodemod化すると、人間の山がまた小さくなる。 codemod → LLM → 人間 → 分類して codemod に戻す 。このループが、移行を加速させます。
落とし穴6つと、撤退ライン
最後に、つまずきポイントを正直に共有しておきます。
| # | 落とし穴 | どうなるか | 対策 |
|---|---|---|---|
| 1 | 機械的な変換までLLMに丸投げ | 差分が読めない・非決定・コスト爆発 | ①は決定的codemodで確定させる |
| 2 | フォルダごとLLMに渡す | 失敗の影響範囲が広がり、リトライも効かない | per-file(1ファイル単位)に分割 |
| 3 | テストの緑だけを信じる | 骨抜きテストで「動くけど死んでる」 | 検証3点セット+高リスク差分は人間が目視 |
| 4 | 失敗ファイルを無理に自動で通す | 静かに壊れたコードが本番へ | 緑にならなければ元に戻し、人間送りに |
| 5 | 不可逆操作を自動化 | 戻せない事故(DB/公開/デプロイ) | 構造で人間承認ゲートを強制 |
| 6 | 既製レシピを探さず自作 | 車輪の再発明・品質も落ちる | ast-grep/OpenRewriteの既製を先に探す |
そして、 撤退ライン(やめどき) も先に決めておきます。移行は「やり切る」ことより「安全に止まれる」ことのほうが、ときどき大事なので。
- 成功率が頭打ちなら、いったん止める。 リトライを増やしても緑率が上がらなくなったら、残りは人間の領域。AIを回し続けない。
- 裾野が大きすぎたら、移行の範囲を切る。 全部を一度に移行しない。「よく触るファイルから」など、価値の高いところだけ先に。
- 不可逆操作が絡む段階に来たら、必ず人間に戻す。 ここはスピードより安全。
人間とAIの役割分担表
最後に、今日の話を1枚にまとめます。
| 工程 | 人間(What / Why を握る) | AI(How を担う) |
|---|---|---|
| 方針決定 | 何を・なぜ移行するか、範囲を決める | 候補と影響範囲の洗い出し |
| 仕分け | 3レイヤーの最終決定 | 機械的/LLM/人間の仕分け案を出す |
| 機械的変換① | codemodルールのレビュー | (ここは決定的ツール。AIは補助でルール下書き) |
| 裾野の変換② | ゲート設計・正解例の用意 | per-fileの書き換え+自己検証 |
| 検証 | 高リスク差分の目視・通す/戻すの判断 | テスト・型・差分量の機械チェック |
| 不可逆操作③ | 承認(ここだけは絶対に握る) | 提案まで(実行はしない) |
きれいに言うと、 「何を・なぜ」は人間、「どう」はAI 。移行という大仕事でも、この線は変わりません。
おわりに:今日の検証ログは、明日の自分への「あざっす」
移行って、ずっと「気が重い作業」の代名詞でした。終わらないし、怖いし、誰もやりたがらない。
でも、ここまで見てきたように、移行は 「丸投げ or 手作業」の二択じゃない 。 機械的な8割は決定的codemodで確定させて、曖昧な裾野はLLMに検証つきで任せて、不可逆と判断だけ人間が握る。 こう仕分けるだけで、1.5年の作業が6週間になる世界線が、ちゃんとあるんですよね。
そして、移行の最中に積み上げた 検証ゲート・移行台帳・codemodルール は、消えてなくなりません。それは次の移行でも使えるし、チームの誰かが引き継ぐときの地図になる。 今日きれいに残した検証ログは、半年後の自分が「あの時の自分、あざっす」って言える置き手紙 なんです。これは、過去の自分を責める軸じゃなくて、未来の自分を思いやる軸。
ここがすごく好きなところで。 移行をやり切る力、仕分ける設計力って、AIに奪われないんですよ。 むしろAIが強くなるほど、「どこを機械に、どこを人間に切るか」を決められる人の価値が上がる。コードを書く速さじゃなくて、 作り替える判断の質 。それは積み上がる資産(Code as Capital)だと思います。
最後に、明日からの小さな4ステップを置いておきますね。
- いま後回しにしている移行を1つ選び、 その中の「機械的な8割」を見つける
- その8割を、 ast-grepで「検索だけ」してみる(まだ書き換えない)
- ルールを1個書いて、 1ファイルだけ 変換 → テストが緑か確認する
- 残った「裾野」を、 per-fileでLLMに渡す前に、不可逆操作が混ざってないかだけ確認する
全部を一気にやらなくていいです。1ファイルから。 小さく仕分けて、検証して、戻れるようにして、また少し進む。 その積み重ねが、いつのまにか「3千ファイルの移行、終わったわ」に変わっています。
今日もここまで読んでくれて、あざっす。あなたの移行が、静かな事故じゃなくて、静かな前進になりますように。
参考にした一次情報
- Airbnb Engineering「Accelerating large-scale test migration with LLMs」 https://medium.com/airbnb-engineering/accelerating-large-scale-test-migration-with-llms-9565c208023b / InfoQ要約 https://www.infoq.com/news/2025/03/airbnb-llm-test-migration/
- 「Migrating Code At Scale With LLMs At Google」(FSE 2025, arXiv:2504.09691) https://arxiv.org/abs/2504.09691
- ast-grep 公式 https://ast-grep.github.io/
- jscodeshift https://github.com/facebook/jscodeshift
- OpenRewrite 公式ドキュメント https://docs.openrewrite.org/