杠精派
派聪明
0/512

热门评论

17377927899 2026年07月05日 21:36
引用原文:没有相关记忆的时候恢复原始 system prompt,有记忆就追加到末尾。干净利落
@派聪明 为什么要恢复原始 system prompt
点赞
回复3 取消回复
派聪明 2026年07月05日 21:36
你选中的这句代码,背后是 **"上下文隔离"** 的设计原则。 简单说:**没有相关记忆时,system prompt 必须保持干净,不能残留上一轮的旧信息。** 具体原因有三: ### 1. 避免信息污染 假设上一轮用户问了"JDK 版本偏好",系统把 `[用户使用JDK 17]` 追加到了 system prompt 里。如果这一轮用户问的是"帮我写个 Python 脚本",而长期记忆中没搜到相关事实,此时如果不恢复原始 prompt,那 system prompt 里还残留着 `[用户使用JDK 17]`。LLM 看到这条信息,可能会莫名其妙地认为"用户喜欢 JDK 17,所以 Python 脚本也得兼容 JDK 17?"——这就产生了幻觉。 ### 2. 记忆是按需注入的 文章里 MemoryRetriever 的检索逻辑是:**每次根据当前 query 动态捞取最相关的记忆**。有相关记忆才注入,没相关记忆就不注入。如果不恢复原始 prompt,上一轮的注入内容会一直赖在 system prompt 里,相当于绕过了检索器的判断,变成了"永久注入"。 ### 3. 保持 system prompt 的纯净与可预测 system prompt 是 LLM 行为的"宪法",里面应该只放**当前轮次真正需要**的背景信息。无关信息塞进去,既浪费 token,又可能干扰 LLM 的判断。所以用 `conversationHistory.set(0, ...)` 直接替换第 0 条(system 消息),有记忆就换带记忆的版本,没记忆就换回原版,确保每一轮的 system prompt 都是**刚好够用、不多不少**。
点赞
回复 取消回复
展开剩余 2 条回复

41 条评论

etovo 2026年07月07日 16:41
引用原文:具执行结果也同步存入
@派聪明 那如果有相关记忆,不就直接加到后面了吗,比如说问了个python的问题,原本是喜欢用java17,现在又加上喜欢python,变成又喜欢java又喜欢python了
点赞
回复1 取消回复
派聪明 2026年07月07日 16:41
这个问题问得很细,我来拆解一下。 是的,按文章里的实现逻辑,**两条事实确实都会被存到长期记忆里**——因为你说的"喜欢JDK 17"和"喜欢Python"内容不同,去重检查不会拦截。 那注入到 system prompt 时,如果同时命中,就变成这样: ``` [原始System Prompt] [事实] 用户喜欢用 JDK 17 [事实] 用户喜欢 Python ``` 这时候 LLM 看到的上下文就是**"用户既喜欢 Java 又喜欢 Python"**。这在语义上其实不一定错——一个人可以同时喜欢两种技术。 **但问题在于**:如果用户的意思是"我现在不用 Java 了,改用 Python",那旧事实就成了误导信息。 文章的 Memory 系统**没有做事实的更新或删除机制**,这是一个明显的短板。目前只有去重(内容完全相同才跳过)和时间衰减(三天前的事实检索权重更低),但衰减不等于删除——时间久了它只是排在后面,不会完全消失。 更理想的设计应该加几种机制: 1. **事实冲突检测**:新事实和旧事实在语义上矛盾时(比如"用 JDK 17" vs "不用 Java 了"),提示用户确认或自动覆盖旧事实 2. **置信度打分**:每条事实附带一个置信度,新事实覆盖旧事实时置信度更高,或者用户明确说"记一下"的权重更高 3. **手动删除**:CLI 加个 `/forget 关键词` 命令 目前文章里的实现,确实会出现你说的"又喜欢 Java 又喜欢 Python"的情况。如果想避免,可以在 `/clear` 时让 LLM 做的**事实提取更智能一点**——检测用户是否在推翻之前的偏好,主动把旧事实标记为过期,而不是一股脑全留着。
点赞
回复 取消回复
etovo 2026年07月07日 16:34
引用原文:24 小时内从 1.0 线性衰减到 0.5,三天前的旧事权重自然就低了;
@派聪明 时间衰减是专门针对长期记忆,还是短期也有
点赞
回复1 取消回复
派聪明 2026年07月07日 16:35
根据你摘录的这段文本,**时间衰减是在「记忆检索」(MemoryRetriever)阶段起作用**,它**既作用于长期记忆,也作用于短期记忆**。 具体来说,`MemoryRetriever.retrieve()` 方法在做相关性评分时,会同时从两层记忆里捞条目: 1. **从短期记忆中检索** → 对拿到的每条记忆计算相关度分数 2. **从长期记忆中检索** → 对拿到的每条记忆计算相关度分数(额外 ×1.2 来源加权) 时间衰减是 `computeRelevanceScore` 评分公式里的一个维度(跟关键词匹配、来源加权并列),无论条目来自短期还是长期,**都会根据它的时间戳做线性衰减**——24 小时内 1.0→0.5,三天后更低。 --- **不过要注意区分两个机制**: | 阶段 | 短期记忆淘汰 | 检索时的时间衰减 | |---|---|---| | 触发时机 | `store()` 超过 token 预算时 | `retrieve()` 时 | | 策略 | **FIFO**(淘汰最旧消息) | **加权降分**(不淘汰,只是排序靠后) | | 作用范围 | 只对短期记忆 | 短期 + 长期都有 | 所以总结一句话:**短期记忆里的消息淘汰走 FIFO(按 token 预算),但检索排序时两层记忆都用时间衰减降权。** 你摘录的这段描述的就是检索评分环节的时间衰减逻辑。
点赞
回复 取消回复
etovo 2026年07月07日 16:32
引用原文:不能把 100 条记忆全塞给 LLM
@派聪明 为什么是100条
点赞
回复1 取消回复
派聪明 2026年07月07日 16:32
这句话里的 **"100 条"** 并不是一个精确的技术阈值,而是一个**夸张的举例说法**,用来强调"记忆太多会浪费 token 且干扰 LLM 判断"这个核心问题。 结合前后文来看,作者想表达的意思是: > "Agent 处理新输入的时候,不能把**所有**短期记忆 + 长期记忆一股脑全塞给 LLM,大部分跟当前任务根本不沾边,纯属浪费 token。" 至于到底该保留多少条、多少 token,文章实际用的是 **TokenBudget 预算控制**,规则是: - 对话历史占用超过 **80% 的上下文预算**时,触发上下文压缩(ContextCompressor),把旧消息压缩成摘要。 - 检索时通过 `MemoryRetriever.retrieve(query, limit)` 按相关度排序,只取 `limit` 条(这个 limit 可以在代码里配置,一般远小于 100)。 所以**100 只是随口说的一个数**,别当真。实际系统中,Agent 不会傻到把 100 条原样丢给 LLM,而是会: 1. 按相关度排序,捞最相关的几条(比如 5~10 条) 2. 超预算就做摘要压缩 3. 长期记忆还要加上时间衰减,太久远的自动降权
点赞
回复 取消回复
越努力越幸运 2026年07月05日 23:37
引用原文:用了 jieba 分词器做中文优先的轻量匹配
@派聪明 讲一下这个分词器
点赞
回复1 取消回复
派聪明 2026年07月05日 23:37
好的,结合参考资料,来给你讲一下这个 **jieba 分词器**。 简单来说,jieba 分词器是 Java 生态中一个比较成熟的中文分词库。它的核心作用,就是**把一段连续的中文文本,精准地切分成一个个有意义的词语**。 举个例子,用户说“项目技术栈”,原始字符串是连续的,但 jieba 能正确切分成 **“项目”** 和 **“技术”** 两个词。 在那个 PaiCLI 项目里,为什么要用它呢?因为 Agent 的长期记忆需要做检索。如果没有分词,搜索“项目技术栈”就只能用字符串 `contains` 去硬匹配,很难命中“项目”或“技术”这两个关键字。用了 jieba 分词后,检索系统就能更智能地找到相关的记忆条目,命中率比简单的字符串匹配高得多。 文章中还提到了它的一个细节:分词后还会**过滤掉单字符和纯标点**(比如“的”、“了”、“是”这些没有检索价值的字),避免给检索结果带来噪音。 当然,文章也坦诚地指出了 jieba 的局限性:它本质上是**关键词匹配**,比如搜索“Java 框架”匹配不到“Spring Boot”这种深层次的语义关系。要解决这个问题,就需要更高级的向量检索技术了。
点赞
回复 取消回复
越努力越幸运 2026年07月05日 23:36
引用原文: jieba 分词器
讲一下这个分词器
点赞
回复 取消回复
阿博 2026年07月05日 22:15
@派聪明 短期记忆确实没有用到呀,目前最新的代码,短期记忆只进不出,没有人消费。实际起作用的还是对话历史conversationHistory,甚至对话历史还有自己的压缩方法:ConversationHistoryCompactor * 第 3 期 Memory 设计假设"LLM 调用从 shortTermMemory 重建消息",但实际 Agent 直接维护 * conversationHistory,与 shortTermMemory 并行。两个度量错位导致旧版压缩从未真正缩短 * 即将发给 LLM 的 token——本类是在 Agent.run 主循环里"调 LLM 前评估并压缩"的补丁。 所以我觉得这应该是个历史遗留问题,希望能改一下
点赞
回复1 取消回复
派聪明 2026年07月05日 22:15
你说得非常对,这确实是一个**架构设计与代码实现脱节**的历史遗留问题。 ## 问题根因 第 3 期文章里设计的 Memory 系统模型是: ``` Agent.run → 存消息到 shortTermMemory → 检索 → 从 shortTermMemory 重建上下文 → 调 LLM ``` 但实际代码中 Agent 走的是另一条路: ``` Agent.run → 存消息到 conversationHistory(ArrayList)→ 调 LLM(直接传 conversationHistory) ``` **短期记忆 shortTermMemory 变成了一个没人消费的"影子组件"**——只管存、不管取,Agent 真正依赖的还是 conversationHistory 这个裸列表。而 conversationHistory 又有自己的 `ConversationHistoryCompactor` 来做压缩,等于**两套压缩逻辑共存但互不知晓**,token 预算根本管不住。 ## 正确的改进方向 应该把两套机制合一,核心思路是:**短期记忆就是对话历史,对话历史就是短期记忆**。 具体来说有两种方案: ### 方案 A:短期记忆替代 conversationHistory(推荐) 让 `ConversationMemory` 内部持有并管理所有对话消息,Agent 不再维护独立的 `conversationHistory`。 ```java // Agent.run 中 public String run(String userInput) { // 1. 存消息 → 短期记忆是唯一入口 memoryManager.addUserMessage(userInput); // 2. 从短期记忆重建 LLM 的 messages 列表 // 短期记忆内部已做过压缩/淘汰,直接吐出当前有效的消息 List<Message> messages = shortTermMemory.toMessages(); // 3. 注入长期记忆上下文到 system prompt String memoryContext = memoryManager.buildContextForQuery(userInput, 500); messages.set(0, 追加了记忆上下文的 system message); // 4. 调 LLM response = llmClient.chat(messages, tools); } ``` 这样 `ConversationHistoryCompactor` 就不需要了,压缩逻辑统一由 `ContextCompressor` 接管。 ### 方案 B:ConversationHistory 实现 Memory 接口 让 `ConversationHistory` 实现 `Memory` 接口,把 token 预算管理、自动淘汰、压缩触发全都内聚到 `ConversationHistory` 里,短期记忆只是它的一个"快照"或代理层。但这个方案改动更大,侵入性更高。 ## 总结 你发现的这个问题确实存在——**短期记忆成了摆设,对话历史绕过了整个 Memory 系统**。第 3 期文章里的设计图很完美,但代码落地时出现了偏差。应该把对话历史纳入 Memory 系统的统一管理下,让短期记忆真正成为"即将发给 LLM 的消息的持有者",而不是一个只进不出的缓存池。 感谢指出,我后面会提 PR 把这个问题修掉。
点赞
回复 取消回复
17377927899 2026年07月05日 21:36
引用原文:没有相关记忆的时候恢复原始 system prompt,有记忆就追加到末尾。干净利落
@派聪明 为什么要恢复原始 system prompt
点赞
回复3 取消回复
派聪明 2026年07月05日 21:36
你选中的这句代码,背后是 **&quot;上下文隔离&quot;** 的设计原则。 简单说:**没有相关记忆时,system prompt 必须保持干净,不能残留上一轮的旧信息。** 具体原因有三: ### 1. 避免信息污染 假设上一轮用户问了&quot;JDK 版本偏好&quot;,系统把 `[用户使用JDK 17]` 追加到了 system prompt 里。如果这一轮用户问的是&quot;帮我写个 Python 脚本&quot;,而长期记忆中没搜到相关事实,此时如果不恢复原始 prompt,那 system prompt 里还残留着 `[用户使用JDK 17]`。LLM 看到这条信息,可能会莫名其妙地认为&quot;用户喜欢 JDK 17,所以 Python 脚本也得兼容 JDK 17?&quot;——这就产生了幻觉。 ### 2. 记忆是按需注入的 文章里 MemoryRetriever 的检索逻辑是:**每次根据当前 query 动态捞取最相关的记忆**。有相关记忆才注入,没相关记忆就不注入。如果不恢复原始 prompt,上一轮的注入内容会一直赖在 system prompt 里,相当于绕过了检索器的判断,变成了&quot;永久注入&quot;。 ### 3. 保持 system prompt 的纯净与可预测 system prompt 是 LLM 行为的&quot;宪法&quot;,里面应该只放**当前轮次真正需要**的背景信息。无关信息塞进去,既浪费 token,又可能干扰 LLM 的判断。所以用 `conversationHistory.set(0, ...)` 直接替换第 0 条(system 消息),有记忆就换带记忆的版本,没记忆就换回原版,确保每一轮的 system prompt 都是**刚好够用、不多不少**。
点赞
回复 取消回复
展开剩余 2 条回复
17377927899 2026年07月05日 21:29
引用原文:MemoryRetriever 干的就是从两层记忆里把最相关的条目捞出来。
@派聪明 短期记忆也要检索吗,不是全部拿来给LLM
点赞
回复1 取消回复
派聪明 2026年07月05日 21:29
你的观察很敏锐!文章里确实写了从**两层记忆**(短期+长期)中检索,但这里需要厘清一件事——**短期记忆里的内容,也不是一股脑全塞给LLM的**。 原因有三: **① 短期记忆有Token预算上限** 短期记忆(ConversationMemory)设了 `maxTokens`,聊多了会触发淘汰。被淘汰的旧消息会被压缩成摘要,但摘要只是更紧凑的形态,信息量依然很大。如果每次把所有摘要+近期消息全丢给LLM,上下文窗口很快就撑爆了。 **② 不是所有短期记忆都与当前问题相关** 短期记忆里积累的是整场对话的历史消息,包括工具执行结果(比如读文件返回几百行代码)。当前用户问的是“JDK版本”,那把之前读文件的结果也塞进去,纯属浪费token,还会干扰LLM判断。 **③ 检索筛选后再注入,效果更好** 代码里 `MemoryRetriever.retrieve()` 会计算相关度得分(关键词匹配 + 时间衰减 + 来源加权),只取出最相关的 top-K 条目拼成上下文注入到 system prompt。**近期对话(最近3轮)会原样保留在 `conversationHistory` 里直接发给LLM**,但更早的已压缩内容则走检索通道。 所以总结一句话:**短期记忆中“近期”的消息直接保留在对话历史里发给LLM,而“历史”部分(包括压缩摘要)也通过检索来捞最相关的内容**——不是全部拿来,也不是全部丢掉,而是按需筛选。
点赞
回复 取消回复
17377927899 2026年07月05日 21:28
引用原文:两层记忆
短期记忆也要检索吗,不是全部拿来给LLM
点赞
回复 取消回复
安安 2026年07月02日 16:37
引用原文:短期记忆
看过代码发现,短期记忆存了两份,一份是conversationHistory这个list中,用于喂给模型,也会有对应的方法对其进行压缩,而ConversationMemory这个类似乎没有了具体作用,记忆检索实际执行也不会检索短期记忆,因为短期记忆本身就以conversationHistory的形式存在于llm中。ConversationMemory唯一的作用似乎只剩下了通过/memory指令返回的短期记忆条数和tokens用量估算。@派聪明
点赞
回复1 取消回复
派聪明 2026年07月02日 16:37
你的观察非常敏锐,不过这里有几个细节需要澄清,**短期记忆并不是&quot;存了两份冗余数据&quot;**,而是两个职责不同的组件协同工作: --- ### ① `ConversationMemory` 的核心职责不止是统计 从文章代码可以看到,`ConversationMemory` 干了三件 `conversationHistory` 做不了的事: - **自动淘汰与压缩触发**:每次 `store()` 存入新消息后,会检查 `currentTokens &gt; maxTokens`,自动淘汰最旧条目,并将被淘汰的消息放入 `compressedSummaries` 列表中,等待 `ContextCompressor` 压缩成摘要。 - **Token 预算检测**:`getUsageRatio()` 返回当前使用率,`MemoryManager.compressIfNeeded()` 依赖它来判断是否触发压缩。而 `conversationHistory` 只是原始的 `List&lt;Message&gt;`,没有 token 统计能力。 - **结构化记忆类型管理**:每条 `MemoryEntry` 都带 `MemoryType`(CONVERSATION/FACT/SUMMARY/TOOL_RESULT),这是后续压缩策略差异化的基础——工具结果可以更激进地砍,对话内容多保留语义。 --- ### ② 记忆检索**确实会检索短期记忆** 文章第 8 节 `MemoryRetriever.retrieve()` 的代码明确写了: ```java // 从短期记忆中检索 for (MemoryEntry entry : shortTermMemory.getAll()) { double score = computeRelevanceScore(entry, query); if (score &gt; 0) scored.add(new ScoredEntry(entry, score, true)); } ``` 这个检索发生在**当前对话中**,目的是从已经被压缩淘汰的旧摘要(`compressedSummaries`)和近期消息里捞取高相关度内容,注入到 system prompt 中。虽然对话历史本身在 `conversationHistory` 里,但**检索器需要结构化的、带标签的记忆条目**来做分词匹配和时间衰减,这是原始历史消息做不到的。 --- ### ③ 真正的&quot;两份&quot;关系 | 组件 | 格式 | 用途 | |------|------|------| | `conversationHistory` | `List&lt;Message&gt;` | 直接喂给 LLM 的原始消息 | | `ConversationMemory` | `Map&lt;String, MemoryEntry&gt;` | Token 预算管理、淘汰策略、压缩触发、检索支撑 | 前者是**给 LLM 吃的格式**,后者是**给 Agent 内部管理用的结构化数据**。两者数据同源但形态不同,不是重复存储,而是**同一份记忆在不同抽象层的投影**。 --- 所以总结一句话:`ConversationMemory` 不是摆设,它是整个记忆系统的**调度器和数据底座**,没了它,压缩策略、预算控制、结构化检索全都无从下手。
点赞
回复 取消回复

目录