☰
AI Agent记忆管理:从上下文窗口到向量检索的工程实践
2026/10/12 8:09:22 网站建设 项目流程

很多做AI Agent的人,一开始可能都有这种感觉:单轮对话的时候,它像个专家,逻辑清晰、条理分明。可一旦进入多轮对话或者复杂任务,它就开始“犯糊涂”,前面刚确认过的信息,转头就忘;你说过的偏好,它完全不记得。这时候的问题,大概率不是模型不够聪明,而是Agent没有记忆管理。

这个主题我放在了AI Agent系列教程的第四篇,原因很简单:前三篇解决了Agent的骨架和工具调用能力,相当于让它有了手和脚;而记忆管理,是让Agent真正拥有“大脑持续性”的核心模块。没有记忆的Agent,本质上就是一条只有七秒记忆的金鱼,每次对话都是一次彻底的重来。

这篇教程里,我会把这个相对抽象的概念拆开揉碎,从记忆的底层分类,到工程级别的实现方案,再到我实操中踩过的坑,尽量一次讲清楚。适合正在搭建Agent应用、或者准备优化多轮对话体验的开发者参考,无论你用的是现成的Agent框架,还是自己从零卷一套逻辑,这篇文章里的思路都能直接套用。

1. 没有记忆的Agent,等于每次都和陌生人聊天

1.1 从一次失败的客服Demo说起

我最早尝试做一个带记忆的Agent,起因特别朴素:一个做电商客服的场景。

当时我搭了个Agent,模型用的某主流大模型,工具调用也接好了,能查订单、能查物流,看起来像模像样。结果测试的时候翻车了——用户在对话里说出了自己的订单编号,Agent查询成功了;但下一轮用户直接说“那这个订单什么时候发货”,Agent立刻懵了。它不知道“这个订单”指的是哪个订单,因为上一轮的上下文在它眼里可能就是一堆毫无关联的历史消息。

那是我第一次意识到:上下文窗口和记忆是两回事。上下文窗口只是给模型看的当前文字段落,但模型并不会从中提取出“用户的订单编号是A12345”这种结构化的事实留给后续使用。这个问题迅速从“怎么改Prompt”演变成了“怎么设计一套记忆系统”。

那次之后,我重新梳理了整个Agent的架构,发现记忆管理不是某一段代码能解决的,它是一个需要贯穿始终的横切模块。你在哪个环节设计它的存取逻辑、用什么格式存、什么时候该检索、什么时候该遗忘,全都会直接影响Agent的表现。

1.2 记忆管理到底管的是什么

通俗地说,Agent的记忆管理要解决三个问题:记什么、存多久、怎么用。

记什么,指的是从用户对话里提取出哪些信息是有长期价值的。比如用户的身份信息、偏好设置、项目目标、历史决策,这些值得沉淀;而“今天天气怎么样”这种一次性寒暄,过去了就该翻篇。

存多久,是区分短期记忆和长期记忆的核心。短期记忆服务于当前对话,上下文窗口满了就该主动做摘要压缩;长期记忆则需要跨对话持久化,在下次会话开始时重新拉取,让Agent知道“我记得你是谁、你上次聊到哪了”。

怎么用,则是检索和注入的工程问题。你存的记忆如果不经过筛选全部塞进上下文,很快就把Token预算撑爆了;只有按相关性、按时间权重把最合适的那部分记忆注入当前会话,才是有效利用。

把这几个问题想明白之后,你才会意识到,记忆管理本质上是Agent系统设计里的“数据库层”加“缓存层”加“预处理层”的组合体,而不是一个简单的数组变量。

2. 记忆的四种形态:像人脑一样分层存储

2.1 工作记忆、情景记忆、语义记忆、程序记忆

我习惯把人脑的记忆机制映射到Agent设计上,这样跟团队里其他人聊起来,沟通成本低很多。人脑可粗略分为四种记忆:工作记忆、情景记忆、语义记忆、程序记忆。

工作记忆对应Agent的上下文窗口和对话历史,容量有限,用完即走;情景记忆对应“某次会话中发生了什么”,比如上次和用户敲定了哪些需求细节;语义记忆对应“用户是谁、世界是什么样的”,比如用户偏好简洁回复、用户所在行业、常识性规则等;程序记忆则对应Agent能力的“肌肉记忆”,比如它学会了如何调用某个工具、某类任务的标准处理流程。

在工程实现上,工作记忆基本靠上下文管理解决,情景记忆和语义记忆靠向量数据库和键值存储配合,而程序记忆通常需要固化到系统提示词或者独立的规则引擎里。

2.2 长期记忆该怎么落库

拿我现在做的一个项目举例:某知识助手Agent,用户会跟它持续讨论多个主题,跨天甚至跨周。

当时我设计了双轨存储:一条轨是结构化键值存储(用的是Redis),专门存用户信息、用户偏好、会话摘要之类的高确定性数据,key按用户ID和业务键拼接,读取速度快;另一条轨是语义向量存储(用的某开源向量数据库),存的是非结构化的对话片段、知识点、决策记录,靠Embedding做相似度召回。

这么拆的原因是,键值存储查起来准,但没法做模糊匹配;向量存储能找“意思相近”的记忆,但偶尔会召回不相关的垃圾。两条轨各管一段,互补性很强。比如用户偏好这种明确的信息,用Redis存,每次对话开头精确取出来;而用户上次提过的一个模糊需求描述,用向量检索去捞,又准又快。

如果你只是在做轻量级原型,前期完全可以用简单的JSON文件加内存做一个简化版记忆系统,但是一旦跑起来你就会发现,查询路径混乱、数据没清理、扩展性为零,所以我还是建议尽早把正式的存储层立起来。

2.3 短期记忆的滚动窗口策略

短期记忆部分,最直接粗暴的做法是只保留最近N轮对话,超过部分直接丢弃。试过几天就会发现两个问题:一是关键信息刚好被挤出窗口,二是模型对前面内容的“遗忘”是硬切断的,对话连贯性很差。

后来我换了滚动摘要策略:维护一个对话摘要变量,每当窗口快满时,调用模型把当前对话压缩成一段结构化摘要,再把摘要与最近几轮完整对话一起送入上下文。这个方案实测下来效果不错,基本相当于一个“自动任务交接笔记”,让旧信息以浓缩的方式持续存活。

这个策略在工程实现上不算复杂,但有一个细节需要处理好:压缩频率不能太高,否则调用模型的成本会增加不少,而且连续压缩会导致信息层层失真。我的经验是,当对话轮数超过窗口容量的一半时触发一次压缩,压缩后保留摘要加最近五轮完整对话作为新的上下文基础,效果比较均衡。

3. 向量化记忆库的实操:从Embedding到召回

3.1 选Embedding模型的几个思路

向量记忆库的核心,是把文本变成向量存起来,再做近似检索。选Embedding模型时,不同场景的侧重完全不同。

如果你做的是中文场景,尽量选对中文支持好的模型,别拿通用英文向量模型硬跑中文文本;如果是多语言混合内容,可以考虑多语言向量模型,避免语言割裂造成的召回不准确。这里我的建议是:在正式选型前,用你自己业务里的真实语料,跑一个简单的召回测试,看top10结果跟预期的匹配程度,这比看公开榜单分数有用得多。

另一个容易忽略的点是文本切片长度。Embedding模型通常对输入长度有上限(比如512个token),如果你的记忆片段超过这个长度,直接编码会丢失信息,或者被模型截断。我一般把单条记忆控制在200-300字左右,超出就做切分,按语义边界拆成多条。

3.2 元数据过滤:记忆检索的精髓

很多人做向量检索只传“查询文本”进去,然后topK召回一堆,看着好像还行,但实际效果极不稳定。我给记忆检索加了几个维度的元数据过滤,效果立刻稳多了。

第一个维度是时间。时效性强的记忆(比如“用户上周提到项目X”)需要限定时间范围;而长期偏好不需要时间限制。所以在存储时,我给每条记忆打了时间戳,检索时根据业务需求选择是否加时间过滤。

第二个维度是会话来源。跨会话的记忆检索容易造成上下文污染,我习惯把记忆的会话ID一起存进去,查询时可以指定“只看当前会话”或者“全局检索”。

第三个维度是记忆类型。我会给每条记忆打上类型标签(用户偏好、项目事实、对话摘要等),检索时按需过滤。比如开场只注入用户偏好类记忆,而任务执行过程中再检索项目事实类记忆,避免一上来就把Agent“带偏”。

这三个过滤条件组合起来,基本能挡住大部分不相关召回。你可以理解为,单纯靠向量相似度是“大海捞针靠手感”,加上元数据过滤,相当于直接换了个池子。

3.3 记忆写入与读取的完整链路

实操层面,我拆成两段:写入链路和读取链路。

写入链路发生在每轮对话结束后。Agent先把当轮抽取的“关键记忆点”格式化成一个结构化对象,包含内容、类型、时间戳、来源会话ID;然后同步写入两个地方——高确定性信息写Redis,需要语义检索的信息生成Embedding后写向量库。

这一段如果想做得更精细,还可以做“记忆合并”。比如用户重复表达了同一个偏好,如果每次都存一条新纪录,库里很快会出现大量冗余向量,召回时翻来覆去就是那几句。我自己的方案是:写入之前先做一次相似度检索,如果发现已存在高度相似的旧记忆,就触发更新合并逻辑,而不是新增一条。

读取链路发生在每轮对话开始前。系统先并行执行几个查询:从Redis取用户偏好与历史摘要;用当前轮次的用户问题做向量检索,结合元数据过滤召回TopK相关记忆;把召回结果按照“时间新近度加相关性”做一个重排,然后注入系统提示词。

这一步的重排很关键。直接按相似度分数排序注入,可能会出现三条几乎同样的旧记忆占据名额的情况。我调试时加了简单的加权公式:最终分 = 相似度分 * 0.7 + 时间衰减系数 * 0.3,跑了几轮测试,召回内容的多样性明显提升了。

下面给一段我用了很久的伪代码框架,能帮你把上面的逻辑串起来:

def fetch_relevant_memories(user_id, query, session_id, top_k=5): # 1. 读结构化记忆 profile = redis_client.get(f"agent:profile:{user_id}") # 2. 向量检索 + 元数据过滤 filters = { "user_id": user_id, "memory_type": ["preference", "project_fact"], # 可选: "session_id": session_id 限定当前会话 } candidates = vector_db.search( embedding=embed(query), filters=filters, top_k=top_k * 2 ) # 3. 重排:结合时间衰减 now = time.time() for mem in candidates: age_hours = (now - mem.timestamp) / 3600 mem.final_score = mem.score * 0.7 + (1 / (1 + age_hours)) * 0.3 candidates.sort(key=lambda m: m.final_score, reverse=True) return { "profile": profile, "memories": candidates[:top_k] }

这段代码的核心思想就是“先精确捞,再模糊捞,最后混排”,它兼顾了短标签型信息的确定性和长文本型信息的语义灵活性。

4. 上下文窗口的压榨艺术:让每一分Token都花在刀刃上

4.1 动态分配Token预算

模型的上下文窗口是有限的,哪怕是最新一代模型,把几十万字全塞进去也不现实,成本和延迟都会失控。我建议的做法是给Agent建立一套Token预算分配体系。

假设你用的是8K上下文窗口,预留1K给系统提示词和工具定义,再预留1K给用户当前的输入,剩下6K分配给记忆注入。在6K里,我习惯按比例切分:40%给最近对话轮次(短期记忆)、30%给向量检索出的历史相关记忆(长期记忆)、20%给结构化用户画像,剩余10%作为缓冲。每次写入前,先把内容Token化,量一量实际长度,超了就触发压缩或裁剪逻辑。

这套分配方案帮我减少了很多“上下文溢出”的尴尬报错。而且你会发现,给不同内容分配固定配额后,模型的回复质量反而更稳定了——因为关键信息不会被海量的历史对话稀释掉。

4.2 记忆更新与遗忘机制:定期做“断舍离”

记忆库如果只进不出,跑一个月之后,里面堆积的记忆碎片能把检索质量拖垮。这里我推荐三个机制配合使用。

第一,过期清理。给每条记忆设置TTL(生存时间),比如临时性记忆保存24小时,项目事实类保存30天。到期后由定时任务统一删除或归档。第二,重要性分级。每次写入记忆时,附上重要性评分(可以从对话中提取情绪强度、用户重复提及次数等信息辅助判断)。高重要性记忆延长TTL,低重要性记忆优先清理。第三,重复合并。前面提过的写入前相似度检查,既控制了冗余,也变相实现了一个“记忆强化”效果——被频繁匹配到的记忆可以人工调高持久化权重。

这三个机制配合起来,记忆库才不会是“垃圾堆”,而更像是真正经过筛选的笔记系统。

4.3 跨会话记忆恢复:开场白的设计

技术上有了长期记忆,接下来要考虑的是“什么时候把记忆用起来”。我的经验是,跨会话记忆恢复最适合放在对话开场,也就是模型第一次生成回复之前。

具体做法是:在每轮对话的预处理阶段,把“用户画像摘要”和“最近一次会话的核心结论”作为固定的前导注入背景信息。比如Agent看到用户上线,系统已经在提示词里注入了“这个用户是某电商公司的运营,上次对话定下了A/B测试方案,优先级较高”这类信息,它就能在开场主动提及上次的讨论,而不是当作新用户一样从头问起。

这个设计对用户感知的提升非常明显。我做了个小范围用户测试,有记忆恢复的版本比无记忆版本的用户满意度高了很多,核心原因就是“被记住的感觉”带来了一种明显的基础信任感。当然,注意不要把记忆恢复做成“查户口”,注入时只挑关键信息,别把用户所有旧历史都轰炸出来。

5. 常见问题与排查技巧:那些年我踩过的坑

5.1 检索结果不相关

向量检索的结果越来越离谱,这个问题我排查了挺久。最后发现大概率不是向量库的问题,而是文本切片方式不对。比如你把一段包含多个主题的对话存成一条长记忆,查询的时候只命中其中一部分,整条召回回来就带着大量无关内容。

解决办法是优化切片逻辑,尽量按照主题语义边界切,而不是简单按字符数硬切。我后来还加了“主题标签”字段,切出来的每片内容在入库前都被打上一个主题标签,查询时如果知道当前对话的主题,就优先过滤这个标签。

5.2 记忆互相“污染”

记忆污染指的是:用户本次聊的是项目A的方案,结果Agent翻出了用户上次讨论项目B的记忆,并且混在回复里,造成了牛头不对马嘴的效果。

这类问题主要靠元数据过滤来解。我在检索时必须加“当前主题”或“当前项目ID”的条件,必要时直接限定只在当前会话的上下文中检索。如果你把记忆体系做到足够精细,甚至可以给每条记忆建立一个“所属项目”的归属链,每次切换项目时,只加载对应项目的记忆子集。

5.3 上下文爆炸:记忆层叠导致Token超限

当记忆体系运转顺畅之后,新的麻烦来了:每次对话开头都要注入那么多记忆,量变叠加后token直接爆了。这个问题我会用两段式压缩来解决。

第一次压缩发生在把记忆提取出来之后,我会先把召回的记忆片段丢给一个小型模型做“合并摘要”,把重复的、低价值的部分合并成几句话;第二次压缩发生在生成用户回复之前,把最终要注入上下文的所有内容做一个预检,超过预算就继续裁剪。实测下来,两段式压缩能把单轮调用成本压下来约三成,同时对回复质量的负面影响非常小。

你可以把这个过程理解成做PPT:不是把全部资料贴在每一页上,而是每页只提炼出最核心的结论和依据。Agent的上下文也一样,信息量越大,越需要提炼。

5.4 隐私和合规:记忆里存了什么得心里有数

做记忆管理还必须留一条底线:用户数据的隐私和合规。有些信息不应该进记忆库,或者至少不应该完整地存下来——比如密码、卡号、身份证号这类敏感字段。我通常在写入链路里接一个简单的脱敏过滤器,用正则匹配和实体识别把敏感信息替换成占位符,再从服务端下发时落库。

另外要设计“忘记”能力:用户要求清除记忆时,系统能按用户ID把键值库和向量库里对应的记忆全部删除。这件事在商品化部署时几乎一定会被问到,留着不做,后面验收会很麻烦。

常见问题排查优先级推荐解法
召回内容不相关高检查切片方式和Embedding模型选型
跨主题记忆污染高检查元数据过滤与主题隔离
上下文Token溢出中引入两段式压缩与预算分配
记忆重复冗余中写入前相似度检查与合并
敏感信息入库存留高写入链路脱敏过滤与“忘记”机制

6. 在项目里落地记忆管理,我的最后三条建议

第一,不要一上来就追求记忆系统的完美形态。先把最基础的双轨存储搭起来,跑通“写入→检索→注入”这个闭环,比纠结用什么向量数据库更重要。我见过不少人在选型上花了两三周,最后跑起来的Demo还是用JSON文件临时顶着的。

第二,记忆管理和业务场景强相关,通用方案只能作为起点。电商客服、知识助手、个人助理,它们的记忆维度差异非常大。电商客服要记住订单和物流,知识助手要记住用户的知识短板,个人助理要记住日程和偏好,你必须根据具体业务定制记忆类型和检索策略。

第三,时刻关注“记忆的负面效应”——也就是记忆注入过多导致的上下文污染和回复僵硬。记忆的目的是辅助Agent,而不是主导Agent。当你发现Agent回复越来越像“复读机”,大概率是记忆权重设置得太高了,适当减少注入量,把更多生成空间还给模型本身。

我在实际项目中见过太多团队,费劲把记忆库做出来了,结果线上效果反而比没记忆的时候差。最后排查下来,几乎都有一个共同特征:过度依赖检索出来的历史记忆,而忽略了模型的实时推理能力。记忆管理要做的,是给模型提供精准的线索,而不是把模型变成一个“只会翻旧账的记录员”。

这个系列进行到这里,Agent算是有了比较完整的“大脑回路”:感知输入、规划动作、调用工具、保留记忆。下一篇我打算聊一聊Agent的自我反思与纠正机制,那也是让Agent从“能干活”到“能越干越好”的关键一步。如果这一篇里某个环节你在实操中遇到了问题,欢迎交流,我踩过的坑大概率能帮你省点时间。

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

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

立即咨询