powered by TechFeed
表示モード
ハウツー

AIエージェントの「会話が終わったら全部忘れる」問題をグラフDBで解決 — モデル指定の1行変更でセッション横断の永続メモリを追加するNPMパッケージが話題に

9月7日、Neo4jが「Your AI Agent Forgets Everything. So We Taught It to Remember — With a Graph.」と題した記事を公開した。この記事では、Vercel AI SDKと連携してAIエージェントにセッション横断の永続メモリをグラフDBで実装するNPMパッケージ @neo4j-labs/nams-ai-provider について詳しく紹介されている。

9月7日、Neo4jが「Your AI Agent Forgets Everything. So We Taught It to Remember — With a Graph.」と題した記事を公開した。この記事では、Vercel AI SDKと連携してAIエージェントにセッション横断の永続メモリをグラフDBで実装するNPMパッケージ @neo4j-labs/nams-ai-provider について詳しく紹介されている。


「会話が終わったら全部忘れる」問題

プロダクション環境のチャットエージェントには、共通の欠陥がある。ユーザーが20分かけて自分のスタック、好み、デプロイ方針の決定経緯を説明したとする。セッションが終わった瞬間、その情報はすべて消える。翌日、エージェントは初対面のように振る舞う。

これはモデルの問題ではなく、アーキテクチャの問題だ。学習したことを保存する永続的な場所がなく、次のプロンプトに何を戻すべきかを判断する仕組みもない。

「書き留める」のは簡単だ。難しいのは、ターン400において、9,000件保存された文章の中からこのプロンプトに必要な3件を選ぶことである。

この2つの問題を、既存コードへの最小限の変更で解決するのが @neo4j-labs/nams-ai-provider だ。


モデル指定の1行変更で永続メモリを追加する

このパッケージは Vercel AI SDK 向けのコミュニティプロバイダーで、ホスト型サービス Neo4j Agent Memory Service(NAMS) をバックエンドに使う。Neo4jクラスターの構築不要、ベクターストアの選定不要、エンベディングパイプラインの管理も不要だ。無料のAPIキーだけ用意すればいい。

統合の手順は、createNamsProvider でプロバイダーを初期化するセットアップブロックを追加したうえで、既存の openai('...') によるモデル指定を nams.languageModel('...') に切り替えるというものだ。実質的な変更点はモデル指定の1行のみで、それ以外のエージェントコードには手を加えない。

変更前:

import { openai } from '@ai-sdk/openai';
import { ToolLoopAgent, stepCountIs } from 'ai';

const agent = new ToolLoopAgent({
  model:        openai('gpt-4o-mini'),
  instructions: 'You are a helpful assistant.',
  stopWhen:     stepCountIs(10),
});

変更後(モデル指定の1行を置き換え):

import { createNamsProvider } from '@neo4j-labs/nams-ai-provider';
import { openai } from '@ai-sdk/openai';
import { ToolLoopAgent, stepCountIs } from 'ai';

const nams = createNamsProvider({
  apiKey:       process.env.MEMORY_API_KEY!,
  baseProvider: openai,
  scope:        { userId: 'user-123' },
});

const agent = new ToolLoopAgent({
  model:        nams.languageModel('gpt-4o-mini'),  // ← ここだけ変更
  instructions: 'You are a helpful assistant.',
  stopWhen:     stepCountIs(10),
});

これだけで、毎回の呼び出しの前に user-123 に関連するメモリを取得し、プロンプトに注入し、やり取りを将来のセッション向けに保存するようになる。新しいフレームワークも、オーケストレーション層も、スキーマ設計も不要だ。


なぜ「文字列のリスト」ではなくグラフなのか

多くのメモリ層は文章を保存してベクター検索する。それはそれで機能するが、「知っていることについての質問」に答えようとすると限界が出る。

記事では日記(ベクターDB)とアドレス帳(グラフ)の比喩で説明している。どちらも「誰かが何を言ったか」は答えられる。しかし「その人が誰か」を答えられるのはグラフだけだ。

具体的には、エンティティベースのメモリにすることで以下が実現できる:

  • 「このユーザーについて記憶していることを全部列挙して」 → クエリ1発。埋め込みチャットログのスキャンではない。
  • 「それは間違い。その箇所だけ直して」 → バンガロールは1ノード。更新は1回。以後の取得はすべて修正後の値を参照する。
  • 「いつそれを学習した?」getEntityHistory() がエンティティの会話横断の言及履歴を返す。
  • 「この2人は同一人物だ」mergeEntities() が重複を統合し、SAME_AS の履歴リンクを残す。

「グラフがかっこいい」からではなく、プロダクションで実際に来る質問がグラフクエリだからNeo4jを選んだ、というのがこの設計の核心だ。「なぜそう思っているのか」とユーザーに問われたとき、信頼度スコアと履歴を持つノードがあれば答えられる。不透明なベクターブロブでは答えられない。


他のメモリプロバイダーにはない保証

ここが最も重要なポイントだ。記事によれば、他のメモリプロバイダーが実装していない保証機構を搭載しているという。

ツールベースのメモリ統合には構造的なトレードオフがある:

  • モデルをラップする方式 → メモリは「保証される」(コードで必ず実行される)が「不可視」(モデルが能動的に追加取得できない)
  • ツールとして公開する方式 → メモリは「可視」でモデル主導だが、実行は保証されない

ツールの説明文やシステムプロンプトはあくまで「アドバイス」だ。モデルはしばしばメモリ参照をスキップして直接回答する。ログ上は正常な成功ターンに見える。ユーザーからはメモリを忘れたエージェントに見える。

このパッケージはこの問題をVercel AI SDKのフックで解決する:

  • enforceQueryMemory()prepareStep フック): query_memory がまだ呼ばれていない間、毎ステップを toolChoice: 'required' に固定する。モデルはファイル読み取りやMCPツール呼び出しなど何でもできるが、メモリ参照前にテキスト回答だけで終わることはできない
  • **enforceStoreMemory()**(onFinish フック): レスポンス後の保存も同様に保証する。

3つの統合モード

パッケージは単一クライアント・単一APIキー・単一メモリストアの上に、3つのモードを提供する。用途に応じて使い分けられるが、いずれも同じNAMSバックエンドを共有する。

Providerモード(最小変更)
nams.languageModel('...')openai('...') のドロップイン代替になる。前述のコード例がこのモードに該当する。設定ファイル上のモデル文字列が 'openai:gpt-4o-mini' から 'nams:gpt-4o-mini' に変わるだけでメモリが有効になるため、既存コードへの影響が最も小さい。

Middlewareモード
モデルインスタンスが別の場所で設定されている場合、nams.wrap() でラップするだけでメモリを追加できる。Providerモードと同様にメモリの取得・注入・保存はすべて自動だが、モデルインスタンスを直接差し替えられない場面でも対応できる点が異なる。

Toolsモード(透明性重視)
query_memorystore_memory の2つのツールとしてメモリを公開する。モデルが能動的に呼び出し、UIストリームにも表示されるため、ユーザーにも開発者にも何が起きているかが見える。store_memory では保存時にメモリタイプ(fact/interaction/pattern/user_preference)、信頼度スコア(0〜1)、タグの指定が必要な構造化スキーマを採用している。書き込み時の構造化が後の取得精度を決める設計だ。前述の enforceQueryMemory() / enforceStoreMemory() フックはこのモードで特に有効で、モデル主導の柔軟性と実行保証を両立させる。

さらに toolsWithMcp() でMCPサーバーのツールとメモリツールをマージし、1つのエージェントで両方を扱うことも可能だ。


取得精度を支える実装上の工夫

記事で紹介されている設計の細部もいくつか注目に値する。

2パス検索: "where do I live?" と検索して "User is from Delhi" を取得しようとすると、フレーズの直接一致では共通語がなくヒットしない。そこで直接検索が空振りした場合、クエリの重要語を元の大文字小文字とTitle Caseの両方でリトライし、元の質問との単語重複度でランク付けして結果を返す。

メモリ数の上限: 取得結果は関連度でランク付けされ、デフォルト6件、最大12件に制限される。これを超えると回答品質が「改善」ではなく「悪化」するという知見が背景にある。

推論トレースの保存: NAMSは推論の痕跡をファーストクラスのメモリとして保存する。「アプローチBを選んだ」という出力だけでなく、「レート制限の問題でそのアプローチを除外した」という推論過程自体をエージェントが後で参照できる。

プロンプト汚染の防止: ユーザーの元のテキストはメモリ注入にWeakMapでキャプチャされ、保存されるのは常にクリーンなメッセージとなる。この処理がなければ、メモリが自身の注入を記憶し続けて徐々に汚染される。


詳細はYour AI Agent Forgets Everything. So We Taught It to Remember — With a Graph.を参照していただきたい。