ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

给 Agent 装上记忆:短期、长期与工作记忆的工程实现

2026/8/5 16:35:06 拓冰建站 浏览量
给 Agent 装上记忆:短期、长期与工作记忆的工程实现 先说两个我亲身经历的「失忆事故」。第一个是刚做客服机器人那会儿测试同事在第三轮对话里明确说了「别用敬语直接说事」。机器人答应得好好的。聊到第七轮它又开始「尊敬的用户您好」。同事把截图甩群里配了三个字金鱼脑。第二个更伤。有个用户上周刚纠正过产品的正确名称这周再来咨询机器人又把名字叫错了。用户当场就不乐意了「我上周是不是白说了」这两个问题的根子是同一个LLM 本身没有记忆。你看到的「对话」全是工程把戏。这篇就把这个把戏的里子拆开讲讲记忆系统到底怎么搭。一、为什么 Agent 天生失忆先纠正一个直觉错误。很多人以为模型聊着聊着就「记住」了其实完全没有。LLM 本质上是个无状态函数给它一段 prompt它吐一段 completion调用之间毫无关联。这次调用不知道上次发生了什么就像每天醒来都失忆的电影主角。你在 ChatGPT 里感受到的「上下文」是客户端每轮都把整段历史拼进 prompt 重新发给模型。模型每次都从头读一遍你们的全部聊天记录然后装作记得你的样子。这个设计带来两个硬伤。第一context window 装不下无限历史。窗口就那么大聊得久了最早的内容只能扔掉。扔掉的那一刻模型就真的忘了。第二就算装得下模型对长上下文中间部分的注意力会衰减。圈里管这个叫 Lost in the Middle你给模型喂 100 条历史它对开头和结尾记得牢中间的经常像没看过一样。所以哪怕历史全在窗口里该忘还是忘。所以结论很直白记忆不是模型的能力是你要写的工程。模型是那个没有硬盘的 CPU内存和硬盘得你来配。二、三层记忆桌面、笔记本、档案柜业界对记忆系统的划分基本收敛到三层我用人脑的方式给你类比。工作记忆Working Memory就是当前 context window 里的东西。像你办公桌上摊开的文件伸手就能够到处理速度最快但桌面就那么大堆不下太多。短期记忆Session Memory是本次会话的压缩历史和状态。像你手边的笔记本桌面放不下的草稿记在本子上今天之内随时翻明天可能就换新本子了。长期记忆Long-term Memory是跨会话持久化的知识库。像公司的档案柜真正的知识沉淀存进去就不丢但取用要按流程走也就是检索。这三层不是孤立的关键是它们之间会流动工作记忆装不下了旧内容压缩进短期记忆短期记忆里发现重要事实归档进长期记忆新对话开始时从长期记忆里检索出相关内容取回工作记忆。记忆系统的大部分复杂度就在这个流动的规则上什么该压缩、什么该归档、什么该取回。后面逐个拆。三、工作记忆上下文窗口是稀缺资源工作记忆管理的核心矛盾一句话就说完了窗口有限对话无限。主流解法就两个滑动窗口和摘要压缩通常组合着用。3.1 滑动窗口最简单但只配兜底只保留最近 N 轮更早的直接丢。代码没什么技术含量def sliding_window(messages, max_turns10):system [m for m in messages if m[“role”] “system”]turns [m for m in messages if m[“role”] ! “system”]return system turns[-max_turns * 2:] # 一轮 一问一答两条粗暴但有效适合短平快的场景FAQ 机器人、单次任务助手用户也不指望你记得三小时前的话。问题也明显被裁掉的内容里如果有关键信息比如用户第一轮报的手机号后面就真的丢了。所以滑动窗口只能兜底不能当主力。3.2 摘要压缩目前的主力方案思路是对话变长后把早期内容让 LLM 压缩成一段摘要摘要加最近几轮组成新的上下文。SUMMARY_PROMPT “”把下面的对话历史压缩成摘要保留用户的身份、偏好、明确要求已确认的关键事实和结论未解决的待办事项控制在 200 字以内。对话{history}“”def compress_context(messages, llm, keep_recent4):# 消息数没超阈值就不压缩if len(messages) keep_recent * 2 1:return messagessystem messages[0] old\_turns messages[1:-keep\_recent \* 2] recent messages[-keep\_recent \* 2:] history\_text \n.join( f{m[role]}: {m[content]} for m in old\_turns ) summary llm(SUMMARY\_PROMPT.format(historyhistory\_text)) return [system, {role: system, content: f此前对话摘要{summary}}, \*recent]压缩后的上下文结构变成这样[system prompt][system: 此前对话摘要] ← 200 字浓缩了几十轮[user / assistant × 最近4轮] ← 原文保留细节不丢这个结构的巧妙在于摘要保「质」、原文保「鲜」。早期交互的价值在于沉淀下来的结论和偏好200 字摘要足够承载最近几轮的价值在于细节和语气必须保留原文。实测数据一个 30 轮的客服对话原文约 8000 token压缩后摘要 260 token 加最近 4 轮约 1200 token总量砍掉 82%而关键信息用户诉求、订单号、情绪倾向一个没丢。摘要策略有两个坑提醒一下。一是压缩触发时机别等窗口快爆了才压建议水位到 60%-70% 就动手不然压缩这个动作本身的输入也会顶到上限。二是摘要要滚动更新每压缩一次新摘要要基于旧摘要加新内容生成而不是把旧摘要当普通历史再压一遍。套娃式压缩会让信息层层失真压三轮下来关键事实就面目全非了。四、短期记忆让会话断得开、接得上短期记忆解决的是另一个问题进程挂了、连接断了、用户明天回来继续聊上下文从哪来答案是把对话状态持久化。LangGraph 里管这个叫 checkpointer名字唬人本质就是一张会话表import sqlite3, json, timeclass SessionStore:def __init__(self, db“sessions.db”):self.conn sqlite3.connect(db)self.conn.execute(“”“CREATE TABLE IF NOT EXISTS sessions(thread_id TEXT PRIMARY KEY,updated REAL,state TEXT)”“”)def save(self, thread\_id, messages): self.conn.execute( REPLACE INTO sessions VALUES (?,?,?), (thread\_id, time.time(), json.dumps(messages, ensure\_asciiFalse))) self.conn.commit() def load(self, thread\_id): row self.conn.execute( SELECT state FROM sessions WHERE thread\_id?, (thread\_id,)).fetchone() return json.loads(row[0]) if row else []关键在thread_id这个设计。每个用户、每个会话一个独立 ID状态按 ID 隔离存取。用户 A 三天前的咨询、用户 B 刚开的对话互不干扰。接到 Agent 主循环里就三行messages store.load(thread_id) # 进来先恢复… 跑一轮对话 …store.save(thread_id, messages) # 出去前落盘生产上 SQLite 换成 Redis 或 Postgres 就行逻辑一模一样。别小看这三行有了它服务重启、负载均衡换机器、用户换设备全都不怕。五、长期记忆真正的杀手锏前面两层管的都是「别忘了当前这场对话」。长期记忆才是跨对话的用户上周的偏好、上个月的投诉、半年前的承诺。这层做好了Agent 才谈得上越用越懂你。5.1 记什么三种长期记忆认知科学把人的长期记忆分成几类搬到 Agent 上意外地合适。语义记忆事实和知识。「用户叫张三」「公司报销上限 5000」「产品 A 不支持 IE」。用得最多的一类回答「是什么」。情景记忆带时间戳的事件。「3 月 2 日用户因物流延迟投诉过一次最后补偿了优惠券」。回答「发生过什么」。对客服场景特别重要用户再次上门时你能说一句「上次您反馈的物流问题后来解决了吗」体验完全不同。程序性记忆流程和技能。「处理退款要先查订单状态再核实支付流水」。回答「该怎么做」通常从成功和失败的历史里提炼。三类记忆可以共用一个存储用 type 字段区分就行。真正难的不是存是下面两个问题什么时候写什么时候读。5.2 记忆写入显式加隐式显式写入最简单用户直接说「记住我对花生过敏」你解析指令存下来。好办但指望用户主动交代不现实90% 的重要信息藏在日常对话里。所以主力是隐式提取每轮对话结束后让 LLM 判断这一轮有没有值得记的东西。EXTRACT_PROMPT “”从下面这轮对话中抽取值得长期记住的信息。只记四类用户偏好喜欢/讨厌/习惯身份信息姓名/角色/联系方式明确的业务事实订单号/约定/承诺用户纠正过的错误认知没有就输出 NONE。每条一行格式[类型] 内容用户{user}助手{assistant}“”def maybe_remember(user_msg, agent_msg, store, llm):result llm(EXTRACT_PROMPT.format(useruser_msg, assistantagent_msg))if “NONE” in result:returnfor line in result.strip().split(“\n”):line line.strip()if not line:continueimportance 0.9 if line.startswith(“[身份”) else 0.7store.add(textline, importanceimportance, tstime.time())注意 prompt 里「只记四类」和「没有就输出 NONE」这两句。不写清楚模型会把「今天天气不错」这种废话也当宝贝存下来记忆库很快就变成垃圾场。5.3 记忆读取相关性 × 新近性 × 重要性存了几百条记忆后不能全塞进 prompt。检索时给每条记忆打分只取 Top-K。评分公式借鉴认知心理学的记忆强度模型score α·相关度 β·新近性 γ·重要性import math, timedef recency(mem, now, half_life_days30):days (now - mem.last_access) / 86400return math.exp(-0.693 * days / half_life_days) # 0.693 ln2def retrieve(store, query, top_k5, a0.6, b0.2, c0.2):q_emb embed(query)now time.time()scored []for m in store.all():s (a * cosine_sim(m.embedding, q_emb) b * recency(m, now) c * m.importance)scored.append((s, m))scored.sort(keylambda x: -x[0])for _, m in scored[:top_k]:m.last_access now # 被想起的记忆更新新近性return [m for _, m in scored[:top_k]]三个因子各有分工。相关度保证当前聊什么取什么新近性让最近用过的记忆优先心理学里叫用进废退被频繁想起的记忆会越记越牢所以代码里取中后要刷新 last_access重要性是写入时打的标保证「用户身份证」这种低频但关键的记忆不会因为聊得少而被挤掉。权重怎么调我的默认值是 0.6 / 0.2 / 0.2相关性主导。如果你的场景用户偏好变化快比如电商导购把 β 调大点让新记忆更快盖过旧记忆。5.4 记忆更新冲突怎么办用户是会变卦的。三月说「我不吃辣」六月说「最近开始吃辣了」。旧记忆不处理库里就同时躺着两条矛盾事实检索出来哪个全看运气。写入时做一次冲突检测CONFLICT_PROMPT “”“旧记忆{old}新事实{new}两者关系是「矛盾」还是「补充」还是「无关」只答一个词。”“”def add_with_conflict_check(store, new_fact, llm):for old in store.search(new_fact, top_k3):verdict llm(CONFLICT_PROMPT.format(oldold.text, newnew_fact))if verdict.strip().startswith(“矛盾”):store.delete(old.id) # 新事实覆盖旧记忆store.add(new_fact)思路就是写之前先查查到矛盾就删旧存新。这个习惯能让你的记忆库长期保持自洽不然用一个月后库里全是自相矛盾的鬼故事。六、我踩过的四个坑理论讲完了说点实战里交过的学费。坑一写入泛滥。最早的版本我不设防LLM 每轮抽三五条往里存两周后库里 4000 多条一检索回来一堆「用户说过谢谢」「用户说了再见」。后来加了两道闸提取 prompt 限定四类重要性低于 0.5 的不入库。记忆贵在精不在多。坑二幻觉入库。有次排查发现库里躺着一条「用户是糖尿病患者」但翻遍对话记录用户从没说过。哪来的模型在某轮回复里自己脑补了一句下一轮提取时把它当事实抽出来存了。教训提取时只看用户说的话或者给每条记忆打上来源标记user_stated 还是 agent_inferred检索时优先用 user_stated。坑三忘了「忘记」。记忆库只进不出三年后用户问「我女儿生日该准备什么」Agent 还按三年前的年龄推荐礼物。情景记忆要定期衰减重要性低于阈值且超过 N 天没被访问的归档或删除。坑四隐私裸奔。手机号、身份证、住址全明文进了向量库还被 embedding 成了向量。合规上这是大事。我现在的做法是入库前过一道脱敏层正则扫身份证、手机号、银行卡号命中就打码或拒绝入库。七、完整案例把三层记忆接进 Agent把前面的零件装起来一个带完整记忆的 Agent 主循环长这样import threadingdef agent_with_memory(user_input, thread_id, store, sessions, llm):# 1. 恢复短期记忆messages sessions.load(thread_id) or [SYSTEM]# 2. 检索长期记忆注入上下文 memories retrieve(store, user\_input, top\_k5) if memories: mem\_text \n.join(f- {m.text} for m in memories) messages.append({role: system, content: f关于这个用户你记得\n{mem\_text}}) # 3. 正常跑一轮对话 messages.append({role: user, content: user\_input}) reply llm(messages) messages.append({role: assistant, content: reply}) # 4. 工作记忆压缩防窗口爆炸 messages compress\_context(messages, llm, keep\_recent4) # 5. 短期记忆落盘 sessions.save(thread\_id, messages) # 6. 长期记忆提取异步做别挡响应 threading.Thread(targetmaybe\_remember, args(user\_input, reply, store, llm)).start() return reply六个步骤对应三层记忆的全部流转进来时恢复短期、取回长期跑完压缩工作记忆、落盘短期、提炼长期。这个骨架 30 来行但已经是一个能投产的记忆系统雏形。第 6 步注意我开了线程记忆提取不挡主响应。用户等的是回复不是你记笔记。生产上这步通常丢消息队列慢归慢反正用户无感。实际效果第一轮用户说「我对花生过敏帮我看看这个零食能吃吗」助手回答并存下偏好。三天后新会话用户问「推荐点宵夜」检索层把「对花生过敏」捞出来注入上下文推荐自动避开花生制品。全程用户没再提过一次过敏的事。记忆的价值就在这说一次终身有效。八、怎么评估记忆系统也要考试记忆系统上线前我习惯给它出套卷子。做法很直接先往库里植入 20 条事实模拟历史对话灌进去的然后开新会话问依赖这些事实的问题看能不能答对。评估集要覆盖三种考法直接回忆植入「用户对花生过敏」问「推荐零食时要避开什么」多跳推理植入「用户是素食者」加「用户不吃香菜」问「推荐一家餐厅」时效判断先植入「预算 3000」后植入「预算改成 5000」问「按什么预算推荐」。这种考的就是冲突处理两个核心指标记忆命中率该记起来的记起来没和错误召回率不该记的瞎记了多少。后者更容易被忽略但对体验的杀伤力更大。Agent 信誓旦旦引用一个错误记忆比它不记得还让用户反感。学术界的 LongMemEval 基准也是这个思路专门测聊天助手的长期记忆能力五大类 500 个问题可以拿来给自己的系统跑分。学AI大模型的正确顺序千万不要搞错了2026年AI风口已来各行各业的AI渗透肉眼可见超多公司要么转型做AI相关产品要么高薪挖AI技术人才机遇直接摆在眼前有往AI方向发展或者本身有后端编程基础的朋友直接冲AI大模型应用开发转岗超合适就算暂时不打算转岗了解大模型、RAG、Prompt、Agent这些热门概念能上手做简单项目也绝对是求职加分王给大家整理了超全最新的AI大模型应用开发学习清单和资料手把手帮你快速入门学习路线:✅大模型基础认知—大模型核心原理、发展历程、主流模型GPT、文心一言等特点解析✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑✅开发基础能力—Python进阶、API接口调用、大模型开发框架LangChain等实操✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经以上6大模块看似清晰好上手实则每个部分都有扎实的核心内容需要吃透我把大模型的学习全流程已经整理好了抓住AI时代风口轻松解锁职业新可能希望大家都能把握机遇实现薪资/职业跃迁这份完整版的大模型 AI 学习资料已经上传CSDN朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费】