Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

エージェント型メモリシステム

動機:エージェントにメモリが必要な理由

大規模言語モデルは本質的に、状態を持たない関数近似器です。プロンプト \(x\) に対して、継続 \(p_\theta(y \mid x)\) を生成する分布を出力します。すべての推論呼び出しは最初から始まります。コンテキストウィンドウ、つまりモデルが注意を向けられる有限のトークン列だけが、生成時に利用できる情報です。短く自己完結したタスクにはこれで十分ですが、長期のエージェント型タスクでは根本的なボトルネックになります。

Important

コンテキストウィンドウのボトルネック

\(L\) は最大コンテキスト長を表します(例:\(L = 128{,}000\) トークン、GPT-4oの場合)。1トークンはおよそ4文字をエンコードし、一般的な書籍には\(\sim!500{,}000\) 語、\(\approx 670{,}000\) トークンが含まれます。コストを無視しても、数日間自律動作するエージェントは、どの固定ウィンドウにも収まらない観察、ツール出力、推論トレースを蓄積します。メモリシステムは、この物理的制約に対する工学的な応答です。

エージェントが永続メモリを持たないと、3つの異なる失敗モードが生じます。

  1. コンテキストの壊滅的忘却。 イベントがコンテキストウィンドウからスクロールして消えると、回復不能に失われます。エージェントは10,000トークン前に下した決定を参照できません。

  2. 経験から学べない。 エピソードの保存がなければ、すべてのエピソードがエージェントにとって初めてのものになります。成功した戦略は再利用できず、ミスが繰り返されます。

  3. パーソナライズの欠如。 ユーザーの好み、ドメインの事実、関係の履歴を各セッションで再確立する必要があり、ユーザー体験と効率が低下します。

Tip

認知アーキテクチャとしてのメモリ

認知科学では、生物エージェントの複数の記憶システムを区別します (Tulving 1985; Squire 1992)。ワーキングメモリ(情報の能動的操作)、エピソード記憶(自伝的な出来事)、意味記憶(世界知識)、手続き記憶(スキルと習慣)です。効果的なエージェント型AIシステムは、神経科学をシミュレーションしているからではなく、これらの分類が本当に異なるアクセスパターン更新頻度検索メカニズムを反映しているため、類似した区別から恩恵を受けます。

形式的には、エージェントをタプル \(\mathcal{A} = (\pi_\theta, \mathcal{M}, \mathcal{R}, \mathcal{W})\) としてモデル化します。ここで \(\pi_\theta\) は方策(LLM)、\(\mathcal{M}\) はメモリストア、\(\mathcal{R}: \mathcal{Q} \times \mathcal{M} \to \mathcal{D}\) はクエリを検索文書へ写像する検索関数、\(\mathcal{W}: \mathcal{M} \times \mathcal{E} \to \mathcal{M}\) は新しい経験 \(\mathcal{E}\) を使ってメモリを更新する書き込み関数です。各ステップ \(t\) でエージェントは \(o_t\) を観察し、関連コンテキスト \(c_t = \mathcal{R}(o_t, \mathcal{M})\) を検索して行動します。

\[ a_t \sim \pi_\theta!\left(\cdot ;\middle|; [s_t;, c_t;, h_t]\right), \]

ここで \(s_t\) は現在のシステムプロンプト、\(c_t\) は検索されたメモリ、\(h_t\) はコンテキスト内の直近の履歴です。行動後、エージェントは新しい情報を書き込めます。\(\mathcal{M} \leftarrow \mathcal{W}(\mathcal{M},, (o_t, a_t, r_t))\).

メモリの種類の分類

ワーキングメモリ(短期)

ワーキングメモリはエージェントのアクティブな作業領域、つまり現在操作している情報です。LLMエージェントでは次に対応します。

  • スクラッチパッド。 最終回答を生成する前に、中間推論ステップを専用バッファーへ書き込むもの(例:chain-of-thought (Wei et al. 2022)、scratchpad (Nye et al. 2021))。

  • 連鎖思考バッファー。 推論トークン \(z_1, z_2, \ldots, z_k\) は回答トークン \(a\) より前に生成され、\(p(a \mid x) = \sum_z p(a \mid x, z),p(z \mid x)\) としてモデル化されます。

  • 会話コンテキスト。 コンテキストウィンドウに保持される直近のターン履歴 \([(u_1, a_1), \ldots, (u_t, a_t)]\)。

ワーキングメモリは高速(検索レイテンシーがゼロで、すでにコンテキスト内にある)、揮発性(コンテキストがクリアされると失われる)、容量制限付き(\(L\)) によって上限が決まる)です。

エピソード記憶(経験ベース)

エピソード記憶は、コンテキストと時刻でインデックス化された特定の過去イベントを保存します。エージェントでは次のものが含まれます。

  • 過去のインタラクション。 過去の会話、タスクの試行、その結果の完全または要約された記録。

  • 成功した軌跡。 類似する将来のタスクに対する少数ショット例として検索できる、高報酬の行動系列。

  • 失敗事例。 根本原因の注釈を付けた記録済みのミス。エージェントがエラーの再発を避けられる。

  • 検索拡張エピソード想起。 新しいタスク \(q\) が与えられたら、最も類似した過去の\(k\) エピソード \(\{e_i\}_{i=1}^k\) を検索し、コンテキストへ含める。

エピソード記憶は通常、エピソード要約に対する埋め込みを持つベクトルストア(節3.3.1)として実装されます。

意味記憶(世界知識)

意味記憶は、特定のエピソードから切り離された一般的な事実と概念をエンコードします。

  • 事実知識。 エンティティ、属性、関係(例:「パリはフランスの首都である」)。

  • ドメイン概念。 エージェントのタスク領域に関連する定義、分類体系、オントロジー。

  • 知識グラフ。 構造化表現 \(\mathcal{G} = (\mathcal{V}, \mathcal{E})\) であり、ノード \(v \in \mathcal{V}\) はエンティティ、エッジ \(e \in \mathcal{E}\) は型付き関係です。

エピソード記憶と異なり、意味記憶はコンテキスト非依存です。水が \(100^\circ\)Cで沸騰するという事実は、いつどこで学習されたかにかかわらず真です。

手続き記憶(スキル)

手続き記憶は、物事のやり方、つまり自動化されたスキルと行動パターンをエンコードします。

  • 学習されたツール利用パターン。 どのタスクでどのAPIを呼び出すか、入力をどう形式化するか、エラーをどう処理するか。

  • 行動系列。 複数ステップの手順(例:「コードを配備するには、テスト実行 \(\to\) イメージのビルド \(\to\) プッシュ \(\to\) マニフェスト更新」)。

  • メモリとしての方策。 モデルの重み \(\theta\) 自体が手続き知識をエンコードします。成功した軌跡でのファインチューニングは、手続き記憶の固定化の一形態です。

Note

メモリ種別の分類

ソフトウェア開発を支援するエージェントは、次を使います。

  • ワーキング: 現在編集しているファイル、たった今受け取ったエラーメッセージ。

  • エピソード: 「先週、モジュールXで似た NullPointerException を修正するため、42行目にnullチェックを追加した。」

  • 意味: 「Pythonの asyncio.gather はコルーチンを並行実行する。return_exceptions=True でない限り、例外は伝播する。」

  • 手続き: 標準的なデバッグワークフロー:再現 \(\to\) 切り分け \(\to\) 仮説化 \(\to\) テスト \(\to\) 修正。

メモリアーキテクチャ

RAGベースのメモリ

検索拡張生成(RAG) (P. Lewis et al. 2020)は、LLMエージェントにおける外部メモリの主要なパラダイムです。メモリストア \(\mathcal{M}\) は文書 \(\{d_i\}_{i=1}^N\) の集合であり、検索はクエリ \(q\) を順位付き部分集合へ写像します。

埋め込みストアとベクトルデータベース

各文書 \(d_i\) は埋め込みモデル \(\phi\): \(\mathbf{v}_i = \phi(d_i) \in \mathbb{R}^{D}\) でエンコードされます。クエリも同様にエンコードされます:\(\mathbf{q} = \phi(q)\)。検索は上位 \(k\) 件の文書を類似度で返します。

\[ \text{Retrieve}(q, \mathcal{M}, k) = \underset{S \subseteq [N],, |S|=k}{\arg\max} \sum_{i \in S} \text{sim}(\mathbf{q}, \mathbf{v}_i), \]

ここで \(\text{sim}(\cdot,\cdot)\) は通常コサイン類似度です。近似最近傍(ANN)インデックス (FAISS (Johnson et al. 2021), HNSW (Malkov and Yashunin 2020), ScaNN (Guo et al. 2020)) は、\(N \sim 10^7\) に対してこれを実用的に処理できます。

検索戦略

  • 密ベクトル検索。 クエリと文書をどちらもニューラルエンコーダー(例:DPR (Karpukhin et al. 2020)、text-embedding-3-large)でエンコードします。意味的類似性を捉えますが、GPU推論が必要です。

  • スパース検索。 トークンの重複に対するBM25またはTF-IDF。高速で解釈しやすく、正確なキーワード一致に強い方式です。

  • ハイブリッド検索。 Reciprocal Rank Fusion(RRF)によって密なスコアとスパーススコアを組み合わせます。

\[ \text{RRF}(d, k) = \sum_{r \in \text{rankers}} \frac{1}{k + \text{rank}_r(d)}, \]

ここで \(k=60\) は平滑化定数です。ハイブリッド検索は、どちらか一方だけを使う場合を一貫して上回ります (Chen et al. 2022)。

再ランキング

クロスエンコーダーの再ランキング器 \(f_\psi(q, d) \in [0,1]\) は、各検索文書をクエリと同時にスコアリングし、\(O(k)\) 回のフォワードパスというコストで高い精度を提供します。パイプラインは、ANNで \(k' \gg k\) 件の候補を検索し、クロスエンコーダーで再ランキングして、上位 \(k\) 件を返すという流れです。

Warning

検索によるハルシネーションのリスク

RAGはハルシネーションをなくしません。むしろ引き起こすことがあります。検索された文書が古い、誤っている、または表面的にしか関連しない場合、モデルは誤情報を自信を持って取り込むことがあります。必ず出所メタデータ(ソース、タイムスタンプ、信頼度)を含め、忠実性の検証ステップを検討してください。

要約ベースのメモリ

逐語的な保存が高コストまたはノイジーすぎる場合、要約によって保存前に情報を圧縮します。

逐次要約

各ステップ \(t\) で、エージェントは継続的な要約 \(S_t\) を維持します。新しい情報 \(e_t\) が到着すると、次のようにします。

\[ S_{t+1} = \text{LLM}!\left(\texttt{“Summarize: [}S_t\texttt{] + [}e_t\texttt{]”}\right). \]

これによりメモリサイズは \(O(1)\) に保たれますが、詳細を失うリスクがあります。

階層的圧縮

メモリを \(L_0 \supset L_1 \supset \cdots \supset L_K\) のレベルに整理します。\(L_0\) は逐語的で、各 \(L_{i+1}\) は \(L_i\) の要約です。検索はまず \(L_K\) (最も圧縮され高速)を確認し、必要に応じて掘り下げます。これはForteの逐次要約技法に対応します (Forte 2022).

要約するか逐語的に保存するか

  • 逐語的に保存:正確な事実、コードスニペット、数値結果、ユーザーの引用。

  • 要約する:物語的コンテキスト、推論の連鎖、重複した観察。

  • 破棄する:ノイズ、情報内容のない失敗したツール呼び出し。

グラフベースのメモリ

知識グラフ

知識グラフ \(\mathcal{G} = (\mathcal{V}, \mathcal{E}, \mathcal{R})\) は事実をトリプル \((h, r, t)\) として保存します。\(h, t \in \mathcal{V}\) はエンティティで、\(r \in \mathcal{R}\) は関係です。エージェントはSPARQL (Harris and Seaborne 2013), Cypher (Francis et al. 2018)、または自然言語からグラフへの変換によって検索できます。

エンティティ・関係抽出

新しい観察は抽出モデル \(\text{IE}: \text{text} \to \{(h_i, r_i, t_i)\}\) によって解析され、\(\mathcal{G}\) へ統合されます。共参照解析とエンティティリンキングによって一貫性を確保します。

GraphRAG

GraphRAG (Edge et al. 2024)はグラフ走査によってRAGを拡張します。クエリが与えられるとシードエンティティを検索し、\(k\)ホップ近傍の走査によって、埋め込み類似度では直接一致しない関連事実を見つけます。これはマルチホップ推論で特に強力です。

\[ \text{GraphRetrieve}(q, \mathcal{G}, k) = \bigcup_{v \in \text{seeds}(q)} \mathcal{N}_k(v, \mathcal{G}), \]

ここで \(\mathcal{N}_k(v, \mathcal{G})\) は \(k\)ホップ近傍である、\(v\) です。

時間的知識グラフ

事実には有効期間があります:\((h, r, t, [t_\text{start}, t_\text{end}])\)。時間的KG (Lacroix et al. 2020)によって、「2023年にOpenAIのCEOだったのは誰か?」というクエリを過去と現在の状態を混同せずに実行できます。

キー・バリューメモリネットワーク

微分可能メモリネットワーク (Weston et al. 2015; Sukhbaatar et al. 2015)は、メモリをキー・バリューペア \(\{(\mathbf{k}_i, \mathbf{v}_i)\}_{i=1}^M\) の集合として表し、ソフトな注意に基づいて検索します。

\[ \alpha_i = \text{softmax}!\left(\frac{\mathbf{q}^\top \mathbf{k}_i}{\sqrt{D}}\right), \qquad \mathbf{c} = \sum_{i=1}^M \alpha_i \mathbf{v}_i. \]

検索されたコンテキスト \(\mathbf{c}\) はクエリの微分可能な関数であり、エンドツーエンド学習を可能にします。現代のトランスフォーマー注意機構はこのメカニズムの特殊例です。エージェント用途では、メモリスロットを勾配降下または明示的な書き込み操作で更新できます。

MemGPTと仮想コンテキスト管理

MemGPT (Packer et al. 2023)は、オペレーティングシステムの仮想メモリに似た仮想コンテキスト抽象化を導入します。メモリは階層に整理されます。

ページイン/ページアウト戦略

エージェントは、次に基づいて、どのメモリをホットコンテキストへ昇格させ(ページイン)、どれを追い出すか(ページアウト)を決めます。

  • 新しさ: 最近アクセスされた項目ほど必要になる可能性が高い。

  • 関連性: 現在のクエリとの類似度が高い項目。

  • 重要性: 書き込み時に高重要度とタグ付けされた項目。

自己指向型メモリ管理

MemGPTでは、LLM自身がアクション空間の一部としてメモリ管理関数呼び出し(memory_searchmemory_insertmemory_delete)を発行します。これにより、メモリ管理はハードコードされた方策ではなく学習された行動になります。RL学習の自然な対象です(節3.7)。

メモリ操作

書き込み:メモリへのコミット

すべての観察を保存すべきとは限りません。書き込みの判断はフィルタリング問題です。

\[ \text{Write}(e) = \mathbf{1}!\left[\text{importance}(e) > \tau\right], \]

ここで \(\tau\) はしきい値であり、\(\text{importance}(e)\) は次のように定義できます。

  • 驚き: \(-\log p_\theta(e \mid \text{context})\) — 予期しないイベントほど情報量が多い。

  • 報酬信号: 高い \(\vert r_t\vert\)(正でも負でも)に関連するイベントは記憶する価値がある。

  • LLMによる自己評価: モデルに重要度を1〜10の尺度で評価させる。

矛盾検出

新しい事実 \(f_\text{new}\) を書き込む前に、既存のメモリとの競合を確認します。

\[ \text{Conflict}(f_\text{new}, \mathcal{M}) = \exists, f \in \mathcal{M} : \text{Contradicts}(f_\text{new}, f). \]

矛盾検出はNLIモデルを使うか、LLMにプロンプトを与えることで実装できます。競合がある場合、エージェントは上書きするか、タイムスタンプ付きで両方を保持するか、人間のレビュー用にフラグを立てるかを決めなければなりません。

メモリの形式と粒度

何を保存するかだけでなく、どのように保存するかも重要です。メモリエントリは原子的な事実から詳細なトランスクリプトまで幅広く、トレードオフも異なります。

形式長所短所
原子的な事実
「ユーザーはPythonを好む。」正確な検索、合成可能、重複排除と矛盾検出が容易コンテキストを失う、抽出エラー、微妙な情報には脆弱
構造化ノート
(A-MEM (W. Xu et al. 2025))豊富なメタデータ(タグ、リンク)、グラフ走査をサポート、精度とコンテキストのバランス書き込みコストが高い、スキーマ設計が必要
要約エピソード
(MemGPT (Packer et al. 2023))物語の一貫性を保持、コンパクト、マルチターン推論に適する要約で情報が失われる、部分更新が難しい
逐語トランスクリプト情報を失わず、抽出エラーがなく、正確な引用をサポートストレージが大きい、検索がノイジー、高価なスキャン

メモリの粒度によるトレードオフ

実際には、本番システムは複数の粒度を組み合わせることが多い (Chhikara et al. 2025)。正確な想起のために原子的な事実を抽出し、物語的コンテキストのために要約エピソードを維持し、監査可能性のために逐語トランスクリプトをコールドストレージへアーカイブします。Generative Agentsアーキテクチャ (Park et al. 2023)は、観察を自然言語の説明、重要度スコア、タイムスタンプを持つ原子的な「メモリオブジェクト」として保存し、正確な検索と時間的推論の両方を可能にします。

設計ガイドライン

  • 粒度をクエリ種別に合わせる。 ユーザーが事実確認の質問(「私のAPIキーは?」)をするなら原子的な事実が有利です。コンテキストに関する質問(「なぜRedisを使うと決めたのか?」)ならエピソード要約が必要です。

  • 負担できる範囲で最も細かい粒度で保存し、その上に粗いビューを構築する。 原子的な事実を要約するのは簡単ですが、情報を失った要約から原子を復元することはできません。

  • 出所を含める。 すべてのメモリエントリは、そのソース(会話ターン、文書、ツール出力)へリンクすべきです。そうすればエージェントが検証でき、ユーザーが監査できます。

読み取り/検索

クエリの定式化

検索クエリ \(q\) は生の観察である必要はありません。よりよい戦略は次のとおりです。

  • HyDE(仮想文書埋め込み) (Luyu Gao et al. 2023): 仮想的な回答を生成して埋め込み、クエリとしてその埋め込みを使う。

  • クエリ拡張: クエリの複数の言い換えを生成し、検索結果の和集合を取る。

  • ステップバック・プロンプティング: 検索前に具体的なクエリをより一般的な質問へ抽象化する。

時間減衰と新しさバイアス

古いメモリは関連性が低い可能性があります。時間を重み付けしたスコアは次のとおりです。

\[ \text{score}(d, q, t) = \lambda \cdot \text{sim}(\mathbf{q}, \mathbf{v}_d) + (1-\lambda) \cdot \exp!\left(-\frac{t - t_d}{\tau_\text{decay}}\right), \]

ここで \(t_d\) はメモリの作成時刻、\(\tau_\text{decay}\) は減衰率を制御します。Generative Agents論文 (Park et al. 2023) は、同様に新しさで重み付けした検索を使います。

更新:矛盾解消と統合

メモリの統合では、冗長性を減らしてより高レベルのパターンを表面化するため、関連するメモリをマージします。

\[ \mathcal{M}' = \text{Consolidate}(\mathcal{M}) = \text{Cluster}(\mathcal{M}) \cup \text{Summarize}(\text{Cluster}(\mathcal{M})). \]

忘却メカニズム

生物の記憶は忘却するため、人工メモリも忘却すべきです。戦略は次のとおりです。

  • LRU退避: 容量を超えたとき、最も長く使われていないエントリを削除する。

  • 重要度重み付き忘却: \(p(\text{forget},\vert ,d) \propto \exp(-\text{importance}(d))\).

  • 間隔反復: 繰り返しアクセスされた記憶をより長く保持する。エビングハウスの指数的忘却曲線に従う (Ebbinghaus 1885)。

反映:メタ認知的操作

Reflection (Park et al. 2023; Shinn et al. 2023)は、より高次のメモリ操作です。エージェントは自分のメモリを読み、洞察を生成します。

\[ \text{Reflect}(\mathcal{M}) \to \{i_1, i_2, \ldots\} \subset \mathcal{M}_\text{semantic}, \]

ここで各洞察 \(i_j\) は複数のエピソード記憶から導かれる、より高レベルの抽象化です。

Note

実践におけるReflection(Reflexion)

3回試してコーディング問題の解決に失敗した後、エージェントは次のように反省します。

  1. エピソード記憶から3つの失敗エピソードを検索する。

  2. 洞察を生成する:「入力リストが空の場合のエッジケースを扱うのを、何度も忘れている。」

  3. この洞察を意味記憶に保存する。

  4. 次の試行で洞察を検索し、空の入力を明示的に確認する。

これはReflexion (Shinn et al. 2023)の中核メカニズム、つまり自己反省による言語的強化学習です。

Reflectionはどこに存在するか

Reflectionはエピソード記憶から読み取り、意味記憶へ書き込みます。得られる洞察はコンテキスト非依存の一般化(「常に空の入力を確認する」)であり、エピソード固有の記録ではないため、意味記憶 \(\mathcal{M}_\text{semantic}\) に属します。ただしReflection処理中は、中間推論(検索したエピソード+統合プロンプト+生成した洞察)がワーキングメモリ(コンテキストウィンドウ)を占有します。要約すると次のとおりです。

  • 入力: エピソード記憶(特定の過去イベント)

  • 計算: ワーキングメモリ(コンテキスト内での能動的推論)

  • 出力: 意味記憶(永続的で一般化された洞察)

これは生物の記憶の固定化に対応します。睡眠とReflectionの間に、エピソード経験が徐々に意味知識へ変換される現象です。

マルチターン対話のためのメモリ

ユーザーモデリングと嗜好追跡

永続的なユーザーモデル \(\mathcal{U}\) は次を保存します。

  • 明示的な嗜好: 明示された好き嫌い、コミュニケーションスタイルの好み。

  • 暗黙的な嗜好: 行動から推測されるもの(例:ユーザーは常にPythonのコードを求め、簡潔な回答を好む)。

  • 専門知識レベル: 語彙と質問の複雑さから推測されるドメイン知識。

  • 目標とコンテキスト: 継続中のプロジェクト、現在のタスク、組織上の役割。

ユーザーモデルは各インタラクション後に更新されます。

\[ \mathcal{U}_{t+1} = \text{Update}(\mathcal{U}_t,, (u_t, a_t, \text{feedback}_t)). \]

セッションの継続性

メモリがなければ、各会話は白紙から始まります。セッションメモリがあれば、次のようになります。

  1. セッション開始時にユーザーモデル \(\mathcal{U}\) と直近のセッション要約を検索する。

  2. パーソナライズされたシステムプロンプトを注入する:「あなたは分散学習プロジェクトに取り組むシニアMLエンジニアのAliceを支援しています。前回のセッションでは、勾配同期の問題のデバッグを手伝いました。」

3 を更新する。 セッション終了時にセッションを要約し、\(\mathcal{U}\).

メモリによるパーソナライズ

パーソナライズにより、効率(明確化の質問が減る)と品質(ユーザーの専門知識に合わせて調整された応答)の両方が向上します。主な技法は次のとおりです。

  • 適応的な冗長性: ユーザーの過去の関与度に基づいて回答の長さを調整する。

  • ドメイン・プライミング: 意味記憶から関連するドメインコンテキストを先頭に付加する。

  • 能動的想起: 求められなくても関連する過去のインタラクションを提示する(「先月このトピックについて質問しました。そのとき分かったことはこちらです」)。

Warning

プライバシーとメモリ

永続的なユーザーメモリは重大なプライバシー上の懸念を生みます。エージェントは、(1) 個人情報を保存する前に明示的な同意を得る、(2) 保存されたメモリを検査・削除する仕組みを提供する、(3) マルチユーザー配備でアクセス制御を実施する、(4) データ保持規制(GDPR、CCPA)を遵守する必要があります。メモリシステムはプライバシーをデフォルトにして設計すべきです。

マルチエージェントシステムのためのメモリ

複数のエージェントが共有タスクで協調するとき、メモリは個人的な知識ストアにとどまらず、調整メカニズムになります。タスクを分解する計画エージェントは実行エージェントへサブゴールを伝え、批評エージェントは評価対象エージェントと同じコンテキストへアクセスし、研究エージェントのチームは作業の重複を避ける必要があります。共有メモリがなければ、エージェントはすべてをダイレクトメッセージで伝えなければならず、帯域幅のボトルネックが生じ、会話がコンテキスト外へスクロールすると情報が失われます。共有メモリは、すべてのエージェントが読み書きできる永続的で検索可能な基盤を提供してこれを解決し、暗黙の調整(「他のエージェントが覚えているとよいのだが」)を明示的な状態(「回答はブラックボードにある」)へ変えます。

共有メモリプール

マルチエージェントシステムでは、エージェントは共通のメモリストア \(\mathcal{M}_\text{shared}\) をプライベートストア \(\mathcal{M}_i\) とともに共有できます。

\[ \text{context}_i(t) = \mathcal{R}(\mathcal{M}_i, q_i) \cup \mathcal{R}(\mathcal{M}_\text{shared}, q_i). \]

共有メモリは暗黙の調整を可能にします。エージェント \(A\) が発見を書き込み、エージェント \(B\) が明示的な通信なしにそれを検索できます。

ブラックボード・アーキテクチャ

ブラックボードパターン (Hayes-Roth 1985)は、古典的なマルチエージェント調整メカニズムです。

各エージェントはブラックボードから読み書きします。コントローラーはブラックボードを監視し、前提条件が満たされたときにエージェントを起動します。これによりエージェントは疎結合になり、ダイレクトメッセージではなく共有状態を介して通信します。

共有知識における合意と競合

複数のエージェントが共有メモリへ書き込むと、競合が生じます。解決戦略は次のとおりです。

  • 最終書き込み優先: 単純ですが情報を失う。

  • バージョン付きメモリ: すべての書き込みの履歴を保持し、エージェントが任意のバージョンを検索できるようにする。

  • 投票/合意: \(k\)-of-\(n\) エージェントが合意してから事実をコミットする。

  • 信頼度重み付きマージ: \(f_\text{merged} = \sum_i w_i f_i\) であり、\(w_i\) はエージェント \(i\) の信頼度。

  • 指定権限者: 特定のエージェントへメモリ領域の所有権を割り当てる。

Note

未解決問題:分散メモリの一貫性

マルチエージェントシステムは、並行書き込み、ネットワーク分断、敵対的エージェントのもとで、どのようにメモリの一貫性を維持すべきでしょうか?古典的な分散システムの解決策(Paxos、Raft)は適用できますが、高価です。限られた陳腐化を伴う近似的一貫性で、多くのエージェント型タスクには十分かもしれません。しかし、適切なトレードオフは未解決の研究課題です。

強化学習によるメモリシステムの学習

メモリ操作の報酬信号

メモリ操作(読み取り、書き込み、更新、Reflection)はRLフレームワークの行動として扱えます。課題は、有用なメモリ動作を促す報酬信号を設計することです。

  • タスク報酬の伝播: メモリ検索が正しい回答につながったら、検索行動へクレジットを与える。スパースですが曖昧さがない。

  • 検索精度報酬: \(r_\text{retrieve} = \text{Relevance}(d_\text{retrieved}, \text{task})\)。学習された関連度モデルで推定する。

  • メモリ効率報酬: 不要な書き込みにペナルティを与える:\(r_\text{write} = -\lambda \cdot \mathbf{1}[\text{write}]\)。選択的な保存を促す。

  • 一貫性報酬: 内部的に一貫した(矛盾のない)メモリ状態に報酬を与える。

メモリ操作 \(m_t\) の、ステップ \(t\) における合成報酬は次のとおりです。

\[ r_t^{\text{mem}} = r_t^{\text{task}} + \alpha \cdot r_t^{\text{retrieve}} + \beta \cdot r_t^{\text{write}} + \gamma \cdot r_t^{\text{consistency}}. \]

何を記憶するかの学習

何を記憶するかの問題はメタ学習の課題です。エージェントは、\(\pi_\text{write}(e)\) は将来のタスク性能を最大化する書き込み方策を学習する必要があります。これは次の理由で困難です。

  1. メモリの価値が将来になって初めて明らかになる(遅延報酬)。

  2. 書き込み時点では、将来のクエリの可能性の空間が未知である。

3 に他に何があるかに依存する。 メモリは相互作用する。\(e\) を保存する価値は、\(\mathcal{M}\).

アプローチは次のとおりです。

  • Hindsight relabeling (Andrychowicz et al. 2017):成功したエピソードの後で、検索されたメモリに遡及的に「重要」とラベルを付け、書き込み方策を学習して類似項目を保存する。

  • Meta-RL (Duan et al. 2016):タスク分布全体で書き込み方策を学習し、タスク間で一般化する情報を保存するよう方策を学習する。

  • 好奇心駆動ストレージ (Pathak et al. 2017):予測誤差が高く驚くべき観察を保存する。そのような観察は情報量が多い可能性が高いため。

メモリ拡張方策最適化

方策とメモリシステムを共同で最適化する考えは、微分可能メモリネットワーク (Graves et al. 2016)に端を発し、REALM (Guu et al. 2020)によって検索拡張LLMへ拡張されました。メモリ拡張エージェントの完全な方策勾配目的関数は次のとおりです。

\[ \mathcal{L}(\theta, \phi) = \mathbb{E}_{\tau \sim \pi_\theta}!\left[\sum_{t=0}^T \gamma^t r_t\right] - \lambda \cdot \mathcal{L}_\text{mem}(\phi), \]

ここで \(\theta\) はLLMパラメーター、\(\phi\) はメモリシステムのパラメーター(例:検索モデルの重み)、\(\mathcal{L}_\text{mem}\) はメモリの複雑さに対する正則化項です。

Important

重要な洞察:学習される帰納バイアスとしてのメモリ

RLでメモリ操作を学習すると、エージェントはタスク固有のメモリ戦略を発展させられます。コーディングエージェントはAPIシグネチャを保存し、研究エージェントは引用の連鎖を保存し、カスタマーサービスエージェントはユーザーの苦情パターンを保存することを学習します。メモリシステムはエージェントのドメインに合わせた学習された帰納バイアスになります。

メモリアプローチの比較

メモリシステムの評価

エージェント型メモリの評価は、メモリ操作の品質が間接的に、つまり長期的な下流タスク性能を通じてしか明らかにならないため困難です。保存した事実を完全に再現できるメモリシステムでも、無関係なコンテキストを検索したりLLMのコンテキストウィンドウを圧倒したりすれば失敗します。

評価の側面

LongMemEval (Wu et al. 2025)は、長期メモリシステムが示すべき5つの中核能力を特定します。

  1. 情報抽出。 会話ターンから顕著な事実を識別して保存できるか?事実再現率、つまり正解の事実のうちどれだけをメモリから復元できるかで測定する。

  2. マルチセッション推論。 複数の過去セッションに散在する情報を統合できるか?例:「先週と昨日の会話に基づくと、プロジェクトのスコープは何が変わったか?」

  3. 時間的推論。 時間依存のクエリに正しく答えられるか?例:「組織再編に、自分の優先事項は何だと言っていたか?」には、時間的状態の区別が必要。

  4. 知識の更新。 事実が変わったとき(ユーザーが都市を移る、嗜好が変わる)、履歴を保持しつつメモリは最新状態を反映するか?

  5. 棄却。 関連メモリがないとき、もっともらしいが捏造された想起をハルシネーションするのではなく、「分かりません」と正しく言えるか?

ベンチマーク

ベンチマーク開催地規模焦点
LongMemEval (Wu et al. 2025)ICLR 2025500問、スケーラブルな履歴5つのメモリ能力、マルチセッションチャット
LOCOMO (Maharana et al. 2024)EMNLP 2024マルチセッション対話会話に対するシングルホップ、時間的、マルチホップ、オープンドメインQA
InfiniteBench (X. Zhang et al. 2024)ACL 202410万以上のトークンコンテキスト長文脈の再現、メモリ固有ではないが限界をテスト

エージェント型メモリシステムを評価するためのベンチマーク

指標

メモリレベルの指標

  • メモリ再現率: \(\frac{\text{# ground-truth facts retrievable from memory}}{\text{# total ground-truth facts}}\)。保存の完全性を測定する。

  • メモリ適合率: \(\frac{\text{# relevant items in top-}k\text{ retrieval}}{k}\)。検索のノイズを測定する。

  • レイテンシー: クエリから検索コンテキストまでの時間(p50とp95)。

  • トークン効率: クエリごとにコンテキストへ注入される総トークン数。少ないほどよい。不要なコンテキストはLLMの精度を低下させ、コストを増加させる。

下流指標

  • 回答精度: メモリを条件とした最終応答の正しさ(EM、F1、またはLLM評価)。

  • 忠実性: 応答は捏造なしにメモリの内容を正確に反映しているか?

  • パーソナライズ品質: ユーザー満足度。メモリ拡張システムとメモリなしシステムの選好評価またはA/Bテストで測定する。

  • 矛盾率: 以前に述べた事実と一致しない応答をシステムが生成する頻度。

運用指標

  • 書き込み選択性: メモリ書き込みを起動するターンの割合。高すぎると\(\to\) noise、低すぎると\(\to\)ギャップが生じる。

  • 陳腐化: 更新が存在するにもかかわらず、古い事実が検索される頻度。

  • ストレージ増加率: 1インタラクション時間あたりに保存されるトークン数。無制限の増加は持続不可能。

Warning

評価ギャップ

ほとんどのメモリ論文は短いベンチマーク(10〜50セッション)で評価します。実際の本番エージェントは数千セッションを何か月も実行します。メモリのドリフト、矛盾の蓄積、ストレージ肥大化が主要な失敗モードとなる長期評価は、依然として未解決の課題です。実務者はベンチマークスコアを、運用指標の長期的な監視で補完すべきです。

実装パターン

埋め込み付きベクトルストアメモリ

最も一般的なメモリパターンでは、エントリをメタデータ(タイムスタンプ、重要度スコア、タグ)とともに埋め込みベクトルとして保存します。検索ではコサイン類似度と時間減衰を組み合わせ、最近の重要なメモリを先に表面化させます。重複検出とLRU退避によってストアのサイズを制限します。

import numpy as np
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
import json

@dataclass
class MemoryEntry:
    """A single memory entry with metadata."""
    content: str
    embedding: np.ndarray
    timestamp: datetime = field(default_factory=datetime.now)
    importance: float = 0.5
    access_count: int = 0
    last_accessed: Optional[datetime] = None
    tags: list[str] = field(default_factory=list)
    source: str = "agent"

class VectorMemoryStore:
    """
    Hybrid dense+sparse memory store with temporal decay.
    Supports importance-weighted retrieval and LRU eviction.
    """

    def __init__(
        self,
        embed_fn,           # callable: str -> np.ndarray
        max_entries: int = 10_000,
        decay_rate: float = 0.01,   # per hour
        recency_weight: float = 0.3,
    ):
        self.embed_fn = embed_fn
        self.max_entries = max_entries
        self.decay_rate = decay_rate
        self.recency_weight = recency_weight
        self.entries: list[MemoryEntry] = []

    # -- Write --------------------------------------------------------------

    def write(
        self,
        content: str,
        importance: float = 0.5,
        tags: list[str] | None = None,
        check_duplicates: bool = True,
    ) -> MemoryEntry:
        """Commit a new memory, evicting if at capacity."""
        if check_duplicates and self._is_duplicate(content):
            return None  # Skip near-duplicate entries

        embedding = self.embed_fn(content)
        entry = MemoryEntry(
            content=content,
            embedding=embedding,
            importance=importance,
            tags=tags or [],
        )

        if len(self.entries) >= self.max_entries:
            self._evict()

        self.entries.append(entry)
        return entry

    def _is_duplicate(self, content: str, threshold: float = 0.95) -> bool:
        """Check if a near-duplicate already exists."""
        if not self.entries:
            return False
        emb = self.embed_fn(content)
        sims = self._cosine_similarities(emb)
        return float(np.max(sims)) > threshold

    def _evict(self):
        """Remove the least important + least recent entry."""
        now = datetime.now()
        scores = []
        for e in self.entries:
            age_hours = (now - e.timestamp).total_seconds() / 3600
            recency = np.exp(-self.decay_rate * age_hours)
            score = e.importance * (1 - self.recency_weight) \
                  + recency * self.recency_weight
            scores.append(score)
        worst_idx = int(np.argmin(scores))
        self.entries.pop(worst_idx)

    # -- Retrieve -----------------------------------------------------------

    def retrieve(
        self,
        query: str,
        k: int = 5,
        recency_boost: bool = True,
    ) -> list[MemoryEntry]:
        """
        Hybrid retrieval: dense similarity + temporal recency.
        Returns top-k entries sorted by combined score.
        """
        if not self.entries:
            return []

        q_emb = self.embed_fn(query)
        dense_scores = self._cosine_similarities(q_emb)

        now = datetime.now()
        combined = []
        for i, (entry, d_score) in enumerate(
            zip(self.entries, dense_scores)
        ):
            if recency_boost:
                age_h = (now - entry.timestamp).total_seconds() / 3600
                recency = np.exp(-self.decay_rate * age_h)
                score = (1 - self.recency_weight) * d_score \
                      + self.recency_weight * recency
            else:
                score = d_score
            combined.append((score, i))

        combined.sort(reverse=True)
        top_k = [self.entries[i] for _, i in combined[:k]]

        # Update access metadata
        for entry in top_k:
            entry.access_count += 1
            entry.last_accessed = now

        return top_k

    def _cosine_similarities(self, query_emb: np.ndarray) -> np.ndarray:
        """Vectorized cosine similarity against all stored embeddings."""
        matrix = np.stack([e.embedding for e in self.entries])
        norms = np.linalg.norm(matrix, axis=1, keepdims=True)
        matrix_norm = matrix / (norms + 1e-8)
        q_norm = query_emb / (np.linalg.norm(query_emb) + 1e-8)
        return matrix_norm @ q_norm

    # -- Reflect ------------------------------------------------------------

    def reflect(self, llm_fn, k: int = 10) -> list[str]:
        """
        Meta-cognitive reflection: retrieve recent memories,
        synthesize higher-level insights, and store them back.
        """
        if len(self.entries) < 3:
            return []

        # Retrieve recent high-importance memories
        recent = sorted(
            self.entries, key=lambda e: e.timestamp, reverse=True
        )[:k]
        context = "\n".join(f"- {e.content}" for e in recent)

        # Ask LLM to generate insights
        prompt = (
            "Given these recent memories, extract 2-3 high-level "
            "insights or patterns:\n" + context
        )
        raw_insights = llm_fn(prompt)

        # Store each insight as a high-importance memory
        insights = []
        for line in raw_insights.strip().split("\n"):
            line = line.strip().lstrip("-*").strip()
            if len(line) > 20:
                self.write(
                    f"[INSIGHT] {line}",
                    importance=0.9,
                    check_duplicates=True,
                )
                insights.append(line)
        return insights

    def get_stats(self) -> dict:
        """Return memory statistics for monitoring."""
        return {
            "total_entries": len(self.entries),
            "avg_importance": float(
                np.mean([e.importance for e in self.entries])
            ) if self.entries else 0.0,
            "oldest_entry": min(
                (e.timestamp for e in self.entries), default=None
            ),
        }

階層型メモリマネージャー

MemGPT (Packer et al. 2023)に着想を得たこのパターンでは、メモリを3つの層に整理します。ホット(コンテキスト内で即時アクセス)、ウォーム(ベクトルストアで高速検索)、コールド(アーカイブで容量無制限)です。エントリは、アクセス頻度と重要度に基づいて自動的に昇格または降格され、CPUキャッシュ階層に似た動作をします。

from enum import Enum
from collections import OrderedDict

class MemoryTier(Enum):
    HOT  = "hot"    # In-context: immediate access
    WARM = "warm"   # Vector store: fast retrieval
    COLD = "cold"   # Archival: slow but unlimited

class HierarchicalMemoryManager:
    """
    Three-tier memory manager inspired by MemGPT.
    Hot tier is an LRU cache; warm is a vector store;
    cold is append-only archival storage.
    """

    def __init__(
        self,
        vector_store: VectorMemoryStore,
        hot_capacity: int = 20,     # max entries in hot tier
        warm_capacity: int = 5_000,
        llm_summarize_fn=None,      # callable for summarization
    ):
        self.vector_store = vector_store
        self.hot_capacity = hot_capacity
        self.warm_capacity = warm_capacity
        self.summarize = llm_summarize_fn

        # Hot tier: ordered dict for LRU semantics
        self.hot: OrderedDict[str, MemoryEntry] = OrderedDict()
        # Cold tier: append-only list (would be a DB in production)
        self.cold: list[MemoryEntry] = []

    # -- Page-in: promote warm -> hot ---------------------------------------

    def page_in(self, query: str, k: int = 3) -> list[MemoryEntry]:
        """
        Retrieve from warm store and promote to hot tier.
        Evicts least-recently-used hot entries if needed.
        """
        candidates = self.vector_store.retrieve(query, k=k)
        promoted = []
        for entry in candidates:
            key = entry.content[:64]  # use prefix as key
            if key not in self.hot:
                if len(self.hot) >= self.hot_capacity:
                    self._evict_hot()
                self.hot[key] = entry
                self.hot.move_to_end(key)
            promoted.append(entry)
        return promoted

    def _evict_hot(self):
        """Evict LRU entry from hot tier back to warm."""
        # OrderedDict: first item is LRU
        key, entry = self.hot.popitem(last=False)
        # Re-insert into warm store (already there, just update access)
        # In a real system, we'd update the warm store's metadata

    # -- Write with tier assignment ------------------------------------------

    def write(
        self,
        content: str,
        importance: float = 0.5,
        tier: MemoryTier = MemoryTier.WARM,
    ) -> MemoryEntry:
        """Write to the appropriate tier."""
        if tier == MemoryTier.HOT:
            entry = MemoryEntry(
                content=content,
                embedding=self.vector_store.embed_fn(content),
                importance=importance,
            )
            key = content[:64]
            if len(self.hot) >= self.hot_capacity:
                self._evict_hot()
            self.hot[key] = entry
            return entry

        elif tier == MemoryTier.WARM:
            return self.vector_store.write(content, importance=importance)

        else:  # COLD
            entry = MemoryEntry(
                content=content,
                embedding=np.array([]),  # no embedding for cold
                importance=importance,
            )
            self.cold.append(entry)
            return entry

    # -- Summarize and compress ---------------------------------------------

    def compress_hot_to_warm(self) -> Optional[str]:
        """
        Summarize hot tier contents and write summary to warm.
        Called when hot tier is full and new important content arrives.
        """
        if not self.hot or not self.summarize:
            return None

        hot_contents = "\n".join(
            f"- {e.content}" for e in self.hot.values()
        )
        summary = self.summarize(
            f"Summarize these memory entries concisely:\n{hot_contents}"
        )
        self.vector_store.write(summary, importance=0.7)
        return summary

    # -- Unified retrieval --------------------------------------------------

    def retrieve(self, query: str, k: int = 5) -> list[MemoryEntry]:
        """
        Retrieve from all tiers, prioritizing hot.
        Returns up to k entries sorted by relevance.
        """
        results = []

        # 1. Check hot tier (exact + semantic)
        q_emb = self.vector_store.embed_fn(query)
        for entry in self.hot.values():
            if entry.embedding.size > 0:
                sim = float(
                    np.dot(q_emb, entry.embedding)
                    / (np.linalg.norm(q_emb) * np.linalg.norm(entry.embedding) + 1e-8)
                )
                if sim > 0.7:
                    results.append((sim + 1.0, entry))  # +1 hot bonus

        # 2. Retrieve from warm store
        warm_results = self.vector_store.retrieve(query, k=k)
        for entry in warm_results:
            results.append((0.5, entry))

        # 3. Deduplicate and sort
        seen = set()
        final = []
        for score, entry in sorted(results, reverse=True):
            key = entry.content[:64]
            if key not in seen:
                seen.add(key)
                final.append(entry)
            if len(final) >= k:
                break

        return final

    def get_hot_context(self) -> str:
        """Return hot tier as a formatted context string."""
        if not self.hot:
            return ""
        lines = ["[Memory Context]"]
        for entry in list(self.hot.values())[-10:]:  # last 10
            lines.append(f"  * {entry.content}")
        return "\n".join(lines)

メモリ拡張エージェントループ

MemGPT (Packer et al. 2023)で導入され、CoALAフレームワーク (Sumers et al. 2024)で形式化されたこのパターンは、読み取り・行動・反映・書き込みサイクルによってメモリシステムをエージェントの推論ループへ組み込みます。応答前にエージェントは関連メモリを検索し、応答後に何を保存するかを決めます。LLM出力の特殊トークンがメモリ操作を起動し、モデル自身が永続性を制御できるようにします。

import re
from typing import Any

class MemoryAugmentedAgent:
    """
    An LLM agent with a full read-act-reflect-write memory cycle.
    Implements the MemGPT-style self-directed memory management.
    """

    SYSTEM_PROMPT = """You are a memory-augmented AI assistant.
You have access to persistent memory across conversations.
At each turn you may issue memory commands:
  [MEMORY_SEARCH: <query>]  - retrieve relevant memories
  [MEMORY_WRITE: <content>] - store important information
  [MEMORY_REFLECT]          - synthesize insights from memory

Always think step by step. Use memory to avoid repeating mistakes
and to personalize your responses."""

    def __init__(
        self,
        llm_fn,                         # callable: messages -> str
        memory_manager: HierarchicalMemoryManager,
        importance_threshold: float = 0.6,
        max_memory_tokens: int = 1500,
    ):
        self.llm = llm_fn
        self.memory = memory_manager
        self.importance_threshold = importance_threshold
        self.max_memory_tokens = max_memory_tokens
        self.conversation_history: list[dict] = []

    # -- Main agent step ----------------------------------------------------

    def step(self, user_message: str) -> str:
        """
        Full agent step:
        1. Retrieve relevant memories
        2. Construct augmented prompt
        3. Generate response (possibly with memory commands)
        4. Execute memory commands
        5. Reflect and consolidate
        6. Return response to user
        """

        # Step 1: Retrieve relevant memories
        memories = self.memory.retrieve(user_message, k=5)
        memory_context = self._format_memories(memories)

        # Step 2: Construct augmented prompt
        messages = self._build_messages(user_message, memory_context)

        # Step 3: Generate response
        raw_response = self.llm(messages)

        # Step 4: Execute any memory commands in the response
        clean_response, memory_ops = self._parse_memory_commands(
            raw_response
        )
        self._execute_memory_ops(memory_ops, user_message, clean_response)

        # Step 5: Auto-write important information
        self._auto_write(user_message, clean_response)

        # Step 6: Update conversation history
        self.conversation_history.append(
            {"role": "user", "content": user_message}
        )
        self.conversation_history.append(
            {"role": "assistant", "content": clean_response}
        )

        return clean_response

    # -- Memory retrieval and formatting -----------------------------------

    def _format_memories(self, memories: list[MemoryEntry]) -> str:
        if not memories:
            return ""
        lines = ["Relevant memories:"]
        for i, m in enumerate(memories, 1):
            age = (datetime.now() - m.timestamp).days
            lines.append(
                f"  [{i}] (importance={m.importance:.1f}, "
                f"{age}d ago) {m.content}"
            )
        return "\n".join(lines)

    def _build_messages(
        self, user_message: str, memory_context: str
    ) -> list[dict]:
        system = self.SYSTEM_PROMPT
        if memory_context:
            system += f"\n\n{memory_context}"
        system += f"\n\n{self.memory.get_hot_context()}"

        messages = [{"role": "system", "content": system}]
        # Include recent conversation history (last 6 turns)
        messages.extend(self.conversation_history[-6:])
        messages.append({"role": "user", "content": user_message})
        return messages

    # -- Memory command parsing ---------------------------------------------

    def _parse_memory_commands(
        self, response: str
    ) -> tuple[str, list[dict]]:
        """Extract and remove memory commands from response."""
        ops = []
        patterns = {
            "search":  r"\[MEMORY_SEARCH:\s*(.+?)\]",
            "write":   r"\[MEMORY_WRITE:\s*(.+?)\]",
            "reflect": r"\[MEMORY_REFLECT\]",
        }
        clean = response
        for op_type, pattern in patterns.items():
            for match in re.finditer(pattern, response, re.DOTALL):
                content = match.group(1) if op_type != "reflect" else None
                ops.append({"type": op_type, "content": content})
                clean = clean.replace(match.group(0), "").strip()
        return clean, ops

    def _execute_memory_ops(
        self,
        ops: list[dict],
        user_msg: str,
        response: str,
    ):
        """Execute memory commands issued by the LLM."""
        for op in ops:
            if op["type"] == "search":
                results = self.memory.retrieve(op["content"], k=3)
                # Page results into hot tier for immediate use
                self.memory.page_in(op["content"], k=3)

            elif op["type"] == "write":
                self.memory.write(
                    op["content"],
                    importance=0.8,  # explicitly written = important
                    tier=MemoryTier.WARM,
                )

            elif op["type"] == "reflect":
                self._reflect()

    # -- Auto-write heuristic -----------------------------------------------

    def _auto_write(self, user_msg: str, response: str):
        """
        Automatically store important information without explicit command.
        Uses a simple heuristic: write if response contains facts,
        decisions, or user preferences.
        """
        importance_keywords = [
            "remember", "important", "note that", "you prefer",
            "your name is", "decided to", "the answer is",
            "key insight", "learned that",
        ]
        combined = (user_msg + " " + response).lower()
        if any(kw in combined for kw in importance_keywords):
            summary = f"User: {user_msg[:100]} | Agent: {response[:200]}"
            self.memory.write(
                summary,
                importance=self.importance_threshold,
                tier=MemoryTier.WARM,
            )

    # -- Reflection --------------------------------------------------------

    def _reflect(self):
        """
        Meta-cognitive reflection: synthesize insights from recent memory.
        Stores high-level insights back into semantic memory.
        """
        recent = self.memory.retrieve("recent important events", k=10)
        if len(recent) < 3:
            return  # Not enough to reflect on

        recent_text = "\n".join(f"- {m.content}" for m in recent)
        insight_prompt = [
            {"role": "system", "content": "You extract high-level insights."},
            {"role": "user", "content":
                f"Based on these memories, what are 2-3 key insights?\n"
                f"{recent_text}\nRespond with bullet points only."},
        ]
        insights = self.llm(insight_prompt)
        # Store each insight as a high-importance semantic memory
        for line in insights.split("\n"):
            line = line.strip().lstrip("*-").strip()
            if len(line) > 20:
                self.memory.write(
                    f"[INSIGHT] {line}",
                    importance=0.9,
                    tier=MemoryTier.WARM,
                )

Tip

読み取り・行動・反映・書き込みサイクル

メモリ拡張エージェントループは、4段階の認知サイクルを実装します。

  1. 読み取り: 行動する前に関連メモリを検索し、応答に情報を与える。

  2. 行動: 検索したコンテキストを条件として応答を生成する。

  3. 反映: 蓄積したメモリから、より高レベルの洞察を定期的に統合する。

  4. 書き込み: 重要な新情報を選択的に永続ストレージへコミットする。

このサイクルは、軍事戦略の観察・情勢判断・意思決定・行動(OODA)ループと、認知心理学のエンコード・保存・検索モデルに対応します。重要な洞察は、メモリが受動的なストアではなく、認知の能動的な参加者だということです。

エージェント型メモリの最近の進展

ここまでで説明したメモリシステムは基礎的なパターンを確立しました。いくつかの最近の研究は、さらに限界を押し広げています。

CoALA:言語エージェントの認知アーキテクチャ

Sumers et al. (Sumers et al. 2024)は、認知科学と記号AIの原則を使って増え続けるLLMエージェントを整理する統一フレームワーク、Cognitive Architectures for Language Agents(CoALA)を提案しています。CoALAは言語エージェントを次のように分解します。

  • モジュール型メモリ: ワーキングメモリ(コンテキストウィンドウ)、エピソード記憶(過去の経験)、意味記憶(世界知識)、手続き記憶(行動スキーマ)。節3.2の分類に対応します。

  • 構造化された行動空間: 内部行動(推論、検索、メモリ書き込み)と外部行動(ツール利用、環境との相互作用)。

  • 意思決定サイクル: 明示的な検索と書き込みのステップを持つ、一般化された検知・計画・行動ループ。

CoALAの貢献は新しいシステムというより設計言語です。既存エージェントを体系的に分析し、欠けている能力を特定する方法を提供するため、実務者にとって有用な参照アーキテクチャになります。

Mem0:本番規模のメモリレイヤー

Mem0 (Chhikara et al. 2025)は、研究用メモリシステムと本番配備の間にある隔たりに対処します。主な考え方は次のとおりです。

  • 自動抽出: LLMが明示的にメモリ書き込みコマンドを発行することに頼らず、Mem0が会話ターンから顕著な事実を自動抽出し、永続ストアへ統合する。

  • グラフベースのメモリ: 平坦なベクトルストアに加えて、Mem0は抽出したエンティティと事実の関係グラフを維持し、マルチホップのメモリクエリ(「プロジェクトYの文脈で、ユーザーはトピックXについて何と言ったか?」)を可能にする。

  • メモリ圧縮: 冗長な、または置き換えられた事実を自動的にマージし、メモリストアをコンパクトかつ最新に保つ。

LOCOMOベンチマークで、Mem0は26%の相対改善を達成し、91% p95レイテンシーを低減し、\(>\)90%フルコンテキスト方式と比べてトークンコストを削減します。

Sleep-Time Compute:オフラインメモリ処理

Lin et al. (K. Lin et al. 2025)は、クエリ時だけでなく、ユーザーインタラクションのにエージェントがメモリを処理・統合するパラダイム、sleep-time computeを導入します。脳が記憶を統合し有用な関連を事前計算する生物学的な睡眠がたとえです。

仕組み

アイドル期間(「睡眠」)中、エージェントは次を行います。

  1. 現在のコンテキストから、将来ありそうなクエリを予測する。

  2. 推論の連鎖、要約、構造化表現を事前計算する。

  3. これらの事前計算した成果物を保存し、テスト時推論で検索して再利用できるようにする。

結果

sleep-time computeは、同等の精度を達成するためにテスト時に必要な計算量を、推論ベンチマークで\(\sim 5\times\)削減します。同じコンテキストに対する複数の関連クエリで償却すると、クエリあたりの平均コストは\(2.5\times\)低下します。このアプローチは、ユーザークエリが予測可能、つまりコンテキストが尋ねられる質問を強く制約するときに最も効果的です。

Tip

オフラインRLとしてのメモリ統合

sleep-time computeはオフライン方策改善とみなせます。アイドル時間中、エージェントは新しい環境インタラクションなしに、すでに収集したデータ(過去のインタラクション)を使ってメモリ表現(方策)を改善します。これは、エージェントが軌跡の静的データセットから学習するオフラインRL手法(章5)とつながります。

A-MEM:Zettelkastenに着想を得たエージェント型メモリ

A-MEM (W. Xu et al. 2025)は、密に相互接続された原子的ノートに基づくノート作成システムであるZettelkasten手法から着想を得て、LLMエージェント向けの動的で自己組織化するメモリを可能にします。

主要な設計原則

  • 構造化ノート。 各メモリエントリは生のテキストチャンクではなく、コンテキスト説明、キーワード、タグ、関連ノートへの明示的なリンクを持つノートです。このメタデータによって、埋め込み類似度だけの場合より豊かな検索が可能になります。

  • 動的リンク。 新しいメモリが追加されると、システムは既存メモリを分析して意味のある接続を特定し、双方向リンクを確立します。その結果は平坦なリストではなく知識ネットワークになります。

  • メモリの進化。 重要なのは、新しいノートの追加が既存ノートの更新を引き起こす可能性があることです。エージェントの理解が深まるにつれて、コンテキスト表現と属性を改良します。これによりメモリは静的アーカイブではなく、時間とともに改善する生きた構造になります。

  • エージェント駆動の整理。 固定スキーマのメモリシステムと異なり、A-MEMではLLM自身がメモリの整理、リンク、更新方法を決められます。整理構造をタスク領域に適応させられます。

結果

マルチセッション推論タスクにおける6つの基盤モデル全体で、A-MEMは平坦なベクトルストア、要約ベースのメモリ、グラフデータベース方式を一貫して上回ります。これは、メモリで何を保存するかと同じくらい、どのように整理するかが重要であることを示します。

まとめ

エージェント型メモリシステムは、能力の高いAIエージェントの基盤コンポーネントであり、有限のコンテキストウィンドウという根本的な制約に対処します。本章では次を概観しました。

  • 4分類: 認知科学に対応し、異なる工学的要件を反映するワーキング、エピソード、意味、手続きの各メモリ。

  • 5つのアーキテクチャファミリー: RAGベース、要約ベース、グラフベース、キー・バリューネットワーク、階層型仮想コンテキスト(MemGPT)。

  • 4つの基本操作: 書き込み(重要度スコアと矛盾検出付き)、読み取り/検索(時間減衰とクエリ拡張付き)、更新(競合解消と統合付き)、反映(メタ認知的な洞察生成)。

  • マルチターン/マルチエージェント拡張: ユーザーモデリング、セッション継続性、共有メモリプール、ブラックボード・アーキテクチャ。

  • メモリシステムのRL学習: メモリ操作の報酬信号、何を記憶するかの学習、メモリ拡張方策最適化。

この分野は急速に発展しています。主な未解決課題には、(1) メモリのグラウンディング—検索したメモリが無視されたり、その上でハルシネーションしたりせず忠実に取り込まれること、(2) スケーラブルな一貫性—大規模なマルチエージェントシステムで共有メモリの整合性を維持すること、(3) プライバシー保護メモリ—ユーザーデータを損なわずにパーソナライズを可能にすること、が含まれます。コンテキストウィンドウが拡大しても、コンテキスト内メモリと外部メモリの境界は変化しますが、選択的で構造化され、検索可能な情報保存の根本的な必要性は残ります。