短期记忆 + 长期记忆 + Context 压缩,让有了 Memory 的 Agent 不再失忆。
大家好,我是二哥呀。
PaiCLI 第二期做完 Plan-Execute 之后,有个问题一直让我很头疼——Agent 的记忆力跟金鱼一样,聊着聊着就忘了前面说过什么。
你跟它说“我喜欢用 JDK 17”,清空对话再来一轮,它又傻x地给你生成 JDK 8 的代码。
这期就来解决这个问题,给 Agent 加上完整的 Memory 系统。

整个 Memory 分三块。
短期记忆,管的是当前对话的上下文——用户说了啥、工具返回了啥、Agent 做了哪些决策。LLM 本身是无状态的,每次请求都是独立的,根本不记得上一轮聊了什么。短期记忆就是替它“记着”,下次输入的时候把历史消息一起带上。

但短期记忆有个致命缺点——会话一关,全没了。下次启动 Agent,它完全不知道你之前的偏好、项目用什么技术栈、代码风格有什么约定。
所以得有长期记忆,把这些跨会话的关键信息持久化到磁盘上,不管开多少个会话窗口,Agent 都能记住。
那问题又来了,上下文窗口是有限的。Claude Opus 4.6 默认 200K,听着很大,但短期记忆加长期记忆一股脑全塞进去,不光浪费 token,不相关的信息还会干扰 LLM 的判断。

所以需要上下文压缩——达到阈值就做摘要,保留关键信息,丢掉冗余细节。这样 LLM 才有足够的空间思考,手里拿到的也都是有用的上下文。
01、Memory 的整体设计
先看全貌。

MemoryManager 是整个系统的门面,底下管着 ConversationMemory、LongTermMemory、ContextCompressor、TokenBudget、MemoryRetriever 五个组件。Agent 不用关心这些细节,对外就暴露两个操作——存消息、取记忆。存的时候自动管预算,取的时候自动检索相关上下文。
02、记忆的基本单元
记忆不是一堆字符串往列表里塞就完事了。Agent 得知道每条记忆是什么类型——对话还是事实?
什么时候产生的——用来做时间衰减。
占多少 token——用来做预算管理。有没有附加信息——比如这条事实来自哪个项目。
所以每条记忆条目长这样:
public class MemoryEntry {
private final String id;
private final String content;
private final MemoryType type;
private final Instant timestamp;
private final Map metadata;
private final int tokenCount;
public enum MemoryType {
CONVERSATION, // 对话记忆
FACT, // 事实记忆(用户偏好、项目信息)
SUMMARY, // 摘要记忆
TOOL_RESULT // 工具执行结果
}
}
CONVERSATION 是对话消息,FACT 是用户偏好、项目信息这类关键事实,SUMMARY 是压缩后的摘要,TOOL_RESULT 是工具执行结果。
TOOL_RESULT 之所以单独分出来,是因为工具返回的内容通常特别长(比如读一个文件能返回几百行),但检索的时候又需要知道“之前执行过什么命令”。标记出来之后,压缩时可以对工具结果更激进地砍,对话内容则多保留一些语义。
token 估算也得有个方法,预算管理全靠它:
public static int estimateTokens(String text) {
if (text == null || text.isEmpty()) return 0;
long chineseChars = text.chars()
.filter(c -> c > 0x4E00 && c < 0x9FFF).count();
long otherChars = text.length() - chineseChars;
return (int) Math.ceil(chineseChars / 1.5 + otherChars / 4.0);
}
中文约 1.5 字一个 token,英文约 4 个字符一个 token。精确计算得用 tokenizer。
04、短期记忆
短期记忆干两件事:存消息和自动淘汰。
token 预算是有限的,聊多了总会撑满。这时候最简单的策略就是淘汰最旧的消息——跟操作系统的页面置换一个道理,内存不够了就把最久没用的页面换出去。对话是顺序的,最旧的消息通常最不重要,FIFO 就够了。
public class ConversationMemory implements Memory {
private final LinkedHashMap entries;
private final int maxTokens;
private final AtomicInteger currentTokens;
private final List compressedSummaries;
@Override
public void store(MemoryEntry entry) {
entries.put(entry.getId(), entry);
currentTokens.addAndGet(entry.getTokenCount());
// 超出预算时自动淘汰最旧的条目
while (currentTokens.get() > maxTokens && entries.size() > 1) {
evictOldest();
}
}
}
被淘汰的消息不会直接扔掉,而是放进 compressedSummaries 列表,等着后面压缩成摘要——信息还在,只是换了个更紧凑的形态。
getUsageRatio() 返回当前 token 使用率,超过 80% 的时候 MemoryManager 就会自动触发 ...
企业级Agent工作流编排项目PaiFlow
Vibe Coding版本的PaiAgent
派聪明RAG AI知识库Java版本+Go版本
微服务 PmHub、技术派、MYDB
求职派JobClaw(OpenClaw/Hermes架构
PaiCLI(类似Claude Code的Agent
派简历(代码已完成)
等实战项目。
1. 微信扫右侧的优惠券加入知识星球
2. 解锁星球的实战项目教程和源码: 项目源码+教程获取
8 条评论
回复