AI & Agents14 min readMarian Engineering

エージェントメモリの設計 — 7軸サリエンス、半減期減衰、エピソードからポリシーへ

短期記憶の昇格(consolidation)と制御された忘却(distillation)を純粋関数で実装する

Key Takeaways

  • 記憶システムの本体は書き込みではなく価値の経時管理(サリエンス・昇格・忘却)
  • 7軸サリエンスは軸ごとに半減期が異なり(urgency 7日〜sensitivity 365日)、時間構造を表現する
  • consolidationは「2回繰り返した」または「一度でもsalience 0.8以上」で短期→長期へ昇格させる
  • distillationは削除せず抽象度を上げる忘却。原本はprovenanceとして残る
  • 全判定が時刻を引数に取る純粋関数で、記憶のライフサイクルをユニットテストできる

課題: 記憶は貯めるほど劣化する

エージェントに長期記憶を持たせる最も素朴な方法は「会話から事実を抽出して全部ベクトルDBへ」ですが、これは数週間でノイズの山になります。古い決定と新しい決定が同じ重みで並び、雑談由来の記憶が重要な制約を埋もれさせる。記憶システムの本体は書き込みではなく、価値の経時管理です。

Marianのメモリ基盤は3つの問いに分けて設計されています。(1)何をどのくらいの強さで覚えるか(サリエンス)、(2)どう思い出すか(リコール)、(3)どう忘れる/抽象化するか(consolidation / distillation)。

エピソードのスキーマ

記憶の単位は「エピソード」です。marian_memory_episodesテーブルに、出来事の叙述・結果・関与者・時間バケットを持ちます。

create table marian_memory_episodes (
  id           text primary key,
  user_id      uuid not null,
  type         text default 'note',  -- meeting | decision | incident | deal | ...
  narrative    text not null,
  outcome      text,                 -- positive | negative | pending
  actors       text[],
  occurred_at  date,
  time_buckets text[],               -- ['2026', '2026-06', '2026-06-07']
  importance   real,                 -- [0,1]
  risk         real                  -- [0,1]
);
-- generated column: importance_band = high(>=0.7) | med(>=0.4) | low
-- GINインデックス: actors, time_buckets

time_bucketsが年・月・日の3粒度の配列なのは、「2026年6月の出来事」「先週の」のような時間キューでGIN索引を引けるようにするためです。日付演算をクエリ時にやるのではなく、書き込み時に検索キーへ展開しておきます。

サリエンス: 7軸の重み付き合成

記憶の強さは単一スコアではなく、7軸のベクトルとして持ちます。合成時の重みは固定です。

重み半減期(日)
importance(重要度)0.28180
impact(影響度)0.2245
risk(リスク)0.18180
urgency(緊急度)0.127
novelty(新規性)0.0814
confidence(確信度)0.1060
sensitivity(機密度)0.02365
compositeSalience(s) = Σ(weight[axis] × s[axis]) × (0.6 + 0.4 × s.confidence)
isProtected(s) = s.risk >= 0.7 || s.importance >= 0.7

confidenceが部分ゲート(0.6〜1.0の乗数)になっているのが特徴です。確信のない記憶は合成スコアを最大40%減衰しますが、ゼロにはしません。「不確かだが重要かもしれない」記憶を完全には殺さない設計です。isProtectedはリスクか重要度が0.7以上の記憶を後述の忘却から保護します。

リコール: キュー索引で「全件走査しない想起」

想起は「actor=顧客A、time=2026-06、outcome=negative」のようなキューの積集合で行います。実装は次元ごとの転置インデックス(ポスティングリスト)です。

// lib/marian-memory/cue-index.ts
// dims: Map<次元, Map<値, Set<エピソードid>>>
prune(index, query) →
  1. 各キューをポスティングリストに解決(次元内はunion)
  2. 選択率の高い(=小さい)リスト順にソート
  3. 最小リストを起点に積集合
  → examined ≈ 最も選択的なキューのリスト長(全件Nに非依存)

計算コストの主部が「最も選択的なキューのポスティングリスト長」になるため、エピソードが100万件あってもリコールで実際に調べるのは数件〜数十件です。pruneの戻り値にはexaminedfullScan(全件走査した場合のコスト)が含まれており、リコール効率の回帰テストができます。

書き込み: LLMなしのヒューリスティック抽出

Askの質問応答から記憶候補を作るextractMemoryは、LLMを呼ばず正規表現ベースの分類で動きます。

  • 選好・決定(I prefer / we decided / 私の◯◯は)→ 長期記憶、salience 0.8
  • 定義(XはYである)→ 長期記憶、salience 0.65
  • 曖昧性解消(ここでのXは〜の意味)→ セッション限定の短期記憶
  • 挨拶・相槌 → 破棄

短期記憶のデフォルトTTLは14日。この段階の精度は粗くてよく、価値の判定は次のconsolidationに委ねます。書き込みを安価で決定的にしておくことで、抽出パイプラインがLLMのレイテンシ・コスト・非決定性から切り離されます。

consolidation: 夜3時の「組織の睡眠」

毎日03:00のスケジュールジョブが短期記憶を走査し、長期記憶へ昇格させます。判定は純粋関数です。

// lib/marian-memory/consolidate.ts (デフォルト値)
// minOccurrences: 2, salienceFloor: 0.8, minConfidence: 0.4
subject(または content先頭60字)でグループ化し、
  (出現回数 >= 2 または 最大salience >= 0.8) かつ 平均confidence >= 0.4
  → 長期へ昇格(内容マージ、salienceはmax、confidenceは平均)
それ以外 → 短期のまま(TTLで自然消滅)

「繰り返し現れた」か「一度でも強烈だった」かのどちらかで昇格する、という2経路です。人間の記憶の固定化(反復学習と一発学習)と同じ構造を、しきい値2つで表現しています。

distillation: 削除しない忘却

週次ジョブは古く参照されない記憶のサリエンスを軸別半減期で減衰させ、床値(0.35)を下回ったものを抽象度の階段で1段上げます: raw → summary → pattern → principle → policy

urgencyは7日、importanceは180日という半減期の差により、「先週は緊急だった」はすぐ消え、「重要だった」は半年残ります。重要なのは元のエピソードを削除しないことです。抽象化されたレコードが前面に出て、原本は来歴(provenance)として保持されます。isProtectedな記憶は減衰対象から除外されます。

エピソードからポリシーへ

最終段では、結果(outcome)つきエピソード群から行動方針を蒸留します。negative outcomeの共通原因はmitigate(回避)ポリシーに、positiveはreinforce(強化)に。最低支持数2、confidenceはcount/(count+1) + salienceSum/(count×4)で支持数とともに漸近します。生成されたポリシー群は矛盾検出(同一主題で否定の偶奇が逆)を通り、新しい状況の判断時にjudge(situation, policies)が根拠エピソードつきで適用ポリシーを返します。

記憶パイプラインの終着点は「思い出せること」ではなく「次の判断に効くこと」。エピソード→パターン→ポリシーという蒸留がその経路になる。

まとめ

Marianのメモリ基盤は、(1)7軸サリエンス+confidenceゲートで記憶の価値をベクトルとして保持、(2)転置キュー索引でN非依存のリコール、(3)反復または強度で昇格するconsolidation、(4)軸別半減期+抽象度ラダーによる削除しない忘却、(5)エピソード→ポリシーの蒸留、で構成されます。すべての判定が時刻を引数に取る純粋関数なので、1年分の記憶のライフサイクルを1秒のユニットテストで検証できます。

FAQ

なぜ記憶のスコアを単一の数値ではなく7軸で持つのか?
減衰速度が軸ごとに違うからです。緊急度は半減期7日で消えるべきですが、重要度とリスクは180日残すべきです。単一スコアではこの時間構造を表現できません。合成が必要な場面ではcompositeSalience(重み付き和×confidenceゲート)に畳み、リスクまたは重要度0.7以上は忘却から保護します。
記憶が増えても想起が遅くならないのはなぜか?
actor・time・outcome・typeの各次元に転置インデックス(ポスティングリスト)を持ち、最も選択的なキューのリストを起点に積集合を取るためです。実際に調べる件数は最小ポスティングリスト長に比例し、総エピソード数Nには依存しません。pruneはexaminedとfullScanのコストを返すので、効率の回帰テストも可能です。
古い記憶は削除されるのか?
削除されません。週次のdistillationジョブが半減期減衰後のサリエンスが床値(0.35)を下回った記憶をraw→summary→pattern→principle→policyの抽象度ラダーで1段引き上げ、原本は来歴として保持します。検索の前面には抽象化された記憶が出て、詳細が必要なときだけ原本に降りられます。