ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AI Agent记忆系统设计与落地:从上下文窗口到长期记忆的完整实现

AI Agent记忆系统设计与落地:从上下文窗口到长期记忆的完整实现

开头前,我先说个真实感受:这两年做Agent应用,最让人崩溃的其实不是模型不够聪明,而是AI Agent记忆系统跟不上。单轮问答个个惊艳,一进入多轮场景,第十一轮对话就开始“失忆”——上午刚聊过的偏好,下午要重新确认一遍;上一步刚说清的需求,下一步又重复追问。这种情况持续久了,用户只会得出一个结论:这Agent不行。但真相是,LLM本身没有记忆机制,上下文窗口再大也有边界,token成本也在那摆着。想让Agent真正像一个“有连续工作经验的人”而不是“每次都失忆的新同事”,就必须自己动手搭一套记忆系统。

这篇文章我会从为什么需要记忆、记忆模型怎么设计、最小可落地实现怎么写,到最后排坑实录,完整走一遍。面向正在做Agent落地、卡在长期上下文和多轮交互上的开发者,也适合刚接触Agent架构、想系统理解记忆模块的新手。内容不绕弯子,代码和思路都可以直接拿去改。

1. 先想清楚:Agent的“记忆”到底在解决什么

1.1 “记不住”比“不够聪明”更致命

很多人一开始会以为Agent体验不好是模型推理能力不足,实际排查下来,大概率是记忆链路的问题。大模型是一个无状态函数,它的“知识”是训练时固化下来的,对话本身并不会改变它的内部状态。你上一次聊了什么,它根本不记得;你切换一个会话,它又是一张白纸。为了缓解这个问题,常规做法是把历史消息全部塞进上下文窗口。但这种方式有两个硬伤。

第一个硬伤是上下文窗口有限。现在主流模型的上下文长度看似在涨,几万甚至几十万token的都有,但窗口越大,花在“翻旧账”上的注意力就越多,模型在关键任务上的表现反而可能下降,成本也跟着水涨船高。第二个硬伤是历史消息的质量参差不齐。对话里的寒暄、重复、来回纠正,这些信息对当前任务没有增量价值,塞进去只会稀释关键信息。我见过不少项目,Agent越聊越“笨”,就是因为prompt被历史消息堆得臃肿,真正有用的实体、偏好、约束被淹没了。

所以记忆系统的核心价值不是“存下所有东西”,而是用更聪明的方式留存关键信息、在合适的时候把它们拿出来。这跟人脑很像:我们不会记住每一顿饭吃了什么,但会记住对方不吃香菜、上次聊过的项目进度、某个人对价格的敏感度。AI Agent也需要这样一张“筛选后的记事本”。

1.2 人的记忆模型给了我们什么启发

在设计Agent记忆之前,建议先花半小时看看认知心理学里的记忆模型。经典的分法是把记忆分成工作记忆、短期记忆和长期记忆。工作记忆对应模型正在处理的上下文窗口,容量极小,只能放当前任务立刻要用的信息;短期记忆对应这次会话里的近期状态,比如用户当前在填的表单、这一轮刚确认的约束;长期记忆则对应跨会话沉淀的知识,比如用户身份、历史偏好、过往项目结论。

进一步还可以把长期记忆拆成语义记忆与情景记忆。语义记忆是“用户比较关注隐私保护、预算上限大概是两万”这种抽象出来的规则;情景记忆是“上周二用户反馈过某个功能卡死”这种带时间和场景的事件记录。两者存储方式和检索策略都不一样。

这对Agent记忆设计的启发很直接:不要只做一个巨大的向量库存所有东西,而是要区分层级、区分类型。比如工作记忆用token预算管住,短期记忆用数据库表存最近状态,长期记忆用向量化加结构化字段配合,让Agent在需要时能精确调取。很多团队一上来就搞“全局向量库”,结果检索质量不高,原因往往是没做分层设计。

1.3 记忆系统的四个核心能力

拆开看,一个可用的Agent记忆系统要具备四个能力:编码、存储、检索、更新与遗忘。

编码是把非结构化的对话内容转化成结构化记忆。例如从“用户说预算不要超过两万块”中抽取出条目:“用户预算上限=20000元”。这一步决定后面存储和检索的上限,编码质量差,后面全白搭。存储是把编码后的记忆按类型落库,既要能精确查结构化信息,也要能语义检索非结构化内容。检索是给定当前问题,从记忆库里捞最相关的内容注入Prompt,这个环节决定“想起什么”。更新与遗忘通常被忽略,但它恰恰是记忆系统能否长期不腐化的关键——旧记忆如何衰减、矛盾记忆如何覆盖、过时记忆如何归档。

这四个能力不是串行跑一次就完事,而是每次对话都可能触发一轮闭环。我在后面第三部分落地时会按这套流程拆成具体代码模块,先把整体逻辑立住。

2. 架构设计:三层记忆体系与数据流闭环

2.1 工作记忆:让当前上下文“够用又不占坑”

工作记忆是每一轮请求里模型实际能看到的token内容,包括系统指令、当前任务相关对话、检索到的记忆片段。它的核心问题是预算管理。我一般建议把上下文预算分成三块:系统指令固定占一块、当前对话上下文占一块、检索记忆占一块,三块之和留10%-20%的余量给模型输出。

预算确定后,工作记忆的难点在于“当前对话上下文”怎么选取。不是全部历史都放进去,而是用滑动窗口加摘要结合的方式。最近的几轮完整保留,更早的内容先过一遍摘要压缩。举个简单例子:如果当前是一个客服Agent,用户第1轮提供了订单号和问题描述,第10轮问“那我之前说的地址还能改吗”,这时候与其把10轮消息全文塞进去,不如在第11轮开始时生成一条摘要“用户订单已找到,曾提问修改收货地址”,再配合一条记忆“用户原收货地址为X市Y区Z街道”,这样模型既明白来龙去脉,又不被无关细节干扰。

我见过不少方案上来就做“无限长对话总结”,但这有个副作用:摘要本身也会越来越长,而且摘要可能丢失关键的实体信息,比如具体数值、ID、日期。我的习惯是给摘要设定主题标签,按主题分别更新,而不是生成一条贯穿全部历史的超长流水账。每个主题下保留摘要、最近N轮原文、关联实体三件套,这样精度和效率都保得住。

2.2 长期记忆:向量化存储与知识沉淀

长期记忆要负责跨会话保留有价值的信息。它不能只靠关键词匹配,因为用户表达同一意思可能有无数种说法,所以核心存储一定是向量化加结构化字段的组合。

向量化的本质是把文本映射到一个高维空间,语义相近的文本在空间里距离也近。实际使用时,需要选一个文本编码模型做Embedding。要提醒的是,编码模型的选型要跟对话模型解耦,不用追求参数最大,稳定性、响应速度、对长文本的截断策略更重要。存储上,向量检索负责“语义找相似”,结构化字段负责过滤条件,比如实体ID、时间范围、记忆类型。这两者必须配合,否则会出现“语义上很像,但实际是另一个用户的记录”这种串记忆的事故。

长期记忆的内容建议分三类存储:实体记忆、情景记忆、规则记忆。实体记忆存用户、商品、项目这类核心对象的属性,比如“用户所在城市=上海”“偏好=预算敏感”。情景记忆存发生过的事件,如“9月10日用户反馈登录页加载超过10秒”,这类记忆要带时间戳,方便后续做时效过滤。规则记忆是从多条情景里提炼出来的结论,比如“用户对登录稳定性要求极高”,这类记忆是总结产物,需要定期由Agent自己归纳生成。

2.3 记忆决策:写什么、怎么写、什么时候忘

存储环节做不好,话痨式记忆会毁掉整个系统。每一轮对话都存,记忆库几天就爆炸,检索也全是噪音。所以必须有一个“记忆写入决策层”。

我常用的打分机制是综合三个维度:业务价值、信息新鲜度、与当前任务的相关性。业务价值看这个信息是否影响后续任务的决策,比如用户预算、权限、关键约束,价值就高;闲聊“今天天气不错”价值就低。信息新鲜度看该信息是否已经被现有记忆覆盖,如果库里已有“用户偏好A”,这轮又出现类似表述,那就应该走更新而非新增。相关性看这轮信息跟当前主线任务的关系,离题的先不写,扔进待定区,等后续重复出现再提升权重。

遗忘策略同样重要。人的记忆有遗忘曲线,Agent记忆也需要衰减机制。我的做法不是物理删除,而是给每条记忆加一个“强度值”。每次被检索命中或再次被提及,强度值加一;长时间未被命中的记忆,强度值逐步衰减,落到阈值以下就进入冷存储归档,不再参与常规检索。这样做的好处是,旧记忆不会彻底消失,可以在专门的历史查询场景被翻出,但不会干扰日常主流程。

3. 手把手落地:构建一个最小可用的记忆子系统

3.1 核心存储结构设计

不动辄上重型中间件,一个最小版本可以直接用本地文件型嵌入式数据库,或者直接用JSON文件也能跑通逻辑。我先给一套字段设计,这套设计后面接任何存储引擎都能平滑迁移。

核心存储按类型拆成三张表:记忆主表、实体表、检索日志表。记忆主表保存一条记忆的全局唯一ID、记忆类型(entity/event/rule)、内容文本、向量ID、重要性得分、强度值、创建时间、最后访问时间、过期时间、关联实体列表。实体表保存实体ID、实体名称、实体类型、属性JSON。检索日志表保存每次命中的历史记录,用于统计哪些记忆在真实场景中发挥作用,反哺打分和衰减策略。

存储结构设计时要特别注意:向量ID要跟记忆主表ID强关联,不能只存向量不存原文。否则即使向量检索命中了,你也不知道命中的内容具体是什么,还要反查一次原文,多一跳就多一重风险。标量字段要建好索引,尤其是实体ID和时间戳,检索时先过滤再算向量距离比全局算距离快一个数量级。

3.2 记忆写入流程:从对话原始流到结构化记忆

写入流程是整个系统里最“吃设计”的部分。我把它拆成四步:清洗、抽取、打分、落库。

清洗阶段处理原始对话,去掉无意义的口头语、重复片段,较长对话先做分段。抽取阶段把对话中出现的实体、属性、事件提取出来。这一步可以调用大模型做,用标准JSON结构输出,例如:

analysis = model_chat( system="你是信息抽取器,只输出JSON,不要多余内容", user="从对话中抽取实体和事实:'用户说:预算两万以内,这周五前要上线。'" ) # 期望输出(实际以模型返回为准) # {"entities": [{"name": "预算", "type": "constraint", "value": "20000"}], # "events": [{"action": "上线", "target_date": "本周五"}]}

打分阶段按前面说的三个维度给每条事实打一个0-10的综合分。参考规则:涉及资金、权限、隐私、明确截止时间的,业务价值加3到5分;与当前任务主线强相关的加2分;信息已被现有记忆覆盖的走更新路径而不是新增。

落库阶段把结构化数据写入主表,同时对内容文本做向量化并存入向量索引。这里有两条代码层面的建议:一是合并发起向量化请求,避免每一条记忆都单独调一次接口,批量处理能省掉大量网络耗时;二是为文本内容做规范化,比如把“两万块”“¥20000”“两万”统一成“20000”,否则向量化后的语义也会有偏差。

3.3 记忆检索与注入:让Agent真正“想起来”

检索这一环的经典问题是:只做向量检索,召回的内容往往“看似相关、实则无用”。比如用户问“这个功能上线多久了”,向量检索可能命中一大堆跟“功能上线”语义相关的制度说明,但真正需要的“该功能上线时间”这条事实被埋在后面。解决办法是混合检索加过滤。

混合检索指同时跑两路:一路是向量检索,按照Embedding相似度召回;另一路是关键词/结构化检索,按实体名、时间范围过滤。两路结果做合并或交叉,再进重排器。重排时我常用的规则是:结构化字段精确匹配的排最前,时间在近7天内的权重上浮,记忆类型与当前请求类型一致的权重上浮。

检索出来的记忆不是全部塞进Prompt,而要按预算挑选。我一般每轮只向Prompt注入3到5条记忆,每条做内容压缩。注入模板可以这样组织:

memory_prompt = """ 用户近期已知信息(用于参考,不要主动复述): - {mem1} - {mem2} - {mem3} """.format(...)

模板里那句“不要主动复述”很关键。没有这句,Agent会把记忆念给用户听,体验非常生硬。注入完成后,Prompt整体再拼接当前对话上下文和系统指令,发给模型。这样模型是在“带着纪要用脑干活”,而不是“读一整本日记再干活”。

3.4 参数调优与成本控制

参数调优是记忆系统从“能跑”到“跑得稳”的分水岭。有几组参数必须关心。

第一组是Top K与相似度阈值。Top K决定注入几条记忆,相似度阈值决定低于多少分的不注入。两者联动。预算充足的场景可以Top K取5到8,阈值设低一点;对话轮次多但对实时性要求高的场景,Top K取3,阈值提高,宁可少给也不要给噪音。第二组是上下文预算比例。我的默认值是系统指令占20%,当前对话占60%,检索记忆占20%。如果任务极度依赖用户历史偏好,可以调到记忆占35%,但记得同步减少对话上下文,防止总token超限。第三组是衰减速度。高频更新的场景衰减周期按天算,低频场景按周算,具体公式是强度值=初始值+命中次数-日期差/衰减半衰期,半衰期一般设为7天。

下面给一份配置速查表,可以直接抄走:

场景特征Top K相似度阈值记忆预算占比衰减半衰期
客服类,多轮短会话50.3020%7天
数据分析类,长上下文80.2530%14天
工具调用类,实时性优先30.4015%3天
个性化推荐类60.3535%30天

表格只是起点。真上线后,这些参数一定要配合检索日志做二次调优。每一条被模型丢弃不用的记忆,都应该从检索日志里找到对应记录,再反推是阈值太低还是重排规则不合理。

4. 坑与对策:我实践过程中踩过的五个坑

4.1 “看着相关,用着无效”的检索结果

这是记忆系统上线后最常见的挫败感来源。向量检索出来的内容,语义上确实跟当前问题沾边,但仔细一看,要么是错误的时间线,要么是另一个维度的信息,放进Prompt后模型根本用不上,甚至会带偏回答方向。

我排查这类问题时发现,根因往往是缺少“场景限定”。解决方法是把实体过滤前置到向量检索之前,检索条件至少带一个实体或时间范围,让向量只在符合场景的子集里找相似,而不是在全库范围内找相似。另外,重排阶段可以加一条“操作相关性”规则:如果记忆里包含具体数值、日期、状态变更等信息,而当前问题恰好是查询类,这类记忆权重上调。

4.2 记忆越存越多,Agent越来越“啰嗦”

记忆库膨胀后,Prompt里的记忆条目开始出现冗余内容,Agent会不自觉地复述记忆、权威化某些旧论断,甚至回答问题时车轱辘话来回说。我一开始以为是指令没写清,试过调整Prompt多次,效果都一般。后来才发现,根子在Top K和记忆粒度上。

粒度问题的解法是写入时合并。同一个实体下的多条相似记忆,合并成一条结构化更强的记录,比如“用户偏好”这个实体下,罗列多条子属性,而不是每次新增一整条独立记忆。啰嗦问题则靠硬约束:Prompt中明确写“只允许在必要信息缺失时主动提及记忆内容,其余情况不要复述”。配合Top K从8降到5,实测输出简洁度明显提升。

4.3 编码结果不稳定,记忆互相“打架”

Embedding服务版本更新、量化策略调整、同一条文本在不同时刻编码,都可能产生距离波动。一旦向量发生偏移,新写入的记忆跟历史记忆之间的相似度计算就会不稳定,可能出现“同一事实被重复存储”或“相似内容无法彼此召回”的情况。

两个对策比较有效:一是为写入的文本内容缓存原始字符串,向量只作为索引,不替代原文;一旦发现向量冲突,可以用原文重算。二是限制Embedding版本升级频率,必须升级时对全库做一次批量向量重建,重建时用统一的批处理脚本,不要边写入边升级,新旧向量混在一起最容易出乱子。

4.4 遗忘机制太激进,关键信息被误删

衰减策略如果只按“最近没用到就降权”,很容易把低频但高价值的记忆给归档掉。典型例子:某重要客户去年提过一个明确的预算约束,过去一年没再提,但今年下单时客户默认这条约束依然有效。如果你的系统按时间衰减把它归档了,就会在关键时刻掉链子。

我把遗忘逻辑改成了分层处理。高价值业务记忆(涉及资金、合规、权限)不做时间衰减或半衰期设得很长;中价值记忆按正常衰减曲线走;低价值记忆如闲聊偏好则快速遗忘,只留最近几条。衰减后状态不是直接删除,而是从热存储转入冷存储,冷存储仍可在显式查询时被检索到。

4.5 多轮循环里的重复记忆轰炸

Agent在执行多步骤任务时,每一轮都可能触发记忆写入,同一个信息被重复存储几十次。有一次我查库,发现用户手机号被存了23遍。这不仅是存储浪费,更会让检索结果被重复信息霸榜,真正多样的记忆没有展示机会。

解法是写入前先做一次“记忆消重”。以实体名+事实类型作为去重键,写入前查一遍,如果已存在,就更新强度值和内容,而不是追加新记录。这步看起来简单,但能有效防住大多数重复轰炸。如果Agent有内部循环调用,还可以在每轮之间做一个短暂去重缓冲,比如20秒内同一实体同一类型不重复写。

5. 常见问题速查与解决路径

这个速查表可以说是浓缩版排错手册,适合上线出问题时快速定位。

现象可能原因优先排查项
Agent回答跟之前事实矛盾历史记忆未注入或已被遗忘检查检索日志是否有命中记录
相关记忆召回但无用缺少实体过滤、只跑向量增加结构化过滤和重排规则
记忆库增长极快写入频率过高、无去重加入消重机制和重要性门槛
Agent复述记忆令人生硬Prompt缺少“不要主动复述”约束调整注入模板
相似度分数普遍偏低Embedding版本漂移或文本过长拆分文本、统一编码版本
旧记忆误导当前决策缺少时效过滤检索时限制最大时间范围
注入记忆超过token上限预算分配失衡收紧Top K,压缩记忆摘要

排查问题的顺序建议是从检索端往前推。先看模型拿到了什么,没有的话看检索是否命中,还没命中就看存储与写入是否遗漏,最后才考虑编码和模型能力问题。沿着这条链路走,大部分问题都能在十分钟内定位出来。

最后说两个个人的体会。一是记忆系统不要追求一步到位,先跑通短会话单Agent的最小闭环,把写入、检索、衰减三个环节的数据都打出来,再逐步加复杂度。二是我建议所有做Agent记忆的团队,从第一天就记录检索日志。很多问题事后无法复盘,就是因为当时没留检索数据。每次对话结束,检查一下哪些记忆被检索了、哪些被模型使用了、哪些虽然命中但被忽略,这些数据比任何参数调优技巧都更能指引你改进的方向。这套思路我已经在多个项目里落地过,改造成本不高,收益却是长效的。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进