Agent Memory:四种记忆
旅游助手现在能查资料、能从失败中学习。但这些能力散落在不同地方:对话历史在 messages 里,检索的知识在向量库里,Reflexion 的教训在 KV 存储里,偏好规则硬编码在 prompt 里。
这一页不引入新的运行机制。它做的是分类:Agent 的记忆有几种、各自存在哪里、什么时候读写。
四种记忆
| 类型 | 对标人类记忆 | Agent 里的表现 | 存储 |
|---|---|---|---|
| Working Memory | 工作记忆 | 当前 messages 列表 |
运行时变量 |
| Episodic Memory | 情节记忆 | Reflexion 的教训、过去的任务摘要 | KV / 文件 / 数据库 |
| Semantic Memory | 语义记忆 | RAG 的文档库、知识图谱 | 向量库 / 搜索引擎 |
| Procedural Memory | 程序记忆 | 工具定义、技能模板、固定规则 | 代码 / 配置文件 |
flowchart TD
WM["Working Memory
当前 messages"] --> EP["Episodic Memory
过去的经验教训"]
WM --> SM["Semantic Memory
外部文档/知识"]
WM --> PM["Procedural Memory
工具/技能/规则"]
EP -.->|"读教训到 prompt"| WM
SM -.->|"检索到 context"| WM
PM -.->|"工具定义到 system prompt"| WM
Working Memory:当前对话
就是 messages 列表。每一轮模型调用都读它。
- 容量有限(context window)。
- 运行结束就没了(除非显式持久化)。
- 前六章讲的对话历史就是 working memory。
Episodic Memory:过去发生过什么
Reflexion 的教训是最典型的 episodic memory。还包括:
- 过去任务的摘要。
- 用户偏好("这个用户不喜欢太多景点")。
- 上一次会话的关键决策。
Semantic Memory:世界知识
RAG 的文档库是 semantic memory。包括:
- 结构化知识库。
- 向量索引的文档片段。
- 知识图谱。
Procedural Memory:怎么做
工具定义、技能模板、固定规则。包括:
- 工具的 JSON schema。
- prompt 模板。
- 业务规则("订票前必须确认预算")。
存储选项
| 存储方式 | 适合什么 | 优点 | 代价 |
|---|---|---|---|
| 运行时变量(dict) | Working Memory | 最快 | 进程结束就没 |
| KV 存储(InMemoryKV / JsonFileKV) | Episodic:教训、偏好 | 简单、可持久化 | 不支持语义检索 |
| 文件系统(JSON / JSONL) | Session 状态、配置 | 可版本控制 | 并发读写要自己管 |
| 向量数据库(FAISS / Pinecone / pgvector) | Semantic:文档检索 | 语义搜索 | 需要嵌入模型,运维成本 |
| 关系数据库(SQLite / PostgreSQL) | 结构化的 Episodic / Semantic | 事务、查询灵活 | Schema 设计 |
完整实现:带长期记忆的旅游助手
下面的例子把四种记忆整合在一个旅游助手里:
"""agent_memory_travel.py — 四种记忆的旅游助手"""
from __future__ import annotations
import json
from dataclasses import dataclass, field
from typing import Any
# ── 极简运行时 ──────────────────────────────────────────
@dataclass(frozen=True)
class Message:
role: str
content: str
class MockLLM:
def __init__(self, responses: list[str]):
self._responses = list(responses)
self._idx = 0
def complete(self, messages: list[Message]) -> str:
if self._idx >= len(self._responses):
raise RuntimeError("MockLLM 回复用完了")
out = self._responses[self._idx]
self._idx += 1
return out
# ── 记忆存储 ────────────────────────────────────────────
class InMemoryKV:
"""Episodic memory 的 KV 存储。"""
def __init__(self):
self._data: dict[str, Any] = {}
def get(self, key: str, default=None) -> Any:
return self._data.get(key, default)
def set(self, key: str, value: Any) -> None:
self._data[key] = value
@dataclass(frozen=True)
class Document:
doc_id: str
text: str
class SemanticStore:
"""Semantic memory:简单的词频检索。"""
def __init__(self, docs: list[Document]):
self._docs = docs
def search(self, query: str, k: int = 3) -> list[Document]:
terms = self._tokenize(query)
if not terms:
return []
scored = []
for doc in self._docs:
doc_tokens = self._tokenize(doc.text)
score = sum(1 for t in terms for d in doc_tokens if t == d)
if score > 0:
scored.append((doc, score))
scored.sort(key=lambda x: x[1], reverse=True)
return [doc for doc, _ in scored[:k]]
@staticmethod
def _tokenize(text: str) -> list[str]:
tokens, buf = [], ""
for ch in text.lower():
if ch.isalnum() or '\u4e00' <= ch <= '\u9fff':
buf += ch
else:
if buf:
tokens.append(buf)
buf = ""
if buf:
tokens.append(buf)
return tokens
# ── Procedural Memory:工具和规则 ───────────────────────
TOOLS = {
"get_weather": "查询城市天气。参数:city, date。",
"search_places": "搜索景点。参数:query, k。",
}
RULES = [
"订票前必须确认用户预算。",
"轻松步行行程不超过 3 个景点。",
"下雨时优先推荐室内景点。",
]
# ── 带四种记忆的 Agent ──────────────────────────────────
def travel_agent(
model: MockLLM,
user_query: str,
*,
episodic: InMemoryKV,
semantic: SemanticStore,
) -> str:
"""
一个带四种记忆的旅游 Agent:
- Working: messages 列表
- Episodic: 教训和用户偏好(KV)
- Semantic: 文档检索(向量库)
- Procedural: 工具定义和规则(代码)
"""
# ── 1. Procedural Memory → system prompt ──
tools_text = "\n".join(f"- {name}: {desc}" for name, desc in TOOLS.items())
rules_text = "\n".join(f"- {r}" for r in RULES)
system_prompt = (
"你是杭州旅游助手。\n\n"
f"可用工具:\n{tools_text}\n\n"
f"必须遵守的规则:\n{rules_text}"
)
# ── 2. Episodic Memory → 读教训和偏好 ──
lessons = episodic.get("lessons", [])
preferences = episodic.get("user_preferences", {})
episodic_context = ""
if lessons:
episodic_context += "历史教训:\n" + "\n".join(f"- {l}" for l in lessons) + "\n\n"
if preferences:
episodic_context += "用户偏好:\n" + "\n".join(
f"- {k}: {v}" for k, v in preferences.items()
) + "\n\n"
print(f"[Episodic] 教训 {len(lessons)} 条,偏好 {len(preferences)} 项")
# ── 3. Semantic Memory → 检索相关文档 ──
docs = semantic.search(user_query, k=3)
semantic_context = ""
if docs:
semantic_context = "相关资料:\n" + "\n".join(
f"- [{d.doc_id}] {d.text[:80]}" for d in docs
) + "\n\n"
print(f"[Semantic] 检索到 {len(docs)} 条文档")
# ── 4. Working Memory → 构建 messages ──
messages: list[Message] = [
Message(role="system", content=system_prompt),
Message(
role="user",
content=(
f"{episodic_context}"
f"{semantic_context}"
f"用户问题:{user_query}"
),
),
]
print(f"[Working] messages 共 {len(messages)} 条")
# ── 生成回答 ──
answer = model.complete(messages)
# ── 5. 写回 Episodic Memory ──
# 保存这次交互的摘要
history = episodic.get("interaction_history", [])
history.append({"query": user_query, "answer_snippet": answer[:60]})
episodic.set("interaction_history", history)
print(f"[Episodic] 交互历史更新,共 {len(history)} 条")
return answer
# ── 运行 ────────────────────────────────────────────────
def main() -> None:
# 初始化 Episodic Memory(模拟之前积累的教训和偏好)
episodic = InMemoryKV()
episodic.set("lessons", [
"轻松步行要求时,景点不超过 3 个。",
"推荐室内景点时要注明开放时间。",
])
episodic.set("user_preferences", {
"步行偏好": "轻松,不赶路",
"饮食": "喜欢茶和素食",
})
# 初始化 Semantic Memory
semantic = SemanticStore([
Document("tea_museum", "中国茶叶博物馆,9:00-17:00,免费,室内,适合雨天。"),
Document("tea_village", "龙井村采茶体验,户外,步行友好。"),
Document("veggie", "杭州素食推荐:灵隐寺素面、龙井虾仁(可做素版)。"),
Document("west_lake", "西湖全天开放,免费,环湖步行约 3 小时。"),
])
model = MockLLM([
(
"根据您的偏好(轻松步行、喜欢茶和素食),推荐:\n"
"1. 上午:龙井村采茶体验(步行友好,约 2 小时)[tea_village]\n"
"2. 中午:灵隐寺素面 [veggie]\n"
"3. 下午:中国茶叶博物馆(室内,9:00-17:00,免费)[tea_museum]\n"
"全程 3 个景点,不赶路。"
),
])
answer = travel_agent(
model,
"杭州一日游,喜欢茶文化",
episodic=episodic,
semantic=semantic,
)
print(f"\n回答:\n{answer}")
# 验证记忆状态
print(f"\n记忆状态:")
print(f" 教训:{episodic.get('lessons')}")
print(f" 偏好:{episodic.get('user_preferences')}")
print(f" 历史:{episodic.get('interaction_history')}")
if __name__ == "__main__":
main()
运行日志
[Episodic] 教训 2 条,偏好 2 项
[Semantic] 检索到 3 条文档
[Working] messages 共 2 条
回答:
根据您的偏好(轻松步行、喜欢茶和素食),推荐:
1. 上午:龙井村采茶体验(步行友好,约 2 小时)[tea_village]
2. 中午:灵隐寺素面 [veggie]
3. 下午:中国茶叶博物馆(室内,9:00-17:00,免费)[tea_museum]
全程 3 个景点,不赶路。
记忆状态:
教训:['轻松步行要求时,景点不超过 3 个。', '推荐室内景点时要注明开放时间。']
偏好:{'步行偏好': '轻松,不赶路', '饮食': '喜欢茶和素食'}
历史:[{'query': '杭州一日游,喜欢茶文化', 'answer_snippet': '根据您的偏好(轻松步行、喜欢茶和素食),推荐:\n1. 上午:龙井村采茶体验(步行友好,约 2 小时)[tea_'}]
调试:trace 里看什么
| 要看的 | 怎么看 | 说明什么 |
|---|---|---|
| Episodic 读取 | prompt 里有没有教训和偏好 | 没有 = 记忆没被注入 |
| Semantic 命中 | 检索到几条 | 0 条 = 检索有问题 |
| Procedural 规则 | system prompt 里有没有规则 | 规则缺失 = 行为不受控 |
| Working memory 长度 | messages 数 | 太长 = context 溢出风险 |
| 记忆写回 | Episodic 更新了吗 | 没写回 = 下次会丢失 |
工程笔记
每种记忆的读写时机
| 记忆类型 | 什么时候读 | 什么时候写 |
|---|---|---|
| Working | 每次模型调用 | 每次工具返回后追加 |
| Episodic | 任务开始时 | 任务结束时(教训、摘要、偏好) |
| Semantic | 检索时 | 索引构建时、新文档入库时 |
| Procedural | Agent 初始化时 | 部署时更新代码/配置 |
存储方案选择
| 场景 | 推荐存储 | 原因 |
|---|---|---|
| 快速原型 | InMemoryKV + 内存列表 | 零依赖 |
| 单用户持久化 | JsonFileKV + JSONL | 文件即数据库 |
| 多用户生产 | PostgreSQL + pgvector | 事务 + 向量搜索 |
| 大规模文档 | 专用向量数据库 | 分布式检索 |
常见坑
| 坑 | 现象 | 修法 |
|---|---|---|
| Episodic 记忆无限增长 | prompt 越来越长 | 加过期、摘要、上限 |
| 跨用户记忆泄漏 | A 用户看到 B 的偏好 | 按用户隔离命名空间 |
| Semantic 检索污染 | 检索到无关文档 | 更好的分块、更好的嵌入 |
| Procedural 规则过时 | 旧规则不适用新场景 | 规则版本化 |
| Working memory 溢出 | context window 超限 | 摘要旧消息、滑动窗口 |
读完以后
Agent Memory 不是一个新模式,而是理解"记忆住在哪里、什么时候读写"的框架。
如果想了解怎么从失败中写教训,读 Reflexion。 如果想了解怎么检索外部知识,读 RAG 或 Agentic RAG。 如果想了解对话历史怎么管理,回到前面的章节。