8月24日、Toward Data Scienceが「AI Agents Don't Need More Context — They Need Typed Context」と題した記事を公開した。この記事では、AIエージェントのプロンプト組み立て時に発生する「コンテキストの型混同」バグを、軽量な型システムで防ぐアーキテクチャ実験について詳しく紹介されている。以下に、その内容を紹介する。
「コンテキストが多すぎる」のが問題ではない
AIエージェントが奇妙な出力を返したとき、開発者が取る典型的な対処法は「もっと情報を渡すこと」だ。検索ドキュメントを追加し、システム命令を足し、リマインダーをさらに重ねる。
しかしこの記事が指摘する問題の根本は、情報量ではない。コンテキストがプロンプトに組み込まれる瞬間、すべてが無差別に str になることだ。
典型的なエージェントのランタイムでは、以下の異種データが混在する。
- システム命令
- ユーザーリクエスト
- ベクトルストアから取得したドキュメント
- 会話履歴
- ツールの実行結果
- アプリケーションの状態変数
これらが "\n".join(...) で一つの文字列に結合された瞬間、境界は消える。ツール出力が命令のように読め、過去の会話履歴が現在の要件に見え、参照ドキュメントがシステム命令に化ける。
例として、配送ツールが以下を返したとする。
Order will arrive August 19.
Previous customer request: August 25.
通常のパイプラインはこの2行を区別しない。どちらも「ツール出力」として文字列に変換され、プロンプトビルダーに渡される。アプリケーションコードがこの文字列を命令として扱っても、何も止めるものがない。tool_output と system_instruction は、プロンプトビルダーに届く時点で同じデータ型 str だからだ。
XMLタグは型システムではない
「<instructions>タグや<tool_output>タグで囲めばいいのでは」という反論は自然だ。しかし著者はこれを明確に否定する。
デリミタはプレゼンテーションの選択であり、ランタイムの保証ではない。
prompt += f"<instructions>{tool_result}</instructions>"
このコードはXMLタグがあっても問題なくコンパイルされ実行される。デリミタは最終的なプロンプトテキストの「中」に境界を表現するだけであり、コードがその構造的な決定を下した後の話だ。型混同はその前に、静かに起きている。
型付きコンテキストシステムの実装
著者が構築したのは、ゼロ依存・6モジュールのPythonランタイムだ。LLMを一切呼び出さず、既存のエージェントフレームワークとも競合しない。「コンテキストオブジェクトに型検査を適用する」一点に絞った実験である。
コンテキストの型は4種類、Pythonの Enum で定義される。
| 型 | 意味 |
|---|---|
INSTRUCTION |
システム命令 |
EVIDENCE |
参照根拠 |
MEMORY |
会話履歴・永続状態 |
TOOL_OUTPUT |
ツール実行結果 |
各コンテキストアイテムはPythonの dataclass として実装され、テキスト本文に加えて以下のメタデータを持つ。
| フィールド | 内容 |
|---|---|
context_type |
上記4種のいずれか |
source |
データの発生元(システム、ツール名など) |
created |
取り込み時のタイムスタンプ |
request_id |
このアイテム固有のID |
derived_from |
変換元アイテムのID(変換操作があった場合) |
最後の derived_from フィールドが設計上の肝だ。型の昇格(型の変換操作)を、どこかのヘルパー関数の中で静かに起きた処理ではなく、監査可能な操作にする。
核心:型混同を検出するエンフォースメント境界
設計の中心は ContextStore の実装だ。ストアは内部台帳でコンテキストの内容と「最初に登録された型」を対応づけて管理する。同一内容が、明示的な変換ステップを経ずに保護された型(INSTRUCTION)として再登録されようとすると、ストアはその操作を拒否する。
existing = self._ledger.get(key)
if existing is not None:
origin_type, origin_id = existing
if origin_type != context_type:
if context_type in PROTECTED_TYPES and not _via_transform:
raise ContextTypeError(
f"{origin_type.value} cannot be inserted into "
f"{context_type.value} context "
f"(content first registered as {origin_type.value}, id={origin_id})"
)
ツール出力として入ってきたコンテンツが、静かに命令に化けることをランタイムレベルで阻止する。モデルがプロンプトを見る前に、である。
型の変換が必要な場合(例:ツール出力を証拠として使いたい場合)は、明示的な transform() 呼び出しが必要だ。この操作はバリデーション("error" や "failed" といる失敗マーカーを含む文字列を拒否するなど)を通過し、derived_from による系譜を記録したうえで新しいオブジェクトを生成する。元のオブジェクトを直接書き換えることはない。
この仕組みはソフトウェア工学におけるDesign by Contract(Bertrand Meyerが1980年代にEiffel言語向けに提唱した概念:事前条件・事後条件を明示し、違反を即座に顕在化させる手法)を、コンテキストオブジェクトに適用したものと見ることができる。
LLMは型分類に関与しない
重要な点として、コンテキストの型付けにLLMは一切使わない。テキストを事後解析して「これは命令っぽい」と分類するわけではない。
ツール呼び出しを行ったコードが、その結果を TOOL_OUTPUT として宣言する。 関数の戻り値型を呼び出し元で推測するのではなく、関数定義側で宣言するのと同じ発想だ。
プロンプトへの最終的なシリアライズは専用のアセンブラが担う。格納されたアイテムを固定の順序(命令→メモリ→証拠→ツール出力)で処理し、クリーンにラベル付けされたセクションとしてレンダリングする。
8テスト、LLM呼び出し0回、全通過
著者はこのランタイムを実際に動作させ、8つのテストを実行している(いずれもLLM呼び出しなし)。ソースコードとターミナル出力の全体はGitHubリポジトリで公開されている。
記事は「これが解決しないこと」についても正直に列挙している。これはモデル精度を上げる仕組みではなく、構造的な正しさと可観測性のレイヤーだ。型チェッカーが間違ったロジックを防げないのと同様、このシステムもプロンプトの内容が適切かどうかは判断しない。
詳細はAI Agents Don't Need More Context — They Need Typed Contextを参照していただきたい。