Chuyển đến nội dung chính

第 19 課:個人化與長期記憶-使用者分析、偏好學習與記憶鞏固

使用者分析、偏好學習、情境個人化、長期記憶架構、記憶鞏固、遺忘策略、隱私意識個人化。

🏗️ 建築 — 第 19 課 第 19 課:個人化與長期記憶 — 使用者分析、偏好學習 & 記憶鞏固

企業人工智慧聊天機器人平台架構-從原型到生產

第 5 部分:多通路與規模

亞洲開發網

1. 長期記憶架構

人們透過許多對話記住上下文——聊天機器人也需要這一點。長期記憶有助於聊天機器人 記住偏好、歷史、關係 跨會話的使用者數。


┌──────────── MEMORY ARCHITECTURE ──────────────────────┐
│                                                        │
│  ┌────────────────────────────────────────────────┐    │
│  │               SHORT-TERM MEMORY                │    │
│  │  (Current conversation — Redis, max 20 turns)  │    │
│  └────────────────────┬───────────────────────────┘    │
│                       │ consolidate                    │
│  ┌────────────────────▼───────────────────────────┐    │
│  │              WORKING MEMORY                    │    │
│  │  (Session buffer — entity extraction,          │    │
│  │   preference tracking, intent patterns)        │    │
│  └────────────────────┬───────────────────────────┘    │
│                       │ persist                        │
│  ┌────────────────────▼───────────────────────────┐    │
│  │              LONG-TERM MEMORY                  │    │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────────┐   │    │
│  │  │Episodic  │ │Semantic  │ │ Procedural   │   │    │
│  │  │(Events)  │ │(Facts)   │ │(Preferences) │   │    │
│  │  └──────────┘ └──────────┘ └──────────────┘   │    │
│  │  PostgreSQL + pgvector                        │    │
│  └────────────────────────────────────────────────┘    │
└────────────────────────────────────────────────────────┘

2. 內存類型和存儲


interface MemoryEntry {
  id: string;
  userId: string;
  tenantId: string;
  type: 'episodic' | 'semantic' | 'procedural';
  content: string;
  embedding: number[];
  importance: number;        // 0.0 - 1.0
  accessCount: number;       // How many times retrieved
  lastAccessed: Date;
  createdAt: Date;
  expiresAt?: Date;          // Optional TTL
  source: {
    conversationId: string;
    messageIndex: number;
  };
  metadata: Record<string, unknown>;
}

// Episodic: specific events/interactions
// "Đã hỏi về đơn hàng ORD-12345 ngày 15/01, đơn bị delay"
// "Đã được hướng dẫn đổi mật khẩu thành công"

// Semantic: facts about the user
// "Tên: Nguyễn Văn A"
// "Thường mua sản phẩm category 'điện tử'"
// "Sử dụng tiếng Việt, thỉnh thoảng mix English"

// Procedural: preferences & patterns
// "Thích response ngắn gọn, không dài dòng"
// "Hay hỏi vào buổi tối (20h-22h)"
// "Muốn nhận thông báo qua Zalo, không email"

3. 記憶提取-從對話中學習


class MemoryExtractor {
  async extractMemories(
    conversation: CompletedConversation,
  ): Promise<MemoryEntry[]> {
    const memories: MemoryEntry[] = [];

    // 1. Extract via LLM
    const response = await this.llm.chat({
      messages: [{
        role: 'system',
        content: `Extract memorable information from this conversation.

For each memory, classify:
- type: episodic (events), semantic (facts about user), procedural (preferences)
- importance: 0.0-1.0 (how important to remember)
- content: concise statement

ONLY extract information that would be useful in future conversations.
Do NOT extract: generic greetings, system messages, or trivial details.

Output JSON: {"memories": [{"type": "...", "importance": 0.X, "content": "..."}]}`,
      }, {
        role: 'user',
        content: JSON.stringify(conversation.messages),
      }],
      response_format: { type: 'json_object' },
      model: 'gpt-4o-mini',
    });

    const extracted = JSON.parse(response.content);

    for (const mem of extracted.memories) {
      // Skip low-importance memories
      if (mem.importance < 0.3) continue;

      // Check for duplicate/conflicting memories
      const existing = await this.findSimilarMemories(
        conversation.userId,
        mem.content,
      );

      if (existing.length > 0 && existing[0].similarity > 0.9) {
        // Update existing memory instead of creating duplicate
        await this.updateMemory(existing[0].id, {
          accessCount: existing[0].accessCount + 1,
          lastAccessed: new Date(),
        });
        continue;
      }

      // Handle contradictions
      if (existing.length > 0 && existing[0].similarity > 0.7) {
        const isContradiction = await this.checkContradiction(
          existing[0].content,
          mem.content,
        );
        if (isContradiction) {
          // Newer info takes precedence — archive old
          await this.archiveMemory(existing[0].id, 'superseded');
        }
      }

      const embedding = await this.embedder.embed(mem.content);
      memories.push({
        id: crypto.randomUUID(),
        userId: conversation.userId,
        tenantId: conversation.tenantId,
        type: mem.type,
        content: mem.content,
        embedding,
        importance: mem.importance,
        accessCount: 0,
        lastAccessed: new Date(),
        createdAt: new Date(),
        source: {
          conversationId: conversation.id,
          messageIndex: 0,
        },
        metadata: {},
      });
    }

    return memories;
  }
}

4. 上下文感知記憶體檢索


class MemoryRetriever {
  async getRelevantMemories(
    userId: string,
    tenantId: string,
    currentMessage: string,
    maxMemories: number = 5,
  ): Promise<MemoryEntry[]> {
    const queryEmbedding = await this.embedder.embed(currentMessage);

    // Hybrid retrieval: semantic similarity + recency + importance
    const candidates = await this.vectorStore.search({
      vector: queryEmbedding,
      filter: { userId, tenantId },
      topK: maxMemories * 3, // Over-fetch for re-ranking
    });

    // Re-rank with combined scoring
    const scored = candidates.map(candidate => {
      const memory = candidate.metadata as MemoryEntry;
      const semanticScore = candidate.score;
      const recencyScore = this.recencyDecay(memory.lastAccessed);
      const importanceScore = memory.importance;
      const accessScore = Math.min(memory.accessCount / 10, 1.0);

      const combined = (
        semanticScore * 0.4 +
        recencyScore * 0.2 +
        importanceScore * 0.3 +
        accessScore * 0.1
      );

      return { memory, score: combined };
    });

    // Return top-K
    const topMemories = scored
      .sort((a, b) => b.score - a.score)
      .slice(0, maxMemories)
      .map(s => s.memory);

    // Update access counts
    await Promise.all(
      topMemories.map(m =>
        this.updateMemory(m.id, {
          accessCount: m.accessCount + 1,
          lastAccessed: new Date(),
        }),
      ),
    );

    return topMemories;
  }

  private recencyDecay(lastAccessed: Date): number {
    const daysSince = (Date.now() - lastAccessed.getTime()) / (24 * 3600 * 1000);
    return Math.exp(-daysSince / 30); // Half-life: ~21 days
  }

  // Format memories for system prompt injection
  formatForPrompt(memories: MemoryEntry[]): string {
    if (memories.length === 0) return '';

    const grouped = {
      semantic: memories.filter(m => m.type === 'semantic'),
      episodic: memories.filter(m => m.type === 'episodic'),
      procedural: memories.filter(m => m.type === 'procedural'),
    };

    let prompt = '## What you remember about this user:\n';

    if (grouped.semantic.length) {
      prompt += '\nFacts:\n';
      grouped.semantic.forEach(m => { prompt += `- ${m.content}\n`; });
    }

    if (grouped.episodic.length) {
      prompt += '\nPast interactions:\n';
      grouped.episodic.forEach(m => { prompt += `- ${m.content}\n`; });
    }

    if (grouped.procedural.length) {
      prompt += '\nPreferences:\n';
      grouped.procedural.forEach(m => { prompt += `- ${m.content}\n`; });
    }

    return prompt;
  }
}

5. 記憶鞏固與遺忘


class MemoryConsolidator {
  // Run periodically (daily cron)
  async consolidate(tenantId: string): Promise<ConsolidationReport> {
    const report = { merged: 0, archived: 0, deleted: 0 };

    // 1. Merge similar memories
    const users = await this.db.memory.distinctUsers(tenantId);

    for (const userId of users) {
      const memories = await this.db.memory.findAll({ userId, tenantId });

      // Find clusters of similar memories
      const clusters = await this.clusterMemories(memories);

      for (const cluster of clusters) {
        if (cluster.length <= 1) continue;

        // Merge into single consolidated memory
        const merged = await this.mergeCluster(cluster);
        await this.db.memory.create(merged);
        await this.db.memory.deleteMany(cluster.map(m => m.id));
        report.merged += cluster.length;
      }
    }

    // 2. Archive old, low-importance, rarely-accessed memories
    const staleMemories = await this.db.memory.findMany({
      where: {
        tenantId,
        lastAccessed: { lt: new Date(Date.now() - 90 * 24 * 3600 * 1000) },
        importance: { lt: 0.3 },
        accessCount: { lt: 2 },
      },
    });

    for (const memory of staleMemories) {
      await this.archiveMemory(memory.id);
      report.archived++;
    }

    // 3. Hard delete very old archived memories (GDPR compliance)
    const expiredMemories = await this.db.memory.findMany({
      where: {
        tenantId,
        status: 'archived',
        archivedAt: { lt: new Date(Date.now() - 365 * 24 * 3600 * 1000) },
      },
    });

    for (const memory of expiredMemories) {
      await this.db.memory.delete(memory.id);
      await this.vectorStore.delete(memory.id);
      report.deleted++;
    }

    return report;
  }

  private async mergeCluster(memories: MemoryEntry[]): Promise<MemoryEntry> {
    const mergedContent = await this.llm.chat({
      messages: [{
        role: 'system',
        content: 'Merge these related memory entries into a single, concise statement. Keep the most recent/important information. Output just the merged text.',
      }, {
        role: 'user',
        content: memories.map(m => `- ${m.content} (importance: ${m.importance})`).join('\n'),
      }],
      model: 'gpt-4o-mini',
    });

    return {
      ...memories[0],
      id: crypto.randomUUID(),
      content: mergedContent.content,
      importance: Math.max(...memories.map(m => m.importance)),
      accessCount: memories.reduce((sum, m) => sum + m.accessCount, 0),
      embedding: await this.embedder.embed(mergedContent.content),
    };
  }
}

6. 隱私感知記憶體(GDPR 合規性)


class MemoryPrivacyManager {
  // User right to be forgotten
  async deleteAllUserMemories(userId: string, tenantId: string): Promise<void> {
    // Delete from DB
    await this.db.memory.deleteMany({ where: { userId, tenantId } });

    // Delete from vector store
    await this.vectorStore.deleteByFilter({ userId, tenantId });

    // Audit log
    await this.auditLog.log({
      action: 'memory_erasure',
      userId,
      tenantId,
      timestamp: new Date(),
      reason: 'user_request',
    });
  }

  // User can view what bot remembers
  async getUserMemories(userId: string, tenantId: string): Promise<MemoryEntry[]> {
    return this.db.memory.findMany({
      where: { userId, tenantId, status: 'active' },
      orderBy: { createdAt: 'desc' },
    });
  }

  // User can delete specific memories
  async deleteMemory(memoryId: string, userId: string): Promise<void> {
    const memory = await this.db.memory.findUnique({ where: { id: memoryId } });
    if (memory?.userId !== userId) throw new ForbiddenError();

    await this.db.memory.delete(memoryId);
    await this.vectorStore.delete(memoryId);
  }
}

第 19 課總結

  • 3 種記憶體類型:情境(事件)、語意(事實)、程序(偏好)
  • 萃取:LLM從對話中提取記憶,去重+矛盾處理
  • 檢索:混合評分=語意(40%)+新近度(20%)+重要性(30%)+訪問(10%)
  • 整合:每日 cron — 合併相似、存檔過時、刪除過期(GDPR 1 年)
  • 隱私:使用者可以查看/刪除記憶-被遺忘權合規性

下一篇: 特定領域的人工智慧——醫療保健、金融、法律聊天機器人合規性、領域知識微調。