エージェントの記憶設計 — 短期・長期・エピソード
LLMは何も覚えていません。会話が続いて見えるのは毎ターン全履歴を読み直させているからです。短期・エピソード・長期の3層に分ける設計、想起を決める式、そして最も設計されていない「忘れる」まで、前提知識ゼロから組み立てます。
要点
- LLMエージェントは訓練後に凍結された重みしか持たず、会話が続くのは毎ターン履歴を丸ごと読み直させているからである。
- 記憶は短期記憶・エピソード記憶・意味記憶の3つに分けて扱う。書き換わる頻度と正しさの寿命が桁で違うからである。
- 毎ターン全履歴を送り直すと、ターン数が2倍になると支払うトークン量は約4倍になる。
- 想起は関連度・新しさ・重要度の3項の足し算で並べ、どれか1つだけでは並べない。
- 本番で壊れるのは忘れないことであり、上書き・減衰・TTL・矛盾検出の4つを最初から入れておく。
MemGPT: Towards LLMs as Operating Systems
一次資料 — この記事の根拠
この解説の公開 2026-08-27
MemGPT: Towards LLMs as Operating SystemsarXiv:2310.08560論文ページ·PDFGenerative Agents: Interactive Simulacra of Human BehaviorarXiv:2304.03442論文ページ·PDF
Reflexion: Language Agents with Verbal Reinforcement LearningarXiv:2303.11366論文ページ·PDF
Lost in the Middle: How Language Models Use Long ContextsarXiv:2307.03172論文ページ·PDF
毎朝、記憶を失って出勤してくる同僚
夜勤明けの引き継ぎを想像してください。交代要員はきわめて優秀で、機械の扱いも手順書も完璧に頭に入っています。ただ一点だけ問題があって、前の晩に何が起きたかをまったく覚えていない。彼が知っているのは、机の上の引き継ぎノートに書いてあることだけです。
LLMエージェントはこれと同じ状況にあります。モデルの重みには膨大な一般知識が焼き込まれていますが、それは訓練が終わった時点で凍結されていて、昨日あなたと話した内容は入っていません。会話が続いているように見えるのは、毎ターン、それまでのやりとりを丸ごともう一度読ませているからです。覚えているのではなく、毎回読み直している。
だとすると「記憶を設計する」という仕事の正体は、モデルを賢くすることではなく、あのノートに何を書き、何を書かず、いつ書き換え、いつ捨てるかを決めることになります。この記事はその3つの判断——書く・選ぶ・忘れる——を順に組み立てます。エージェントのループそのもの(考える→道具を使う→結果を見て考え直す)はLLMエージェントを1から解説にあるので、ここでは「そのループが何を持ち歩くか」だけを扱います。
記憶を3つに分ける
人間の記憶学で使われる分類を借りると、設計の見通しが一気によくなります。エージェントの記憶は、性質の違う3つに分けて別々に扱うのが基本形です。
- 短期記憶(作業記憶) — いま机の上に広げてある紙。直近のやりとりを、要約も加工もせず生のまま持つ。寿命は数ターン。
- エピソード記憶 — 「いつ、何が起きたか」という出来事の記録。〈8月3日、このユーザーは請求書のPDFが開けないと言い、こちらは再発行を提案して解決した〉。時刻と文脈がセットで残る。
- 意味記憶(長期記憶) — 出来事から抽出された、時刻から切り離された安定した事実。〈このユーザーの請求先はA社〉〈設定ファイルは YAML ではなく TOML〉。
この3つを混ぜると必ず破綻します。理由は書き込みの頻度と、正しさの寿命が桁で違うからです。短期記憶は毎ターン書き換わりますが、意味記憶は月に一度更新されるかどうか。逆に意味記憶へ間違いが1つ混ざると、それはすべての会話に効き続けます。同じ場所に置いて同じ規則で扱う理由がありません。
「4つ目」として手続き記憶——〈このユーザーのデプロイは必ずステージングを経由する〉のような、やり方についての知識——を足すこともあります。これも書き換わる速度が違うので、システムプロンプトや専用の指示ファイルに置いて別管理にするのが普通です。
なぜ「全部載せる」ではいけないのか
素朴な実装は「毎ターン、全履歴をそのまま送る」です。文脈長が十分に長ければこれで済みそうに見えます。しかし2つの壁にぶつかります。
1つ目はコストの壁です。毎ターン全履歴を送り直すという操作は、見た目より高くつきます。1ターンあたりの追加分(あなたの発言+エージェントの返答)を トークン、システムプロンプトなど固定部分を トークンとすると、 ターン目までに送った入力トークンの累計は次のようになります。
読み方はこうです。ターン数が2倍になると、支払うトークン量は約4倍になる。 ターン目に送る量が に比例して増え、それを 回足し合わせるので、合計は の2乗で伸びます。会話が2倍長くなっただけで請求が4倍になるのは直感に反しますが、式(1)右辺の がそう言っています。
2つ目は注意の壁です。文脈に入れさえすればモデルが使ってくれる、という前提は成立しません。Liu らの "Lost in the Middle"(arXiv:2307.03172)は、必要な情報を長い文脈の先頭・末尾に置いたときと中央に置いたときで正答率が変わることを測り、中央の情報が使われにくいというU字型の傾向を報告しています。履歴を全部詰め込んだ結果として大事な事実が中央に埋もれれば、入っているのに参照されない事故が起きます。記憶設計の側から見た結論は一行です。文脈は貯蔵庫ではなく、作業机である。
要約という圧縮 — 何を捨てるかの設計
短期記憶が机からあふれたら、どこかで圧縮します。定番はローリング要約です。古い方から ターンを1つの要約に畳み、要約+直近ターンだけを持ち歩く。
ここで気づきにくい事故が1つあります。要約の要約を繰り返すと、劣化が累積することです。JPEGを保存し直すたびに画質が落ちるのと同じ構図で、圧縮は不可逆なので、2回目の要約は「元の会話」ではなく「1回目の要約」を入力にしています。細かく詰めた仕様が、5回の畳み込みを経て「ユーザーは設定について話した」まで痩せる。そして痩せたことは、痩せた側からは見えません。
実務での対処は2つです。
- 生ログを別に残し、要約はそこへのポインタを持つ。 各項目に元のターンIDを添えておけば、必要なときだけ原文を引き直せます。要約は索引であって、原本の置き換えではない、という立て付けにします。
- 畳み込みの回数を数える。 一度要約した区間は凍結し、新しい区間だけを別の要約として足す(階層化する)。同じ内容を繰り返し圧縮しないので、劣化がそこで止まります。
そして、要約に何を残すかは自動では決まりません。「会話を要約して」と丸投げすると、モデルはあらすじを書きます。エージェントに要るのは決定事項・未解決の課題・ユーザーが明示的に述べた制約の3つなので、要約プロンプトはこれを出力スキーマとして固定してしまうのが安全です。
外部記憶 — 想起は「関数」である
意味記憶とエピソード記憶は、文脈の外——データベースに置きます。ここで設計すべきものは保存ではなく想起、つまり「いま、どの記憶を机に載せるか」を決める関数です。
素朴には埋め込みの類似度検索、つまりRAGと同じ仕組みを使います(RAGの基礎と設計パターンで扱った、埋め込み・チャンク・上位k件の話がそのまま効きます)。ただし記憶の想起は文書検索と1点だけ違います。新しさと重要さが効く。「先週の話」と「1年前の話」は、内容の似ている度合いが同じでも、価値が違います。
Park らの Generative Agents(arXiv:2304.03442)は、この直感を3項の足し算にしました。記憶 を現在の状況 に対して評価するスコアは、おおむね次の形です。
各項の意味です。 は関連度(いまの話題とその記憶の埋め込みがどれだけ近いか)。 は新しさで、 は最後にその記憶を使ってからの経過時間、 は忘却の速さを決める定数です。時間が経つほど指数関数的に小さくなります。 は重要度で、記憶を書き込むときにモデル自身に10段階で採点させるなどして付けておきます。 は3つの重みで、実装では各項を0〜1に正規化してから足します。
言い換えると、この式は「いまの話に近くて、最近使っていて、そもそも重要な記憶を上に持ってこい」と言っているだけです。どれか1つだけで並べると必ず失敗します。関連度だけなら1年前の似た話が毎回顔を出し、新しさだけなら直前の雑談が本題を押しのけます。
保存の粒度も設計対象です。会話1ターンをそのまま1レコードにすると想起したとき前後が切れて意味を失い、逆に大きくすると無関係な部分まで机に載ります。「1つの記憶=1つの言い切れる事実(または1つの出来事)」に正規化し、元ターンへのリンクを持たせる形が扱いやすい落としどころです。
忘れる設計 — いちばん作られていない部分
記憶システムを作るとき、9割の労力は「書く」と「思い出す」に向かいます。しかし本番で壊れるのは、たいてい忘れないことが原因です。
典型はこれです。3月に「東京在住」と書き込まれた。8月にユーザーが「大阪に引っ越しました」と言った。素直な実装はこれを新しいレコードとして追記します。結果、矛盾する2つの事実が並び、想起はどちらか(あるいは両方)を机に載せ、エージェントは会話ごとに違う住所を口にします。
忘却は削除ではありません。実装として使える型は4つです。
- 上書き(キー付き記憶) — 意味記憶には〈属性キー+値+更新時刻〉の形を強制し、同じキーは常に最新1件だけを有効にする。住所や所属のような一意であるべき事実は、これで矛盾が構造的に起きなくなります。
- 減衰 — 式(2)の をスコアだけでなく保持判断にも使い、一定値を下回った記憶を冷たい層へ落とす。消すのではなく降格させる(hot → cold → アーカイブ)ので、後から監査できます。
- TTL(有効期限) — 「今週の締切は金曜」のようなそもそも寿命のある事実には、書き込み時に期限を付ける。期限切れは想起の候補から自動で外れます。
- 矛盾検出 — 書き込み時に同じキーの既存値と食い違わないかを見る。食い違うなら追記ではなく更新にし、旧値はエピソード記憶側に〈いつ変わったか〉として残す。
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、減衰定数 (半減期で考えると調整しやすい: 半減期7日なら )、TTL。既製の記憶レイヤなら Letta(MemGPT)、Mem0、Zep、LangGraph の checkpointer が候補で、自前なら pgvector か既存のベクトルDBに key / value / status / ts / source_turn_id の列を足すだけで始められます。
知らないと事故になる落とし穴。
- 要約の劣化累積 — 再要約を繰り返す実装は、静かに情報を失います。畳み込み回数を監視項目にしてください。
- キャッシュ破壊による請求増 — 想起結果をプロンプト先頭に差し込むと、前方一致が毎回崩れます。トークン単価が変わっていないのに請求が伸びたら、まずここを疑います。
- 記憶経由のプロンプトインジェクション — ユーザー入力から抽出した「事実」に指示文が紛れ込むと、次のセッションで発火する永続的な攻撃になります。書き込み時に命令形を弾く、記憶は必ずデータとして区切って提示する、の2段構えが要ります。
- 忘れる権利の未実装 — 個人情報を保存した以上、削除要求に応える経路が必要です。
source_turn_idを持たない設計は「この人の記憶を全部消して」に答えられません。 - 評価なしの記憶追加 — 記憶を足せば必ず良くなる、という保証はありません。想起がノイズを持ち込んで悪化する例は珍しくないので、記憶あり/なしを同じ会話ログで比べる回帰テストを先に作ってください。
設計レビューで問われる形。 「ユーザーが引っ越したと言ったとき、住所の記憶をどう更新しますか」——追記ではなくキー単位の上書き、旧値はエピソード側に履歴として残す。「文脈長が10倍になったら記憶システムは不要になりますか」——なりません。コストは長さの2乗で効き、中央の情報は使われにくく、忘れる仕組みは長さでは代替できないからです。
まとめ
- LLMは覚えていない。会話が続いて見えるのは、毎ターン読み直させているから。設計対象は机に何を載せるか。
- 短期・エピソード・意味の3層に分ける。理由は、書き換わる頻度と正しさの寿命が桁で違うから。
- 想起は関数である。関連度・新しさ・重要度の3項で並べ替え、どれか1つだけで並べない。
- 本番で壊れるのは忘れないこと。上書き・減衰・TTL・矛盾検出の4つを、最初から入れておく。
記憶を足す前に、まず自分のエージェントが「いま何を読んでいるか」を1回ダンプして眺めること。設計はそこからしか始まりません。
コメント
コメントにはログインが必要です