Skip to content

智能体记忆系统 ​

标签
AI/agent/记忆
字数
4428 字
阅读时间
18 分钟

LLM 本身无状态,上下文窗口又有限——02-Agent Loop 里那个 history 列表一满,前面的工作就丢了。记忆系统要解决的是在窗口之外维持状态。

但「记忆」不是一种东西。这篇从认知科学的映射讲起,再到四类记忆的工程实现。

为什么需要记忆:LLM 的两个根本局限 ​

局限一:无状态导致的对话遗忘 ​

当前 LLM 在设计上是无状态的——每一次 API 调用都是一次独立的、无关联的计算,模型本身不会自动「记住」上一次对话。由此产生四类问题:

问题表现
上下文丢失长对话中早期的重要信息因窗口限制而丢失
个性化缺失记不住用户的偏好、习惯或特定需求
学习能力受限无法从过往的成功或失败经验里改进
一致性问题多轮对话中可能出现前后矛盾的回答
python
from hello_agents import SimpleAgent, HelloAgentsLLM

agent = SimpleAgent(name="学习助手", llm=HelloAgentsLLM())
response1 = agent.run("我叫张三,正在学习Python,目前掌握了基础语法")

# 新的会话(例如重启程序后重新创建 Agent)
agent = SimpleAgent(name="学习助手", llm=HelloAgentsLLM())
response2 = agent.run("你还记得我的学习进度吗?")
# → "抱歉,我不知道您的学习进度..."

这里有个容易误解的地方:SimpleAgent 会在同一个实例的 _history 里暂存当前对话,所以同一进程、同一实例内的连续对话是能带上最近上下文的。但那只是个临时消息列表——它不跨会话持久化,也不能做长期检索、遗忘和整合。「能记几轮」和「有记忆系统」是两件事。

局限二:内置知识是静态且有限的 ​

LLM 的知识完全来自训练数据,因此:

问题说明
知识时效性训练数据有时间截止点,拿不到最新信息
专业领域深度通用模型在垂直领域的深度知识可能不足
事实准确性需要靠检索验证来减少幻觉(见 07-模型幻觉)
可解释性提供信息来源才能增强回答的可信度

RAG 就是冲这条来的:在生成回答之前,先从外部知识库(文档、数据库、API)检索最相关的信息,作为上下文一并喂给模型。 它单独成篇在 11-RAG 检索增强。

认知科学的映射 ​

人类记忆的层级 ​

层级持续时间容量
感觉记忆(Sensory)极短,0.5–3 秒巨大,暂存感官接收到的全部信息
工作记忆(Working)短,15–30 秒有限,7±2 个项目
长期记忆(Long-term)可达终生几乎无限

长期记忆再分两层:

长期记忆
├── 程序性记忆   技能和习惯(如骑自行车)
└── 陈述性记忆   可用语言表达的知识
    ├── 语义记忆   一般知识和概念("巴黎是法国首都")
    └── 情景记忆   个人经历和事件("昨天的会议内容")

注意工程的四种记忆类型与这套层级不是一一对应的:工程上把「工作记忆」直接对应到对话上下文,把「语义 / 情景」拆成两种独立的存储,另外多加了一个人类模型里没有的「感知记忆」——那是为了处理图像、音频这类多模态输入。所以是借鉴结构,不是照搬。

记忆形成的五个阶段 ​

认知科学把记忆的形成分成五步,工程上每一步都能落到具体组件:

阶段认知含义工程映射
编码(Encoding)把感知到的信息转成可存储的形式文本嵌入 / 实体关系抽取
存储(Storage)把编码后的信息保存进记忆系统写入向量库 / 图库 / 文档库
检索(Retrieval)按需从记忆中提取相关信息向量检索 + 结构化过滤 + 重排
整合(Consolidation)把短期记忆转化为长期记忆consolidate 操作(工作记忆 → 语义/情景)
遗忘(Forgetting)删除不重要或过时的信息TTL 过期 + 容量淘汰 + forget 策略

「整合」和「遗忘」这两个阶段是自建记忆系统最容易漏掉的。只做「存 + 查」,长期跑下来记忆库会退化成一个只增不减的日志——早期噪声永远占着检索结果的高位。

记忆形成的五个阶段,每一步都能落到具体组件。

① 编码 Encoding        把感知到的信息转成可存储的形式
   └─ 工程:文本嵌入 / 实体关系抽取
        │
        ▼
② 存储 Storage         把编码后的信息保存进记忆系统
   └─ 工程:写入向量库 / 图库 / 文档库
        │
        ▼
③ 检索 Retrieval       按需从记忆里提取相关信息
   └─ 工程:向量检索 + 结构化过滤 + 重排
        │
        ▼
④ 整合 Consolidation   把短期记忆转化为长期记忆
   └─ 工程:consolidate 操作(工作记忆 → 语义 / 情景记忆)
        │
        ▼
⑤ 遗忘 Forgetting      删除不重要或过时的信息
   └─ 工程:TTL 过期 + 容量淘汰 + forget 策略

④ 与 ⑤ 是自建记忆系统最容易漏掉的两个阶段。
只做「存 + 查」,长期跑下来记忆库会退化成一个只增不减的日志 ——
早期噪声永远占着检索结果的高位。

整体架构 ​

记忆与 RAG 被设计成两个独立工具:

  • memory_tool 负责存储和维护对话过程中的交互信息
  • rag_tool 负责从用户知识库检索信息作为上下文,并可以把重要的检索结果自动存进记忆系统

这个分工有点绕但很关键:记忆系统管「我经历过什么」,RAG 管「我知道什么」;两者共享同一套嵌入与向量存储,靠元数据字段隔离命名空间。

记忆与 RAG 被设计成两个独立工具:管不同的事,共用同一套底座。

   ┌─────────────────────────────────────────────────────┐
   │  memory_tool                                        │
   │  管「我经历过什么」                                   │
   │  存储与维护对话过程中的交互信息                        │
   └───────────────────────┬─────────────────────────────┘
                           │
                           ▼
   ┌─────────────────────────────────────────────────────┐
   │  共享的嵌入与向量存储                                 │
   │  靠元数据字段隔离命名空间 —— 两套系统不互相污染          │
   └───────────────────────▲─────────────────────────────┘
                           │
                           │ 重要的检索结果可自动存进记忆系统
                           │
   ┌───────────────────────┴─────────────────────────────┐
   │  rag_tool                                           │
   │  管「我知道什么」                                     │
   │  从用户知识库检索信息,作为上下文一并喂给模型           │
   └─────────────────────────────────────────────────────┘

记忆系统的四层 ​

HelloAgents记忆系统
├── 基础设施层
│   ├── MemoryManager    记忆管理器(统一调度和协调)
│   ├── MemoryItem       记忆数据结构(标准化记忆项)
│   ├── MemoryConfig     配置管理
│   └── BaseMemory       记忆基类(通用接口)
├── 记忆类型层
│   ├── WorkingMemory    临时信息,TTL 管理
│   ├── EpisodicMemory   具体事件,时间序列
│   ├── SemanticMemory   抽象知识,图谱关系
│   └── PerceptualMemory 多模态数据
├── 存储后端层
│   ├── QdrantVectorStore     向量存储(语义检索)
│   ├── Neo4jGraphStore       图存储(知识图谱)
│   └── SQLiteDocumentStore   文档存储(结构化持久化)
└── 嵌入服务层
    ├── DashScopeEmbedding        通义千问嵌入(云端 API)
    ├── LocalTransformerEmbedding 本地嵌入(离线部署)
    └── TFIDFEmbedding            TF-IDF 嵌入(轻量兜底)

嵌入服务层那三级是一条降级链,不是三个并列选项:云端 API 最准但依赖网络和额度;本地 Transformer 离线可用但吃显存;TF-IDF 什么依赖都不需要、纯词法匹配,作为最后的兜底。「每一层都有兜底」是这套架构最值得学的工程习惯——检索链路上任何一环挂掉,系统降级但不停摆。

统一入口:MemoryTool 的九个操作 ​

MemoryTool 是记忆系统的统一接口,遵循「统一入口,分发处理」:

python
def execute(self, action: str, **kwargs) -> str:
    """执行记忆操作

    支持的操作:
    - add: 添加记忆(支持4种类型: working/episodic/semantic/perceptual)
    - search: 搜索记忆
    - summary: 获取记忆摘要
    - stats: 获取统计信息
    - update: 更新记忆
    - remove: 删除记忆
    - forget: 遗忘记忆(多种策略)
    - consolidate: 整合记忆(短期→长期)
    - clear_all: 清空所有记忆
    """
    if action == "add":
        return self._add_memory(**kwargs)
    elif action == "search":
        return self._search_memory(**kwargs)
    ...

用 action 字符串加 **kwargs 的设计,让每个操作可以有完全不同的参数集,同时对外只暴露一个入口。这也是「万物皆为工具」原则的体现(见 07-Agent 框架的抽象设计)——记忆被包装成一个普通工具,Agent 不需要为它学一套新接口。

九个操作里,consolidate(整合)和 forget(遗忘)值得单独注意:它们对应上面认知模型里的第四、第五阶段,是「只做存查」的实现会缺掉的部分。

调用形式:

python
from hello_agents.tools import MemoryTool

memory_tool = MemoryTool(user_id="user123")

memory_tool.run("add", content="用户张三是一名Python开发者,专注于机器学习和数据分析",
                memory_type="semantic", importance=0.8)

memory_tool.run("search", query="前端工程师", limit=3)
memory_tool.run("summary")

user_id 是构造参数——记忆按用户隔离,这是检索时的硬过滤条件。

四种记忆类型 ​

类型存什么存储方案检索策略
工作记忆当前会话的临时信息纯内存 + TTL组合词法检索(TF-IDF 词项权重 + 关键词匹配)
情景记忆具体事件与经历SQLite(结构化)+ Qdrant(向量)结构化预过滤 → 语义向量检索 → 综合评分
语义记忆抽象概念、规则、知识Neo4j(图)+ Qdrant(向量)向量检索与图检索并行,再混合排序
感知记忆多模态数据(文本/图像/音频)按模态分离的向量集合跨模态相似性搜索

工作记忆:快,且会自动过期 ​

python
class WorkingMemory:
    def __init__(self, config: MemoryConfig):
        self.max_capacity = config.working_memory_capacity or 50
        self.max_age_minutes = config.working_memory_ttl or 60

    def add(self, memory_item: MemoryItem) -> str:
        self._expire_old_memories()               # 过期清理
        if len(self.memories) >= self.max_capacity:
            self._remove_lowest_priority_memory() # 容量管理
        self.memories.append(memory_item)

两个约束同时生效:容量上限(默认 50 条)加 TTL(默认 60 分钟),写入前先清过期、再按优先级淘汰。纯内存存储意味着系统重启后内容丢失——这正好符合工作记忆「临时、易变」的定位。

评分公式:

final = base_relevance × time_decay × (0.8 + importance × 0.4)
其中 base_relevance = vector_score × 0.7 + keyword_score × 0.3   (TF-IDF 可用时)

注意 base_relevance 那个分叉:TF-IDF 拿不到有效得分时,直接退化成纯关键词得分,而不是把 0 加权进公式。这是把「降级路径」显式写进评分,避免检索直接空掉——和嵌入服务层的三级兜底是同一个思路。

另外要区分清楚:这里的 TF-IDF 是词法相似度(稀疏词项向量),不等于稠密嵌入的语义检索。两者经常被混为一谈。

情景记忆:结构化过滤在向量检索之前 ​

python
def retrieve(self, query: str, limit: int = 5, **kwargs) -> List[MemoryItem]:
    candidate_ids = self._structured_filter(**kwargs)                  # 1. 结构化预过滤
    hits = self._vector_search(query, limit * 5, kwargs.get("user_id")) # 2. 向量检索
    ...                                                                # 3. 综合评分排序

顺序不能反:先用结构化条件(时间范围、重要性、用户)把候选缩到合规集合,再在里面做语义检索。反过来的话,语义最相似的十条可能全都不符合过滤条件,白跑一趟向量库。

评分公式多了一项时间因素:

(vec_score × 0.8 + recency_score × 0.2) × (0.8 + importance × 0.4)

向量检索时取 limit * 5 条候选,再筛到 limit——扩大候选池再重排是这套框架的通用手法(11-RAG 检索增强 里的 candidate_pool_multiplier 是同一个套路)。

语义记忆:图 + 向量的混合 ​

写入时不仅要存内容,还要抽实体和关系建图:

python
def add(self, memory_item: MemoryItem) -> str:
    embedding = self.embedding_model.encode(memory_item.content)          # 1. 文本嵌入
    entities = self._extract_entities(memory_item.content)                # 2. 抽实体
    relations = self._extract_relations(memory_item.content, entities)    # 3. 抽关系
    for entity in entities: self._add_entity_to_graph(entity, memory_item)
    for relation in relations: self._add_relation_to_graph(relation, memory_item)
    self.vector_store.add_vectors(...)                                    # 4. 存向量

检索时两条路并行,再合并:

python
vector_results = self._vector_search(query, limit * 2, user_id)
graph_results  = self._graph_search(query, limit * 2, user_id)
combined = self._combine_and_rank_results(vector_results, graph_results, query, limit)

合并策略是按 memory_id 取并集,两条路都命中的记录同时带上 vector_score 和 graph_score,只被一条命中的另一项记 0:

base_relevance = vector_score × 0.7 + graph_score × 0.3
final = base_relevance × (0.8 + importance × 0.4)

0.7 / 0.3 这个配比是有意的:语义相似度是主判据,图检索作为发现隐含关联的补充。反过来的话,靠关系链兜出来的远邻概念会挤掉直接相关的记录。

感知记忆:按模态分离存储 ​

多模态的关键设计是为不同模态建独立的向量集合:

python
self.text_embedder  = get_text_embedder()
self._clip_model    = self._init_clip_model()   # 图像编码
self._clap_model    = self._init_clap_model()   # 音频编码

self.vector_stores = {
    "text": QdrantConnectionManager.get_instance(collection_name="perceptual_text"),
    ...
}

理由是维度不匹配:文本、图像、音频的嵌入模型输出维度不同,混在一个集合里没法算距离。分模态存之后,跨模态检索要靠对齐过的编码空间(如 CLIP 把文本和图像投影到同一空间)来桥接。

三条跨类型的规律 ​

把四种实现放在一起看,有三条反复出现的设计规律:

一、重要性权重是同一个函数。 四种记忆里都出现 0.8 + importance × 0.4,取值范围 [0.8, 1.2]。这是刻意的——乘数被限制在 1 附近,重要性只能微调排序,不能压过相关性。写成 importance × 2 的话,一条高重要性的不相关记录就会挤掉真正相关的结果。

二、存储分层决定检索成本。 越临时、越易变的记忆用越轻的存储(内存 + 词法检索);越持久、越需要推理的记忆用越重的存储(图 + 向量 + 混合排序)。四类记忆的复杂度阶梯清晰。

三、写入时预处理,读取时才不慢。 语义记忆在 add 阶段就把实体和关系抽出来建图,情景记忆在 add 阶段就写进两个库。检索路径上只做查询和排序,不做抽取和解析。

四种记忆的评分里,调整项的量级是被刻意限定的。

工作记忆
   final = base_relevance × time_decay × (0.8 + importance × 0.4)
                                          └─ 取值范围 [0.8, 1.2]

情景记忆
   (vec_score × 0.8 + recency_score × 0.2) × (0.8 + importance × 0.4)

语义记忆(图与向量并行检索后合并)
   (vector_score × 0.7 + graph_score × 0.3) × (0.8 + importance × 0.4)

   ┌─ 共同的 (0.8 + importance × 0.4):乘数被限制在 1 附近,
   │  重要性只能微调排序,压不过相关性。
   └─ 写成 importance × 2 的话,一条高重要性的不相关记录
      就会挤掉真正相关的结果。

   ┌─ base_relevance 里的分叉:TF-IDF 拿不到有效得分时,
   │  直接退化成纯关键词得分,而不是把 0 加权进公式 ——
   │  把降级路径显式写进评分,避免检索直接空掉。
   └─ 0.7 / 0.3 的配比同样有意图:语义相似度是主判据,
      图检索只作为发现隐含关联的补充;反过来的话,
      靠关系链兜出来的远邻概念会挤掉直接相关的记录。

另外要分清:这里的 TF-IDF 是词法相似度(稀疏词项向量),
不等于稠密嵌入的语义检索 —— 两者经常被混为一谈。

存储后端的分工 ​

后端存什么为什么是它
Qdrant向量高性能语义检索,支持 payload 过滤(结构化条件与向量检索能一次查询里完成)
Neo4j实体与关系知识图谱的关系推理,是向量检索补不上的那一部分
SQLite文档与结构化字段零运维、单文件、支持复杂查询;适合记忆条目这种「量不大但要按字段筛」的数据

三者的组合按记忆类型选,而不是「都要用」:工作记忆一个都不用(纯内存),情景记忆用 SQLite + Qdrant,语义记忆用 Neo4j + Qdrant,感知记忆只用 Qdrant(多集合)。

相关 ​

参考 ​

  • 《Hello-Agents》第八章 §8.1–§8.2
  • RUSSELL S, NORVIG P. Artificial Intelligence: A Modern Approach[M]. 4th ed. 2020.

贡献者 ​

文件历史 ​