1. 从“hindsight”说起:为什么我们需要给Agent装上记忆
“hindsight”这个词本身很有意思,字面意思是“事后的洞察力”,也就是我们常说的“后见之明”。放在LLM Agent的语境里,它指向一个非常具体且棘手的问题:Agent如何记住过去发生过的事情,并在后续决策中真正用上这些经验。
我接触过不少做Agent项目的团队,大家一开始都兴致勃勃地接上大模型、写好工具调用、跑通几个demo,但一旦进入真实场景,问题就暴露了——Agent像个失忆症患者,每次对话都从零开始,昨天用户明确说过的偏好今天完全不记得,上周踩过的坑这周原封不动再踩一遍。这不是模型能力不够,而是记忆架构缺失。
这个项目标题“hindsight”加上agent memory、LLM、MCP、Docker这几个关键词,勾勒出的是一套完整的Agent记忆系统方案。它要解决的核心问题是:让Agent具备跨会话、跨任务的长期记忆能力,并且这种记忆不是简单的聊天记录堆砌,而是经过结构化、可检索、可推理的知识沉淀。适合谁来参考?如果你正在做Agent应用开发、正在被“Agent记不住东西”困扰、或者想了解MCP协议在实际项目中怎么落地,这篇内容应该能给你一些可以直接抄作业的思路。
我下面会从整体设计思路、核心细节、实操过程、问题排查几个维度展开,把“hindsight”这套记忆方案拆开揉碎讲清楚。
2. 整体设计思路:Agent记忆到底该怎么存
2.1 为什么传统方案不够用
大部分Agent项目处理记忆的方式非常粗暴:把历史对话拼接到prompt里。短会话还行,一旦对话轮次多了,token爆炸不说,模型对超长上下文的注意力也会严重衰减。我实测过,当历史对话超过30轮之后,模型对早期信息的召回率下降得非常明显,基本上前10轮的内容等于白存了。
另一种常见做法是接一个向量数据库做RAG,把历史对话embedding后存进去,需要时检索。这比拼接prompt进了一步,但问题也很突出:检索出来的记忆是碎片化的,缺乏上下文关联,模型拿到几条孤立的对话片段,很难还原出完整的决策链路。而且单纯的向量检索对“时间维度”不敏感,Agent分不清哪些是过时的信息、哪些是仍然有效的约束。
“hindsight”这套方案的核心思路,我理解是分层记忆架构:把Agent的记忆分成工作记忆、情景记忆和语义记忆三层,分别对应不同的存储介质和检索策略。工作记忆就是当前会话的上下文,存在内存里,生命周期短;情景记忆是过去发生过的具体事件,带时间戳和场景标签,存在结构化数据库里;语义记忆是从大量情景中抽象出来的规律和偏好,存在知识图谱或向量库里。三层之间通过MCP协议进行统一的读写调度。
2.2 为什么选MCP作为记忆调度层
MCP(Model Context Protocol)在这里扮演的角色是记忆的统一接口层。没有MCP的话,你的Agent要同时对接Redis、Postgres、向量数据库、图数据库,每换一个存储后端就要改一遍Agent代码,维护成本极高。MCP把这些存储能力抽象成标准的工具调用,Agent只需要知道“我要存一条记忆”或“我要检索相关记忆”,具体存到哪、怎么存,由MCP Server内部决定。
这个设计的好处在于可替换性。今天你用Docker跑一个本地的Postgres做情景记忆存储,明天业务量上来了想换成云上的托管数据库,只需要改MCP Server的配置,Agent侧完全不用动。我在实际项目中深刻体会到这一点的重要性——早期没做这层抽象的时候,每次存储层变更都是一次伤筋动骨的改造。
2.3 Docker在其中的角色
Docker解决的是环境一致性和快速部署的问题。Agent记忆系统涉及多个组件:MCP Server、数据库、向量检索引擎、可能还有缓存层。如果每个组件都手动装,光是版本兼容就能折腾掉一整天。用Docker Compose把这些组件编排在一起,一条命令拉起整个记忆系统,开发环境和生产环境用同一套镜像,省去了大量“在我机器上能跑”的扯皮。
而且Docker的隔离性对Agent记忆系统特别重要。不同Agent实例的记忆需要隔离,不同用户的记忆更需要隔离。用Docker的网络和卷机制,可以很自然地实现这种隔离,不用担心数据串号。
3. 核心细节解析:三层记忆的存储与检索
3.1 工作记忆的设计要点
工作记忆就是Agent当前正在处理的上下文,包括最近的几轮对话、当前任务的状态、临时变量等。这部分我建议不要持久化到磁盘,放在内存里就行,因为它的生命周期就是一次会话,会话结束就该释放。
但工作记忆有一个关键设计点:容量控制。不能无限往里塞,要有淘汰策略。我通常用滑动窗口加重要性评分的方式:最近N轮对话无条件保留,更早的对话根据是否包含关键信息(比如用户明确说的偏好、任务的关键约束)来决定是否保留。这个评分可以用一个轻量级的规则引擎来做,不需要动用大模型,否则每次淘汰都要调一次模型,成本扛不住。
工作记忆的另一个要点是与情景记忆的联动。当工作记忆中的某条信息被判定为“值得长期保留”时,要触发一个写入情景记忆的操作。这个判定逻辑我一般设几个触发条件:用户明确说“记住这个”、信息包含时间/地点/人物等结构化要素、信息在多次对话中重复出现。满足任一条件就写入情景记忆。
3.2 情景记忆的存储结构
情景记忆存的是“发生了什么”,所以时间戳是必须的,场景标签也很重要。我用的表结构大概是这样:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | UUID | 主键 |
| agent_id | String | 所属Agent实例 |
| user_id | String | 关联用户 |
| timestamp | DateTime | 事件发生时间 |
| scene | String | 场景标签,如“订机票”“查天气” |
| content | Text | 事件内容原文 |
| embedding | Vector | 内容向量,用于语义检索 |
| importance | Float | 重要性评分,0-1 |
| ttl | Integer | 过期时间,秒为单位,0表示永不过期 |
这个结构里,importance和ttl是两个容易被忽视但极其重要的字段。不是所有记忆都值得永久保留,比如“用户今天问了一句天气”这种,存一周就可以清了。而“用户对花生过敏”这种,必须永久保留。ttl字段让清理策略可以自动化执行,不用人工干预。
检索的时候,我通常用混合检索:先按agent_id和user_id过滤,再按scene做精确匹配,最后用embedding做语义相似度排序。这样既能保证检索范围可控,又能兼顾语义相关性。实测下来,纯向量检索的准确率大概在60%左右,加上结构化过滤后能提升到85%以上。
3.3 语义记忆的抽象与推理
语义记忆是从大量情景记忆中提炼出来的“规律”。比如Agent发现用户每周五晚上都会问电影推荐,这就形成了一条语义记忆:“用户有周五晚看电影的习惯”。这条记忆不是从单次对话中来的,而是从多次情景记忆中归纳出来的。
实现上,我建议用定时任务+LLM归纳的方式。每天凌晨跑一个批处理任务,把当天新增的情景记忆喂给LLM,让它输出可能的规律和偏好,然后写入语义记忆库。语义记忆库可以用知识图谱的形式存储,实体是用户、时间、场景,关系是“偏好”“习惯”“禁忌”等。
语义记忆的检索和工作记忆、情景记忆不同,它更多是主动推送而非被动查询。当Agent开始一个新任务时,先根据任务类型和用户ID去语义记忆库里拉取相关的规律和偏好,作为系统提示的一部分注入到prompt里。这样Agent在决策时就能自然而然地考虑到用户的长期偏好,而不是每次都要显式检索。
4. 实操过程:从零搭建一套Agent记忆系统
4.1 环境准备与Docker编排
先确保你的开发机装好了Docker Desktop。Windows用户如果遇到“Virtualization support not detected”的报错,需要进BIOS开启虚拟化支持,这个坑我踩过好几次,不是Docker的问题,是主板设置的问题。
Docker装好之后,创建一个项目目录,写一个docker-compose.yml文件,把需要的服务编排进去。我用的基础配置大概是这样:
version: '3.8' services: postgres: image: postgres:16 environment: POSTGRES_DB: agent_memory POSTGRES_USER: agent POSTGRES_PASSWORD: agent_pass ports: - "5432:5432" volumes: - pg_data:/var/lib/postgresql/data redis: image: redis:7-alpine ports: - "6379:6379" qdrant: image: qdrant/qdrant:latest ports: - "6333:6333" volumes: - qdrant_data:/qdrant/storage mcp-server: build: ./mcp-server ports: - "8080:8080" depends_on: - postgres - redis - qdrant environment: PG_HOST: postgres REDIS_HOST: redis QDRANT_HOST: qdrant volumes: pg_data: qdrant_data:这里Postgres存情景记忆的结构化数据,Redis做工作记忆的缓存,Qdrant做向量检索,mcp-server是我们自己写的记忆调度服务。四个组件各司其职,通过Docker网络互相通信。
注意:Postgres的密码不要用默认的,生产环境一定要改。我见过太多因为密码没改导致数据泄露的案例,这个不是小事。
4.2 MCP Server的核心实现
MCP Server是整个记忆系统的中枢,它对外暴露几个核心工具:store_memory、retrieve_memory、forget_memory、summarize_memory。Agent通过MCP协议调用这些工具,不需要关心底层用的是Postgres还是Qdrant。
store_memory的实现逻辑是:接收记忆内容、场景标签、重要性评分,先写入Postgres,同时把内容的embedding写入Qdrant,如果重要性超过阈值,再触发一次语义归纳任务。这里有个细节:写入Postgres和写入Qdrant要保证原子性,不能一个成功一个失败。我的做法是先写Postgres,拿到id后再写Qdrant,如果Qdrant写入失败就回滚Postgres的事务。虽然这样性能会差一点,但数据一致性比性能重要。
retrieve_memory的实现是混合检索:先用结构化条件过滤出候选集,再用向量相似度排序,最后取Top-K返回。K值我一般设5到10,太多会稀释相关性,太少可能漏掉关键信息。返回的结果里要带上时间戳和重要性评分,让Agent自己判断这条记忆的可信度。
forget_memory不是物理删除,而是标记为已失效。物理删除太危险了,万一误删了重要记忆没法恢复。标记失效后,检索时自动过滤掉,但数据还在,需要时可以恢复。
4.3 Agent侧的接入方式
Agent侧接入MCP Server很简单,在初始化的时候配置好MCP Server的地址和认证信息,然后就可以像调用普通工具一样调用记忆工具了。我通常会在Agent的系统提示里加一段说明,告诉它什么时候该存记忆、什么时候该检索记忆。
比如这样的提示词片段:
你拥有长期记忆能力。当用户告诉你重要信息(如偏好、禁忌、关键日期)时,调用store_memory工具保存。当开始新任务时,先调用retrieve_memory检索相关记忆。当发现用户行为有规律时,调用summarize_memory生成语义记忆。
这段提示词看起来简单,但效果非常明显。没有这段提示的时候,Agent基本不会主动使用记忆工具,它不知道这些工具的存在意义。加上之后,记忆的读写频率会大幅提升,整个系统的价值才能真正体现出来。
4.4 记忆的初始化与冷启动
新用户第一次使用的时候,记忆库是空的,这时候检索不到任何东西,Agent的表现和没有记忆系统时一样。这是正常的,但用户体验上会有落差。我的做法是预置一批通用记忆,比如“用户偏好简洁回答”“用户使用中文交流”等,这些通用记忆对所有用户生效,让Agent从第一次对话就表现得“有记忆”。
预置记忆的另一个作用是引导用户。当用户看到Agent说“我记得你之前提到过...”的时候,他会更愿意主动告诉Agent更多信息,形成正向循环。这个心理效应在实际产品中非常明显,有预置记忆的版本,用户主动提供信息的比例比没有预置的高出三倍多。
5. 常见问题与排查技巧实录
5.1 记忆检索不准确怎么办
这是最常见的问题。Agent检索出来的记忆和当前任务不相关,导致回答跑偏。排查思路分三步:先看embedding模型是否适合当前语言和领域,中文场景用中文embedding模型效果会好很多;再看检索的过滤条件是否太宽或太窄,太宽会引入噪声,太窄会漏掉相关记忆;最后看重要性评分是否合理,低重要性的记忆应该降权。
我自己的经验是,embedding模型的选择比检索算法本身更重要。换一个更适合领域的embedding模型,准确率提升往往比调检索参数更明显。另外,检索时加上时间衰减因子也很有效,越久远的记忆权重越低,除非它的重要性评分特别高。
5.2 Docker网络不通的排查
Docker Compose启动后,如果mcp-server连不上postgres,先检查它们是否在同一个网络里。默认情况下,Compose会创建一个共享网络,所有服务都在里面,用服务名做主机名就能互相访问。如果连不上,大概率是服务启动顺序的问题——mcp-server启动的时候postgres还没准备好。
解决办法是在mcp-server的启动脚本里加一个等待逻辑,不断重试连接postgres,直到成功为止。或者用Compose的healthcheck机制,让mcp-server依赖postgres的健康状态。我一般用前者,简单直接,不依赖Compose的版本特性。
5.3 记忆膨胀导致性能下降
跑了一段时间之后,情景记忆表可能积累了几十万条记录,检索速度明显变慢。这时候需要做记忆压缩:把低重要性、低访问频率的旧记忆归档到冷存储,主表只保留最近三个月的高频记忆。归档的数据不是删除,需要时还能查,只是不在主检索路径上。
另一个优化是索引优化。Postgres的GIN索引对JSON字段的查询很快,Qdrant的HNSW索引对向量检索也很快,但索引本身会占空间,需要根据数据量调整参数。我一般把HNSW的ef_construct设成200,m设成16,这个配置在大多数场景下都能兼顾速度和准确率。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| Agent不主动存记忆 | 系统提示未说明记忆工具 | 检查prompt中是否有记忆使用说明 | 在系统提示中加入记忆工具的使用指引 |
| 检索结果不相关 | embedding模型不匹配 | 用测试用例验证embedding质量 | 更换为领域适配的embedding模型 |
| 记忆写入失败 | 数据库连接超时 | 查看mcp-server日志 | 增加连接重试逻辑和超时时间 |
| 检索速度慢 | 数据量过大或索引缺失 | 用explain分析查询计划 | 添加索引、归档旧数据、调整HNSW参数 |
| 记忆串号 | 隔离字段未正确过滤 | 检查检索条件是否包含agent_id和user_id | 在所有检索路径强制加上隔离过滤 |
| Docker启动失败 | 虚拟化未开启或端口冲突 | 查看Docker Desktop报错信息 | 开启BIOS虚拟化、更换冲突端口 |
提示:记忆系统的调试一定要有日志。每次读写记忆都记一条日志,包含时间、操作类型、记忆id、检索条件、返回结果数量。出问题的时候,这些日志就是你的救命稻草。
6. 记忆系统的扩展方向与个人体会
这套基于MCP和Docker的Agent记忆方案,我在几个项目中落地过,整体稳定性不错。但它不是终点,还有几个方向可以继续深挖。
一个是记忆的跨Agent共享。现在每个Agent实例的记忆是隔离的,但实际上多个Agent之间可能需要共享某些记忆。比如一个负责订机票的Agent和一个负责订酒店的Agent,它们都需要知道用户的出行偏好。通过MCP Server做一个共享记忆池,让多个Agent按权限读写,这个扩展在业务上很有价值。
另一个是记忆的可解释性。现在Agent检索到一条记忆后,直接就用在了回答里,用户看不到这条记忆的来源和推理过程。如果能把“我为什么记得这个”展示给用户,信任度会大幅提升。实现上可以在记忆存储时记录来源对话的id,检索时一并返回,前端展示时做一个溯源链接。
我个人在实际操作中的体会是,记忆系统的价值不在于技术多复杂,而在于是否真正融入了Agent的决策流程。我见过一些项目,记忆系统做得很精致,但Agent根本不用它,等于白做。反过来,哪怕只是简单的键值存储,只要Agent在每次决策前都查一下,效果就比不用强很多。所以如果你刚开始做,不要追求大而全,先把“存”和“取”两个动作跑通,让Agent养成用记忆的习惯,再逐步优化存储结构和检索策略。
最后分享一个小技巧:在开发阶段,给记忆系统加一个调试面板,能实时看到Agent存了什么、取了什么、取了之后怎么用的。这个面板对排查问题和优化策略的帮助极大,比看日志直观得多。我用Grafana加一个简单的Web界面就搞定了,成本很低,收益很高。