
1. 从“hindsight”这个词说起为什么它值得单独拿出来聊“hindsight”这个词本身的意思是“事后之明”——事情发生之后回头看才明白当时应该怎么做。把这个词放到 LLM Agent 的语境里它指向的东西就非常具体了Agent 在完成任务之后如何把“刚才发生了什么”沉淀成可复用的记忆而不是每次对话都从零开始。我最初注意到这个方向是因为在实际搭 Agent 的过程中反复撞到同一堵墙一个 Agent 在上午的会话里已经摸清了某个项目的目录结构、某个 API 的返回格式、某个报错的处理方式结果到了下午新开一个会话它又像个新人一样从头问起。上下文窗口再大也扛不住这种浪费而且 token 成本是实打实烧出去的。关键词里出现的agent memory、working memory、MCP、Docker这几个词其实勾勒出了一条完整的技术链路Agent 需要一套记忆机制memory记忆里要区分短期的工作记忆working memory和长期沉淀记忆的读写要通过某种标准化接口暴露给模型MCP 就是当前最主流的那个接口协议而整套东西要跑起来、要能复现、要能给别人用Docker 是最省事的打包方式。所以这篇不是空谈概念我会把“hindsight”这个方向拆成几个能落地的层面记忆到底该怎么分层、MCP 在这里扮演什么角色、Docker 怎么把环境固化下来、以及我在实操中踩过的那些坑。适合已经在写 Agent、但被“记忆”这件事卡住的开发者也适合刚接触 MCP 想找个具体场景练手的人。说明项目正文和关键词原始输入为空以下内容基于标题“hindsight”与相关热搜词agent memory、LLM、MCP、Docker所指向的常见技术实践进行合理展开涉及具体实现的部分均为业界常见做法的归纳不代表某个特定项目的官方文档。2. Agent 记忆的分层别把“记住”当成一件事2.1 工作记忆和长期记忆混在一起就是灾难很多人做 Agent 记忆的第一步就是把所有对话历史塞进一个向量库然后每次检索 top-k 拼进 prompt。这个做法在 demo 阶段能跑通但一上真实场景就崩。原因很简单工作记忆和长期记忆的读写频率、生命周期、精度要求完全不同。工作记忆working memory是当前任务进行中的临时状态——比如“用户刚才说他想要 JSON 格式”“当前正在处理的文件是 config.yaml”“上一步工具调用返回了一个 404”。这些东西变化极快几分钟甚至几秒就失效但精度要求极高错一个字整个任务就跑偏。长期记忆是跨会话沉淀下来的知识——比如“这个项目的数据库是 PostgreSQL”“用户偏好简洁的回答风格”“某个 API 的鉴权方式是 Bearer token”。这些东西相对稳定可以容忍一定的检索误差但需要长期保存和定期整理。把这两类东西塞进同一个存储、用同一套检索策略结果就是要么工作记忆被长期记忆淹没检索出来的全是无关的历史要么长期记忆被高频写入的工作记忆冲垮重要的知识反而被挤掉。我的做法是物理隔离工作记忆放在进程内存或 Redis 这类低延迟存储里带 TTL长期记忆放在向量库或结构化存储里写入时做去重和摘要。两者通过一个统一的 memory 接口对外暴露但底层是两套东西。2.2 hindsight 的核心写入时机比检索策略更重要大部分关于 Agent 记忆的讨论都在讲“怎么检索”但我在实操中发现真正决定记忆质量的是“什么时候写”。如果你在每个 turn 结束都写一次记忆向量库里会塞满大量低价值的碎片——“用户说了你好”“Agent 回复了问候”。这些碎片在检索时会污染结果而且让存储成本线性膨胀。hindsight 这个思路的价值就在于它强调“事后”写入也就是在一个任务段落结束、或者一个明确的里程碑达成之后才触发记忆的提炼和写入。这个时机选择背后有很实际的考虑任务结束时Agent 已经掌握了完整的上下文能写出比中途更准确的摘要里程碑是天然的切分点围绕它组织的记忆天然带有“这件事解决了什么问题”的语义写入频率大幅降低存储和检索的噪声都跟着降下来。具体实现上我会在 Agent 的循环里埋一个should_commit_memory的判断触发条件包括任务状态从进行中变为完成、工具调用连续失败超过阈值、用户显式说“记住这个”、或者对话轮数达到某个上限。触发之后让模型对最近的交互做一次结构化提炼输出“发生了什么 / 结论是什么 / 下次遇到类似情况该怎么做”三段式再写入长期记忆。2.3 记忆的 key-value 设计三个问题定乾坤热搜词里有一条很精辟的总结key 是“我是谁”query 是“我在找什么”value 是“我能提供什么”。这三句话其实就是 Agent 记忆的元数据设计原则。我见过太多人把记忆存成裸文本检索时纯靠语义相似度。这样做的后果是当 Agent 需要“关于数据库配置的记忆”时它可能检索出一段关于“数据库连接失败怎么排查”的对话因为两者语义相近但前者是配置事实后者是排错经验用途完全不同。给每条记忆打上结构化的元数据检索时先按元数据过滤再按语义排序命中率会有质的提升。我常用的字段包括字段含义示例agent_id我是谁project-assistant-v2memory_type记忆类型fact / procedure / preference / episodescope作用范围global / project-x / session-123source来源user_stated / tool_observed / inferredconfidence置信度0.0 - 1.0created_at创建时间ISO 8601last_used_at上次使用时间ISO 8601memory_type这个字段尤其关键。事实类记忆fact用于回答“是什么”流程类记忆procedure用于回答“怎么做”偏好类记忆preference影响回答风格情节类记忆episode用于复盘。检索时如果能把类型作为硬过滤条件能砍掉大量误召回。confidence字段是我踩坑之后加的。模型推断出来的记忆比如“用户可能喜欢简短回答”和用户明确说出来的记忆“以后回答简短点”可信度完全不一样。前者在检索时应该降权后者应该优先。没有这个字段Agent 会把模型的猜测当成事实来用翻车是迟早的事。3. MCP 在记忆系统里的位置它不是存储是接口3.1 为什么记忆要用 MCP 暴露而不是直接调 SDKMCPModel Context Protocol这两年被讨论得很多但很多人对它的定位有误解以为它是某种存储方案或者框架。MCP 本质上是软件协议解决的是“模型怎么标准化地调用外部能力”这个问题和硬件协议那个概念是两回事。把 Agent 记忆通过 MCP server 暴露出去好处有三个而且都是实打实的第一解耦。记忆的存储实现可以是向量库、可以是关系库、可以是文件但对外只暴露一组标准工具memory_write、memory_search、memory_forget。换存储实现的时候Agent 侧一行代码都不用改。第二复用。同一个记忆 MCP server 可以同时被多个 Agent 客户端使用。我在本地跑的一个记忆服务既给命令行里的 Agent 用也给 IDE 里的助手用记忆是打通的。第三可观测。MCP 的调用是显式的工具调用每次读写都能在日志里看到。调试记忆问题时能清楚知道是“没写进去”还是“写进去了但没检索到”而不是对着一团黑盒猜。3.2 一个记忆 MCP server 该暴露哪些工具工具设计的原则是少而正交每个工具只做一件事。我见过有人把记忆 server 设计成一个大而全的memory工具靠参数区分行为结果模型经常传错参数。拆成独立工具之后调用准确率明显上升。我常用的工具集是这样的memory_write写入一条记忆。参数包括 content、memory_type、scope、confidence。返回写入后的 id。memory_search检索记忆。参数包括 query、memory_type可选过滤、scope可选过滤、top_k。返回带元数据的记忆列表。memory_update更新已有记忆。用于修正错误或提升置信度。memory_forget删除记忆。用于处理过期信息或用户要求删除的场景。memory_summarize对某个 scope 下的记忆做摘要压缩。用于定期整理防止记忆库膨胀。这里有个细节值得说memory_search的返回结果里我坚持带上last_used_at并在检索时更新它。这样做的目的是让“经常被用到的记忆”浮出来配合一个简单的衰减策略可以自动淘汰那些长期没人用的记忆。这比单纯按时间淘汰要合理得多因为有些老记忆比如项目的基础架构信息虽然旧但一直在用不该被清掉。3.3 MCP 工具描述怎么写模型才不容易调错MCP 工具的 description 字段是给模型看的不是给人看的。我一开始按写文档的习惯写描述结果模型经常在不需要检索的时候调检索或者在应该写入的时候调了更新。后来我改成“场景化描述”也就是在 description 里直接写清楚“什么时候该用这个工具”。比如memory_search的描述不是“检索记忆”而是“当需要回忆之前会话中提到的项目配置、用户偏好或已解决的问题时调用。不要在每次对话开始时无条件调用。”这个改动看起来小但对调用准确率的影响很大。模型对“什么时候用”的敏感度远高于对“这个工具能做什么”的敏感度。另外参数描述里要给出明确的边界。比如top_k要写“建议 3-8超过 10 会引入噪声”而不是只写“返回条数”。模型会参考这些建议值减少乱传参数的情况。4. 用 Docker 把记忆服务固化下来从能跑到能复现4.1 为什么记忆服务特别适合容器化记忆服务有几个特点让它天然适合跑在容器里它需要长期运行、它依赖特定的存储后端、它的配置项比较多、它经常需要在不同机器之间迁移。我在裸机上跑记忆服务的时候最头疼的就是环境问题。向量库的版本、Python 的版本、某个依赖的编译选项任何一个不一致都可能导致行为差异。有一次在本地跑得好好的检索部署到另一台机器上召回率明显下降排查了半天发现是向量库版本不同导致的索引行为差异。容器化之后镜像就是环境的唯一真相。docker compose up一跑存储、服务、配置全部到位换机器也是同一套。4.2 一个可用的 compose 结构我不追求把 compose 写得花哨实用为主。一个记忆服务的 compose 通常包含三部分记忆服务本身、向量存储、以及可选的缓存。services: memory-server: build: . ports: - 8080:8080 environment: - VECTOR_STORE_URLhttp://vector-store:6333 - CACHE_URLredis://cache:6379 - MEMORY_TTL_DAYS90 depends_on: - vector-store - cache volumes: - ./config:/app/config:ro vector-store: image: qdrant/qdrant:latest ports: - 6333:6333 volumes: - vector-data:/qdrant/storage cache: image: redis:7-alpine command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru volumes: - cache-data:/data volumes: vector-data: cache-data:几个我踩过坑才加上的配置MEMORY_TTL_DAYS这个环境变量控制长期记忆的默认过期时间。一开始我没设结果记忆库无限膨胀检索越来越慢。加上 TTL 之后配合定期整理任务库的大小稳定在一个合理范围。Redis 的maxmemory-policy设成allkeys-lru是因为工作记忆本来就是临时的内存满了淘汰最久未用的就行不需要持久化保证。这样即使 Redis 挂了重启丢的也只是临时状态长期记忆在向量库里安然无恙。向量库的数据卷一定要挂出来。我见过有人忘了挂卷容器一重建数据全没记忆服务变成了一次性的。4.3 Windows 上跑 Docker 的那点事热搜词里“windows安装docker”“virtualization support not detected”出现频率很高说明不少人在 Windows 上起步就卡住了。我简单说下关键点。Windows 上跑 Docker Desktop底层依赖虚拟化支持。如果 BIOS 里没开虚拟化或者和 Hyper-V、WSL2 的配置冲突就会报virtualization support not detected或者启动失败。排查顺序是先确认 BIOS 里虚拟化已启用再确认 WSL2 已安装并设为默认最后确认 Docker Desktop 用的是 WSL2 后端而不是 Hyper-V。另一个常见问题是网络不通。容器里访问宿主机服务不能用localhost要用host.docker.internal。这个在配置记忆服务连接本地模型或者本地存储时特别容易踩。我一开始把VECTOR_STORE_URL写成http://localhost:6333容器里死活连不上改成服务名http://vector-store:6333才通。提示如果你在 Windows 上开发建议把代码放在 WSL2 的文件系统里而不是 Windows 的挂载盘。跨文件系统的 IO 性能差距很大向量库这种频繁读写磁盘的服务感受尤其明显。5. 记忆写入的实操细节从“记下来”到“记得有用”5.1 提炼 prompt 怎么写决定了记忆的质量上限写入记忆不是把对话原文存进去而是让模型做一次提炼。这个提炼 prompt 的质量直接决定了记忆库的上限。我试过很多版本最后稳定下来的结构是这样的你是一个记忆提炼器。请阅读以下交互片段输出一条结构化记忆。 要求 1. 只记录对未来有复用价值的信息忽略寒暄、重复确认、临时状态。 2. 如果片段中没有值得长期保留的信息输出 SKIP。 3. 输出格式为 JSON包含字段summary一句话概括、detail必要细节、memory_typefact/procedure/preference/episode、confidence0-1。 交互片段 {interaction}关键在第二条“输出 SKIP”。没有这个出口模型会强行从任何片段里挤出点东西来导致大量低价值记忆。加上之后大概有三分之一的片段会被正确跳过记忆库的纯度明显提升。confidence让模型自己评估也有讲究。用户明确陈述的事实模型通常给 0.9 以上模型自己推断的给 0.5-0.7。这个分数在检索时参与排序推断类记忆不会轻易盖过事实类记忆。5.2 去重和冲突处理记忆库的免疫系统记忆库用久了一定会出现重复和冲突。同一个事实被记了三次或者新旧信息矛盾“数据库是 MySQL” vs “数据库已迁移到 PostgreSQL”。我的处理策略分两层。写入时做一次轻量去重用新记忆的 summary 去检索 top-3如果相似度超过阈值我用的 0.92就不新增而是更新已有记忆的last_used_at和confidence。这个阈值不能太低否则会把相关但不同的记忆误判为重复。冲突处理放在定期整理任务里。每周跑一次对同一 scope 下的记忆做聚类发现矛盾时保留created_at更新的那条把旧的标记为superseded而不是直接删除。保留历史的好处是当新信息被证明是错的还能回溯到旧信息。5.3 检索时的重排序别只信向量相似度向量检索返回的 top-k顺序不一定合理。我习惯在向量检索之后加一层重排序综合考虑几个因素语义相似度向量分数记忆类型匹配度query 意图和 memory_type 是否一致置信度新鲜度last_used_at越近越好但要有上限避免新记忆霸屏使用频率这几个因素加权求和权重要根据场景调。做事实问答时置信度权重高做流程复现时procedure 类型的权重高。我一般先用一组默认权重跑观察一段时间后针对性地调。重排序的另一个好处是能实现“多样性”。纯向量检索经常返回一堆内容高度相似的记忆重排序时可以加一个惩罚项降低与已选记忆过于相似的候选的分数让结果覆盖更全面。6. 那些文档里不会写的坑6.1 记忆污染Agent 把自己的猜测当成事实这是我最惨痛的一次翻车。Agent 在排查一个网络问题时推断“可能是防火墙规则导致的”然后这条推断被写进了记忆memory_type标成了 factconfidence给了 0.8。之后每次遇到网络问题它都先入为主地认为是防火墙绕了很多弯路。修复方案有两个层面。一是写入时严格区分sourceuser_stated、tool_observed、inferred三选一推断类记忆的 confidence 上限压到 0.6。二是检索时对inferred来源的记忆加提示让模型知道这是推断而非事实。这个坑的教训是记忆系统的可信度管理比记忆容量重要得多。一条错误的记忆造成的损害远大于十条缺失的记忆。6.2 上下文窗口和记忆的边界什么该进 prompt什么该留在库里不是所有检索到的记忆都该塞进 prompt。我见过有人检索 top-20 全塞进去结果 prompt 被记忆占满模型反而抓不住重点。我的做法是分层注入高置信度、高相关度的记忆通常 3-5 条直接进 system prompt相关但置信度一般的作为“参考信息”放在用户消息附近其余的留在库里等模型主动调用memory_search时再取。这样做的另一个好处是省 token。记忆检索本身也要花 tokenquery 的 embedding、结果的格式化无节制地检索和注入成本会失控。6.3 记忆的“遗忘”比“记住”更难设计删除记忆听起来简单实际很难。删早了Agent 丢失有用信息删晚了库膨胀、检索变慢、噪声增多。我现在的策略是“软删除 定期硬清理”。过期或低价值的记忆先标记为archived检索时默认不返回但保留在库里。每月跑一次硬清理把archived超过 30 天且从未被memory_update过的记录真正删掉。这个缓冲期给了纠错的机会——有时候一条记忆被归档后才发现它其实还有用。6.4 MCP 连接的超时和重试别用默认值MCP server 如果跑在容器里网络抖动是常态。默认的超时和重试策略往往不够用表现为 Agent 偶尔“失忆”——其实是记忆检索请求超时了但错误被吞掉Agent 以为没有相关记忆。我在客户端侧把记忆相关工具调用的超时设成 10 秒重试 2 次并且把失败明确暴露给 Agent让它知道“这次没查到”而不是“确实没有”。这个区别在调试时很重要。7. 把这套东西跑起来的最小路径如果你现在就想动手我给一条最小路径不追求完整先跑通闭环。第一步用 Docker 起一个向量库和一个 Rediscompose 文件参考上面那节把卷挂好。第二步写一个最简单的 MCP server只实现memory_write和memory_search两个工具存储先用向量库元数据先用 JSON 塞在 payload 里。别一上来就搞复杂的 schema。第三步在你的 Agent 里接入这个 MCP server先只在任务结束时调用memory_write在任务开始时调用一次memory_search。观察一周看检索出来的东西有没有用。第四步根据观察结果调整提炼 prompt 和检索策略。这一步是迭代的核心没有一劳永逸的配置。我自己的经验是前两周的记忆质量一定很差因为提炼 prompt 还没调好、元数据设计还没稳定。别急着放弃把每次检索的结果和实际需要的信息对比逐条分析哪里出了问题迭代几轮之后会有明显改善。最后分享一个我一直在用的小技巧给记忆库加一个“人工审核”入口。定期比如每周抽几条新写入的记忆看看发现质量问题就修正提炼 prompt。这个习惯帮我抓出了好几个系统性的提炼偏差比单纯看指标有效得多。