体系 › エージェント
·無料·論文·14分で読めます

エージェントの記憶設計 — 短期・長期・エピソード

LLMは何も覚えていません。会話が続いて見えるのは毎ターン全履歴を読み直させているからです。短期・エピソード・長期の3層に分ける設計、想起を決める式、そして最も設計されていない「忘れる」まで、前提知識ゼロから組み立てます。

要点

  • LLMエージェントは訓練後に凍結された重みしか持たず、会話が続くのは毎ターン履歴を丸ごと読み直させているからである。
  • 記憶は短期記憶・エピソード記憶・意味記憶の3つに分けて扱う。書き換わる頻度と正しさの寿命が桁で違うからである。
  • 毎ターン全履歴を送り直すと、ターン数が2倍になると支払うトークン量は約4倍になる。
  • 想起は関連度・新しさ・重要度の3項の足し算で並べ、どれか1つだけでは並べない。
  • 本番で壊れるのは忘れないことであり、上書き・減衰・TTL・矛盾検出の4つを最初から入れておく。
対象textタスクagent

MemGPT: Towards LLMs as Operating Systems


毎朝、記憶を失って出勤してくる同僚

夜勤明けの引き継ぎを想像してください。交代要員はきわめて優秀で、機械の扱いも手順書も完璧に頭に入っています。ただ一点だけ問題があって、前の晩に何が起きたかをまったく覚えていない。彼が知っているのは、机の上の引き継ぎノートに書いてあることだけです。

LLMエージェントはこれと同じ状況にあります。モデルの重みには膨大な一般知識が焼き込まれていますが、それは訓練が終わった時点で凍結されていて、昨日あなたと話した内容は入っていません。会話が続いているように見えるのは、毎ターン、それまでのやりとりを丸ごともう一度読ませているからです。覚えているのではなく、毎回読み直している。

だとすると「記憶を設計する」という仕事の正体は、モデルを賢くすることではなく、あのノートに何を書き、何を書かず、いつ書き換え、いつ捨てるかを決めることになります。この記事はその3つの判断——書く・選ぶ・忘れる——を順に組み立てます。エージェントのループそのもの(考える→道具を使う→結果を見て考え直す)はLLMエージェントを1から解説にあるので、ここでは「そのループが何を持ち歩くか」だけを扱います。

記憶を3つに分ける

人間の記憶学で使われる分類を借りると、設計の見通しが一気によくなります。エージェントの記憶は、性質の違う3つに分けて別々に扱うのが基本形です。

この3つを混ぜると必ず破綻します。理由は書き込みの頻度と、正しさの寿命が桁で違うからです。短期記憶は毎ターン書き換わりますが、意味記憶は月に一度更新されるかどうか。逆に意味記憶へ間違いが1つ混ざると、それはすべての会話に効き続けます。同じ場所に置いて同じ規則で扱う理由がありません。

「4つ目」として手続き記憶——〈このユーザーのデプロイは必ずステージングを経由する〉のような、やり方についての知識——を足すこともあります。これも書き換わる速度が違うので、システムプロンプトや専用の指示ファイルに置いて別管理にするのが普通です。

なぜ「全部載せる」ではいけないのか

素朴な実装は「毎ターン、全履歴をそのまま送る」です。文脈長が十分に長ければこれで済みそうに見えます。しかし2つの壁にぶつかります。

1つ目はコストの壁です。毎ターン全履歴を送り直すという操作は、見た目より高くつきます。1ターンあたりの追加分(あなたの発言+エージェントの返答)を mm トークン、システムプロンプトなど固定部分を bb トークンとすると、TT ターン目までに送った入力トークンの累計は次のようになります。

C(T)=∑t=1T(b+t m)=bT+m T(T+1)2C(T) = \sum_{t=1}^{T}\bigl(b + t\,m\bigr) = bT + m\,\frac{T(T+1)}{2}
(1)

読み方はこうです。ターン数が2倍になると、支払うトークン量は約4倍になる。tt ターン目に送る量が tt に比例して増え、それを TT 回足し合わせるので、合計は TT の2乗で伸びます。会話が2倍長くなっただけで請求が4倍になるのは直感に反しますが、式(1)右辺の T(T+1)/2T(T+1)/2 がそう言っています。

FIG 1対数目盛りに切り替えて O(n) と O(n²) の開きを見てください。全履歴を毎ターン送る実装は右側の曲線に乗ります。50ターンの会話が払うトークンは、20ターンの会話の2.5倍ではなく、およそ6倍です

2つ目は注意の壁です。文脈に入れさえすればモデルが使ってくれる、という前提は成立しません。Liu らの "Lost in the Middle"(arXiv:2307.03172)は、必要な情報を長い文脈の先頭・末尾に置いたときと中央に置いたときで正答率が変わることを測り、中央の情報が使われにくいというU字型の傾向を報告しています。履歴を全部詰め込んだ結果として大事な事実が中央に埋もれれば、入っているのに参照されない事故が起きます。記憶設計の側から見た結論は一行です。文脈は貯蔵庫ではなく、作業机である。

要約という圧縮 — 何を捨てるかの設計

短期記憶が机からあふれたら、どこかで圧縮します。定番はローリング要約です。古い方から kk ターンを1つの要約に畳み、要約+直近ターンだけを持ち歩く。

ここで気づきにくい事故が1つあります。要約の要約を繰り返すと、劣化が累積することです。JPEGを保存し直すたびに画質が落ちるのと同じ構図で、圧縮は不可逆なので、2回目の要約は「元の会話」ではなく「1回目の要約」を入力にしています。細かく詰めた仕様が、5回の畳み込みを経て「ユーザーは設定について話した」まで痩せる。そして痩せたことは、痩せた側からは見えません。

実務での対処は2つです。

そして、要約に何を残すかは自動では決まりません。「会話を要約して」と丸投げすると、モデルはあらすじを書きます。エージェントに要るのは決定事項・未解決の課題・ユーザーが明示的に述べた制約の3つなので、要約プロンプトはこれを出力スキーマとして固定してしまうのが安全です。

外部記憶 — 想起は「関数」である

意味記憶とエピソード記憶は、文脈の外——データベースに置きます。ここで設計すべきものは保存ではなく想起、つまり「いま、どの記憶を机に載せるか」を決める関数です。

素朴には埋め込みの類似度検索、つまりRAGと同じ仕組みを使います(RAGの基礎と設計パターンで扱った、埋め込み・チャンク・上位k件の話がそのまま効きます)。ただし記憶の想起は文書検索と1点だけ違います。新しさと重要さが効く。「先週の話」と「1年前の話」は、内容の似ている度合いが同じでも、価値が違います。

Park らの Generative Agents(arXiv:2304.03442)は、この直感を3項の足し算にしました。記憶 mm を現在の状況 qq に対して評価するスコアは、おおむね次の形です。

s(m,q)=α rel(m,q)+β e−λΔt+γ imp(m)s(m, q) = \alpha\,\mathrm{rel}(m, q) + \beta\,e^{-\lambda \Delta t} + \gamma\,\mathrm{imp}(m)
(2)

各項の意味です。rel\mathrm{rel} は関連度(いまの話題とその記憶の埋め込みがどれだけ近いか)。e−λΔte^{-\lambda \Delta t} は新しさで、Δt\Delta t は最後にその記憶を使ってからの経過時間、λ\lambda は忘却の速さを決める定数です。時間が経つほど指数関数的に小さくなります。imp\mathrm{imp} は重要度で、記憶を書き込むときにモデル自身に10段階で採点させるなどして付けておきます。α,β,γ\alpha, \beta, \gamma は3つの重みで、実装では各項を0〜1に正規化してから足します。

言い換えると、この式は「いまの話に近くて、最近使っていて、そもそも重要な記憶を上に持ってこい」と言っているだけです。どれか1つだけで並べると必ず失敗します。関連度だけなら1年前の似た話が毎回顔を出し、新しさだけなら直前の雑談が本題を押しのけます。

FIG 2クエリを動かすと上位k件の顔ぶれが入れ替わります。距離の測り方(内積/コサイン/L2)を切り替えると順位が変わることに注目してください。想起の質は、記憶の中身より先に「何で測るか」で決まります

保存の粒度も設計対象です。会話1ターンをそのまま1レコードにすると想起したとき前後が切れて意味を失い、逆に大きくすると無関係な部分まで机に載ります。「1つの記憶=1つの言い切れる事実(または1つの出来事)」に正規化し、元ターンへのリンクを持たせる形が扱いやすい落としどころです。

忘れる設計 — いちばん作られていない部分

記憶システムを作るとき、9割の労力は「書く」と「思い出す」に向かいます。しかし本番で壊れるのは、たいてい忘れないことが原因です。

典型はこれです。3月に「東京在住」と書き込まれた。8月にユーザーが「大阪に引っ越しました」と言った。素直な実装はこれを新しいレコードとして追記します。結果、矛盾する2つの事実が並び、想起はどちらか(あるいは両方)を机に載せ、エージェントは会話ごとに違う住所を口にします。

忘却は削除ではありません。実装として使える型は4つです。

4つとも効かせるには、記憶に出所(provenance)が要ります。〈ユーザーが明言した〉のか〈こちらが推測した〉のかで、扱いはまったく違うはずです。推測はいつでも棄却できる仮説として持ち、明言は上書きされるまで保持する。レコードに source_turn_id と status(stated / inferred)の2列を足すだけで、この区別は付けられます。

最小の記憶レイヤ

ここまでを20行に落とすとこうなります。

def turn(user_msg, state, store):
    # 1. 想起: いまの発言に対して外部記憶を並べ替え、上位だけ机に載せる
    recalled = store.recall(query=user_msg, k=5)   # rel + recency + importance

    # 2. 組み立て: 固定部 → 長期(安定) → 想起 → 要約 → 直近ターン の順
    prompt = [SYSTEM, state.profile, recalled, state.summary, *state.recent, user_msg]
    reply = llm(prompt)

    # 3. 短期の更新と、あふれたぶんの圧縮
    state.recent += [user_msg, reply]
    if tokens(state.recent) > BUDGET:
        old, state.recent = state.recent[:-KEEP], state.recent[-KEEP:]
        state.summary = summarize(state.summary, old)   # 決定 / 未解決 / 制約 のみ

    # 4. 書き込み: 明言された事実だけをキー付きで上書き保存
    for fact in extract_facts(user_msg, reply):        # {key, value, status}
        store.upsert(fact, ts=now, source_turn_id=state.turn_id)
    return reply

2行目の順序に意味があります。変わらないものを前、変わるものを後ろへ。これは読みやすさの話ではなく請求額の話で、プロンプトキャッシュは前方一致でしか効かないため、先頭に「現在時刻」や「今回の想起結果」を置くとそれ以降の全トークンがキャッシュから外れます(詳細はプロンプトキャッシュとコンテキスト設計)。記憶を注入する位置は、記憶システムの設計と同時に決める事項です。

なお、この「文脈をどう編集するか」自体をモデルに任せる方向もあります。MemGPT(arXiv:2310.08560)はOSの仮想メモリになぞらえ、文脈の中身をページのように出し入れする操作をモデル自身の関数呼び出しで行う設計を示しました。Reflexion(arXiv:2303.11366)は、失敗した試行への自己反省を言語のままエピソード記憶に積み、次の試行の入力に混ぜます。

現場ではこう使う

誰が、いつ触るか。 社内アシスタントやサポート自動化を運用しているエンジニアが、「同じことを何度も聞かれる」「先週決めたことを忘れる」という苦情を受けた直後が典型です。最初にやることは記憶機能の追加ではなく、いまの文脈の中身をそのままダンプして読むことです。多くの場合、必要な情報は入っているのに中央に埋もれているか、要約で消えています。

触るパラメータとツール。 文脈予算(要約を起動する残トークン率。0.6〜0.8あたりを閾値にすることが多い)、KEEP(生のまま残す直近ターン数)、想起の top_k、減衰定数 λ\lambda(半減期で考えると調整しやすい: 半減期7日なら λ=ln⁡2/7\lambda = \ln 2 / 7)、TTL。既製の記憶レイヤなら Letta(MemGPT)、Mem0、Zep、LangGraph の checkpointer が候補で、自前なら pgvector か既存のベクトルDBに key / value / status / ts / source_turn_id の列を足すだけで始められます。

知らないと事故になる落とし穴。

  1. 要約の劣化累積 — 再要約を繰り返す実装は、静かに情報を失います。畳み込み回数を監視項目にしてください。
  2. キャッシュ破壊による請求増 — 想起結果をプロンプト先頭に差し込むと、前方一致が毎回崩れます。トークン単価が変わっていないのに請求が伸びたら、まずここを疑います。
  3. 記憶経由のプロンプトインジェクション — ユーザー入力から抽出した「事実」に指示文が紛れ込むと、次のセッションで発火する永続的な攻撃になります。書き込み時に命令形を弾く、記憶は必ずデータとして区切って提示する、の2段構えが要ります。
  4. 忘れる権利の未実装 — 個人情報を保存した以上、削除要求に応える経路が必要です。source_turn_id を持たない設計は「この人の記憶を全部消して」に答えられません。
  5. 評価なしの記憶追加 — 記憶を足せば必ず良くなる、という保証はありません。想起がノイズを持ち込んで悪化する例は珍しくないので、記憶あり/なしを同じ会話ログで比べる回帰テストを先に作ってください。

設計レビューで問われる形。 「ユーザーが引っ越したと言ったとき、住所の記憶をどう更新しますか」——追記ではなくキー単位の上書き、旧値はエピソード側に履歴として残す。「文脈長が10倍になったら記憶システムは不要になりますか」——なりません。コストは長さの2乗で効き、中央の情報は使われにくく、忘れる仕組みは長さでは代替できないからです。

まとめ

記憶を足す前に、まず自分のエージェントが「いま何を読んでいるか」を1回ダンプして眺めること。設計はそこからしか始まりません。

参考文献

  1. MemGPT: Towards LLMs as Operating Systems. arXiv:2310.08560論文ページ·PDF
  2. Generative Agents: Interactive Simulacra of Human Behavior. arXiv:2304.03442論文ページ·PDF
  3. Reflexion: Language Agents with Verbal Reinforcement Learning. arXiv:2303.11366論文ページ·PDF
  4. Lost in the Middle: How Language Models Use Long Contexts. arXiv:2307.03172論文ページ·PDF

本記事は上記論文の本文にもとづいて執筆しています。数値・主張は原典を優先してください。

運営: TechAthletes · 編集方針 · 出典の付け方 · 訂正の記録

コメント

コメントにはログインが必要です