1. 从“hindsight”说起:为什么我们需要给Agent装上一双“后视之眼”
“hindsight”这个词,直译过来就是“后见之明”。放在人类身上,它描述的是一种再普通不过的能力:你做完一件事,回头复盘,突然意识到“当时要是那样做就好了”。这种能力看似廉价,甚至常常伴随着懊悔,但它恰恰是智能行为中极其关键的一环——没有它,一个系统就只能在同一个坑里反复跌倒。
把这个概念搬到AI Agent身上,事情就变得非常有意思了。当前绝大多数基于LLM的Agent,本质上都是“失忆”的。你跟它聊了二十轮,它可能还记得上下文窗口里的内容;但一旦会话结束,或者上下文被截断,它就彻底忘了你是谁、你之前交代过什么偏好、上次那个任务为什么失败。它永远活在“当下”,没有过去,也就谈不上真正的成长。
“hindsight”这个项目标题,指向的正是这个痛点:给Agent构建一套事后记忆机制。它不是简单地把聊天记录存进数据库,而是要让Agent能够从过去的交互中提取经验、形成可复用的记忆,并在未来的决策中主动调用这些记忆。换句话说,它试图回答一个核心问题:Agent如何像人一样,从“事后诸葛亮”变成“事前有准备”?
这个方向之所以在当下特别值得聊,是因为整个行业正在从“单次对话”向“长期陪伴型Agent”演进。无论是个人助理、客服机器人,还是自动化工作流中的决策节点,大家都不再满足于“一问一答”,而是希望Agent能记住用户的习惯、项目的上下文、历史任务的成败教训。而支撑这一切的技术底座,恰好就是热词里反复出现的几个关键词:agent memory、LLM、MCP、Docker。
这篇文章适合谁看?如果你正在做Agent相关的产品开发,或者对LLM应用架构感兴趣,又或者你只是好奇“为什么我的Agent总是记不住事”,那接下来的内容应该能给你一些可以直接抄作业的思路。我会从整体设计、核心细节、实操落地、问题排查四个层面,把“hindsight”这类Agent记忆系统的构建逻辑拆开来讲,尽量做到既有原理,也有能跑起来的代码和配置。
2. 整体设计与思路拆解:Agent记忆系统到底该怎么搭
2.1 为什么“存聊天记录”远远不够
很多人第一次做Agent记忆,第一反应就是:把对话历史存进数据库,下次对话时捞出来塞进prompt。这个做法在Demo阶段没问题,但一旦对话轮次上去,就会立刻撞墙。原因很简单:上下文窗口是有限的,而记忆是无限的。你不可能把过去一百次对话全部塞进prompt,那样token成本会爆炸,而且模型对超长上下文的注意力也会稀释,关键信息反而被淹没。
所以“hindsight”这类系统的第一个设计决策,就是把记忆分成不同的层次。我在实际项目中通常会分成三层:
- 工作记忆(Working Memory):当前会话的上下文,存在内存或Redis里,生命周期就是这次会话。它负责维持对话的连贯性,让Agent知道“刚才聊到哪了”。
- 短期记忆(Short-term Memory):最近几次会话的摘要或关键事件,存在关系型数据库或文档数据库里,生命周期可能是几天到几周。它负责让Agent知道“最近发生了什么”。
- 长期记忆(Long-term Memory):从大量历史交互中提炼出来的经验、偏好、事实性知识,存在向量数据库里,生命周期是永久的。它负责让Agent知道“用户是谁、什么做法有效、什么坑不能踩”。
这个分层不是拍脑袋定的,它对应的是人类记忆的认知模型。工作记忆就像你脑子里正在想的事,短期记忆像你记得昨天吃了什么,长期记忆像你记得自己的名字和专业技能。Agent要表现得“聪明”,这三层缺一不可。
2.2 为什么选MCP作为记忆的接入协议
热词里反复出现“MCP”,而且有人问“MCP是软件协议还是硬件协议那个概念”。这里先澄清一下:MCP(Model Context Protocol)是一个软件协议,它定义的是LLM应用如何与外部工具、数据源进行标准化交互。你可以把它理解成“AI世界的USB接口”——不管你是数据库、文件系统、还是某个API,只要实现了MCP,LLM就能用统一的方式去调用。
在“hindsight”这个场景里,MCP的价值在于解耦。记忆的存储、检索、更新逻辑,可以封装成一个独立的MCP Server,而Agent本身只需要通过MCP协议去调用它。这样做的好处是:
- Agent的代码不需要关心记忆存在哪、怎么查,它只管“我要回忆一下上次类似任务是怎么做的”,MCP Server负责返回结果。
- 记忆系统可以独立升级、独立扩展,不会因为换了向量数据库就把Agent代码重写一遍。
- 多个Agent可以共享同一个记忆MCP Server,实现“团队记忆”。
我试过把记忆逻辑直接写在Agent里,也试过用MCP拆出去,实测下来后者的维护成本低很多。尤其是当你的Agent需要接入多个工具时,MCP的统一接口能让整个架构清爽不少。
2.3 Docker在其中的角色:为什么不是“可选项”而是“必选项”
热词里“docker安装”“docker compose”“docker desktop”出现频率极高,说明很多人正在被环境问题折磨。在Agent记忆系统里,Docker不是锦上添花,而是基础设施。原因有三个:
第一,依赖隔离。向量数据库(比如Milvus、Qdrant)、关系型数据库(比如MySQL、PostgreSQL)、缓存(Redis),这些东西如果直接装在宿主机上,版本冲突能让人崩溃。Docker Compose一拉,各服务互不干扰。
第二,一键复现。你在这台机器上跑通的配置,换台机器只要docker compose up就能复现。这对于团队协作和部署来说,省掉的是无数个“在我机器上是好的”的扯皮时间。
第三,资源控制。记忆系统里的向量检索是吃内存的,Docker可以限制每个容器的资源上限,避免一个服务把整台机器拖垮。
所以我的建议很直接:先把Docker环境搞稳,再谈Agent记忆。后面我会给出具体的Compose配置。
3. 核心细节解析与实操要点:记忆的写入、检索与遗忘
3.1 记忆写入:不是所有对话都值得记住
一个常见的误区是“把所有东西都存下来”。我踩过这个坑:早期版本把每一轮对话都写进向量库,结果检索出来的全是“好的”“谢谢”“明白了”这种废话,真正有用的信息被淹没在噪声里。
“hindsight”的核心思路是选择性写入。具体来说,我会在Agent的对话流程里加一个“记忆提取”步骤,用LLM来判断当前这轮交互是否包含值得长期记住的信息。判断标准可以归纳成三个问题:
- 这轮对话里有没有出现新的事实?(比如用户说“我对花生过敏”)
- 有没有出现明确的偏好?(比如用户说“以后回答尽量简短”)
- 有没有出现任务成败的关键教训?(比如某个API调用方式导致失败,换了一种方式成功)
如果三个都没有,那就只留在工作记忆里,不往长期记忆写。这个判断本身可以用一个轻量级的LLM调用完成,成本很低,但能大幅提升记忆库的信噪比。
代码层面,这个提取步骤大概长这样:
def extract_memory(conversation_turn: str) -> dict: prompt = f""" 分析以下对话片段,判断是否包含值得长期记忆的信息。 如果有,提取成JSON格式:{{"type": "fact|preference|lesson", "content": "..."}} 如果没有,返回 {{"type": "none"}}。 对话:{conversation_turn} """ response = llm_client.chat(prompt) return parse_json(response)注意:这个提取步骤的prompt要写得足够具体,否则LLM会把“今天天气不错”也当成事实存下来。我一般会在prompt里加几个反例,效果会好很多。
3.2 记忆检索:Token的三个点——Key、Query、Value
热词里有一条很有意思:“llm的token三个点key我是谁、query我在找什么、value我能提供什么”。这其实是在用通俗的方式解释注意力机制,但放在记忆检索里同样适用。你可以把记忆库想象成一个巨大的键值对集合:
- Key:这条记忆是关于什么的?比如“用户饮食偏好”“项目部署环境”“上次任务失败原因”。
- Query:当前情境下,Agent需要什么信息?比如用户问“今晚吃什么”,Query就是“饮食相关记忆”。
- Value:这条记忆的具体内容。比如“用户对花生过敏,偏好清淡口味”。
检索的过程,就是用Query去匹配Key,然后取出Value。在向量数据库里,Key和Query都被编码成向量,通过余弦相似度来匹配。但纯向量检索有个问题:它有时候会“语义漂移”。比如你查“部署问题”,它可能返回“部署成功”的记忆,因为语义太接近了。
我的做法是混合检索:向量相似度占70%,关键词匹配占30%。关键词匹配用BM25或者简单的全文索引就行。这样既能抓住语义,又能保证关键实体(比如具体的错误码、人名、项目名)不被漏掉。
3.3 记忆遗忘:为什么“忘掉”和“记住”一样重要
这一点很少有人提,但极其关键。记忆库如果只增不减,几个月后就会变成一个臃肿的垃圾场。检索延迟上升,噪声比例增加,最终拖垮整个Agent的响应质量。
“hindsight”里我设计了一个衰减机制:
- 每条记忆有一个“最后访问时间”和“访问次数”。
- 每次被检索到并实际用于生成回答,访问次数加一,最后访问时间更新。
- 定期(比如每天凌晨)跑一个清理任务,把超过30天未被访问且访问次数低于3次的记忆标记为“冷记忆”。
- 冷记忆不直接删除,而是压缩成更抽象的摘要,或者转移到更便宜的存储层。
这个机制的逻辑是:频繁被用到的记忆,说明它有价值;长期不用的记忆,要么过时了,要么本来就不重要。这跟人脑的记忆巩固机制很像——反复强化的记忆会变成长期记忆,不用的就慢慢淡忘。
4. 实操过程与核心环节实现:从零搭一套可跑的记忆系统
4.1 环境准备:Docker Compose一把梭
先把基础设施搭起来。下面这个docker-compose.yml是我在多个项目里验证过的配置,包含向量数据库(Qdrant)、关系型数据库(PostgreSQL)、缓存(Redis)和MCP Server:
version: '3.8' services: qdrant: image: qdrant/qdrant:latest ports: - "6333:6333" volumes: - ./data/qdrant:/qdrant/storage deploy: resources: limits: memory: 2G postgres: image: postgres:16 environment: POSTGRES_USER: agent POSTGRES_PASSWORD: agent_pass POSTGRES_DB: memory ports: - "5432:5432" volumes: - ./data/postgres:/var/lib/postgresql/data redis: image: redis:7-alpine ports: - "6379:6379" volumes: - ./data/redis:/data memory-mcp: build: ./memory-mcp ports: - "8080:8080" depends_on: - qdrant - postgres - redis environment: QDRANT_HOST: qdrant POSTGRES_HOST: postgres REDIS_HOST: redis提示:如果你在Windows上跑,Docker Desktop的WSL2后端记得开启。热词里有人遇到“virtualization support not detected”,那基本就是BIOS里的虚拟化没开,或者Hyper-V冲突了。先去BIOS开VT-x/AMD-V,再检查Windows功能里有没有勾选“虚拟机平台”。
启动命令就一句:
docker compose up -d等所有容器healthy之后,用docker compose ps确认一下状态。如果某个容器反复重启,先看日志:docker compose logs qdrant。
4.2 记忆MCP Server的核心实现
MCP Server的作用是暴露几个标准接口给Agent调用。我用Python写一个最小实现,基于mcp库:
from mcp.server import Server from mcp.types import Tool, TextContent import qdrant_client import redis import psycopg2 app = Server("memory-server") qdrant = qdrant_client.QdrantClient(host="qdrant", port=6333) redis_client = redis.Redis(host="redis", port=6379, decode_responses=True) @app.tool() async def write_memory(type: str, content: str, metadata: dict) -> str: """写入一条长期记忆""" vector = embed(content) # 调用embedding模型 qdrant.upsert( collection_name="long_term_memory", points=[{ "id": generate_id(), "vector": vector, "payload": {"type": type, "content": content, **metadata} }] ) return "记忆已写入" @app.tool() async def search_memory(query: str, top_k: int = 5) -> list: """检索相关记忆""" query_vector = embed(query) results = qdrant.search( collection_name="long_term_memory", query_vector=query_vector, limit=top_k ) return [{"content": r.payload["content"], "score": r.score} for r in results]这个Server跑起来后,Agent只需要通过MCP协议调用write_memory和search_memory两个工具,就能完成记忆的读写。具体的MCP客户端接入方式,取决于你用的Agent框架,但核心逻辑是一样的。
4.3 记忆写入的触发时机与参数选择
写入时机我一般放在两个地方:
- 对话结束时:整段会话结束后,跑一次批量提取,把值得记的挑出来。
- 关键事件发生时:比如用户明确说“记住这个”,或者任务执行失败时,立即触发写入。
参数方面,有几个经验值可以参考:
| 参数 | 建议值 | 说明 |
|---|---|---|
| embedding维度 | 768或1024 | 取决于模型,768够用且省存储 |
| 相似度阈值 | 0.75 | 低于这个值的检索结果直接丢弃 |
| top_k | 3-5 | 太多会稀释prompt,太少可能漏信息 |
| 记忆过期天数 | 30天 | 冷记忆清理的默认阈值 |
注意:embedding模型的选择很关键。我试过用OpenAI的text-embedding-3-small,效果稳定但成本不低;也试过本地的bge-m3,中文场景下表现很好,而且免费。如果你的数据敏感,建议用本地模型。
4.4 检索结果如何注入Prompt
检索出来的记忆不能直接一股脑塞进prompt,那样会干扰模型的判断。我的做法是结构化注入:
[相关记忆] - 用户偏好:回答尽量简短,不要用列表(来源:2024-01-15) - 历史教训:调用XX API时需要先获取token,否则会返回401(来源:2024-01-20) - 事实:用户的项目使用Python 3.11,部署在K8s上(来源:2024-01-10)然后在系统prompt里加一句:“以上是历史记忆中与当前问题相关的信息,请参考但不要盲从,如果与当前对话冲突,以当前对话为准。”这句话很重要,它给了模型一个“优先级判断”的依据,避免旧记忆覆盖新信息。
5. 常见问题与排查技巧实录
5.1 记忆检索不准:先查Embedding,再查分块
检索不准是最常见的问题。排查顺序我一般是:
- 看Embedding质量:把Query和几条已知相关的记忆拿出来,手动算一下余弦相似度。如果相关记忆的相似度低于0.6,那基本是Embedding模型的问题,换模型或者微调。
- 看分块策略:如果一条记忆太长(比如超过500字),Embedding会丢失细节。我一般会把长记忆拆成200-300字的块,每块单独存,但保留同一个
parent_id,检索时可以把同一父块的记忆合并返回。 - 看是否该用混合检索:纯向量检索对实体名不敏感。如果用户经常问“XX项目怎么样了”,而XX项目在记忆里是以“Project Alpha”存储的,向量检索可能匹配不上。加关键词索引就能解决。
5.2 Docker网络不通:90%是端口和网段问题
热词里“docker网络不通”出现多次,我分享一下排查套路:
- 先
docker compose ps看容器是否都在运行。 - 再
docker exec -it <container> ping <other_container>,如果ping不通,说明不在同一网络。Compose默认会创建一个bridge网络,所有服务应该能互相访问。如果不行,检查docker-compose.yml里有没有手动指定network_mode。 - 如果容器间能通,但宿主机访问不了,检查端口映射。比如Qdrant的6333端口,
ports: "6333:6333",前面是宿主机端口,后面是容器端口。 - 如果宿主机端口被占用,
docker compose up会报错。用netstat -ano | findstr 6333(Windows)或lsof -i:6333(Mac/Linux)查一下。
提示:Windows上Docker Desktop有时候会有端口转发延迟,重启一下Docker Desktop通常能解决。
5.3 记忆写入失败:检查数据库连接和向量维度
写入失败一般报错在日志里能看到。常见原因:
- Qdrant collection不存在:第一次跑的时候需要先创建collection,指定向量维度和距离度量。我一般会在MCP Server启动时自动检查并创建。
- 向量维度不匹配:如果你换了Embedding模型,维度从768变成1024,但collection还是768,就会报错。解决办法是删掉collection重建,或者做一次全量重新embedding。
- PostgreSQL连接池耗尽:如果写入频率很高,连接池可能不够用。把
max_connections调大,或者用连接池中间件。
5.4 常见问题速查表
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| 检索结果全是无关内容 | Embedding模型不适合当前语言/领域 | 换模型或微调 |
| 记忆库增长过快 | 没有选择性写入 | 加提取步骤,过滤噪声 |
| 检索延迟越来越高 | 记忆库太大,没有清理机制 | 加衰减和冷记忆清理 |
| Docker容器反复重启 | 内存不足或配置错误 | 看日志,调资源限制 |
| MCP工具调用超时 | Server处理太慢或网络问题 | 加缓存,优化检索逻辑 |
| 记忆内容与当前对话冲突 | 没有优先级提示 | 在prompt里加冲突处理规则 |
6. 一些踩坑之后的个人体会
这套东西我在几个项目里反复迭代过,最大的体会是:Agent记忆的难点不在“存”,而在“取”和“忘”。存数据谁都会,但怎么在正确的时间取出正确的记忆,怎么让不重要的记忆自然淡出,这才是真正影响体验的地方。
另一个体会是,不要追求一步到位。我一开始就想做一个“全自动”的记忆系统,结果复杂度失控,调试成本极高。后来改成“半自动”:写入时用规则+LLM判断,检索时用混合策略,清理时用简单的衰减公式。每一步都留了人工干预的接口,反而跑得更稳。
最后分享一个小技巧:在开发阶段,我会给每条记忆加一个debug_info字段,记录它是从哪次对话、哪个提取步骤来的。这样当检索结果不对劲时,我能快速回溯到源头,判断是提取错了还是检索错了。这个字段在生产环境可以关掉,但开发阶段能省很多时间。
如果你也在做类似的事情,建议先从工作记忆和短期记忆做起,把长期记忆的写入频率降下来,观察一段时间再逐步放开。记忆系统跟人一样,先学会记眼前的事,再学会记一辈子的事。