構造で意図を伝える:階層化と次元

意図のドキュメント化

前のセクションでは、Vibe Codingの三つの失敗パターンを診断しました。初期の指示が注意力の範囲から押し出される、前後矛盾する指示に優先順位がない、compactionが情報を能動的に削除する。これら三つの問題には共通の構造的原因があります。意図が対話の中に存在しているということです。

意図をドキュメントに書き出すと、これら三つの問題が同時に緩和されます。

考えが変わった場合、対話の中に新しい指示を追加してAgentが新旧の指示の優先順位を正しく判断してくれることを祈る必要はありません。ドキュメントのその行を直接書き換えればよいのです。ドキュメントの現行バージョンには常に一つの立場しかなく、調整すべき矛盾は存在しません。

ドキュメントは対話から独立して存在し、チャットの内容とcontextの空間を奪い合うことがありません。対話の中では探索的なアイデア、デバッグの過程、一時的な試行をいくらでも議論できます。しかし最終的な意図はドキュメントに沈殿します。Agentがあなたの意図を知る必要があるとき、ドキュメントを読めばよく、何十回もの対話から抽出して推論する必要はありません。

ドキュメントはファイルシステムに永続化されます。対話がcompactionで圧縮されても、ドキュメントは影響を受けません。Agentはいつでも完全な原文を再読み込みできます。対話の前半で二十分かけて説明したアーキテクチャ上の制約も、ドキュメントに書き込まれていれば、compactionによって消えることはありません。

多くの開発者が、純粋な対話方式からspecドキュメントの維持に切り替えた直後に、出力品質の変化を実感しています。Agentが制約を忘れなくなったのは、制約がドキュメントに書かれていて毎回ロードされるからです。この方向性は正しいものです。

しかし自然言語のドキュメントは新たな問題を持ち込みます。曖昧さです。あなたがある記述を書き、意味は明確だと思います。Agentも意味は明確だと理解します。しかし両者が理解しているものは同じでないかもしれません。この差異は、Agentが生成したコードを見るまで発覚しません。

曖昧さを解消する方法は、自然言語をもっと書くことではありません。三ページの記述は三行の記述より曖昧さが多い場合があります。文が増えるほど、異なる解釈が可能な箇所が増えるからです。本当に有効な方法は、正しい構造で書くことです。何が正しい構造かを理解するには、まず情報そのものの性質を理解する必要があります。

四層の情報と階層別ロード

人間のチームがどのように協働するかを考えてみましょう。

五人の開発チームがブログシステムを開発しています。プログラマAは記事エディタを担当し、プログラマBは検索機能を担当しています。AはBが検索インデックスにどのフィールド名を使っているか知りません。BはAのエディタがどのリッチテキストライブラリを使っているか知りません。しかし最終的にコードを統合すると、検索はエディタで公開した記事を検索でき、エディタで保存したコンテンツは検索で正しくインデックスされます。

これはどうやって実現されているのでしょうか。AとBは十分に高いレベルのcontextを共有していたのです。二人ともこの製品が非技術者向けであることを知っていたので、インタラクションはシンプルにする必要がありました。二人ともシステムがフロントエンドとバックエンドに分離されていて、APIはRESTで、データベースはPostgreSQLであることを知っていました。二人とも記事がpostsテーブルに保存され、title、body、published_atといったフィールドがあることを知っていました。相手のコードのすべての詳細を知る必要はありませんでした。これらの共有された高レベルの意思決定が、それぞれの実装方向を十分に制約していたからです。

もしこれらの高レベルの共通認識がなければどうなるでしょうか。Aが独自の記事データ構造を定義し、Bも独自のものを定義して、二つは互換性がない。Aが記事のコンテンツをHTMLで保存したのに、Bの検索エンジンはプレーンテキストを期待している。二人のコードはそれぞれ単独では動きますが、統合するとすべてが破綻します。

この観察は、情報に関する基本的な事実を明らかにしています。情報には自然に階層があり、異なる階層の性質はまったく異なるということです。

このブログシステムを例に取ると、上から下へ少なくとも四つの層が見えます。

最上層はプロダクトのvisionです。「非技術者でも簡単にコンテンツを公開・管理できるようにする。」この文は非常に抽象的で、実装の詳細をほとんど含みません。しかし変化する頻度は極めて低く、プロダクトのライフサイクル全体を通じて変わらない可能性があります。そして適用範囲が最も広く、チームの全員がこれを知っている必要があり、すべての意思決定がこれと一貫している必要があります。

次の層はアーキテクチャの意思決定です。「フロントエンドとバックエンドを分離し、APIはREST、データベースはPostgreSQL、フロントエンドはReact。」これらの意思決定はvisionより具体的ですが、依然としてシステムレベルのものです。変化することもありますが(たとえばRESTからGraphQLへの移行)、その頻度は具体的な機能よりはるかに低いです。適用範囲は開発チーム全体であり、全員が遵守する必要があります。

さらに下の層はfeature specです。「記事検索機能:キーワードによる全文検索をサポートし、結果は関連度順にソートし、結果がない場合はプロンプトメッセージを表示する。」これはアーキテクチャの意思決定よりはるかに具体的で、プロダクトのイテレーションに伴って頻繁に変化します。今月の検索はタイトルのみ対応ですが、来月には全文対応が必要になるかもしれません。適用範囲も狭くなり、検索機能を担当する人だけがその詳細をすべて理解している必要があります。

最下層はtaskの詳細です。「search.tsのbuildQuery関数でPostgreSQLのtsvector全文検索を呼び出し、結果はrelevanceの降順で上位20件を取得する。」これは最も具体的な情報であり、一回のコードレビューのフィードバックで変わる可能性があります。適用範囲は最も狭く、このtaskを実行している時にのみ必要です。

四つの階層の情報には明確なパターンがあります。上から下へ、より具体的になり、変化の速度が速くなり、知る必要のある人が少なくなります。

この階層化は組織上の好みではありません。情報の内在的な性質を反映しています。visionが抽象的なのは、それが目標を記述しているのであってパスを記述しているのではなく、同じ目標に到達するパスは多数あり得るからです。taskが具体的なのは、それがあるパス上の一つのステップを記述しており、実行可能な精度が必要だからです。これら二種類の情報のライフサイクル、変化の頻度、適用範囲は本質的に異なり、混在させて管理するのは不合理です。

この階層化はAgentのcontext制約に直接関係しています。

四つの階層の情報をすべてAgentのcontextに投入すると、大部分の情報は現在のタスクにとってノイズになります。Agentが検索機能を実装している時、コメントシステムのspecを知る必要はなく、ユーザー認証モジュールのtask詳細を知る必要もありません。これらの情報はcontextの空間を占め、注意力を分散させますが、現在のタスクのアラインメントには何の助けにもなりません。

合理的な方法は、階層ごとにロードすることです。高層の情報(vision、アーキテクチャの意思決定)は安定しており適用範囲が広いので、毎回ロードします。低層の情報(現在のfeatureのspec、現在のtaskの詳細)は必要な時にのみロードし、完了したらcontextから除去できます。だからこそ、情報を異なるドキュメントに分割する必要があるのであり、すべての内容を含む一つの大きなファイルを維持するのではないのです。

Ryanの実践はこの原則の具体的な体現です。彼のproject context(project-context.md)は高層の情報を担っています。技術スタック、アーキテクチャ、ディレクトリ構造、モジュールインデックス、コードパターンです。このドキュメントは200行以内に制限されており、Agentは毎回作業開始時にこれをロードします。各featureには独自のspec、task list、checklistがあり、Agentがそのfeatureを実行する時にのみロードされます。さらにCLAUDE.mdを使って、階層をまたぐ強制的な制約(コーディング規約、データベースの直接操作の禁止といったルール)を保存しています。三種類のドキュメントが異なる階層と異なるロード戦略に対応しています。

ある情報がどの階層に属するかを判断するには、二つの指標を見ます。変化の頻度と適用範囲です。ある情報がプロジェクトのライフサイクル全体を通じてほとんど変わらず、すべてのタスクがそれを必要とするなら、高層に属し、毎回ロードされるプロジェクトコンテキストに配置すべきです。ある情報が特定のfeatureにのみ関連し、feature完了後は陳腐化するなら、タスク層に属し、そのfeatureのspecに配置すべきです。

階層化が適切かどうかを判断するための二つのシグナルがあります。各featureのspecで毎回同じ情報を繰り返し書いていることに気づいたなら、たとえば毎回「REST APIを使うこと、GraphQLは使わないこと」とAgentに念を押しているなら、その情報はプロジェクトコンテキスト層に引き上げるべきで、一度書けば十分です。逆に、プロジェクトコンテキストが数千行に膨張し、Agentが毎回ロードするのに大量のcontext空間を消費しているなら、低層の詳細が多すぎるので、具体的なfeature specに移すべきです。

意図、受入、制約

階層化は「なぜ情報を分割する必要があるのか」と「各情報をどの階層に配置すべきか」に答えます。次の問いは、各階層の中に何を書くべきかです。

コミュニティの主流specフレームワークを見ると、テンプレートの具体的な設計は大きく異なります。BMADのspecには十数個のフィールドがあり、OpenSpecは比較的簡潔で、Spec Kitには独自の構造があり、AILock-Stepはまた別の体系です。しかしフィールド名や構成方法の違いを無視して、実際にどのような質問への回答を求めているかを観察すると、一貫したパターンが見えてきます。各階層において、三つの次元から情報を記述する必要があるのです。

意図:この階層の目的は何か。

意図の次元は「何をするか」と「なぜするか」に答えます。その内容は各階層で異なりますが、機能は同じです。その階層の方向性を確立することです。

vision層では、意図はプロダクトが解決すべき根本的な問題です。「非技術者でも簡単にコンテンツを公開・管理できるようにする。」アーキテクチャ層では、意図はシステムがなぜこのように構成されているかです。「フロントエンドとバックエンドを分離する。プロダクトはWebとモバイルの両方をサポートする必要があり、同一のAPIセットを共有することで重複開発を削減できるためである。」feature層では、意図は通常ユーザーストーリーで表現されます。「ブログの著者として、キーワードで公開済みの記事を検索したい。以前書いた内容を素早く見つけられるようにするためだ。」この文には三つの情報が含まれています。誰が(ブログの著者)、何を(キーワードで公開済み記事を検索)、なぜ(過去の内容を素早く見つける)。いずれか一つが欠けても、Agentの理解がずれる可能性があります。「誰が」がなければ、これが著者向けの検索なのか読者向けの検索なのか区別できず、両者の要件はまったく異なります。「なぜ」がなければ、検索の優先すべき特性が精度なのか速度なのか判断できません。

ユーザーストーリーはプロダクトマネジメントの分野から来ており、ここではその方法論を詳述しません。Agent開発において特有の価値を持つのは、ユーザー価値で記述された要件は本質的に検証可能性を備えているという点です。「ユーザーはキーワードで記事を検索できるか」には明確なyes/noの答えがあります。この特性は、後の実践編で非常に重要になります。

受入:この階層が正しいかどうかをどう知るか。

受入の次元は三つの次元の中で最も重要です。各階層で検証しているのは一つの核心的な問い、すなわちこの階層の内容が上位層の意図を忠実に実現しているかどうかです。

vision層では、受入はユーザーストーリーとユーザージャーニーがvisionを反映できているかの確認です。visionが「非技術者でも簡単にコンテンツを公開できるようにする」と述べているのに、ユーザージャーニーの中にユーザーがMarkdownレンダリングエンジンを手動で設定するステップがあれば、ジャーニーとvisionの間に乖離が生じています。アーキテクチャ層では、受入は技術的な意思決定がPRDの要件を支えられるかの確認です。PRDがリアルタイム協調編集を要求しているのに、アーキテクチャがWebSocketをサポートしないフレームワークを選択していれば、アーキテクチャと要件の間にギャップがあります。

feature層では、受入は通常Given/When/Then形式のシナリオで表現されます。たとえば、Given: データベースにタイトルに「パフォーマンス最適化」を含む記事がある、When: ユーザーが検索ボックスに「パフォーマンス」と入力して検索ボタンをクリックする、Then: 検索結果にその記事が表示され、マッチしたキーワードがハイライトされる。

一つのfeatureは通常、異なる状況をカバーするために複数の受入シナリオを必要とします。正常パスが一つのシナリオです。境界ケースが別のシナリオです。検索語が空の場合はどうなるのか。異常ケースがまた別のシナリオです。マッチする結果がない場合は何を表示するのか。各シナリオは、featureの振る舞いが意図と一致しているかを検証しています。

受入シナリオの価値は、「正しく作れた」を主観的な印象から検証可能な条件の集合に変えることにあります。受入シナリオがない場合、あなたとAgentの「完成」の定義はまったく異なる可能性があります。Agentは検索機能が完成したと考えます。検索できて、結果が出る。あなたは完成していないと考えます。結果がハイライトされていない、ページネーションがない、検索語が空の場合にエラーが直接表示される。それぞれの「完成」基準は妥当ですが、アラインメントが取られたことは一度もありません。受入シナリオは、こうした暗黙の期待の差異を事前に露出させるツールです。

Ryanは実践の中で、受入シナリオがspecをレビューする際の最も効率的な確認手段でもあることを発見しました。ユーザーストーリーと対応するGherkinシナリオを一目見るだけで、Agentの要件理解が自分と一致しているかどうかを判断できます。彼の原文はこうです。「一つのuser storyと数個のシナリオ記述があれば、AIと自分の理解が一致しているかを判断できる。」

制約:この階層の境界はどこか。

制約の次元は「やるべきでないこと」と「すでに利用可能なものは何か」に答えます。各階層で防いでいるのは同じことです。意図の範囲外で予期しない変更を生み出すことです。

vision層では、制約は市場の限定、コンプライアンス要件、ビジネスモデルの境界かもしれません。アーキテクチャ層では、制約は技術的な能力の境界、パフォーマンス要件、既存システムとの互換性です。feature層では、制約が答えるのは以下の問いです。今回の変更の境界はどこか。触れてはならないモジュールは何か。再利用できる既存のコンポーネントは何か。

feature層の制約は特に見落とされやすいものです。Agentに検索機能を追加するよう依頼すると、ついでに記事一覧ページのソートロジックをリファクタリングするかもしれません。検索結果のソートと一覧のソートを統一すべきだと判断したからです。技術的な観点からはこれは妥当かもしれません。しかしこの変更はあなたの期待の範囲外であり、既存ユーザーの使用習慣を壊す可能性があり、一覧のソートロジックに依存する他の機能に影響を与える可能性があります。制約の次元の役割は、変更の境界をAgentに明確に伝えることです。検索機能のみを実装し、一覧ページのソートには手を触れないこと。

Ryanのspecテンプレートにはコンテキスト分析フィールドがあり、三つの内容を含みます。参照コード(プロジェクト内の再利用可能な既存モジュール)、関連ドキュメント(このfeatureに関連する設計ドキュメントと過去の議論)、過去のfeature(以前実装した関連機能とその変更記録)。このフィールドはAgentが既存のものを理解するのを助け、車輪の再発明を効果的に防ぎます。

空フィールドのコストは強調する価値があります。Ryanはあるパターンを観察しました。specの中で技術方針に関するフィールドが空の場合、後続のコーディングを担当するAgentは独自の方針を捏造し始めます。捏造された方針はプロジェクトの既存アーキテクチャと互換性がないかもしれず、不必要な依存関係を導入するかもしれず、既存の機能を重複実装するかもしれません。空フィールドが伝えるシグナルは明確です。その次元の意図アラインメントが行われなかったということです。Agentはその次元で何の指針も受け取っておらず、自ら推測するしかありません。

分解プロセスにおけるドリフト

三つの次元は各階層に何を含めるべきかを記述しています。しかし実際の作業では、すべての階層を一度に書き上げるわけではありません。まずfeature specを書き、次にAgentにspecからtask listを生成させ、さらにtask listからchecklistを導出させます。各ステップで、より低い階層の新しい情報が生成されます。

この生成プロセス自体がドリフトを引き起こします。

ドリフトには二つの発生源があります。一つ目は、Agentが「翻訳」の過程で独自の解釈を加えることです。specに「ユーザーが記事を検索できる」と書かれていると、Agentはそれを具体的な実行ステップに翻訳します。「search.tsにbuildQuery関数を追加し、PostgreSQLの全文検索を呼び出す。」この翻訳プロセスでAgentは一連の意思決定を行います。関数名を何にするか、どのデータベースインターフェースを呼び出すか、戻り値のフォーマットは何か。これらの意思決定はあなたの意図と一致しているかもしれませんし、一致していないかもしれません。specにtsvectorを使うかLIKEクエリを使うかは書かれておらず、Agentが独自に選択しました。選択した方向があなたのパフォーマンス上の期待に合わなければ、それがドリフトです。

二つ目の発生源はより目立ちにくいものです。Agentがtask listを生成する時点で、contextにはすでに大量の情報が蓄積されています。project context、feature spec、対話履歴、以前生成したコード断片。specのある制約が、注意力の配分不足により無視されるかもしれません。これは前述の注意力の減衰と同じメカニズムですが、今回は対話の過程ではなく、階層生成の過程で発生しています。Agentが意図的にあなたの制約を無視したのではなく、contextの情報が多すぎて、その制約が注意力の競争で敗れたのです。

二つのドリフトの帰結は同じです。下位層の情報が上位層の意図と不一致になります。そしてドリフトは階層を経るごとに蓄積します。specからtaskへの段階で少しずれ、taskからコードへの段階でずれた基盤の上にさらにずれるかもしれません。最終的な成果物だけでアラインメントを確認すると、元の意図から大きくかけ離れた結果に直面する可能性があり、どの段階でずれ始めたのかを特定するのは非常に困難です。

そのため、受入は最後に一度行えば済むものではありません。新しい階層の情報を生成するたびに、上位層との一貫性を確認する必要があります。

クロスバリデーションによるドリフトの検出

効果的な方法の一つは、Agentに同じ意図を異なる角度から見させ、複数の出力間の一貫性を比較することです。

Ryanのやり方は三ラウンドの思考です。第一ラウンドでは、要件から出発して実装方針と影響分析を生成し、specドキュメントにまとめます。これはAgentによる要件の一回目の理解です。第二ラウンドでは、specから出発して具体的な実行ステップに分解し、task listにまとめます。これは二回目の理解です。ステップに分解するプロセス自体が一種の検証です。あるステップが明確に書けない場合、その箇所のspecがまだ十分に具体的でなく、方針に穴があることが多いからです。第三ラウンドでは、task listから出発して受入基準を逆算し、checklistにまとめます。これは三回目の理解であり、方向は前の二回とは逆です。「何をするか」から「どうやるか」を推論するのではなく、「どうやるか」から「正しくできたかどうかをどう判断するか」を逆算します。

三つのドキュメント間の一貫性がアラインメントの証拠です。矛盾がドリフトのシグナルです。

checklistにある検査項目が現れたが、specには対応する機能がまったく言及されていない場合、Agentがtask生成の過程でspecにカバーされていない内容を独自に追加したことを意味します。それは合理的な補完かもしれません(あなたがその状況を確かに見落としていた)し、Agentが独自に範囲を拡張したのかもしれません(要件の範囲を誤解した)。いずれにせよ、矛盾の存在自体が確認する価値があります。

あるchecklist項目がspecの記述と直接矛盾している場合、たとえばspecには「検索結果は関連度順にソートする」と書かれているのに、checklistの受入基準が「時間順にソートする」となっていれば、specからtask、さらにchecklistへの伝達過程で意図のずれが発生したことを意味します。このクロスチェックを行わなければ、このずれは最終的なコードにまで持ち込まれます。

Ryanは第三ラウンドを「対抗的テスト」と呼んでいます。この名前は的確です。前の二ラウンドとはまったく異なる角度(実装ではなく受入)から同じ要件を再検証しているからです。三つの異なる角度から見たものが同じであれば、アラインメントが保たれていると信じる根拠があります。三つの角度から見たものが異なれば、コードを書く前に問題を発見できたことになります。

この方法の本質は「三つのドキュメント」という具体的な形式に依存するものではありません。核心的な考え方は、ある階層の情報から次の階層を生成する際に、異なる角度で両者の一貫性を検証するということです。具体的な実装としては、Ryanのspec/task/checklistの三ドキュメント体系でもよいですし、別の独立したAgentに最初のAgentの出力をレビューさせることでもよいですし、自動化された一貫性チェックツールでもかまいません。Spec Kitのanalyzeコマンドがまさにこれを行っています。生成済みのすべてのドキュメントを読み取り専用でスキャンし、ドキュメント間の重複、矛盾、欠落、用語の不一致がないかを確認します。

形式は異なっても、原則は同じです。新しい階層の情報を生成するたびにドリフトのリスクがあり、その境界で検出するメカニズムが必要です。

構造で曖昧さを排除する

ここまでの内容は、曖昧さを排除するにはより多くの内容を書き、より多くのフィールドをカバーすべきだという印象を与えるかもしれません。この誤解を明確に解いておく必要があります。

曖昧さの根本原因は情報量の不足ではなく、重要な次元の問いに答えが出されていないことです。検索の要件を三ページの自然言語で詳細に記述し、検索ボックスの配置、色、フォントサイズまで説明することはできます。しかし「検索結果がゼロの場合はどうするか」「検索範囲には記事本文を含むか」「入力と同時に検索するのかボタンをクリックして実行するのか」といった問いに答えていなければ、三ページの記述も一行のpromptも曖昧さの程度は同じです。違いは、曖昧さがより多くのテキストの中に隠れて発見しにくくなるという点だけです。

構造化された次元(意図、受入、制約)の価値は、より多く書かせることではなく、答えなければならない問いを目の前に提示し、飛ばせなくすることにあります。たった五行のspecでも、「誰のためか」「何をするか」「正しいとはどういう状態か」「触れてはならないものは何か」という問いに答えていれば、十ページあるが実装の詳細ばかりのドキュメントよりはるかに効果的です。

Spec Kitのclarifyワークフローは興味深い設計をしています。10のカテゴリの構造化された質問を使ってspecの曖昧さを検出します。機能範囲、ドメインモデル、インタラクション設計、非機能要件、統合ポイント、境界ケース、制約条件、用語定義、完了シグナル、プレースホルダーマーカーです。各ラウンドで最大5つの的を絞った質問をします。これらの質問はspecの作成者に向けられており、自分がまだ十分に考えていない箇所を発見するのを助けます。一回のclarifyを経た後のspecは、長さが数行しか増えていないかもしれませんが、曖昧さは大幅に減少します。なぜなら、下すべき意思決定が下されたからです。

results matching ""

    No results matching ""