[{"data":1,"prerenderedAt":302},["ShallowReactive",2],{"content:article:agent-memory-pipeline":3,"content:article-surround:\u002Farticles\u002Fagent-memory-pipeline":293},{"id":4,"title":5,"body":6,"category":276,"cover":277,"date":278,"description":279,"draft":280,"extension":281,"featured":280,"meta":282,"navigation":283,"path":284,"seo":285,"stem":286,"tags":287,"updated":291,"__hash__":292},"articles\u002Farticles\u002Fagent-memory-pipeline.md","Agent 的记忆，不止 ChatMemory：从五轮对话到带冲突检测的记忆系统",{"type":7,"value":8,"toc":267},"minimark",[9,13,17,25,36,40,46,62,69,75,96,100,107,114,124,131,137,163,166,176,190,193,207,217,227,230,243,249,255,258,264],[10,11,12],"h2",{"id":12},"一条让我睡不着觉的用户反馈",[14,15,16],"p",{},"Gym-Agent 上线两周后，收到一条反馈，用户原话：\"我上周不是问过你凌晨能不能订场吗？怎么又问我要订哪个场馆？\"",[14,18,19,20,24],{},"我第一反应是去查 Redis——当然什么都没有。",[21,22,23],"code",{},"RedisChatMemory"," 配的是 5 轮窗口，别说上周，用户昨天聊的内容都随会话一起蒸发干净了。对用户来说，\"上周的我\"和\"今天的我\"是同一个 Agent；对我来说，它们是两个毫无关系的 memoryId。",[14,26,27,28],{},"那一刻我意识到，",[29,30,31,32,35],"strong",{},"用户对\"记住我\"的预期，和 ",[21,33,34],{},"ChatMemory"," 能给的能力之间，隔着一条鸿沟。",[10,37,39],{"id":38},"chatmemory-到底是什么不是什么","ChatMemory 到底是什么、不是什么",[14,41,42,43,45],{},"先把 ",[21,44,34],{}," 的能力边界说清楚，LangChain4j 给我的东西是真实的：",[47,48,49,56],"ul",{},[50,51,52,55],"li",{},[29,53,54],{},"会话内上下文","：按 memoryId 隔离，窗口内多轮对话模型都能接住；",[50,57,58,61],{},[29,59,60],{},"指代消解的原料","：因为窗口里有原文，\"这个场馆\"\"时间改成下午\"这类指代，模型接得住。",[14,63,64,65,68],{},"在 Gym-Agent 里我们把这套用到极致：",[21,66,67],{},"@SystemMessage"," 里写死了多轮规则（\"用户说'这个场地'时指代上一轮查询结果中的场地\"），窗口从 5 轮调到 8 轮——再往上调，单次请求的 token 开销就压不住了，而场馆预约这个场景的信息密度又低，8 轮对话里有效信息可能就 3 句，剩下的全是\"好的\"\"谢谢\"\"帮我看看\"这种寒暄，纯粹是花钱让模型读废话。",[14,70,71,72,74],{},"这暴露了 ",[21,73,34],{}," 的三个结构性局限：",[76,77,78,84,90],"ol",{},[50,79,80,83],{},[29,81,82],{},"窗口截断","：只有最近 N 轮，窗口外的信息物理性消失；",[50,85,86,89],{},[29,87,88],{},"只有原文，没有提炼","：存的是对话流水，不是\"用户是谁、他要什么\"这种可复用的知识；",[50,91,92,95],{},[29,93,94],{},"生命周期等于会话","：会话结束，记忆归零。这对场馆预约还能忍（今天订场和上周订场没什么连续性），对绝大多数长周期场景是致命的。",[10,97,99],{"id":98},"job-agent记忆从上下文变成资产","Job-Agent：记忆从\"上下文\"变成\"资产\"",[14,101,102,103,106],{},"做 Job-Agent 的时候，记忆问题绕不过去了。求职是",[29,104,105],{},"以周为单位的长周期过程","：用户第一周说\"期望薪资 15-20k，base 只考虑杭州\"，第三周改口\"其实上海也可以，薪资好商量\"。如果 Agent 只靠会话记忆，用户每次来都要重新自我介绍一遍；更糟的是，它可能拿着三周前的旧偏好，给一个刚改了期望的用户推一堆不匹配的岗位。",[14,108,109,110,113],{},"所以这一次，我们把记忆当成核心资产来设计，而不是对话的附属品。整个 ",[21,111,112],{},"memory\u002F"," 包最后长这样（类名都是真实的）：",[115,116,121],"pre",{"className":117,"code":119,"language":120},[118],"language-text","memory\u002F\n├── AgentMemoryCandidate.java        \u002F\u002F 记忆候选（还没入库的）\n├── AgentMemoryLlmExtractor.java     \u002F\u002F LLM 通道：从对话里捞语义记忆\n├── AgentMemoryRuleExtractor.java    \u002F\u002F 规则通道：正则\u002F关键词抓硬信息\n├── AgentMemoryActionClassifier.java \u002F\u002F 动作分类：CREATE\u002FUPDATE\u002FDELETE\u002FIGNORE\n├── AgentMemoryActionDecision.java   \u002F\u002F 动作决策\n└── AgentMemoryWritePolicy.java      \u002F\u002F 写入策略：能不能写、怎么写\n","text",[21,122,119],{"__ignoreMap":123},"",[14,125,126,127,130],{},"设计思路是一条",[29,128,129],{},"五段式管道","：对话流 → 提取 → 分类 → 策略 → 入库。逐段说。",[14,132,133,136],{},[29,134,135],{},"第一段：双通道提取。"," 记忆候选从两个通道产生：",[47,138,139,149],{},[50,140,141,144,145,148],{},[21,142,143],{},"AgentMemoryRuleExtractor","（规则通道）：正则 + 关键词，抓",[29,146,147],{},"硬信息","——城市、薪资区间、工作年限、学历。快、免费、零幻觉，但只会抓\"明说\"的。",[50,150,151,154,155,158,159,162],{},[21,152,153],{},"AgentMemoryLlmExtractor","（LLM 通道）：拿一个小参数模型，从最近几轮对话里捞",[29,156,157],{},"软信息","——\"用户在准备 Java 后端方向\"\"用户对大小周比较敏感\"\"用户似乎在犹豫要不要去上海\"。规则抓不住的语义交给模型，产出统一是 ",[21,160,161],{},"AgentMemoryCandidate","（候选），带置信度和来源对话轮次。",[14,164,165],{},"为什么不全用 LLM 通道？贵、慢，而且它会\"热心过头\"——见下一个坑。",[14,167,168,171,172,175],{},[29,169,170],{},"第二段：动作分类。"," 候选不能直接 append 进库。",[21,173,174],{},"AgentMemoryActionClassifier"," 先判断这条候选对应什么动作：",[47,177,178,181,184,187],{},[50,179,180],{},"CREATE：全新偏好，如第一次提到\"期望城市\"；",[50,182,183],{},"UPDATE：和已有记忆冲突，如\"15-20k\" → \"20k 以下不考虑\"；",[50,185,186],{},"DELETE：用户明确否定，如\"我不想去外包了\"要删掉相关偏好；",[50,188,189],{},"IGNORE：吐槽、玩笑、假设句（\"要是我也有五年经验就好了\"不是事实）。",[14,191,192],{},"这一步是整个系统里 LLM 判断压力最大的环节，它本质是在做一次\"对话理解\"，做错的代价是记忆库被污染。",[14,194,195,198,199,202,203,206],{},[29,196,197],{},"第三段：写入策略。"," ",[21,200,201],{},"AgentMemoryWritePolicy"," 是最后一道闸门：置信度低于阈值的候选挂起（攒够多次出现再转正）；来源对话轮次太边缘的丢弃；带情绪色彩的降权。策略层全是确定性代码，不放任何模型——",[29,204,205],{},"记忆库的写入门槛必须是确定性的","，这是我们从\"记忆污染事故\"里换来的铁律（下面细说）。",[14,208,209,212,213,216],{},[29,210,211],{},"第四段：冲突检测与版本历史。"," 这是设计里我最得意的一块。记忆更新不做覆盖，做",[29,214,215],{},"版本链","：每条记忆有版本历史，\"期望薪资\"从 v1（15-20k，10月8日，来自某轮对话）演进到 v2（20k+，10月29日）。好处有两个：一是能回答\"用户为什么拒了那个 offer\"这类回溯问题（当时他的薪资预期就是 15k，那个 offer 给 12k）；二是出了污染事故可以回滚到上一个版本。",[14,218,219,222,223,226],{},[29,220,221],{},"第五段：存储与注入。"," MySQL 打底持久化，Redis 做热缓存。注入的时候",[29,224,225],{},"不做全量塞入","——用户记忆可能有几十条，全塞进 system prompt 既是 token 浪费也会稀释注意力，而是按当前对话意图检索相关记忆（对，记忆的注入本身也是个检索问题）。注入格式上，给每条记忆标注\"（用户于10月提及）\"，让模型知道这是背景知识而不是当前指令。",[10,228,229],{"id":229},"三个真实事故",[14,231,232,235,236,239,240],{},[29,233,234],{},"事故一：记忆污染。"," 用户某天情绪低落，说了句\"唉，感觉找不到工作，不想找了\"。LLM 提取通道忠实地把它捞出来：",[21,237,238],{},"{求职状态: 已放弃}","。之后两周，Agent 给他的所有回复都带着\"既然您已经放弃求职……\"的基调，岗位推荐停了，模拟面试也劝他\"注意休息\"。用户差点流失。修复：情绪类陈述单独建类，默认不产生状态记忆；以及 IGNORE 分类门槛大幅调低——宁可漏记，不可错记。",[29,241,242],{},"记忆系统的信任和 Agent 的信任一样，一次污染，清零重建。",[14,244,245,248],{},[29,246,247],{},"事故二：提取器幻觉。"," LLM 通道偶尔会\"脑补\"出对话里根本没有的偏好——大概是小模型的推理能力撑不住提取任务。修复很土但有效：提取模型 temperature 调 0，prompt 里强制要求每条候选必须附带原文引用，校验不到原文的候选直接丢弃。",[14,250,251,254],{},[29,252,253],{},"事故三：记忆过期。"," 用户已经拿到 offer 入职了，Agent 还在按\"求职中\"的状态每天推岗位。这是记忆只有\"写入\"没有\"生命周期\"的问题。修复是把记忆和任务状态机联动：求职状态机的每次流转（投递中 → 面试中 → 已入职）都触发对应记忆的批量失效。",[10,256,257],{"id":257},"回头看",[14,259,260,261,263],{},"如果用一句话总结这一篇：**",[21,262,34],{}," 解决的是\"这个会话说了什么\"，记忆系统解决的是\"这个用户是谁\"。**前者框架给你现成的，后者只能自己搓，而且搓的时候要想清楚——记忆不是缓存，是有版本、有生命周期、可回滚的资产。",[14,265,266],{},"下一篇回到硬核话题：RAG 从 Demo 到生产，那些调参和中文分词的坑。",{"title":123,"searchDepth":268,"depth":268,"links":269},3,[270,272,273,274,275],{"id":12,"depth":271,"text":12},2,{"id":38,"depth":271,"text":39},{"id":98,"depth":271,"text":99},{"id":229,"depth":271,"text":229},{"id":257,"depth":271,"text":257},"LLM 应用","\u002Fimages\u002Fcovers\u002Fagent-memory-pipeline.jpg","2025-11-20","五段式记忆管道与三起污染事故",false,"md",{},true,"\u002Farticles\u002Fagent-memory-pipeline",{"title":5,"description":279},"articles\u002Fagent-memory-pipeline",[288,289,290],"Agent 记忆","长期记忆","LangChain4j",null,"kMoC9A6bOOnkNbNdRUKMfrHPBMKkjFbztNpX1pXWtkY",[294,298],{"title":295,"path":296,"date":297},"RAG 落地实录：pgvector、增量索引、调参和中文别名那点事","\u002Farticles\u002Frag-pgvector-in-production","2025-12-15",{"title":299,"path":300,"date":301},"Java 虚拟线程为什么不等于无限并发：从等待型任务说起","\u002Farticles\u002Fjava-virtual-threads-and-backpressure","2025-11-18",1792063306798]