LLM长期记忆架构实验:解决上下文窗口限制的实践指南
2026/8/31 1:35:20 网站建设 项目流程

围绕 LLM 的长期记忆(Long term memory)做架构实验,是我最近花时间最多的一块。原因很简单:LLM 的上下文窗口再大,也只能装下当前这一次会话的内容;一旦对话结束、服务重启、或者请求被分配到另一个 session,之前的重要信息就全丢了。很多人第一反应是“把历史对话全部塞进 prompt”,或者干脆“把上下文窗口调大”,但真正做完一轮实验就会发现,窗口变大只是推迟了溢出问题,并没有解决“该记住什么、怎么更新、怎么快速找到”这三个核心问题。这篇文章把我这轮架构实验里的几种路线、参数取舍和踩坑点整理出来,适合正在给 LLM 应用加长期记忆、或者准备做个人知识库和 Agent 记忆系统的开发者参考。我的核心判断是:长期记忆不是某个模型功能,而是一套需要自己设计的数据读写架构,越早按这个思路做,后面改造成本越低。

1. 先把问题定义清楚:长期记忆到底要解决什么

1.1 上下文窗口不是记忆,只是临时缓冲区

LLM 的上下文窗口本质上是模型一次推理时能看到的输入拼接区。窗口里的内容会被完整当作输入参与计算,窗口之外的内容模型根本看不见。所以它更像是“工作台”或者“临时缓冲区”,而不是真正意义上的记忆。临时缓冲区有两个天然限制:

一是容量有限。即便窗口从几千 token 扩到几十万 token,总会有装满的一天,而且窗口越大,单次请求的延迟和费用通常也越高。

二是生命周期短。对话轮次结束、服务重启、或者你换了主题之后,之前的上下文会被清理掉。用户第二天再回来问“我上次让你整理的那份需求”,模型大概率什么都想不起来。

这就像一个人只有“正在思考的桌面”,没有“长期存储的书架”。没有书架,桌面上无论堆多少纸,换一篇文章就全得清空。

1.2 先分清“记忆”和“检索”这两个能力

做架构设计之前,我建议先分清两组概念。

记忆系统可以拆成四个环节:

  • 写入:从对话或文档里提取值得保留的信息。
  • 存储:把信息按一定结构保存下来。
  • 读取:在需要时把相关信息找回来。
  • 更新:当旧信息被新信息覆盖或冲突时,做修改、合并、删除。

检索只是其中的一个环节。很多人把“向量数据库 + 相似度查询”直接等同于长期记忆,这其实只完成了存储和读取的一部分。写入时提什么、存什么,读取时怎么过滤、怎么排序,更新时怎么处理重复和冲突,才是真正决定记忆质量的地方。

1.3 什么场景值得动手做这套实验

不是所有 LLM 应用都需要长期记忆。我建议按下面的场景来判断:

  • 一次性问答、代码生成、翻译:不需要,给足上下文就行。
  • 多轮客服、咨询助手:需要短期记忆,至少要能记住本会话内的关键信息。
  • 个人知识库、AI 助手长期陪伴、Agent 反复执行同一个长期任务:需要真正的长期记忆。

如果你只是玩票性质做一个 demo,可以不那么讲究,把历史消息全部塞进上下文也能跑。但如果要做成一个能被反复使用、跨会话连贯的产品,长期记忆就是从“能用”到“好用”的分水岭。

2. 动手前的架构选型:不要一上来就上大模型

2.1 最小可运行环境其实不需要多高配置

我第一轮实验用的环境很普通:本地一台 16G 内存的机器,模型通过 API 访问,向量数据库用轻量级方案, embedding 模型也选了小体积版本。这里要说清楚,整个记忆系统的资源消耗大头通常不在 LLM 本身,而在 embedding 计算和向量库的检索性能。

如果你打算跑本地模型,比如基于 Llama 或 Qwen 系列权重做实验,建议先确认显存和内存。模型精度方面,常见选择是 fp16 和 bf16,占用的显存大约是权重参数量的两倍;fp32 会再翻一倍,机器配置不够时很容易直接 OOM。关于精度问题,我的建议是:纯 float32 在本地实验里很少有必要,bf16 或者 fp16 已经能覆盖绝大多数任务,省下来的显存可以留给更大的 embedding 模型或更长的上下文。

2.2 三种常见记忆载体对比

我把常见的记忆存储方式分成了三类,分别适合不同的实验阶段。

记忆载体适合场景优点缺点实现成本
纯文本文件/JSON原型验证、日志记录、单用户实验简单、可读、容易调试检索能力弱,只能靠关键词最低
向量数据库语义检索、跨文档联想、知识库能按语义找相似内容需要设计写入和更新策略中等
关系型/图数据库实体关联、多跳查找、结构化记忆关系清晰,适合更新和一致性控制结构化成本高,写入麻烦较高

第一轮实验我强烈建议先用文本文件或 JSON 把流程跑通,再替换成向量数据库。不要一开始就纠结 Milvus、Chroma、FAISS、Elasticsearch 选哪个。记忆系统的核心难点在流程,不在存储引擎;流程没想清楚,换再强力的数据库也没用。

2.3 数据准备:把记忆拆成可检索的单元

无论用哪种存储,都要先决定“记一条”的最小单位是什么。

常见粒度有三种:

  • 按单轮对话记录:实现最简单,但信息碎片化严重,后续检索容易命中一堆半句话。
  • 按主题段落记录:先让 LLM 把一段对话里与某主题相关的内容压缩成一条记忆,信息密度更高。
  • 按实体和属性记录:例如“用户:张三;偏好:喜欢简洁回答;约束:对价格敏感”,适合做结构化记忆。

我的原则是:先做主题段落,再做实体属性。主题段落的好处是粒度适中,既能保留上下文,又不像单轮对话那样细碎。等系统积累了足够数据、发现主题段落之间出现重复和矛盾时,再引入实体抽取也不迟。

3. 第一轮实验:向量检索式长期记忆

3.1 写入流程怎么设计

向量检索式记忆最简单的结构是这样:

  1. 用户和 LLM 完成一轮对话。
  2. 把这一轮对话交给一个压缩模型或规则模块,提取“值得记住的事实”。
  3. 对提取结果做 embedding 向量化。
  4. 把向量和原文一起写入向量数据库。

这里最关键的是第二步。直接存原始对话会带来两个问题:一是内容冗余,一条记忆里塞满了寒暄和无关细节;二是后续读取时 token 消耗大,因为检索回来的是整段原文。

我第一版就是这么做的,结果检索回来的基本都是“用户今天测试了系统”“用户说好的谢谢”这类低价值内容。后来改成先让 LLM 做一轮抽取,规则是:只保留事实性、偏好性、任务进展类信息,过滤寒暄、情绪和临时状态,写出来的记忆尽量控制在 50 到 100 个 token 内。

写入前还应该做一次去重检查。如果新记忆和已有记忆在语义上高度相似,优先走更新而不是追加。这个判断可以先用向量相似度粗筛,再用 LLM 判断是否合并。

3.2 读取流程怎么设计

读取流程决定了记忆能不能在最合适的时机被看到。

我的推荐顺序是:

  1. 用户发起新问题时,先对问题做 embedding。
  2. 在向量库里做相似度检索,取 top-k 条候选记忆。
  3. 用一个相关性过滤规则清掉相似度过低的条目。
  4. 把留下的记忆拼接到系统提示词或上下文中。
  5. LLM 生成回答。

这里有两个参数最容易出问题。一个是 top-k 取值,我见过很多人在默认参数下取 20、30 条,结果有用的没几条,上下文反而被无关记忆撑爆。另一个是相似度阈值,阈值设太低会混入噪声,设太高又会漏掉弱相关的记忆。建议先在小样本上观察一轮,把每条检索结果的相似度分数打印出来,再决定阈值。

不要一上来就开最大查询并发。向量库看起来只是“查一下”,但并发高的时候,embedding 服务和数据库查询都是资源大户。先把单条检索的延迟压到可接受范围,再逐步加并发。

3.3 判断一轮实验是否成功的标准

一轮实验跑完,不要只看“它能记住之前说过的话”这种模糊结论。我给自己定的验证清单是这样的:

  • 跨会话命中:新开会话后,问一个只在前一次会话中出现过的细节,看能否正确回答。
  • 无关内容不命中:问一个和已存记忆无关的问题,确认不会因为相似度误匹配而引入旧信息。
  • 检索耗时:单次记忆检索加拼接的总耗时,是否在应用可接受范围内。
  • token 开销:平均每次请求因为记忆拼接多消耗了多少 token,和直接塞全部历史相比是否明显下降。
  • 可维护性:记忆内容是否能被人工查看、修改和删除,而不是只能黑盒写入。

最后一条经常被忽略。如果记忆写坏了没法修,那这套系统在长期运行中一定会积累大量垃圾数据。

4. 第二轮实验:记忆摘要与分层压缩

4.1 为什么单靠向量检索不够

第一轮实验跑通之后,很快会遇到一个新问题:当记忆条目越来越多,且它们之间存在演进关系时,单靠向量检索并不能表达“变化”。

举个例子。用户第一天说“我喜欢简洁的回复”,第三天说“我现在需要非常详细的技术报告”。两条都是事实,语义上并不冲突,但都检索出来之后,模型不知道该以哪条为准。这就是典型的记忆冲突问题。

另一个问题是记忆会过时。用户换了公司、换了项目、改了偏好,旧记忆如果不更新,反而会变成干扰项。向量检索只能保证“找到语义相近的内容”,不能保证“找到的内容仍然有效”。

4.2 分层记忆:工作记忆、情景记忆、语义记忆

为了处理这些问题,我参考了认知科学里常见的分层方式,把记忆分成三层:

  • 工作记忆:当前会话内已经确认的关键信息,比如用户刚刚提供的需求细节,不跨会话保留。
  • 情景记忆:某个时间点发生过的事情,比如“昨天上午,用户要求把报表改成按周汇总”。
  • 语义记忆:从情景中抽象出来的稳定偏好和规则,比如“用户倾向于按周查看报表”。

每次写入时,先判断这条信息应该进哪一层。情景记忆可以大量保留,负责具体细节;语义记忆负责抽象稳定结论,数量少但优先级高。到了读取阶段,优先使用语义记忆,再按需补充情景记忆,工作记忆只作用于当前会话。

这个设计能明显减少信息噪声。因为很多应用根本不需要把“昨天发生了什么”全部记住,只需要记住“由这些事情推导出的规则”。

4.3 更新策略:什么时候改写旧记忆

我踩过的最大坑是:记忆只增不改,最后变成一个相互矛盾的垃圾桶。

更新策略我没有用复杂的规则,而是采用了一个相对简单的流程:

  1. 新记忆写入前,先做相似度检索,找出可能相关的旧记忆。
  2. 如果相似度超过阈值,把新旧记忆一起交给 LLM 判断。
  3. 判断结果有三种:以新为准并删除旧条目、把新旧合并成一条、两者独立保留。
  4. 每次更新都保留一条简要的变更记录,方便回溯。

变更记录很重要。因为在调试阶段,你经常会遇到“模型没按预期回答”的情况。有变更记录,你才能知道它到底是没读到记忆,还是读到了过时的记忆。没有变更记录,你只能靠猜。

5. 第三轮实验:结构化记忆与多会话一致性

5.1 从自由文本到实体关系

第二轮实验之后,如果只是单用户个人知识库,其实已经够用了。但如果你要做 Agent 或者多用户系统,就会发现自由文本式的记忆还有一个短板:无法表达复杂的实体关系。

比如“张三负责 A 项目,A 项目上周延期了,原因是依赖 B 团队”这是一段文本。你能检索到“A 项目”,也能检索到“B 团队”,但模型不一定能理解“项目依赖团队”这条关系链。

第三轮实验我尝试了把部分记忆改造成结构化形式:实体表和关系表。实体保存属性,关系保存连接。这样做的收益是,当用户问“B 团队还影响了哪些项目”时,系统可以先通过关系拿到候选实体,再回到原始记忆中取上下文,检索的准确性明显高于纯向量匹配。

代价也很明显:抽取和维护结构成本高,而且 LLM 抽取出的实体关系不一定稳定。我的建议是只对高频核心实体做结构化,其他内容继续用自由文本。全量结构化是个无底洞,收益边际递减很快。

5.2 多用户记忆的隔离与命名空间

一旦系统里不止一个用户,就要处理记忆隔离。最简单也最稳妥的方案是给每条记忆带上命名空间标识,比如 user_id、conversation_id、session_id。所有写入和读取都强制走命名空间过滤。

这一步看起来简单,但特别容易出 bug。常见问题是写入时忘了存 user_id,读取时忘了过滤,结果用户 A 的私密对话跑到了用户 B 的上下文里。这类问题一旦上线就是事故级问题。建议在写入接口和读取接口两层都加上校验,而不是只在业务层依赖开发者的自觉。

5.3 并发写入与版本控制

多个会话同时写同一条用户记忆时,会出现互相覆盖的问题。用户一边在网页端说“我不要 weekly 报告了”,一边在 API 端说“weekly 报告改为周二发送”,两条写入如果并发执行,最后落库的版本取决于谁后完成,逻辑上可能完全错乱。

处理方式不复杂:给每条记忆增加版本号或更新时间戳,写入选主或合并;冲突时先比较时间或优先级,必要时把两条都保留并标记冲突,让 LLM 在读取时进行裁决。这里不要追求强数据库事务,记忆系统对偶尔的不一致是能容忍的,更关键的是不要静默丢数据。

6. 参数调优与效果验证

6.1 最影响结果的五个参数

一轮实验做完,真正值得反复调优的参数其实就几个。

参数影响调优建议
记忆条目标题/摘要长度影响 token 开销和检索精度先控制在 50 到 100 token,再根据场景调整
分块大小影响语义粒度单条记忆太碎会噪声大,太大则检索不精准
top-k影响上下文清洗度从 3 到 5 开始,不要默认直接拉满
相似度阈值影响召回率和误报率用小样本打印分数分布后决定
记忆更新频率影响过时率与写入成本按重要程度决定,不要每条都更新

修改任何一个参数前,先做一次小样本对比。我的习惯是准备一组包含 30 到 50 个问题的测试集,分布覆盖跨会话命中、无关信息过滤、冲突信息更新三类场景,每次调整后统一跑一遍。

6.2 怎么判断记忆“好用”

我给这些实验项目写过一个粗糙的评分表,每项按 1 到 5 打分:

  • 命中率:该记住的信息,在后续会话里有多少次被正确找回。
  • 误用率:不该被使用的旧信息,有多少次被拼进上下文并影响了回答。
  • 更新及时性:用户改口之后,系统多久开始使用新信息。
  • 资源开销:每次请求的记忆检索耗时和 token 增量是否稳定。
  • 可解释性:出现错误回答时,能不能快速定位到是哪条记忆导致了错误。

不用追求每一项都满分。不同的应用优先级完全不同:个人知识库更看重命中率和可解释性,客服助手更看重误用率和更新及时性。

6.3 从单机实验到框架化部署

当实验变得稳定之后,再考虑把它整理成一套可复用的组件。这里我特别想强调一个大家常误解的点:“LLM 框架”不是长期记忆的前提。像 LangChain、LlamaIndex 这类编排框架提供了记忆相关的封装,但它们主要解决的是调用链路问题,不是记忆质量本身。

我自己更倾向于先把记忆逻辑写成独立模块,接口保持简单,只暴露三个方法:写入、查询、更新。这样无论底层换不换框架,记忆系统都可以独立演进。框架帮不了你的部分,比如“用户改口之后下一次回答要立刻使用新偏好”,还是要靠自己在写入和读取环节做判断。

7. 常见问题排查顺序

7.1 检索不到记忆:先看输入,再看存储

现象是:用户明明之前说过某件事,跨会话提问却完全没想起来。

排查顺序应该是:

  1. 先确认记忆是否真的写入了,查数据库里的记录数和最近写入时间。
  2. 再确认写入时的输入是什么,检查抽取模块是否把关键信息过滤掉了。
  3. 检查查询问题与记忆的 embedding 相似度分数,看是检索不到还是被阈值过滤掉了。
  4. 最后检查命名空间和过滤条件,是不是查到了但被 user_id 等条件挡掉了。

我遇到过的最多情况是第 2 种:抽取规则写太严格,把“用户提到城市是杭州”这种关键信息当成噪声过滤了。也有不少情况是第 4 种,查询条件少了过滤字段,反而什么都查不到。

7.2 记住旧信息但不更新:问题多半在写入侧

现象是:用户在对话里明确改了偏好,但后续回答还是按旧偏好执行。

重点检查三点:

  • 新信息是否触发了写入流程,有时改写对话没有被判定为“值得记忆”。
  • 更新策略里的相似度阈值是不是设太高,导致新旧记忆没有关联上。
  • 检索读取时是不是只取了旧记忆,没有把新记忆同时带入上下文。

这里有一个小技巧:当用户明确否定之前的信息时,可以在抽取规则里给“否定句”更高优先级,强制触发更新流程,而不是等相似度匹配。

7.3 记忆膨胀导致响应变慢

现象是:记忆条目越来越多,单次请求拼接的 token 越来越大,响应延迟明显上升。

处理顺序:

  1. 检查每次请求实际读取了多少条记忆,是否超过预期 top-k。
  2. 看有没有大量低价值记忆长期堆积,比如“用户今天说了谢谢”这类信息。
  3. 给记忆增加时间衰减或访问频率权重,降低过时条目的读取优先级。
  4. 定期做一轮归档或压缩,把不活跃的细节记忆合并成摘要,只保留核心结论。

7.4 本地部署资源占用过高

如果使用本地模型跑这套系统,资源占用过高通常有三个瓶颈:

  • 本地 LLM 权重本身占显存,fp16/bf16 下 7B 模型大约需要 14G 显存左右,这还没算推理时的中间激活值。
  • embedding 模型虽然小,但并发请求多时 CPU 占用会很高。
  • 向量库随着数据量增长,内存占用会持续上升。

排查时用资源监控工具看是哪个进程在涨,不要凭感觉猜。如果是向量库内存暴涨,先降低向量维度或清理历史版本;如果是 embedding 并发问题,就先加队列限流,而不是盲目加机器。

8. 实验结论与我的最终建议

这轮实验做下来,我最大的感受是:长期记忆系统的复杂度不来自某个单一技术,而是来自数据治理。你要不断回答“什么值得记”“什么时候更新”“怎么避免互相矛盾”“怎么让模型在合适的时机读到合适的信息”。每一个问题单独看都不难,连在一起就成了架构设计。

给不同阶段的读者一个直接的结论:

  • 如果你只是做学习验证,用“文本文件 + 手动记录”就能理解记忆流程,没必要先上向量库。
  • 如果要做个人知识库,直接按“主题段落 + 向量检索 + 摘要压缩”这套组合做,性价比最高。
  • 如果做 Agent 或生产级应用,必须补上更新策略、命名空间隔离、版本记录和可解释性日志,而不是只堆检索能力。
  • 如果发现记忆经常互相矛盾,先别急着换向量数据库,先检查你的写入和更新流程。

我个人更建议先把单条记忆的写入、更新、读取链路跑稳,再考虑批量导入、并发、多用户这些扩展问题。技术方案可以不断换,但流程设计一旦乱掉,后面所有实验都是在错误的地基上垒墙。真正落地时,最该盯住的不是功能列表,而是输入抽取、更新冲突和失败重试这几条容易被低估的细节。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询