1. 从“hindsight”这个词说起:为什么它值得单独拿出来聊
“hindsight”直译过来是“后见之明”,但在技术语境里,它指向一个非常具体的东西:让智能体在任务执行完之后,回头审视自己走过的路,把当时的判断、踩过的坑、有效的路径沉淀成可复用的记忆。这个词之所以在 agent memory 圈子里被反复提起,是因为它精准地戳中了一个长期痛点——大多数 agent 在完成一次任务后,记忆就归零了,下一次遇到类似场景,它还是会用同样的方式犯错。
我最早接触这个概念是在做一套基于 LLM 的自动化流程编排时。当时团队里有个共识:模型能力本身在快速拉平,真正拉开差距的是记忆的组织方式。你可以用同样的模型、同样的工具链,但一个会“事后复盘”的 agent 和一个只会“当下反应”的 agent,在连续任务中的表现差距会随着轮次增加而指数级放大。hindsight 要解决的,就是把这个“复盘”动作工程化、结构化,而不是靠人手动往 prompt 里塞历史记录。
这篇文章适合三类人看:第一类是在做 agent 应用、被上下文窗口和记忆管理折磨过的开发者;第二类是对 MCP 协议、Docker 部署这套组合拳感兴趣、想找一个完整案例练手的工程师;第三类是对 LLM 记忆机制好奇、想理解“working memory”和“long-term memory”在工程上到底怎么落地的人。我会围绕 hindsight 这个核心,把它的设计逻辑、部署方式、和 MCP 的配合、以及实际跑起来之后会遇到的问题,一层层拆开讲。
需要先说明一点:hindsight 本身是一个偏概念性的项目名,它不是一个开箱即用的商业产品,更像是一套记忆架构的设计范式。所以下面的内容里,我会把“hindsight 思路”和“具体实现手段”分开讲,前者是方法论,后者是你可以直接抄的工程方案。
2. hindsight 要解决的核心问题:agent 的“记忆断层”
2.1 为什么普通上下文拼接撑不住长任务
大多数人做 agent 记忆的第一反应是:把历史对话全部塞进 context window。这个做法在短任务里没问题,但一旦任务轮次超过十几轮,就会遇到三个硬墙。
第一是token 成本。假设每轮对话平均 800 token,20 轮就是 16000 token,每次请求都要重新传一遍,成本是线性甚至超线性增长的。第二是注意力稀释。模型对长上下文的中间部分注意力会明显下降,你塞进去的关键信息很可能被淹没。第三是噪声累积。历史里包含大量无效的试错、重复的工具调用结果,这些噪声会干扰模型对当前状态的判断。
hindsight 的思路是:不要把所有历史都当成同等重要的记忆,而是在任务结束后做一次结构化的提炼,把“发生了什么、为什么这么做、结果如何、下次该怎么做”压缩成几条高密度的记忆条目。这样下一次任务开始时,注入的不是原始流水账,而是经过加工的“经验”。
2.2 working memory 和 hindsight memory 的分工
这里要引入一个关键区分。working memory是任务执行期间的临时状态,比如当前步骤、已调用的工具、中间结果,它需要高频读写、生命周期短。hindsight memory是任务结束后的沉淀,它低频写入、长期保存、跨任务复用。
我自己的做法是用两层存储:working memory 放在内存或 Redis 里,任务结束就丢弃;hindsight memory 落到持久化存储,比如 SQLite 或 Postgres,带向量索引。两者的数据结构也不一样,working memory 是线性的步骤列表,hindsight memory 是带标签的条目,每条包含场景描述、采取的动作、结果评价、可复用建议。
这个分工的好处是,你不需要在任务执行时频繁写数据库,也不会让长期记忆被临时状态污染。等任务真正结束、结果确认之后,再触发一次 hindsight 提炼,把这次任务里真正有价值的部分抽出来。
2.3 一个具体的失败案例
我之前做过一个自动整理文件的 agent,任务是“把下载目录里超过 30 天的安装包删掉,其余按类型归档”。第一次跑,它把几个正在用的项目依赖包也删了,因为那些包的文件名里带日期,被误判为“旧安装包”。
如果没有 hindsight,第二次跑同样的任务,它大概率还会犯同样的错。但加了 hindsight 之后,任务结束时我让它复盘:哪些文件被删了、删除依据是什么、有没有误删。它自己总结出一条:“文件名含日期不等于安装包,需要检查文件扩展名和所在目录”。这条记忆被存下来,下次任务开始前注入,它就会先做扩展名过滤。这就是 hindsight 的价值——把一次性的错误转化成跨任务的约束。
3. 把 hindsight 落地:存储层怎么选、数据怎么组织
3.1 存储选型:从 SQLite 到向量库的渐进路线
hindsight memory 的存储方案,我建议按规模分阶段选,不要一上来就上重型向量数据库。
| 阶段 | 数据量 | 推荐方案 | 理由 |
|---|---|---|---|
| 原型验证 | < 1000 条 | SQLite + FTS5 | 零依赖,全文检索够用,部署成本极低 |
| 小规模生产 | 1000 ~ 10万 | Postgres + pgvector | 事务可靠,向量和结构化字段统一管理 |
| 大规模 | > 10万 | 专用向量库 + 关系库分离 | 检索性能和写入吞吐需要独立优化 |
我早期用 SQLite 跑了大概两个月,存了不到两千条记忆,检索用 FTS5 的关键词匹配,效果已经能覆盖大部分场景。后来条目多了、需要语义检索,才迁到 Postgres + pgvector。这个迁移过程本身不复杂,因为记忆条目的结构是固定的,换个存储后端改一下 DAO 层就行。
提示:不要为了“看起来专业”一上来就上向量库。hindsight memory 的条目数量增长是慢的,因为它是任务级沉淀,不是对话级。一个每天跑 50 个任务的系统,一年也就一万多条,SQLite 完全扛得住。
3.2 记忆条目的字段设计
一条 hindsight memory 应该包含哪些字段,直接决定了它后续能不能被有效检索和复用。我踩过的坑是:一开始只存了一段自然语言描述,结果检索时只能靠模糊匹配,命中率很低。后来改成结构化字段 + 自然语言描述的组合。
我目前用的字段结构是这样的:
- scene:场景标签,比如“文件清理”“代码审查”“数据抓取”,用于粗筛
- trigger:触发条件,描述什么情况下这条记忆适用
- action:当时采取的动作,可以是工具调用序列的摘要
- outcome:结果评价,成功/失败/部分成功,以及关键指标
- lesson:可复用的经验,这是最核心的字段,用自然语言写
- embedding:lesson 字段的向量表示,用于语义检索
- created_at / last_used_at:时间戳,用于衰减和淘汰
其中lesson 字段的写法很讲究。不要写“删除了文件”,要写“删除文件前必须确认扩展名,仅凭文件名日期判断会误删依赖包”。前者是流水账,后者是可执行的约束。我一般要求 lesson 控制在 50 字以内,一句话说清一个点,多条记忆比一条长记忆更好用。
3.3 记忆的写入时机与去重
hindsight 的写入不是每轮都做,而是任务边界触发。什么叫任务边界?我的定义是:用户明确表示任务完成、或者 agent 连续 N 轮没有新的工具调用、或者达到了预设的终止条件。这时候触发一次复盘,生成记忆条目。
写入时一定要做去重。我遇到过同一个错误被反复记录的情况,因为 agent 在多个任务里犯了同样的错,每次都生成一条几乎一样的记忆。去重的做法是:新记忆写入前,先用 embedding 检索最相似的已有记忆,如果相似度超过阈值(我用的 0.92),就不新增,而是更新已有记忆的 last_used_at 和出现次数。出现次数高的记忆,在检索时权重更高。
这个机制让记忆库保持精简,同时让高频问题浮到前面。实测下来,一个跑了三个月的系统,记忆条目稳定在几百条,而不是无限膨胀。
4. MCP 在 hindsight 架构里的位置:别把它当成万能胶
4.1 MCP 到底解决的是哪一层问题
MCP(Model Context Protocol)这两年被讨论得很多,但很多人对它的定位有误解。它不是记忆存储协议,也不是 agent 框架,它解决的是“模型如何标准化地调用外部能力”这个问题。你可以把它理解成一套约定:工具提供方按这个约定暴露接口,模型侧按这个约定发起调用,双方不用为每个工具写定制适配。
在 hindsight 架构里,MCP 的位置是记忆的读写通道。也就是说,hindsight memory 的存储和检索逻辑可以封装成一个 MCP server,agent 通过 MCP 协议来查询记忆、写入记忆。这样做的好处是,记忆能力变成了一个可插拔的组件,换 agent 框架、换模型,只要支持 MCP,记忆层不用重写。
但要注意,MCP 本身不负责记忆的组织逻辑。它只管“你给我一个查询,我返回匹配的记忆”,至于记忆怎么提炼、怎么去重、怎么衰减,那是 hindsight 层的事。把这两层混在一起,是很多项目做复杂的原因。
4.2 把 hindsight 封装成 MCP server 的实操
我实际做的时候,MCP server 暴露了三个工具:
search_memory:输入场景标签和查询文本,返回 top-k 相关记忆write_memory:输入结构化记忆条目,写入存储update_memory_usage:更新某条记忆的使用时间和次数
用 Python 实现的话,核心就是包一层 MCP 的 SDK,把上面的存储逻辑挂上去。启动方式可以是 stdio,也可以是 SSE,看你的 agent 运行环境。如果是本地开发,stdio 最简单;如果是多 agent 共享记忆,用 SSE 起一个常驻服务更合适。
# 伪代码示意,展示 MCP tool 的注册结构 from mcp.server import Server from mcp.types import Tool server = Server("hindsight-memory") @server.tool() async def search_memory(scene: str, query: str, top_k: int = 5): # 1. 按 scene 粗筛 # 2. 对 query 做 embedding # 3. 向量检索 + 关键词检索混合排序 # 4. 返回记忆条目列表 ... @server.tool() async def write_memory(scene: str, trigger: str, action: str, outcome: str, lesson: str): # 1. 生成 lesson 的 embedding # 2. 检索相似记忆,判断是否去重 # 3. 写入或更新 ...这里有个细节:search_memory 的返回格式要控制好。不要返回整条记忆的所有字段,而是返回 lesson 和 trigger 为主,附带一个 id 供后续更新使用。返回太多字段会占用 context,反而稀释了有效信息。
4.3 MCP 接入时的常见坑
第一个坑是工具描述写得太模糊。MCP 的工具描述是给模型看的,如果 search_memory 的描述只写“搜索记忆”,模型不知道什么时候该调用它。要写清楚:“当需要回忆过去类似任务的处理经验时调用,输入场景标签和当前问题描述”。
第二个坑是超时和重试。MCP 调用是跨进程的,如果记忆检索涉及向量计算,可能耗时几百毫秒。agent 侧要设置合理的超时,并且对检索失败做降级——检索不到记忆时,任务应该继续,而不是卡住。
第三个坑是权限和隔离。如果多个 agent 共享一个记忆库,要按 agent 或用户做隔离,否则 A 任务的记忆会污染 B 任务。我的做法是在记忆条目里加一个 namespace 字段,检索时强制带上。
5. Docker 部署 hindsight 服务:从 compose 到网络排查
5.1 为什么用 Docker 而不是直接跑
hindsight 服务涉及几个组件:MCP server、向量存储、可能还有一个定时复盘的任务队列。直接跑在宿主机上,依赖管理会很乱,尤其是向量库对系统库有要求。用 Docker 的好处是环境隔离,每个组件一个容器,compose 一把起。
我的 compose 结构大概是三个服务:hindsight-mcp(MCP server)、hindsight-db(Postgres + pgvector)、hindsight-worker(定时复盘和记忆衰减)。三者通过内部网络通信,只有 MCP server 对外暴露端口。
services: hindsight-db: image: pgvector/pgvector:pg16 environment: POSTGRES_DB: hindsight POSTGRES_USER: hindsight POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - hindsight_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U hindsight"] interval: 10s retries: 5 hindsight-mcp: build: ./mcp depends_on: hindsight-db: condition: service_healthy environment: DB_URL: postgresql://hindsight:${DB_PASSWORD}@hindsight-db:5432/hindsight ports: - "8765:8765" hindsight-worker: build: ./worker depends_on: - hindsight-db environment: DB_URL: postgresql://hindsight:${DB_PASSWORD}@hindsight-db:5432/hindsight volumes: hindsight_data:5.2 Windows 上装 Docker Desktop 的几个真实坑
如果你在 Windows 上开发,Docker Desktop 的安装有几个高频问题。第一个是WSL2 没装或版本太旧,Docker Desktop 启动会直接报 virtualization support not detected。解决办法是先跑wsl --update,然后在 BIOS 里确认虚拟化开启。
第二个是端口冲突。Docker Desktop 默认会占用一些端口,如果你本机已经跑了别的服务,compose 起来时会报端口绑定失败。我一般把 MCP server 的端口设成不常用的,比如 8765,避开 3000、8080 这些。
第三个是文件挂载的性能。Windows 下把宿主机目录挂进容器,IO 性能会比 Linux 差很多。如果记忆数据量大,建议用 named volume 而不是 bind mount,让数据待在 Docker 的虚拟磁盘里。
5.3 容器间网络不通的排查链路
compose 起来之后,最常见的问题是 MCP server 连不上数据库。排查顺序我一般是这样的:
- 先看
docker compose ps,确认所有容器都是 healthy 或 running - 进 MCP 容器
docker exec -it hindsight-mcp sh,用nc -zv hindsight-db 5432测端口 - 如果端口不通,检查 compose 里两个服务是否在同一个 network(默认在同一个 compose 项目下是通的)
- 如果端口通但连不上,检查数据库的
pg_hba.conf是否允许来自容器网段的连接 - 最后看环境变量里的 DB_URL 有没有写错,尤其是密码里的特殊字符需要转义
这个链路我走过好几次,大部分问题出在第 4 步,Postgres 默认只允许本地连接,容器网段需要额外配置。
6. 记忆检索的质量调优:从“能查到”到“查得准”
6.1 混合检索比纯向量检索更稳
一开始我只用向量检索,后来发现有些查询的语义相似度很高,但实际场景不匹配。比如“删除文件”和“删除数据库记录”,向量距离很近,但一个是文件操作,一个是数据库操作,混在一起会误导 agent。
后来改成混合检索:先用 scene 标签做硬过滤,再在过滤后的集合里做向量检索,同时叠加关键词匹配的分数。最终排序分数是向量相似度和关键词命中率的加权和。这个改动之后,检索准确率明显提升,尤其是场景边界清晰的记忆。
权重的设置我调过几轮,目前用的是向量 0.7、关键词 0.3。如果你的记忆条目里专业术语多,关键词权重可以再高一点。
6.2 记忆衰减与淘汰策略
记忆不是越多越好。过期的、不再适用的记忆会干扰检索。我设计了一个简单的衰减机制:每条记忆有一个score,初始为 1.0,每次被检索到并实际使用后加 0.1,每 30 天没有被使用则乘以 0.9。当 score 低于 0.3 时,进入待淘汰队列,由 worker 定期清理。
这个机制的效果是,高频有效的记忆会浮上来,一次性的、不再适用的记忆会慢慢沉下去。实测三个月后,活跃记忆条目只占总数的 40% 左右,检索信噪比明显改善。
注意:衰减周期不要设太短。有些记忆是低频但关键的,比如“每年报税时的特殊处理”,可能一年才用一次,如果衰减太快会被误删。我一般对标记为“关键”的记忆关闭衰减。
6.3 用 LLM 做记忆提炼的 prompt 设计
hindsight 的核心动作是任务结束后的复盘提炼,这一步我用 LLM 来做。prompt 的设计直接决定记忆质量。我现在的 prompt 结构是:
- 输入:本次任务的完整步骤记录、工具调用结果、最终状态
- 要求:输出 1 到 3 条记忆条目,每条包含 trigger、action、outcome、lesson
- 约束:lesson 必须是一句可执行的建议,不超过 50 字;如果本次任务没有新经验,返回空
关键约束是“没有新经验就返回空”。早期没有这条约束时,LLM 会强行编出一些泛泛而谈的“经验”,比如“要注意细节”,这种记忆毫无价值。加上这条之后,记忆库干净了很多。
另外,我让 LLM 在输出时附带一个置信度,低于 0.6 的记忆不写入。这个置信度是模型对自己提炼质量的评估,实测能过滤掉一部分低质量条目。
7. 跑通之后的一些真实体会
这套 hindsight 架构我断断续续迭代了大半年,从最早的 SQLite 单文件,到现在的 Docker compose 三服务,中间踩的坑比预想的多。有几个体会是文档里不会写的。
第一,记忆的价值不在于多,而在于准。我一度追求记忆条目的数量,觉得存得越多越智能,结果检索时噪声太大,agent 反而被误导。后来把写入门槛提高,宁缺毋滥,效果反而更好。
第二,MCP 是通道不是大脑。不要把记忆的组织逻辑塞进 MCP server,它只负责读写。提炼、去重、衰减这些逻辑放在独立的 worker 里,职责清晰,调试也方便。
第三,Docker 部署的稳定性取决于健康检查。我一开始没写 healthcheck,导致 MCP server 在数据库还没就绪时就启动,连接失败后不会自动重试。加上condition: service_healthy之后,启动顺序问题就解决了。
第四,记忆检索要能降级。检索服务挂了或者超时,agent 不能卡死。我的做法是检索失败时返回空列表,agent 按无记忆状态继续执行,同时记录一次失败日志,后续排查。
最后分享一个小技巧:如果你刚开始做,不要急着上向量库。先用 SQLite 加关键词检索跑通整个流程,把记忆的写入、检索、衰减逻辑验证清楚,再换存储后端。存储是最容易替换的一层,逻辑才是核心。我见过不少人卡在选型上,结果核心逻辑一直没跑起来,本末倒置了。