メインコンテンツへスキップ

AI開発の第一原理:CIを止め、コードレビューを捨て、AIペアプログラミングを採用した理由

Robert
AIDeveloper

この数ヶ月、チームでAI開発ツールの集中的なイテレーションを行ってきた。多くの壁にぶつかり、直感に反する発見もいくつか検証できた。核心的な結論:LLMの開発効率、開発能力、開発品質は完全に実証済みで、疑いの余地はない。ただし、最大の価値を引き出すには、正しい協業方法を身につける必要がある。従来のエンジニアリングプラクティスと一致するものもあれば、まったく逆のものもある。

完璧な方法論を提示するつもりはない。AI の能力は急速に進化しており、今日のベストプラクティスが明日には時代遅れになるかもしれない。以下は実戦ノートに近い:何がうまくいき、何が失敗し、その理由は何か。最も意外だった発見は、従来のエンジニアリングプラクティスへの衝撃だった。私たちは徐々にCIパイプラインを止め、手動コードレビューを放棄し、AIペアプログラミングを全面的に採用するようになった。これは怠慢ではない。実践を通じてより効果的な方法を見つけたのだ。異端に聞こえるかもしれないが、背後には確かなロジックがある。

なぜAIペアプログラミングを全面採用したのか

最初は素朴な考えがあった:AIがこれほど強力なら、一晩中自動で動かして、朝に成果を収穫すればいいのでは?人間は寝て、機械が働く。それぞれの得意分野を活かせる。試してみた。結果は深い教訓となった。

その夜、タスクを設定してAIを自動で働かせ、安心して寝た。翌朝、すべてのユニットテストがパスしていた。生産性の聖杯を見つけたと思い、興奮した。その後、丸一日かけて手動で検証したところ、結果はまったく求めていたものではなかった。さらに悪いことに、以前正しかったコードが壊されていた。コードベース全体が「正常に見えるが、実際はめちゃくちゃ」な状態だった。すべてのテストがグリーンだったため、問題は完璧に隠されていた。危うくこのコードをメインブランチにマージするところだった。

何が問題だったのか?繰り返し振り返った結果、根本原因を見つけた:ドリフト。AIは各ステップであなたの意図から少しずつずれる可能性がある。個々のステップを見ると、ずれは小さく、合理的でさえあり、明らかな欠陥は見つからない。しかし、誰かがタイムリーに軌道修正しないと、ずれは蓄積し続ける。校正されていないコンパスのように、一歩ごとに1度ずれると、100歩後には完全に迷子になる。数時間の自動作業の後、AIは元の目標から大きくずれている可能性があり、自分ではまったく気づかない。

さらに厄介なのは、AIが「自己正当化」し、間違った方向にますます遠くまで進み、ますます自己一貫性を持つようになることだ。自分が軌道を外れているかどうかを立ち止まって疑問視することはない。その視点からは、各推論ステップは論理的だからだ。ずれに対して完璧な正当化を見つけ、テストを修正して誤った実装に合わせることさえあり、最終的に「辻褄の合う」結果を提示する。この自己合理化能力は、正しい方向に向かっているときは資産だが、間違った方向に向かっているときは災害になる。

例えを思いついた:AIはグローバルな視点を持たない極めて有能な実行者のようなものだ。技術力は一流だがビジネス目標の理解が限られているエンジニアに似ている。現在のステップを高品質で実行でき、きれいなコードと明確なロジックを生み出すが、このステップが正しい目標への道の途中にあるかどうかは分からないかもしれない。方向を常に校正するグローバルな視点を持つ人が必要だ。これがペアプログラミングの価値だ。

私は協業アプローチを完全に変え、「完全自動」から「小さなステップの反復」によるペアプログラミングモードに移行した。5-10分ごとにAIの出力を確認し、ずれが見つかったらすぐに引き戻し、エラーが蓄積するのを防ぐ。各マイルストーンを確認してから次のステップに進む。効率が低いように見える(人間がより多くの時間を監視に費やす)が、実際の効果は劇的に向上した。事後の大規模な検証とやり直しが不要になるからだ。やり直しのコストは、途中での軌道修正の10倍から100倍だ。

AIを安心して独立で作業させられるのはどんなときか?確定性の高いタスク:インターフェースが明確に定義されていて実装だけが必要なモジュールで、設計上の決定が関与しない場合。境界が明確で他のモジュールに触れない作業で、グレーゾーンがない場合。バッチリファクタリング、キャッシュ追加、スキン変更などの反復作業で、「正しい」が客観的に検証可能なタスク。チームメンバーがこの判断を検証した:「インターフェースはすでに確定的だった。1つのモジュールを実装するだけでよかった。AIにほとんどフィードバックを与えずに、動く最初のバージョンができた。」別のメンバーは言った:「L2キャッシュの追加は非常に確定的なタスクだった。AIは一発で決め、調整不要で、そのままPRになった。」これらのケースの共通点:タスクの境界と受け入れ基準が明確で、人間の判断を必要とする曖昧さがない。

逆に、探索的なプロジェクト、設計上の決定を伴う作業、モジュール間にまたがる変更は、AIの独立作業には適さない。これらのシナリオの共通の特徴:何が「正しい」かが本質的に曖昧で、人間の判断が必要。これにはペアプログラミングが必要だ。人間とAIが密接に協力し、人間が方向と判断を担当し、AIが実行と実装を担当する。この分担により、双方が最大の価値を発揮できる。

意図のパラドックス:なぜ行動が思考に勝るのか

これは最も直感に反する発見で、「行動する前に考え抜く」という従来の知恵に真っ向から挑戦する:実際に作業をしなければ、正しい意図を生み出すことはできない。

従来のソフトウェアエンジニアリング方法論では、まず要件分析を行い、設計文書を書き、すべての詳細を考え抜いてから実装すべきとされている。このアプローチの前提は、思考が実践の代わりになりうること、つまり十分に深い分析を通じて、開始前にすべての問題を解決できるというものだ。しかし、AI協業開発では、この前提はしばしば間違っている。明確だと思っている要件には、実際には大量の柔軟性と不確実性が含まれている。多くの重要な技術選択は、何かを構築して初めて表面化する。純粋な「思考」は表面的な情報しか捉えられない。真の複雑さは詳細に隠れている。

具体的な例がある。チームでデスクトップクライアントにTauriを使うかElectronを使うか議論した。一見シンプルな技術選択だ。ベンチマーク、バンドルサイズ、メモリ使用量、コミュニティの活発さを徹底的に分析した。結論はTauriがより適している:バンドルが小さく、パフォーマンスが良く、技術スタックがより現代的。分析は厳密に見え、すべての議論がデータに裏付けられていた。この決定に自信があった。

そしてTauriで最初のバージョンを構築した。コードレビュー時に、2つのフレームワークのアーキテクチャ哲学がまったく異なることに気づいた。この違いはベンチマーク数値よりはるかに重要だ。Tauriはサイドカーアーキテクチャを採用している:アプリケーションのコアロジックは別のサイドカープロセスにコンパイルされ、IPCを介してTauriのメインUIと通信する。フロントエンドはシステムのネイティブWebViewでレンダリングされ、バックエンドロジックはRustプロセスで実行される。両側は分離されている。このアーキテクチャは、フロントエンドとバックエンドの責任分担に強い制約を与える。どのロジックをどこに置くか考え抜かなければならず、境界を越える呼び出しはすべてIPCを経由する必要がある。Electronはまったく異なり、ChromiumとNode.jsを一緒にバンドルし、フロントエンドとバックエンドが同じプロセス空間で実行される。通信コストはほぼゼロで、境界を曖昧にでき、高速なイテレーションと複雑なUIロジックに適している。

このアーキテクチャの違いは、構築を始める前には見えなかった。ベンチマーク比較記事には載っていない。議論中は表面的な指標を見ていた。真のアーキテクチャの違いは、コードを書いて具体的な問題にぶつかって初めて現れた。実装中に、Tauriのモデルと要件の間に根本的な矛盾があることに気づいた。設計段階では見えなかった矛盾だ。「紙上の知識は浅く感じる。真の理解には実践が必要」。AIの時代でも当てはまる。

これが意図のパラドックスだ:実装を導くために意図が必要だが、正しい意図は実装を通じてのみ得られる。鶏と卵の問題で、完璧な解決策はなく、実用的な解決策のみがある。解決策:まずプロトタイプを作り、実践を通じて意図を明確にし、進みながら改善し、最初から完璧を期待しない。意図が反復することを受け入れ、最初の実装を最終成果物ではなく学習プロセスとして扱う。

これは別の直感に反する発見につながる:プロトタイプ完成後の再構築は、効率を劇的に向上させる。信じられないほど速い。

TauriとElectronの例に戻る。Tauriで最初のバージョンを構築するのに約1時間かかった。落とし穴にはまり、デバッグし、さまざまな概念を学んだ。このプロセスで要件の複雑さが完全に明らかになり、真の技術要件が明確になった。そして私は一から作り直すことを決め、Electronで再構築した。AIに作業を開始させた。5分後、タスク完了、すべてのユニットテストがパスと言われた。間違いか手抜きだと思った。手動テストしたところ、すべての機能が正しく動作し、コード品質も高かった。

なぜ2回目がこれほど速かったのか?背後に明確なロジックがある。すべての落とし穴を一度経験した。どこにトラップがあるか、どのAPIに問題があるか、どのエッジケースを処理する必要があるか、すべて最初の実装で露呈した。意図は極めて明確で曖昧さがなく、すべての機能の正確な動作が頭の中にあった。技術選択は確定し、探索的な作業は不要になった。すべての詳細がコンテキストに取り込まれた。AIの2回目の実装では、推測や推論すべきものが何もなかった。これら4つの要素が組み合わさり、2回目は純粋な「翻訳」作業になった:明確な意図をコードに翻訳する。この確定的な翻訳こそ、AIが最も得意とするところだ。

これでプロトタイプの価値を再理解した。プロトタイプの価値はコードを生み出すことではなく、学びを生み出すことだ。最初の実装は学習のため:要件の真の複雑さを学び、技術アプローチの実際の限界を学び、さまざまなエッジケースと例外シナリオを学ぶ。2回目の実装は本番のため。その時点ですべてが明確で、実行効率は驚くほど高い。最初の実装を最終成果物として扱うと、サンクコストに悩まされ、やり直すことを躊躇し、最終的に中途半端な解決策で苦しむことになる。正しいマインドセット:プロトタイプは捨てるためにある。執着してはいけない。

TDDが必須になる:二段階タスク法

「AIはTDDに最適。AIはTDDを切実に必要としている。」これは実践から得た最も確信のある結論の一つだが、皮肉に聞こえる。従来の見方では、TDDは人間のために設計された開発手法だからだ。

従来のソフトウェアエンジニアリングは常にTDDを説いてきた。どの教科書も、コードの前にテストを書くのがベストプラクティスだと言う。しかし正直なところ、厳密に実践しているプロジェクトは少ない。少なくとも私が見たほとんどのプロジェクトはできていない。タイトなスケジュール、重いワークロード。テスト時間は最初に削られる。コードを先に書いてからテストを後付けする(あるいはまったくテストを書かない)のが多くのプロジェクトの常態だ。人間の時代、TDDはある意味理想だった。誰もがすべきだと知っていたが、現実の制約で実行が難しかった。

AIの時代はまったく違う。TDDは「理想」から「必須」にシフトした。

AIはテストを書くのが好きではない。興味深い現象を観察した。コードとテストを同時に書かせると、本能的にコードにエネルギーを集中し、テストは形だけになる。手を抜き、ケースをスキップし、弱いテストを書く。カバレッジは良さそうに見えるが、実際の検証能力は弱い。これはバグではなく「好み」だ。コーディング能力を示すことにより熱心で、テスト能力を示すことにはそうでない。コードの後にテストを書かせるとさらに悪い。「既存のコードをパスさせる」テストを作る。これらのテストには検証価値がない。要件ではなく実装に基づいてテストを設計しているからだ。テストはコードの脚注になり、守護者ではなくなる。

しかし、テストを先に書いてからコードを書くと、効果が劇的に変わる。正確性が大幅に向上する。

私は「二段階タスク法」を開発した。第一段階:AIにテストだけを書かせる。これは独立したタスクだ。要件を与え、ユニットテストを要求する。ハッピーパス、アンハッピーパス、少なくともN個の深刻な例外シナリオ(ネットワーク障害、データベース利用不可など)、少なくともN個のセキュリティシナリオ(インジェクション攻撃、権限昇格など)をカバーする必要がある。キーポイント:AIは次に実装を書くことを知らない。「実装の詳細」という荷物がないので、テストシナリオを真剣に考える。具体的な実装のアイデアに制約されず、要件の観点から「どんな状況でテストが必要か」をより純粋に考えられる。

第二段階:テストをAIに渡し(通常は新しいセッションで)、要件とこれらのテストに基づいて機能を実装させる。これで明確な受け入れ基準があり、テストを使って自己チェックできる。コードの一部を書くたびにテストを実行し、緊密なフィードバックループを形成する。テストは「ナビゲーションシステム」になり、正しい軌道にいるかどうかを教えてくれる。

なぜ段階を分けて異なるセッションを使うのか?コンテキストの汚染を避けるためだ。同じセッションでAIにテストを書いてから実装を書かせると、テストを書いている時点ですでに実装アプローチを「想像」している。テストは無意識に実装に傾く。そのテストは想像した実装アプローチを検証することになり、要件自体を検証しない。分離すると、第一段階のAIは「純粋なテストマインドセット」で、完全に要件とユーザーの視点から考える。第二段階は「純粋な実装マインドセット」で、テストをパスすることに集中する。それぞれが自分の仕事をする。

もう一つの発見:AIのE2Eテスト設計能力は驚くほど強い。予想を超えていた。デプロイ機能のすべての可能なケースをAIにリストアップさせた:エッジケース、バッドケース、異なるフレームワーク、さまざまな境界条件。数十の異なるテストシナリオを出してきた。私が思いつかなかったエッジケースも含まれていた。いくつか追加し、すべてのテストフィクスチャを生成させた。約15分の議論の後、50以上の異なるテストプロジェクトが生成された。さまざまな技術スタック、デプロイ方法、例外シナリオをカバーしていた。この網羅度を手動で設計するには数日かかるだろう。

しかし、AIにE2Eテストをさせるにはいくつかの重要なテクニックがある。第一に、AIにブラウザを操作させない。ブラウザベースのテストは遅く不安定で、タイムアウト問題でテストが脆弱になる。より良い方法は、各アプリケーションにcurlなどのツールを使って主要なエンドポイントをチェックする検証スクリプトを持たせること。はるかに速く安定する。第二に、コードコンテキストを切断する。私はAIに明示的に伝える:「E2Eテストをするとき、私のコードを見ることは許可されていない。使えるのはこのCLIだけ。CLIで操作を完了できなければ、それはバグだ。」これにより、AIが内部構造を直接操作したり、通常のユーザーパスをバイパスしたりするショートカットを防ぎ、内部実装ではなく外部インターフェースを真にテストすることを保証する。第三に、自動修正を禁止する。AIはテストが失敗すると本能的にコードを修正したがる。テストをグリーンにするのが本能だ。しかしこれは本当の問題を隠し、バグを露呈させるのではなく隠してしまう可能性がある。テスト段階は問題を記録するだけ。修正は別のタスクだ。

なぜCIを止め、コードレビューを放棄したのか

さて、タイトルの最も議論を呼ぶ部分を説明しよう:なぜCIパイプラインを止め、手動コードレビューを放棄したのか。後退のように聞こえる。数十年にわたるソフトウェアエンジニアリングの公認ベストプラクティスを放棄するのだから。しかし、AI協業開発の文脈では、これらのプラクティスの価値は根本的な変化を遂げている。

まずCI。従来のCIのコア価値は何か?すべてのコミットがすべてのチェックをパスすることを保証する。lint、test、type check、build。悪いコードがメインブランチに入るのを防ぐ。「人間がコードを書き、機械がチェック」モードではこれは理にかなっている。人間はさまざまな低レベルのエラーを犯し、機械のゲートキーピングが必要だからだ。しかしAI協業開発では、AIはすでにこれらすべてのチェックをローカルで完了している。コードを書きながらテストを実行し、問題を修正しながらlintを行い、コミット時には基本的に「グリーン」だ。GitHubで再度CIを実行しても、ほとんどの場合「はい、グリーンです」と確認するだけで、新しい問題は見つからない。CIが完了するまで数分から数十分待っても、価値は生まれない。

CIにはまだ価値があるか?ある。しかし価値のポイントが変わった。「コード品質チェック」から「デプロイ自動化」へ。コード品質チェックはすでにローカルで完了している。CIは現在主にデプロイパイプラインを処理する:ビルド、パッケージ、リリース、さまざまな環境へのデプロイ。これはまだ自動化が必要だが、従来の「継続的インテグレーション」とは異なるものだ。

次にコードレビュー。従来のコードレビューのコア価値は何か?知識共有、品質ゲートキーピング、チームコラボレーション。経験豊富なエンジニアが新人のコードをレビューし、潜在的な問題を発見し、ベストプラクティスを教え、コードがチーム標準に準拠していることを確認する。「人間がコードを書き、人間がコードをレビュー」モードではこれは理にかなっている。コード量が限られており、人間には注意深く見る余裕があるからだ。しかしAI協業開発では、コード出力は桁違いに変化した。AIが生成するコードは大きすぎる。しばしば数千行で、1つの機能のPRが十数ファイルに触れることもある。人間には注意深くレビューすることは不可能だ。ほとんどの場合、diffをちらっと見て、重要な部分をざっと確認して終わりだ。これは怠慢ではない。現実だ。人間の帯域幅は限られている。

コードレビューの価値をどう代替するか?2つのアプローチがある。1つ目:AIにコードレビューをさせる。別のAI(または同じAIの新しいセッション)にコードを監査させる。すべての行を注意深く調べられる。疲れないし、見落とさない。2つ目のより根本的なアプローチ:レビュー対象を変える。本当にレビューすべきはコードではなく、意図(Intent)だ。意図が真のソースコードであり、コードは意図のコンパイル成果物の1つに過ぎない。意図が正しければ、コードの問題は低コストで再生成できる。意図が間違っていれば、どれだけ美しいコードも役に立たない。間違った方向に速く走るのはより危険だ。だから私たちは今、意図のドキュメント、設計上の決定、技術選択の理由をレビューしている。コードの各行ではない。

良いニュースと悪いニュースがある。

良いニュース:リファクタリングが痛みなくなった。以前は1つの名前を変更すると多くのファイルに触れる必要があった。作業量が多すぎて、間違いをそのままにしていた。変数名が誤解を招くと分かっていても変更する勇気がなかった。小さな問題が大きな問題に蓄積し、技術的負債が積み上がり、いつか爆発する。今やAIが数分で完全にリファクタリングしてくれる。意図が非常に明確(「AをBに名前変更、グローバル置換」)なので、リファクタリングは通常極めて確定的なタスクで、ほとんど間違わない。技術的負債を容認してはいけない。リファクタリングコストはすでに非常に低い。変更したいものを変更し、コードベースを健全に保とう。

悪いニュース:マージが最大の課題になる。AIは毎日数万行のコード変更を生成できる。誰もが頻繁にリファクタリングしている。コードベースの変化速度は以前の10倍から100倍だ。従来のマージワークフローは崩壊する。1つのPRをレビューし終える前に、コードベースは3回変わっており、コンフリクトがあちこちで発生する。対処戦略はまだ模索中だ。現在のアイデア:第一に、マルチリポジトリに戻る。インターフェースを使って異なるモジュールを分離する。各モジュールは独自のコードベースで、マージコンフリクトの可能性を減らす。モジュールは明確に定義されたインターフェースを通じて通信し、内部実装の変更は他に影響しない。第二に、明確なモジュール境界を確立する。各モジュールは独自の「領域」を持ち、その領域内のコードには1人のオーナーのみがいる。AIは自分の領域内でのみ作業でき、境界を越えて他人のコードを変更しない。第三に、チェックアウト/チェックインのようなメカニズム。誰がどのモジュールを変更しているかを明確にする。同じモジュールを同時に変更できるのは1人だけで、同じ領域への並行変更によるコンフリクトを避ける。これらのアイデアはまだ検証中だ。

最後に、AIと協業するときのマインドセットについて話そう。これは具体的な方法論よりも重要かもしれない。

深い印象を残した場面がある。私は自分でエレガントだと思うデータ構造を設計し、かなりの時間をかけて磨き上げ、最適だと確信していた。そしてAIに実装させた。実装を始めたが、困惑し続け、さまざまな質問をしてきた:「ここのこの処理は意図的ですか?」「このエッジケースは意図的ですか?」私は自分のアプローチを主張し、続けるよう言った。従ったが、コードは不自然になり、あちこちにワークアラウンドがあった。最後に注意深くレビューして、私が間違っていたことに気づいた。最初の困惑は正しかった。採用したがっていたアプローチは最初からより良かった:よりシンプルで、より堅牢で、より理解しやすい。

これで気づいたことがある:より弱い方法でより強い人を指揮すると、パフォーマンスが悪くなる。そのような低いレベルで考えることができないからだ。自分の判断を抑えて、あなたの指示に従わなければならない。あなたの意図を理解しようと努力し、あなたの命令を実行しようと努力する。たとえあなたの命令が次善であっても。この「服従」は実は無駄だ。AIの能力を無駄にしている。

良いAI開発ツールを構築した後、私たちは本質的に極めて高いレベルのコーダーにアクセスできるようになった。問題は:私たちはこの人を指揮する資格があるか?これは謙遜ではない。実用的な自己検証だ。AIは私たちが思っているより正しいかもしれない。特に実装レベルでは。学習とオープンなマインドセットを維持し、自分のアプローチが最適ではないことを認める意思を持ち、AIの「提案」(困惑や質問として表現されていても)に耳を傾ける意思を持つ。これがこのツールを真に活用する方法だ。

AIの能力は急速に進化している。この記事は過去数ヶ月の実践経験を記録している。コアの洞察はしばらく有効だと確信しているが、具体的なプラクティスは常に調整が必要かもしれない。1年後に振り返ると、一部の見方は時代遅れになり、新しいベストプラクティスが現れているかもしれない。しかし探求の精神は変わらない:継続的な検証、オープンであり続けること、古いパラダイムに縛られないこと、自分の以前の結論を覆す意思を持つこと。

意図がソースコードだ。コードはコンパイル成果物に過ぎない。