1. 从“hindsight”这个词说起:为什么它值得单独拿出来聊
“hindsight”这个词本身的意思是“事后之明”,也就是回头看的时候才明白当时应该怎么做。把这个词放到 LLM Agent 的语境里,它指向的东西就非常具体了:Agent 在完成任务之后,如何把“刚才发生了什么”沉淀成可复用的记忆,而不是每次对话都从零开始。
我最初注意到这个词,是因为在调试一个基于 MCP 协议的多轮 Agent 时,发现一个很尴尬的现象——同一个会话里,Agent 能记住用户三分钟前说过的话;但只要会话一断,或者换一个任务入口,它就像失忆一样,把之前所有的上下文、工具调用结果、失败尝试全部丢掉。用户不得不反复重复背景信息,Agent 也不断重复犯同样的错误。
这就是“hindsight”要解决的核心问题:让 Agent 具备事后回看、提炼、存储、再调用的能力。它不是一个具体的库或者框架,而是一种设计思路——把 Agent 的执行轨迹当作一等公民来对待,在任务结束后主动做一次“复盘”,把有价值的片段写进长期记忆,下次遇到相似场景时能直接命中。
关键词里出现的agent memory、LLM、MCP、Docker四个词,基本勾勒出了这个方向的完整技术栈:LLM 是推理内核,MCP 是工具调用和上下文交换的协议层,Docker 是运行环境的隔离手段,而 agent memory 是最终要落地的能力。这篇文章我会围绕这四个点,把“hindsight”这个思路从概念到落地讲透,包括我自己踩过的坑和目前跑得比较稳的一套做法。
适合读这篇的人:正在做 Agent 长期记忆的开发者、被多轮上下文丢失问题困扰的工程师、想用 MCP 把工具链串起来但不知道记忆层怎么设计的人。如果你只是刚听说 LLM,还没写过 Agent,建议先补一下基础的工具调用和会话管理,再回来看这篇会更顺。
2. hindsight 思路下的 Agent 记忆分层:不是所有东西都值得记
2.1 为什么“全量存上下文”是一条死路
很多人做 Agent 记忆的第一反应是:把每轮对话、每次工具调用结果全部塞进向量库,需要的时候检索出来拼进 prompt。这个做法在 demo 阶段能跑通,但一上真实场景就会崩。原因有三个,而且都是硬伤。
第一是噪声爆炸。Agent 执行一个任务可能产生几十次工具调用,其中大量是中间状态、失败重试、无关的探测。这些内容全存进去,检索时命中的概率反而下降,因为真正有用的那几条被淹没了。
第二是token 成本失控。LLM 的上下文窗口再大也是有上限的,而且长上下文带来的推理成本是线性甚至超线性增长的。你不可能每次对话都把历史全量喂进去。
第三是语义漂移。原始的执行日志是面向过程的,而记忆应该是面向结果的。把过程日志直接当记忆用,Agent 下次检索到的是一堆“我当时调用了什么”,而不是“我学到了什么”。
所以 hindsight 的第一个设计原则就是:记忆不是日志,记忆是提炼后的结论。任务执行过程中的原始轨迹可以短期保留用于调试,但进入长期记忆的必须是经过压缩、抽象、去噪的版本。
2.2 三层记忆结构:working memory、episodic memory、semantic memory
参考认知科学里对记忆的分类,我在实际项目里把 Agent 记忆分成三层,每层的生命周期、存储介质、检索方式都不一样。
| 层级 | 生命周期 | 存储方式 | 典型内容 | 检索触发 |
|---|---|---|---|---|
| Working Memory | 单次会话内 | 内存 / 会话上下文 | 当前任务目标、最近几轮对话、临时变量 | 每轮自动携带 |
| Episodic Memory | 数天到数周 | 结构化数据库 + 向量索引 | 任务执行摘要、成功/失败模式、工具调用序列 | 相似任务触发 |
| Semantic Memory | 长期 | 向量库 + 知识图谱 | 领域事实、用户偏好、稳定结论 | 语义相似度检索 |
Working Memory就是当前会话的上下文窗口,它解决的是“这一轮对话里我要知道什么”。这部分不需要持久化,会话结束就释放。但要注意,working memory 的裁剪策略很关键——不是简单保留最近 N 轮,而是按重要性加权,把任务目标、关键约束、已确认的事实优先保留。
Episodic Memory是 hindsight 的核心。每次任务结束后,Agent 应该主动生成一份“执行摘要”,记录:任务目标是什么、用了哪些工具、哪些步骤成功了、哪些失败了、失败原因是什么、最终结果如何。这份摘要用结构化格式存下来,同时把摘要文本做 embedding 存进向量库。下次遇到相似任务时,先检索历史 episodic memory,把相关的几条注入 working memory 作为参考。
Semantic Memory是最稳定的一层,存的是从多次 episodic memory 中归纳出来的通用知识。比如“用户偏好用 Python 而不是 JavaScript”“这个 API 在并发超过 10 的时候会限流”“某类任务的正确执行顺序是先 A 后 B”。这层记忆的更新频率最低,但价值最高。
2.3 三层之间的流转:hindsight 的“复盘”动作发生在哪里
关键点来了:hindsight 的核心动作,就是在任务结束时,把 working memory 里的执行轨迹提炼成 episodic memory,并定期把 episodic memory 归纳成 semantic memory。
这个“复盘”动作可以是一个独立的 LLM 调用,给它一段执行日志,让它输出结构化摘要。提示词大概长这样:
你是一个 Agent 执行复盘助手。下面是一次任务的完整执行轨迹。 请输出 JSON 格式的复盘结果,包含以下字段: - task_goal: 任务目标的一句话描述 - tools_used: 使用过的工具列表 - success_steps: 成功的关键步骤 - failure_steps: 失败的步骤及原因 - final_result: 最终结果 - reusable_insight: 可复用的经验(如果有) - confidence: 你对这次复盘的置信度(0-1)这个复盘调用本身也消耗 token,所以不是每次任务都值得做。我的做法是设置一个触发条件:任务执行超过 5 步、或者涉及工具调用超过 3 次、或者用户明确表示“记住这个”,才触发复盘。简单的一问一答不触发,避免浪费。
3. MCP 在记忆链路里的位置:它不只是工具调用协议
3.1 MCP 解决的是“上下文交换”而不是“记忆存储”
很多人对 MCP 的理解停留在“让 LLM 调用外部工具”,这没错,但不够。MCP 的全称是 Model Context Protocol,它的核心价值在于标准化了模型和外部系统之间的上下文交换格式。这个“上下文”既包括工具调用的输入输出,也包括资源的读取、提示模板的传递。
在 hindsight 的记忆链路里,MCP 扮演的是记忆读写通道的角色。Agent 通过 MCP server 暴露的接口来写入 episodic memory、查询 semantic memory,而不是把记忆逻辑硬编码在 Agent 内部。这样做的好处是记忆层可以独立演进,换一个记忆后端不需要改 Agent 代码。
一个典型的 MCP 记忆服务会暴露这几个工具:
memory_write: 写入一条记忆,参数包括内容、类型、标签、置信度memory_search: 按语义相似度检索记忆,参数包括查询文本、返回条数、类型过滤memory_summarize: 触发一次复盘,把原始轨迹压缩成摘要memory_forget: 删除或降权某条记忆
这些工具通过 MCP 协议注册后,Agent 在需要的时候就能像调用普通工具一样调用它们。关键设计点是:记忆的写入和检索应该是 Agent 的显式动作,而不是隐式的副作用。隐式写入会导致记忆污染,你根本不知道什么东西被存进去了。
3.2 用 MCP 做记忆层的一个实际配置
下面是我目前在用的一个 MCP 记忆服务的配置片段,基于 stdio 传输方式,跑在本地 Docker 容器里:
{ "mcpServers": { "agent-memory": { "command": "docker", "args": [ "run", "-i", "--rm", "-v", "agent-memory-data:/data", "-e", "MEMORY_BACKEND=chroma", "-e", "MEMORY_DB_PATH=/data/chroma", "agent-memory-server:latest" ] } } }这个配置里几个点值得说明。-v挂载了一个命名卷,保证容器重启后记忆数据不丢。MEMORY_BACKEND指定用 Chroma 做向量存储,换成其他后端只需要改这个环境变量。-i是必须的,因为 MCP 的 stdio 传输需要保持标准输入打开。
Agent 侧连接这个 MCP server 之后,就能在对话中调用memory_search来检索历史经验。我实测下来,在 prompt 里注入 3 到 5 条相关记忆,对任务成功率的提升最明显,超过 5 条之后边际收益递减,反而增加 token 负担。
3.3 MCP 记忆服务的 Docker 化:为什么强烈建议这么做
把记忆服务跑在 Docker 里,不是为了赶时髦,而是有三个实打实的好处。
环境一致性。记忆服务依赖向量库、embedding 模型、数据库驱动,这些东西版本冲突起来非常难查。Docker 镜像一旦构建好,在任何机器上跑出来的行为都一样。
数据隔离。记忆数据是 Agent 的核心资产,用 volume 挂载出来,容器删了数据还在。而且可以针对不同项目挂不同的 volume,避免记忆串味。
资源控制。embedding 计算和向量检索是吃内存的,用 Docker 可以限制内存和 CPU,防止记忆服务把宿主机拖垮。我一般给记忆容器分配 2GB 内存上限,对于中小规模记忆库足够用了。
构建镜像的 Dockerfile 大概是这样:
FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . ENV MEMORY_DB_PATH=/data/chroma VOLUME ["/data"] CMD ["python", "-m", "memory_server", "--transport", "stdio"]requirements.txt里主要是chromadb、sentence-transformers、mcp这几个包。构建一次大概 3 到 5 分钟,取决于网络。构建好之后启动是秒级的。
4. 把 hindsight 跑起来:从零搭建一套可用的 Agent 记忆系统
4.1 环境准备:Docker 和 Python 环境的几个坑
先说 Docker 这块。Windows 上装 Docker Desktop 最常见的问题是虚拟化没开,报错信息里会出现Virtualization support not detected。解决办法是进 BIOS 打开 VT-x 或 AMD-V,然后在 Windows 功能里确认 WSL2 或者 Hyper-V 至少开了一个。我建议用 WSL2 后端,资源占用比 Hyper-V 小,和 Linux 工具链的兼容性也更好。
装完之后验证一下:
docker --version docker run hello-world第二条命令能正常输出就说明 Docker 跑通了。如果卡在拉镜像,检查一下 Docker Desktop 的代理设置,公司网络环境下这一步经常出问题。
Python 环境我建议用 3.11,因为mcp这个包对 3.10 以下的支持不太稳定。用 venv 隔离:
python -m venv agent-memory-env source agent-memory-env/bin/activate # Windows 用 agent-memory-env\Scripts\activate pip install mcp chromadb sentence-transformers这里有个坑:sentence-transformers第一次运行会下载 embedding 模型,大概几百 MB。如果网络不好,可以提前把模型下载到本地,然后用SENTENCE_TRANSFORMERS_HOME环境变量指过去。
4.2 记忆写入:什么时候写、写什么、怎么写
记忆写入的时机比写入的内容更重要。写早了,任务还没结束,记的是半成品;写晚了,working memory 已经被裁剪,细节丢了。
我的做法是在任务状态机进入终态时触发写入。所谓终态,就是任务成功完成、明确失败、或者用户主动中断。这三种情况都值得复盘,但复盘的重点不同:
- 成功完成:重点记录可复用的步骤序列和工具组合
- 明确失败:重点记录失败原因和排除掉的错误路径
- 用户中断:重点记录中断时的上下文,方便下次续接
写入的内容用结构化 JSON,不要直接存自然语言。结构化之后检索和过滤都方便。一个实际的记忆条目长这样:
{ "id": "mem_20250115_001", "type": "episodic", "task_goal": "从 CSV 文件读取销售数据并生成月度汇总报告", "tools_used": ["file_read", "pandas_query", "chart_generate"], "success_steps": [ "先用 file_read 读取 CSV,确认列名和编码", "用 pandas_query 做 groupby 月度聚合", "用 chart_generate 生成柱状图" ], "failure_steps": [ "第一次直接读取时编码识别错误,改用 utf-8-sig 解决" ], "reusable_insight": "处理中文 CSV 时优先尝试 utf-8-sig 编码", "confidence": 0.85, "timestamp": "2025-01-15T10:30:00Z", "tags": ["data-processing", "csv", "encoding"] }注意reusable_insight这个字段,它是 hindsight 价值的集中体现。这条记忆下次被检索到时,Agent 直接就知道“中文 CSV 用 utf-8-sig”,不用再踩一遍编码的坑。
4.3 记忆检索:相似度不是唯一指标
检索记忆的时候,很多人只用向量相似度,这是不够的。相似度高不代表有用。我实际用下来,检索排序应该综合考虑三个因素:
- 语义相似度:查询文本和记忆内容的 embedding 余弦相似度,权重 0.5
- 时间衰减:越新的记忆权重越高,用指数衰减,半衰期设 7 天,权重 0.3
- 置信度:记忆条目自身的 confidence 字段,权重 0.2
最终得分是三者加权和。这样能避免检索出一堆语义相似但已经过时或者本身就不靠谱的记忆。
检索的返回条数也要控制。我一般返回 top 5,然后在注入 prompt 之前再做一次筛选,把相似度低于 0.6 的丢掉。如果一条都没剩下,就不注入记忆,让 Agent 从零开始,避免被弱相关记忆误导。
def retrieve_memories(query, top_k=5, min_score=0.6): query_embedding = embed(query) candidates = vector_store.search(query_embedding, top_k=top_k * 3) scored = [] for mem in candidates: sim = cosine_similarity(query_embedding, mem.embedding) age_days = (now() - mem.timestamp).days time_weight = 0.5 ** (age_days / 7) score = 0.5 * sim + 0.3 * time_weight + 0.2 * mem.confidence if score >= min_score: scored.append((score, mem)) scored.sort(reverse=True) return [mem for _, mem in scored[:top_k]]这段代码里top_k * 3是先粗召回再精排的思路,避免直接 top_k 检索漏掉一些综合得分高的记忆。
4.4 记忆更新与遗忘:不清理的记忆库会变成垃圾场
记忆库不是只写不删的。用久了之后,过时的、矛盾的、低质量的记忆会越积越多,检索质量直线下降。必须有一套清理机制。
我的清理策略分三种:
过期清理。episodic memory 默认保留 30 天,超过之后如果没被检索命中过,就降权;60 天还没命中,直接删除。semantic memory 不设过期,但每次更新时如果发现和已有记忆矛盾,走冲突解决流程。
冲突解决。当新记忆和旧记忆在同一个主题上给出不同结论时,不能简单覆盖。我的做法是保留两条,但在检索时优先返回置信度高的那条,同时把冲突标记出来。如果同一个结论被多次验证,置信度累加;如果被多次推翻,置信度衰减。
容量控制。给记忆库设一个上限,比如 10000 条。超过之后按综合得分(相似度使用频率 + 置信度 + 时间新鲜度)淘汰最低的一批。这个上限根据你的向量库性能和检索延迟来定,Chroma 在 10000 条量级下检索延迟还能控制在 100ms 以内。
5. 实测中遇到的几个真问题:hindsight 不是银弹
5.1 记忆污染:Agent 把错误结论写进了长期记忆
这是我最开始踩的最大的坑。有一次 Agent 在调用某个 API 时因为网络超时失败了,复盘时它把“这个 API 不可用”写进了记忆。结果后面几天,所有涉及这个 API 的任务,Agent 检索到这条记忆后都直接跳过,导致大量任务失败。
根因是复盘时没有区分临时性失败和永久性失败。网络超时是临时的,API 下线才是永久的。复盘提示词里必须明确要求区分这两类,并且临时性失败不应该写入长期记忆,最多写进 episodic memory 并标记为“待验证”。
修复后的做法是在复盘输出里增加一个failure_type字段,取值transient或permanent。只有permanent的失败才进入 semantic memory,transient的只留在 episodic 层,并且 24 小时后自动降权。
5.2 检索延迟:记忆库大了之后,每次对话都卡
记忆库到几千条之后,每次对话前的检索开始变得明显。Chroma 默认的检索是暴力搜索,数据量大了之后延迟上来了。解决办法有两个:一是换用支持 HNSW 索引的向量库,二是把检索做成异步的,不阻塞主对话流程。
我最后用的是异步方案:对话开始时先不等检索结果,直接开始生成;检索在后台跑,结果出来之后如果还没生成完,就动态注入。这样用户感知不到延迟。代价是第一次回复可能用不上记忆,但第二次之后就能用上,实际体验可以接受。
5.3 多 Agent 场景下的记忆共享:谁写谁读
当系统里有多个 Agent 时,记忆共享就复杂了。A Agent 写的记忆,B Agent 能不能读?我的经验是按领域划分记忆空间,而不是全局共享。每个 Agent 有自己的私有记忆空间,同时有一个共享空间存放跨领域的通用知识。检索时先查私有空间,再查共享空间,合并排序。
这样做的好处是避免 Agent 之间的记忆互相干扰。比如一个专门做代码的 Agent 和一个专门做数据分析的 Agent,它们的 episodic memory 差异很大,混在一起检索反而降低精度。
6. 关于 hindsight 这套思路,我目前的几个判断
第一,记忆的质量比数量重要一个数量级。与其存一万条粗糙的执行日志,不如存一百条精炼的复盘结论。复盘这个动作本身消耗 token,但它带来的检索精度提升和 token 节省,长期看是划算的。
第二,MCP 让记忆层可以独立演进。把记忆服务做成 MCP server 之后,换向量库、换 embedding 模型、调整检索策略,都不需要动 Agent 的核心代码。这个解耦在项目早期可能感觉不到价值,但一旦要迭代就会感谢当初的决定。
第三,Docker 化不是可选项。记忆服务涉及多个依赖,本地直接跑迟早会遇到环境问题。用 Docker 把依赖封起来,是保证可复现性的最低成本手段。
第四,遗忘机制必须从第一天就设计。我见过太多项目,记忆库只增不减,最后检索出来的全是噪声。清理策略、冲突解决、容量控制,这些不是后期优化,是架构的一部分。
最后分享一个我最近在试的小技巧:在复盘提示词里加一句“如果你是这个任务的执行者,下次再做类似任务,你最希望提前知道什么”,让 LLM 从“未来的自己”的视角来提炼经验。实测下来,这样生成的reusable_insight比直接让它总结要精准得多,因为它被迫站在使用者的角度思考,而不是站在记录者的角度。这个思路后续还可以扩展到多轮复盘——让 Agent 定期回看自己过去一周的 episodic memory,归纳出更高层的 semantic memory,形成记忆的自我进化。