AIのコードは「機械語」になれるか — 第3言語とTDDで考える信用の置き場所

AIのコードは「機械語」になれるか — 第3言語とTDDで考える信用の置き場所 AI

1万行を書いて、中身を知らない

AIが書いたコードは、読まなくても信用していい。ただし条件がある。それが、この記事の結論です。

先日、Claude Codeでバックエンドとフロントエンドを合わせて約1万行を書き、期限の日に間に合わせました。正直に言うと、中身はほとんど把握していません。

TDDもしていません。書かせただけです。そして、書かせた結果を疑ってもいませんでした。理由は、決定的だからです。一度書かれたコードは、同じ入力には毎回同じように動く。だから大丈夫だろう、と。

不思議なことに、「読めない」焦りはありませんでした。焦っていたのは「説明できない」ことです。ビジネス側に「これ、どういう時にこうなるの」と聞かれて答えられない。障害が起きたときに、どこが悪いかを言えない。困るのはいつもそっちです。要求する側も、コードを全部見せろとは言ってきません。聞かれるのはいつも、どう動くかです。

この違和感をたどっていくと、ひとつの問いに行き着きました。私たちは、読んだこともない機械語を、なぜ毎日信用しているのか。

読んでいない機械語を信用できる4つの理由

ほとんどのエンジニアは、自分のコードがどんな機械語になっているか見たことがありません。それでも疑わないのは、確かめたからではなく、次の4つが揃っているからです。

  1. 実績:コンパイラもCPUも、世界中で何十年、何十億回と動いてきた。壊れたら誰かが気づき、直してきた。
  2. 仕様:命令セットにも言語にも仕様があり、コンパイラはそれに対して大量のテストで検証されている。仕様で定義された範囲では、同じ入力から同じ振る舞いが出る。
  3. 分業:全員が疑わなくても、疑う人がいる。コンパイラの開発者、セキュリティ研究者、バグを報告する利用者。残りの私たちは、その人たちが疑ってくれていることを信用している。
  4. 追跡可能性:まれにバグが起きても、どこで何が起きたかを特定できる。1994年のPentiumの割り算バグも、2018年のSpectreも、原因が特定され、対策が配られた。

つまり、信用の根拠は出力そのものではなく、出力を生む仕組みにあります。

もっとも、完全な信用ではありません。Ken Thompsonは1983年度チューリング賞の受賞講演「Reflections on Trusting Trust」(1984年発表)で、コンパイラ自体に裏口を仕込めば、ソースをどれだけ読んでも見つからないと示しました。私たちが機械語を信じているのは、確かめるコストが高すぎて、しかもこれまで信じて大丈夫だったからです。

そして、この講演そのものが疑う人の仕事でした。みんなが信じているところを、誰かが疑い続けている。信じて大丈夫だったのは、そのおかげです。

層で見ると、非決定的なのは一番上だけ

自分の書いたものがCPUで動くまでを層で並べると、こうなります。

第3言語  指示と仕様(人間が書く)
   │  AIが訳す ……… 毎回少し違う・実績はまだない  ← 検証はここの出口に置く
   ▼
第2言語  TypeScript / React(AIが書く)
   │  コンパイラ・JSエンジン ……… 何十年の実績
   ▼
第1言語  機械語
   │  CPU ……… 何十年の実績
   ▼
  実行

下の2つの矢印は、何十年も信用を積んできた翻訳です。一番上の、AIによる翻訳だけが毎回少し違う結果を出し、信用の実績もまだありません。だから検証は、その矢印の出口に置きます。

細かく言えばTypeScriptは一度JavaScriptになり、JSエンジンが実行時の様子を見ながら機械語にします。生成される機械語は毎回同じとは限りませんが、振る舞いは言語仕様で決まっていて、そこは検証済みの側です。

決定的であることと、正しいことは別

私が1万行を疑わなかった理由、「決定的だから」は半分だけ正しかったと思います。

AIの生成は毎回少し違いますが、生成されたコードは決定的です。同じ入力には毎回同じように動く。ここまでは合っています。言い換えると、生成されたコードは、確率から偶然引いた一回分を、決定的に固めたものです。

でも、決定的なのは同じように動くことの保証であって、正しく動くことの保証ではありません。決定的なコードは、間違えるときも決定的に、毎回同じように間違えます。

コンパイラを信用できるのも、決定的だからだけではありません。決定的で、しかもその振る舞いが仕様に照らして検証されているからです。最初に挙げた4つの理由のうち、決定性は「仕様」の一部でしかありません。あの1万行には、決定性はあっても、それが私の意図どおりかを確かめた人が誰もいませんでした。疑う人が、一人もいなかったのです。

逆に言えば、決定的であることはむしろ追い風です。決定的なコードなら、一度通ったテストは何度やっても通る。テストに意味があるのは、相手が決定的だからです。足りなかったのは決定性ではなく、それを確かめる仕組みのほうでした。

「AIは決定的じゃないから、人間がコードをレビューすべきだ」という意見もよく聞きます。前半は正しいと思います。生成が決定的でないので、書き手のAIを一度確かめても、次の出力は保証されません。出力は毎回疑う必要があります。でも、そこから「人間が毎回コードを読む」までは一足飛びです。生成されたコードは決定的なので、疑う手段には決定的なテストが使えます。そもそも要求する側が求めているのも、コードを読んだという事実ではなく、どういう時にどう動くかを説明できることです。

本当のソースコードは「第3言語」

AIと開発していると、言語の層がひとつ増えたことに気づきます。

  • 第3言語:自分が書く短い指示と仕様
  • 第2言語:TypeScriptやReactのコード
  • 第1言語:機械語

昔はプログラマーが第2言語を書き、コンパイラが第1言語に訳していました。今は自分が第3言語を書き、AIが第2言語に訳し、そこから先は昔と同じ道を通ります。第2言語は、かつてのアセンブラと同じ「中間の言語」の位置に下がりました。

そう考えると、説明できない焦りの正体が見えます。説明すべきなのは、AIが書いた1万行ではなく、第3言語のほうです。ところが第3言語は、頭の中とチャットの履歴に散らばっていて、どこにもまとまっていない。ソースコードが保存されていない状態なので、説明しようとすると1万行を読みに行くしかなくなります。

ただし、第3言語は穴だらけの仕様

ここで、コンパイラのたとえが一か所だけ崩れます。

TypeScriptから機械語への翻訳は、意味を変えない翻訳です。TypeScriptには厳密な仕様があり、書いていないことは起きません。一方、第3言語は自然言語で、書いていないことだらけです。「ログインできるようにして」と書いたとき、パスワードを何回間違えたらロックするか、セッションは何分で切れるか、AIは何かを選んで埋めます。

つまりAIは、訳しているだけでなく、仕様の穴を埋める決定をしています。ビジネス側の「これ、どういう時にこうなるの」に答えられなかったのは、たいていこの穴の部分です。自分が決めていないのだから、説明できるはずがありません。

なので、対策は二段になります。

  1. 第3言語をリポジトリに残す
    • 仕様と、決めたことと、なぜそう決めたかを短いメモで残す(CLAUDE.mdや設計メモ)
    • 機能ごとに、AIへの指示の要点を残す
    • 機能ごとに「何をするか、データはどこから来てどこへ行くか、テストで何が保証されているか、どこを信用してどこを疑っているか」の4点を書けるようにする
  2. AIが埋めた穴のうち、大事なものをテストで固定する

2つ目が、次のTDDの話です。

TDDは「信用の基準」を人間が書く仕組みになる

下の層が信用できるのは、世界中で使われて検証されてきたからです。自分のプロジェクトのコードには、その実績がありません。だから、TDDで「このプロジェクト専用の検証の実績」を作ります。テストが、世界中の利用者の代わりをするわけです。

もうひとつの見方をすると、テストは第3言語のうち、穴のない部分です。自然言語の指示は曖昧ですが、「5回間違えたらロックされる」というテストは曖昧になりようがありません。仕様の穴は、テストを書いた分だけ人間の決定で埋まります。

AI以前のTDDは、人が書くコードを良くするための作法でした。AI以後は、「何が通れば信用していいか」を、AIが書く前に人間が決めておく仕組みになります。人間は実装ではなく、信用の基準を書く側に回ります。TDDを提唱したKent Beck自身も、AIエージェントと組むときTDDは「超能力」になると語っています。同時に、エージェントがテストを通すためにテストを消そうとする、とも。

ただし、成立には条件があります。

  • 何をテストするかは人間が決め、テストの変更は人間が承認する:AIはテストに合わせにいきます。テストを通すためだけの特殊処理を書いたり、テスト自体を書き換えたりする。テストコードを書く作業はAIに手伝わせてもいいですが、何を確かめるかは人間が決め、変更は必ず人間が見ます。お願いではなく仕組みで止めるのが確実です。Claude Codeならpermissionsでテストディレクトリの編集を禁止できますし、GitHubならCODEOWNERSでテストの変更にレビューを必須にできます。
  • テストの強さを確かめる:わざとコードを壊してテストが落ちるか見るミューテーションテスト(JS/TSならStryker)、入力を大量に投げるプロパティベーステスト(fast-check)が効きます。
  • 画面は画面単位で縛る:Reactならコンポーネント単位のテストと、実際に操作するE2Eテストを組み合わせる。壊れたときに「この画面のここ」と場所が言えます。

これが揃えば、AIが書いた層を、読まずに下の層と同じように扱えます。

4つの理由を、AIの層に移す

最初に挙げた「機械語を信用できる4つの理由」は、AIの層でも自分で作れます。

機械語の層AIの層で作るもの
実績:世界中で何十年も動いてきた人間が決めたテストが、毎回通っていること
仕様:言語と命令セットの仕様リポジトリに残した第3言語と、穴を埋めたテスト
分業:疑う人がいる疑う役を置く。書いたAIとは別の目によるレビュー
追跡可能性:どこで何が起きたか言える指示とコミットの対応、画面単位のテスト

4つ目は見落としがちです。どの指示からどのコードが出たかが辿れれば、障害のときに「この指示のこの解釈が悪かった」と言えます。指示ごとに小さくコミットし、コミットメッセージに指示の要点を残すだけで、かなり変わります。

「意図通り動く」と「やってはいけないことをしない」は別のゲート

AIが決定的なコードを出し、それが意図通りに動くと確かめられているなら、それでOKだと思います。ただし、それがセキュリティに違反していたらダメです。検証には軸が2つあります。

軸問い確かめ方
機能意図通り動くか仕様に沿ったテスト(TDD)
セキュリティやってはいけないことをしていないか別のゲート:静的解析、依存関係の脆弱性スキャン、シークレット検出、認可まわりの専用テスト

テストは「できること」を確かめるのは得意ですが、「していないこと」を確かめるのは苦手です。認可を飛ばしていないか、入力を素通しにしていないか、秘密情報をログに出していないか。意図通りに動きながら、同時に穴が開いている、ということは普通に起きます。

もう一つ大事なのは、書いたAIとは別の目で見ることです。書いた本人に自分の穴を探させると、同じ前提で見落とします。

そして、機能のテストは「こう動くべき」を書くのに対し、セキュリティのテストは「こう使われたら困る」を書きます。後者、つまりアビューズケースは、悪用を想像できる人にしか書けません。AI駆動開発で人間が一番価値を出せるのは、たぶんここです。

まとめ:層が上がるたびに、同じ話を繰り返してきた

人間は信用の基準を書き、AIは実装を書き、検証は別の目で行う。これが、AIのコードを機械語と同じように扱うための条件です。

振り返ると、あの1万行はこの条件をひとつも満たしていませんでした。決定性だけを頼りに信用していた。動いたのは偶然です。AIの出力はもともと偶然の産物で、私はその偶然を確かめないまま固めて出荷しました。

機械語を疑わずに済むのは、どこかで誰かが疑ってくれているからです。AIが書いた自分のプロジェクトのコードには、その誰かがいません。疑う役は、人でもテストでも別のAIでもいいから、自分で置くしかありません。

  • 第3言語(指示と仕様)は人間が書いて、リポジトリに残す
  • AIが埋めた仕様の穴は、人間が決めたテストで固定する
  • セキュリティは、機能テストとは別のゲートと別の目で見る
  • その下の層は、世界の検証を信用する

1950年代にFORTRANが出たとき、アセンブラを手で書いていたプログラマーの多くは「機械が書いたコードは手書きにかなわない」と疑いました。開発チームを率いたJohn Backusは、手書きの半分の速さしか出なければ受け入れられないと考え、最適化するコンパイラを作ることで答えました。信用は説得ではなく、仕組みで勝ち取られたわけです。クラウドが出たときは「触れないサーバーは信用できない」でした。層が一段上がるたびに、同じ抵抗と、同じ決着がありました。

AIのコードは、その何回目かです。読めることに価値を置いてきた人ほど戸惑うかもしれません。でも問うべきは「読むかどうか」ではなく、「何が揃っていれば信用できるか」です。

あなたのチームなら、AIの書いたコードをコンパイラが出した機械語と同じように扱うために、何が揃っていれば信用できますか。


参考

コメント

タイトルとURLをコピーしました