Agent 的记忆系统怎么设计?面试官想听的是这个

0 阅读33分钟

第八章:Agent 记忆与状态管理

记忆是智能的核心特征。一个没有记忆的 Agent,每次交互都面对全新世界;具备良好记忆系统的 Agent,能记住用户偏好、学习历史模式、保持跨会话上下文连贯。本章从记忆类型划分到存储设计,从压缩策略到遗忘机制,给出一个完整的记忆架构设计。8.png

8.1 Agent 记忆类型:短期、长期与工作记忆

AI Agent 的记忆系统借鉴了认知科学中感觉记忆、短期记忆和长期记忆的分类框架,但根据自身特点做了调整和扩展。

三种核心记忆类型

Agent 的记忆通常分为三种类型:短期记忆(Short-term Memory)、长期记忆(Long-term Memory)和工作记忆(Working Memory)。这三种记忆各自承担不同的职能,协同构成完整的记忆体系。

短期记忆对应 Agent 在当前会话中的上下文窗口。最近的消息历史保存在这里,访问速度快、信息完整度高,但容量受限于 LLM (Large Language Model, 大语言模型) 的上下文窗口大小。一旦会话结束或上下文溢出,信息就丢失了。

长期记忆是跨会话持久化保存的信息,存储用户偏好、历史决策、学习到的知识等。不受会话边界限制,任意时刻可检索调用,容量大、持久性强,但访问需要经过检索过程,速度相对较慢。

工作记忆是 Agent 在执行具体任务时临时维护的一组相关信息。比如多步推理时需要记住中间结果、待办事项和当前进度。工作记忆可以是短期记忆的子集,也可以是从长期记忆中检索出的信息与当前上下文的临时组合。

记忆类型的对比

维度短期记忆工作记忆长期记忆
生命周期单次会话单次任务跨会话持久
存储位置上下文窗口临时变量/缓存数据库/文件系统
访问速度最快(直接可用)快(内存访问)较慢(需检索)
容量限制受 token 限制受内存限制理论上无限制
典型内容对话历史任务中间状态用户偏好、知识库
持久化否否是

短期记忆的管理挑战

短期记忆管理的核心挑战是上下文窗口有限。GPT-4 的 128K tokens 看似很大,长对话场景下仍然会溢出。常见的处理方式有滑动窗口(保留最近 N 条消息)、摘要压缩(将早期对话压缩为摘要)、选择性保留(只留关键信息),各有优劣,需根据场景选择。

class ShortTermMemory:
    """短期记忆管理器,基于滑动窗口 + 摘要策略"""
    
    def __init__(self, max_messages: int = 50, token_limit: int = 8000):
        self.messages = []          # 完整消息列表
        self.max_messages = max_messages  # 最大保留消息数
        self.token_limit = token_limit    # token 上限
        self.summary = ""           # 早期对话摘要
    
    def add_message(self, role: str, content: str):
        self.messages.append({"role": role, "content": content})
        self._evict_if_needed()
    
    def _evict_if_needed(self):
        """超出容量时,将最旧的消息压缩为摘要"""
        if len(self.messages) > self.max_messages:
            old_msgs = self.messages[:10]
            self.summary = self._compress(old_msgs)
            self.messages = self.messages[10:]
    
    def get_context(self) -> str:
        """获取当前可用的上下文"""
        parts = []
        if self.summary:
            parts.append(f"[Earlier conversation summary]: {self.summary}")
        for msg in self.messages:
            parts.append(f"{msg['role']}: {msg['content']}")
        return "\n".join(parts)

工作记忆的实践意义

工作记忆在复杂任务执行中尤为重要。以代码编辑 Agent 为例,重构大型文件时需要维护:当前修改的函数名、已完成修改点、待修改位置列表、依赖关系图。信息在任务执行中不断更新,任务完成后即丢弃。

工作记忆通常采用结构化格式而非自然语言文本,这样 Agent 可以精确更新和查询特定字段,不用担心自然语言歧义。

class WorkingMemory:
    """工作记忆:任务执行期间的临时状态管理"""
    
    def __init__(self):
        self.task_stack = []        # 任务栈,支持子任务嵌套
        self.facts = {}             # 已知事实
        self.todo = []              # 待办事项
        self.results = {}           # 中间结果
    
    def push_task(self, task: dict):
        self.task_stack.append(task)
    
    def pop_task(self) -> dict:
        return self.task_stack.pop()
    
    def set_fact(self, key: str, value):
        self.facts[key] = value
    
    def get_fact(self, key: str):
        return self.facts.get(key)
    
    def add_todo(self, item: str):
        self.todo.append({"item": item, "done": False})
    
    def complete_todo(self, index: int):
        if 0 <= index < len(self.todo):
            self.todo[index]["done"] = True

三种记忆类型并非孤立存在,而是形成一条信息流动管道:短期记忆中的信息经筛选和压缩后写入长期记忆,长期记忆中的信息在需要时被检索到工作记忆中参与任务执行。理解三者的协作方式,是设计 Agent 记忆系统的基础。

8.2 长期记忆存储设计:向量、KV 与图数据库

长期记忆的存储设计是 Agent 记忆系统中最核心的工程问题。存储方案直接决定记忆的写入速度、检索效率和可维护性。主流方案有三种:向量数据库、KV (Key-Value, 键值对) 存储和图数据库。

向量数据库:语义检索的首选

向量数据库是当前 AI Agent 长期记忆最常用的存储方案。核心思路是将文本转换为高维向量,通过向量相似度检索找到语义相近的记忆。它天然适配 LLM 的语义理解能力,能实现"模糊匹配"——查询词与存储内容字面不同,语义相近就能命中。

工作流程分三步:写入时,文本经 Embedding 模型转为向量连同元数据存入数据库;检索时,查询文本同样转为向量,在数据库中找最相似的 K 条记忆;返回时,将匹配的记忆文本和相似度分数返回给 Agent。

import numpy as np
from dataclasses import dataclass, field
from typing import List, Dict, Optional
import json

@dataclass
class MemoryItem:
    """单条记忆的数据结构"""
    id: str                          # 唯一标识
    content: str                     # 记忆文本内容
    embedding: List[float]           # 向量表示
    metadata: Dict = field(default_factory=dict)  # 元数据
    timestamp: float = 0             # 创建时间戳
    access_count: int = 0            # 访问次数
    importance: float = 0.5          # 重要度评分 [0, 1]


class VectorMemoryStore:
    """基于向量的记忆存储"""
    
    def __init__(self, dim: int = 1536):
        self.dim = dim
        self.memories: Dict[str, MemoryItem] = {}
    
    def add(self, item: MemoryItem):
        self.memories[item.id] = item
    
    def search(self, query_vec: List[float], top_k: int = 5) -> List[MemoryItem]:
        """余弦相似度检索"""
        scores = []
        for mid, mem in self.memories.items():
            sim = self._cosine(query_vec, mem.embedding)
            # 综合相似度、重要度和时间衰减
            final_score = sim * 0.7 + mem.importance * 0.2 + self._recency_bonus(mem.timestamp) * 0.1
            scores.append((final_score, mem))
        scores.sort(key=lambda x: -x[0])
        return [mem for _, mem in scores[:top_k]]
    
    def _cosine(self, a: List[float], b: List[float]) -> float:
        return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-8)
    
    def _recency_bonus(self, ts: float) -> float:
        """时间衰减因子,越新得分越高"""
        import time
        age = time.time() - ts
        return max(0, 1 - age / (86400 * 30))  # 30天线性衰减

KV 存储:精确匹配的利器

KV 存储是最简单的方案。每条记忆以键值对形式存储,键是唯一标识或分类标签,值是记忆内容。读写速度极快,适合结构化的、需要精确匹配的信息。

典型场景:用户偏好设置(如"语言=中文"、“回答风格=简洁”)、事实性知识(如"用户生日=1990-01-01")、配置信息。不需要语义检索,只需精确查找。

class KVMemoryStore:
    """键值对记忆存储,适合结构化信息"""
    
    def __init__(self):
        self.store: Dict[str, any] = {}
    
    def set(self, key: str, value):
        self.store[key] = {
            "value": value,
            "timestamp": time.time(),
            "version": self.store.get(key, {}).get("version", 0) + 1
        }
    
    def get(self, key: str, default=None):
        entry = self.store.get(key)
        return entry["value"] if entry else default
    
    def delete(self, key: str):
        self.store.pop(key, None)
    
    def list_keys(self, prefix: str = "") -> List[str]:
        return [k for k in self.store if k.startswith(prefix)]

图数据库:关系记忆的骨架

图数据库的存储方式与前两者截然不同,以节点和边组织信息——节点代表实体或事件,边代表关系。核心优势是能捕捉记忆之间的关联关系,支持多跳推理。

比如 Agent 要记住"用户在去年圣诞节提到了他的女儿小美"。图数据库中,表示为:用户节点 --(has_child)–> 小美节点,用户节点 --(mentioned_at)–> 圣诞节事件节点。查询"用户家庭成员"时,通过图遍历直接找到关联节点。

@dataclass
class GraphNode:
    """图节点:实体或概念"""
    id: str
    type: str               # person, event, concept, etc.
    properties: Dict = field(default_factory=dict)

@dataclass
class GraphEdge:
    """图边:关系"""
    source: str             # 源节点 ID
    target: str             # 目标节点 ID
    relation: str           # 关系类型
    properties: Dict = field(default_factory=dict)

class GraphMemoryStore:
    """图数据库记忆存储"""
    
    def __init__(self):
        self.nodes: Dict[str, GraphNode] = {}
        self.edges: List[GraphEdge] = []
    
    def add_node(self, node: GraphNode):
        self.nodes[node.id] = node
    
    def add_edge(self, edge: GraphEdge):
        self.edges.append(edge)
    
    def get_neighbors(self, node_id: str, relation: str = None) -> List[GraphNode]:
        """获取邻居节点"""
        neighbors = []
        for edge in self.edges:
            if edge.source == node_id:
                if relation is None or edge.relation == relation:
                    neighbors.append(self.nodes.get(edge.target))
            elif edge.target == node_id:
                if relation is None or edge.relation == relation:
                    neighbors.append(self.nodes.get(edge.source))
        return [n for n in neighbors if n]
    
    def multi_hop_query(self, start_id: str, hops: int = 2) -> List[GraphNode]:
        """多跳查询"""
        visited = {start_id}
        frontier = [start_id]
        for _ in range(hops):
            next_frontier = []
            for nid in frontier:
                for edge in self.edges:
                    other = edge.target if edge.source == nid else (
                        edge.source if edge.target == nid else None
                    )
                    if other and other not in visited:
                        visited.add(other)
                        next_frontier.append(other)
            frontier = next_frontier
        return [self.nodes[nid] for nid in visited if nid in self.nodes]

三种存储方案的对比与选型

维度向量数据库KV 存储图数据库
检索方式语义相似度精确键匹配关系遍历
写入速度中等(需计算向量)最快较慢(需维护关系)
检索精度模糊匹配,适合召回精确匹配关系推理强
适用场景对话历史、知识库用户偏好、配置实体关系、因果推理
典型产品Pinecone, MilvusRedis, DynamoDBNeo4j, NebulaGraph
扩展性好极好一般

实际系统通常不会只用一种方案,而是三者结合:向量数据库负责语义检索,KV 存储负责精确查找,图数据库负责关系推理,通过统一的记忆管理器协调工作。

8.3 Memory Compression:记忆压缩与摘要策略

随着 Agent 运行时间增长,记忆数据量持续膨胀。不加控制的话,存储成本线性增长,检索效率显著下降。记忆压缩将冗长的记忆数据浓缩为高信息密度的摘要,是记忆管理中不可或缺的一环。

为什么需要记忆压缩

压缩需求来自三方面压力:存储成本(完整保存一个月对话量可能达数百 MB)、检索效率(向量检索时间复杂度与数据量正相关)、上下文窗口限制(检索到的记忆太长也无法全部放入 LLM 上下文)。

记忆压缩的本质是信息蒸馏——在尽可能保留关键信息的前提下减少数据体积,要求压缩算法能区分"核心信息"和"可丢弃细节"。

压缩策略分类

记忆压缩分三个层次:消息级压缩针对单条消息,去除罜余信息(如 500 字调试日志压缩为"执行命令 X,报错 Y,通过 Z 修复"),通常在消息写入记忆前实时完成;会话级压缩将数十条消息浓缩为结构化摘要,在会话结束时批量执行,用 LLM 生成高质量摘要;跨会话压缩将多次会话摘要再次压缩合并,形成用户级知识沉淀,频率低但跨度大。

from dataclasses import dataclass
from typing import List
import time

@dataclass
class CompressedMemory:
    """压缩后的记忆结构"""
    id: str
    summary: str                   # 压缩摘要
    source_ids: List[str]          # 原始记忆 ID 列表
    key_entities: List[str]        # 关键实体
    key_topics: List[str]          # 关键主题
    created_at: float              # 压缩时间
    original_size: int             # 原始字数
    compressed_size: int           # 压缩后字数
    
    @property
    def compression_ratio(self) -> float:
        if self.original_size == 0:
            return 0
        return 1 - self.compressed_size / self.original_size


class MemoryCompressor:
    """记忆压缩器"""
    
    COMPRESSION_PROMPT = """请将以下对话历史压缩为简洁的结构化摘要。
要求:
1. 保留所有关键事实、决策和用户偏好
2. 丢弃寒暄、重复内容和无关细节
3. 提取关键实体和主题
4. 输出格式:JSON,包含 summary, key_entities, key_topics 字段

对话历史:
{conversation}
"""
    
    def compress_conversation(self, messages: List[dict]) -> CompressedMemory:
        """压缩一次会话的记忆"""
        # 1. 拼接对话文本
        conv_text = "\n".join(
            f"{m['role']}: {m['content']}" for m in messages
        )
        
        # 2. 调用 LLM 生成压缩摘要
        prompt = self.COMPRESSION_PROMPT.format(conversation=conv_text)
        # result = llm.generate(prompt)  # 实际调用 LLM
        result = {
            "summary": "用户咨询了 Agent 记忆系统设计,讨论了短期和长期记忆方案",
            "key_entities": ["用户", "Agent记忆系统", "短期记忆", "长期记忆"],
            "key_topics": ["记忆架构", "存储设计", "压缩策略"]
        }
        
        original_size = sum(len(m["content"]) for m in messages)
        
        return CompressedMemory(
            id=f"compressed_{int(time.time())}",
            summary=result["summary"],
            source_ids=[m.get("id", str(i)) for i, m in enumerate(messages)],
            key_entities=result["key_entities"],
            key_topics=result["key_topics"],
            created_at=time.time(),
            original_size=original_size,
            compressed_size=len(result["summary"])
        )
    
    def merge_compressed(
        self, memories: List[CompressedMemory]
    ) -> CompressedMemory:
        """合并多条已压缩的记忆(跨会话压缩)"""
        combined_text = "\n---\n".join(m.summary for m in memories)
        # 调用 LLM 再次压缩
        # result = llm.generate(self.COMPRESSION_PROMPT.format(...))
        
        total_original = sum(m.original_size for m in memories)
        return CompressedMemory(
            id=f"merged_{int(time.time())}",
            summary=combined_text[:500],  # 简化示例
            source_ids=[m.id for m in memories],
            key_entities=[],
            key_topics=[],
            created_at=time.time(),
            original_size=total_original,
            compressed_size=len(combined_text[:500])
        )

压缩质量评估

记忆压缩不是简单的文本摘要,要保证压缩后不丢失关键内容。评估压缩质量看三个维度:

信息保留率——压缩后摘要覆盖了多少原始信息。从原始记忆中提取关键事实点,检查是否在摘要中出现,一般要求不低于 80%。

压缩比——原始数据量与压缩后数据量的比值,合理范围 5:1 到 20:1。过高导致信息丢失,过低没有发挥压缩价值。

检索影响度——压缩后记忆在检索时的召回率和准确率不应显著低于原始记忆。如果明显下降,说明压缩策略需调整。

压缩的时机选择

何时触发压缩需要权衡:太频繁增加计算开销(每次压缩都需调用 LLM),太晚导致短期记忆溢出或存储成本过高。常用触发条件包括消息数量阈值(超过 50 条触发会话级压缩)、token 数量阈值(接近上下文窗口 80% 时触发)、时间阈值(每 6 小时一次跨会话压缩)、事件驱动(会话结束时自动触发)。实际系统通常组合使用。

8.4 遗忘机制:有选择地丢弃信息

遗忘和记忆同等重要。不加区分地保留所有信息,存储成本不可控,检索时大量无关记忆会干扰真正有用的信息,"记忆噪音"淹没信号。遗忘机制就是有选择地丢弃低价值信息,让有限资源集中在重要内容上。

遗忘的心理学基础

认知科学中,艾宾浩斯遗忘曲线描述了记忆随时间衰减的规律:没有重复强化的记忆快速遗忘,速度先快后慢。主动抑制理论则认为大脑会主动抑制无关记忆提取以减少干扰。这些理论为 Agent 遗忘机制设计提供了启发。

遗忘策略设计

Agent 的遗忘策略有几种类型。

时间驱动遗忘是最基础的策略。每条记忆有 TTL (Time-to-Live, 生存时间) 属性,超过 TTL 后标记为可遗忘。TTL 长度取决于记忆类型和重要度——日常闲聊 TTL 可能 7 天,用户偏好 365 天,关键决策记录永久不遗忘。

访问频率驱动遗忘借鉴 LRU (Least Recently Used, 最近最少使用) 缓存淘汰思路:长时间未被检索的记忆价值较低,优先遗忘。每条记忆维护 last_accessed 字段,定期清理超时未访问的记忆。

重要度驱动遗忘通过 LLM 或规则引擎对记忆评分,低分优先遗忘。评估维度包括:是否包含用户偏好、是否涉及关键决策、是否被多次引用。

冲突驱动遗忘处理信息更新场景:新记忆与旧记忆矛盾时,旧记忆需更新或遗忘。比如用户先前说"住上海",后来又说"搬到北京了",旧记忆应更新。

遗忘策略触发条件优点缺点适用场景
时间驱动TTL 到期简单可靠不考虑内容价值临时信息、日志
访问频率驱动长期未访问自动适应用户行为可能误删重要但少用的记忆对话历史、知识片段
重要度驱动评分低于阈值内容感知评估成本高所有类型记忆
冲突驱动检测到矛盾信息保持记忆一致性冲突检测复杂事实性记忆
容量驱动超过存储上限保证系统稳定可能删除有用记忆存储受限场景

遗忘机制的代码实现

import time
from dataclasses import dataclass
from typing import List, Optional

@dataclass
class ForgettableMemory:
    """可遗忘的记忆条目"""
    id: str
    content: str
    created_at: float
    last_accessed: float
    access_count: int = 0
    importance: float = 0.5        # [0, 1]
    ttl_seconds: float = 86400 * 30  # 默认 30 天
    tags: List[str] = None

class ForgettingManager:
    """遗忘管理器"""
    
    def __init__(self):
        self.memories: dict = {}
    
    def should_forget(self, mem: ForgettableMemory, 
                      capacity_limit: Optional[int] = None) -> bool:
        """判断一条记忆是否应该被遗忘"""
        now = time.time()
        
        # 策略 1: TTL 过期
        if now - mem.created_at > mem.ttl_seconds:
            if mem.importance < 0.8:  # 高重要度记忆豁免
                return True
        
        # 策略 2: 长期未访问
        idle_days = (now - mem.last_accessed) / 86400
        if idle_days > 14 and mem.access_count < 3:
            return True
        
        # 策略 3: 容量驱动
        if capacity_limit and len(self.memories) > capacity_limit:
            # 当超过容量时,淘汰重要度最低且最久未访问的记忆
            score = self._forget_score(mem)
            if score < 0.3:
                return True
        
        return False
    
    def _forget_score(self, mem: ForgettableMemory) -> float:
        """计算遗忘分数,越低越应该被遗忘"""
        now = time.time()
        recency = max(0, 1 - (now - mem.last_accessed) / (86400 * 30))
        frequency = min(1, mem.access_count / 10)
        return recency * 0.3 + frequency * 0.3 + mem.importance * 0.4
    
    def run_forgetting_cycle(self, capacity_limit: Optional[int] = None):
        """执行一轮遗忘清理"""
        to_remove = []
        for mid, mem in self.memories.items():
            if self.should_forget(mem, capacity_limit):
                to_remove.append(mid)
        
        for mid in to_remove:
            # 遗忘前可以归档而非直接删除
            del self.memories[mid]
        
        return len(to_remove)
    
    def update_on_access(self, memory_id: str):
        """记忆被访问时更新访问记录"""
        if memory_id in self.memories:
            mem = self.memories[memory_id]
            mem.last_accessed = time.time()
            mem.access_count += 1

渐进式遗忘 vs 硬遗忘

遗忘不一定是非黑即白的删除。渐进式遗忘分多个阶段:先降低检索权重,使其不太容易被检索到;再压缩摘要,只保留核心信息;最后才真正删除。好处是后续发现某条记忆有用,还有挽回余地。

硬遗忘是立即删除,适用于明确无用的信息(用户要求删除的隐私数据、明显错误的信息等),不可恢复。

建议默认采用渐进式遗忘,仅在用户明确要求删除或信息被判定为有害时才用硬遗忘。

8.5 跨会话记忆:用户级记忆的持久化

单会话内的记忆管理相对简单,真正的挑战在于跨会话持久化。用户今天上午聊了工作计划,下午来问进度,明天还要继续推进——Agent 必须"记住"之前会话的关键信息,在新会话中正确检索和使用。

跨会话记忆的核心问题

跨会话记忆要解决三个核心问题:信息提取(并非所有对话值得跨会话保留,需筛选值得长期记忆的信息)、信息组织(记忆数量持续增长,需要合理的组织方式保证检索效率)、信息更新(用户情况随时间变化,旧记忆需更新而非简单堆积)。

用户级记忆的数据结构

用户级记忆通常以用户 ID 为命名空间进行隔离,每个用户拥有独立的记忆空间。以下是用户级记忆的典型数据结构。

{
  "user_id": "user_001",
  "memories": [
    {
      "id": "mem_20260801_001",
      "type": "preference",
      "content": "用户偏好简洁的回答风格,不喜欢冗长的解释",
      "embedding": [0.12, -0.34, 0.56, "..."],
      "metadata": {
        "source": "session_20260801",
        "confidence": 0.92,
        "category": "communication_style"
      },
      "created_at": "2026-08-01T10:30:00Z",
      "last_accessed": "2026-08-03T14:00:00Z",
      "access_count": 5,
      "importance": 0.85,
      "version": 1,
      "superseded_by": null
    },
    {
      "id": "mem_20260802_002",
      "type": "fact",
      "content": "用户是一名后端工程师,主要使用 Java 和 Go",
      "embedding": [0.23, -0.45, 0.67, "..."],
      "metadata": {
        "source": "session_20260802",
        "confidence": 0.95,
        "category": "user_profile"
      },
      "created_at": "2026-08-02T09:15:00Z",
      "last_accessed": "2026-08-03T15:30:00Z",
      "access_count": 8,
      "importance": 0.9,
      "version": 1,
      "superseded_by": null
    },
    {
      "id": "mem_20260803_003",
      "type": "event",
      "content": "用户提到正在重构一个支付系统,预计两周完成",
      "embedding": [0.34, -0.56, 0.78, "..."],
      "metadata": {
        "source": "session_20260803",
        "confidence": 0.88,
        "category": "work_project"
      },
      "created_at": "2026-08-03T16:00:00Z",
      "last_accessed": "2026-08-03T16:00:00Z",
      "access_count": 1,
      "importance": 0.7,
      "version": 1,
      "superseded_by": null
    }
  ],
  "memory_index": {
    "by_category": {
      "communication_style": ["mem_20260801_001"],
      "user_profile": ["mem_20260802_002"],
      "work_project": ["mem_20260803_003"]
    },
    "by_importance": {
      "high": ["mem_20260802_002", "mem_20260801_001"],
      "medium": ["mem_20260803_003"],
      "low": []
    }
  },
  "last_updated": "2026-08-03T16:00:00Z",
  "total_count": 3
}

记忆提取流程

从会话中提取值得跨会话保留的信息,通常在会话结束时自动执行,分三步:

原始信息筛选——通过规则或 LLM 评估每条消息是否包含值得长期记忆的信息,筛选标准包括用户偏好、事实性信息、决策计划、情感倾向等。

信息结构化——将筛选出的信息转为结构化记忆条目,提取关键实体、分类标签、重要度评分,通常由 LLM 通过精心设计的 prompt 完成。

记忆写入——将结构化记忆条目写入长期存储并更新索引,发现与已有记忆冲突时触发冲突处理流程。

class CrossSessionMemoryManager:
    """跨会话记忆管理器"""
    
    EXTRACTION_PROMPT = """分析以下对话,提取值得长期记忆的信息。
输出 JSON 数组,每个元素包含:
- type: preference/fact/event/skill/relationship
- content: 记忆内容(简洁描述)
- importance: 重要度 [0, 1]
- category: 分类标签

只提取确实值得长期保留的信息,忽略寒暄和临时性内容。

对话内容:
{conversation}
"""
    
    def extract_memories(self, session_messages: List[dict]) -> List[dict]:
        """从会话消息中提取值得持久化的记忆"""
        conv_text = "\n".join(
            f"{m['role']}: {m['content']}" for m in session_messages
        )
        
        prompt = self.EXTRACTION_PROMPT.format(conversation=conv_text)
        # 调用 LLM 提取记忆
        # extracted = llm.generate(prompt)
        extracted = [
            {
                "type": "preference",
                "content": "用户偏好用中文交流",
                "importance": 0.8,
                "category": "language"
            },
            {
                "type": "fact",
                "content": "用户在使用 PostgreSQL 数据库",
                "importance": 0.7,
                "category": "tech_stack"
            }
        ]
        return extracted
    
    def persist_user_memory(
        self, user_id: str, new_memories: List[dict]
    ):
        """将提取的记忆持久化到用户记忆空间"""
        for mem_data in new_memories:
            # 检查是否与已有记忆冲突
            existing = self._find_conflict(user_id, mem_data)
            if existing:
                # 更新已有记忆
                self._update_memory(existing, mem_data)
            else:
                # 写入新记忆
                self._write_memory(user_id, mem_data)
    
    def load_user_context(self, user_id: str, query: str = "") -> str:
        """在新会话开始时加载用户记忆上下文"""
        if query:
            # 有查询时,基于查询检索相关记忆
            memories = self._semantic_search(user_id, query, top_k=5)
        else:
            # 无查询时,加载高重要度记忆
            memories = self._get_top_memories(user_id, limit=10)
        
        if not memories:
            return ""
        
        lines = [f"[User Memory Context]"]
        for mem in memories:
            lines.append(f"- {mem['content']}")
        return "\n".join(lines)

隐私与安全考量

跨会话记忆涉及用户数据长期存储,隐私和安全必须考虑:记忆存储应加密(特别是个人身份信息),用户应拥有对自身记忆的完全控制权(查看、编辑、删除),记忆数据访问需严格权限控制。

可以引入记忆分级机制:公开级(如语言偏好)、个人级(如工作信息)、敏感级(如健康信息),不同等级采用不同存储和访问策略。敏感级记忆可采用本地存储而非云端。

8.6 记忆检索策略:效率与准确性的平衡

记忆检索是记忆系统中最高频的操作。Agent 每次回忆信息,都要从可能数以万计的记忆条目中找到最相关的那几条。检索策略直接决定响应速度和回答质量。

检索的核心挑战

检索面临两个矛盾目标:准确性和效率。准确性要求结果高度相关,不遗漏也不多返;效率要求速度快,不能让用户等数秒。深度语义匹配准确率最高,但记忆库到数万条时计算成本不可接受。分层检索策略是平衡两者的常用方案。

分层检索架构

分层检索将检索过程分多个阶段,逐步缩小候选集:

粗粒度过滤——通过元数据过滤(时间范围、类别标签)将记忆缩小到数百条,毫秒级完成。

向量检索——查询文本转为向量,在候选集中做相似度搜索取 Top-K,通常用 HNSW (Hierarchical Navigable Small World, 分层可导航小世界图) 等近似最近邻算法加速。

精细排序——对 Top-K 候选用多因子评分函数重排序,综合语义相似度、时间衰减、重要度、访问频率。

记忆检索分层架构

用户查询
  |
  v
[第一层: 元数据过滤]  --> 时间/类别/标签过滤
  |                     候选集: ~500 条
  v
[第二层: 向量检索]    --> ANN 近似最近邻搜索
  |                     候选集: ~20 条
  v
[第三层: 精细排序]    --> 多因子重排序
  |                     最终结果: ~5 条
  v
返回给 Agent
from typing import List, Optional
import numpy as np

class RetrievalPipeline:
    """分层记忆检索管道"""
    
    def __init__(self, memory_store):
        self.store = memory_store
    
    def retrieve(
        self,
        query: str,
        query_vec: List[float],
        top_k: int = 5,
        filters: Optional[dict] = None
    ) -> List[dict]:
        """执行分层检索"""
        
        # 第一层: 元数据过滤
        candidates = self._metadata_filter(filters)
        
        # 第二层: 向量检索
        vector_candidates = self._vector_search(
            query_vec, candidates, top_k * 4
        )
        
        # 第三层: 精细排序
        ranked = self._rerank(
            query, query_vec, vector_candidates, top_k
        )
        
        return ranked
    
    def _metadata_filter(self, filters: Optional[dict]) -> List:
        """第一层: 基于元数据的粗过滤"""
        if not filters:
            return list(self.store.memories.values())
        
        candidates = []
        for mem in self.store.memories.values():
            match = True
            for key, value in filters.items():
                if mem.metadata.get(key) != value:
                    match = False
                    break
            if match:
                candidates.append(mem)
        return candidates
    
    def _vector_search(
        self, query_vec, candidates, k
    ) -> List:
        """第二层: 向量相似度检索"""
        scores = []
        for mem in candidates:
            sim = np.dot(query_vec, mem.embedding) / (
                np.linalg.norm(query_vec) * np.linalg.norm(mem.embedding) + 1e-8
            )
            scores.append((sim, mem))
        scores.sort(key=lambda x: -x[0])
        return [mem for _, mem in scores[:k]]
    
    def _rerank(self, query, query_vec, candidates, top_k) -> List[dict]:
        """第三层: 多因子精细排序"""
        import time as _time
        now = _time.time()
        
        results = []
        for mem in candidates:
            # 语义相似度 (40%)
            vec_sim = np.dot(query_vec, mem.embedding) / (
                np.linalg.norm(query_vec) * np.linalg.norm(mem.embedding) + 1e-8
            )
            
            # 时间衰减 (20%): 越新越好
            age_days = (now - mem.timestamp) / 86400
            recency = np.exp(-age_days / 30)  # 指数衰减, 半衰期约 20 天
            
            # 重要度 (25%)
            importance = mem.importance
            
            # 访问频率 (15%): 常被访问的记忆更可靠
            frequency = min(1.0, mem.access_count / 10)
            
            final_score = (
                vec_sim * 0.40 +
                recency * 0.20 +
                importance * 0.25 +
                frequency * 0.15
            )
            
            results.append({
                "memory": mem,
                "score": final_score,
                "components": {
                    "semantic": round(vec_sim, 3),
                    "recency": round(recency, 3),
                    "importance": round(importance, 3),
                    "frequency": round(frequency, 3)
                }
            })
        
        results.sort(key=lambda x: -x["score"])
        return results[:top_k]

检索质量优化

分层架构之外,还有几个实用技巧:

查询扩展——检索前用 LLM 将原始查询扩展为多个相关查询,分别检索后合并去重,提升召回率,减少查询表述不当导致的遗漏。

记忆去重——同一条信息可能在多次会话中被重复记忆,不做去重会浪费上下文空间。通过计算记忆间向量相似度自动去重。

上下文窗口适配——检索到的记忆不一定全部放入上下文,按相关度从高到低排列,填满 token 预算为止。

8.7 冲突信息处理:新旧记忆的取舍

用户的信息不是静态的——换工作、改偏好、修正之前说过的话。新记忆与旧记忆矛盾时,Agent 需要一套机制检测冲突、评估可信度、做出取舍。

冲突的类型

记忆冲突分几种类型:

事实性冲突最常见。用户之前说"住上海",后来又说"搬到北京了"。处理相对直接:新信息覆盖旧信息,旧信息标记为过期但保留作历史。

偏好性冲突涉及偏好变化。用户之前说"喜欢简洁回答",后来又说"最近学新东西,回答可以详细些"。需要判断是永久变化还是临时调整——如果多个会话持续表达新偏好,可认定为永久变化。

时间性冲突是时间推移导致的信息矛盾。用户上周说"项目下周交付",到这周还没完成。不是信息错误,而是信息有时效性,需在记忆中标注有效期。

来源性冲突出现在多渠道获取信息时。用户在对话中说自己是工程师,但简历记忆中记录的是设计师,需通过信息来源可信度来判断。

冲突检测机制

冲突检测必须自动化。常用方法:基于向量相似度的预筛选、基于实体匹配的规则检测、基于 LLM 的语义判断。

class ConflictDetector:
    """记忆冲突检测器"""
    
    CONFLICT_PROMPT = """判断以下两条记忆是否存在冲突。

记忆 A: {mem_a}
记忆 B: {mem_b}

判断标准:
- 如果两条记忆描述同一实体的不同状态/属性,且无法同时为真,则为冲突
- 如果是不同维度的信息(如偏好 vs 事实),则不冲突
- 如果 B 是 A 的更精确版本,则为更新而非冲突

输出 JSON: {{"conflict": true/false, "type": "fact/preference/time/source", "resolution": "keep_new/keep_old/merge/both"}}
"""
    
    def detect_conflict(self, new_mem: dict, existing_mem: dict) -> dict:
        """检测两条记忆是否冲突"""
        # 快速预筛选: 向量相似度
        sim = self._cosine_sim(
            new_mem.get("embedding", []),
            existing_mem.get("embedding", [])
        )
        
        if sim < 0.5:
            # 语义差距大,不太可能冲突
            return {"conflict": False}
        
        if sim > 0.95:
            # 高度相似,可能是重复
            return {"conflict": False, "duplicate": True}
        
        # 中等相似度,需要 LLM 判断
        prompt = self.CONFLICT_PROMPT.format(
            mem_a=existing_mem["content"],
            mem_b=new_mem["content"]
        )
        # result = llm.generate(prompt)
        result = {
            "conflict": True,
            "type": "fact",
            "resolution": "keep_new"
        }
        return result
    
    def _cosine_sim(self, a, b):
        if not a or not b:
            return 0
        import numpy as np
        return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-8)


class ConflictResolver:
    """冲突解决器"""
    
    def __init__(self):
        self.detector = ConflictDetector()
    
    def resolve(self, new_mem: dict, existing_mems: List[dict]) -> dict:
        """处理新记忆与已有记忆的冲突"""
        for existing in existing_mems:
            result = self.detector.detect_conflict(new_mem, existing)
            
            if not result.get("conflict"):
                if result.get("duplicate"):
                    # 重复记忆,跳过或合并
                    return {"action": "skip", "reason": "duplicate"}
                continue
            
            # 存在冲突,根据解决方案处理
            resolution = result["resolution"]
            
            if resolution == "keep_new":
                # 新信息覆盖旧信息
                existing["superseded_by"] = new_mem["id"]
                existing["status"] = "outdated"
                new_mem["version"] = existing.get("version", 1) + 1
                return {"action": "add", "reason": "new_overrides_old"}
            
            elif resolution == "keep_old":
                # 旧信息更可信,忽略新信息
                return {"action": "skip", "reason": "old_more_reliable"}
            
            elif resolution == "merge":
                # 合并两条记忆
                merged_content = self._merge_contents(
                    existing["content"], new_mem["content"]
                )
                existing["content"] = merged_content
                existing["version"] = existing.get("version", 1) + 1
                return {"action": "update", "reason": "merged"}
            
            elif resolution == "both":
                # 两者都保留,但标注关系
                new_mem["related_to"] = existing["id"]
                return {"action": "add", "reason": "both_kept"}
        
        return {"action": "add", "reason": "no_conflict"}
    
    def _merge_contents(self, old: str, new: str) -> str:
        """合并两条记忆的内容"""
        # 实际应用中可以用 LLM 来智能合并
        return f"{old} [updated: {new}]"

冲突解决的决策框架

冲突解决不是简单的"新的总是对的",需要综合考量:

信息置信度取决于来源——用户直接陈述 > Agent 推断,正式语境(设置页面)> 闲聊。

时间因素也重要。新信息更可能反映当前状态,但用户可能口误后纠正。冲突时不应立即覆盖,可将两条记忆都保留,标注冲突状态,等后续信息确认。

无法自动解决时,Agent 可以主动询问用户确认:"我之前记得您住上海,但您刚提到搬到北京,需要更新吗?"多一次交互,但保证准确性。

8.8 完整记忆架构设计:从理论到实践

前面几节分别讨论了记忆类型、存储方案、压缩策略、遗忘机制、跨会话持久化、检索策略和冲突处理。这些组件如何组合成完整可运行的系统?本节给出端到端架构设计。

架构总览

完整的 Agent 记忆系统由以下层次组成:记忆接入层、记忆管理层、记忆存储层和记忆检索层。每一层承担明确的职责,层与层之间通过清晰的接口交互。

Agent 记忆系统总体架构

+------------------------------------------------------------------+
|                        Agent 核心逻辑                             |
+------------------------------------------------------------------+
         |                                          |
         v                                          v
+------------------+                     +-------------------+
|   记忆接入层      |                     |    记忆检索层      |
|  (Memory Gate)   |                     | (Retrieval Pipe)  |
|                  |                     |                   |
| - 信息提取       |                     | - 元数据过滤      |
| - 重要度评估     |                     | - 向量检索        |
| - 冲突检测       |                     | - 精细排序        |
| - 记忆写入       |                     | - 上下文适配      |
+--------+---------+                     +--------+----------+
         |                                        |
         v                                        v
+------------------------------------------------------------------+
|                         记忆管理层                                |
|                     (Memory Manager)                             |
|                                                                  |
|  +------------+  +-------------+  +--------------+              |
|  | 压缩引擎    |  | 遗忘引擎     |  | 冲突处理器    |              |
|  |(Compressor)|  |(Forgetting) |  |(ConflictRes) |              |
|  +------------+  +-------------+  +--------------+              |
|                                                                  |
|  +---------------------+  +------------------------+            |
|  | 索引管理器           |  | 生命周期管理器          |            |
|  |(Index Manager)      |  |(Lifecycle Manager)    |            |
|  +---------------------+  +------------------------+            |
+------------------------------------------------------------------+
         |
         v
+------------------------------------------------------------------+
|                         记忆存储层                                |
|                    (Storage Layer)                               |
|                                                                  |
|  +----------+  +----------+  +----------+  +----------+         |
|  |向量数据库 |  |KV 存储   |  |图数据库   |  |文件系统   |         |
|  |(Vector)  |  |(KVStore) |  |(Graph)   |  |(Files)   |         |
|  +----------+  +----------+  +----------+  +----------+         |
+------------------------------------------------------------------+

各层职责详解

记忆接入层是入口。Agent 产生新对话或交互时,接入层负责提取值得记忆的信息,执行信息提取、重要度评估、冲突检测和写入,目标是"不遗漏重要信息,也不存储无用噪音"。

记忆管理层是核心调度中枢,管理记忆全生命周期:压缩、遗忘、冲突处理、索引更新、状态维护。通过后台任务定期执行压缩和遗忘,保证记忆库体积可控、内容新鲜。

记忆存储层提供底层持久化能力,封装多种存储引擎,向上层提供统一读写接口。不同类型记忆自动路由:语义记忆→向量数据库,结构化记忆→KV 存储,关系记忆→图数据库。

记忆检索层是出口。Agent 需要回忆信息时,检索层从海量记忆中找最相关内容,执行分层检索流程,平衡效率和准确性。

端到端记忆管理器实现

下面是一个整合了前面各组件的完整记忆管理器示例。

import time
import json
from typing import List, Dict, Optional
from dataclasses import dataclass, field

@dataclass
class MemoryRecord:
    """统一记忆记录格式"""
    id: str
    content: str
    type: str                     # preference/fact/event/skill
    embedding: List[float] = field(default_factory=list)
    metadata: Dict = field(default_factory=dict)
    timestamp: float = field(default_factory=time.time)
    last_accessed: float = field(default_factory=time.time)
    access_count: int = 0
    importance: float = 0.5
    status: str = "active"        # active/outdated/archived/deleted
    version: int = 1
    superseded_by: Optional[str] = None
    source_session: Optional[str] = None


class AgentMemorySystem:
    """Agent 完整记忆系统"""
    
    def __init__(self, user_id: str):
        self.user_id = user_id
        self.short_term: List[dict] = []      # 短期记忆(对话历史)
        self.long_term: Dict[str, MemoryRecord] = {}  # 长期记忆
        self.working: Dict = {}                # 工作记忆
        self.compressor = MemoryCompressor()
        self.forgetting = ForgettingManager()
        self.conflict_resolver = ConflictResolver()
        self.retrieval = RetrievalPipeline(self)
    
    # ========== 记忆写入 ==========
    
    def on_message(self, role: str, content: str):
        """消息到达时更新短期记忆"""
        self.short_term.append({
            "role": role,
            "content": content,
            "timestamp": time.time()
        })
        # 短期记忆溢出处理
        if len(self.short_term) > 50:
            self._compact_short_term()
    
    def on_session_end(self):
        """会话结束时,提取并持久化长期记忆"""
        # 1. 压缩会话记忆
        compressed = self.compressor.compress_conversation(self.short_term)
        
        # 2. 提取值得长期记忆的信息
        extracted = self._extract_memories()
        
        # 3. 持久化,处理冲突
        for mem_data in extracted:
            self._persist_with_conflict_resolution(mem_data)
        
        # 4. 运行遗忘周期
        self.forgetting.run_forgetting_cycle()
        
        # 5. 清空短期记忆
        self.short_term.clear()
    
    def _extract_memories(self) -> List[dict]:
        """从短期记忆中提取值得长期保存的信息"""
        # 使用 LLM 提取关键信息
        return [
            {
                "type": "preference",
                "content": "用户偏好简洁回答",
                "importance": 0.8
            }
        ]
    
    def _persist_with_conflict_resolution(self, mem_data: dict):
        """持久化记忆,自动处理冲突"""
        # 查找可能冲突的已有记忆
        candidates = self._find_potential_conflicts(mem_data)
        
        result = self.conflict_resolver.resolve(mem_data, candidates)
        
        if result["action"] == "add":
            record = MemoryRecord(
                id=f"mem_{int(time.time()*1000)}",
                content=mem_data["content"],
                type=mem_data["type"],
                importance=mem_data.get("importance", 0.5),
                source_session=f"session_{int(time.time())}"
            )
            self.long_term[record.id] = record
    
    def _find_potential_conflicts(self, mem_data: dict) -> List[MemoryRecord]:
        """查找可能与新记忆冲突的已有记忆"""
        candidates = []
        for record in self.long_term.values():
            if record.type == mem_data["type"] and record.status == "active":
                # 同类型的活跃记忆可能是冲突候选
                candidates.append(record)
        return candidates
    
    # ========== 记忆检索 ==========
    
    def recall(self, query: str, query_vec: List[float] = None,
               top_k: int = 5) -> List[MemoryRecord]:
        """检索相关长期记忆"""
        results = []
        for record in self.long_term.values():
            if record.status != "active":
                continue
            # 更新访问记录
            record.last_accessed = time.time()
            record.access_count += 1
            results.append(record)
        
        # 按重要度和时间排序 (简化版)
        results.sort(
            key=lambda r: (r.importance, r.timestamp),
            reverse=True
        )
        return results[:top_k]
    
    # ========== 上下文构建 ==========
    
    def build_context(self, query: str = "") -> str:
        """构建当前会话的完整上下文"""
        parts = []
        
        # 1. 长期记忆上下文
        if query:
            memories = self.recall(query, top_k=5)
            if memories:
                parts.append("[Long-term Memory]")
                for mem in memories:
                    parts.append(f"- {mem.content}")
        
        # 2. 工作记忆上下文
        if self.working:
            parts.append("[Working Memory]")
            for key, value in self.working.items():
                parts.append(f"- {key}: {value}")
        
        # 3. 短期记忆上下文 (最近的消息)
        recent = self.short_term[-20:]  # 最近 20 条
        if recent:
            parts.append("[Recent Conversation]")
            for msg in recent:
                parts.append(f"{msg['role']}: {msg['content']}")
        
        return "\n\n".join(parts)
    
    # ========== 内部维护 ==========
    
    def _compact_short_term(self):
        """压缩短期记忆"""
        old_msgs = self.short_term[:30]
        summary = self.compressor.compress_conversation(old_msgs)
        self.short_term = [{"role": "system", "content": f"[Summary] {summary.summary}"}] + self.short_term[30:]
    
    def get_stats(self) -> dict:
        """获取记忆系统统计信息"""
        active = sum(1 for r in self.long_term.values() if r.status == "active")
        outdated = sum(1 for r in self.long_term.values() if r.status == "outdated")
        return {
            "short_term_count": len(self.short_term),
            "long_term_active": active,
            "long_term_outdated": outdated,
            "working_memory_keys": list(self.working.keys()),
            "total_storage": sum(len(r.content) for r in self.long_term.values())
        }

记忆系统的运行时序

记忆系统各组件在时间维度上的协作关系,典型一次会话的操作时序如下:

会话开始时,Agent 通过 build_context 构建初始上下文:从长期记忆检索与查询相关的记忆,加载到工作记忆,连同最近会话历史组成完整上下文。

会话进行中,用户消息和 Agent 回复写入短期记忆。短期记忆接近溢出阈值时触发压缩,将早期消息压缩为摘要。工作记忆随任务进展更新,记录中间状态和待办事项。

会话结束时执行收尾:从短期记忆提取值得长期保存的信息,检测处理冲突,写入长期存储,执行遗忘周期清理过期记忆,清空短期记忆。

性能优化方向

实际部署中性能瓶颈通常在两处:向量检索和 LLM 调用。向量检索用高效 ANN (Approximate Nearest Neighbor, 近似最近邻) 算法优化,如 HNSW 或 IVF (Inverted File Index, 倒排文件索引)。LLM 调用通过缓存优化——相似的压缩和提取请求直接用缓存结果。

另一个方向是异步化。压缩、遗忘和索引更新不需要在用户等待时同步完成,放到后台执行。只有写入和检索需同步处理,其他操作都可延迟。

本章知识点总结

知识点核心内容关键要点
记忆类型短期、长期、工作记忆短期受 token 限制,长期跨会话持久,工作记忆服务当前任务
存储方案向量数据库、KV 存储、图数据库向量适合语义检索,KV 适合精确匹配,图适合关系推理
记忆压缩消息级、会话级、跨会话压缩目标是信息保留率和压缩比的平衡,常用 LLM 生成摘要
遗忘机制时间/频率/重要度/冲突/容量驱动渐进式遗忘优于硬遗忘,高重要度记忆可豁免
跨会话记忆用户级持久化存储需要信息提取、冲突检测、隐私保护机制
检索策略分层检索:过滤-向量-精排平衡准确性与效率,支持查询扩展和去重
冲突处理检测-评估-解决三步走新旧信息不是简单覆盖,需综合考虑置信度和时间
架构设计接入层-管理层-存储层-检索层四层架构各司其职,支持异步处理和性能优化

记忆系统是需要持续迭代的工程。初始版本可以从最简单方案起步——短期记忆用消息列表,长期记忆用向量数据库加 KV 存储,暂不实现图数据库和复杂冲突处理。随场景复杂化逐步引入高级功能,关键是建立可扩展的架构基础,让后续迭代不需要推翻重来。