1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIエージェントに永続記憶を実装する:3層メモリの最小設計とチェックリスト

1
Last updated at Posted at 2026-05-31

AIエージェントを長く運用すると、「賢いのに会話をまたぐと前提が毎回リセットされる」という壁に当たります。原因は記憶を実装していないこと。この記事は、永続記憶を最小構成で実装する手順と、運用でハマる落とし穴・チェックリストを、コード中心でまとめます。

設計思想の背景は別記事に分けています。ここは実装の手を動かす側に寄せます。

全体像:3層 × 3段

  • 記憶を確度で3層に分ける:tentative(仮) / confirmed(確定) / working(実働)
  • 流れは3段:保存 → 昇格/降格 → 想起

これだけ押さえれば、素朴な「全部ためて全部流し込む」実装の破綻(トークン爆発・古い前提の混入・文脈の取り違え)を避けられます。

Step 1. スキーマを決める

1記憶=1事実。scope(用途境界) と tier(確度) を必ず持たせます。

type MemoryTier = "tentative" | "confirmed" | "working";

interface MemoryRecord {
  id: string;
  scope: string;          // プロジェクト/用途。混ぜない単位
  type: "episodic" | "semantic" | "procedural";
  tier: MemoryTier;
  content: string;
  evidenceCount: number;  // 昇格判定
  createdAt: string;
  updatedAt: string;
  embedding?: number[];   // 想起の関連検索
}

DBはPostgres + pgvector でも、SQLite + 自前コサイン類似度でも構いません。最初はscope/tier/embeddingの3つが引ければ十分です。

Step 2. 保存(capture)

会話の終わりに要点を抽出し、既定 tentative で書き込みます。ここで confirmed にしないのが事故防止の肝。

async function capture(turn: ConversationTurn) {
  const facts = await extractSalientFacts(turn); // LLMで要点抽出
  for (const f of facts) {
    await db.insert({
      ...f, tier: "tentative", evidenceCount: 1,
      embedding: await embed(f.content),
    });
  }
}

Step 3. 昇格・降格(reconcile)

繰り返し確認された記憶だけ confirmed へ昇格。矛盾したものは降格。降ろす処理を入れないと古い地図で走り続けます。

async function reconcile(memo: MemoryRecord, incoming: Fact) {
  if (semanticallyEqual(memo, incoming)) {
    memo.evidenceCount++;
    if (memo.evidenceCount >= 2) memo.tier = "confirmed";  // 昇格
  } else if (contradicts(memo, incoming)) {
    memo.tier = "tentative";                                // 降格
  }
  memo.updatedAt = new Date().toISOString();
  await db.update(memo);
}

会話のレイテンシに乗せず、低頻度のバッチ(cron等)で回すのがコツ。

Step 4. 想起(recall)

次の会話で必要な分だけ注入。全件入れない。

async function recall(query: string, scope: string, tokenBudget: number) {
  const hits = await db.search({
    scope,                                   // 境界を越えない
    tierPriority: ["working", "confirmed"],  // 仮は原則入れない
    vector: await embed(query),
    limit: 20,
  });
  return packWithinBudget(hits, tokenBudget);  // 予算で打ち切る
}

想起の3原則:scopeを越えない / tierで絞る / トークン予算で打ち切る

Step 5. MCPサーバーとして公開する(任意だが推奨)

記憶をモデルの外に出し、memory_save / memory_recall を MCP ツールとして公開すると、どのエージェント・どのモデルからも同じ記憶を使えます。

{
  "tools": [
    { "name": "memory_save",   "description": "事実を保存(既定tier=tentative)" },
    { "name": "memory_recall", "description": "queryとscopeで関連記憶を取得" }
  ]
}

外に置く利点:可搬性(モデル乗り換えで記憶を失わない)/ 共有(複数エージェント・チーム)/ 所有(見える・直せる・持ち出せる)。

ハマりどころチェックリスト

  • 1記憶=1事実。scopetier を必須にしたか
  • 保存は既定 tentative。いきなり confirmed にしていないか
  • evidenceCount で昇格、矛盾検知で降格を実装したか
  • recall は scope限定 + tier優先 + トークン予算で打ち切り
  • 想起は関連検索(埋め込み)で必要分だけか
  • 昇格・圧縮は低頻度バッチに逃がしたか
  • 別用途の記憶が混ざっていないか(scopeの切り方を点検)

まとめ

記憶は「ためる」ではなく「整理して取り出す」。3層で確度を分け、保存→昇格/降格→想起で流し、scopeとトークン予算で絞る。MCPで外に出せば可搬・共有・所有が手に入ります。

モデルは使い捨て、記憶は育つ資産。実装の主戦場は保存ではなく**想起(取り出し方)**です。


記憶レイヤーの設計と実録は、エージェントメモリーズで継続的に公開しています。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?