LLM长期记忆架构:从上下文到分层记忆的工程实践

发布时间:2026/8/31 17:17:49
LLM长期记忆架构:从上下文到分层记忆的工程实践 有开发者在社区分享了一个很克制的标题“Show HN: Tried some experiments with architecture for Long term memory for LLM”。字面意思是他围绕 LLM 的长期记忆做了一些架构实验但点开之后真正值得展开的不是那一次实验的结论而是“长期记忆到底应该怎么做”这个问题。如果你试着让一个对话式助手连续聊几十轮很快就会碰到一个别扭的体验模型没有变笨但它开始忘了你说过的话。前几天确定的偏好、项目计划、已经排掉的坑下一场对话又得重新交代。最常见的解法是把历史记录全部塞回上下文可窗口永远不够用费用和延迟也跟着涨。于是越来越多实验开始把目光从“更大上下文”转向“长期记忆架构”本身。长期记忆不是把历史变成更大的上下文而是让模型在需要的时候能想起来。它需要一套写入、索引、检索、更新、遗忘的机制。这篇文章不准备给你一个“最优架构”因为目前还没有标准答案。我更想拆开这类实验背后真正需要想清楚的问题以及几个我反复见到别人踩进去的坑。1. 先搞清楚“长期记忆”到底在解决什么问题1.1 不是“记住更多”而是“需要时能想起来”很多人想到长期记忆第一反应是把对话记录保存下来下次再让模型重新读一遍。这是一种“记住更多”的思路但实际效果往往不好。原因很直接模型在每个请求里能同时处理的信息量是有限的塞进去的信息越多真正相关的信息在注意力里所占的比例就越低。这和人读书时做笔记很像你不是把整本书背下来而是在书上划重点、写批注、做索引需要用到某个章节时再翻出来。长期记忆更应该被理解成一个“检索系统”加上一个“写入策略”。写入时要决定哪些信息值得存存成什么结构检索时要根据当前的问题、场景和用户目标找出最相关的记忆还要有一种机制来处理信息过时或冲突。这三个动作加起来才是长期记忆。单靠向量数据库或者单靠摘要都只是其中一块拼图。1.2 LLM 的上下文为什么不适合承载长期记忆上下文窗口近几年涨得很快从几万 token 到几十万、上百万。数字看似很大但它对长期记忆来说只是一个必要条件不是充分条件。首先是成本每次请求携带的历史越长按 token 计费的成本就越高响应延迟也会线性增加。其次是质量当对话历史里混入了大量已经解决过的中间状态、重复解释和过时信息时模型生成的注意力会被分散。还有一个经常被忽略的问题上下文是一次性的。每次新会话都需要重新携带历史否则模型无法记得你上星期跟它聊过什么。长期记忆的目标其实是要把一次性的上下文转化为可持续跨会话的结构化信息。否则哪怕上下文窗口继续扩大也只是把“短期失忆”变成“中期失忆”。如果只是几十轮对话直接把历史拼进去通常是最简单的解法但要做一个长期不变的个人助手或知识库就必须换思路。2. 从“塞满上下文”到“分层记忆”一次典型的实验路径2.1 第一阶段把所有历史对话都丢给模型很多实验一开始都会这样做把多轮对话拼成一个超长字符串每次请求带上所有历史。好处是零改造只要工具支持超长上下文就能立刻看到效果。这也是最常见的最小可用方案。它的缺点是会在增长到某个规模后突然失效。我在实际实验里见过不少类似案例起初 20 轮对话效果很好到 50 轮开始出现“前面信息记不住但 token 还是被占着”的情况。原因有两层一是超出模型有效窗口后超出的部分直接被截断导致早期关键信息丢失二是即使没截断长文本里的旧信息也可能被新信息覆盖模型并不是真的逐字阅读整段历史。2.2 第二阶段把历史压缩成摘要为了缓解 token 压力一个自然想法是用模型把旧历史写成一两段摘要下一次请求时把摘要和最近几轮原文一起放进去。这个方案比全量拼接到了一截适合“只要结论、不要细节”的场景。但它的问题也很典型。摘要会丢失细节比如用户随口提到的一个偏好、一个尚未完成的小任务、一个否定过的选项。摘要还会在迭代更新时引入误差把旧摘要和新内容再压缩一遍错误会逐层放大。更麻烦的是摘要没有结构检索时只能整体使用很难回答“用户什么时候说过不喜欢某个方案”这类具体问题。2.3 第三阶段按信息类型拆分记忆模块当我意识到“摘要 最近轮次”不够用之后通常的实验方向是往记忆模块化走。不再让系统“记住对话”而是让系统“记住结构化的事实、偏好、进度和结论”。这里的核心变化是把对文字的保存变成了对信息的提取和归类。可以先把记忆分成几类用户偏好、任务进度、关键结论、长期目标、临时上下文。每一类用不同的 schema 存储和更新。比如用户偏好记录的是“回答要短先给结论”或“不要讨论某个话题”任务进度记录的是“文档初稿已完成本周待校对”。这些信息不是某句话的原文而是从对话里抽取出来的、可以被后续决策利用的数据。记忆类型典型示例存储重点更新方式用户偏好“回答要短先给结论”结构化文本新偏好覆盖旧偏好任务进度“初稿完成待校对”状态字段按事件推进关键结论“选择 A 方案因为成本低”结论 原因新结论覆盖旧结论临时上下文“当前正在调试文件解析”短期生效超时后过期这种分层方式是后续更复杂实验的底盘。3. 长期记忆架构的核心模块拆解3.1 记忆写入层什么时候该记记什么写入层是最容易被低估的一环。多数实验把精力花在检索和向量库上结果发现记忆质量不好往前一查其实是写入时就把垃圾存进去了。写入层要回答三个问题这段对话里有没有值得记的信息它属于哪一种记忆类型它和已有记忆是新增、更新还是冲突一个可行做法是定义好 schema 之后用提示词让模型抽取关键信息再通过规则或小模型做去重。不要指望一次抽取就完美。更稳的做法是先记录原始文本作为证据再在证据之上建立结构化字段。def write_memory(user_id, segment): candidates extract_memory_candidates(segment) # 候选必须包含信息类型、主实体、结论、发生时间、是否与旧记忆冲突 for item in candidates: if has_conflict(item, existing_memory(user_id)): item.status pending_review # 或直接用新记忆覆盖旧记忆取决于业务规则 store_memory(user_id, item)注意写入层的原则是“少而精”。把一句话完整存下来不如提炼成一条干净的偏好记录。存错了检索再准也无济于事。3.2 记忆索引层向量之外还需要时间、来源和关系很多实验会把对话切片后直接丢进向量数据库只靠 embeddings 去召回。这在 demo 里通常没问题但真实场景远远不够。向量语义相似适合判断“两段话是否相关”但不擅长处理“这个偏好是否已经被用户推翻”“这条记录是否已经过期”这类问题。所以索引层不能只有一个向量字段。比较常见的做法是给每条记忆加上时间戳、来源会话、用户 ID、记忆类型、有效状态、实体标签等元数据。检索时可以先用元数据过滤再算向量相似度最后再排序。这样能把范围缩小到“该用户近 30 天与当前项目相关的记忆”而不是在整个库里找最像的句子。如果你用过 Obsidian 和 LLM Wiki 这类工具搭个人知识库会发现思路类似先有笔记的文件夹、标签、双链再让模型根据当前笔记去找相关上下文。个人知识库和系统级长期记忆在原理上其实很接近。另外向量相似度计算通常会用 FP16 或 BF16 压缩来省显存精度压缩可能带来一点排序偏移但一般不是长期记忆的主要矛盾。3.3 记忆检索层宁可少给不要错给检索层决定了每次请求有多少记忆被注入到提示词里。一个常见误区是 top_k 设得很大以为相关片段越多越好。实际效果往往相反无关记忆会污染注意力甚至让模型把记忆当成了用户当前输入。我在实验里更推荐的顺序是先理解当前请求的意图判断需要哪些记忆类型再按元数据过滤然后做向量召回最后做一次重排只保留和当前问题紧密相关的 3 到 5 条。注入到 Prompt 时最好明确标注“以下是历史记忆仅供参考”和用户当前输入区分开。def retrieve(user_query, user_id): relevant_types detect_memory_types(user_query) candidates db.prefilter(user_iduser_id, memory_type in relevant_types) ranked vector_search(user_query, candidates) return rerank(ranked)[:5]3.4 记忆更新层新信息与旧结论冲突时怎么办长期记忆最大的坑不是“记不住”而是“记错了还不改”。用户今天说“我更喜欢邮件沟通”明天改成“微信也可以”如果系统同时保存这两条检索时可能随机返回一条结果模型就表现得很不稳定。所以要有一个更新策略。最简单的规则是“同类型、同实体的新记忆覆盖旧记忆”复杂一点的做法是“保留旧版本但标记为过期或低置信度”。还可以引入时间衰减超过一定天数没有被访问的记忆检索权重下降减少它被注入的概率。如果实验规模还小先不用上复杂的知识图谱。可以先给记忆增加一个 status 字段可选值包括 active、archived、superseded。检索时默认只返回 active 的记忆除非当前问题明确与“历史变化”有关。这个改动很小但效果很明显。3.5 与外部工具配合让记忆触发 Agent 行为当长期记忆进入 Agent 场景它就不只是 Prompt 里的一段背景资料了。记忆可以影响 Agent 的工具调用。比如记忆里有“用户在等待上周五的周报”模型在回答时可能会主动去查文档目录、生成周报草稿而不是只回复一句“我记得你有一份周报”。这也是为什么很多应用开始引入编排框架把长期记忆模块、工具调用循环、模型角色绑定在一起。单独做一个记忆 API 并不难难的是在每轮决策里判断“现在要不要去查记忆”“要不要去更新记忆”。这些听起来像流程问题但在架构层面它就是需要显式设计的环节。4. 实验中最容易踩的四个坑4.1 只调检索相似度忽略了记忆粒度和写入时机如果有人长期记忆效果不好最常见的一个操作是去调阈值、换 embedding 模型、换向量库但问题往往根本不在检索。如果写入时记的是整段对话里面包含大量寒暄和临时状态检索回来自然不干净。真正的做法是先调整记忆粒度把信息按事件、结论、偏好等语义单元切分而不是按固定字数切片。另一个容易被忽略的是写入时机。有些信息需要立即写入比如用户明确改变偏好有些信息可以等到一个任务结束再总结写入比如整段工作流里的关键结论。如果每个中间步骤都写记忆库很快就会充满噪音。4.2 把向量数据库当成万能存储器向量数据库擅长做召回但不少实验者把它当成了关系型数据库。比如要实现“查一下用户上周是否提过某个项目的截止日期”这种查询用元数据过滤更精准而不是靠向量相似度。更好的做法是让向量库只负责语义召回把时间、状态、用户、类型这些结构化条件放在过滤里。如果项目规模很小也可以直接用 PostgreSQL 或 SQLite 的向量扩展甚至用内存里的表格加简单的点积计算来算相似度。重点是把逻辑跑通而不是第一步就选重型组件。4.3 没有做记忆分层所有信息放一个池子把用户偏好、任务进度、临时闲聊、领域知识全部塞进同一个 collection 的做法在实验初期可以跑但到 50 次以上交互后检索结果会越来越像“混合果汁”。原因是不同类型的记忆对当前问题的相关性判定标准不一样临时上下文可能几小时内就失效用户偏好则是长期稳定领域知识可能和个人无关。所以更建议至少分两层工作记忆短期、与当前任务相关和长期记忆跨会话稳定。工作记忆可以放到上下文或快速缓存长期记忆进外部存储。这样检索时不会互相干扰。4.4 缺少更新和遗忘机制记忆越多越不准没有遗忘机制的长期记忆就像一本越记越厚的错题本里面全是已经作废的旧答案。比如用户已经换了方案但旧方案说明还存在库里检索时新方案和旧方案同时被携带模型就可能在一次回答里自相矛盾。可以给记忆加置信度、时间戳、最后访问时间。定期跑一个清理任务把被覆盖的旧记忆标记为 superseded把超过有效期且没有访问的临时记忆归档。要记住长期记忆的目标不是“信息永不丢失”而是“信息在需要时仍然可用”。5. 从实验走向可用的排查链路和验证流程5.1 记忆效果不好时先按这个顺序排查很多长期记忆问题看起来是“模型记性差”实际上可能出在各个不同的环节。与其一次调三个参数不如按顺序排查现象可能原因先查什么记忆完全不生效写入层没存进去日志里是否有提取后的记忆记录检索到的东西和问题无关索引层缺过滤看返回结果的元数据是否匹配当前场景检索到相关记忆但回答没用上Prompt 里记忆和用户问题混在一起看注入格式和位置回答自相矛盾更新层没处理冲突检查新旧记忆是否同时处于 active对话前几轮还行越往后越差记忆过多或检索 top_k 过大统计单次注入 token 数量这个表可以作为每次实验回归时的一等检查单。5.2 一个可复用的五步实验流程定义 20 到 50 个真实测试场景记录“在什么情况下你希望模型想起哪一条记忆”。先不引入向量库用一个 JSON 文件或 SQLite 表保存结构化记忆把检索逻辑跑通。把日志和记忆存成可回放格式。每一次对话都记录当前输入、被检索到的记忆、最终输出。加入分层与元数据过滤对比一次“不分层”和“分层”的效果差异。做回归测试和对抗测试故意制造用户推翻旧偏好、任务完成、信息过期等场景看系统能否更新记忆。这套流程的优点是一次只改一个变量而不是把所有模块同时升级。很多实验失控都是因为第一版就跑全链路出了问题不知道往哪看。5.3 适用场景与不适用场景长期记忆架构更适合这些场景个人专属助手、知识库问答、长期项目协作者、Agent 任务跟踪。它对“多轮对话中需要持续理解用户目标”的应用尤其有价值。但也有一段边界它不适合高频毫秒级响应的场景因为每次请求都追加检索会带来额外延迟它也不适合隐私要求极高的场景除非记忆数据的存储、加密、删除策略都设计好。如果只是单轮问答、一次性工具调用长期记忆反而可能徒增复杂度。如果你发现自己的系统连“上一次对话的结论”都用不上不要急着加向量库先跑一遍检查单很多时候问题出在写入和更新不是在检索。回到最开始那个实验。长期记忆这个题目最缺的不是更大的上下文窗口也不是更贵的模型而是一套稳定的写入、索引、检索、更新和遗忘机制。你可以从一个很小的范围开始先记住用户的偏好下一次对话正确用上再记住一个项目的进度每次开会前自动拉取相关结论。把这些连起来就是一次有效的架构实验。真正长期可用的 LLM 应用大概率不是靠某一次实验的惊艳结果而是靠记忆架构和工具循环一层层托起来的。下一次再做实验时先别急着追求“什么都能记住”先把“该记住的为什么没记住”搞清楚。