构建生产级 Agent Memory 系统架构
很多人在做 Agent 时,第一版 Memory 的实现大概长这样:
memory = []
memory.append({"role": "user", "content": "你好"})
memory.append({"role": "assistant", "content": "你好!有什么可以帮你的?"})
import java.util.*;
List<Map<String, String>> memory = new ArrayList<>();
memory.add(Map.of("role", "user", "content", "你好"));
memory.add(Map.of("role", "assistant", "content", "你好!有什么可以帮你的?"));
跑几天后发现问题了:
- 对话一长,Token 消耗爆炸
- 用户上次说过的偏好,下次完全不记得
- 多个 Session 之间的信息没法共享
- 想查某个历史事实,得翻完整条对话
- 模型在长上下文里注意力稀释,反而答得更差
于是开始往里面塞向量数据库、塞总结、塞 Graph,结果又发现:Memory 系统不是越复杂越好,而是越适合你 Agent 的行为模式越好。
这篇文章不讲泛泛的概念,而是从工程角度拆解:一个生产级的 Agent Memory 系统到底该考虑哪些维度,每种方案解决什么问题、带来什么新问题,以及如何组合它们。
目录
- 一、Agent Memory 到底是什么
- 二、整体架构:三层分离
- 三、Episodic Memory(情节记忆):最基础也最容易搞砸的部分
- 四、Semantic Memory(语义记忆):从经历中提炼事实
- 五、Procedural Memory(程序记忆):Agent 怎么学会"做事情的方式"
- 六、Context Builder:决定往 Prompt 里塞什么
- 七、数据生命周期与维护
- 八、从 Claude Code 中看到的优秀设计
- 九、主流框架的 Memory 实现案例
- 十、生产级 Checklist
- 十一、总结
一、Agent Memory 到底是什么
很多人把 Memory 等同于"聊天记录",这是第一个坑。
在 Agent 的语境下,Memory 应该覆盖三类信息,每一类的存储和检索方式完全不同:
| 类型 | 定义 | 例子 | 特点 |
|---|---|---|---|
| 情节记忆 (Episodic) | Agent 经历过的具体交互过程 | 用户上一次问了什么、我上次怎么回答的 | 时序敏感、总量大、按时间衰减 |
| 语义记忆 (Semantic) | 从经历中提炼出的事实和知识 | 用户叫张三、他偏好 PostgreSQL、项目 deadline 是下周五 | 需要提取和泛化,跨 session 共享 |
| 程序记忆 (Procedural) | Agent 学会的做事方式和策略 | 用户习惯先概要再细节、工具调用失败后的回退策略 | 相对稳定,可跨 session 复用 |
大多数 Memory 系统只实现了"情节记忆"这一层——而且实现方式是"全量塞进上下文"。这种做法在 5 轮对话内还好,到 50 轮就基本不可用了。
所以生产级 Memory 的第一个设计决策是:把三类记忆分离开,用不同的策略管理它们。
二、整体架构:三层分离
先把架构轮廓画出来,再逐一拆开讲每层的设计。
flowchart TD
A["Agent 运行时"] --> B["Memory Orchestrator"]
B --> C["Episodic Store (情节)"]
B --> D["Semantic Store (语义)"]
B --> E["Procedural Store (程序)"]
C --> C1["Time-series DB / Log"]
C --> C2["Sliding Window Reader"]
D --> D1["Vector Store (嵌入)"]
D --> D2["Key-Value Store (结构化事实)"]
D --> D3["Graph Store (关系)"]
D --> D4["Summarizer + Extractor"]
E --> E1["Preference KV"]
E --> E2["Tool Usage Patterns"]
B --> F["Context Builder"]
F --> G["LLM Context Window"]
B --> H["Memory Maintenance"]
H --> H1["TTL Expiry"]
H --> H2["Consolidation"]
H --> H3["Importance Scoring"]
核心思路是:不在同一个存储系统里解决所有问题。 让每种记忆用最合适的工具管理,由 Orchestrator 统一协调"当前上下文里应该放什么"。
三、Episodic Memory(情节记忆):最基础也最容易搞砸的部分
问题本质
Episodic Memory(情节记忆)是一个时序日志。它的核心约束是:
- 写多读少(每次交互都写,只在构建上下文时读)
- 越旧的信息价值越低(但不一定为零)
- 总量线性增长,必须在某个点截断或压缩
常见实现方式对比
| 方案 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| 全量塞入 Context | 零实现成本 | Token 爆炸,稀释注意力 | 短对话(<10 轮) |
| Sliding Window(截断) | 简单可控 | 丢失早期关键信息 | 通用兜底 |
| 定期 Summarization | 压缩效果好 | 有信息损失,增加 LLM 调用成本 | 长对话,对信息完整性要求适中的场景 |
| 分层 Summarization | 保留细节和概要 | 实现复杂,管理多级缓存 | 极长会话(企业级场景) |
| Rolling Window + RAG | 兼顾近期细节和远期事实 | 需要嵌入和检索设施 | 大多数生产场景 |
推荐方案:分层管理
当前轮次(完整保留) → 最近 N 轮(滑动窗口) → 历史总结(压缩存储) → 必要时检索
具体来说:
- 当前轮次:完整保留本轮用户输入和 Agent 输出,用于本轮后续调用。
- 滑动窗口:保留最近 K 轮完整对话(K 取决于模型上下文长度和预算,一般 10-20 轮)。
- 历史总结:超出滑动窗口的内容,定期由 LLM 总结后压缩存储。
- 检索补充:当需要历史细节时,从原始日志中通过向量检索找回。
这个分层的好处是:日常推理只在滑动窗口和当前轮次上进行,不增加延迟。 只有确实需要历史信息时,才触发检索路径。
工程实现要点
以下是分层记忆管理的完整实现,Python 和 Java 各一版。两者都包含:滑动窗口、历史总结、向量检索补充、TTL 过期。
from collections import deque
from datetime import datetime, timedelta
from typing import Optional
import json
class EpisodicMemory:
"""分层情节记忆:当前轮次 → 滑动窗口 → 历史总结 → 向量检索"""
def __init__(self,
sliding_window_size: int = 20,
summary_llm=None,
vector_store=None,
ttl_days: int = 30):
self.window = deque(maxlen=sliding_window_size)
self.summary: Optional[str] = None
self.raw_store: list = [] # 完整日志,带时间戳
self.summary_llm = summary_llm # 用于总结的 LLM
self.vector_store = vector_store # 向量检索后端
self.ttl = timedelta(days=ttl_days)
def add(self, turn: dict):
"""写入一轮对话,附带时间戳"""
turn["timestamp"] = datetime.utcnow().isoformat()
self.raw_store.append(turn)
self.window.append(turn)
def build_context(self,
need_detail: bool = False,
query: Optional[str] = None) -> list:
"""组装最终送入 LLM 的上下文"""
context = []
# 1. 注入历史总结
if self.summary:
context.append({
"role": "system",
"content": f"[历史摘要] {self.summary}"
})
# 2. 滑动窗口(最近 K 轮)
context.extend(list(self.window))
# 3. 检索补充:按需从向量库召回
if need_detail and query and self.vector_store:
retrieved = self.vector_store.similarity_search(query, k=3)
for doc in retrieved:
context.append({
"role": "system",
"content": f"[历史记录] {doc}"
})
return context
def summarize(self):
"""将窗口中较旧的内容压缩为摘要"""
if not self.summary_llm:
return
# 保留最近 5 轮不压缩,压缩之前的
turns_to_summarize = list(self.window)[:-5]
if not turns_to_summarize:
return
prompt = (
f"以下是对话历史,请压缩为一段摘要(保留关键决定、"
f"用户偏好、任务进展):\n{json.dumps(turns_to_summarize, ensure_ascii=False)}"
)
new_summary = self.summary_llm(prompt)
# 增量合并摘要
if self.summary:
self.summary = (
f"{self.summary}\n[增量] {new_summary}"
)
else:
self.summary = new_summary
def cleanup_expired(self):
"""删除超出 TTL 的原始日志"""
cutoff = datetime.utcnow() - self.ttl
self.raw_store = [
t for t in self.raw_store
if t["timestamp"] >= cutoff.isoformat()
]
import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.function.Function;
/**
* 分层情节记忆 —— 滑动窗口 + 历史总结 + 向量检索 + TTL 过期
*/
public class EpisodicMemory {
private final int slidingWindowSize;
private final Deque<Map<String, Object>> window;
private final List<Map<String, Object>> rawStore;
private String summary;
private final Function<String, String> summaryLlm;
private final Function<String, List<String>> vectorStore;
private final Duration ttl;
public EpisodicMemory(int slidingWindowSize,
Function<String, String> summaryLlm,
Function<String, List<String>> vectorStore,
Duration ttl) {
this.slidingWindowSize = slidingWindowSize;
this.window = new ArrayDeque<>(slidingWindowSize);
this.rawStore = new ArrayList<>();
this.summary = null;
this.summaryLlm = summaryLlm;
this.vectorStore = vectorStore;
this.ttl = ttl;
}
/** 写入一轮对话,附带时间戳 */
public void add(Map<String, Object> turn) {
turn.put("timestamp", Instant.now().toString());
rawStore.add(new HashMap<>(turn));
if (window.size() >= slidingWindowSize) {
window.pollFirst();
}
window.addLast(turn);
}
/** 组装最终送入 LLM 的上下文 */
public List<Map<String, Object>> buildContext(
boolean needDetail, String query) {
List<Map<String, Object>> context = new ArrayList<>();
// 1. 注入历史摘要
if (summary != null && !summary.isEmpty()) {
context.add(Map.of("role", "system",
"content", "[历史摘要] " + summary));
}
// 2. 滑动窗口
context.addAll(window);
// 3. 向量检索补充
if (needDetail && query != null && vectorStore != null) {
List<String> retrieved = vectorStore.apply(query);
for (String doc : retrieved) {
context.add(Map.of("role", "system",
"content", "[历史记录] " + doc));
}
}
return context;
}
/** 将窗口中较旧的内容压缩为摘要 */
public void summarize() {
if (summaryLlm == null) return;
List<Map<String, Object>> toSummarize = new ArrayList<>(window);
// 保留最近 5 轮不压缩
if (toSummarize.size() <= 5) return;
toSummarize = toSummarize.subList(0, toSummarize.size() - 5);
String prompt = "压缩以下对话历史为一段摘要(保留关键决定、"
+ "用户偏好、任务进展):" + toSummarize;
String newSummary = summaryLlm.apply(prompt);
if (summary != null) {
summary = summary + "\n[增量] " + newSummary;
} else {
summary = newSummary;
}
}
/** 清理超出 TTL 的原始日志 */
public void cleanupExpired() {
Instant cutoff = Instant.now().minus(ttl);
rawStore.removeIf(turn -> {
String ts = (String) turn.get("timestamp");
return ts != null && Instant.parse(ts).isBefore(cutoff);
});
}
}
需要注意的是:总结也有信息损失。 是否需要检索能力来补充损失,取决于你的 Agent 在历史信息上的准确度要求。如果是客服场景,用户说"上次你们赔了我 500 块",你总结里没有这个数字,那就麻烦了。
四、Semantic Memory(语义记忆):从经历中提炼事实
这是最有价值、也最难做好的部分。
核心思路
不求"记住所有对话",而是从对话中提取有长期价值的事实。
典型流程:
flowchart LR
A["对话完成"] --> B["Extractor: 提取事实对"]
B --> C["Deduplicator: 去重/合并"]
C --> D[("Vector Store")]
C --> E[("KV Store")]
D --> F["Retriever: 语义检索"]
E --> G["Retriever: 精确匹配"]
提取什么
经验来看,以下几类信息值得提取:
- 用户画像:姓名、偏好、技术栈、行业
- 正在进行的任务:项目名、目标、关键限制
- 重要约定:用户明确说过的偏好或规则
- 已排除的方案:避免重复推荐
存储选型
| 信息类型 | 存储方案 | 检索方式 | 例子 |
|---|---|---|---|
| 事实性偏好 | KV Store (Redis / SQL) | 精确匹配 | user.preferred_language = "Java" |
| 语义化知识 | Vector Store | Top-K 语义检索 | “用户不喜欢用 MongoDB” |
| 实体关系 | Graph DB | 关系遍历 | “张三 → 负责 → 项目A” |
提取策略:被动 vs 主动
被动提取:每轮对话完成后,由 LLM 从对话中提取事实。优点是简单,缺点是有延迟。
主动提取:Agent 在对话过程中发现新信息时主动写入。优点是实时,缺点是增加 Agent 决策复杂度。
生产建议是两者结合:Agent 在对话中主动写入明确的事实(如"我叫张三"),后台定期跑批处理做被动提取兜底。
关键难点:事实的更新与冲突
用户说"我喜欢 PostgreSQL",下个月说"我换用 MySQL 了"。
这里的处理策略:
- 时间戳标记:每个事实记录写入时间,检索时优先返回最新的
- 版本化存储:保留历史版本,可追溯
- 置信度评分:明确表达 > 间接推断 > 默认值
{
"fact": "user.preferred_database",
"value": "MySQL",
"confidence": 0.95,
"source": "explicit_statement",
"updated_at": "2026-04-20T10:30:00Z",
"history": [
{"value": "PostgreSQL", "updated_at": "2026-03-15T08:00:00Z"}
]
}
五、Procedural Memory(程序记忆):Agent 怎么学会"做事情的方式"
这可能是三类记忆里最容易被忽视的。
Procedural Memory(程序记忆)解决的场景是:同一个用户,每一次交互 Agent 都要重新"试探"用户的偏好。比如:
- 用户喜欢先看结论再看细节
- 用户在做代码审查时希望逐文件过,不要一次性给所有 diff
- 用户习惯用中文提问,但代码注释要用英文
这些信息既不是"情节"也不是"事实",而是做事的方式。
实现方式
最简单的做法:在 KV Store 里存一组配置。
{
"user_id": "zhangsan",
"preferences": {
"response_style": "concise_with_details",
"code_review_mode": "per_file",
"language": "zh",
"code_comments": "en"
}
}
进阶做法是让 Agent 自己学习和沉淀:
- 每次交互后,让 Agent 判断是否学到了用户的新偏好
- 如果识别到稳定的行为模式,写入 Procedural Memory(程序记忆)
- 下次交互时,将相关偏好注入 system prompt
何时写入
这是 Procedural Memory(程序记忆)最微妙的地方:写入太积极,用户会觉得你固执;写入太保守,又记不住。
一个工程上的折中是"三次确认"规则:
同一行为模式出现三次以上,才写入 Procedural Memory(程序记忆)。出现一次反例,移除或降级该模式。
六、Context Builder:决定往 Prompt 里塞什么
上面三类 Memory 是数据层,Orchestrator 需要把它们组装成最终送入 LLM 的上下文。
这是整个系统中对延迟和效果影响最大的环节。
组装策略
以下实现展示 Orchestrator 如何协调三层记忆,并按 Token 预算组装上下文。
from dataclasses import dataclass
from typing import Optional
@dataclass
class TokenBudget:
"""每类内容的 Token 预算软限制"""
system_prompt: int = 1024
procedural: int = 512
semantic: int = 1024
episodic: int = 4096
current_turn: int = 1536
class ContextBuilder:
"""将三层记忆按策略组装为 LLM 上下文"""
def __init__(self,
procedural_store,
semantic_store,
episodic_store,
budget: Optional[TokenBudget] = None):
self.procedural_store = procedural_store
self.semantic_store = semantic_store
self.episodic_store = episodic_store
self.budget = budget or TokenBudget()
def build(self, request: str, user_id: str,
scene: str = "general") -> list:
"""根据场景决定检索策略"""
system_parts = []
# 1. 程序记忆 —— 固定注入,体积小
procedural = self.procedural_store.get(user_id)
if procedural:
system_parts.append({
"role": "system",
"content": f"[用户偏好] {self._truncate(procedural, self.budget.procedural)}"
})
# 2. 语义记忆 —— 按场景决定是否检索
if self._should_retrieve(scene):
semantic = self.semantic_store.query(
user_id=user_id,
query=request,
top_k=5,
min_score=0.7
)
if semantic:
system_parts.append({
"role": "system",
"content": f"[相关事实] {self._truncate(semantic, self.budget.semantic)}"
})
# 3. 情节记忆 —— 分层加载
need_detail = self._need_historical_detail(scene, request)
episodic = self.episodic_store.build_context(
need_detail=need_detail,
query=request if need_detail else None
)
return system_parts + episodic
def _should_retrieve(self, scene: str) -> bool:
"""按场景决定是否触发语义检索"""
retrieval_scenes = {"task_continuation", "qa", "tool_call"}
return scene in retrieval_scenes
def _need_historical_detail(self, scene: str, request: str) -> bool:
"""判断是否需要检索历史细节"""
if scene in {"task_continuation", "tool_call"}:
return True
if any(kw in request for kw in ["刚才", "之前", "上次", "还记得"]):
return True
return False
@staticmethod
def _truncate(text: str, max_tokens: int) -> str:
"""按 Token 预算截断(简易版,生产应使用 tokenizer)"""
words = text.split()
if len(words) * 1.3 > max_tokens: # 粗略估算
limit = int(max_tokens / 1.3)
return " ".join(words[:limit]) + "..."
return text
import java.util.*;
/**
* 将三层记忆按策略组装为 LLM 上下文。
* 支持 Token 预算控制和场景驱动的检索策略。
*/
public class ContextBuilder {
private final ProceduralStore proceduralStore;
private final SemanticStore semanticStore;
private final EpisodicMemory episodicStore;
private final TokenBudget budget;
public ContextBuilder(ProceduralStore proceduralStore,
SemanticStore semanticStore,
EpisodicMemory episodicStore,
TokenBudget budget) {
this.proceduralStore = proceduralStore;
this.semanticStore = semanticStore;
this.episodicStore = episodicStore;
this.budget = budget;
}
/** 按场景组装上下文 */
public List<Map<String, Object>> build(String request, String userId, String scene) {
List<Map<String, Object>> parts = new ArrayList<>();
// 1. 程序记忆 —— 固定注入
String procedural = proceduralStore.get(userId);
if (procedural != null && !procedural.isEmpty()) {
parts.add(Map.of("role", "system",
"content", "[用户偏好] " + truncate(procedural, budget.procedural)));
}
// 2. 语义记忆 —— 按场景检索
if (shouldRetrieve(scene)) {
List<SemanticResult> semantic = semanticStore.query(userId, request, 5, 0.7);
if (!semantic.isEmpty()) {
parts.add(Map.of("role", "system",
"content", "[相关事实] " + truncate(semantic.toString(), budget.semantic)));
}
}
// 3. 情节记忆 —— 分层加载
boolean needDetail = needHistoricalDetail(scene, request);
List<Map<String, Object>> episodic = episodicStore.buildContext(needDetail, request);
parts.addAll(episodic);
return parts;
}
private boolean shouldRetrieve(String scene) {
return Set.of("task_continuation", "qa", "tool_call").contains(scene);
}
private boolean needHistoricalDetail(String scene, String request) {
if (Set.of("task_continuation", "tool_call").contains(scene)) return true;
return request.contains("刚才") || request.contains("之前")
|| request.contains("上次") || request.contains("还记得");
}
private String truncate(String text, int maxTokens) {
String[] words = text.split("\\s+");
if (words.length * 1.3 > maxTokens) {
int limit = (int) (maxTokens / 1.3);
return String.join(" ", Arrays.copyOf(words, limit)) + "...";
}
return text;
}
/** Token 预算配置 */
public record TokenBudget(
int systemPrompt,
int procedural,
int semantic,
int episodic,
int currentTurn
) {
public TokenBudget() {
this(1024, 512, 1024, 4096, 1536);
}
}
// 以下为依赖接口定义
public interface ProceduralStore {
String get(String userId);
}
public interface SemanticStore {
List<SemanticResult> query(String userId, String query, int topK, double minScore);
}
public record SemanticResult(String fact, double score, String updatedAt) {}
}
Token 预算分配
生产实践中一个重要的经验:提前为不同类型的内容分配 Token 预算。 而不是"能塞多少塞多少"。
例如一个 8K 上下文的 Agent:
| 内容类型 | 预算 | 说明 |
|---|---|---|
| System Prompt | 1K | 角色、规则、输出格式 |
| Procedural Memory(程序记忆) | 0.5K | 用户偏好 |
| Semantic Memory(语义记忆) | 1K | 检索到的相关事实 |
| Episodic Window(情节窗口) | 4K | 最近 N 轮对话 |
| 当前轮次 | 1.5K | 本次输入 + 中间结果 |
这个预算是个软限制——当某一项超过预算时,不是直接截断,而是压缩或降级。例如 Semantic Memory(语义记忆)有 10 条匹配结果但只放得下 5 条,就取置信度最高的 5 条。
检索触发策略
不是每一轮都需要检索。根据场景决定是否需要查 Semantic Memory(语义记忆):
- 闲聊场景:不检索,只走 Episodic(情节记忆)
- 任务延续场景:检索任务相关的 Semantic(语义)信息
- 知识问答场景:优先检索 Semantic(语义),再查 Episodic(情节)
- 工具调用场景:检索类似的工具使用案例
七、数据生命周期与维护
Memory 不是"写进去就不管了"。生产级系统必须考虑:
1. TTL 与过期
不同类型的记忆有不同的生命周期:
| 类型 | 建议 TTL | 原因 |
|---|---|---|
| Episodic(情节·原始日志) | 7-30 天 | 用于调试和审计,过久无意义 |
| Episodic(情节·总结) | 永久(可压缩) | 摘要本身占空间小 |
| Semantic(语义·用户事实) | 90 天或用户明确更新 | 长期偏好可能变化 |
| Semantic(语义·会话事实) | 随会话结束 | 例如"正在处理订单 #12345" |
| Procedural(程序) | 永久,但需要验证 | 稳定的行为模式 |
2. Consolidation(合并)
定期将多条相关事实合并为更精简的表达。例如:
[1] 用户说喜欢 Java
[2] 用户说用 Spring Boot
[3] 用户说主要在写微服务
→ 合并:用户是 Java / Spring Boot 后端开发者,专注微服务架构
合并减少 Token 占用,也提高信息的密度。但需要注意:合并不可逆,必须保留原始数据用于回溯。
3. Importance Scoring(重要性评分)
不是所有信息都有长期保留价值。可以在写入时给每条记忆打分:
import re
from typing import Set, Optional
class ImportanceScorer:
"""记忆重要性评分 —— 多信号加权评估"""
def __init__(self, current_goal: Optional[str] = None):
self.current_goal = current_goal
def score(self, content: str) -> float:
signals = []
# 用户明确强调
if self._has_emphasis(content):
signals.append((0.9, "user_emphasis"))
# 包含个人身份信息
if self._contains_pii(content):
signals.append((0.85, "pii"))
# 涉及当前任务目标
if self._relates_to_goal(content):
signals.append((0.75, "goal_relevant"))
# 包含具体数值或时间
if self._contains_specifics(content):
signals.append((0.7, "has_specifics"))
# 情感强烈
if self._has_strong_sentiment(content):
signals.append((0.6, "strong_sentiment"))
# 默认识别
signals.append((0.3, "default"))
best_score, best_reason = max(signals, key=lambda x: x[0])
return best_score
# ---- 信号检测方法 ----
_EMPHASIS_KEYWORDS: Set[str] = {"记住", "重要的是", "一定要注意", "千万别", "务必"}
def _has_emphasis(self, text: str) -> bool:
return any(kw in text for kw in self._EMPHASIS_KEYWORDS)
def _contains_pii(self, text: str) -> bool:
# 简单 PII 模式匹配
patterns = [
r'\b\d{3,4}-?\d{4}-?\d{4}\b', # 手机号/银行卡
r'[我](?:叫|是|的(?:名字|邮箱|电话)(?:是|为))[一-鿿]+',
]
return any(re.search(p, text) for p in patterns)
def _relates_to_goal(self, text: str) -> bool:
if not self.current_goal:
return False
goal_keywords = set(self.current_goal.split())
text_keywords = set(text.split())
return len(goal_keywords & text_keywords) > 0
_SPECIFICS_PATTERNS = [
r'\d+', # 数字
r'\d{4}-\d{2}-\d{2}', # 日期
r'https?://\S+', # 链接
]
def _contains_specifics(self, text: str) -> bool:
return any(re.search(p, text) for p in self._SPECIFICS_PATTERNS)
def _has_strong_sentiment(self, text: str) -> bool:
strong_words = {"糟糕", "太好了", "气死了", "完美", "绝对不行", "非常满意"}
return any(w in text for w in strong_words)
import java.util.*;
import java.util.regex.Pattern;
/**
* 记忆重要性评分 —— 多信号加权评估。
* 用于判断哪些记忆值得长期保留。
*/
public class ImportanceScorer {
private final String currentGoal;
public ImportanceScorer(String currentGoal) {
this.currentGoal = currentGoal;
}
/** 对一段记忆内容进行重要性评分 [0, 1] */
public double score(String content) {
List<ScoredSignal> signals = new ArrayList<>();
if (hasEmphasis(content))
signals.add(new ScoredSignal(0.9, "user_emphasis"));
if (containsPii(content))
signals.add(new ScoredSignal(0.85, "pii"));
if (relatesToGoal(content))
signals.add(new ScoredSignal(0.75, "goal_relevant"));
if (containsSpecifics(content))
signals.add(new ScoredSignal(0.70, "has_specifics"));
if (hasStrongSentiment(content))
signals.add(new ScoredSignal(0.60, "strong_sentiment"));
signals.add(new ScoredSignal(0.30, "default"));
return signals.stream()
.mapToDouble(ScoredSignal::score)
.max()
.orElse(0.3);
}
// ---- 信号检测 ----
private static final Set<String> EMPHASIS_KEYWORDS = Set.of(
"记住", "重要的是", "一定要注意", "千万别", "务必");
boolean hasEmphasis(String text) {
return EMPHASIS_KEYWORDS.stream().anyMatch(text::contains);
}
private static final Pattern PII_PATTERN = Pattern.compile(
"\\b\\d{3,4}-?\\d{4}-?\\d{4}\\b");
boolean containsPii(String text) {
return PII_PATTERN.matcher(text).find();
}
boolean relatesToGoal(String text) {
if (currentGoal == null || currentGoal.isEmpty()) return false;
// 简易关键词重叠判断
Set<String> goalWords = new HashSet<>(Arrays.asList(currentGoal.split("\\s+")));
Set<String> textWords = new HashSet<>(Arrays.asList(text.split("\\s+")));
goalWords.retainAll(textWords);
return !goalWords.isEmpty();
}
private static final Pattern SPECIFICS_PATTERN = Pattern.compile(
"\\d+|\\d{4}-\\d{2}-\\d{2}|https?://\\S+");
boolean containsSpecifics(String text) {
return SPECIFICS_PATTERN.matcher(text).find();
}
private static final Set<String> STRONG_SENTIMENT_WORDS = Set.of(
"糟糕", "太好了", "气死了", "完美", "绝对不行", "非常满意");
boolean hasStrongSentiment(String text) {
return STRONG_SENTIMENT_WORDS.stream().anyMatch(text::contains);
}
private record ScoredSignal(double score, String reason) {}
}
低分记忆在 Consolidation 阶段优先被合并或丢弃。
八、从 Claude Code 中看到的优秀设计
我在分析 Claude Code 源码 时,注意到它的 Memory 系统有一些值得借鉴的设计决策:
1. 文件即数据库
没有用向量数据库或 Redis,而是直接用文件系统(memory/ 目录)。每个记忆是一个独立的 .md 文件,通过 MEMORY.md 作为倒排索引管理。
这个设计的好处:
- 零基础设施依赖:不需要额外启动 DB 服务
- 可读可审计:任何时刻都能打开文件查看记忆内容
- 版本控制友好:记忆变化可以通过 git diff 追踪
缺点也很明显:不适合大规模场景。但这个设计思路的价值在于:先选最简单的存储,等确实不够用了再升级。
2. 类型化记忆
Claude Code 把记忆分为 user、feedback、project、reference 四种类型。每种类型有不同的写入策略、读取优先级的更新逻辑。
类型化的意义:不是所有的记忆都被同等对待。
user记忆在每次对话开始时加载feedback记忆在需要判断行为模式时检索project记忆有有效期,过期自动忽略
3. 检索时的验证机制
它对每个从记忆中读取的"事实"都做验证:
如果记忆提到函数 X 存在 → 调用前检查文件是否存在
如果记忆提到标志位 Y 存在 → grep 确认
这个机制解决了 Memory 系统中一个核心问题:记忆是过去某个时刻的快照,不是当前状态。 直接信任记忆而不验证,会导致 Agent 引用已经不存在的东西。
4. 不混淆"记忆"和"任务状态"
Claude Code 明确把当前对话的工作状态(plan、tasks)和跨对话的记忆分开。Plan 和 Task 是临时工作产物,Memory 是长期沉淀。这两类信息有不同的生命周期管理策略,放在一起会导致混乱。
九、主流框架的 Memory 实现案例
理论讲完,来看几个主流框架实际怎么做 Memory。它们的取舍能帮你理解"理论到工程"的映射。
1. LangChain:模块化 Memory 体系
LangChain 把 Memory 设计为独立的 Chain 间的状态传递层,提供多种 Memory 类型:
| Memory 类型 | 底层原理 | 适用场景 |
|---|---|---|
ConversationBufferMemory | 全量缓存对话历史到列表 | 短对话、调试 |
ConversationSummaryMemory | 每次交互后 LLM 总结,存摘要 | 长对话压缩 |
ConversationBufferWindowMemory | 固定大小滑动窗口 | 通用兜底 |
VectorStoreRetrieverMemory | 对话存入向量库,按相似度检索 | 需要历史回忆的场景 |
PostgresChatMessageHistory | PostgreSQL 持久化 | 生产环境、多实例 |
关键设计思路:LangChain 把 Memory 抽象为两个操作 —— load_memory_variables(读)和 save_context(写),你可以自由组合不同的存储后端与不同的 Memory 策略。
from langchain.memory import ConversationSummaryBufferMemory
from langchain_community.chat_message_histories import SQLChatMessageHistory
memory = ConversationSummaryBufferMemory(
llm=llm,
max_token_limit=2000,
chat_memory=SQLChatMessageHistory(
session_id="user_123",
connection_string="sqlite:///memory.db"
),
return_messages=True
)
优缺点:模块化好、可组合性强;但 Memory 和 Agent 逻辑耦合较紧,大量对话时 performance 是瓶颈。
2. CrewAI:基于角色的记忆分工
CrewAI 的 Memory 系统设计更贴近"多 Agent 协作"场景。每个 Agent 可以有独立的记忆,支持三类:
- ShortTermMemory:基于
ConversationBufferWindowMemory,保存最近交互 - LongTermMemory:基于 SQLite,记录任务执行的任务和结果
- EntityMemory:基于向量存储,提取和记住任务中提到的实体信息
from crewai import Agent, Task, Crew, Process
from crewai.memory import LongTermMemory
agent = Agent(
role="数据分析师",
goal="分析用户数据并提供洞察",
memory=True, # 开启记忆
long_term_memory=LongTermMemory() # 使用长期记忆
)
优缺点:开箱即用、多 Agent 场景表现出色;但定制灵活性不如 LangChain,Memory 类型扩展受限。
3. Mem0:专注"记忆层"的独立框架
Mem0 是一个专注于 LLM Memory 层的独立框架(可以看作"Memory 即服务")。它的设计思路与本文最接近:
- 三层记忆:User Memory、Session Memory、Agent Memory
- 自动提取:从对话中自动提取事实和偏好
- 历史版本:记录事实的变更历史,支持回滚
- 多种存储:支持向量库(Qdrant、Pinecone)、图数据库、KV 存储
from mem0 import Memory
m = Memory(
vector_store="qdrant", # 向量存储
graph_store="neo4j", # 图存储(可选)
llm="gpt-4", # 用于事实提取
)
# 写入记忆
m.add("用户说:我喜欢 PostgreSQL,但最近换成了 MySQL", user_id="zhangsan")
# 检索记忆
results = m.search("用户用什么数据库", user_id="zhangsan")
print(results)
# → "用户偏好数据库从 PostgreSQL 切换到了 MySQL(更新于 2026-04-20)"
优缺点:独立于任何 Agent 框架,API 干净,三层的设计理念和本文一致;但较新,生态不够成熟,大规模场景的性能需验证。
4. Spring AI:Java 生态的 Memory 实现
Spring AI 是为 Java / Spring 生态设计的 AI 框架,其 Memory 通过 ChatMemory 接口抽象,并提供开箱即用的实现:
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.chat.memory.jdbc.JdbcChatMemory;
// 内存实现 —— 开发和测试
ChatMemory memory = new InMemoryChatMemory();
// JDBC 实现 —— 生产环境,自动建表
ChatMemory jdbcMemory = new JdbcChatMemory(dataSource);
// 使用:在 ConversationChain 中注入
ConversationChain chain = new ConversationChain(
chatClient,
"user-session-001",
jdbcMemory
);
优缺点:Java / Spring 生态的无缝集成,生产级 JDBC 持久化;但目前只有"全量记录"和"滑动窗口"两种策略(通过 PromptMessageUtils 截断),缺少语义记忆和向量检索支持。需要自行对接 Spring AI 的 VectorStore 来补充。
5. AgentScope4J:observe/call 分离与 MsgHub 记忆共享
AgentScope4J 是一个 Java 生态的多 Agent 框架,它的 Memory 设计有些独特的工程决策。
核心设计:observe 与 call 分离
这是 AgentScope4J 最值得注意的设计——把"写入记忆"和"触发推理"拆成两个独立操作:
ReActAgent agent = ReActAgent.builder()
.name("analyst")
.sysPrompt("你是研究员,先澄清问题,再调用工具")
.memory(windowMemory) // 短期记忆
.maxTurns(8)
.build();
agent.observe(Msg.user("请分析 A 公司利润变化")).block(); // 只写记忆,不触发模型
Msg answer = agent.call().block(); // 读取记忆,执行推理
为什么这样设计?
- 防止"收到消息就自动回复"的无限连锁反应
- 让调度层精准控制"何时生成",而不是让模型决定
- 回放和调试更容易:输入与触发时机是可复现的
记忆的写入与读取路径
AgentScope4J 的 Memory 是一个接口,WindowMemory 是默认实现:
// 写入路径
protected Mono<Void> doObserve(Msg msg) {
memory.addMessage(msg); // 追加到滑动窗口
return Mono.empty();
}
// 读取路径 — call() 时构建快照
public Mono<Msg> call() {
return plannerLoop(memory.snapshot()); // 快照不可变,避免并发问题
}
snapshot() 返回当前记忆的不可变副本,后续工具执行结果通过 observe(Msg.tool(result)) 写回,形成"读时快照、写时追加"的天然隔离。
MsgHub:多 Agent 记忆共享
AgentScope4J 通过 MsgHub 实现跨 Agent 记忆共享:
try (MsgHub hub = MsgHub.builder()
.name("analysis_hub")
.participants(researcher, riskReviewer, writer)
.announcement(Msg.system("请围绕同一主题协作"))
.enableAutoBroadcast(true)
.build()) {
hub.enter().block();
researcher.call().block(); // call 完成后,结果自动 broadcast
riskReviewer.call().block(); // 上一轮结果已通过 observe 写入记忆
writer.call().block();
}
enableAutoBroadcast(true) 的含义:某个 Agent call() 完成后,其输出通过 broadcast 推送给其他参与者并触发 observe(),从而实现"共享上下文但不自动触发推理"——避免了 Agent 间无限连锁调用。
ReAct 循环中的记忆流转
在 AgentScope4J 的 ReAct 实现中,每轮推理都从 memory.snapshot() 构建上下文,工具结果写回后进入下一轮:
for (int i = 0; i < maxTurns; i++) {
ModelOutput out = llm.generate(memory.snapshot()); // 读快照
if (out.hasToolCall()) {
ToolResult tr = toolGateway.invoke(out.toolCall());
memory.addMessage(Msg.tool(tr)); // 写回记忆
continue;
}
return Msg.assistant(out.finalAnswer());
}
这套机制映射到本文的三层架构来看:
| 本文概念 | AgentScope4J 对应 |
|---|---|
| Episodic Memory(情节) | WindowMemory + observe() 追加 |
| Semantic Memory(语义) | 需自行实现,框架提供 observe 扩展点 |
| Procedural Memory(程序) | 通过 system prompt 动态注入 |
| Context Builder | memory.snapshot() 构建不可变上下文 |
优缺点:observe/call 分离是工程上非常务实的设计,MsgbHub 的多 Agent 记忆共享机制简洁干净;但 Semantic Memory 和 Procedural Memory 需要自行扩展,框架本身没有提供事实提取和检索的基础设施。
6. Hermes Agent(Nous Research):四层记忆与可插拔 Provider
Hermes Agent 是 Nous Research 开发的自主 AI Agent 框架,它的 Memory 系统是目前开源社区中最完整的设计之一,分为 四层内建记忆 + 可插拔外部 Provider。
架构总览
flowchart TD
A["System Prompt"] --> B["Layer 1: Prompt Memory"]
B --> B1["MEMORY.md(~800 tokens)"]
B --> B2["USER.md(~500 tokens)"]
A --> C["Layer 2: Session Archive"]
C --> C1["SQLite + FTS5 全文检索"]
A --> D["Layer 3: Skills"]
D --> D1["Markdown 技能文件"]
A --> E["Layer 4: External Provider"]
E --> E1["Hindsight / Mem0 / OpenViking…"]
Layer 1:Prompt Memory(热记忆)
Hermes 最核心的设计:每次会话启动时,将 MEMORY.md 和 USER.md 的内容作为固定快照注入 system prompt。
# MEMORY.md 存放的内容
- 持久事实(用户的长期偏好、项目约定)
- 环境信息(开发环境、API Key 位置)
- 已确定的决策记录
# USER.md 存放的内容
- 用户基本信息
- 交互风格偏好
- 高频需求模式
关键机制 —— Frozen Snapshot(冻结快照):
- 会话启动时,读取文件内容,冻结为 system prompt 的一部分
- 会话中写入的修改立即持久化到磁盘,但不会出现在当前会话的 prompt 中
- 修改在下一次会话启动时才生效
这样做的目的是保持 LLM Prefix Cache 的稳定性——如果 prompt 在会话中变化,缓存命中率会大幅下降。这是工程效率和 Agent 智能之间的一个务实取舍。
Layer 2:Session Archive(冷记忆)
所有 CLI 交互和消息历史存储在 SQLite 数据库中(~/.hermes/state.db),使用 FTS5 全文检索引擎。
# Hermes 通过 session_search 工具按需检索历史
result = agent.search_session("我们之前讨论过什么数据库方案?")
# → FTS5 全文匹配,返回相关历史片段
Agent 可以在对话中自主决定何时调用 session_search 工具来回忆历史——不需要每轮都加载。
Layer 3:Skills(程序记忆)
Hermes 将"做事的方式"沉淀为 Skill 文件,存储在 ~/.hermes/skills/ 目录下:
- 每个 Skill 是一个 Markdown 文件,描述完成某类任务的步骤
- 任务完成后由 Agent 自动创建(反应式学习)
- 后续遇到类似任务时,Agent 自动检索并复用相关 Skill
- 通过复用和修正实现自我进化
这是与本文的 Procedural Memory 最直接对应的实现——Agent 自己学会怎么做事情。
Layer 4:External Provider(可插拔外部记忆)
Hermes 近期推出了统一的 Pluggable Memory Provider 系统,运行 hermes memory setup 选择:
| Provider | 存储 | 核心特性 | LongMemEval |
|---|---|---|---|
| Hindsight | PostgreSQL(本地/云) | 知识图谱 + 结构化事实 + 反射合成 | 91.4% |
| Holographic | 本地 SQLite | HRR 代数 + 信任评分,零依赖 | — |
| OpenViking | 自托管 | L0/L1/L2 分层加载,节省 80-90% Token | — |
| Mem0 | 云端 | 最快部署(30 秒),服务端 LLM 提取 | 67.6% |
| Honcho | 云端 | 辩证用户建模(学习你的思维方式) | — |
| ByteRover | 本地/云 | 可读 Markdown 知识树 + 预压缩提取 | — |
| RetainDB | 云端 | 混合搜索(向量 + BM25 + 重排序) | — |
Hermes Memory Provider 不是 RAG。RAG 解决的是"模型知识不足,检索外部文档增强回答"——读多写少,写入后基本不变。Memory Provider 解决的是"Agent 需要跨会话记住用户、事实和偏好"——持续读写,需要处理事实更新、冲突解决、版本追溯。两者的核心区别在于写入频率和更新语义。Provider 中的 RetainDB 和 OpenViking 确实用到了向量检索技术,但那是实现手段,不是设计目的。
重要设计原则:外部 Provider 是叠加在内建记忆之上的,不替代内建记忆。MEMORY.md / USER.md 始终活跃。
与本文三层架构的映射
| 本文概念 | Hermes 对应实现 |
|---|---|
| Episodic Memory(情节) | FTS5 Session Archive + 全量会话历史 |
| Semantic Memory(语义) | MEMORY.md(结构化事实)+ 外部 Provider(知识图谱) |
| Procedural Memory(程序) | Skills 目录 + 自动创建/复用机制 |
| Context Builder | Frozen Snapshot 注入 system prompt |
| Memory Maintenance | TTL 由 Provider 管理 + Agent 自主 curation |
优缺点:四层记忆设计是目前开源社区最完整的方案之一,Frozen Snapshot 机制在工程效率上非常聪明,7 种可插拔 Provider 覆盖了从本地到云端的各种场景;但对于简单场景来说配置偏重,核心的 FTS5 检索依赖关键词匹配(语义检索需要外部 Provider),Agent 自主 curation 在短会话中可能导致 MEMORY.md 为空。
7. OpenClaw:文件即记忆的"梦境"系统
OpenClaw 是一个以 文件优先、本地优先 为设计哲学的 AI Agent 框架。它的记忆系统非常独特:以 Markdown 文件为数据源,通过一个名为 Dreaming(梦境) 的异步机制完成记忆的沉淀和固化。
记忆文件体系
OpenClaw 的记忆 = 一组有明确职责划分的 Markdown 文件:
| 文件 | 职责 | 读写时机 |
|---|---|---|
MEMORY.md | 长期记忆(决策、事实、经验教训) | Agent 自主写入,Dreaming 晋升 |
memory/YYYY-MM-DD.md | 每日对话日志 / 会话笔记 | 每次会话自动追加 |
AGENTS.md | 工作区规则与安全边界 | 启动时加载 |
USER.md | 用户画像(姓名、偏好等) | Agent 写入,跨会话持久 |
SOUL.md | Agent 人格与价值观定义 | 几乎不变 |
DREAMS.md | Dreaming 系统输出(仅人工审查) | Dreaming 写入 |
这个设计与 Claude Code 的文件即记忆思路同源,但多了 SOUL.md 和 DREAMS.md 两个概念。
内建记忆组件
OpenClaw 提供两个开箱即用的记忆后端:
# 默认:memory-core —— 本地 Markdown + 混合检索
from openclaw.memory import MemoryCore
memory = MemoryCore() # BM25 + 向量混合搜索
# 可选:memory-lancedb —— 本地向量数据库
from openclaw.memory import LanceDBMemory
memory = LanceDBMemory() # 自动召回 + 自动捕获
关键限制:两者都缺乏多用户隔离,所有用户的数据混在同一存储空间中,会导致"记忆污染"。
Dreaming System(梦境系统):异步记忆固化
这是 OpenClaw 最具创新性的设计 —— 一个基于 Cron 调度的后台记忆处理管线,模拟人类睡眠的记忆固化过程:
Light Sleep(浅睡)→ REM Sleep(快速眼动)→ Deep Sleep(深睡)
Light Sleep(浅睡):摄入对话日志,做 Jaccard 去重
REM Sleep(快速眼动):模式分析,候选事实选择,置信度评分
Deep Sleep(深睡):六维评分筛选候选事实,达到阈值后晋升到 MEMORY.md:
| 评分维度 | 含义 |
|---|---|
| Frequency(频率) | 该信息出现的次数 |
| Relevance(相关性) | 与当前任务的相关程度 |
| Diversity(多样性) | 覆盖不同场景的程度 |
| Recency(时效性) | 最近是否还被提及 |
| Consolidation(整合度) | 与其他事实的关联程度 |
| Conceptual Richness(概念丰富度) | 信息的抽象层次 |
默认晋升条件:综合评分 ≥ 0.80,且至少被 3 个独立信号触发。
企业级方案
OpenClaw 社区和云厂商提供了多个企业级记忆扩展:
| 方案 | 提供方 | 核心能力 |
|---|---|---|
| memory-agentcore | AWS | Bedrock AgentCore Memory,4 种提取策略,层级命名空间,多用户隔离 |
| AgentLoop MemoryStore | 阿里云 | 多维提取(偏好/事实/摘要/情节),异步非阻塞管线,L3 Agentic 检索 |
| PolarDB Mem0 | 阿里云 | 云原生长期记忆,事实自动演化与冲突解决,多设备同步 |
| @mem0/openclaw-mem0 | Mem0 | 系统层强制捕获/召回,每轮自动 capture + 自动 recall |
与本文三层架构的映射
| 本文概念 | OpenClaw 对应实现 |
|---|---|
| Episodic Memory(情节) | memory/YYYY-MM-DD.md 每日日志 |
| Semantic Memory(语义) | MEMORY.md + Dreaming 系统 + 外部 Provider |
| Procedural Memory(程序) | AGENTS.md(规则)+ Agent 配置文件 |
| Context Builder | 文件读取 → 注入 system prompt |
| Memory Maintenance(维护) | Dreaming 管线(Light → REM → Deep Sleep) |
优缺点:Dreaming 系统是唯一一个模拟生物记忆固化机制的工程实现,文件即记忆的设计非常透明可审计,企业级方案丰富;但内建组件缺乏多用户隔离,Dreaming 的 Jaccard 去重无法处理语义级别的重复,统计评分机制(信号计数)而非语义重要性判断可能导致关键信息被遗漏。
8. AutoGen(Microsoft):对话驱动的记忆共享
Microsoft AutoGen 通过 Agent 间的对话驱动模式管理记忆。每个 Agent 有自己的上下文,通过 ConversableAgent 的 chat_history 管理和检索历史:
from autogen import ConversableAgent, AssistantAgent
# Agent 1:用户代理
user = ConversableAgent("user",
max_consecutive_auto_reply=0,
human_input_mode="ALWAYS")
# Agent 2:带记忆的助手
assistant = AssistantAgent("assistant",
llm_config={"config_list": [{"model": "gpt-4"}]},
system_message="你是有记忆的助手")
# 多轮对话 —— 记忆在 chat_history 中累积
user.initiate_chat(assistant, message="我上周说的项目方案还记得吗?")
# 查看历史
print(len(assistant.chat_history)) # 累计消息数
优缺点:多 Agent 通信和记忆共享机制灵活;但本身不提供长期记忆持久化或语义检索,生产部署需要用 Redis 或 MongoDB 做外部存储扩展。
框架对比小结:Hermes vs OpenClaw
在所有框架中,Hermes Agent 和 OpenClaw 代表了两种差异鲜明的设计哲学。放在一起对比,能更清楚地看到 Memory 系统设计中的核心取舍:
| 对比维度 | Hermes Agent | OpenClaw |
|---|---|---|
| 设计哲学 | 多层架构 + 可插拔 Provider | 文件即记忆 + 生物模拟 |
| 热记忆 | MEMORY.md + USER.md,Frozen Snapshot 注入 system prompt,会话中不更新 | MEMORY.md + USER.md + SOUL.md(人格)+ AGENTS.md(规则),直接读取注入 |
| 冷记忆 | SQLite + FTS5 全文检索,按需 session_search | memory/YYYY-MM-DD.md 每日日志,按文件时间访问 |
| 程序记忆 | Skills 目录,Agent 任务完成后自动创建/复用 | AGENTS.md 中定义的规则,偏静态配置 |
| 最大亮点 | Frozen Snapshot —— 保护 LLM Prefix Cache,工程效率极高 | Dreaming System —— Light/REM/Deep Sleep 三级管线,模拟生物记忆固化 |
| 记忆固化方式 | Agent 自主 curation + Provider 后台提取 | 异步 Cron 调度,六维评分晋升到 MEMORY.md |
| 外部扩展 | 7 种 Provider(Hindsight/Mem0/OpenViking 等),统一 Pluggable 接口 | 5 种企业方案(AWS Bedrock/阿里云 AgentLoop/PolarDB/Mem0) |
| 多用户隔离 | 由 Provider 各自实现 | 内建组件不支持,企业级方案才支持 |
| 语义检索 | 依赖外部 Provider(内建 FTS5 仅关键词) | 内建 memory-core 支持 BM25 + 向量混合搜索 |
| Agent 自主性 | Agent 决定写什么到 MEMORY.md,短会话可能为空 | Dreaming 异步处理,不依赖 Agent 实时决策 |
| 运维复杂度 | 中等,Provider 配置需要学习成本 | 低(内建)/ 高(企业方案),Dreaming 需要 Cron |
| 最适合场景 | 需要长期连续对话 + 跨会话记忆的项目 | 单用户深度使用 + 对记忆透明度要求高的场景 |
一句话区分:Hermes 的强项是工程效率(Frozen Snapshot + Pluggable Provider),OpenClaw 的强项是记忆固化的仿生机制(Dreaming System)。选择哪个,取决于你对"记忆"的核心诉求是"低成本的跨会话持久化"还是"高保真的信息沉淀"。
十、生产级 Checklist
如果你正在设计一个 Agent Memory 系统,以下是需要回答的问题清单:
存储层
- 三类记忆有没有明确的分离?
- 每种记忆的存储选型是否匹配其访问模式?
- 写入路径是否考虑了去重和更新?
- 是否有 TTL 和过期策略?
检索层
- 检索是不是每次请求都做,还是按需触发?
- 检索结果的排序和过滤策略是什么?
- 跨 Session 的记忆共享规则是什么?
- 事实更新后,旧版本的缓存是否及时失效?
上下文组装层
- 不同类型的记忆按什么优先级注入 Prompt?
- Token 预算有没有按类型分配?
- 长上下文的压缩/截断策略是什么?
运维层
- 记忆系统的读写延迟是否可观测?
- 是否有审计日志追踪"Agent 读了哪些记忆"?
- 当记忆数据损坏或错误时,能否手动修正?
十一、总结
Agent Memory 不是一个"用向量数据库存起来"就能解决的问题。它是一个从数据采集 → 提取 → 存储 → 检索 → 组装 → 维护的完整链路。每个环节都有自己的权衡:
- 存得越多,检索越慢,上下文越臃肿
- 提取越积极,信息损失越少,但噪音也越多
- 跨 Session 共享越开放,Agent 越智能,隐私风险越大
好的 Memory 系统不是把所有信息都记住,而是在正确的时候,把正确的信息,以正确的形式,放到正确的位置。
生产级的设计思路可以总结为:
- 分类型管理,不用一种方案解决所有问题
- 分层存储,热数据放窗口、冷数据放检索
- 按需检索,不每轮都查,减少无效延迟
- 验证而非信任,记忆是过去状态的快照,使用前需要验证
- 从简单开始,先用文件 + KV,不够再加向量和 Graph