ベクトル埋め込みはAIエージェントメモリのデフォルトとして誤っている
ベクトル埋め込みはAIエージェントメモリのデフォルトとなっているが、実際の運用ではドリフト、非効率性、書き込みの複雑さにより性能が低下することが多い。構造化キーバリューストアとMCPスタイルのメモリサーバーの組み合わせが、ほとんどのエージェントメモリワークロードに優れた基盤を提供し、ベクトルDBは大規模な意味検索にのみ使用すべきである。
過去約2年間、「LLMエージェントにメモリをどう与えるか」という質問に対するデフォルトの答えは、ほとんど常に「ベクトルDBをセットアップして会話チャンクを埋め込む」というものでした。筆者はこのパターンを半ダースの本番エージェントで実装し、毎回期待外れの結果を目の当たりにしてきました。この記事では、エージェントが実際に必要とするメモリタスクに対して、構造化キーバリュー検索とMCPスタイルのメモリサーバーが通常ベクトル埋め込みよりも優れている理由と、筆者がベクトルDBを最初に使うことをやめるに至った具体的な障害モードについて説明します。
この主張は聞こえよりも狭い範囲を対象としています。ベクトル埋め込みは、大規模な非構造化コーパスに対する意味検索という本来の目的では優れた性能を発揮します。しかし、エージェントメモリに誤って適用されることが常態化しています。その理由は、ツールが成熟しており、カンファレンストークで紹介されるエージェントアーキテクチャが「RAGをあらゆるものに」と説くからです。エージェントが実際に最も必要とするのは、より単純で難しいもの、すなわち信頼性が高く一貫した検索が可能な構造化永続状態です。
筆者は複数のデプロイメントで3つの具体的な障害モードに遭遇しました。
障害1:高再現率クエリのドリフト
古典的なベクトルDBメモリパターンは、すべての会話メッセージを埋め込み、現在のターンに最も類似した上位k個を検索し、システムプロンプトに詰め込むというものです。これはおもちゃのデモでは機能しますが、本番環境では「ドリフト幻覚」を引き起こします。エージェントが、現在のターンと緩く関連するだけのベクトルヒットから事実を自信満々に引用するのです。
具体例:カスタマーサポートエージェントが過去のチケットのベクトルメモリを持っていました。ユーザーが「ログインできない」という新しいチケットを開きます。ベクトル検索は「ログイン」を含む3つの過去チケットを返し、その中には1年前の別の機能に関するもの(しかもその機能は名称変更されていた)が含まれていました。エージェントは自信を持って「以前のチケットで問題を解決したワークフローフラグを確認してください」とユーザーに伝えました。そのフラグは9か月前に廃止されていました。ユーザーはもう存在しないUI要素を探すのに20分を費やしました。
ベクトルDBは設計通りの動作をしました。意味的に類似した項目を検索したのです。問題は、「このユーザーの最後のログイン試行は5日前」と「このユーザーの廃止されたワークフローフラグは2024年2月に関連していた」の両方が有効な検索結果であり、エージェントが「現在の関連事実」と「過去の遺物」を区別する方法がないことです。
明示的な新鮮さマーカーと事実のライフサイクル(「この事実は2024-02に真だった、この事実は現在のもの」)を持つ構造化KVストアには、このような障害モードはありません。類似度ではなくキーでクエリを実行します。事実が現在のものでなければ、そこには存在しません。
障害2:小規模で高頻度の状態
ほとんどのエージェントメモリは、ユーザーごとに数百バイトに収まります。ユーザー名、現在の設定、最近の対話の要約、進行中のプロジェクトコンテキストなどです。この規模のデータに対してベクトルDBは完全に間違ったツールです。200バイトの構造化状態をインデックスするために1536次元の浮動小数点数を保存し、キー1つでマイクロ秒で取得できる情報を得るために30倍のストレージと100倍のクエリレイテンシを支払っているのです。
これは明白に聞こえますが、筆者は今でも、最も頻繁にクエリされるメモリ事実(「ユーザーの希望する応答形式は?」)がキーの背後ではなくベクトル検索の背後にある本番エージェントを何度も見つけています。ベクトルDBの擁護者は「それは埋め込みのメタデータとして保存すればよい」と言うでしょう。その通りです。しかし、それは余計な手順が加わり、不要なベクトルインデックスを備えたキーバリューストアにすぎません。
障害3:書き込みが読み取りよりも厄介
エージェントは頻繁にメモリに書き込む必要があります。ユーザーが好みを述べるたび、事実が変わるたび、セッションが終了するたびです。ベクトルDBは書き込みを処理しますが、そのライフサイクル管理は厄介です。簡単な例を考えましょう。ユーザーが「実は、私のメールアドレスは[email protected]です。前に言った[email protected]ではありません」と言ったとします。ベクトルDBは両方を埋め込みとして保存します。将来の検索では両方が返され、エージェントは矛盾する2つの事実を手にし、どちらが最新でどちらが過去のものかをマークする一流の方法を持ちません。これを回避するためにエンジニアリングすることは可能ですが(「superseded_by」フィールドを追加し、検索時にフィルタリングし、独自の競合解決ロジックを書く)、その時点であなたはベクトルインデックスを無理やり取り付けた構造化データベースを再発明していることになります。
構造化KVストアはこれを自然に処理します。user:jane:emailは上書きされ、現在の値は1つだけです。古い値は明示的な履歴であり、検索ノイズではありません。
「エージェントメモリ」の実際の意味
このフレーズは4つの異なるワークロードをカバーしており、それぞれに最適なストレージがあります。
- エピソード記憶:「ユーザーは前回の会話で何と言ったか?」——順序性、最近重視、通常はスライディングコンテキストウィンドウまたは構造化セッションログで処理され、検索は不要。
- 知識ベースの意味検索:「私たちの製品は何をするのか?」——これがベクトルDBの得意分野:ドキュメント、コード、チケットのRAG。大規模コーパス、あいまいクエリ、意味的類似性が正しいプリミティブ。
- 構造化状態:「ユーザーの現在の設定は?」——明示的なキー、現在の値、書き込みによる上書き。KVの領域。
- セッション間の継続性:「過去6週間、ユーザーと私は何について協力してきたか?」——これが本当に難しい部分。エピソード、意味、構造化が混在。事実によって老化速度が異なる。単純な「すべてのターンを埋め込む」パターンはこれに最も弱い。
間違いは、4つのワークロードを1つとして扱い、ベクトルDBに飛びつくことです。実際には、メモリタイプごとに異なるストアを使い、エージェントのツール層がルーティングを行うべきです。
MCPサーバーパターン
モデルコンテキストプロトコル(MCP)は、メモリを特定のストアに結合することなくエージェントに公開するクリーンな方法を提供しました。MCPメモリサーバーは、エージェントが呼び出す軽量なツール層(memory.recall、memory.set、memory.list_recent)であり、その背後ではデータに適したストレージ(KVは構造化、ベクトルは意味、追加専用ログはエピソード)が使用されます。
このパターンには「埋め込みと検索」に対して2つの実用的な利点があります。
- エージェントが明示的な操作手段を持つ。エージェントは「ユーザーの保存された設定を調べる必要がある」と判断し、memory.recall("preferences")を呼び出します。決定はエージェントの推論内で行われ、埋め込み類似性のブラックボックス内ではありません。
- エージェントを変更せずにストレージバックエンドを混在できる。MCP層が実装を隠蔽します。筆者は、構造化ユーザー状態がPostgresテーブル、エピソードセッション要約がRedisソートセット、意味的ドキュメント検索がQdrantインデックスというプロジェクトを、すべて1つのMCPメモリサーバーの背後で出荷しました。エージェントは気にしませんでした。
Memnode(完全な開示:筆者のプロジェクト)はそのようなサーバーの1つで、特に構造化とエピソード側に焦点を当てています。他にもあります。重要なのはMemnode自体ではなく、MCPサーバーパターンがワークロードごとに適切なストレージを選択できるようにし、すべてをベクトルDBに強制しないことです。
ベクトルDBが正しいツールである場合
明確にしておきます:筆者はベクトルDBを使うなと言っているのではありません。エージェントメモリのデフォルトにするなと言っているのです。ベクトル検索が真に最適なプリミティブであるユースケース:
- ドキュメントRAG:数千のドキュメント、あいまいな意味クエリ、「Xに関するポリシーは?」
- 大規模リポジトリのコード検索:シンボル名検索が不十分で、実装パターンによる類似性が必要な場合。
- 会話コーパスに対する長期意味記憶:「6か月間にわたって類似概念に触れた会話を検索する」場合、ただしエージェントの推論がその検索を明確に正当化する場合に限り、デフォルトの最初の検索としてではありません。
パターンは:コーパスが大規模で、クエリがあいまいで、キーが事前にわからないために正確なキー検索が機能しない場合にベクトルDBを使用する。エージェントのメモリが「ユーザーごとの小さな構造化状態」または「会話の最後の50メッセージ」であれば、それはベクトルDBの用途ではありません。
具体的に:どれが必要かを知る方法
筆者が有用だと思う診断質問:エージェントが自分自身に問いかける最も一般的なメモリ関連の質問を5つ書き出してください。
それらが次のような場合:
- 「ユーザーの名前は?」
- 「最後のアクションは?」
- 「どのドキュメントを見ている?」
- 「最後のエラーメッセージは?」
- 「現在のタスク状態は?」
→ 構造化KVメモリ。ベクトルDBは使わない。
それらが次のような場合:
- 「この質問に関連するポリシーテキストを見つける」
- 「このスニペットに類似したコードパターンを検索する」
- 「この問題に似たチケットを取得する」
→ ベクトル検索。ベクトルDBを使用する。
それらが次のような場合:
- 「3回前のセッションで移行計画について何を議論したか?」
- 「先週のコンテキストを要約する」
→ ハイブリッド:エピソードログとオンデマンド要約、場合によっては要約(生のターンではない)に対する小さなベクトルインデックス。クエリタイプごとにバックエンドを調整できるよう、MCPサーバーパターンを採用する。
現在の筆者のアプローチ
新しいエージェントプロジェクトでは、デフォルトのスタックは次のとおりです。
- 構造化ユーザー状態はPostgresまたはSQLiteに保存し、明示的なset/recall/deleteセマンティクスを持つMCPメモリサーバー経由で公開。
- エピソードセッションメモリは追加専用ログとして、同じMCPサーバー内に保存し、最近性ウィンドウ検索を備える。
- ベクトルRAGは、大規模な外部コーパスがありクエリが真に意味的な場合のみ使用。
結果として、コストが低くなり(ベクトルDBの運用不要)、障害モードが予測可能になり(エージェントが3か月前のワークフローフラグを引用しなくなる)、デバッグが容易になりました(構造化メモリは読めるが、埋め込みベクトルは読めない)。
今日エージェントプロジェクトを始めるなら、ベクトルDBファーストの反射をスキップし、構造化KVとMCPメモリ層から始めることをお勧めします。ベクトル検索は、特定のワークロードが正当化する場合にのみ追加し、デフォルトのアーキテクチャとしては使用しないでください。デフォルトはしばらく前から間違っていました。カンファレンストークはまだ追いついていません。
構造化メモリの具体的な実例を見たい場合は、Claude Codeメモリデモがインストール、記録、リコール、系譜のループを4ステップで示しています。MCPセマンティクスなしの単純なキー値検索(キャッシュ、フィーチャーフラグ、セッションキー)だけで十分なら、basekvのような汎用KVで十分です。