AI智能体长期记忆系统设计:EverOS架构与工程实践
1. 为什么AI智能体需要一套独立的长期记忆系统做过AI智能体开发的人大概都有过这种体验单轮对话里模型表现得像个专家一旦把对话拉长到几十轮它就开始“失忆”——前面明确说过的偏好、约束、背景信息到后面全被抛到脑后。这不是模型本身变笨了而是它的上下文窗口被塞满了早期的信息被挤了出去。更麻烦的是即便上下文窗口足够大把几百轮对话全部塞进去推理成本和延迟也会高到无法接受。这就是长期记忆系统要解决的核心问题。它要做的不是简单地“把历史对话存起来”而是要让智能体在需要的时候能够精准地回忆起相关的信息并且把这些信息以合适的形式注入到当前的推理过程中。EverOS 这个项目就是围绕这个目标构建的一整套方案。它面向的是所有正在做AI智能体开发、希望让智能体具备跨会话记忆能力的开发者不管你是用Python还是Java不管你用的是哪种智能体框架这套思路都可以参考。我最初接触这个方向是因为一个客服场景的智能体项目。用户第一天反馈了某个问题第二天再来问智能体完全不记得之前发生过什么用户不得不把背景重新讲一遍。这种体验非常糟糕。后来我们尝试把历史记录全部拼进prompt结果token消耗暴涨响应速度从两秒变成十几秒成本也翻了好几倍。从那时候起我就意识到长期记忆不是一个“存”的问题而是一个“检索和编排”的问题。EverOS 的设计思路本质上是在智能体和原始对话数据之间插入了一层记忆管理层。这层管理层负责把非结构化的对话内容转化成结构化的、可检索的记忆单元然后在推理时按需召回。听起来简单但里面的细节非常多从记忆的切分粒度、存储结构、检索策略到召回后的排序和注入方式每一步都有讲究。2. 长期记忆系统的整体架构与设计取舍2.1 记忆分层短期、工作、长期三层怎么划分在动手写代码之前先把记忆的层次想清楚比什么都重要。我见过不少项目一上来就搞一个向量数据库把所有对话都往里塞结果检索出来的东西要么不相关要么重复冗余。问题就出在没有对记忆做分层。EverOS 的思路是把记忆分成三层。第一层是短期记忆也就是当前会话的上下文通常就是最近几轮对话直接放在prompt里不需要额外检索。第二层是工作记忆指的是当前任务相关的、跨会话但仍然活跃的信息比如用户正在处理的一个工单、一个正在进行的项目背景。第三层是长期记忆是那些沉淀下来的、可能在未来任何时间被召回的信息比如用户的长期偏好、历史问题的解决方案、领域知识。这三层的生命周期完全不同。短期记忆随会话结束就丢弃工作记忆在任务完成后归档长期记忆则持续存在。如果混在一起管理就会出现“把临时信息当成永久偏好”这种错误。举个例子用户某次说“这次先不用发邮件通知”这是短期约束如果被当成长期偏好存下来下次用户就会莫名其妙收不到通知。注意分层的关键不是存储位置的不同而是召回优先级和过期策略的不同。短期记忆永远优先工作记忆次之长期记忆只有在前面两层无法满足时才召回。2.2 存储选型向量库、关系库、图数据库各自的位置存储选型是另一个容易踩坑的地方。很多人一提到记忆就想到向量数据库但实际上向量库只解决了“语义相似”这一个维度的问题。EverOS 的做法是组合使用。向量数据库负责语义检索把对话片段转成embedding存进去适合“找意思相近的内容”。关系型数据库负责结构化信息的存储比如用户的ID、偏好标签、时间戳、会话ID这些适合精确查询和过滤。图数据库则用来存储实体之间的关系比如“用户A提到了产品B的问题C”这种关系网络在需要多跳推理时非常有用。我自己的项目里最开始只用了向量库后来发现一个问题用户问“我上次说的那个问题解决了吗”向量检索能找回相关对话但无法判断“上次”是哪次也无法追踪问题的状态变化。后来加了关系库来记录问题的生命周期才把这个问题解决。所以选型不是越多越好而是要看你的场景需要哪些维度的检索能力。存储类型适用场景典型查询注意事项向量数据库语义相似检索“找和当前问题相关的历史对话”需要控制chunk大小太大检索不准太小丢失上下文关系型数据库结构化过滤“查用户A最近7天的偏好变更”需要设计好schema避免频繁改表图数据库实体关系推理“和这个问题相关的所有实体和事件”维护成本高小规模场景可以先用关系库模拟2.3 记忆写入策略什么时候该记什么时候不该记不是所有对话都值得存进长期记忆。如果每句话都存检索时噪声会非常大。EverOS 在写入环节做了几层过滤。第一层是重要性判断。用一个轻量模型或者规则引擎判断当前对话片段是否包含值得长期保留的信息。比如“今天天气不错”这种寒暄直接丢弃“我对花生过敏”这种事实必须保留。第二层是去重和合并。如果用户多次提到同一个偏好不应该存多条而是更新已有记录。第三层是摘要压缩。对于长对话不是原样存储而是生成一个摘要保留关键信息减少存储和检索的开销。这里有个经验写入策略的阈值不要设得太激进。我一开始为了控制存储量把重要性阈值调得很高结果很多有用的信息被过滤掉了智能体表现得像得了选择性失忆。后来把阈值调低同时加强去重和摘要效果反而更好。存储成本其实不是主要矛盾检索质量才是。3. 核心模块拆解与关键实现细节3.1 记忆抽取从对话流中识别值得记住的内容记忆抽取是整个系统的入口也是最容易被低估的环节。如果抽取阶段漏掉了关键信息后面的检索再精准也没用。EverOS 的抽取模块通常包含几个步骤。首先是对话分块把连续的对话按语义边界切成片段。这里不能简单地按轮次切因为一个完整的信息可能跨越多轮。常见的做法是按话题切换点来切或者用一个小的分段模型来判断。然后是实体和关系识别把对话中的人、事、物、时间、地点等要素提取出来形成结构化的表示。最后是重要性打分给每个记忆单元一个权重用于后续的检索排序。我在实际项目里发现抽取阶段最麻烦的是指代消解。用户说“它坏了”这个“它”指的是什么必须结合上下文才能确定。如果抽取时没有解决指代问题存进去的记忆就是一堆无法理解的碎片。EverOS 的做法是在抽取时就把指代替换成具体实体虽然会增加一些计算量但检索时的准确率提升非常明显。# 记忆抽取的简化示例 def extract_memory(dialogue_chunk, context): # 第一步指代消解 resolved resolve_coreference(dialogue_chunk, context) # 第二步实体和关系抽取 entities extract_entities(resolved) relations extract_relations(resolved, entities) # 第三步重要性打分 importance score_importance(resolved, entities, relations) # 第四步生成摘要 summary generate_summary(resolved) return { entities: entities, relations: relations, importance: importance, summary: summary, raw_text: resolved, timestamp: get_current_time() }3.2 记忆索引让检索又快又准的索引结构设计索引结构决定了检索的效率和召回率。EverOS 采用的是多路索引的方案也就是同一份记忆用多种方式建立索引检索时并行查询最后合并结果。具体来说至少要有三路索引。第一路是向量索引把记忆的摘要或原文转成embedding支持语义检索。第二路是关键词索引用倒排索引支持精确匹配比如用户问“我的订单号是多少”关键词索引能快速定位到包含订单号的记忆。第三路是时间索引按时间范围过滤比如“上周提到的那个问题”。多路索引的挑战在于结果融合。不同索引返回的候选集可能有重叠也可能有冲突需要一个排序策略来决定最终召回哪些。EverOS 用的是加权融合每路索引的权重可以根据场景调整。比如客服场景里时间索引的权重可以高一些因为最近的问题更相关知识问答场景里向量索引的权重更高。提示索引的更新策略也很关键。如果每次写入都同步更新所有索引延迟会很高。EverOS 采用的是异步更新写入先落库索引在后台批量构建用的时候如果索引还没准备好就降级到全量扫描。这个降级策略在流量高峰期非常有用。3.3 记忆召回多路召回与重排序的配合召回环节是把用户当前的查询和记忆库进行匹配找出最相关的记忆单元。EverOS 的召回流程分两步粗排和精排。粗排阶段多路索引各自返回一批候选通常每路返回几十到上百条。这个阶段的目标是高召回宁可多召回一些不相关的也不能漏掉相关的。精排阶段用一个交叉编码器或者更复杂的模型对候选集进行精细打分选出最相关的几条。这个阶段的目标是高准确把不相关的过滤掉。重排序的模型选择上我试过几种方案。用大模型直接打分效果最好但成本高、延迟大。用小的交叉编码器效果稍差但速度快很多。EverOS 的建议是分级处理先用小模型快速筛一遍如果候选集里分数差异不大再上大模型精排。这样在大多数情况下都能在可接受的延迟内完成召回。# 多路召回与重排序的简化流程 def recall_memories(query, top_k5): # 粗排多路召回 vector_candidates vector_index.search(query, top_k50) keyword_candidates keyword_index.search(query, top_k50) time_candidates time_index.search(query, top_k50) # 合并去重 all_candidates merge_and_dedup( vector_candidates, keyword_candidates, time_candidates ) # 精排交叉编码器打分 scored cross_encoder.score(query, all_candidates) # 返回top_k return sorted(scored, keylambda x: x.score, reverseTrue)[:top_k]3.4 记忆注入把召回结果优雅地放进prompt召回之后怎么把记忆放进prompt也是一门学问。放得太少智能体还是记不住放得太多token消耗大而且可能干扰当前推理。EverOS 的做法是按需注入。不是把所有召回的记忆都塞进去而是根据当前任务的需要选择最相关的几条并且用结构化的格式呈现。比如用“已知信息”的段落把记忆以要点形式列出而不是原样拼接对话。这样既节省token又让模型更容易理解。注入的位置也有讲究。通常放在系统提示之后、用户当前输入之前这样模型在生成回复时能优先看到这些记忆。如果记忆很多可以按重要性排序重要的放前面。另外注入的记忆要标注来源和时间方便模型判断信息的时效性。我踩过的一个坑是把召回的记忆直接拼在prompt里没有做格式区分结果模型把记忆当成了当前对话的一部分回复时产生了混淆。后来加了明确的分隔标记和说明文字问题就解决了。4. 从零搭建一套可运行的记忆系统4.1 环境准备与依赖安装动手之前先把环境搭好。EverOS 本身是一个架构方案不是某个具体的库所以你需要自己选型组合。下面是我在实际项目中用过的一套组合比较稳妥。基础环境是Python 3.10以上Java开发者可以用Spring Boot配合相应的客户端库。核心依赖包括向量数据库我用的Milvus或者Qdrant轻量场景可以用Chroma、关系库PostgreSQL足够、embedding模型可以用开源的BGE或者M3E也可以用API、以及一个用于摘要和抽取的小模型7B级别的开源模型就够用。# 以Python为例安装核心依赖 pip install pymilvus psycopg2-binary sentence-transformers pip install openai # 如果使用API做摘要和抽取 pip install fastapi uvicorn # 如果需要暴露HTTP接口安装完之后先跑一个最小验证把一句话转成embedding存进向量库再查出来。这一步能跑通说明基础环境没问题。4.2 记忆库的初始化与Schema设计Schema设计是搭建过程中最需要花时间的地方。设计得好后面查询和扩展都很顺设计得不好改起来非常痛苦。我的建议是至少设计三张表。一张是记忆主表存记忆的ID、摘要、原文、时间戳、重要性分数、会话ID等。一张是实体表存抽取出来的实体及其类型。一张是关系表存实体之间的关系。向量索引单独建和主表通过ID关联。-- 记忆主表 CREATE TABLE memories ( id UUID PRIMARY KEY, summary TEXT NOT NULL, raw_text TEXT, importance FLOAT DEFAULT 0.5, session_id VARCHAR(64), created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); -- 实体表 CREATE TABLE entities ( id UUID PRIMARY KEY, memory_id UUID REFERENCES memories(id), entity_type VARCHAR(32), entity_value TEXT, created_at TIMESTAMP DEFAULT NOW() ); -- 关系表 CREATE TABLE relations ( id UUID PRIMARY KEY, source_entity UUID REFERENCES entities(id), target_entity UUID REFERENCES entities(id), relation_type VARCHAR(32), created_at TIMESTAMP DEFAULT NOW() );注意时间戳字段一定要有而且要用带时区的类型。我见过因为时区问题导致“最近记忆”检索出错的案例排查了很久才发现是数据库时区设置的问题。4.3 写入流程的完整实现写入流程从对话结束或者对话进行中触发。EverOS 的建议是异步写入不要阻塞主对话流程。具体来说对话产生后先把原始内容丢进一个消息队列后台消费者负责抽取、摘要、索引构建。# 写入流程的简化实现 async def write_memory(dialogue_chunk, session_id): # 第一步抽取 memory_data extract_memory(dialogue_chunk, context) # 第二步生成摘要和embedding summary memory_data[summary] embedding embedding_model.encode(summary) # 第三步写入关系库 memory_id save_to_postgres(memory_data, session_id) # 第四步写入向量库 save_to_vector_db(memory_id, embedding, summary) # 第五步更新索引 update_keyword_index(memory_id, summary) update_time_index(memory_id, memory_data[timestamp]) return memory_id这里有个细节写入顺序很重要。先写关系库拿到ID再写向量库这样如果向量库写入失败关系库里还有记录可以重试。反过来如果先写向量库关系库失败向量库里就会留下孤儿记录。4.4 检索接口的设计与参数调优检索接口是智能体调用记忆系统的入口。设计得好智能体用起来很顺手设计得不好调用方要写很多胶水代码。EverOS 的检索接口通常接受几个参数查询文本、会话ID用于过滤当前会话的记忆、时间范围可选、返回条数。返回结果包含记忆的摘要、原文、相关性分数、时间戳。# 检索接口示例 def search_memories(query, session_idNone, time_rangeNone, top_k5): # 构建过滤条件 filters {} if session_id: filters[session_id] session_id if time_range: filters[created_at] time_range # 多路召回 candidates recall_memories(query, filters, top_k * 10) # 重排序 ranked rerank(query, candidates) # 返回top_k return ranked[:top_k]参数调优上top_k不要设得太大通常3到5条就够了。太多会稀释相关性也会增加token消耗。时间范围要根据场景来客服场景通常看最近7天知识场景可以放宽到30天甚至更久。5. 实际部署中的性能优化与踩坑记录5.1 延迟优化从秒级到毫秒级的几个关键改动记忆系统的延迟直接影响智能体的响应速度。我最初做的版本一次检索要两秒多用户明显感觉到卡顿。后来做了几个优化把延迟降到了200毫秒以内。第一个优化是缓存。对于高频查询把检索结果缓存起来下次直接返回。缓存的有效期可以设短一些比如5分钟避免记忆更新后返回旧结果。第二个优化是索引预热。把常用的索引加载到内存里避免每次查询都走磁盘。第三个优化是并行召回。多路索引的查询并行执行而不是串行这样总延迟取决于最慢的那一路而不是所有路之和。还有一个容易被忽略的点是embedding计算。如果每次查询都要实时计算embedding延迟会很高。EverOS 的做法是对于常见的查询模式预计算一批embedding缓存起来。当然这个要看场景不是所有场景都适用。5.2 常见问题速查表问题现象可能原因排查方向解决方案检索结果不相关chunk太大或太小检查分块策略调整chunk大小增加重叠记忆重复去重逻辑失效检查去重阈值降低相似度阈值加强合并写入延迟高同步写入阻塞检查写入流程改为异步写入加消息队列召回率低索引覆盖不全检查索引更新确保所有记忆都建了索引时间检索不准时区问题检查数据库时区统一使用UTC时间指代错误消解模型不准检查上下文长度增加上下文或换模型5.3 记忆膨胀与过期策略系统跑久了记忆库会越来越大检索效率会下降。EverOS 建议设置过期策略。不是所有记忆都永久保留可以根据重要性和时间做清理。我的做法是分三档重要性高的永久保留重要性中等的保留90天重要性低的保留30天。过期前可以归档到冷存储需要时再恢复。另外对于同一实体的多条记忆可以定期做合并把碎片化的记忆整合成一条完整的记录。提示过期策略一定要可配置不同场景的需求不一样。客服场景可能需要保留更久因为用户可能几个月后再来问同样的问题。6. 智能体框架集成与多场景适配6.1 与主流智能体框架的对接方式EverOS 作为记忆层需要和上层的智能体框架对接。不同的框架对接方式不同但核心思路是一样的在智能体处理用户输入之前调用记忆检索接口把召回的记忆注入到prompt里在智能体生成回复之后调用记忆写入接口把本轮对话存进去。以常见的ReAct框架为例可以在Agent的think步骤之前插入记忆检索在act步骤之后插入记忆写入。如果是基于LangChain之类的框架可以写一个自定义的Memory组件实现load_memory_variables和save_context两个方法。# 自定义Memory组件示例 class EverOSMemory(BaseMemory): def load_memory_variables(self, inputs): query inputs.get(input, ) memories search_memories(query, top_k5) return {history: format_memories(memories)} def save_context(self, inputs, outputs): dialogue fUser: {inputs[input]}\nAssistant: {outputs[output]} write_memory(dialogue, session_idself.session_id)对接时要注意会话ID的传递。每个会话要有唯一的ID这样检索时才能区分不同会话的记忆。如果会话ID丢失记忆就会串台。6.2 多用户、多会话场景下的隔离多用户场景下记忆隔离是必须的。用户A的记忆不能被用户B检索到。EverOS 的做法是在所有查询里强制加上用户ID过滤并且在存储层面也做隔离比如不同的用户用不同的collection或者不同的表空间。多会话场景下同一个用户可能有多个并行的会话。这时候要区分会话级记忆和用户级记忆。会话级记忆只在当前会话内有效用户级记忆跨会话共享。检索时先查会话级再查用户级合并结果。我踩过的一个坑是没有做用户隔离测试时两个用户的记忆混在一起智能体把A的偏好用在了B身上闹了笑话。后来在检索接口里强制加了用户ID参数并且在数据库层面也加了行级安全策略才彻底解决。6.3 效果评估怎么判断记忆系统好不好用记忆系统的效果评估是个难题因为它不像分类任务那样有明确的准确率。EverOS 建议从几个维度来评估。第一个维度是召回相关性。人工标注一批查询和对应的相关记忆计算召回率和准确率。第二个维度是任务完成度。在具体的智能体任务里看加入记忆系统后任务成功率有没有提升。第三个维度是用户体验。让真实用户使用收集反馈看他们是否感觉到智能体“记住了”之前的信息。我的经验是任务完成度是最有说服力的指标。在客服场景里加入记忆系统后用户重复描述问题的比例下降了60%以上问题一次解决率提升了将近一倍。这个效果比任何离线指标都更有说服力。7. 我在这套系统上踩过的坑和总结的经验做长期记忆系统这一年多踩的坑比预想的多得多。有些是技术选型的问题有些是设计思路的问题还有些是工程实现上的细节。最大的一个教训是不要试图用一套方案解决所有场景。我一开始想做一个通用的记忆系统结果发现客服场景需要强时间过滤知识场景需要强语义匹配个人助手场景需要强实体关系。后来把系统做成可配置的不同场景用不同的检索策略和权重效果才好起来。第二个教训是记忆的写入比读取更重要。很多人把精力花在检索优化上但如果没有好的写入策略检索再优化也是在垃圾里找金子。抽取的准确性、去重的彻底性、摘要的质量这些决定了记忆库的上限。第三个教训是评估要尽早做。不要等到系统上线了才想起来评估那时候改成本太高。在开发阶段就建立评估集每次改动都跑一遍确保效果不退化。最后分享一个实用的小技巧在记忆注入prompt的时候给每条记忆加上时间戳和来源标注比如“[2026-01-15 用户反馈] 用户对花生过敏”。这样模型在生成回复时能更好地判断信息的时效性和可信度。这个改动很小但效果提升很明显尤其是在处理时效性强的信息时。这套系统后续还可以往几个方向扩展。一个是主动记忆不等用户查询智能体主动判断当前场景是否需要召回记忆。另一个是记忆推理不只是召回原始记忆还能基于记忆做推理比如“用户上次说对花生过敏这次推荐的餐厅要避开含花生的菜品”。这些方向都还在探索中但已经能看到不小的潜力。