说实话,Agent 这个方向最让我头疼的,不是模型能力不够,也不是工具调用写得不顺,而是它真的记不住事。你今天跟它聊过的偏好、喜欢的时间格式、上次项目的结论,一到新会话就归零。Hindsight 就是我最近在开源社区里翻到的一个项目,专门给这类“健忘”的 Agent 补上一套长期记忆系统。它不是简单做个向量库就完事,而是把记忆当成一个完整的系统来设计:写入、检索、遗忘、巩固,都有相对成熟的方案。这篇文章我就围绕设计与实现,把里面真正值得学的东西拆开讲,也把我实际调试中踩过的坑一并晒出来,给想给 Agent 加记忆的朋友一个能直接上手的参考。
1. 先把 Agent 失忆的问题想清楚
1.1 Agent 不是缺上下文,而是缺记忆
很多人一开始会觉得,Agent 记不住事,那不是因为上下文窗口不够吗?把所有的对话历史都塞进去不就行了?我一开始也这么干过。结果就是:钱烧得快、响应慢,而且最关键的是,塞进去的 90% 都是没用的闲聊和重复信息,真正关键的偏好和结论反而被淹没在大段历史里。
上下文窗口是“工作台”,不是“仓库”。人脑做事情从来不会把从小到大的经历全部倒带到眼前,而是只把当前任务需要的那部分记忆调出来。Hindsight 的核心思路就是这个:它给 Agent 装了一个“长期记忆层”,把值得记住的信息抽取成结构化条目,存下来;需要的时候,只把与当前问题相关的几条记忆捞出来,塞进上下文。这样既控制成本,又提升准确率。
这个思路和普通 RAG 还不一样。普通 RAG 是把知识库拆成片段,靠向量相似度去匹配,适合做“查找资料”。但 Agent 的记忆更像是一个“个人履历”,里面有用户偏好、历史决策、任务进度、踩过坑的教训。这些内容很多都带有时效性,有的重要,有的会过期。如果只是粗暴地建成向量库,就会出现“昨天的过期结论今天还在干扰判断”这种问题。
Hindsight 给出的解法值得参考:它把记忆按生命周期和层级拆开,不同种类的记忆用不同方式处理,形成一个有“新陈代谢”的系统。这看起来比单纯套 RAG 复杂,但踩过坑之后你会明白,这一步省不掉。
1.2 Hindsight 的记忆分层设计:工作、情景、语义、程序
读这个项目的源码时,最吸引我的就是它借鉴了认知科学里关于人类记忆的分层方式,没有把所有内容一锅端。它的分层结构大致是这样的:
- 工作记忆:也就是当前的会话上下文、正在处理的临时信息。它在任务完成后就清空或转储。
- 情景记忆:记录具体发生过的事件,比如“上周三用户要求把报表改成每周一凌晨三点跑”。
- 语义记忆:从情景里提炼出来的一般性知识,比如“用户偏好每周一接收报表”“用户喜欢用中文术语而非英文缩写”。
- 程序记忆:也被称为技能记忆,记录的是“怎么做”的经验,比如“处理登录失败时,先查 token 再查网络”。
这个分层最大的好处是,查询的时候可以按类型过滤。比如 Agent 在执行一个报表任务时,优先捞语义记忆和程序记忆,不用把大量早期情景翻出来。从工程实现上看,它也方便做差异化处理:情景记忆可以实时新增,语义记忆要定期合并去重,程序记忆的写入则要更谨慎,因为技能类的东西错了会反复出错。
我在自己实现时,一开始没搞这么复杂,只做了个“全部向量化入库”,结果量大了之后检索质量直线下降。后来参考这种分层把数据表拆开,再加上类型标签,才真正解决了问题。所以说,分层的意义不光是理论漂亮,它对检索质量、存储成本和维护效率都有实打实的影响。
1.3 为什么不用“一股脑全存”的笨办法
我知道你可能会想:既然要做长期记忆,那不是存得越多越好吗?真不是。存得越多,噪音越多,检索时命中的垃圾越多,最终回答就越容易出错。Hindsight 在写入阶段就做了关键动作:过滤和提炼。它不会把你和用户整整两小时的闲聊全存下来,而是用模型从中抽取“有记忆价值”的信息,比如用户明确表达过的偏好、承诺过的截止时间、任务的重要转折点等。
这个思路本质上是在做“记忆的选择性编码”。你可以把它类比成写日记:写日记不是把每分钟的流水账都记下来,而是记下这天真正值得记住的事。AI 做这件事比人还更需要克制,因为存储和检索成本都是真金白银,而且垃圾记忆会让整个系统变傻。
所以,在动手之前,第一件事不是选数据库,而是想清楚一个问题:什么信息对你的 Agent 来说,值得记住?这个问题想不通,后面所有模块做得再漂亮都是白搭。Hindsight 的做法是提供了可配置的抽取规则和 Prompt 模板,让开发者根据自己的领域自定义“值得记住”的判断标准。这一点我强烈建议你不要偷懒,直接用自己的业务场景跑几轮,你会发现默认规则往往不够贴合。
2. 核心机制拆解:写入、检索、遗忘是怎么协同的
2.1 写入侧:不是所有对话都值得记住
Hindsight 的写入管线本质上是一条异步流水线:原始对话 → 抽取判断 → 格式化 → 向量化 → 入库。它刻意把写入和读取分开,不让记忆的存储阻塞对话主流程。
具体拆开看,它的关键步骤有三个:
第一步,抽取。系统会拿着当前这一轮或一轮批量对话,交给语言模型,让它判断这段对话里有没有值得长期记住的信息。同时会给一个分类标签,是偏好、事实、任务、还是技能。我见过一些简化实现是直接从原文里抠句子,不走模型,但效果很差,因为用户表达意图时往往很口语化,直接截取很难形成清晰的知识条目。
第二步,去重和归一化。存入前会和已有记忆做一次相似度比对,如果发现类似的条目已经存在了,就合并或跳过。这一环节很多人忽略,结果就是跑一个月之后库里躺了几十条互相矛盾或高度重复的“用户喜欢简洁回答”。
第三步,重要性打分。这一步直接决定了后续的遗忘策略怎么走。Hindsight 里每个记忆条目都带一个重要性分数,这个分数会随时间衰减。比如一个重要度 0.9 的偏好,可能三个月后衰减到 0.4,如果期间没有再次被强化,就会被清理掉。而有些信息虽然重要,但如果用户已经明确表示“这个不用了”,就会被打上失效标签。
写入侧还有一个容易被忽略的细节:时效性字段。每条记忆都要记录“创建时间”和“最后访问时间”,甚至有的要显式标注“有效截止时间”。没有时间戳的记忆系统,基本没法做长期维护。我的建议是,就算你不想一开始就实现遗忘机制,也务必把时间字段先留好,否则后续想加就得迁移数据。
2.2 检索侧:相关性、近因、重要性一起打分
记忆写进去了,关键的是能查出来。Hindsight 的检索不是单纯向量相似度,而是算一个综合分数。我的理解是这样的:
最终得分 = 向量相似度 × 权重1 + 近因加权 × 权重2 + 重要性分数 × 权重3
其中近因加权的典型实现是按时间衰减来算的,比如公式:
权重 = (0.5) ^ (年龄天数 / 半衰期)假设你设定半衰期是 30 天,那么一个 30 天前的记忆权重就是 0.5,一个 60 天前的就只剩下 0.25。这样做就是为了防止“旧记忆固执地占据注意力”。重要性分数则类似一个“信息价值标签”,保证像“用户对某个知识点有过敏反应”这类信息,即使时间久远也能被捞出来。
检索的流程一般是两段式:先在向量库里做 ANN(近似最近邻)召回候选集,例如取 20 条;再用上面的综合打分函数精排,取前 5 到 8 条。这套方案的核心价值在于,如果把所有历史记忆全塞进去打分,性能上扛不住,而向量召回能快速缩圈,精排阶段又有足够时间做复杂计算。
我在实际项目中踩过的坑是,只做向量相似度时,检索结果经常只追着字面相似跑。用户说“我又不喜欢那种风格了”,系统查出来的却还是以前“喜欢某种风格”的记忆,因为字面上有很多重叠。加了时间和重要性加权之后,问题就减弱了不少,因为新写入的“不喜欢”在近因权重上碾压了旧记忆。
2.3 遗忘与巩固:记忆的“新陈代谢”
遗忘这件事,听起来像是系统的缺陷,但其实是长期记忆系统里最需要主动设计的能力。如果只存不删,存储成本逐年上涨是小事,真正致命的是陈旧错误信息对 Agent 判断的持续污染。
Hindsight 实现了一套类似人脑记忆巩固的机制。它有两种操作:一种是衰减,一条记忆的权重随着时间降低;另一种是再巩固,当同一条记忆被反复检索或关联到时,它的热度会提升,半衰期会被拉长,相当于“这个信息一直在用,所以它更重要”。这背后的逻辑就是:记忆不是越老越不重要,而是“被用到的经验会反过来强化自己”。
具体操作上,系统会周期性地跑一个批次任务,筛选出衰减到阈值以下的记忆,做一次复核。复核的方式通常是让模型判断:这条记忆是否已经被新记忆覆盖?是否仍然有参考价值?如果确认无效,就归档或删除。这个任务不用很频繁,每天跑一次就够。
你可能会担心:万一删错了怎么办?Hindsight 的做法是软删除,也就是先打上 archived 标记,不参与检索,保留在独立表里。如果后续发现某条被删的记忆很重要,还能通过管理接口恢复。这套设计非常实用,我在调优时经常手动把误删的东西捞回来,所以强烈建议你在实现时也给删除留后路,不要物理删除。
3. 动手实现一个缩短版 Hindsight
3.1 技术选型与基础架构
Hindsight 本身的实现选用了一些常见技术栈,但核心思路并不绑定某一家,我用一套更通用的方案也能复刻出主要功能。推荐的最小可行组合是:
- 一个支持向量检索的数据库(或本地向量库)
- 一个关系型数据库或文档库存记忆元数据
- 一个增强语言模型做抽取和摘要
- 一个异步任务队列跑写入和清理
注意,向量库和元数据不要混在同一个表里。向量库负责相似度搜索,元数据负责存重要性、类型、时间等结构化信息,两边通过记忆 ID 关联。这样做的原因是,向量库对结构化过滤支持通常较弱,而关系库做复杂查询更强。混合查询时,先从向量库召回候选 ID,再去关系库里做过滤和排序。
数据模型上,一条记忆的最小字段可以这样搞:
CREATE TABLE memory ( memory_id TEXT PRIMARY KEY, agent_id TEXT, memory_type TEXT, -- episodic / semantic / procedural content TEXT, importance REAL, -- 0.0 - 1.0 created_at DATETIME, last_accessed DATETIME, archived INTEGER DEFAULT 0 );再加上一张用于向量检索的 collection 表,每条记录对应一个向量。两边用 memory_id 关联,查询时先向量召回,再带条件过滤。
3.2 记忆写入模块:先抽取再入库
我做一个简化版写入函数的思路是这样的:
def write_memory(agent_id, dialog): # 1. 抽取:判断这轮对话是否有值得记住的信息 extraction = llm_extract(dialog) if not extraction.is_valuable: return # 2. 格式化:把抽取结果变成结构化条目 memory_item = MemoryItem( agent_id=agent_id, memory_type=extraction.memory_type, content=extraction.content, importance=extraction.score ) # 3. 去重:检查库中是否已有语义相似的记忆 embedding = embed(memory_item.content) similar_ids = vector_search(embedding, top_k=1) if similar_ids and similarity >= 0.9: merge_or_skip(memory_item, similar_ids[0]) return # 4. 向量化并入库 vector_db.insert(memory_id, embedding) sql_db.insert(memory_item)这里的抽取 Prompt 很关键,需要明确告诉模型:提取用户明确表达的偏好、承诺、重要事实,忽略寒暄和临时性讨论。我给过一版效果还不错的 Prompt 核心句,大意就是:“以下是一段对话,请判断是否存在应当长期记住的信息。如果有,请输出信息类型、内容摘要、重要程度 0 到 1。如果都是无关紧要的闲聊,输出 IS_VALUABLE=false。”
实测中一个重要心得是:不要追求抽取一次成功。宁可少抽,不要误抽。一条误入库的错误记忆侵害力远大于两条漏掉的记忆。因为漏掉的只是“没记住”,而错误记忆是“记错了”,它会反复污染后续所有决策。
3.3 检索增强模块:把记忆注入 Agent 的 Prompt
检索侧的实现思路在我看来更接近工程核心。整个过程可以拆成三步:触发判断、候选召回、精排注入。
触发判断的意思是,不是每一轮对话都需要访问记忆库。如果用户只是在闲聊或者当前上下文已经足够,就没有必要多此一举。Hindsight 有一个轻量判断逻辑,如果当前上下文中已经包含足够的信息以支撑回答,就跳过检索。
候选召回的精简写法:
def retrieve_memories(query, agent_id, top_k=8): # 1. 向量召回 query_embedding = embed(query) candidates = vector_db.search(query_embedding, top_k=30) # 2. 精排:计算综合分数 ranked = [] for cand in candidates: near_weight = 0.5 ** (age_days(cand) / 30.0) importance = cand.importance final_score = 0.6 * cand.similarity + 0.3 * near_weight + 0.1 * importance ranked.append((final_score, cand)) # 3. 过滤低质量结果并取 top filtered = [c for c in ranked if c.similarity > 0.75] return [c for _, c in sorted(filtered, reverse=True)[:top_k]]注入时,我通常会在 system prompt 里加一个“记忆上下文”区块,把这些记忆条目逐条列清楚,并且标明时间和类型。例如:
以下是从长期记忆中找回的相关信息,请参考但不盲从: [情景记忆 · 2025-02-10] 用户明确表示不接受销售电话推销。 [语义记忆 · 2025-03-01] 用户偏好在周一早上收到周报。这里有一个很深的体会:检索结果除了要带内容,一定要带“元信息”。如果 Agent 只看到内容,看不到时间、来源、类型,它可能会把一条去年的事实当作现在的用户状态来用。加上了时间字段,模型至少有机会判断“这条信息可能已过时”。另外,聪明的做法是让 Agent 拥有“怀疑记忆”的权利,也就是 prompt 里写清楚,如果记忆与当前对话直接冲突,以当前对话为准。
3.4 参数调优:半衰期、阈值、TopK 怎么定
参数这个东西,不可能有放之四海皆准的值,但可以给一组我实测下来比较稳的起点:
- 向量召回量:30 条。太少了会漏掉合适候选,太多则精排压力大。
- 精排输出数(TopK):5 到 8 条。超过 8 条,模型处理起来会很累,效果反而下降。
- 相似度阈值:0.75。低于这个值的大概率是不相关内容,放进 prompt 只会制造噪音。
- 时间衰减半衰期:30 天,适合一般助手型 Agent。如果是做客服,7 到 14 天更合适,因为客服场景里用户近期诉求的权重远大于半年前的诉求。
- 重要性分数权重:0.1 到 0.2 够了。目的是让“重要且旧”的信息能被捞出来,但压制作用不能太强。
调参的时候,我的建议是先固定住 TopK 和阈值,只是去调半衰期,同时观察两个指标:记忆命中率(检索出来的记忆有几次被最终回答真正用到)和记忆污染率(几条记忆因为过时导致回答错误的比例二者一般会形成一个权衡。半衰期太长,污染率高;太短,命中率低。做一组灰度实验,挑一个让两者都相对能接受的值就行。这个过程没有捷径,但一次调好之后,系统能稳定跑很久。
4. 跑起来之后踩过的坑
4.1 检索命中率低,问题出在阈值和模型
第一次跑通系统后,我发现了一个尴尬的情况:明明库里已经有用户说过的偏好,可查询时死活召不回来。排查后原因有两个。
一个是相似度阈值设太高。我最初设了 0.85,结果很多语义相关但字面不匹配的条目都被过滤掉了。这个问题的本质是,embedding 模型的相似度分数分布和你想象的不一样,有的模型 0.7 就已经是高度相关了。我的做法是跑一批真实样本,统计“人工判断为相关”的那些条目的相似度分数分布,再决定阈值,而不是拍脑袋定。
另一个原因是 embedding 模型太弱,或者说它没有经过针对性的微调。如果你做的领域名词很专,比如医疗术语、工程材料、法律条款,通用 embedding 模型的召回效果会明显不够。这时不要急着换系统,先花时间对比两三个领域内表现更好的模型,看它们在同样数据集上的召回率表现。我换过一次模型之后,命中率提升了接近一倍。
4.2 记忆污染:昨天的事在今天还在捣乱
记忆污染是长期记忆系统里最让人抓狂的问题。表现出来就是:用户昨天说“暂时不要执行自动部署”,今天说“现在恢复自动部署”,系统却因为昨天那条记忆的残余影响,回答得特别犹豫。
这个问题的根源在于,旧记忆没有被及时标记为失效,或者新记忆写入时缺少冲突检测。Hindsight 的做法是在写入新记忆前,先把相关旧记忆找出来,如果类型相同、主题相似、语义相反的,就把旧记忆标记为 deprecated。这个步骤用一次向量检索 + 一个简单的语义冲突判断就能实现。
我在自己实现时加了一个补充手段:在检索结果里加入显式的时间衰减提示。当一条记忆的年龄超过一定值,但重要性又高所以被召回了,我会在 prompt 中给模型加一句提示:“这是一条较旧的记忆,请确认是否与当前情境仍然一致。”这个做法模型很吃这一套,能显著降低过时记忆误用概率。
4.3 存储膨胀与重复记忆
跑了两个月之后,我统计了一下记忆库里的数据量,涨得比我预期快很多。打开明细一看,里面有大量重复内容:同一件事被抽取了三四次,只是表述略有不同。这不仅占了空间,还会在检索时频繁霸榜,把其他有价值的记忆挤下去。
这个问题需要在写入侧加一道更严格的去重。具体办法是,在向量召回阶段,除了召回过候选中同一条记忆之外,再额外检索“重复率最高的一条”,把相似度达到预设阈值的条目拦下来。如果相似度很高但想保留后者,可以合并两条记录,增加后者的重要性分数,同时把旧的那条标记为 merged。
另外,我调整了抽取时机:从“每一轮对话都尝试抽取”改成“一个任务或主题收敛后再批量抽取一次”。这样能大幅减少同一件事被反复写入的次数。虽然抽取实时性差了点,但换来的是记忆库整洁度大幅提升,很值。
5. 再往后走:记忆系统的进化方向
5.1 从单机记忆到群体记忆
单个 Agent 的记忆做得再漂亮,也只是一个人的记忆。实际业务中你会发现,有很多场景是一个团队/多个 Agent 共享一套集体经验。举个很简单的例子:客服系统里,不同座席 Agent 处理不同用户,但“某个产品型号最近频繁出现某类故障”这种经验应当是共享的。
Hindsight 的架构里本身就把 agent_id 作为了记忆表的重要维度,这给多智能体场景留了口子。进一步做扩展时,可以引入记忆的可见性和权限:个人记忆只有自己能读写,团队记忆有一份公共视图,大家都能检索和贡献,但写入要经过更严格的审批。这个设计有点像“个人笔记”和“团队 Wiki”的区别,做得好,协同效率提升非常明显。
这部分我自己还在摸索。目前采用的折中方案是:抽出语义记忆和程序记忆的上层部分,定时汇总成团队记忆;情景记忆保持个人隔离。既保证个性化,又让经验可以横向流动。
5.2 记忆可视化与用户反馈闭环
长期记忆最大的隐性问题,是用户不知道系统记住了什么,不知道哪条记错了,也不知道怎么纠正。这会导致用户对系统的信任一点点流失。我在研究过程中越发觉得,记忆系统一定要“可查看、可纠正”。
具体来说,前端可以做一个记忆面板,分类型展示当前智能体记住了哪些信息,每条记忆后面附带“删除”和“纠正”按钮。用户纠正一条记忆之后,系统不只是改内容,还应该触发一次相关性级联调整:类似主题的记忆的权重降一点,纠正后的条目权重提高。这个闭环比任何算法优化都管用,因为来自用户的显式反馈是最强的强化信号。
Hindsight 在这方面提供了基础的管理接口,但没有做成完整的可视化前端。不过它的数据模型设计得比较干净,接一个简单的管理界面并不难,这算是我认为最值得继续投入的方向。
5.3 我对长期记忆的实践建议
最后聊一点宏观层面的体会。做完了这套系统,我对“Agent 记忆”这件事的看法变了很多。以前总觉得记忆是锦上添花,现在认为它是一个合格 Agent 的基础设施。对比一下就知道:一个带了长期记忆的客服 Agent 和一个每次会话都从零开始的客服 Agent,前者的体验完全是另一个层级。
但在实操层面,我的建议却是“克制”。不要把记忆系统一开始就设计得特别庞大,最好先跑一个只支持“用户偏好 + 重要事实”的最简版本,观察它能不能稳定工作三个月。稳定之后,再加入技能记忆、记忆共享、自动清理这些复杂能力。系统的复杂度是滚出来的,不是设计出来的。要是开头就堆全套,遇到问题你根本定位不了是哪里出的错。
还有一个很值得记住的细节:记忆系统必须能“被遗忘”。如果一个系统只进不出,它最终会被信息垃圾淹没。设计时先把遗忘机制想好,比把存储容量买大有用得多。人脑之所以记忆靠谱,恰恰是因为它擅长忘记。Agent 也要学会这件事。