1. 从“hindsight”说起:为什么我们需要给Agent装上“后视镜”
第一次看到“hindsight”这个词,我脑子里蹦出来的不是词典释义,而是自己踩过的一个坑。去年做客服Agent项目,用户反馈“上周明明告诉过你们我的订单号”,结果Agent一脸无辜地重新问了一遍。那一刻我意识到,大多数Agent的“记忆”其实是个摆设——它们只有working memory,会话一断,全忘光。hindsight要解决的,就是这个“事后才想起来”的尴尬。
hindsight在英文里是“后见之明”,放在Agent语境下,我把它理解成一套让Agent能够回溯、检索、复用历史交互的长期记忆机制。它和传统的RAG不一样:RAG是“我有一堆文档,你来查”,hindsight是“我自己经历过的事,我得记住,下次遇到类似情况能想起来”。这中间的差别,就像你查百科和回忆自己昨天干了什么的差别。
这套东西适合谁?如果你正在做多轮对话Agent、任务型Agent、或者任何需要“记住用户偏好和历史操作”的LLM应用,hindsight就是你必须啃下来的骨头。它涉及的核心技术点包括:Agent Memory分层架构、MCP协议做工具调用、Docker做环境隔离、以及LLM驱动的记忆压缩与检索。下面我会把这几个点串起来,讲清楚怎么从零搭一套能用的hindsight记忆系统。
提示:本文所有代码和配置都基于我实际跑通的方案,环境是Ubuntu 22.04 + Docker 24.0 + Python 3.11。如果你用Windows,Docker Desktop的坑我会单独说。
2. 核心架构拆解:hindsight记忆系统到底怎么分层
2.1 三层记忆模型:working memory、episodic memory、semantic memory
我试过直接把所有对话历史塞进context window,结果token爆炸,模型还经常“抓错重点”。后来参考认知科学的分类,把hindsight拆成三层:
- Working Memory:当前会话的短期上下文,就是常规的messages数组。生命周期是单次会话,会话结束就清空。这层不需要额外存储,直接放在内存里。
- Episodic Memory:情景记忆,记录“什么时间、什么场景、发生了什么”。比如“2024-03-15 14:23,用户A询问订单12345的物流状态,Agent调用了物流查询API”。这层需要持久化,我选的是PostgreSQL + pgvector,既能存结构化字段,又能做向量检索。
- Semantic Memory:语义记忆,从情景记忆里抽象出来的“事实”和“偏好”。比如“用户A偏好顺丰快递”“用户A的订单号格式是1开头”。这层是压缩后的知识,用向量库存储,检索时优先查这层。
为什么要分三层?因为检索成本和精度不一样。Working memory零成本但容量有限;Episodic memory全量但检索慢;Semantic memory是精华但需要定期提炼。实际运行时,Agent先查Semantic,命中就直接用;没命中再查Episodic,做相似度检索;最后才把Working memory拼进prompt。
2.2 记忆写入的触发时机:不是每句话都值得记
我一开始犯的错是“每轮对话都写库”,结果数据库膨胀得飞快,检索噪声也大。后来改成事件驱动写入,只在以下几种情况触发:
- 用户明确表达了偏好(“我喜欢用微信联系”)
- Agent执行了工具调用并拿到结果(“查询到订单状态为已发货”)
- 会话结束时的总结(用LLM把整段对话压缩成3-5条要点)
- 用户主动纠正Agent(“不对,我的地址是XXX”)
这四种情况覆盖了90%的有价值记忆。其他寒暄、确认类对话,直接丢弃。实测下来,写入量减少了70%,检索准确率反而提升了。
2.3 MCP协议在hindsight里的角色:让记忆变成可调用的工具
MCP(Model Context Protocol)是我今年最看好的Agent基础设施。在hindsight里,我把记忆的读写封装成MCP Server,这样任何支持MCP的Agent框架都能直接调用,不用改代码。
具体来说,我写了三个MCP Tool:
memory_write:写入一条记忆,参数包括content、memory_type、metadatamemory_search:检索记忆,参数包括query、top_k、memory_type_filtermemory_summarize:触发LLM对当前会话做总结并写入semantic memory
这样设计的好处是解耦。Agent框架只管调用工具,记忆的存储、检索、压缩逻辑全在MCP Server里。我换过三次向量库(从Chroma到Qdrant再到pgvector),Agent侧一行代码没改。
# memory_mcp_server.py 核心片段 from mcp.server import Server from mcp.types import Tool, TextContent import asyncpg app = Server("hindsight-memory") @app.list_tools() async def list_tools(): return [ Tool(name="memory_write", description="写入一条Agent记忆", inputSchema={"type":"object","properties":{ "content":{"type":"string"}, "memory_type":{"type":"string","enum":["episodic","semantic"]}, "metadata":{"type":"object"}}, "required":["content","memory_type"]}), Tool(name="memory_search", description="检索相关记忆", inputSchema={"type":"object","properties":{ "query":{"type":"string"}, "top_k":{"type":"integer","default":5}}, "required":["query"]}) ] @app.call_tool() async def call_tool(name, arguments): if name == "memory_write": await write_memory(arguments) return [TextContent(type="text", text="记忆已写入")] elif name == "memory_search": results = await search_memory(arguments["query"], arguments.get("top_k",5)) return [TextContent(type="text", text=format_results(results))]这段代码跑起来后,Agent侧只需要在system prompt里加一句“你可以调用memory_search来回忆历史交互”,剩下的交给MCP协议。
3. 环境搭建实操:Docker + PostgreSQL + MCP Server全流程
3.1 Docker环境准备:避开Windows下的虚拟化坑
我主力开发机是Ubuntu,但帮朋友在Windows上配过一次,踩了个经典坑:Virtualization support not detected。Docker Desktop启动失败,原因是BIOS里没开虚拟化。解决办法:
- 重启进BIOS,找到Intel VT-x或AMD-V,设为Enabled
- Windows功能里勾选“Hyper-V”和“虚拟机平台”
- 如果还报错,检查是否装了WSL2,
wsl --update更新内核
Ubuntu下就简单多了,一行命令:
# 安装Docker curl -fsSL https://get.docker.com | sh sudo usermod -aG docker $USER # 重新登录后验证 docker run hello-world注意:国内网络拉镜像慢的话,配置镜像加速器。编辑
/etc/docker/daemon.json,加入registry-mirrors,然后sudo systemctl restart docker。
3.2 用Docker Compose一键拉起PostgreSQL + pgvector
hindsight的存储层我选PostgreSQL + pgvector,原因是一个数据库同时搞定结构化查询和向量检索,不用维护两套系统。Docker Compose文件如下:
version: '3.8' services: postgres: image: pgvector/pgvector:pg16 container_name: hindsight-db environment: POSTGRES_USER: hindsight POSTGRES_PASSWORD: hindsight123 POSTGRES_DB: memory ports: - "5432:5432" volumes: - ./pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U hindsight"] interval: 10s timeout: 5s retries: 5启动命令:docker compose up -d。等healthcheck变绿后,进容器建表:
CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE episodic_memory ( id BIGSERIAL PRIMARY KEY, content TEXT NOT NULL, embedding vector(1536), metadata JSONB, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX ON episodic_memory USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100); CREATE TABLE semantic_memory ( id BIGSERIAL PRIMARY KEY, fact TEXT NOT NULL, embedding vector(1536), confidence FLOAT DEFAULT 1.0, updated_at TIMESTAMPTZ DEFAULT NOW() );这里vector(1536)对应OpenAI text-embedding-3-small的维度。如果你用其他embedding模型,改这个数字就行。ivfflat索引的lists参数,我一般设成数据量的平方根,100万条以下用100够用。
3.3 MCP Server的Docker化部署
MCP Server我也打包成Docker镜像,这样Agent框架无论跑在哪,都能通过stdio或SSE连接。Dockerfile:
FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD ["python", "memory_mcp_server.py"]requirements.txt里关键依赖:mcp、asyncpg、openai、pgvector。构建并运行:
docker build -t hindsight-mcp:latest . docker run -d --name hindsight-mcp \ --network host \ -e DATABASE_URL=postgresql://hindsight:hindsight123@localhost:5432/memory \ -e OPENAI_API_KEY=sk-xxx \ hindsight-mcp:latest提示:
--network host在Linux下让容器直接用宿主机网络,省去端口映射的麻烦。Windows/Mac下用-p 8080:8080并改用SSE传输。
4. 记忆的写入、检索与压缩:核心逻辑实现
4.1 写入逻辑:LLM驱动的记忆抽取
原始对话不能直接存,得先让LLM抽取“值得记”的部分。我用的prompt模板:
你是一个记忆抽取器。从以下对话中提取需要长期记住的信息,按JSON数组返回。 每条记忆包含:content(一句话描述)、type(episodic或semantic)、importance(1-5)。 对话: {conversation} 只返回JSON,不要解释。拿到LLM返回后,对每条记忆做embedding,然后写入对应表。这里有个细节:episodic memory存原始描述,semantic memory存抽象事实。比如对话里用户说“我上次用顺丰寄到上海花了23块”,episodic存“用户2024-03-10用顺丰寄上海,费用23元”,semantic存“用户偏好顺丰,寄上海费用约23元”。
4.2 检索逻辑:三路召回 + 重排序
检索时我并行跑三路:
- 向量检索:query embedding后,在semantic_memory里找cosine相似度top 10
- 关键词检索:用PostgreSQL的
to_tsvector做全文检索,top 10 - 时间衰减检索:最近7天的episodic memory,按created_at倒序取10条
三路结果合并去重后,用一个小型cross-encoder做重排序,取top 5拼进prompt。实测下来,比单纯向量检索的命中率高35%。
async def hybrid_search(query: str, top_k: int = 5): query_emb = await get_embedding(query) # 向量召回 vector_results = await db.fetch(""" SELECT id, fact as content, 1 - (embedding <=> $1) as score FROM semantic_memory ORDER BY embedding <=> $1 LIMIT 10 """, query_emb) # 关键词召回 keyword_results = await db.fetch(""" SELECT id, content, ts_rank(to_tsvector(content), plainto_tsquery($1)) as score FROM episodic_memory WHERE to_tsvector(content) @@ plainto_tsquery($1) LIMIT 10 """, query) # 合并重排 merged = merge_and_rerank(vector_results, keyword_results) return merged[:top_k]4.3 压缩逻辑:定期把episodic提炼成semantic
我写了个定时任务,每天凌晨跑一次:取过去24小时新增的episodic memory,按用户ID分组,每组让LLM总结成3-5条semantic fact,写入semantic_memory,然后给对应的episodic打上compressed=true标记。
这个压缩过程很关键,它让记忆库不会无限膨胀。我试过不压缩跑一个月,episodic表到了50万行,检索延迟从80ms涨到1.2s。压缩后稳定在5万行左右,延迟回到100ms以内。
注意:压缩时一定要保留原始episodic的引用ID,方便追溯。我吃过亏,用户投诉“你记错了”,结果发现是压缩时LLM幻觉了,但原始记录已经删了,没法对账。
5. 常见问题与排查技巧实录
5.1 Docker网络不通:容器间通信的三种排查思路
最常见的问题是MCP Server容器连不上PostgreSQL容器。排查顺序:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
connection refused | 端口没映射或网络模式不对 | 检查docker network ls,确保两容器在同一network |
timeout | 防火墙或bind地址不对 | PostgreSQL的postgresql.conf里listen_addresses='*' |
password authentication failed | 环境变量没生效 | docker exec进容器env检查 |
我一般用docker compose统一管理,所有服务在同一个默认network里,用服务名当hostname,省心。
5.2 LLM request failed: provider rejected the request schema
这个报错我遇到三次,每次原因不一样:
- MCP Tool的inputSchema写错了:比如
required字段拼成require,或者type写成str而不是string。MCP协议对schema校验很严,一个字段不对就拒。 - embedding维度不匹配:数据库建表时vector(1536),但换了embedding模型输出1024维,插入时报错。解决办法是统一模型,或者建表时用
vector不指定维度。 - token超限:检索回来的记忆太多,拼进prompt超过模型context。我加了截断逻辑,按score排序,累加token数到阈值就停。
5.3 记忆检索“答非所问”:重排序的重要性
早期版本我只用向量检索,经常出现“用户问订单,检索出来的是用户上次问的天气”。原因是向量模型对短文本的语义区分度不够。加了cross-encoder重排序后,准确率明显提升。重排序模型我用的是BAAI/bge-reranker-base,本地跑,延迟增加约50ms,但值得。
另一个技巧是在query里注入意图。不要直接拿用户原话去检索,而是让LLM先改写:“用户问订单状态,检索关键词:订单、物流、发货”。这样召回的相关性高很多。
5.4 记忆冲突:新旧信息不一致怎么办
用户上周说“我住北京”,这周说“我搬上海了”。两条semantic memory冲突。我的处理策略是时间戳优先 + 置信度衰减。检索时如果发现同一subject有多条fact,取最新的;同时给旧fact的confidence乘以0.8,低于0.3就归档不参与检索。
这个逻辑写在检索层,不删数据,保留追溯能力。实测下来,用户很少投诉“你记错了”。
6. 一些实操心得和后续扩展方向
跑通这套hindsight系统后,我最大的体会是:Agent记忆不是“存得越多越好”,而是“该记的记,该忘的忘”。我见过太多项目把全部对话历史塞进向量库,结果检索噪声大到没法用。三层记忆模型 + 事件驱动写入 + 定期压缩,这套组合拳打下来,记忆库始终保持在可控规模。
另一个心得是MCP协议真的能省事。以前每换一个Agent框架就要重写记忆模块,现在MCP Server一次写好,LangChain、AutoGen、甚至自己写的Agent都能接。Docker化部署后,迁移环境就是docker compose up的事。
后续我打算扩展两个方向:一是记忆的可视化面板,用Grafana接PostgreSQL,实时看记忆增长和检索命中率;二是多Agent共享记忆,让多个Agent通过同一个MCP Server读写记忆,实现团队协作。这两个方向跑通后,hindsight就不只是单个Agent的“后视镜”,而是整个Agent集群的“集体记忆”了。
最后分享一个小技巧:调试记忆检索时,把每次检索的query、召回结果、最终拼进prompt的内容都打到日志里。我靠这个日志发现了好几个隐蔽的bug,比如embedding缓存没更新导致检索到旧数据。日志级别设DEBUG,生产环境关掉就行。