系列第一篇拆了 5 层总览,第二篇深入了上下文管理。这篇聚焦第 3 层------记忆。上下文管理处理的是"当前 session 内的信息",记忆层处理的是"跨 session 的知识持久化"。这一层决定了你的 Agent 是一个"每次见面都忘了你是谁的陌生人",还是一个"记得你偏好和历史的助手"。
记忆层和上下文管理层的区别
很多人混淆这两层。一张表说清楚:
| 维度 | 上下文管理(第 2 层) | 记忆(第 3 层) |
|---|---|---|
| 作用范围 | 当前 session | 跨 session |
| 生命周期 | session 结束即消失 | 永久(或按策略过期) |
| 存储位置 | 内存中的消息数组 | 数据库 / 文件系统 |
| 注入方式 | 直接拼进 prompt | 检索后注入 |
| 典型内容 | 最近几轮对话 | 用户偏好、项目知识、历史决策 |
一句话区分:上下文管理管"这次对话记得什么",记忆管"下次对话还记得什么"。
3 种持久化架构
架构 1:键值对存储(Key-Value Memory)
最简单的记忆方案。把关键信息存成 key-value 对,下次 session 开始时按 key 检索注入。
python
import sqlite3
class KeyValueMemory:
def __init__(self, db_path="memory.db"):
self.db = sqlite3.connect(db_path)
self.db.execute("""
CREATE TABLE IF NOT EXISTS memories (
key TEXT PRIMARY KEY,
value TEXT,
category TEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
def remember(self, key: str, value: str, category: str = "general"):
self.db.execute(
"INSERT OR REPLACE INTO memories (key, value, category, updated_at) VALUES (?, ?, ?, datetime('now'))",
(key, value, category)
)
self.db.commit()
def recall(self, category: str = None, limit: int = 20) -> list:
if category:
rows = self.db.execute(
"SELECT key, value FROM memories WHERE category = ? ORDER BY updated_at DESC LIMIT ?",
(category, limit)
).fetchall()
else:
rows = self.db.execute(
"SELECT key, value FROM memories ORDER BY updated_at DESC LIMIT ?",
(limit,)
).fetchall()
return [{"key": r[0], "value": r[1]} for r in rows]
def inject_into_context(self, category: str = None) -> str:
"""把记忆格式化成可注入上下文的文本"""
memories = self.recall(category)
if not memories:
return ""
lines = [f"- {m['key']}: {m['value']}" for m in memories]
return "[已知信息]\n" + "\n".join(lines)
使用场景:
python
# Agent 在对话中发现了需要记住的信息
memory.remember("project_name", "Phoenix", category="project")
memory.remember("preferred_language", "Python", category="user_pref")
memory.remember("db_type", "PostgreSQL 16", category="tech_stack")
# 下次 session 开始时注入
context = memory.inject_into_context(category="project")
# 输出:
# [已知信息]
# - project_name: Phoenix
# - db_type: PostgreSQL 16
| 优点 | 缺点 |
|---|---|
| 实现极简(SQLite 就够) | 只能精确匹配 key,不能语义检索 |
| 读写性能高 | 需要显式调用 remember(),不能自动提取 |
| 占用空间小 | 不适合存长文本(如完整对话记录) |
| 零依赖 | 记忆量大了之后全部注入会撑爆上下文 |
适用场景:Agent 需要记住的信息量较小(<100 条)、结构明确(用户偏好、项目配置、固定事实)。大部分个人 Agent 用这种方案就够了。
OpenClaw 的 memory-core 插件底层就是这个思路------用 SQLite 存 slot 式的记忆。
架构 2:对话日志 + 摘要归档(Log + Digest)
把完整对话历史存下来,定期做摘要归档。下次 session 加载摘要,不加载原始对话。
python
class DigestMemory:
def __init__(self, db_path="memory.db"):
self.db = sqlite3.connect(db_path)
self.db.execute("""
CREATE TABLE IF NOT EXISTS conversation_logs (
id INTEGER PRIMARY KEY,
session_id TEXT,
role TEXT,
content TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
self.db.execute("""
CREATE TABLE IF NOT EXISTS digests (
id INTEGER PRIMARY KEY,
period TEXT,
summary TEXT,
key_decisions TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
def log_message(self, session_id: str, role: str, content: str):
"""记录每条消息"""
self.db.execute(
"INSERT INTO conversation_logs (session_id, role, content) VALUES (?, ?, ?)",
(session_id, role, content)
)
self.db.commit()
def create_digest(self, period: str = "daily"):
"""把最近的对话日志压缩成摘要"""
# 拉最近 24 小时的对话
logs = self.db.execute("""
SELECT role, content FROM conversation_logs
WHERE created_at > datetime('now', '-1 day')
ORDER BY created_at
""").fetchall()
if not logs:
return
# 用轻量模型做摘要
conversation_text = "\n".join([f"{r}: {c}" for r, c in logs])
summary = summarize_with_llm(conversation_text,
prompt="提取这段对话中的关键决策、用户偏好和待办事项。用简洁的列表格式。")
self.db.execute(
"INSERT INTO digests (period, summary) VALUES (?, ?)",
(period, summary)
)
self.db.commit()
def get_recent_digests(self, days: int = 7, limit: int = 5) -> str:
"""获取最近的摘要,用于注入上下文"""
digests = self.db.execute("""
SELECT period, summary FROM digests
WHERE created_at > datetime('now', ?)
ORDER BY created_at DESC LIMIT ?
""", (f'-{days} day', limit)).fetchall()
if not digests:
return ""
lines = [f"[{d[0]}] {d[1]}" for d in digests]
return "[历史摘要]\n" + "\n".join(lines)
核心设计:原始对话存日志(用于审计和回放),摘要存归档(用于注入上下文)。两者分开存。
| 优点 | 缺点 |
|---|---|
| 保留完整历史(可回放任何 session) | 摘要有信息损失 |
| 注入量可控(只注入摘要) | 定期归档需要额外的定时任务 |
| 自然支持时间维度("上周讨论了什么") | 不支持语义检索 |
| 审计和 debug 方便 | 日志量大了 SQLite 会变慢 |
适用场景:需要审计能力的团队 Agent("回看上周的对话决策"),或者需要时间维度记忆("上周做了什么决定")的项目管理类 Agent。
架构 3:向量记忆(Vector Memory)
把每条消息/知识做 embedding,存入向量数据库。每次 session 按语义相关性检索最相关的记忆注入上下文。
python
from openai import OpenAI
import numpy as np
class VectorMemory:
def __init__(self, db_path="vector_memory.db"):
self.client = OpenAI(base_url="https://your-api-gateway.com/v1")
self.db = sqlite3.connect(db_path)
self.db.execute("""
CREATE TABLE IF NOT EXISTS memories (
id INTEGER PRIMARY KEY,
content TEXT,
embedding BLOB,
category TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
def store(self, content: str, category: str = "general"):
"""存储一条记忆(自动做 embedding)"""
response = self.client.embeddings.create(
model="text-embedding-3-small",
input=content
)
embedding = np.array(response.data[0].embedding, dtype=np.float32)
self.db.execute(
"INSERT INTO memories (content, embedding, category) VALUES (?, ?, ?)",
(content, embedding.tobytes(), category)
)
self.db.commit()
def search(self, query: str, top_k: int = 5) -> list:
"""按语义相关性检索记忆"""
# query 做 embedding
response = self.client.embeddings.create(
model="text-embedding-3-small",
input=query
)
query_emb = np.array(response.data[0].embedding, dtype=np.float32)
# 全量检索(小规模用 numpy 就够)
rows = self.db.execute("SELECT id, content, embedding FROM memories").fetchall()
scores = []
for row in rows:
mem_emb = np.frombuffer(row[2], dtype=np.float32)
similarity = np.dot(query_emb, mem_emb) / (np.linalg.norm(query_emb) * np.linalg.norm(mem_emb))
scores.append((row[1], float(similarity)))
scores.sort(key=lambda x: x[1], reverse=True)
return [{"content": s[0], "score": s[1]} for s in scores[:top_k]]
def inject_into_context(self, current_query: str, top_k: int = 5) -> str:
"""按当前问题的语义检索相关记忆,格式化注入"""
results = self.search(current_query, top_k)
if not results:
return ""
lines = [f"- {r['content']} (相关度: {r['score']:.2f})" for r in results]
return "[相关历史信息]\n" + "\n".join(lines)
| 优点 | 缺点 |
|---|---|
| 不受时间顺序限制,100 轮前的信息也能检索到 | 需要 Embedding 模型(额外成本) |
| 按相关性注入,上下文利用率高 | 检索准确率不是 100%,可能漏掉关键信息 |
| 记忆量可以很大(万级) | 实现复杂度高 |
| 天然支持跨渠道(飞书说的话 Discord 也能检索到) | 丢失时间顺序信息 |
适用场景:长期运行的个人助手(跑了几个月,积累了大量知识),需要从海量历史中按需召回特定信息。知识库类 Agent(FAQ、文档助手)。
3 种架构的决策矩阵
| 维度 | KV 存储 | 日志+摘要 | 向量记忆 |
|---|---|---|---|
| 实现复杂度 | ⭐ | ⭐⭐ | ⭐⭐⭐⭐ |
| 记忆容量 | <100 条 | 千级 | 万级 |
| 检索方式 | 精确 key | 时间范围 | 语义相关 |
| 信息损失 | 无 | 摘要有损 | 检索可能漏 |
| 额外成本 | 零 | 摘要调用 | Embedding 调用 |
| 跨 session | ✅ | ✅ | ✅ |
| 审计能力 | 弱 | 强 | 弱 |
| 适用规模 | 个人 | 团队 | 个人/知识库 |
我的建议:从 KV 存储开始。 不要上来就搞向量库------90% 的 Agent 记忆需求用 SQLite 的 KV 存储就够了("记住用户叫什么"、"项目用什么技术栈"、"上次做了什么决定")。等你发现 KV 存不下或者精确匹配不够用了,再升级。
记忆的写入时机:自动提取 vs 显式存储
一个容易忽视的设计问题:什么时候往记忆里写?
| 方式 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 显式存储 | 用户说"记住这个",Agent 才存 | 精确,不存垃圾 | 依赖用户主动触发 |
| 自动提取 | 每轮对话后模型自动判断是否有值得记住的信息 | 不需要用户操心 | 可能存入噪音 |
| 混合 | 重要信息自动提取 + 用户可以手动纠正 | 兼顾 | 实现稍复杂 |
自动提取的实现:
python
def auto_extract_memories(conversation_turn: str, memory: KeyValueMemory):
"""让模型判断这轮对话是否有值得记住的信息"""
response = client.chat.completions.create(
model="qwen/qwen3.5-9b", # 用便宜模型做提取
messages=[{
"role": "user",
"content": f"""分析以下对话内容,提取需要长期记住的信息。
只提取以下类型:
1. 用户偏好(语言、代码风格、工作习惯)
2. 项目信息(名称、技术栈、架构决策)
3. 重要决定(选了什么方案、为什么)
如果没有值得记住的信息,返回空 JSON 数组。
对话内容:
{conversation_turn}
返回格式:[{{"key": "xxx", "value": "xxx", "category": "xxx"}}]"""
}],
response_format={"type": "json_object"}
)
memories = json.loads(response.choices[0].message.content)
for m in memories:
memory.remember(m["key"], m["value"], m.get("category", "auto"))
关键点:自动提取用便宜模型。 它是一个"信息分类"任务,不需要高级推理。Qwen 3.5 9B 或类似的轻量模型够用了。
记忆衰减与整合
记忆不是越多越好。存了 1000 条记忆,全部注入上下文是不可能的------那是几万 token。
记忆的生命周期管理:
python
def maintain_memories(memory: KeyValueMemory, max_memories: int = 100):
"""定期维护记忆:合并重复、清理过期"""
all_memories = memory.recall(limit=9999)
if len(all_memories) <= max_memories:
return # 还没超限
# 策略1:按更新时间淘汰最旧的
# 策略2:让模型合并重复/相似的记忆
# 策略3:让模型判断哪些记忆已经不再相关
merge_prompt = f"""以下是 Agent 的记忆列表({len(all_memories)} 条):
{json.dumps(all_memories, ensure_ascii=False, indent=2)}
请执行以下操作:
1. 合并重复或高度相似的记忆
2. 删除明显过期或不再相关的记忆
3. 保留最多 {max_memories} 条
返回精简后的记忆列表(JSON 格式)。"""
# 用模型做记忆整合
result = client.chat.completions.create(
model="qwen/qwen3.5-plus",
messages=[{"role": "user", "content": merge_prompt}],
response_format={"type": "json_object"}
)
# ... 用整合后的结果替换原有记忆
定期整合比无限堆积重要。 建议每周或每 100 条新记忆做一次整合。
常见问题
Q: 在内存里保存完整对话历史和持久化记忆有什么区别?
A: 内存里的历史是给上下文管理层用的(当前 session 内的滚动窗口)。持久化记忆是给记忆层用的(跨 session 的知识)。两者独立存储。之前出过 OOM 就是因为内存里的历史无限增长------内存里的要定期清理或归档到磁盘。
Q: 向量记忆需要专门的向量数据库吗?
A: 记忆量 <1 万条时,SQLite + numpy 余弦相似度就够了(上面代码的方案)。超过 1 万条再考虑 Chroma、Milvus 这类专用向量库。不要过早引入复杂依赖。
Q: 多渠道(飞书+Discord)的记忆怎么统一?
A: 所有渠道的记忆写入同一个数据库。记忆本身不区分来源渠道------"用户的项目叫 Phoenix"这条信息不管是在飞书说的还是 Discord 说的,都是同一条记忆。OpenClaw 的做法就是一个 SQLite 存所有渠道的记忆。