OpenViking:AI Agent长期记忆系统设计实战复盘
2026/9/4 10:44:20 网站建设 项目流程

做了大半年AI应用,我一直被同一个问题折磨:模型明明聊得好好的,换个会话就“失忆”了。用户几个小时前交代的偏好、之前明确说过的规则、项目里反复强调的口径,一到新对话里全得重新问一遍。后来我干脆不指望靠塞上下文解决这件事,动手做了一套叫 OpenViking 的长期记忆系统,算是把这个问题彻底从根上收拾了一轮。

OpenViking 不是聊天记录备份工具,它是一层独立的“记忆治理服务”。它可以接在你现有的 AI Agent 后面,也可以跟 LangChain、Spring AI 这类框架共存,专门处理一件大事:把对话和交互过程中产生的信息,筛选、压缩、组织成可跨会话持久使用的长期记忆。这篇文章就是我的完整复盘,覆盖了记忆分层设计、数据建模、写入管线、检索唤起、参数调优和评测方法,适合正在做 AI 助手、智能客服、Agent 应用,或者被“AI 前后不一致”困扰的开发者和AI产品经理参考。

1. 整体设计:不是给模型塞聊天记录,而是帮 AI 建一个会“遗忘”的脑子

1.1 长期记忆系统到底解决什么问题

先说一个很常见的误区。很多团队觉得,要让 AI 记住东西,就把历史聊天记录全部塞进上下文里。我早期也这么干过,上下文窗口越来越大,单次请求的 token 费用越来越贵,模型却越来越“糊涂”。原因并不难理解:人类一次能专注处理的信息量有限,大模型也一样。你把三个月前的闲聊、昨天的日志、今天的临时指令全部堆在一起,它会分不清轻重,最后被无关信息干扰得答非所问。

长期记忆系统解决的,是“在正确的时间,把正确的那部分过去,带回模型面前”。它更像一个图书管理员,而不是一间堆满纸箱的仓库。系统要做的不是存储一切,而是理解哪些信息值得长期保留、哪些信息只会带来噪音、哪些信息需要定期修正或者淘汰。这听起来像哲学问题,落地时却非常工程化:你得给每条记忆打标签、算权重、定生命周期,还要在用户提问的时候,从成千上万条记忆里把真正相关的那几条捞出来。

OpenViking 这个名字是我内部用的项目代号,本质上它是一套用 Python + FastAPI 写的记忆服务,核心模块包括记忆抽取器、嵌入器、混合检索器和记忆维护器。你不需要完全照抄我的代码,但里面“记忆要分层、要衰减、要可触发”的设计思路,在我看来是所有需要长期记忆的 AI 应用都绕不开的。

如果要说清楚这套东西适合谁,最典型的几类人包括:做 AI 情感陪伴或者个人助理的开发者,因为这类产品必须记住用户的喜好和经历;做企业知识库问答的团队,因为领域口径和用户身份信息需要跨会话稳定;还有做 AI Agent 的工程师,因为 Agent 执行多步任务时,很容易把中间步骤的结果和用户约束搞丢。

1.2 记忆不是一个大筐:四层记忆架构怎么划分

我自己吃过“把所有东西塞一个向量库”的亏,所以这次在设计的时候,一开始就把记忆做了严格分层。人脑如果只有一种记忆,根本没法工作:你不会用“你昨天中午吃了什么”的记忆来驱动“怎么回答老板的问题”,也不会把骑自行车的肌肉记忆当成百科知识来调用。AI 的长期记忆系统也应该参照这个逻辑。

OpenViking 里面分了四层:

  • 工作记忆:当前会话正在处理的信息,聊天记录本身、临时状态、上一步返回的结果,都属于这一层。会话结束就清理,甚至不需要落库。
  • 语义记忆:用户长期不变的画像和规则。比如“用户是产品经理”“不喜欢长篇大论”“对外报告统一用中文”。这类记忆更新频率低,但引用优先级极高。
  • 情景记忆:过去发生的具体事件。比如“上周三用库存接口导过一次数据”“昨天给某个客户生成过一份周报”。这类记忆必须带时间戳,用户常会问“上次那个文件在哪儿”之类的问题。
  • 程序记忆:解决问题的流程和工具方法。例如“要查视频审核状态,应该调 audit API,而不是 content API”“创建工单前需要先查客户是否欠费”。这类记忆通常来自成功执行过的任务或人工整理,价值密度最高。

分层之后,各层的存储方式和检索策略都不一样。语义记忆我放在键值库加向量索引,因为要稳定而且经常按用户维度整批召回。情景记忆放在事件表里,配合向量和时间范围过滤,因为用户经常会带“最近”“之前”“上周”这种时间条件提问。程序记忆则更像技能文档,用文本索引加标签管理,像查说明书一样去查询。

很多人会觉得分层麻烦,不如全塞进向量库。但真实场景里,不分层的后果相当难受。举一个我踩过的例子:有个用户在某次闲聊里抱怨“最近加班太多,好累”,这句话如果不加区分地进入记忆库,就可能被当成用户长期状态,之后每次打招呼,AI 都会问“您是不是最近很累”。但实际上,那只是一条有时效的情绪表达,最多一两天就过期了。所以分层还有一个重要作用:帮你区分“该记住的”和“该淡忘的”。

1.3 为什么要引入遗忘机制:一个会遗忘的系统才是健康的

做长期记忆系统,如果不谈遗忘,一定会在运行一段时间后翻车。我说的“遗忘”不是把数据删掉,而是引入一套权重衰减和生命周期管理机制。

你可以把每条记忆想象成一个参与投票的人,票数权重就是它的“有效度”。某条记忆刚写入时有效度很高,但如果没有持续被索引、被强化,就会随时间推移慢慢降低,降到阈值以下就进入冷档。冷档记忆不会完全删除,但不再参与常规检索,除非用户主动触发非常久远的回忆场景。

这个机制直接解决的问题是“观点过期”。我印象很深的一个例子是:一个用户在系统里存了“我平时喝咖啡都选深烘”,过了两个月他又说“最近在控制咖啡因摄入,改喝低因豆”。如果系统没有衰减机制,新旧记忆权重一样,模型检索时会同时看到两条冲突信息,最后生成一个含糊的答案。但有了时间衰减后,新写入的记忆权重更高,旧记忆则逐渐退居次要位置,只有在新记忆不明确时才会被翻出来。

实现这个机制并不复杂,每条记忆上带一个发布时间或更新时间,定期用一个衰减函数重算其有效分数就行。我在实测里发现,遗忘机制还能顺带解决一个问题:记忆库的检索质量不会随着数据量增大而线性下降。因为旧数据会不断降权,最终能被查到的永远是近期、高重要度、强相关的那部分,这跟向量库塞满了之后噪声太多的情况完全不同。

2. 记忆数据怎么建,细节决定这套系统能不能“跑得久”

2.1 单条记忆的字段:不只是“文本加向量”

我在最初设计表结构时以为很简单:内容文本、embedding 向量、时间戳,完事了。结果第一个月就被自己坑了。没有足够元数据的记忆向量就像一本没有目录、没有页码的书,你知道里面有内容,但不知道什么时候该引用它,也不知道该以多高的优先级信任它。

OpenViking 里每条记忆目前包含这些核心字段:

字段名类型说明
memory_idstring全局唯一标识
user_idstring所属用户或会话主体
memory_typestring语义/情景/程序,对应前面的分层
contenttext改写压缩后的记忆正文
embeddingvectorcontent 的语义向量
entitiesarray涉及的人、组织、时间、地点、事件名
importancefloat重要度基线,写入时打分
decay_ratefloat衰减速率,不同类型不一样
valid_scorefloat当前有效度,随时间和检索动态调整
source_infojson来源是聊天记录、任务日志还是人工录入
last_accessed_atdatetime最近一次被召回的时间
created_at / updated_atdatetime创建和更新时间

这里多出来的几个字段里,importancedecay_rate是灵魂。importance表示这条记忆在刚诞生时有多重要,decay_rate表示它随时间消减的速度。比如用户主动说“记住,以后我不喝冰的”和一句随口的闲聊“今天有点冷”,前者的重要度可能打到 0.9,后者只有 0.3;前者衰减率很慢,后者可能几天权重就趋近于零。

entities字段也非常关键。用户提问时经常不会完整复述记忆内容,而是只说一个关键词:“那个北京的项目怎么样了?”如果你的记忆没有实体索引,单靠向量检索有可能匹配不精准。把实体单独抽出来后,它既可以参与 SQL 过滤,也可以配合检索后做精确匹配,召回质量提升非常明显。

2.2 存储底座怎么选:一张表打天下基本不现实

在存储选型上,我的要求比较朴素:不引入太多中间件,同时满足三类查询。第一类是向量相似度查询,第二类是精确关键词/实体过滤,第三类是时间范围和来源筛选。早先设想用单个向量数据库搞定,后来发现不少向量库对文本过滤支持比较薄弱,条件复杂一点的查询要么性能差,要么语法绕。最后 OpenViking 用的是 PostgreSQL + pgvector + 全文索引的组合,Java 那边如果上 Spring AI,也可以沿用同一套表结构。喜欢轻量一点的团队,用 SQLite 加 sqlite-vec 做单机演示也够用。

为什么没有单独引入 Elasticsearch?坦白说,单条记忆的文本量很小,往往只是一两句话,不像文档搜索那样需要复杂的倒排索引和分片。PostgreSQL 内置的 tsvector 全文检索已经可以覆盖关键词查询需求,配合正则做实体匹配也很快。这样部署的时候只需要一个 Postgres 实例,运维成本低很多。

向量库我用 pgvector 的 HNSW 索引,索引参数按经验设置为m = 16ef_construction = 64,查询时根据返回条数调整ef_search大小。这个参数组合在千万级以内的记忆量上表现很稳,不需要一上来就上 Milvus 或 Qdrant。如果你的系统真的做大了,从 pgvector 迁到独立向量库也相对简单,因为上层服务只依赖一个抽象出来的向量检索接口,具体实现随时可以替换。

2.3 去重和关联:避免记忆库变成“复读机”

还有一类问题很隐蔽:用户会在不同时间用不同说法表达同一个意思。比如周一他写“我叫Chris”,周五又说“大家可以叫我小C”,如果系统不管,就会存下两条看似不同、实则指向同一属性的记忆。检索的时候两条都命中,AI 回答时反而不知道用哪个称呼好。

OpenViking 处理这个问题的思路是:写入前先做一次“近似查重”。用新记忆的 embedding 到已有记忆里找高相似度候选,相似度超过一定阈值时,不新增记录,而是更新原记录的内容合并别名。实际操作里我给相似度阈值设了 0.86,低于这个值会把两条当成完全无关,我自己测试下来漏判和误判的比例比较平衡。

如果两条高相似记忆内容上有冲突,那就不是简单合并能解决的了,需要进入“矛盾消解”流程。系统会标记这条记忆处于需确认状态,在后续对话中向用户核实一句:“我记得你之前说过不喝冰的,现在是改成常温了吗?”收到明确答复后才覆盖更新。这个做法,本质上就是把记忆当作一等公民,而不是无脑往数据库里插文本,长期跑下来记忆库的质量会稳定很多。

3. 记忆写入管线:AI 怎么学会“记重点”

3.1 候选抽取:不是每句话都值得放进长期记忆

记忆系统的第一条管线,是用来判断“什么值得记住”。这步是在跟大模型交互时通过一个抽取代理实现的。每轮对话结束后,OpenViking 会把最近若干轮内容打包发给一个提取模型,让模型判断里面有哪些值得沉淀到长期记忆。提示词里我会明确告诉模型:只抽用户偏好、身份类信息、任务成功经验、跨会话需要的约束和规则;不要抽寒暄、一次性情绪、正在处理又很快会变的临时状态。

为了让抽取结果更可控,模板里要求模型输出结构化的 JSON 列表。每个候选包含content(改写后的记忆正文)、memory_type(语义/情景/程序)和importance(重要度)。下面是一个简化版的解析样例:

[ { "content": "用户偏好美式咖啡,不加糖,但在控制咖啡因摄入时选择低因咖啡", "memory_type": "semantic", "importance": 0.85 }, { "content": "本周二为用户生成过5月销售看板,数据源来自MySQL的sales库", "memory_type": "episodic", "importance": 0.6 } ]

你可能想问,为什么不直接用规则匹配关键词来判断?因为用户的表达实在太多样了,有人会说“别放糖”,有人说“我不爱吃甜的”,有人会讲“控糖期还是少碰甜的吧”,规则引擎很难覆盖这么多写法,LLM 做语义层面的价值判断反而更适合。代价是每轮对话会多一次模型调用,实测增量成本百分之几,换来的是记忆质量大幅提升,很划算。

但要注意,不推荐把抽取和对话主流程做成强同步。如果抽取接口超时,不应该影响主对话的返回。我现在的做法是把抽取任务丢进一个任务队列,对话正常返回后,后台异步处理记忆抽取和写入。这样即使用户一次性聊了很多轮,也不会有明显延迟感。

3.2 文本改写与压缩:别把流水账原样写进记忆库

很多系统死在“原样存取”上:用户说了一句“我现在在做一个智能硬件项目,需要一个帮忙写文案的助手”,系统就把这个长句原封不动存进去。等过了一周,模型检索到这句话,发现话里带着“现在”“一个”这种临时性措辞,反而影响理解。

写入长期记忆之前,一定得做一次“去情境化改写”。OpenViking 会用另一个轻量模型,把原始表述中的冗余去掉、时态理顺、补全丢失的指代。比如原文是“我和老王合作的项目下周三上线”,改写后就变成“用户与老王合作的智能门锁项目,预计 2024-05-15 上线”。这样写的好处是,将来检索到这条记忆时,它本身已经不依赖当时的上下文了。

对话里的指代是改写中最容易出错的点。模型必须结合前面的多轮对话,把“它”“那个项目”这类代词还原成具体主体,否则记忆写下来就是残缺的。我在工程实现上会把触发写入的相关对话上下文一并传给改写模型,而不是只传用户当前那一句。有人说多轮对话历史会造成 prompt 过长,但实际记忆改写是后台任务,不要求秒回,所以哪怕传二十轮历史,只要模型能稳定输出结构化结果就行。

嵌入向量在改写完成后再生成,这一步顺序不能反。如果你拿原始对话生成 embedding,存进库里的是带噪音的语义,检索时很容易召回到一堆历史口水话。改写完,再调用 embedding 模型得到向量,比如可以用 bge-m3 或者 text-embedding-3-small,都会有一个不错的泛化能力。我这边是本地部署了 bge-m3 的 ONNX 版本,单条生成大概几十毫秒,批量处理非常适合作异步队列消费。

3.3 写入更新与衰减:把“冲突消解”做在平时

记忆写入不是简单的 insert。OpenViking 的写入逻辑分四步:近似查重、冲突检测、插入或者合并、重算关联实体的有效度。大概的伪代码如下:

def write_memory(user_id: str, candidate: MemoryCandidate): # 1. 向量召回疑似重复记忆 similar = vector_search(candidate.embedding, top_k=3, score_threshold=0.86) # 2. 对召回结果做类型与实体的交叉验证 for old in similar: if jaccard_similarity(candidate.entities, old.entities) > 0.5: if candidate.importance > old.importance: old.plus_or_update(candidate) # 保留新信息并更新重要度 old.valid_score = min(1.0, old.valid_score + candidate.importance * 0.2) return else: candidate.importance = old.importance * 0.9 # 降级写入 break # 3. 没有重复则作为新记忆写入,并标记最后一次访问时间 insert_one(user_id, candidate) refresh_cache(user_id) def decay_memoryjob(user_id: str): # 后台定时任务,按衰减率重算每条记忆的有效度 for mem in get_active_memories(user_id): elapsed = now() - mem.updated_at decay_factor = exp(-mem.decay_rate * elapsed.days()) mem.valid_score = mem.importance * decay_factor if mem.valid_score < 0.1: archive_memory(mem.memory_id)

这里需要补充说明一下“降级写入”这个逻辑。当新记忆和旧记忆冲突,但旧记忆重要度更高时,我们不能直接把旧记忆删掉,因为用户很可能只是临时改口,而不是彻底颠覆之前的偏好。最稳妥的方式是新记忆降级写入,旧记忆仍保留高权重,等用户再次确认新偏好后,再通过更新接口把旧记忆置为低优先级。很多同学喜欢“后说为准”的原则,但放到真实场景里,尤其是情绪化的对话场景,这个原则并不总是可靠。加上一个降级缓冲,能少掉很多不可逆的误伤。

衰减任务我是通过 PostgreSQL 定时触发器调度的,每天凌晨跑一次全量重算。如果记忆量超大,可以改成实时流水式衰减,只重算最近被访问过的记忆。不过我在千万级以下的记忆量时,每天跑全量也只需要几分钟,完全不用担心。

4. 检索与唤起:用户问一句,AI 是怎么翻“旧账”的

4.1 查询侧意图识别:先搞清楚用户是在“查事实”还是在“要上下文”

光有存储没有好的检索,记忆系统还是会沦为摆设。用户问“我之前让你保存的那个表格模板呢”,和问“我的报告偏好是什么”,表面上是两种不同的提问,检索策略应该完全不同。

OpenViking 在检索前会先用一个小模型对用户当前这句话做意图分类,判断需要访问哪一层记忆,以及是否需要带时间条件。虽然这个环节会额外增加推理延迟,但收益非常明显。因为加上记忆类型过滤之后,向量检索的候选范围缩小了一大半,召回准确率会更高。

意图分类的结果就是一个简单的结构化标签。举几个常见的例子:偏好查询对应语义记忆;事件回查对应情景记忆,并附带一个可能的时间跨度;技能调用对应程序记忆。如果查询里出现“上次”“之前”“昨天”“那天”这类时间词,识别模块会自动从上下文推断一个时间范围,检索 SQL 就会带上created_at的过滤条件。

这个设计来自一个体验很差的教训。最早我只做纯向量相似度检索,用户问“上次那个方案里提到的新功能是什么”,向量检索确实把“方案”相关记忆召回了,但由于时间权重没参与排序,模型找到的可能是一周前那份旧方案,而不是昨天刚更新的版本。后来加了时间条件解析,这个问题基本就消失了。

4.2 多路召回与重排:不只是算一个余弦相似度

单路向量召回很容易碰到“语义很像,但不是用户要的”这种尴尬。用户问“我的写作风格偏好”,系统应该优先召回“用户喜欢结构化表达,结论先行”,但向量相似度可能把“用户看了一篇关于写作风格的文章”这种相似的闲聊也捞出来。所以 OpenViking 的检索环节做成了多路召回加统一重排。

向量召回是一路,关键词精确匹配是另一路。关键词那一路我会把用户问题里的实体和名词短语抽取出来,到contententities字段里做全文匹配。两路结果统一进入重排序模型。重排的评分函数比较直观,综合考虑语义相似度、文本关键词命中数、记忆类型的优先级、有效度分数和时间新鲜度。下面是一个简化版评分公式:

score = 0.55 * vector_sim(query.embedding, mem.embedding) \ + 0.25 * bm25_score(query.terms, mem.content) \ + 0.10 * type_priority(mem.memory_type) \ + 0.10 * valid_score(mem) \ + 0.05 * recency_bonus(mem)

权重是我试了几百轮之后调出来的。vector_sim权重最大,因为它负责泛化,能关联到同义表达;bm25_score负责精确性,防止完全跑偏;valid_scorerecency_bonus保证时间近、未被遗忘的记忆优先出现。如果你们对特定业务有要求,比如金融领域必须优先看最新监管口径,可以把valid_score的权重调高,把时间新鲜度上升为主导因子。

重排之后,系统会按预算选 Top N 条记忆拼进最终的提示词。N 的设置很关键,不是越多越好,我通常设 5 到 8 条左右。记忆太多会让系统“啰嗦”,反而干扰主任务;记忆太少又可能漏掉关键信息。你需要通过评测去压这个参数,我后面会专门说评测方法。

4.3 触发条件:有的记忆不需要每次召回

还有一个很大的痛点,某些记忆虽然长期有效,但它只适用于特定情景,你不能让它在不合适的时候跑出来捣乱。比如用户曾说过“写周报时我习惯先看数据再看评论”,这算一条很好的语义记忆,但用户问“今天天气怎么样”的时候不应该把这条记忆塞进上下文。

为此 OpenViking 在记忆里加了一个可选字段:trigger_conditions,用自然语言或结构化条件表示这条记忆的适用场景。只有当当前查询或上下文满足触发条件时,这条记忆才会进入召回候选。比如上面那条周报偏好,触发条件就是“用户在讨论周报/月报/汇报文档的生成”,不满足就直接跳过。

这个字段不一定要非常形式化。我自己是直接让写库模型生成触发条件描述,检索时再利用 embedding 判断当前查询和触发条件的相关度。等于在大的记忆召回前,加了一个守卫层。好处很明显:上下文空间不再被那些“长期有效但本刻不相关”的记忆浪费,模型能更专注于当前任务。

5. 参数怎么调、效果怎么测:凭感觉调参必翻车

5.1 重要度阈值和 TopK:哪些参数需要关注

很多人在第一次搭完记忆系统后都会问我:为什么我的 AI 还是那么笨?答案多半是在参数上一刀切。以下几个参数基本决定了记忆系统的性格,值得你细细调:

  • importance_threshold:判断一条新记忆是否值得入库,我默认设 0.4,低于这个值直接丢弃或者只做短期缓存。
  • vector_similarity_threshold:用于查重判断,默认 0.86。不同 embedding 模型的取值范围会有差异,换模型后需要重新校准。
  • memory_type_priority:语义记忆的召回优先度我设为最高,程序记忆次之,情景记忆按时间范围收敛,工作记忆不进长期检索。
  • top_k_memories:最终塞进上下文的记忆条数。我做陪伴型和助手型场景一般设 6 左右,如果任务本身逻辑强、上下文长,可以把值降到 4;如果是信息密集的咨询场景,可以放宽到 10。
  • decay_rate:不同类型记忆的速率差异较大。语义记忆一天衰减不到 0.005,情景记忆大概一天 0.02 到 0.05,临时情绪类甚至可以 0.1 起步,几天内就归档。

调这些参数时,要尽量避免一次性全改,不然出了问题你根本不知道是哪个参数造成的。每次都只改一个变量,跑一组评测对话,观察结果再动下一个。这跟训练模型调超参的原则一样:先固定大部分,再单变量搜索。

5.2 搭一个最小可行的记忆评测集

记忆系统最麻烦的地方在于:它不像模型精度那样有个标准测试集。同一个问题,昨天回答得好,今天可能因为召回到的记忆不同而变化。所以我自己做了一个非常轻量但是足够说明问题的评测集,大概是二十多条带标准答案的交互场景。

每一条评测包括:一段前序对话上下文(用来种下记忆)、一个当前用户输入、一个理想回答里必须包含的信息点。跑评测时,系统先按前序对话把记忆写入库,再执行检索和生成,最后检查回答是否覆盖了必需的信息点。比如前序对话里用户说“我下个月要去北京出差”,当前输入是“帮我规划下周的日程”,理想回答必须包含“用户在下个月有北京出差安排”这条信息。

人工一条条看结果太费劲,我把输出做成了自动化检查。GPT-4 这类通用模型可以直接当裁判,让它判断回答是否包含目标信息点,并将包含情况分为符合、不符合、有歧义三档。跑完一周的评测后,我把符合率作为核心指标来跟踪。OpenViking 在只做向量检索时,符合率大概只有 62%;加上关键词召回、时间衰减和触发条件之后,稳定在 84% 左右。数据不一定普适,但至少说明每一步都带来了一点真实提升,而不是自我感觉良好。

冷启动阶段,如果没有历史对话怎么办?有两种补数据的路径:第一种是用户/运营主动录入规则,例如“公司产品对外名称统一叫XX”,这类数据直接写入语义记忆,不做抽取;第二种是从历史聊天记录里批量离线抽取,让抽取模型跑一轮,人工抽检后再导入。不要一上来就追求全自动,冷启动时人工整理一批高质量种子记忆,比系统跑一个月自动积累的效果都明显。

6. 常见问题与排查技巧实录

6.1 记忆串味和幻觉扩散

最典型的问题是 AI 会一本正经地把不同用户的记忆混在一起,或者把用户某次极端情绪当成长期人设。比如用户只是吐槽了一句“这产品太烂了”,系统竟然在后续对话里默认用户对该产品持有强烈负面态度,甚至替用户脑补出一堆没说过的问题。

排查的时候先看数据:把这条记忆从库里捞出来看原始文本、来源信息和重要度。如果有问题的记忆确实被写进去了,往往是因为抽取阶段的重要度打分失效。此时应降低临时情绪的写入重要度,并让抽取模型在遇到情绪夸张表述时特别标注为低置信度。另外,如果同一时间有多个用户的数据在并发写入,还需要检查 user_id 是否关联正确,避免把不同用户的上下文错配到同一个 user_id 下。

6.2 检索延迟和记忆库膨胀

上线初期,记忆库几千条的时候,检索特别快。跑到几十万条以上时,如果接口变慢,先看执行计划是不是没走到向量索引。pgvector 的 HNSW 索引如果不生效,召回会退化成暴力扫描,几十万条的记忆全量计算相似度,不快才怪。解决办法是重建索引并检查enable_seqscan设置。另一个常被忽略的原因是查询里带了太多 OR 类型的实体过滤条件,导致优化器放弃走索引。把记忆类型和实体过滤改成“先缩小候选集,再进向量检索”的顺序,效果会好很多。

还可以用一层 Redis 缓存做常见查询的快路径。用户重复问相似问题时,直接返回上一次召回的记忆集合,避免每轮都跑全链路的检索和重排。缓存过期时间设 5 到 15 分钟足够了,既不会太陈旧,又能明显降低延迟。

6.3 覆盖丢失和重要记忆被顶掉

写入新记忆后,旧记忆被意外合并或覆盖,是用户投诉的重灾区。排查这类问题时,建议先把重复判定阈值调高一点,比如从 0.86 调到 0.93,宁可多存两条近似记忆,也别把关键信息覆盖掉。如果遇到的是用户主动更新的情况,不要直接物理删除旧记录,在旧记录上打一个superseded_by标记并降权即可,万一用户反悔还能追溯回来。

我的经验是,在记忆系统上宁可保守也不要激进。“记住了但没用上”的代价,远比“忘了但假装记得”要小。很多 AI 产品聊着聊着让人出戏,根源都是记忆不精准。做这套系统半年,我最深的体会是:长期记忆不是无限量地记住,而是有策略地遗忘,在每次需要时,精确找回那一小段真正该被想起来的过去。

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

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

立即咨询