Skip to content

Agent Memory 系統:從 RAG 到 Read-Write 記憶的演化

2026年3月19日 1 分鐘
TL;DR RAG 是唯讀的。Agent Memory 讓 AI 不只能讀,還能寫入和持久化資訊。三種記憶類型:Procedural(行為模式)、Episodic(時間事件)、Semantic(事實知識),構成完整的認知記憶系統。

你的 AI Agent 很聰明,但它記不住任何事。

每次對話開始,它都是一張白紙。上一次聊了什麼、使用者偏好什麼語氣、三天前提過的旅行計畫——全部歸零。你可以透過 RAG 餵它知識,但 RAG 是唯讀的:Agent 能讀取外部資料,卻無法把新學到的東西寫回去。

這就像一個人能看書,但不能做筆記。

Agent Memory 改變了這件事。它讓 AI 系統不只能讀取,還能寫入和持久化資訊。這篇文章走過從 RAG 到 Agent Memory 的完整演化路徑,拆解三種記憶類型的設計,並附上 TypeScript 實作。


從 RAG 到 Agent Memory 的演化

階段一:RAG(Read-Only)

RAG 是最基本的外部記憶形式。系統從向量資料庫檢索相關文件,塞進 context window,讓 LLM 基於這些資料回答問題。

使用者問題 → 向量搜尋 → 取回文件片段 → LLM 生成回答

這個流程是單向的:資料從外部流入 context,但 LLM 無法把任何東西寫回去。

interface RAGPipeline {
  retrieve(query: string): Promise<Document[]>;
  generate(query: string, docs: Document[]): Promise<string>;
}

async function ragAnswer(query: string, pipeline: RAGPipeline): Promise<string> {
  const docs = await pipeline.retrieve(query);
  return pipeline.generate(query, docs);
}

RAG 的限制很明確:

  • 無法學習:使用者糾正了 Agent 的錯誤,下次還是會犯
  • 無法個人化:不知道使用者偏好什麼格式、語氣、深度
  • 無法累積:多輪對話的洞察無法跨 session 保留

階段二:Agentic RAG(Read + Decide)

Agentic RAG 讓 LLM 在檢索流程中有決策權——評估結果是否充分,不夠就改寫查詢再搜一次。

type Decision =
  | { action: 'ANSWER'; confidence: number }
  | { action: 'RETRIEVE'; rewrittenQuery: string }
  | { action: 'BROADEN'; relaxedFilters: FilterSet };

async function agenticRagAnswer(
  query: string,
  pipeline: AgenticRAGPipeline,
  maxSteps = 5,
): Promise<string> {
  let currentQuery = query;
  let allDocs: Document[] = [];

  for (let step = 0; step < maxSteps; step++) {
    const docs = await pipeline.retrieve(currentQuery);
    allDocs = mergeAndDedupe(allDocs, docs);

    const decision = await pipeline.evaluate(currentQuery, allDocs);
    if (decision.action === 'ANSWER') break;
    if (decision.action === 'RETRIEVE') currentQuery = decision.rewrittenQuery;
  }

  return pipeline.generate(query, allDocs);
}

Agentic RAG 讀得更聰明,但仍然是唯讀的。它不會記住這次搜尋的經驗、不會學到使用者偏好什麼類型的答案。

階段三:Agent Memory(Read + Write)

Agent Memory 補上了最後一塊拼圖:寫入

對話 → 記憶提取 → 寫入記憶庫

新對話 → 記憶檢索 → 注入 context → LLM 回答

              持久化記憶庫
interface AgentMemorySystem {
  // Read
  retrieve(query: string, userId: string): Promise<Memory[]>;
  // Write
  store(memory: MemoryInput, userId: string): Promise<string>;
  // Manage
  update(memoryId: string, patch: Partial<MemoryInput>): Promise<void>;
  delete(memoryId: string): Promise<void>;
  listByUser(userId: string): Promise<Memory[]>;
}

interface Memory {
  id: string;
  type: 'procedural' | 'episodic' | 'semantic';
  content: string;
  embedding: number[];
  importance: number;        // 0-1
  createdAt: Date;
  lastAccessedAt: Date;
  accessCount: number;
  metadata: Record<string, unknown>;
}

三個階段的差異一目瞭然:

                RAG          Agentic RAG     Agent Memory
─────────────────────────────────────────────────────────
讀取             ✓              ✓               ✓
決策             ✗              ✓               ✓
寫入             ✗              ✗               ✓
跨 session      ✗              ✗               ✓
個人化          ✗              ✗               ✓
持續學習         ✗              ✗               ✓

關鍵洞察:Read-Write Memory 是從工具到 Agent 的分水嶺。 一個能學習、能記住、能累積的系統,才像一個真正的 Agent。


三種記憶類型

認知科學把人類記憶分成幾種類型。Agent Memory 借用了這個框架,定義了三種最有用的記憶類型。

1. Procedural Memory:行為模式

Procedural Memory 存的是如何做事——規則、偏好、行為模式。

人類的 Procedural Memory 是騎腳踏車、打字這類「身體記住」的技能。對 AI Agent 來說,它是回應使用者時應該遵循的模式:

  • 「永遠使用正式語氣」
  • 「回答技術問題時附上程式碼範例」
  • 「使用者不喜歡冗長的開場白,直接切入重點」
interface ProceduralMemory {
  id: string;
  type: 'procedural';
  rule: string;
  source: 'explicit' | 'inferred';
  confidence: number;
  scope: 'global' | 'topic';
  topic?: string;
  active: boolean;
  createdAt: Date;
}

const examples: ProceduralMemory[] = [
  {
    id: 'proc_001',
    type: 'procedural',
    rule: '使用者偏好繁體中文回覆,技術術語保留英文原文',
    source: 'explicit',
    confidence: 1.0,
    scope: 'global',
    active: true,
    createdAt: new Date('2026-01-15'),
  },
  {
    id: 'proc_002',
    type: 'procedural',
    rule: '回答程式碼問題時,先給完整可執行範例,再解釋',
    source: 'inferred',
    confidence: 0.85,
    scope: 'topic',
    topic: 'programming',
    active: true,
    createdAt: new Date('2026-02-20'),
  },
];

Procedural Memory 的注入方式是直接放進 system prompt:

function buildSystemPrompt(
  basePrompt: string,
  memories: ProceduralMemory[],
): string {
  const activeRules = memories
    .filter((m) => m.active)
    .sort((a, b) => b.confidence - a.confidence);

  if (activeRules.length === 0) return basePrompt;

  const rulesBlock = activeRules
    .map((m) => {
      const prefix = m.scope === 'global' ? '[全域]' : `[${m.topic}]`;
      return `- ${prefix} ${m.rule}`;
    })
    .join('\n');

  return `${basePrompt}\n\n## 使用者偏好與行為規則\n\n${rulesBlock}`;
}

特性: 數量少、權重高、不衰減、每次對話都全部載入。

2. Episodic Memory:時間事件

Episodic Memory 存的是發生了什麼事——帶有時間戳記的具體事件。

人類的 Episodic Memory 是「昨天在咖啡廳遇到 Kevin」「上週五的會議決定用 PostgreSQL」。對 AI Agent 來說:

  • 「使用者在 2026/03/15 提到下個月要去日本旅行」
  • 「使用者在 2026/03/20 說專案 deadline 是四月底」
  • 「使用者在 2026/03/25 抱怨回覆太長,之後調整了風格」
interface EpisodicMemory {
  id: string;
  type: 'episodic';
  event: string;
  context: string;
  emotion?: 'positive' | 'negative' | 'neutral';
  importance: number;
  timestamp: Date;
  sessionId: string;
  embedding: number[];
}

Episodic Memory 的檢索需要同時考慮語意相關性時間近度

async function retrieveEpisodicMemories(
  query: string,
  userId: string,
  vectorStore: VectorStore,
  options: { limit: number; recencyWeight: number; relevanceWeight: number },
): Promise<EpisodicMemory[]> {
  const candidates = await vectorStore.search({
    query,
    filter: { userId, type: 'episodic' },
    limit: options.limit * 3,
  });

  const now = Date.now();
  const scored = candidates.map((c) => {
    const relevanceScore = c.score;

    // 時間衰減:指數衰減,半衰期 7 天
    const ageDays = (now - new Date(c.metadata.timestamp).getTime()) / 86_400_000;
    const recencyScore = Math.pow(0.5, ageDays / 7);

    const finalScore =
      options.relevanceWeight * relevanceScore +
      options.recencyWeight * recencyScore +
      0.1 * c.metadata.importance;

    return { ...c, finalScore };
  });

  scored.sort((a, b) => b.finalScore - a.finalScore);
  return scored.slice(0, options.limit).map((s) => s.memory);
}

特性: 數量多、有時間維度、會衰減、用 relevance + recency 加權檢索。

3. Semantic Memory:事實知識

Semantic Memory 存的是世界是什麼樣的——去脈絡化的事實和概念。

人類的 Semantic Memory 是「巴黎是法國首都」「水的沸點是 100 度」。對 AI Agent 來說:

  • 「使用者的公司使用 Next.js + TypeScript + Tailwind」
  • 「使用者的名字是 Kevin,職位是前端工程師」
  • 「專案資料庫是 PostgreSQL 14,部署在 AWS」
interface SemanticMemory {
  id: string;
  type: 'semantic';
  fact: string;
  category: string;         // personal, technical, project...
  confidence: number;
  source: 'user_stated' | 'inferred' | 'verified';
  embedding: number[];
  createdAt: Date;
  updatedAt: Date;
  supersededBy?: string;    // 被更新的資訊取代時指向新 ID
}

Semantic Memory 的檢索主要靠向量相似度,但要排除已被取代的舊記憶:

async function retrieveSemanticMemories(
  query: string,
  userId: string,
  vectorStore: VectorStore,
  options: { limit?: number; category?: string } = {},
): Promise<SemanticMemory[]> {
  const results = await vectorStore.search({
    query,
    filter: {
      userId,
      type: 'semantic',
      supersededBy: null,  // 排除已被取代的
      ...(options.category ? { category: options.category } : {}),
    },
    limit: options.limit ?? 10,
  });

  return results.map((r) => r.memory);
}

特性: 中等數量、不衰減但可過時、用 similarity 檢索、需要 supersede 機制。

三種記憶的比較

              Procedural        Episodic           Semantic
────────────────────────────────────────────────────────────
存什麼        行為規則           時間事件            事實知識
像什麼        肌肉記憶           個人日記            百科全書
範例          「用正式語氣」     「3/15 提到旅行」    「用 Next.js」
注入方式      system prompt      context injection   context injection
檢索策略      全部載入           relevance + recency  similarity
更新頻率      低                 高                  中
衰減          不衰減             會衰減              不衰減(但可過時)

記憶的寫入機制

記憶系統最關鍵的設計決策:什麼時候該寫入記憶? 寫太少,Agent 學不到東西;寫太多,記憶庫充斥雜訊。

顯式寫入 vs 隱式學習

顯式寫入:使用者明確要求。

使用者: 記住,我所有專案都用 pnpm,不要建議我用 npm
使用者: 我不喜歡你用 emoji,以後回覆不要加

隱式學習:Agent 從對話行為中推斷。

使用者: (連續三次把 Agent 的回覆從中文改成英文)
→ 推斷:使用者可能偏好英文回覆

使用者: 我下個月要去日本出差
→ 推斷:這是一個值得記住的未來事件

記憶提取器

在每次對話結束後,用一個獨立的 LLM 呼叫來分析對話、提取記憶:

interface MemoryExtraction {
  type: 'procedural' | 'episodic' | 'semantic';
  content: string;
  importance: number;
  source: 'explicit' | 'inferred';
  confidence: number;
  category?: string;
}

const EXTRACTION_PROMPT = `你是記憶提取系統。分析對話,提取長期有價值的資訊。

分三類:
1. **Procedural**: 偏好、規則、行為模式
2. **Episodic**: 具體事件、計畫、經歷(帶有時間性)
3. **Semantic**: 事實知識(技術棧、個人資訊、專案資訊)

規則:
- 只提取有長期價值的資訊,忽略瑣碎對話
- 顯式請求(「記住...」)的 confidence 為 1.0
- 推斷記憶 confidence 在 0.5-0.9 之間
- 沒有值得記的就回傳空陣列

回傳 JSON 陣列。`;

async function extractMemories(
  conversation: Message[],
  existingMemories: Memory[],
  llm: LLMClient,
): Promise<MemoryExtraction[]> {
  const existingContext = existingMemories
    .map((m) => `- [${m.type}] ${m.content}`)
    .join('\n');

  const conversationText = conversation
    .map((msg) => `${msg.role}: ${msg.content}`)
    .join('\n');

  const response = await llm.generate({
    system: `${EXTRACTION_PROMPT}\n\n已存在的記憶(避免重複):\n${existingContext}`,
    user: conversationText,
    responseFormat: 'json',
  });

  const extractions: MemoryExtraction[] = JSON.parse(response);
  return extractions.filter((e) => e.confidence >= 0.5);
}

寫入流程

提取後還需要去重和衝突檢查:

async function processMemoryWrites(
  extractions: MemoryExtraction[],
  userId: string,
  memoryStore: AgentMemorySystem,
  vectorStore: VectorStore,
): Promise<void> {
  for (const extraction of extractions) {
    // 檢查是否與既有記憶重複或衝突
    const similar = await vectorStore.search({
      query: extraction.content,
      filter: { userId },
      limit: 3,
      minScore: 0.8,
    });

    if (similar.length > 0 && similar[0].score > 0.95) {
      continue; // 幾乎相同,跳過
    }

    if (similar.length > 0 && similar[0].score > 0.8) {
      // 高度相似但不完全相同,可能是更新
      await handleMemoryUpdate(similar[0], extraction, memoryStore);
      continue;
    }

    // 建立新記憶
    const embedding = await vectorStore.embed(extraction.content);
    await memoryStore.store({
      type: extraction.type,
      content: extraction.content,
      importance: extraction.importance,
      metadata: {
        source: extraction.source,
        confidence: extraction.confidence,
        category: extraction.category,
      },
    }, userId);
  }
}

記憶的檢索與注入

寫入只是一半的工作。另一半是在正確的時機正確的記憶注入 context window。

統一檢索介面

三種記憶用不同策略檢索,最後合併排序:

class UnifiedMemoryRetriever {
  constructor(
    private vectorStore: VectorStore,
    private metadataStore: MetadataStore,
  ) {}

  async retrieve(query: string, userId: string, limit = 20): Promise<RetrievalResult[]> {
    // 並行檢索三種記憶
    const [procedural, episodic, semantic] = await Promise.all([
      this.loadProceduralMemories(userId),
      this.searchEpisodicMemories(query, userId, limit),
      this.searchSemanticMemories(query, userId, limit),
    ]);

    const all = [...procedural, ...episodic, ...semantic];
    all.sort((a, b) => b.score - a.score);
    return all.slice(0, limit);
  }

  private async loadProceduralMemories(userId: string): Promise<RetrievalResult[]> {
    const memories = await this.metadataStore.find({ userId, type: 'procedural', active: true });
    return memories.map((m) => ({ memory: m, score: 1.0, source: 'procedural' as const }));
  }

  private async searchEpisodicMemories(
    query: string, userId: string, limit: number,
  ): Promise<RetrievalResult[]> {
    const candidates = await this.vectorStore.search({
      query, filter: { userId, type: 'episodic' }, limit: limit * 2,
    });

    const now = Date.now();
    return candidates.map((c) => {
      const ageDays = (now - new Date(c.metadata.timestamp).getTime()) / 86_400_000;
      const recencyScore = Math.pow(0.5, ageDays / 7);
      return {
        memory: c.memory,
        score: 0.6 * c.score + 0.3 * recencyScore + 0.1 * c.metadata.importance,
        source: 'episodic' as const,
      };
    });
  }

  private async searchSemanticMemories(
    query: string, userId: string, limit: number,
  ): Promise<RetrievalResult[]> {
    const results = await this.vectorStore.search({
      query, filter: { userId, type: 'semantic', supersededBy: null }, limit,
    });
    return results.map((r) => ({
      memory: r.memory, score: r.score, source: 'semantic' as const,
    }));
  }
}

Context 注入

不同類型的記憶注入不同位置:

class MemoryAwareContextBuilder {
  build(params: {
    baseSystemPrompt: string;
    memories: RetrievalResult[];
    conversation: Message[];
  }): { systemPrompt: string; messages: Message[] } {
    const { baseSystemPrompt, memories, conversation } = params;

    const procedural = memories.filter((m) => m.source === 'procedural');
    const episodic = memories.filter((m) => m.source === 'episodic');
    const semantic = memories.filter((m) => m.source === 'semantic');

    // Procedural → system prompt
    let systemPrompt = baseSystemPrompt;
    if (procedural.length > 0) {
      systemPrompt += '\n\n## 使用者偏好\n\n';
      systemPrompt += procedural.map((m) => `- ${m.memory.content}`).join('\n');
    }

    // Episodic + Semantic → 使用者訊息前的 context block
    const contextParts: string[] = [];

    if (episodic.length > 0) {
      contextParts.push('## 相關過往互動\n');
      contextParts.push(episodic.map((m) => {
        const date = new Date(m.memory.metadata.timestamp as string).toISOString().split('T')[0];
        return `- [${date}] ${m.memory.content}`;
      }).join('\n'));
    }

    if (semantic.length > 0) {
      contextParts.push('\n## 已知資訊\n');
      contextParts.push(semantic.map((m) => `- ${m.memory.content}`).join('\n'));
    }

    const messages = [...conversation];
    if (contextParts.length > 0) {
      const lastMsg = messages[messages.length - 1];
      if (lastMsg.role === 'user') {
        lastMsg.content = `<memory_context>\n${contextParts.join('\n')}\n</memory_context>\n\n${lastMsg.content}`;
      }
    }

    return { systemPrompt, messages };
  }
}

排序策略

三個信號的加權組合。不同場景用不同權重:

const SCORING_PROFILES: Record<string, { relevance: number; recency: number; importance: number }> = {
  default:       { relevance: 0.5, recency: 0.3, importance: 0.2 },
  task:          { relevance: 0.7, recency: 0.1, importance: 0.2 },  // 寫程式
  casual:        { relevance: 0.3, recency: 0.5, importance: 0.2 },  // 閒聊
  retrospective: { relevance: 0.2, recency: 0.6, importance: 0.2 },  // 「上次聊什麼」
};

記憶管理

記憶庫不是只寫不管的。隨著時間累積,記憶會重複、衝突、過時。

1. 去重(Deduplication)

純向量相似度不夠——「使用者用 React」和「使用者的前端框架是 React 18」語意很接近但資訊量不同。需要 LLM 判斷:

const DEDUP_PROMPT = `比較兩條記憶,判斷關係:

記憶 A: {memoryA}
記憶 B: {memoryB}

回傳 JSON:
- "DUPLICATE": 完全相同,保留任一即可
- "MERGE": 部分重疊,應合併(提供合併文字)
- "UPDATE": B 是 A 的更新版本,應取代 A
- "DISTINCT": 不同資訊,兩條都保留`;

async function smartDeduplicate(
  memoryA: string,
  memoryB: string,
  llm: LLMClient,
): Promise<{ relation: 'DUPLICATE' | 'MERGE' | 'UPDATE' | 'DISTINCT'; mergedContent?: string }> {
  const prompt = DEDUP_PROMPT.replace('{memoryA}', memoryA).replace('{memoryB}', memoryB);
  const response = await llm.generate({ system: prompt, user: 'Analyze.', responseFormat: 'json' });
  return JSON.parse(response);
}

2. 衝突解決(Conflict Resolution)

當新記憶和舊記憶矛盾時,遵循優先順序:

async function resolveConflict(
  existing: Memory,
  incoming: MemoryExtraction,
  memoryStore: AgentMemorySystem,
): Promise<void> {
  // 規則 1: 顯式 > 推斷(使用者直接說的 > Agent 猜的)
  if (incoming.source === 'explicit' && existing.metadata.source === 'inferred') {
    await supersede(existing.id, incoming, memoryStore);
    return;
  }

  // 規則 2: 新的顯式 > 舊的顯式(使用者改變了偏好)
  if (incoming.source === 'explicit' && existing.metadata.source === 'explicit') {
    await supersede(existing.id, incoming, memoryStore);
    return;
  }

  // 規則 3: 推斷 vs 推斷 → 保留既有,除非新的信心度明顯更高
  if (incoming.confidence > (existing.metadata.confidence as number) + 0.2) {
    await supersede(existing.id, incoming, memoryStore);
  }
}

async function supersede(
  oldId: string,
  incoming: MemoryExtraction,
  memoryStore: AgentMemorySystem,
): Promise<void> {
  const newId = await memoryStore.store({
    type: incoming.type,
    content: incoming.content,
    importance: incoming.importance,
    metadata: { source: incoming.source, confidence: incoming.confidence },
  }, 'current_user');

  await memoryStore.update(oldId, { metadata: { supersededBy: newId } });
}

3. 衰減與過期(Decay & Expiration)

Episodic Memory 會隨時間衰減。如果很久沒被存取且重要度不高,就該被降級或刪除:

interface DecayConfig {
  halfLifeDays: number;
  minScore: number;
  importanceShield: number;  // 重要度高於此值不衰減
  accessBoostDays: number;   // 每次存取延壽天數
}

async function runDecayCycle(
  userId: string,
  memoryStore: AgentMemorySystem,
  config: DecayConfig = { halfLifeDays: 30, minScore: 0.1, importanceShield: 0.9, accessBoostDays: 7 },
): Promise<{ decayed: number; deleted: number }> {
  const memories = await memoryStore.listByUser(userId);
  let decayed = 0, deleted = 0;
  const now = Date.now();

  for (const memory of memories) {
    if (memory.type === 'procedural') continue;
    if (memory.importance >= config.importanceShield) continue;

    const accessBoostMs = memory.accessCount * config.accessBoostDays * 86_400_000;
    const effectiveAgeDays = Math.max(0,
      (now - memory.lastAccessedAt.getTime() - accessBoostMs) / 86_400_000,
    );

    const decayScore = Math.pow(0.5, effectiveAgeDays / config.halfLifeDays);

    if (decayScore < config.minScore) {
      await memoryStore.delete(memory.id);
      deleted++;
    } else if (decayScore < 0.5) {
      await memoryStore.update(memory.id, { metadata: { ...memory.metadata, decayScore } });
      decayed++;
    }
  }

  return { decayed, deleted };
}

4. 使用者控制(User Control)

使用者必須能查看、修改和刪除自己的記憶。這不只是功能需求,也是信任和隱私的問題:

class MemoryDashboard {
  constructor(private memoryStore: AgentMemorySystem) {}

  async listAll(userId: string) {
    const all = await this.memoryStore.listByUser(userId);
    return {
      procedural: all.filter((m) => m.type === 'procedural'),
      episodic: all.filter((m) => m.type === 'episodic'),
      semantic: all.filter((m) => m.type === 'semantic'),
    };
  }

  async forget(memoryId: string): Promise<void> {
    await this.memoryStore.delete(memoryId);
  }

  async forgetAllByType(userId: string, type: Memory['type']): Promise<number> {
    const all = await this.memoryStore.listByUser(userId);
    const toDelete = all.filter((m) => m.type === type);
    await Promise.all(toDelete.map((m) => this.memoryStore.delete(m.id)));
    return toDelete.length;
  }

  async exportAll(userId: string): Promise<string> {
    const all = await this.memoryStore.listByUser(userId);
    return JSON.stringify(all, null, 2);
  }

  async purgeAll(userId: string): Promise<void> {
    const all = await this.memoryStore.listByUser(userId);
    await Promise.all(all.map((m) => this.memoryStore.delete(m.id)));
  }
}

實作架構

把所有元件組合起來。

架構概覽

┌─────────────────────────────────────────────────────┐
│                   Agent Runtime                      │
│                                                     │
│  ┌─────────────┐     ┌──────────────────────────┐  │
│  │ LLM Engine  │◄────│ Memory-Aware Context     │  │
│  │             │     │ Builder                   │  │
│  └─────────────┘     └──────────┬───────────────┘  │
│                                  │                   │
│                        ┌─────────┴──────────┐       │
│                        │                    │       │
│                ┌───────▼──────┐    ┌────────▼────┐  │
│                │ Memory       │    │ Memory      │  │
│                │ Retriever    │    │ Extractor   │  │
│                │ (Read Path)  │    │ (Write Path)│  │
│                └───────┬──────┘    └────────┬────┘  │
│                        │                    │       │
└────────────────────────┼────────────────────┼───────┘
                         │                    │
              ┌──────────▼────────────────────▼──────┐
              │          Memory Store                 │
              │                                      │
              │  ┌────────────┐  ┌────────────────┐  │
              │  │ Vector DB  │  │ Metadata Store │  │
              │  │ (Qdrant /  │  │ (PostgreSQL /  │  │
              │  │  Pinecone) │  │  Redis)        │  │
              │  └────────────┘  └────────────────┘  │
              └──────────────────────────────────────┘

Agent 主迴圈整合

class MemoryEnabledAgent {
  constructor(
    private memory: AgentMemory,
    private contextBuilder: MemoryAwareContextBuilder,
    private llm: LLMClient,
  ) {}

  async chat(userId: string, conversation: Message[], userMessage: string): Promise<string> {
    // 1. 檢索相關記憶
    const memories = await this.memory.retrieve(userMessage, userId);

    const retrievalResults: RetrievalResult[] = memories.map((m) => ({
      memory: m, score: 1.0, source: m.type,
    }));

    // 2. 建構 memory-aware context
    const updatedConversation = [...conversation, { role: 'user' as const, content: userMessage }];
    const context = this.contextBuilder.build({
      baseSystemPrompt: 'You are a helpful assistant with persistent memory.',
      memories: retrievalResults,
      conversation: updatedConversation,
    });

    // 3. 呼叫 LLM
    const response = await this.llm.generate({
      system: context.systemPrompt,
      user: context.messages.filter((m) => m.role === 'user').pop()?.content ?? userMessage,
    });

    // 4. 非同步提取並儲存新記憶(不阻塞回應)
    const fullConversation = [
      ...updatedConversation,
      { role: 'assistant' as const, content: response },
    ];

    this.memory
      .extractAndStore(fullConversation, userId)
      .catch((err) => console.error('Memory extraction failed:', err));

    return response;
  }
}

使用範例

// 初始化
const vectorStore = new QdrantVectorStore({ url: 'http://localhost:6333' });
const metadataStore = new PostgresMetadataStore({ connectionString: '...' });
const llm = new AnthropicClient({ apiKey: process.env.ANTHROPIC_API_KEY });

const memory = new AgentMemory(vectorStore, metadataStore, llm);
const contextBuilder = new MemoryAwareContextBuilder();
const agent = new MemoryEnabledAgent(memory, contextBuilder, llm);

// 第一次對話
await agent.chat('user_123', [], '我是 Kevin,技術棧是 Next.js + TypeScript');
// → Agent 回覆,同時背景記住:
//   [semantic] 使用者名字是 Kevin
//   [semantic] 技術棧:Next.js + TypeScript

// 第二次對話(新 session)
await agent.chat('user_123', [], '幫我寫一個 API route');
// → Agent 已知道你用 Next.js + TypeScript,直接生成對應程式碼

// 第三次對話
await agent.chat('user_123', [], '下週二要去東京出差');
// → 背景記住:[episodic] 使用者下週二要去東京出差

// 第四次對話
await agent.chat('user_123', [], '幫我準備一下出差的東西');
// → Agent 記得你要去東京,直接提供東京相關建議

設計取捨

什麼時候不需要 Agent Memory?

  • 一次性工具型 Agent:每次都是獨立任務(翻譯、格式轉換)
  • 高敏感場景:醫療、法律等不應依賴推斷記憶的領域
  • 成本敏感:記憶提取和檢索會增加延遲和費用

Memory 的風險

風險說明解法
幻覺記憶LLM 推斷出錯誤偏好Confidence 門檻 + 主動確認
過時資訊使用者換了技術棧衰減機制 + supersede
隱私問題記住不該記的使用者控制面板
偏見放大錯誤推斷自我強化定期審查 + 低信心度不儲存

Token 預算

Context window 有限,記憶注入需要上限:

const TOKEN_BUDGET = { procedural: 500, episodic: 1000, semantic: 1000 };

function trimMemoriesToBudget(
  memories: RetrievalResult[],
  budget: Record<string, number>,
  tokenCounter: (text: string) => number,
): RetrievalResult[] {
  const result: RetrievalResult[] = [];
  const used: Record<string, number> = { procedural: 0, episodic: 0, semantic: 0 };

  for (const memory of memories) {
    const tokens = tokenCounter(memory.memory.content);
    const type = memory.source;
    if (used[type] + tokens <= (budget[type] ?? 500)) {
      result.push(memory);
      used[type] += tokens;
    }
  }
  return result;
}

總結

Agent Memory 的核心轉變是從 read-onlyread-write

RAG 讓 Agent 能讀取外部知識,Agentic RAG 讓它讀得更聰明,但 Agent Memory 讓它能學習和累積。三種記憶類型——Procedural(怎麼做)、Episodic(發生了什麼)、Semantic(世界是什麼樣的)——構成了一個完整的認知記憶系統。

實作上的四個關鍵決策:

  1. 寫入時機:顯式請求一定寫,隱式推斷要有信心度門檻
  2. 檢索策略:Procedural 全載入,Episodic 用 relevance + recency,Semantic 用 similarity
  3. 記憶管理:去重、衝突解決、衰減、使用者控制缺一不可
  4. Token 預算:Context window 有限,記憶注入需要有上限

最後一個想法:記憶不只是技術問題,也是產品問題。使用者對「AI 記住了什麼」這件事非常敏感。做好透明度和控制權,比做好檢索演算法更重要。

參考資料