AIによる大量コードのマイグレーションスクリプト作成の試行錯誤と知見
大量コードのマイグレーション with AI
「アーキテクチャの刷新のため、1000 ファイルほどを書き換えてください」
こんな要件が明日あなたの手元にも飛び込んでくるかもしれません。
私には先日飛び込んできました。
でも今の時代、我々には AI があります。
AI に全部やってもらいましょう!
いやー、便利な時代でよかったよかった!
本当にうまくいく?
ということで、意気揚々と AI ツールを使って大量ファイルの書き換えにトライしてみると、実際にはなかなか厳しい点が見えてきます。
実行の遅さ
Claude Code・Cursor・Codex といった AI によるコーディングツールを使っている方なら日々体感していると思いますが、大量ファイルを処理させようとすると、単純に実行に非常に時間がかかります。
1000 ファイル以上の書き換えとなると、数十分では済まない時間を待たされる可能性も高いでしょう。
実行結果が期待通りでなかった場合などには再実行する必要もあり、そのたびに数十分待たされてしまうと、トライ&エラーをしているだけで退勤の時間になります。生産性とは一体。
そもそも期待通りに動かない
ファイル数が膨大だと、どうしてもコンテキストウィンドウを圧迫します。
プロンプトで指示を完璧にし、かつコンテキストを忘れない小さいスコープで作業させるなど、工夫によってある程度安定化させることはできます。
しかし、それでも実行のたびに結果がブレてしまったり、意図通りの変換をしてくれない可能性も出てきます。また、そもそも結果が「期待通りか?」の判断も大変です。
再現性の無さ
リトライするたびに実行結果が変わるのも懸念点となります。
マイグレーションの実行を何度か試行する中で、変換に成功していたはずのファイルがおかしくなってしまった…、というケースも珍しくないでしょう。
同じ対象ファイルに同じプロンプトで変換を指示しても違う結果になることもあり、実行のたびに必ず全ファイルの変換結果を精査する必要があります。
解決策: マイグレーション用のスクリプトを書かせる
こういった課題に対しては、直接 AI に書き換えを命じるのではなく、マイグレーション用のスクリプトや codemod を作成させるのが効果的です。
このアプローチについては、すでに他の方も記事や登壇資料などで紹介されています。
- https://zenn.dev/socialplus/articles/ai-powered-codemod-refactoring
- https://speakerdeck.com/yamachu/let-ai-generate-code-to-ensure-reproducibility
事例 / Hugo → Astro へのマイグレーション
実際にマイグレーションスクリプトを作成した事例を一つご紹介します。
(AI への指示とその結果がベースであり、完全な再現性はないので、参考になりそうな部分だけ掻い摘んで頂けると良いかなと思います。)
Go 製の静的サイトジェネレータである Hugo で作成された Web サイトを、Astro へマイグレーションする、という必要が生じました。
元のコンテンツは Markdown で記述されており、1000 ファイルを超える規模になっています。
しかし、Hugo が固有で提供する機能が多く利用されており、それらはすべて Astro 向けに書き換える必要があります。手動で書き換えるには量が多く、かといって AI に直接任せるのも先に挙げた点から現実的ではないため、マイグレーション用のスクリプトを AI に作成させることにしました。
どのような変換が必要か、少しだけ例を見てみましょう。
Hugo 固有の機能と Astro 向けの書き換えの例
Shortcodes → Components
Hugo では Shortcodes と呼ばれる仕組みで、繰り返し用いたいパーツなどを HTML でテンプレート化します。これは引数も取ることができます。
<span class="my-image">
<img src="{{ .Get "src" }}" alt="{{ .Get "alt" }}" />
</span>
<!-- my-image Shortcodes を利用する -->
{{< my-image src="https://example.com/image1.png" alt="Example1" />}}
{{< my-image src="https://example.com/image2.png" alt="Example2" />}}
{{< my-image src="https://example.com/image3.png" alt="Example3" />}}
Astro ではコンポーネントとして定義することで同等の機能を再現できます。
---
const { src, alt } = Astro.props;
---
<span class="my-image">
<img src={src} alt={alt} />
</span>;
---
import MyImage from '../components/MyImage.astro';
---
<MyImage src="https://example.com/image1.png" alt="Example1" />
<MyImage src="https://example.com/image2.png" alt="Example2" />
<MyImage src="https://example.com/image3.png" alt="Example3" />
変換時は、利用しているショートコードに応じた import の追加や、{{< my-image ... />}}
といった記述箇所の置換が必要となります。
Render Hooks → MDX & Custom Components
Hugo では、Markdown から HTML に変換する際に、Render Hooks という仕組みで、独自の変換処理でオーバライドできます。
<a href="{{ .Destination | safeURL }}" class="my-link">
{{- with .Text }}{{ . }}{{ end -}}
</a>
<!-- render-link.html の内容で描画される -->
[リンク](https://example.com)
Astro では、MDX での Custom Components の割当てという機能で似た機能を再現できます。
---
const { href, children } = Astro.props;
---
<a href={href} class="my-link">{children}</a>
import MyLink from '../components/MyLink.astro';
export const components = { a: MyLink }
<!-- MyLink コンポーネントで描画される -->
[リンク](https://example.com)
Markdown コンテンツの本体には変化はありませんが、必要に応じて import および export の追加が必要となります。
AI ツールでのスクリプト作成 / 失敗例
ということで、上記の仕様をもとに、Claude Code にスクリプトの作成を依頼しました。
おおむね、次のような形でのプロンプトです。
# Hugo から Astro へのコンテンツマイグレーションスクリプトの作成
静的サイトジェネレータである Hugo 向けに書かれた *.md ファイルを、
Astro で利用可能な *.mdx ファイルに変換するスクリプトを作成してください。
## 変換の仕様
### Shortcodes
〜ショートコードに関する説明〜
### Render Hooks
〜Render Hooks に関する説明〜
## 変換例
〜 変換前と変換後のコードの例 〜
このプロンプトをベースにプランニングから順を追って作成させたところ、
結果として、渡した仕様についてはほぼ完璧な動作のスクリプトが生成されました。
しかし、すべてのコンテンツに対して変換を実行してみると、ファイルは生成されますが、残念ながら大半のファイルが Astro ではビルドできない Invalid なものでした。
仕様の不足
中身を確認してみると、Hugo と Astro のコンテンツでは、Shortcodes や Render Hooks に限らず、さまざまな差異があることがわかりました。
- HTML の扱い
- e.g.
<tr><td>foo</td><tr><td>bar</td></tr>
(閉じタグの欠落) - Hugo : 多少崩れていてもテンプレートとして扱える
- Astro : エラーとなる
- e.g.
- Frontmatter への値の埋め込み
- e.g.
title: "{{< sitename >}} について"
- Hugo : Shortcodes を扱える
- Astro : コンポーネントを埋め込むことはできない
- e.g.
- エスケープが必要な文字種に差異がある
- e.g.
<
や>
など - Hugo : エスケープせずとも描画可能
- Astro : エスケープが必須
- e.g.
- インデントサイズによってレンダリング結果が異なる
- etc...
残念ながら最初に渡したプロンプトだけでは仕様を満たせておらず、多くの追加実装が必要でした。
仕様を洗い出した人間である私が最大のボトルネックだったということですね...
修正と破壊の無限ループが始まる
とはいえ、開発の中で仕様が追加されること自体は珍しくありません。
Claude Code に追加で対応をしてもらいましょう。
そしてここから、まったく前進しなくなります。
仕様を足して変換処理が複雑となったことで、従来うまく変換できた箇所で失敗するケースが発生し始めます。
テストコードも用意させたものの、実装の追加によって既存テストが Fail し、
そのテストを直すと今度は追加した部分がエラーになり…、というループをし始めました。
挙句の果てには次のような状態が多発しました。
- 「テストコードの期待結果を修正します」
- 「テストコードを一時的にコメントアウトします」
- 「変換後のファイルを書き換えて修正します」
- 「できました!」(できてない)
- Approaching Usage Limit ・ resets at 午後 6 時 (午後 3 時の出来事)
そして /clear
を叩いてはプロンプトを工夫してリトライ...、というのを繰り返していました。
改善したアプローチ
試行錯誤した結果、次のアプローチが有効でした。
責務を分割させる
指示しなくてもある程度はやってくれますが、責務を小さく分割させるよう明確に指示するのが非常に重要でした。
今回のような変換処理の場合は、少なくとも各仕様の変換処理は個別の関数として定義させるように指示しました。
変換スクリプトは、責務を分割し、各処理を個別の関数として定義してください。
関数間の依存は無いように実装してください。
例:
- CLI用のエントリーファイル: cli.ts
- Shortcodes の変換処理 : shortcode-processor.ts
- Render Hooks の変換処理 : render-hook-processor.ts
- フロントマターの変換処理 : frontmatter-processor.ts
それぞれの関数にはテストコードを用意して動作を担保してください。
最終的には次のように分割されています。
- cli.ts # スクリプトのエントリ
- convert-content.ts # 変換のメイン処理
- preprocessor.ts # 事前の調整用変換プロセス
- shortcode-processor.ts # Shortcodesの変換
- codeblock-processor.ts # コードブロックの変換
- escape-processor.ts # エスケープ処理
- frontmatter-processor.ts # フロントマターの変換
- heading-processor.ts # 見出しの変換
- html-processor.ts # HTML構造の変換
- image-processor.ts # 画像用記述の変換
- link-processor.ts # リンク用記述の変換
これにより、さらに追加の仕様があった場合や、一部変換に不備があって修正が必要になったとしても、それぞれの影響範囲が閉じているため、意図しない範囲を破壊してしまう頻度が大幅に減りました。
テストと型は偉大
AI ツール関係のナレッジでよく言われることですが、テストコードと型の存在は非常に重要です。特に試行錯誤を繰り返すような実装をする場合では、手戻りを避けるための仕組みが必要となります。
Claude Code を利用していたため、CLAUDE.md でテストに関するルールを細かく記述しておき、既存のテスト結果は尊重するように指示しておきました。TypeScript による型定義も同様に大きな恩恵を得られます。
常にテストと型チェックはパスする状態を維持するのを遵守させることで、自律的に問題点を特定し修正するループが上手く回るようになりました。
TDD で段階的に作成する
一気に作ってほしい気持ちになりますが、段階的に作らせることで、問題を最小化しつつ進めることができました。
実際のところ、私は一度全てのコードを捨て、ゼロから段階的に作り直すよう指示し直しました。
その際には、次のような内容をプロンプトに盛り込んでいます。
- t-wada スタイルの TDD で実装を進める
- 仕様は一つずつ実装し、必ず先にテストを書くこと
- 実装後にはテストを実行し、すべてのテストがパスすることを確認する
失敗した際に、その原因となる範囲を小さくすることで、問題の特定がより的確かつスムーズに行えるようになった体感がありました。
上手くいかないときは一旦リファクタさせる
上記を踏まえていても、仕様追加時に上手くいかないケースもありました。
そういった場合には、一度別のセッションでコードをリファクタさせる、というのも効果的でした。
単純にコードの可読性が悪かったり、複雑性が増したことが失敗する要因なことも多いです。
テストコードが用意されていることを活用し、テストには一切手を加えない形で、複雑なコードの関数の分割や、重複が激しい箇所の共通化などを一度指示することで、結果として別の指示もスムーズに行くようになった、というケースも多かったです。
従来の開発で意識していた点が結局大事だったという話
これらについて改めて考えてみると、従来我々が開発する際に意識していた点とほぼ同じだな、という気付きを得ます。
- 「コードの責務は小さく分割しよう」
- 「テストコードをちゃんと書きましょう」
- 「一度に大量に実装せず、小さく進めるのを意識しましょう」
- 「複雑性が増したらリファクタを検討しましょう」
すべて、どこかで誰かに言った/言われたことがあるような気がします。
まとめ
AI ツールは便利とはいえ、雑に投げてもすべてが上手くいくわけではない、というのは多くの方が経験しているものかなと思います。
しかし、我々が今まで培ってきた開発におけるノウハウで活用できる部分も多くあるため、一度冷静に「いままでどういった流れで開発を進めたら上手くいったっけ?」と考えてみると、プロンプト作成のヒントに繋がるかもしれません。
正直この記事の内容は、様々なところで言及されている内容も多いのですが、改めて実際の事例から得られた体験の共有ということで、少しでも参考になれば幸いです。
Discussion