LLM時代のソフトウェア開発は「読めない」問題を抱えている——MIT研究が示す驚くべき1つの解決策
大規模言語モデル(LLM)のコーディングアシスタントは驚くほど便利です。
しかし、LLM時代のソフトウェア開発では多くの開発者がこんな悪夢を経験しているのではないでしょうか?
「ユーザーモデルに簡単な変更を加えたら、なぜか決済処理のロジックが予期せず壊れてしまい、週末がデバッグで潰れた」。なぜ、このような問題が頻繁に起きるのでしょうか?
そして更に修正させると別のところが壊れる、、、バイブコーディングを行なっているとそんなことが多々あるかと思います。私もあります。
この問題に、MITの研究者たちが発表した論文「What You See Is What It Does: A Structural Pattern for Legible Software」が一つの答えを示しています。
今回はその論文についてまとめてみたいと思います。
Table of Contents
LLM時代のソフトウェア開発の問題点
彼らが指摘するのは、問題はLLMの能力不足にあるのではなく、現代のソフトウェアが抱える根本的な「可読性の低さ(illegibility)」にあるという点です。
つまり、コードの特定の部分が、ユーザーから見えるどの振る舞いに対応しているのかが、直接的かつ明確に対応していないことが問題の核心なのです。
興味深いことに、彼らはLLMがプロンプトという形で「仕様」を開発の最前線に引き戻したとも指摘しています。
LLM時代のソフトウェア開発に対する新しい提案
そうした問題点に関するアプローチとして、この論文ではコンセプト (Concepts) と シンクロナイゼーション (Synchronizations) という新しい構造パターンによる解決を提案しています。
その中から特に、従来の常識を覆すかもしれない、驚くべき5つのポイントに絞ってご紹介します。
LLMはソフトウェアの「可読性の低さ」を暴いた
論文が指摘する核心的な問題は、現代のソフトウェアの多くが「読みにくい(illegible)」状態にあるということです。
これは、コードのある断片を読んでも、それがユーザーに見えるどの機能に対応しているのかが明確ではない、という状況を指します。
例えば、従来のオブジェクト指向的なアプローチでは、一つのUserクラスが、認証のためのパスワード情報、公開用のプロフィール情報、本人識別のためのID管理など、複数の異なる関心事をまとめて扱ってしまうことがよくあります。
LLMがコードを生成・修正する際に既存の機能を壊しやすいのは、まさにこの「コードと振る舞いの間の不明確なマッピング」が原因です。
LLMはコードの局所的な変更は得意ですが、その変更がシステムの広範囲にどのような副作用を及ぼすかを完全に理解することはできません。なぜなら、その関係性がコード上からはっきりと読み取れないからです。
この論文が提案するアプローチでは、Userクラスはパスワード、プロフィール、ユーザー(ID管理)といった、それぞれが独立した「コンセプト」に分割します。
この視点に立つと、LLMは単なるコーディングツールではなく、私たちが長年抱えてきたソフトウェア構造の欠陥を浮き彫りにする「鏡」のような存在だと言えます。
LLMがうまく機能しないとき、それはモデルの限界だけでなく、私たちのソフトウェアの構造的な問題を示唆しているのです。
解決策は「徹底的に独立した」サービス
この「読めない」問題を解決するために論文が提案するのが、コンセプトという構成要素です。コンセプトとは、パスワード、投稿、コメントといった、ユーザー向けの単一目的の機能を持つ、完全に独立したサービスを指します。
ここで重要なのは、コンセプトが従来のマイクロサービスと決定的に違う点です。
多くのマイクロサービスアーキテクチャが結局は「もつれた接続の網(tangled web of connections)」に陥りがちなのに対し、コンセプトは互いに直接APIを呼び出したり、状態を問い合わせたりすることが一切ないという徹底した独立性を持ちます。各コンセプトは、他のコンセプトの存在を一切知りません。
例えば、「コメント」コンセプトは、コメントの対象が「投稿」であるか「商品」であるかを気にしません。ただ、何らかの対象に対してコメントを紐づける機能だけを提供します。この徹底した分離により、一つのコンセプトへの変更が、他のコンセプトに予期せず影響を及ぼすこと——論文の言葉で言えば「インテグリティ(以前のインクリメントを破壊しないこと)を損なう」こと——を構造的に防ぐことができるのです。
サービス間の連携は宣言的な「ルール」に任せる
では、コンセプトが完全に独立した「島」であるならば、機能的なアプリケーションを形成するための「橋」はどのように架ければよいのでしょうか?
その役割を担うのが、もう一つの構成要素であるシンクロナイゼーションです。これは、コンセプト間のすべてのデータフローや制御フローを司る、イベントベースのシンプルなルール群です。
例えば、「ユーザーが投稿にコメントしたら、投稿者に通知を送る」というビジネスロジックを考えてみましょう。
従来のアーキテクチャでは、このロジックはCommentServiceの中やPostServiceの中に埋め込まれるかもしれません。しかし、この新しいパターンでは、以下のような宣言的なルールとして記述されます。
「Webからのコメント要求があり、コメントコンセプトへの書き込みが成功したら、通知コンセプトのアクションを呼び出す」
このように、複雑なビジネスロジックが、システムの他の部分から完全に分離された、読みやすい「ルール」として表現されます。これにより、機能の追加や変更が、他のコンセプトのコードを一切触ることなく、小さなルールの追加や修正だけで済むようになります。
改善されたインクリメンタリティは、コンセプトの独立性から得られます。これにより、各コンセプトは他を知ることなく生成・修正が可能になります。また、シンクロナイゼーションの粒度の高さにより、振る舞いの拡張は、小さなシンクロナイゼーションの追加、削除、置換として表現できることが多くなります。
インクリメンタリティとは
ここでいうインクリメンタリティとは堅牢なコーディングを実現するための主要な要件の一つで、これは、ソフトウェア開発において、以下の能力を指します。
- 局所的な変更による小さな増分の提供:局所的な変更を行うことによって、システムの機能に小さな増分(インクリメント)を提供できる能力です。
- 既存機能の整合性の維持:新しい機能を追加してシステムの機能を成長させると同時に、既存の機能の整合性(integrity)を維持する能力です。
ソフトウェアが「判読不能」(コードと実際の挙動が直接対応していない)であったり、モジュール性が不十分であったりすると、このインクリメンタリティの要件が満たされにくくなります。
特に、LLM(大規模言語モデル)を利用したコーディングでは、既存の機能が壊れるパッチが推奨されてしまうことがあり、インクリメンタルに作業が進められない場合、LLMの役割は制限されてしまいます。
提案されている「コンセプト」(concept)と「同期」(synchronization)という新しい構造パターンは、インクリメンタリティを向上させるように設計されています。
コンセプトは互いに完全に独立したサービスとして機能するため、他のコンセプトについての知識を持たずにそれぞれを生成したり変更したりすることが可能になり、インクリメンタリティが向上します。
コンセプトの概念上では、同期(概念間のデータや制御フローを調整するイベントベースのルール)の粒度が高く設計されているため、振る舞いの拡張は、既存のコードに広範な変更を加えるのではなく、多くの場合、小さな同期の追加、削除、または置き換えとして表現できます。
この構造により、開発は非常に粒度が高く、インクリメンタルなスタイルで行うことが可能になります。
従来の開発では、小さな新しいパーツ(機能)を追加しようとするたびに、城全体の土台(既存の機能)が崩れてしまうリスクがありましたが(整合性の失敗)、コンセプトと同期を用いたアプローチでは、パーツ(コンセプト)が独立しており、接続ルール(同期)が明確かつ細分化されているため、新しいパーツを安全に、かつ局所的な修正だけで追加していけるようになります。
状態を隠すのではなく「抽象的に公開」する
オブジェクト指向設計の「情報隠蔽(カプセル化)」の原則を学んだ人にとって、状態を隠すのではなく「抽象的に公開」するというのは最も直感に反するポイントかもしれません。実際、私たちは、オブジェクトの状態は隠し、メソッドを通じてのみアクセスべきだと教わってきました。
しかし、このパターンでは逆のアプローチを取ります。
コンセプトはその状態を(抽象的な形で)外部のシンクロナイゼーション層に公開します。
これは生のデータベーステーブルを晒すという意味ではありません。むしろ、「どのユーザーがどの投稿を持っているか」といった構造化されたリレーショナルなビューを提供し、シンクロナイゼーション層がコンセプトの内部実装を知ることなくクエリできるようにすることを意味します。
この設計の目的は、コンセプト自身に無数の「状態取得のためのメソッド(ゲッター)」を実装する手間を省き、シンクロナイゼーション層が複数のコンセプトにまたがる複雑なデータ問い合わせ(例:「あるユーザー名を持つユーザーのプロフィール情報を取得する」)をシンプルかつ効率的に行えるようにするためです。
そして、この一見過激に見える設計思想こそが、このアーキテクチャが持つ最大の超能力、すなわち完全な透明性を可能にする鍵です。
シンクロナイゼーションエンジンがコンセプトの状態を読み取れるからこそ、システム内のあらゆるアクションの完全な因果関係の記録、すなわち「来歴トレース」を構築できるのです。
バグ修正が劇的に変わる「来歴追跡(Provenance Tracking)」
このアーキテクチャがもたらす最大の利点の一つが、システムの透明性と来歴追跡(Provenance Tracking)能力です。
すべての動作は明示的なシンクロナイゼーションによって引き起こされるため、あるアクションが「いつ、どのルールによって、どのアクションをきっかけに」実行されたのかという因果関係の完全な記録(トレース)が自動的に生成されます。
論文では、この能力がいかに強力かを示す具体的なバグ修正の事例が紹介されています。
そのバグとは、ユーザー登録フローにおいて、パスワードが検証される前にユーザーがデータベースに作成されてしまう、というものでした。
その結果、無効なパスワードで登録しようとすると失敗するにもかかわらず、データベースには不完全なユーザーデータが残り、次に同じユーザー名で正しく登録しようとしても「ユーザーが既に存在します」というエラーで失敗してしまっていたのです。
開発者は、バグが発生した処理の来歴トレースを追跡し、User/registerアクションが、Password/setアクションが失敗する前に実行されていることを即座に発見しました。
そして驚くべきことに、原因となった数個の小さなシンクロナイゼーション(ルール)だけをLLMに渡し、「パスワードを検証する前にユーザーが作成されている。これを修正してほしい」と依頼したのです。
LLMはパスワード検証を先に行うようにルールを的確に修正し、開発者はそのルールを置き換えるだけで、他の機能に一切影響を与えることなく問題を解決できました。
これは、システムの振る舞いが透明であり、変更の単位が極めて小さく分離されているからこそ可能になる、強力な開発・デバッグ手法です。
まとめ:人間とAIの両方にとって「読みやすい」ソフトウェアへ
今回紹介した「コンセプト」と「シンクロナイゼーション」というパターンは、ソフトウェアの可読性、モジュール性、そして透明性を劇的に向上させる可能性を秘めています。
コンセプトは、機能を徹底的に独立させることで、変更の影響範囲を局所化します。
シンクロナイゼーションは、複雑なビジネスロジックをシステムから分離し、宣言的なルールとして記述することで、振る舞いを明確にします。
この構造は、人間が理解しやすいだけでなく、コードの文脈理解が限定的なLLMにとっても、遥かに扱いやすいものになります。変更点が明確に分離されているため、LLMが意図せず他の機能を破壊するリスクを大幅に低減できるのです。
将来的には、セキュリティやパフォーマンスが検証済みのコンセプトをライブラリから選び、アプリケーション固有のロジックをシンクロナイゼーションとして記述する、という新しいソフトウェア開発の形が主流になるかもしれません。
