TECH BLOG
AIを導入するだけじゃ開発は早くならない話
はじめに
こんにちは。データコンサルの佐藤です。
私は業務を支援するAIプロダクトの開発を担当しています。その中で実装のほとんどをAIエージェントに任せて開発する体制を試してきました。自分でコードを書く時間は、かなり減りました。しかし、それで開発が楽になったかというと、思ったほどではありませんでした。
コードは速く出るようになったのに、レビューが追いつかない。そういう状態にいるチームに向けてこの記事を書きました。
この記事は、次の問いに答えます。
- AIに書かせると、なぜ開発全体は速くならないのか
- 人が読む量を減らすには、どこに投資すればいいのか
- 結局、人が見なければならないものは何か
4人のチームで3週間ほど回した記録です。効果を測った数字はありません。代わりに、何を機械に任せ、何を人に残したかの線引きを書いています。
1. 速くなったのは、書く工程だけでした
現在開発中のプロダクトは画面を持つフロントエンドと、業務ロジックを持つバックエンドのプロセスに分かれた構成で、TypeScriptとPythonが混在しています。実装の主な担い手はAIエージェント(Claude Code)で、仕様の起票から、コードとテストの作成までを任せています。ただし、書く工程を任せることと、検証を省くことは別の話です。
チームは4人です。レビューの専任は置かず、空いている人が見ていました。プルリクエストは週に8本くらいです。この記事で紹介する仕組みは、ここまで20本あまりのプルリクエストで回したところです。
エージェントを使って最初に驚くのは、生成される量です。以前なら数日かけていた規模のコードが、数分で出てきます。一度の指示で数百行、内容によっては数千行。これは率直にすごいと思いました。
ところが、仕様が固まってからマージされるまでの時間は、その速さほど短くなりませんでした。 書く時間は以前の何十分の一になったはずなのに、機能が出るまでの体感はそこまで変わりません。
理由は単純でした。出てきたコードは、誰かが読んで判断しないとマージできません。生成量が何十倍にもなれば、読む量もその分増えます。 エージェントが速くしたのは書く工程だけで、読む工程は人に残ったままです。
むしろ、書く速さに引っ張られて読む量だけが増えた、と言うほうが実態に近いかもしれません。
コードは速く出ます。ですが品質を担保したものを出す速度は、思ったほど上がりません。速くなったのは生成であって、開発ではありませんでした。
2. 投資すべきは「書く速さ」ではない
この状況で「もっと速く書かせる」方向に投資しても、あまり報われません。書く工程はすでに十分に速く、そこは律速ではないからです。効くのは、人が読む量そのものを減らす方向でした。
やれることは、突き詰めると3つしかありません。
- 読める形で書かせる。 出てくるものの形が毎回違えば、読む側は毎回ゼロから読み方を組み立てることになります。
- 人が読む前に機械が落とす。 人の目に入る前に弾かれたものは、そもそも読む対象になりません。
- 人が読む範囲を閉じる。 何を見て、何を見ないかを先に決めてしまいます。
同じ生成側への投資でも、速く書かせる方向は成果に結びつかず、読める形で書かせる方向は確かな見返りがありました。
私たちは品質保証の仕組みを、この3方向に対応する形で設計しました。以降は、それぞれ何を置いたのか、そして置いてみて何が分かったのかをひとつずつ書いていきます。1つめは3章、2つめは4章と5章、3つめは6章にあります。
3. 読める形で書かせる — 規約は、置くだけでは読まれない
生成物のばらつきは、エージェントの性能の問題ではありませんでした。基準が明文化されていないことの問題でした。
基準がなければ、書く人によって成果物の形が変わりますし、エージェントに書かせればセッションごとに構成も観点の抜けも変わります。同じ人が同じ意図で指示しても、次のセッションでは違うものが出てきます。
そこで、判断基準になるものをリポジトリに置きました。
- テスト規約 — テスト観点、書き方、「どこまで書くか」の量の規律
- 仕様書・設計書のテンプレート — 記入欄そのものが基準を要求する形にしたもの
- 着手フロー — 変更に取りかかるとき、最初に何をするか
- アーキテクチャガイド — 実装をどの層に置くか、依存をどちら向きに保つか
ただ、置くだけでは読まれません。
自動で読み込まれるファイル(AGENTS.md)に規約をすべて書く方法もあります。しかし、自動で読み込まれることと、書いてあることが守られることは別です。
長くなるほど個々の指示は薄まり、無視されるものが増えます。更新もされなくなります。
そこで、この一枚のファイルには規約の本文や要約を書かず、規約への読み込み指示と着手フローだけを置きました。
変更種別の宣言、テンプレートの参照、テスト規約の事前の読み込み。いずれも「次にどこを見るか」の案内です。規約に関してこの一枚が持っているのは、その案内だけです。
規約へのリンクは、テンプレートの中にも置きました。その中には、規約を参照しないと埋められない欄を作ってあります。
「この実装をどの層に置くか、その根拠」「この変更種別に必要なテスト観点」といった欄です。空欄では出せないので、開いた側は貼られたリンクを辿ることになります。
すべてを一枚に書くのではなく、そのときに必要なものだけを次に示す形にしています。
これが機能するかを確かめるために、事前指示を何も与えていないサブエージェントに「仕様書を起票して」とだけ渡してみました。規約を読めという指示はしていません。エージェントは着手フローに従ってテンプレートを開き、必須欄を埋めるためにテスト規約を参照し、必要な観点が入った仕様書を出してきました。
導線が通っていれば、指示しなくても最後まで辿ります。 その後も同じ形で回し続けましたが、いまのところ毎回そのとおりに動いています。逆に、導線を通す前に同じ指示を出したときは、着手フローもテスト規約も参照されませんでした。
規約をいくら整備しても、そこへ至る道がなければ読まれません。文書を追加するときは、内容を書く前に「これはどこから到達するのか」を決めるほうが確実でした。
置き場所については、もう1つ意図がありました。プロンプトに書いた基準は、書いた人のそのセッションにしか届きません。更新されず、他のメンバーにも伝わりません。
リポジトリに置いて導線を通せば、以降すべてのセッションに届きます。 そして同時に、人にも届きます。エージェントの出力を揃えることと、チームの足並みを揃えることは、私たちにとって最初から同じ目的でした。
だから、置き場所はここでなければなりませんでした。人向けに書いた規約が、そのままエージェント向けの指示書になります。
4. 機械で落とす — CIの役割が従来とは変わった
入口を揃えても、間違ったものは出てきます。次に必要なのは、人が読む前に機械が確実に落とす仕組みでした。
lintと整形(ruff / biome)、型検査(ty / tsc)、既知の脆弱パターンとシークレット混入の検出(bandit / semgrep / gitleaks)、レイヤー間の依存方向(import-linter)、そしてテスト(pytest)。マージの必須条件にしているのは、どれもAI以前から知られている、地味な道具です。
ただ、エージェントを主担当にすると、これらの意味が変わってきました。
以前のCIは「人のレビューを補助するもの」でした。人が主に見て、機械的なところを機械が拾ってくれる、という位置づけです。エージェントが大量に書くようになると、これが逆転します。
主に見るのは機械で、人はそこを通過したものだけを見ます。 CIは補助ではなく、エージェントの出力を受け止める門番になりました。
依存方向の検査は、この転換が分かりやすい例です。エージェントは局所的には妥当なコードを書きますが、それをどこに置くかは間違えることがあります。目の前のファイルに追記するのが最短だからです。
置き場所の誤りのうち、内側の層が外側を参照してしまうような依存方向として現れるものは、機械が確実に落とせます。人が毎回レビューで指摘するのは退屈ですし、見落としもします。落とせる範囲は限られていますが、限られていても確実なほうが、レビュー負荷の軽減としては効きました。
カバレッジは、少し違う扱いにしています。変更行に対するカバレッジは計測して表示しますが、マージの条件にはしていません。 数値をゲートにすると、数値を満たすためのテストが書かれるからです。エージェントは、そういうテストをいくらでも書けます。
私たちの規約は「実装にバグを入れたら、このテストは落ちるか」を判定基準に据えていて、カバレッジは参考値の位置に留めています。そのうえで、表示された未カバー行をどこまで許容するかは人が判断します。ゲートにはしませんが、放置もしません。
そして決定論的な検査には、後の章で重要になる性質があります。ルールが定義する範囲については、検出されなかったことに意味がある、ということです。裏を返せば、意味を持つのはルールの範囲内だけで、既知のパターンに当たらない脆弱性が存在しないことまでは保証できません。
5. AIで一次チェックする — 見ているだけでは保証にならない
決定論的なゲートで落とせるのは、ルールに書けるものだけです。書けないものについては、AIによる差分レビューを入れました。プルリクエストがレビュー可能になった時点で自動的に走るようにし、同じものを手元でも実行できるようにしています。
これはよく機能しました。人のレビュアーに届く前に、かなりの指摘が解消されます。指摘して直してまた見る、という往復が減るので、体感としては最も分かりやすい効果がありました。
ただ、運用しているうちに1つはっきりしてきたことがあります。AIレビューは、見ているだけで、保証はしていません。
理由は3つあります。1つめ、同じ差分でも出力が毎回同じとは限りません。2つめ、マージを止めない運用にしている以上、指摘を無視して進めることもできます(重大と判定された場合はジョブが赤くなるので、無視したこと自体は残ります)。そして3つめが決定的で、見落としたときに何も言いません。
前章に書いたとおり、lintは「ルールに一致すれば必ず出る」ので、出なかったことに意味があります。AIレビューには「必ず出る」という保証がありません。指摘がゼロだったことは、問題がなかったことの証明にはならないわけです。
保証とは、出なかったことに意味がある状態だと言い換えられます。 この定義で見ると、決定論的なCIとAIレビューを同じ棚に並べてはいけないことが明確になります。並べた瞬間、「AIレビューが通ったから大丈夫」という判断が生まれてしまいます。
とはいえ、これはAIレビューの価値を下げる話ではありません。位置づけを間違えなければ、ルールに書けない領域まで含めて、人が読む量を確実に減らしてくれます。それで十分に元が取れています。
なお、CIと重ねて見るかどうかは、観点によって分けています。型の不整合や整形、import漏れのように、機械が確実に検出できるものは、AIレビューの指摘から捨てるよう指示しました。 屋上屋を架すだけだからです。
一方で、重複を避けていない領域もあります。 シークレットの混入は、専用の検査を通したうえで、AIレビューにも見させています。機密情報のログ出力は、AIレビューに加えて人も直接見ます。ここは見落としたときの代償が大きいので、確実な1層より、性質の違う2層のほうが安心できます。
機械的に確実なものは重ねない。ただし致命度の高いところだけは、意図的に重ねる。 この考え方は、次の章でもう一度出てきます。
6. 人が読む範囲を閉じる — 見ない場所を決める
ここまでで、人が読む前の段階はかなり整いました。それでも、人が何を見るのかが決まっていなければ、レビューは差分の全量を眺めて問題を探す作業のままです。生成量が多いのですから、この作業は必ず破綻します。
そこで、レビュー観点をすべて書き出して、それぞれの担い手を決めました。先に決めたのは「見ないもの」です。
スタイル、整形、型、importの順序。これらは決定論的な検査が保証しているので、人は見ません。null参照、境界条件の誤り、エラーハンドリングの漏れ、リソースリーク、並行性の問題。つまりコードを読めば見つかる種類の欠陥を、人が探すこともやめました。
ここはAIレビューが担っている領域なので、人が重複して探しません。運用ルールとして、これらの欠陥を見つけても指摘しないことにしています。見つけたら、仕組み側の改善課題として別に起票します。
取りこぼしはあります。そこは、振る舞い単位で書いたテストが受け止めます。それでも抜けたものは、バグを再現するテストを先に書いてから直す運用で、少しずつ積み上がっていきます。
「見ない」と決めるには、抜けがないことを言えなければなりません。そこで、レビューの対象と関心事で表を作り、すべてのマスに担い手を割り当てました。軸の中身は私たちの都合ですが、2軸で区切って全マスを埋めるという手順自体は、どの現場でも同じように使えると思います。
目的は網羅性の主張というより、誰も見ていないマスを見つけることでした。そして実際に、空白は見つかりました(これは次章に書きます)。
そのうえで、人が自分の目で直接読まなければならないものを見ていくと、委譲できない理由は3種類しかありませんでした。
理由i:正解が、要求者や組織の頭の中にしかない
そもそも文書化されていないので、機械にもAIにも参照先がありません。この仕様は元の要求を本当に満たしているのか。いまこのスコープで作るのが妥当か。後から変えるのに高い代償を伴う決定を、いま固めてよいか。たとえば外部のLLMに送るデータの妥当性です。何を収集し、何を保存し、何を外部に送るのか。それは目的に対して必要最小限か。保存期間やマスキング、送信先は妥当か。AIを組み込んだプロダクトを作っていると必ず出てくる論点ですが、これはコードを読んでも答えが出ません。仕様の妥当性の問題であり、判断する人が要ります。
理由ii:差分にもリポジトリにもない文脈が必要
他システムとの連携、運用手順、リリース順序の依存関係。機械やAIの視界の外にある情報を得なければ判断できないものです。関係者に聞く以外の方法がありません。
理由iii:AIの沈黙を安全と読めない
前章のとおり、AIレビューは見落としても黙っています。ですから、致命度の高い領域については、指摘があったかどうかに関係なく人が直接読むことにしました。不可逆な操作やデータを破壊しうる処理、認証まわりの変更です。運用としては、指摘を起点にするのをやめて、領域を起点に網を張っています。 該当する場所が差分に含まれていたら、AIが何も言っていなくても全行読みます。
これに加えて、人が判断だけを持つものがあります。未カバー行をどこまで許容するか、不安定なテストの隔離を認めるか、複雑度を受け入れるか。検出も一次分析も機械とAIに任せて、人はコードを直接読まずに、受け入れるかどうかだけを決めます。 委譲できないのは分析の正しさではなく、受け入れる責任のほうだからです。
こうして範囲を閉じると、レビューが「探す作業」から「確認する作業」に変わりました。見るものが決まっているので、終わりも決まっています。この変化が、読む量の問題に対しては最も効きました。
7. 担保していない領域は、担保していないと書く
前章の表で見つかった空白は、1つではありませんでした。3マスありました。
うち2つは、差分の外にあるものでした。呼び出し元やドキュメント、マイグレーションの追随漏れ。そして既存の設計思想との矛盾。どちらも移管先は決まっています。 判定の基準になる文書(テスト規約やアーキテクチャの記録)がすでにあるので、それをコンテキストとして渡せばAIが一次チェックできるはずです。まだ繋いでいないだけで、これは繋ぐ話です。
もう1つは、繋ぎ先が思いつかないものでした。「テストが本当にバグを検出できるか」です。
カバレッジは通るが、アサーションが弱くて実質何も検証していないテストがあります。エージェントはテストも書きますし、書かせれば通るものを書いてきます。ここを検出する手段は、私たちの仕組みの中にありませんでした。
対策としてミューテーションテスト(コードに意図的な変更を加えて、テストがそれを検出できるかを見る手法)を導入しました。加えた変更をテストが検出できることの確認までは行いました。
ただ、実行コストが高く、プルリクエストごとに回すのは現実的ではないと判断して、いまは止めています。変更されたモジュールだけに絞る工夫もしたのですが、それでもコストは高いままでした。
止めたうえで私たちが選んだのは、担保していないと明記することでした。 テストのバグ検出力は現在どの層も担保していない、という一文をレビューガイドラインに書きました。あわせて、埋め戻しの候補(コアドメインに限定した定期実行、静的に検出できる範囲のルール化、AIによる診断)も残してあります。
そのどれをやるかは、まだ決めていません。 決めていないことも含めて、いま無担保である、という状態を共有しています。
ここから学んだことは2つあります。1つは、検証側の機械化は、コストが高いと続かないということです。続かない仕組みは担保になりません。導入したかどうかではなく、回り続けるかどうかで判断する必要がありました。
もう1つは、品質保証というのは、穴をすべて埋めることではないのだろうということです。どこが埋まっていないかを全員が同じ言葉で知っている状態のほうが、実際には安全に近いと考えています。埋まっていないことを知らないまま「一通り整えました」と言っている状態が、たぶんいちばん危ないと思います。
8. 降ろせるものは降ろす — 序列は処理の順で、重要度の順ではない
ここまでを整理すると、1つの序列が見えてきます。観点は、決定論的な検査に降ろせるなら降ろす、降ろせないものはAIが一次チェックする、それでも無理なものだけ人が見る — この順です。これは処理の順番であって、重要度の順ではありません。 人に残るものほど、実際には重い判断です。
そう考えると、いま「AIが見ている」ことになっているものの一部は、実はルールに書き下せることに気づきました。文書に書いてエージェントに読ませていましたが、lintのルールを有効にするだけで決定論的に落とせます。
規約文書に書いてエージェントに読ませる前に、それはlintにできないかを問うべきでした。 順番を間違えると、確率的な担保で満足してしまいます。
ルールに書き下せるかの見分け方
書き下せるかどうかは、3つの問いで見分けられました。判定に文脈が要らないか。対象を構文で特定できるか。例外を列挙できるか。 私たちの規約を当てはめると、こうなりました。
| 規約の例 | 文脈不要 | 構文で特定 | 例外の列挙 | 移せるか |
|---|---|---|---|---|
| タイムゾーンを持たない日時を使わない | ◯ | ◯ | ◯ | lintに移せる |
| docstringの有無・体裁 | ◯ | ◯ | ◯ | lintに移せる |
| docstringに何を書くか | ✕ | ✕ | — | 人が見る |
| コードの複雑度・ネストの深さ | ◯ | ◯ | ◯ | 検出は移せる |
タイムゾーンを持たない日時の禁止は、3点すべてを満たします。どのプロダクトでも判定は同じで、datetime.now() のような呼び出しとして特定でき、例外は注記で列挙できます。
docstringは、途中で線が引かれました。あるかないか、体裁が揃っているかまでは満たします。ですが「何を書くべきか」は、このプロダクトの業務ルールを知らないと判定できません。
複雑度は、測るところまでは移せます。ただし許容するかどうかの判断は、6章に書いた受容判断として人に残ります。
つまり、判断に「このプロダクトにとって」という前置きが要るものは、ルールになりません。それは6章の理由iに属します。
まだ降ろせていないもの
降ろす方向は、AIレビュー側にも残っています。7章に書いた2つの空白(差分の外の追随漏れと、既存設計との整合)は、渡すべき文書が分かっているのに、まだAIに渡していません。
5章に書いた「機械が確実に検出できるものは捨てる」という指示も、実のところ誤検知を減らす目的で書いたものにとどまっています。CIが担保している範囲と正面から突き合わせた整理は、まだできていません。
つまり、AIレビューに何を見せるかも、何を見せないかも、まだ設計しきっていません。
そして、lintへの移管も着手していません。さきほど「書き下せる」と判定したものを、実際にルールとして有効にする作業が残っています。7章のテストのバグ検出力の穴も、埋め戻すかどうかを決めていません。
初版として組み上げたところまでが現在地で、ここから先は運用しながら人の分担を機械に降ろしていく段階です。
この方向に進んでいくと、面白いことになると思っています。観点を機械に降ろすほど、その効率化はエージェントに依存しない資産として残るからです。 lint、テスト、レビュー観点の言語化。どれもAIが出てくる前からある地味な営みです。AIによって不要になったのではなく、むしろAIが自分たちのやっていないことを可視化してくれた、という順序でした。
おわりに
AIエージェントを本格的に使い始めて、最も価値があったのは、コードが速く書けるようになったことではなかったように思います。それよりも、自分たちがレビューで何を見ているのかを、言語化させられたことの方が大きかったです。
言語化してみると、レビューで見ていたことの多くは、コードに関しては実は機械に任せられる内容でした。逆に言えば、機械に任せられないものだけが、はっきりと残りました。3週間では、開発が速くなったとまでは言えません。ですが、レビューで何を見るかに迷う時間はなくなりました。
エージェントに書かせる量を増やすより先に、書かれたものを受け止める側を作るほうが効果があると思います。