はじめに — 1,494 個のメモリで、edge の 87% が消えていた
Kagura Memory Cloud は私が個人で開発している MCP (Model Context Protocol) ベースの「LLM 共有記憶」です。Claude, Cursor, Gemini CLI など MCP 対応クライアントから同じ context を recall できるよう、長期記憶を メモリ単位 と メモリ間の関係 (edge) で構造化して保持します。
ある日、開発用 context (kagura-dev) の状態を確認したら、奇妙な数字が出てきました。
-- 2026-05-19 00:30 UTC 時点
SELECT COUNT(*) FROM neural_memory_edges WHERE context_id = 'abfd654d…'; -- 4
SELECT COUNT(*) FROM sleep_actions a JOIN sleep_reports r ON r.id = a.report_id
WHERE r.context_id = 'abfd654d…' AND a.action_type = 'create_edge'; -- 31
1,494 個のメモリ。Sleep Maintenance が 31 本の edge を作った。なのに残ったのは 4 本。生存率 13%、消失率 87%。残った 4 本もすべて当日生まれの新しい edge で、過去の edge はゼロでした。
これは普通に壊れている。今回はその原因を突き止めて直した話です。直してみたら、結局のところ「LLM の長期記憶は脳と同じく 2 層に分けるべき」という、神経科学では 1995 年に決着していた話に行き着きました。
何が起きていたか — Hebbian 学習だけの世界
Kagura Memory Cloud の neural memory layer は、最初は古典的な Hebbian 学習 で edge を作っていました。「Neurons that fire together, wire together」というやつです。
具体的には、ユーザーが recall(query="...") した時に同時に取れたメモリ同士に edge を張り、回数を重ねるほど edge の重みを上げます。実コードはこんな感じ:
# backend/src/neural/hebbian.py
#
# Δw_ij ← η · (a_i · C_i) · (a_j · C_j) − λ · w_ij
#
# - η: learning rate
# - a_i, a_j: activation strengths of the co-activated nodes
# - C_i, C_j: confidence / trust scores (poisoning defense)
# - λ: L2 decay coefficient (prevents weight explosion)
# - w_ij: current edge weight, clamped to [0.0, 3.0]
Hebbian は良い性質を持っていて、「一緒に思い出された」「関連付けてユーザーが使った」という事実を 使用頻度のトレース として記録します。これだけで edge ベースの探索 (explore(seed=memory_id)) が動くし、誰かが似た2つを連続でアクセスすれば自動で edge が育つ。
代わりに古い edge は 時間で減衰 させます。これも普通の設計です:
# backend/src/neural/decay.py
#
# w_ij(t + Δt) = w_ij(t) · exp(−decay_rate · Δt)
#
# After decay, w_ij < prune_threshold rows are removed.
使われない edge は消える。これも妥当に見えました。「1,494 メモリで 87% 消失」が観測されるまでは。
N² の壁 — Hebbian がスケールしない理由
問題は edge の作り方が 2 種類混在していた ことです。
Kagura Memory Cloud には Hebbian co-activation とは別に、Sleep Maintenance という夜間バッチがあります。これは context 内のメモリ全体を見渡して、コサイン類似度が高い (≥ 0.5) ペアを LLM に判定させ、「意味的に関連する」と判定された pair に edge を張ります。
ここが落とし穴で、Sleep が張った edge も 同じ neural_memory_edges テーブル に書き込まれ、同じ Hebbian decay の対象 になっていました。
ここで簡単な確率計算をしてみます。context に N 個のメモリがあるとして、「特定ペア (i, j) が一定時間内に同時に recall される」確率はざっくり 1/N² で減ります。N が大きくなるほどあるペアが co-recall される確率はゼロに近づく。
| N (メモリ数) | 1/N² | 直感 |
|---|---|---|
| 100 | 1 / 10,000 | たまに reinforce される |
| 1,000 | 1 / 1,000,000 | ほぼ reinforce されない |
| 1,494 (実測) | 1 / 2,232,036 | reinforce ほぼゼロ |
decay は時間で必ず効くので、reinforce が来ない pair の edge は 必ず消える。そして 意味的に類似しているという事実は、reinforce しなくても変わらない静的な性質 です。それを Hebbian decay にさらしていたのが category error でした。
Sleep が頑張って「この 2 つは意味的に関連します」と発見しても、翌日には消えている。Sleep の労力が全部無駄になっていたわけです。
これに気付いた瞬間、配置すべき設計が見えました。
海馬と大脳皮質 — Complementary Learning Systems の話
神経科学に Complementary Learning Systems theory (McClelland, McNaughton & O'Reilly, 1995) という枠組みがあります。「脳は短期と長期で別の場所を使って記憶している」という有名な仮説です。
| 海馬 (hippocampus) | 大脳皮質 (cortex) | |
|---|---|---|
| 担当 | エピソード的連合 (短期) | 意味的連合 (長期) |
| 形成 | 個別の経験で速く形成 | 経験を繰り返し見て遅く形成 |
| 性質 | 動的・経験依存・上書きされやすい | 静的・構造的・上書きされにくい |
| 消失 | 使われないと数日で消える | 内容そのものが意味を持つので残る |
Karpathy も最近 LLM の memory を語る文脈で同じ比喩を使っていました。記憶を 「いつ思い出されるか (episodic)」 と 「何の話か (semantic)」 で分けるのは、生物が 5 億年かけて見つけた最適解の一つなんでしょう。
Kagura の問題に戻ると、これと完全に符合します:
- Hebbian edge = 海馬的: 「最近 co-recall された」という episodic な痕跡。reinforce しなければ消えていい。
- Sleep が発見する意味的 edge = 大脳皮質的: メモリの内容そのものが類似しているという 静的な性質。reinforce に依存して維持すべきではない。
そして、せっかくなのでもう一種類追加します:
-
Declared edge = ユーザー宣言: 「これとこれは関連する」と人間が
create_edgeMCP tool で明示的に宣言した edge。これも当然消えてはいけない。
直し方 — origin discriminator の導入
設計は単純です。neural_memory_edges に origin カラムを足し、3 値で分類します。
ALTER TABLE neural_memory_edges
ADD COLUMN origin VARCHAR(20) NOT NULL DEFAULT 'hebbian';
ALTER TABLE neural_memory_edges
ADD CONSTRAINT valid_edge_origin
CHECK (origin IN ('hebbian', 'semantic', 'declared'));
そして decay は hebbian だけを対象にする:
# backend/src/neural/decay.py
edges_decayed = await edge_repo.bulk_decay_weights(
user_id, decay_factor, only_origin='hebbian', # ← この一行が肝
)
edges_pruned = await edge_repo.prune_weak_edges(
user_id, self.config.prune_threshold, only_origin='hebbian',
)
これで semantic と declared の edge は 時間で消えない。代わりに月 1 回 semantic_edge_reverify cron が走って、endpoint memory が削除された孤児 edge だけを掃除します (O(edges)、O(N²) ではない、というのが重要)。
| origin | 誰が作るか | 重みの意味 | decay 対象 |
|---|---|---|---|
hebbian |
runtime の recall() co-activation |
使用頻度トレース (0.0–3.0) | yes (毎晩) |
semantic |
Sleep の edge_discovery
|
コサイン類似度 (0.5–1.0) そのもの | no |
declared |
ユーザーが create_edge で宣言 |
ユーザー指定 (default 1.0) | no |
Sticky-upsert — 「降格しない」保証
origin を導入したら、もう一つ気をつけるべきことがあります。同じメモリペアに対して時間差で複数の origin が候補になる可能性です。
例えば: あるペアが昨日 Sleep に「意味的に関連する」と判定されて origin='semantic' で edge が張られた。今日たまたまそのペアが co-recall されて、Hebbian の path も発動した。
この時、何も考えずに INSERT … ON CONFLICT DO UPDATE で上書きすると、origin が semantic から hebbian に 降格 してしまう。すると翌日からこの edge も decay 対象になり、また消える。N² の壁が再発する。
なので upsert は 片方向に固定 しました:
-
hebbian→semantic,declaredへの 昇格 は OK (情報量が増える) -
semantic,declared→hebbianへの 降格 は禁止
invariant としてシンプルで、テストも書きやすい。create_or_update_edge の ON CONFLICT 節に CASE 式を一個書けば終わりです。
ボーナス: edge の origin と「memory の scope」は別物
おまけの整理として、Kagura には memory レベルの分類も存在します。Memory.scope が working(短期)と persistent(長期)の 2 値で、Sleep Consolidation phase が working memory を access pattern と LLM 判断で persistent に 昇格 させます。
ここで「working memory は Hebbian 管理になるの?」という質問が出ました。答えは No、独立した 2 軸です:
| 何の lifetime | 誰が管理 | |
|---|---|---|
Memory.scope |
memory node の寿命 | Sleep Consolidation phase |
Edge.origin |
edge の寿命 | DecayManager / semantic_edge_reverify
|
ただし 間接連動はあります: Hebbian の co-recall が増える working memory は access frequency が上がるので、Consolidation が「これは promote すべき」と判断しやすい。Hebbian は edge 寿命を直接管理しないが、node 昇格の signal を間接供給する。
神経科学アナロジーで言えば、「edge consolidation」(Hebbian → Semantic) と 「node consolidation」(working → persistent) が別タイムスケール・別メカニズムで動いている 2 階層構造です。
デプロイ後 — 数字で見る効果
実際にこの設計を v0.16.x で deploy して、kagura-dev context のバックフィル (過去メモリに対する 1 回だけの cosine 類似度計算) を走らせました。
SELECT origin, COUNT(*) FROM neural_memory_edges
WHERE context_id = 'abfd654d…' GROUP BY origin;
origin | count
----------+-------
hebbian | 86
semantic | 1929
4 本だった edge が、2,015 本 になりました。semantic は decay の対象外なので、明日も明後日も残り続けます。今後 Sleep が走るたびに新しい意味的 edge が積み上がり、hebbian はその上で「最近どう使われたか」のトレースを保ち続ける。
explore(seed_memory) の graph traversal が、ようやくまともな密度のグラフ上で動くようになった、というのが体感です。「explore したら関連メモリが芋づる式に出てくる」状態。
注意点と限界
ひとつ釘を刺しておきたいのは、海馬/大脳皮質の比喩は 教育的な類比 (pedagogical analogy) であって、実装としての主張ではない ということです。Kagura のコードは LSTM や Differentiable Neural Computer のように脳の構造を真似ているわけではありません。「両者の lifetime semantics が、Complementary Learning Systems の 2 系列と対応している」というだけです。神経科学の overclaim はやめておきましょう。
もう一つ、今回の origin は edgeを「どう作られたか」だけで分類しています。「どういう関係なのか」についてはedge_typeという別軸が既に存在し、related_to / depends_on / learned_from などを持ちますが、粒度は粗く、純粋な「因果」「時間的前後」「上位下位」のような分類はまだ持っていません。さらに edge_type にはsemantic_similarity / declared_link のように origin と概念重複する値も残っており、taxonomyの整理は別 issue として継続中です。これを将来どう分けるかは「N² で消える」とは独立した話題なのでここでは扱いません。
まとめ
LLM の長期記憶を扱う時、「最近一緒に思い出された」と「内容として似ている」を区別しないと、N² で scale しません。
- Hebbian decay は episodic な痕跡には正しい挙動。
- だが意味的近傍は静的な性質で、decay の対象にしてはいけない。
- これを 1 カラムの
origindiscriminator で分け、decay loop をhebbianだけに当てる。 - ついでに sticky-upsert で「降格させない」invariant を確保する。
設計判断としては小さな変更ですが、Sleep が見つけた知識が「翌日には消えている」状況から「ちゃんと積み上がる」状況への転換でした。同じ「記憶が消える」問題を踏んでいる方の参考になれば。
参考リンク
- リポジトリ: https://github.com/kagura-ai/memory-cloud
- 設計ドキュメント: docs/concepts.md § Edge Origins / docs/architecture.md § Edge Origins
- 運用 runbook: docs/operations/semantic-edge-rollout.md
- 実装 PR: #726 (origin discriminator) / #735 (本記事の元になったドキュメント整備)
- 神経科学: McClelland, McNaughton, O'Reilly. Why there are complementary learning systems in the hippocampus and neocortex. Psychological Review (1995).