智能体记忆系统
LLM 本身无状态,上下文窗口又有限——02-Agent Loop 里那个 history 列表一满,前面的工作就丢了。记忆系统要解决的是在窗口之外维持状态。
但「记忆」不是一种东西。这篇从认知科学的映射讲起,再到四类记忆的工程实现。
为什么需要记忆:LLM 的两个根本局限
局限一:无状态导致的对话遗忘
当前 LLM 在设计上是无状态的——每一次 API 调用都是一次独立的、无关联的计算,模型本身不会自动「记住」上一次对话。由此产生四类问题:
| 问题 | 表现 |
|---|---|
| 上下文丢失 | 长对话中早期的重要信息因窗口限制而丢失 |
| 个性化缺失 | 记不住用户的偏好、习惯或特定需求 |
| 学习能力受限 | 无法从过往的成功或失败经验里改进 |
| 一致性问题 | 多轮对话中可能出现前后矛盾的回答 |
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 是记忆系统的统一接口,遵循「统一入口,分发处理」:
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(遗忘)值得单独注意:它们对应上面认知模型里的第四、第五阶段,是「只做存查」的实现会缺掉的部分。
调用形式:
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(向量) | 向量检索与图检索并行,再混合排序 |
| 感知记忆 | 多模态数据(文本/图像/音频) | 按模态分离的向量集合 | 跨模态相似性搜索 |
工作记忆:快,且会自动过期
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 是词法相似度(稀疏词项向量),不等于稠密嵌入的语义检索。两者经常被混为一谈。
情景记忆:结构化过滤在向量检索之前
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 是同一个套路)。
语义记忆:图 + 向量的混合
写入时不仅要存内容,还要抽实体和关系建图:
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. 存向量检索时两条路并行,再合并:
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 这个配比是有意的:语义相似度是主判据,图检索作为发现隐含关联的补充。反过来的话,靠关系链兜出来的远邻概念会挤掉直接相关的记录。
感知记忆:按模态分离存储
多模态的关键设计是为不同模态建独立的向量集合:
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(多集合)。
相关
- Context Engineering —— 记忆是上下文六个信息源之一,检索结果最终要进那个 token 预算
- 11-RAG 检索增强 —— 同一套向量检索能力用在外部知识上
- 02-Agent Loop —— 被替代掉的那个裸
history列表 - 01-智能体核心概念 —— 「部分可观察」这个环境特性就是记忆需求的来源
参考
- 《Hello-Agents》第八章 §8.1–§8.2
- RUSSELL S, NORVIG P. Artificial Intelligence: A Modern Approach[M]. 4th ed. 2020.
YJ