AI Agent分层记忆架构:短期、长期与语义记忆的设计与实战

发布时间:2026/10/28 10:27:03
AI Agent分层记忆架构:短期、长期与语义记忆的设计与实战

2. 为什么需要给Agent配分层记忆

先说个我自己的体会。做Agent开发这件事,最早期大家拼的是模型能力,谁接的模型强,谁的提示词写得好,谁的效果就领先。但做久了你会发现一个尴尬的问题:再聪明的模型,没有记忆,它就是个“金鱼脑”——你跟它聊十轮,它连你第一轮说过什么都记不住。

我印象特别深的一个项目是做一个企业内部的知识助理Agent,需求很简单:员工问它问题,它要能结合这个员工过往问过什么、关注过什么、公司知识库里有什么,给出个性化回答。听起来不复杂对吧?但一跑起来全是坑。第一版我只用了上下文窗口,把所有历史对话全部塞进提示词里,结果聊不到十轮,Token费用就爆炸了,而且模型开始“遗忘”最早的信息,回答质量直线下滑。后来我换成了简单的向量检索,把所有历史消息都存进向量库,每次检索Top K塞进上下文,问题也没完全解决——因为历史消息里噪音太多了,检索出来的东西经常跟当前问题八竿子打不着。

那段时间我就在想,人对记忆的处理方式是怎样的?短期记得刚说的话,中期记得昨天做过的事,长期记得自己的身份偏好和知识储备。Agent其实也一样,只是很多项目把它当成了一锅乱炖。后来我在一个开源项目里看到了“分层记忆”的设计思路,才彻底想通:Agent记忆不该是一个大池子,而应该是分层的、各司其职的多个子系统。

这篇文章不聊虚的,直接把我踩过的坑和验证过的方案梳理一遍,包括为什么分层、层与层之间怎么配合、每一层的数据结构和存储选型怎么做、以及检索策略怎么调。无论你是在做客服Agent、个人助理Agent,还是在捣鼓多Agent协作框架,这套思路应该都能帮上忙。

3. 分层记忆的整体设计与核心思路

3. 分层记忆的整体设计与核心思路

3.1 一套记忆系统要解决哪三个核心问题

在设计记忆架构之前,我习惯先把它拆成三个问题来看:

第一,记忆要能“用得上”。模型本身有上下文窗口限制,靠全量塞入来处理记忆是最笨的办法,成本高、效果差。真正的问题是:从海量存储中,怎么把与当前任务最相关的记忆捞出来?

第二,记忆要能“长得对”。用户跟Agent的互动是持续性的,今天说的话和三个月前的某个偏好可能有关联。记忆系统需要把这种跨时间的关联提炼出来,而不是简单堆积原始记录。

第三,记忆要能“放得下”。无论是Token预算还是存储成本,资源永远是有限的。Agent不可能无限记住所有东西,它得知道什么是值得记的、什么是该忘的。

分层记忆的核心价值就在这里:它把这三个问题分别交给不同层去解决,每一层专注一件事,既不互相干扰,也不重复造轮子。

3.2 三层划分是最实用的起点

我目前的方案是三层划分:短期记忆层、长期记忆层、语义记忆层。这个划分参考了认知科学中工作记忆、情景记忆和语义记忆的概念,但落实到工程上做了简化。

短期记忆层对应的是当前会话上下文。它负责承载正在进行的对话、当前任务的状态、最近几轮的临时信息。这一层的特点是高频读写、即时生效、生命周期短。会话一结束,或者上下文长度超过阈值,该压缩的压缩,该淘汰的淘汰。

长期记忆层对应的是跨会话的事实性信息。比如用户的名字、偏好、历史行为、项目背景。这一层的特点是低频写入、高频检索、需要持久化存储。它是Agent“认识这个人”的基础。

语义记忆层对应的是经过整理和抽象的知识沉淀。它跟具体用户绑定不那么紧密,更多是领域知识、产品信息、业务规则、历史决策的复盘。这一层的特点是不常变化、价值密度高、需要定期维护。

实际项目里,短期记忆可以用一个简单的会话窗口加摘要来实现,长期记忆用键值库加向量库,语义记忆用向量库加结构化文档存储。三层各司其职,Agent在面对一个新问题时,它会同时从三层里检索相关信息,拼装成最终的上下文。

3.3 为什么短期、长期、语义不能混在一起存

不少人会问:反正都是存起来,一层向量库全搞定,干嘛要分那么细?这个问题我当初也纠结过,直到我实际对比过两种方案才明白差距在哪。

全量向量存储的问题在于召回质量的不可控。你把对话记录、用户画像、知识文档全部向量化存进同一个集合,检索的时候关键词一偏,召回结果就会被短期会话的琐碎内容淹没。比如用户问“上次那个项目的进展”,系统可能召回一堆“嗯嗯”“好的”“明天见”之类的寒暄内容。混杂在一起,既浪费Token,又拉低回答质量。

分层之后,每个层有独立的存储结构、独立的写入策略、独立的检索逻辑。短期层优先用最近交互的原始数据,长期层用结构化事实加向量索引,语义层用经过清洗的知识条目。检索时按比重分配:当前会话相关的高频信息优先,然后是用户的长期事实,最后才是领域知识。这样召回的上下文在相关性上有了天然的优先级排序。

注意:分层不是“分类存储”这么简单。真正的关键是每层要有独立的写入与淘汰机制。只做物理隔离,不做逻辑隔离,效果会大打折扣。

4. 短期记忆层的设计与实操细节

4.1 短期记忆的本质是“只保留当前任务的拼图”

短期记忆要实现的目标很明确:让Agent在单次会话里保持连贯性,不丢前文信息,不让上下文无限膨胀。它的设计逻辑跟人脑的工作记忆很像——容量有限,需要持续更新。

我常用的实现方式是“滑动窗口 + 动态摘要”。滑动窗口保证最近N轮对话始终完整可见,这个N要根据模型上下文长度和任务的复杂度来定。比如用GPT-4级别的模型,上下文窗口很大,我可以开一个50轮的滑动窗口;如果用轻量级模型,可能只能容纳15到20轮。窗口之外的旧内容,不是直接扔掉,而是触发一次摘要生成,把更早的对话浓缩成一段结构化摘要,塞到上下文的最前面。

具体的数据结构,我一般用一个数组加一个索引指针:数组保存最近的对话消息,指针记录当前窗口的起点。触发摘要时,把指针之前的所有消息喂给模型,生成一段不超过200字的摘要,替换掉原来的全量内容。

4.2 窗口大小和摘要频率怎么定

窗口大小和摘要频率,这两个参数直接决定了短期记忆层的成本和质量。

先说窗口大小。我的经验是:不要贪大。窗口越大,Token成本越高,而且模型对长上下文的注意力会分散,反而不利于聚焦当前问题。一般场景下,30轮左右是一个平衡点。如果任务本身是长流程的,比如代码生成或者多步骤操作,可以放宽到50轮,但一定要配合间隔摘要。

再说摘要频率。我的做法是在会话轮次达到窗口上限的80%时触发第一次摘要,之后每增加10轮再触发一次。这样既避免频繁摘要带来额外API调用,又能保证窗口内始终有足够空间容纳新内容。

摘要的内容结构也很重要。我维护的摘要包含三块:用户的核心目标、已经完成的事项、当前未解决的问题。这种结构化摘要比“纯文本概括”效果好得多,因为Agent需要的是“任务状态”而不是“对话回顾”。

4.3 短期记忆的存储选型

短期记忆对存储的要求是低延迟、支持持久化、结构灵活。我推荐用Redis加JSON文档的组合。Redis负责缓存最近几轮的对话,保证毫秒级读写;JSON文档负责保存完整的会话记录,用于后续摘要生成和长期记忆提取。

需要注意的一个坑是并发问题。同一个用户的多个请求可能同时读写短期记忆,如果不用事务或者原子操作,会出现消息顺序错乱。我一般会给每个会话加一个版本号,写入时做乐观锁校验,简单可靠。

5. 长期记忆层的设计与实操细节

5.1 长期记忆要回答的是“用户是谁”

如果说短期记忆保证Agent“接得住话”,长期记忆保证的就是Agent“认得出人”。这一层存储的是跨会话稳定存在的用户事实——姓名、职位、偏好、习惯、历史目标、重要节点。

我听不少人说过,长期记忆不就是把用户画像存起来嘛。但实际操作下来,真正的难点有两个:一是怎么从对话中自动抽取稳定的事实,而不是把随口一说的内容当成永久记忆;二是怎么在合适的时候把这些事实拿出来,而不是每次对话都全量加载。

针对第一个难点,我目前的做法是“规则抽取 + 模型标注 + 人工确认”三层过滤。规则抽取负责抓明显的实体信息,比如称呼、地点、公司名;模型标注负责提炼偏好和意图类的信息;人工确认负责处理高价值、低频次的重大信息,比如用户换工作了、项目换方向了。这个过程在每次会话结束后异步执行,不阻塞主流程。

5.2 长期记忆的存储结构与更新策略

长期记忆的存储我推荐用“结构化键值 + 向量索引”的组合。结构化键值用于精确查询,比如“这个用户的推荐阈值是什么”;向量索引用于语义查询,比如“这个用户最近关心什么方向”。

每条长期记忆我维护这么几个字段:记忆ID、用户ID、记忆类型、内容描述、置信度、创建时间、最后更新时间、来源会话ID。置信度这个字段尤其关键,它是判断“该不该信这条记忆”的核心依据。比如用户随口说了一句“我好像对咖啡因敏感”,置信度可能只有0.5;但如果他三次提到“我喝咖啡会失眠”,置信度就该升到0.85以上。

更新策略我遵循一个“衰减加强化”的机制:每次用户的新对话如果命中某条记忆,该记忆的置信度就提升;如果一条记忆长时间未被命中,置信度逐步衰减,跌到阈值以下就归档或删除。这样能避免把用户已经改变的习惯当作长期偏好来用。

5.3 长期记忆的检索时机

长期记忆的检索不是每次都全量查一遍,而是要讲究时机。我的经验是分两个阶段:会话开始时,先做一次“轻量预热检索”,把用户的基础画像拿出来;当对话中出现特定触发词,比如用户提到“上次”“我之前说过”“我的习惯是”,再触发一次“深度检索”,把相关的历史事实和决策记录捞出来。

这个设计很关键,因为全量加载用户的所有长期记忆到上下文里,Token消耗巨大,而且会稀释当前任务的注意力。轻量预热加按需深度检索,是在质量和成本之间平衡下来比较理想的方案。

6. 语义记忆层的设计与实操细节

6.1 语义记忆存的是“知识”,不是“记录”

语义记忆层和前两层的最大区别在于,它不绑定具体用户或具体会话,它绑定的是Agent要服务的那一整个领域的知识。比如你做的是客服Agent,语义记忆层就该存产品文档、常见问题解决方案、流程规则、历史工单的处理经验。

为什么需要单开这一层?因为知识类信息的生命周期和用户记忆完全不同。用户记忆是动态变化的,今天喜欢这个,明天可能就变了;但产品知识是相对稳定的,一条FAQ可能在半年内都不过时。如果混在一起存,知识检索会被用户的碎碎念干扰,用户画像也会被知识文档的返回结果冲淡。

6.2 语义记忆的构建流程

语义记忆层的构建,本质上是一个知识工程问题。我从零搭建时走了这样几步:

第一步,收集原始资料。包括产品文档、帮助中心文章、历史工单、FAQ、内部Wiki。这一步不设门槛,先全部收集起来。

第二步,清洗与去重。把格式不统一的文档转成统一的结构化格式,抽取标题、正文、关键词、适用场景等字段。去重这一步很关键,很多知识库里躺着五六份内容基本一样的文档,不清洗直接向量化,召回时会出现大量重复信息,白白浪费上下文空间。

第三步,切片与向量化。把长文档按语义边界切成合适的块,每块300到500字,然后向量化存入向量库。切片的大小很讲究:太大,检索出来的内容不够精准;太小,语义不完整。我一般用标题加段落边界来做切片,保证每块都有相对完整的语义。

第四步,建立索引增强。除了向量索引,我还会维护一个关键词索引和标签索引。这样在面对“模糊的语义查询”和“精确的关键词查询”时可以双路召回后融合排序,效果比纯向量检索稳定得多。

6.3 语义记忆层的更新与维护

语义记忆不是一次建好就完事了,它是需要持续维护的。我一般设定一个每周一次的维护流程:检查新增的文档是否已经入库,检查现有条目是否有过期内容,检查历史召回记录里有没有“召回了但用户反馈没用”的低质量条目。

这里我要特别说一个容易忽略的细节:低质量条目的删除。很多人只关注怎么把知识加进去,却很少关注怎么把没用的知识踢出去。实际做的时候会发现,随着资料越来越多,向量库的检索质量会明显下降——因为噪音条目会干扰相似度计算。我现在的做法是:每次检索结果返回后,记录哪些条目被实际采纳进了最终上下文,哪些条目只被召回但没被模型使用。持续统计一段时间后,把那些“高召回低使用”的条目标记删除或降权。

7. 实操:从零搭建一套分层记忆模块

7.1 整体架构与数据流

用一个完整的项目来说明吧。最近我在做一个面向电商客服的Agent系统,它需要知道每个用户的购物偏好、历史订单、退款记录,同时还要掌握商品知识库和售后政策。我按前面说的三层模型,搭了下面这套结构:

短期记忆层:Redis + 会话窗口 + 动态摘要 长期记忆层:PostgreSQL(结构化画像) + Milvus(向量索引) 语义记忆层:文档存储 + 向量库 + 关键词索引

数据流是这样的:用户发消息进来,系统先把短期会话上下文取出来,加上长期记忆层检索出的用户画像,再加上语义记忆层检索出的相关知识,拼装成一个完整的Prompt,交给大模型。大模型返回回答后,系统异步更新短期记忆、抽取新的长期记忆事实、记录知识召回情况。

7.2 数据结构定义

先看长期记忆层的数据结构。我用SQL定义了一张用户画像表:

CREATE TABLE user_profile ( user_id VARCHAR(64) PRIMARY KEY, profile JSONB, -- 画像主体,存结构化事实 memory_tags TEXT[], -- 标签,用于关键词索引 confidence FLOAT DEFAULT 0.5, -- 整体置信度 created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() );

语义记忆层的向量召回,我用的索引结构是这样:

{ "chunk_id": "kb_10086", "title": "七天无理由退货规则", "content": "消费者自签收商品之日起七日内...", "tags": ["退货", "售后", "政策"], "source": "helpcenter/after_sale_v2.md", "embedding": [0.01, 0.23, ...], "last_reviewed": "2025-01-10" }

短期记忆层的会话窗口,我用Redis里的一个列表结构保存:

# Key: session:{user_id}:{session_id} # Value: JSON数组,每项是一条消息 LPUSH session:u10086:s20250201 '{"role":"user","content":"这款手机有货吗?","ts":1700000000}' LTRIM session:u10086:s20250201 0 49

这里的LTRIM操作很重要,它把列表长度限制在最近50条,超出部分自动淘汰。

7.3 记忆写入与检索的伪代码

下面是我模块核心逻辑的简化伪代码,完整实现了三层记忆的写入和检索:

def remember(conversation): # 短期记忆层:直接追加到最近会话 session_id = conversation["session_id"] for message in conversation["messages"]: redis.lpush(f"session:{session_id}", json.dumps(message)) redis.ltrim(f"session:{session_id}", 0, MAX_WINDOW_SIZE-1) # 触发窗口摘要 if conversation_len(session_id) >= TRIGGER_THRESHOLD: summary = summarize_old_messages(session_id) redis.set(f"summary:{session_id}", summary) # 长期记忆层:异步抽取用户事实 facts = extract_facts(conversation["messages"]) for fact in facts: upsert_user_profile(conversation["user_id"], fact) # 语义记忆层:记录知识召回情况 log_retrieval_stats(conversation["retrieved_chunks"]) def recall(user_id, query, session_id): # 短期记忆:读取窗口摘要+最近消息 short_memory = redis.get(f"summary:{session_id}") recent_messages = redis.lrange(f"session:{session_id}", 0, -1) # 长期记忆:轻量预热+按需深度检索 user_profile = get_user_profile(user_id) if needs_deep_recall(query): deep_facts = vector_search("user_memory", query, top_k=5) # 语义记忆:混合检索 semantic_results = hybrid_search("knowledge_base", query, top_k=8) # 拼装上下文(按优先级排序) context = { "summary": short_memory, "recent": recent_messages, "profile": user_profile, "knowledge": semantic_results } return context

这段代码的核心逻辑就两个:写入时三层各走各的更新路径,检索时三层联合拼装。不要把检索写成一个函数全查一遍,那样成本高、优先级也乱。

7.4 参数设置经验值

给出我在实际测试中比较稳定的一组参数,供参考:

参数推荐值说明
短期窗口消息数30~50条超出50条必须触发摘要
摘要触发阈值窗口上限的80%留出余量,避免窗口打满才操作
概括摘要字数150~250字太长占Token,太短信息丢失
长期记忆置信度阈值0.75低于该值的记忆不进Prompt
向量召回Top K短期5条,长期5条,语义8条总量控制在20条以内
知识切片长度300~500字以语义完整为优先

这个参数表不是万能的,但它是一个经过项目验证的合理起点。不同的模型、不同的业务场景需要适当微调,后面我会说怎么调。

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

8.1 检索结果“答非所问”

这是我遇到的最频繁的问题。排查的时候,我一般按这个顺序来:

先看是短期层、长期层还是语义层的召回出问题。我会在Agent的调试日志里把各层召回的条目数量、相似度分数、实际取得的上下文内容都打印出来。如果是短期层出问题,多半是摘要没有及时更新,旧摘要和新对话产生了冲突。如果是长期层出问题,多半是置信度过滤太松,低质量的画像信息混进了Prompt。如果是语义层出问题,多半是向量检索的Top K取太小,或者切片粒度不合理。

提示:强烈建议给召回过程加详细日志。不要只看最终回答好不好,要看是哪一层、哪条记忆导致回答偏离的。没有日志,排查效率会非常低。

8.2 记忆写入造成数据污染

有一段时间,长期记忆层老是混入垃圾信息。比如用户在对话里说了一句“我今天心情不好”,系统居然把“用户容易情绪化”抽成了画像特征。这就是抽取规则不够严格导致的。

我的解决办法是给事实抽取加“重复验证”门槛:一条事实至少要出现两次,且间隔要超过一定时间,才有资格进入长期记忆。临时性的情绪表达、随口一提的偏好,统统拦截在门外。这个方法牺牲了一点信息完整度,但换来的是长期记忆层的整体质量大幅提升。

8.3 向量召回效果越来越差

向量库刚建立的时候召回效果还可以,用了两个月之后明显变差。这种问题常常是知识库持续写入造成的。新条目和旧条目的向量分布逐渐偏离,语义空间被撑开,相似度计算的区分度下降。

我的解法是定期做向量索引的“紧凑化处理”:把相似度极高的重复条目合并,把长期没有被召回过的低频条目降权,把已经过期或者被人工标记为废弃的条目物理删除。做一次这种维护,召回质量的提升是非常明显的。

8.4 上下文拼装后Token超限

分层记忆检索出来的内容加起来很容易超过模型上下文限制。我的应对策略是“分层配额”:短期层配额40%,长期层配额20%,语义层配额40%,各层在配额内按相似度分数截断。这样既保证了当前会话的主导地位,又不会让长期层的画像信息过多挤占知识条目的位置。

如果某个场景下知识检索的必要性非常高,比如用户明确在问产品政策,我会动态调整配额,把语义层的比例调到60%,短期层降到30%。

8.5 会话结束后短期记忆还在“误导”新会话

短期记忆的生命周期必须严格控制。每个会话一旦结束,或者用户主动开启新会话,之前的短期记忆就要归档清理。很多Agent翻车就是因为上一轮聊的旧内容还在窗口里,新会话的用户问了一个完全不同的问题,结果模型被旧上下文“带偏”了。

我的做法是:Redis里给每个会话设置过期时间,默认30分钟无新消息自动过期;过期后窗口内容异步写入历史归档,供长期记忆抽取使用,但不再参与新会话的Prompt拼装。

9. 一点收尾的实操心得

最后分享几条我在做了好几个项目之后沉淀下来的经验。

第一,不要一上来就上多复杂的架构。如果你的Agent还在原型验证阶段,一个短期窗口加一个向量库就够跑了。分层记忆的价值要在对话轮次足够多、知识量足够大之后才会体现出来。过早分层,只会给自己增加维护成本。

第二,记忆质量比记忆数量重要得多。我见过不少团队,想方设法把所有的聊天记录、所有文档、所有知识都存进去,结果检索出来的全是噪音,回答质量不升反降。我的原则是:能进长期记忆和语义记忆的内容,一定是经过清洗、去重、验证过的。宁可记住十条高质量的,也不记一千条没用的。

第三,分层记忆这个架构真正的难点不在存储,而在写入逻辑和淘汰逻辑。你能不能准确判断“这条信息值得记住多久”,决定了整个系统最终是聪明的还是笨的。所以我强烈建议你在Agent上线之后,花至少两周时间持续观察记忆层的行为,看看哪些记忆被频繁命中、哪些记忆从来没人触发。根据这些数据来调整写入规则和置信度阈值,比任何一次性设计都管用。

我个人在实际操作中还有一个很有意思的发现:分层记忆的收益,在非大模型的大型语言模型上反而更明显。轻量模型上下文短、推理能力弱,更需要靠高质量的上下文来补足。也就是说,如果你的产品受成本限制只能用中小模型,分层记忆几乎是必选项,它能帮你用更少的Token换来更精准的回答。这一点,可能是这篇文章里我最想让你带走的一个建议。