高速に悪性技術的負債が蓄積される世界に備えて
技術的負債は、ソフトウェアが現実の変化に適応しながら成長する過程において、避けることのできない要素である
それ自体が悪ではなく、適切に管理されることでソフトウェアの成長を後押しするものでもある
問題は、返済計画も意図の継承もないまま積み上がってしまった “悪性技術的負債” とでも呼ぶべき類のものである
これらは往々にして、ソフトウェアの保守性、拡張性を著しく低下させ、手が付けられない状態を引き起こす
これまで我々はこの課題に対し、分割統治、凝集度・結合度といった設計上の原則を用いてきた
またレビューやテスト、リファクタリングなどの実践によって、コードの構造を “人間の理解可能な範囲” に保とうとしてきた
こうした努力は、個人差を埋めるための知的インフラとして機能してきたが、LLM の登場によって状況は一変する
LLM は現在の意図を読み取り、高速にコードを生成・修正できる
だが過去の経験、設計の理由、文脈といった背景情報までは前提として与えられない限り理解できない
そして背景情報とは、時間とともに自然に増えていき、これを明示的に残し続けるには、相応の労力が求められる
この構造のもと、LLM は過去の意図を十分に踏まえずに見た目上 “正しそうな変更” を大量に生成する
結果として、従来よりもはるかに高速に悪性技術的負債が蓄積されうる土壌を作り出してしまった
現状では、LLM の生成コードを人間がレビューし、適切に判断することでバランスを取っているが、これは “人間がボトルネック” であることを示している
今後、LLM ネイティブな開発を持続的かつ健全に進めるためには、このボトルネック構造を前提にしないアプローチが必要となる
ここでは “人間をボトルネックにせず、LLM と共に健全に開発を続けるためには何が必要か” に対するヒントを探る
LLM ネイティブな開発組織とは
コードリーディングにおいて最も困難なのは、単にロジックを追うことではない
異なる思想のもとに書かれたコードを読むとき “なぜそう書かれたのか” に辿り着くまでに最も時間がかかる
アーキテクチャ図や設計書のようなドキュメントは、その背景にある思想を推測するヒントにはなるが、答えを与えてくれるわけではない
特に LLM を活用する開発においては、構造の変化やコードの更新が高速に行われる一方で、判断の背後にある思想が構造に定着せず、明示的に残らないまま失われているリスクがある
言葉による解釈は人によって揺らぐため、実際にはモブプロやレビューといったプロセスの中で、チーム内の思想のズレを少しずつ埋めてきた
しかし LLM を前提にした開発においてはこの仕組みが成立しない
思想の揺らぎや曖昧さは、人間同士であれば対話によって解決できるが、LLM にはそれができない
LLM は与えられた文脈の中で整合的な出力ができても、文脈そのものの正しさや一貫性を自律的に評価する力は持たない
このため LLM を利用した開発では、思想の曖昧さが解消されないままコピーされ、拡散され、加速度的に悪性技術的負債として蓄積されていく
従来の ADR のようにある時点の判断を記録するだけでは、次に LLM がその文脈に基づいて判断するときに必要な “考え方の軸” まで共有することはできない
LLM と共に開発をするには “どう判断したか” ではなく “どう考えるべきか” を明文化する必要がある
思想の軸が共有されていれば、アーキテクチャ図や設計書のような静的なドキュメントは実装から自動的に生成されれば十分であり、その役割を人間が担う必要はなくなる
人間は判断の結果を記録することから判断の前提となる価値観や原理、思想そのものを言語化し、LLM を含めたチームが一貫した方向に向かえるようにすることにシフトしていく
こうした考え方が LLM ネイティブな開発組織の出発点ではないだろうか
プロンプトの構造化と現状の LLM の限界
ソフトウェアの構造は、初期から完成された設計をもとに作るものではなく、実際の要件の変化に応じて自然と分岐、発展していくものだと私は考える
例えば、タスク管理アプリの API サーバが要件の追加に応じてどのように成長していくかを考える
この場面において “価値観” とは判断の背後にある設計思想を指す
最初の段階
まず main.go
にすべてを書くことから始まる
- main.go
概念の分離
タスクやユーザのような概念が生まれ、それぞれの操作が追加されていく
- main.go
- task.go
- user.go
要件の複雑化
ロジックが肥大化してきたので、ドメインモデルを分離する
- main.go
- task.go
- user.go
- model/
- task.go
- user.go
永続化や通知など非機能要件の複雑化
キャッシュや通知処理など、非機能要件への対応が始まる
- main.go
- task.go
- user.go
- model/
- task.go
- user.go
- internal/
- persistence/
- task_repository.go
- user_repository.go
- cache/
- client.go
- queue/
- publisher.go
複数エントリポイントへの対応
バッチ処理など、別のバイナリからの操作が発生するので分離が必要になる
- cmd/
- server/
- main.go
- job/
- main.go
- task.go
- user.go
- model/
- task.go
- user.go
- internal/
- persistence/
- task_repository.go
- user_repository.go
- cache/
- client.go
- queue/
- publisher.go
複雑なルーティング
ルーティング要件が複雑化し、ハンドラとルータの責務が発生する
- cmd/
- server/
- main.go
- internal/
- router.go
- handler.go
- job/
- main.go
- task.go
- user.go
- model/
- task.go
- user.go
- internal/
- persistence/
- task_repository.go
- user_repository.go
- cache/
- client.go
- queue/
- publisher.go
このように構造は段階的に成長していく
これはあくまで私の考える成長の流れだが、これらは以下の価値観に基づいて設計判断をした
- 凝集度を高め、結合度を下げる
- 現在必要な構造に集中する
私はこれらの価値観や判断の流れ (振る舞い) を LLM に与え、同様の成長を再現できるかを試みた
しかし現時点では LLM には次のような課題がある
- 明示されていない設計判断を一般論で埋めようとする傾向がある
- 価値観や振る舞いに従い続けることが困難
そのため、現状では以下が必要になる
- 価値観と振る舞いの明示
- “やること、やらないこと” の具体的な指示
とはいえ、LLM の性能が今後さらに向上すれば、より少ない指示でも開発者の価値観を反映した最小ステップでの成長を再現できる可能性は十分にある
ソフトウェアに悪性技術的負債が生まれる原因のひとつは “未来のつもりで導入した構造が、実際の成長と乖離していくこと” にある
この問題に LLM とともに立ち向かうには、価値観そのものを共有し、成長の判断に関与させることが重要と考える
LLM がそれを理解し、模倣できるようになれば、構造を壊さずに育てるチームメンバーになりうる
LLM と健全に開発を続けるために
LLM を前提にした開発では、今後次のような構造的な問題が表出すると考える
- 人間がボトルネックになり、結局、現在の開発速度から大きく変化できない
- 一貫性のない判断が繰り返され、悪性技術的負債が蓄積する
これに対し、”人間がすべてをレビュー、判断しなくても済むように、LLM と価値観を共有できればどうか” という仮説を立ててみた
価値観に基づいた一貫した意思決定を LLM が模倣できれば、人間をボトルネックにせず、技術的負債も健全にコントロールできるのではないかと考えたのである
この仮説を、Go のパッケージ構成の変遷を例に検証してみた結果、現在の LLM では具体的な指示を完全になくすことはまだ難しいという現実が見えてきた
しかし、これは将来的な LLM の性能向上によって解消される見込みのある問題だ
そのため、今我々がやることは、価値観や振る舞いを言語化し、共有可能な形にしておくことではないかと考えている
LLM をツールとして使うだけでなく、共に育つ存在として捉える
そのためには、プロンプトで望む結果を得られたかどうかに一喜一憂するのではなく、なぜその結果が得られたのか、なぜ得られなかったのかを言語化し続けることが必要だ
この姿勢こそが LLM ネイティブな開発組織に必要な文化の土台になると私は考える
Discussion