1. 从“hindsight”说起为什么我们需要给 Agent 装上“后视镜”第一次看到“hindsight”这个词我脑子里蹦出来的不是词典释义而是每次调完一个多轮对话 Agent 之后回看日志时那种“早知道当时就该把上下文压一压”的懊恼。Hindsight 直译是“后见之明”放在 LLM Agent 的语境里它指向的其实是一个非常具体、非常工程化的问题Agent 的记忆到底该怎么存、怎么取、怎么在事后被复盘和修正。这两年大家聊 Agent聊得最多的是工具调用、是 MCP 协议、是工作流编排但真正让一个 Agent 从“玩具”变成“能长期干活”的往往是记忆系统。一个没有记忆的 Agent每次对话都是失忆重来一个记忆设计糟糕的 Agent要么把上下文塞爆导致 token 成本失控要么检索出一堆无关的旧信息把模型带偏。而“hindsight”这个视角的价值在于它不追求“预测未来”而是强调对已经发生的交互进行结构化沉淀让 Agent 在后续任务里能像人一样“回想起来”。这篇文章我想聊的就是围绕 Agent Memory 的一整套落地思路从记忆的分层模型到 MCP 协议如何把记忆能力标准化成可插拔的服务再到用 Docker 把整套东西跑起来。关键词里出现的agent memory、LLM、MCP、Docker基本就是这条链路上的四个核心节点。适合谁看如果你已经在写 Agent、被上下文窗口折磨过、或者想给自己的项目加一个“能记住事”的模块那这篇就是写给你的。哪怕你只是刚听说 MCP 是什么我也会从最基础的地方讲清楚不跳步。我个人的判断是Agent Memory 正在从“每个框架自己造轮子”走向“协议化、服务化”而 hindsight 这类思路本质上是把“事后复盘”变成了记忆写入的一个正式触发点而不是靠模型在对话里随机记住点什么。下面我按设计思路、核心细节、实操落地、问题排查四个大块来拆。2. 整体设计思路把记忆当成一个独立的服务来对待2.1 为什么记忆不该塞在 Agent 主逻辑里我见过太多项目记忆逻辑是直接写在 Agent 的 prompt 拼接函数里的把历史对话一股脑塞进 messages 数组超过长度就截断最早的几条。这种做法在 demo 阶段没问题但一旦任务变长、工具调用变多就会暴露三个致命问题。第一是成本失控。上下文越长每次请求的 token 越多而很多历史信息其实是冗余的。第二是检索精度崩塌。简单截断意味着你丢掉的可能恰恰是关键的那条约束。第三是无法复盘。对话结束后这些信息就散了你没法回答“上周这个 Agent 为什么做了那个决定”。把记忆抽成独立服务好处是它可以用自己的存储、自己的检索策略、自己的生命周期管理和 Agent 主逻辑解耦。Agent 只负责“我要回忆关于 X 的事”记忆服务负责“从哪找、怎么排序、返回多少”。这也是 MCP 协议能切入的地方——它把“记忆”变成一种标准的工具能力任何支持 MCP 的客户端都能调用。2.2 记忆的三层结构working memory、episodic、semantic我在实际项目里会把 Agent 记忆分成三层这个划分参考了认知科学里比较经典的模型但落地时做了简化。Working Memory工作记忆是最短命的一层基本就是当前任务链路的临时状态。比如用户说“帮我订明天去上海的票”那“明天”“上海”就是工作记忆里的槽位。它通常放在内存里任务结束就清掉或者压缩成一条摘要再往下传。Episodic Memory情景记忆是“发生过什么”的记录。每一次完整的交互、每一个决策点、每一次工具调用的结果都可以作为一条 episode 存下来。hindsight 的核心就在这里——它强调的不是实时记住而是事后把 episode 结构化打上时间戳、任务标签、结果标签。Semantic Memory语义记忆是抽离出来的稳定知识。比如从多次订票交互里总结出“这个用户偏好靠窗座位”这条就属于语义记忆它不依赖具体某次对话而是跨会话的沉淀。这三层的读写频率、存储介质、检索方式都不一样。工作记忆用内存或 Redis情景记忆用带向量的文档库语义记忆可以用图数据库或者结构化的 KV。把它们混在一起是很多记忆系统跑着跑着就乱掉的根因。2.3 为什么选 MCP 而不是自己写一套 SDKMCPModel Context Protocol这两年被讨论得很多它的定位是让模型和外部能力之间的连接标准化。放到记忆这个场景MCP 的价值是你写一个 memory server暴露store、recall、forget这几个工具那么任何支持 MCP 的客户端——不管是 IDE 里的 Agent、还是你自己写的编排框架——都能直接接上。自己写 SDK 的问题是每个框架的接口都不一样你为 A 框架写的记忆模块换到 B 框架就得重写适配层。MCP 把这层抹平了。而且 MCP 的传输方式支持 stdio 和 SSE本地跑和远程跑都行这对 Docker 化部署特别友好。提示MCP 本身只是一个协议规范它不规定你用什么数据库、什么检索算法。所以“用 MCP 做记忆”和“记忆系统怎么设计”是两件事别指望协议帮你解决检索质量问题。2.4 Docker 在这套架构里的角色关键词里 Docker 出现频率很高这不是偶然。记忆服务通常要依赖向量库、关系库、缓存本地裸装这些东西版本冲突和环境差异能让人崩溃。Docker 的价值是把“记忆服务 它的依赖”打包成一个可复现的单元。我的习惯是memory server 一个容器向量库一个容器Redis 一个容器用 docker-compose 编排。这样换台机器docker compose up就能跑起来不用重新配环境。后面实操部分我会给出具体的 compose 配置。3. 核心细节解析记忆的写入、检索与遗忘3.1 写入策略什么时候该记记什么记忆系统最容易犯的错是“什么都记”。我早期做过一个版本把每一轮对话原封不动存进去结果检索时噪声极大召回的内容一半是寒暄。后来我改成事件驱动写入只在几个明确的触发点写记忆。触发点一任务完成或失败时。这时候把整个 episode 的输入、关键决策、工具调用序列、最终结果打包成一条记录。这条记录是 hindsight 的核心素材。触发点二用户显式表达偏好或约束时。比如“以后都用中文回复我”“这个项目不要动生产库”这类信息直接进语义记忆。触发点三检测到重复模式时。如果同一个用户连续三次问类似的问题可以触发一次语义记忆的写入把模式固化下来。写入的内容也要做结构化。我一般会存这几个字段timestamp、session_id、task_type、summary、raw_context、embedding、tags。其中summary是用一个小模型或者规则生成的短摘要检索时先匹配 summary命中后再取 raw_context这样能显著降低 token 消耗。3.2 检索策略token 的三个点——key、query、value热词里有一条很有意思“llm的token三个点key我是谁、query我在找什么、value我能提供什么”。这其实是在用 KV 的视角理解注意力机制但放到记忆检索里也特别贴切。Key我是谁对应的是记忆条目的身份标识。检索时你得先确定“我现在以什么身份在找”是同一个用户、同一个项目、还是同一类任务。身份不对检索出来的东西就是错的。Query我在找什么是当前的检索意图。这里不能只用原始问题做向量检索因为原始问题往往很短、信息量低。我的做法是把当前任务描述 最近几轮对话摘要拼成一个 query再做 embedding。Value我能提供什么是返回的记忆内容。这里有个关键取舍返回原文还是返回摘要我的经验是分层返回——先返回摘要列表让模型判断相关性如果模型觉得需要细节再发起一次精确召回拿原文。这样既省 token 又保证精度。检索的排序我一般用混合策略向量相似度占 60%时间衰减占 20%标签匹配占 20%。时间衰减很重要因为 Agent 记忆里“最近发生的事”通常比“很久以前的事”更相关除非用户明确在问历史。3.3 遗忘机制不会忘的记忆系统是灾难这一点我想重点讲因为很多教程都不提。记忆系统如果只增不减迟早会变成一个巨大的噪声库。遗忘不是可选项是必需项。我的遗忘策略分三种。时间衰减超过一定天数的情景记忆权重自动降低低于阈值就不再参与检索但数据保留。容量淘汰每个用户或每个 session 的记忆条目设上限超了就淘汰最旧、最少被召回的。主动遗忘提供forget工具让 Agent 或用户可以显式删除某条记忆这在隐私合规上也很重要。注意遗忘不等于删除。我通常做的是“软遗忘”——把条目标记为 inactive检索时过滤掉但保留在库里以便审计。真正删除只在用户明确要求时执行。3.4 记忆与 RAG 的区别别搞混了很多人把 Agent Memory 和 RAG 当成一回事其实差别很大。RAG 通常是静态知识库 单次检索知识是预先灌进去的检索是只读的。而 Agent Memory 是动态写入 持续演化记忆内容随交互不断变化还涉及遗忘和更新。打个比方RAG 像是查字典字典内容基本不变Agent Memory 像是写日记每天都在写还会回头修改以前的日记。这个区别决定了它们在存储结构、检索策略、更新机制上都不一样。热词里提到的rag graphrag llm wiki 本体rag其实就是在探索把知识图谱和 RAG 结合而 Agent Memory 更偏向“个人化的、随时间演化的知识”。4. 实操落地用 Docker 把记忆服务跑起来4.1 环境准备与 Docker 安装要点先说环境。我假设你在 Linux 或者 Windows WSL2 上操作。Windows 用户如果遇到virtualization support not detected或者docker desktop failed to start八成是 BIOS 里的虚拟化没开或者 WSL2 没装好。这两个问题我在热词里都看到了确实是高频坑。Linux 上装 Docker 我一般用官方脚本curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh sudo usermod -aG docker $USER最后一步把当前用户加进 docker 组避免每次都要 sudo。加完要重新登录才生效。验证的话跑docker run hello-world能出欢迎信息就说明装好了。Windows 用户装 Docker Desktop 之后记得在设置里确认 WSL2 backend 是开着的。如果公司网络有限制镜像拉取可能会慢可以配一下镜像加速这个各家云厂商都有公开的加速地址自己查一下当前的即可。4.2 用 docker-compose 编排记忆服务栈下面是我常用的一个 compose 配置包含记忆服务本体、向量库和缓存三层。这里用 Qdrant 做向量库Redis 做工作记忆缓存。version: 3.9 services: memory-server: build: ./memory-server ports: - 8080:8080 environment: - VECTOR_DB_URLhttp://qdrant:6333 - REDIS_URLredis://redis:6379 - EMBEDDING_MODELtext-embedding-3-small depends_on: - qdrant - redis networks: - mem-net qdrant: image: qdrant/qdrant:latest ports: - 6333:6333 volumes: - ./data/qdrant:/qdrant/storage networks: - mem-net redis: image: redis:7-alpine ports: - 6379:6379 volumes: - ./data/redis:/data networks: - mem-net networks: mem-net: driver: bridge这里有几个细节值得说。depends_on只保证启动顺序不保证服务真的 ready所以 memory-server 里最好加一个重试逻辑连不上向量库就等几秒再试。数据卷一定要挂出来不然容器一删记忆就没了。网络用自定义 bridge容器之间用服务名互相访问比用 IP 稳。4.3 实现一个最小可用的 MCP 记忆服务MCP server 的核心是暴露工具。下面是一个 Python 版的最小实现用官方 SDK 的思路写展示store和recall两个工具。from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent import json app Server(memory-server) app.list_tools() async def list_tools(): return [ Tool( namestore_memory, description存储一条记忆, inputSchema{ type: object, properties: { content: {type: string}, tags: {type: array, items: {type: string}}, layer: {type: string, enum: [episodic, semantic]} }, required: [content] } ), Tool( namerecall_memory, 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 store_memory: # 这里接向量库写入逻辑 result await save_to_vector_db(arguments) return [TextContent(typetext, textjson.dumps(result))] elif name recall_memory: result await search_vector_db(arguments[query], arguments.get(top_k, 5)) return [TextContent(typetext, textjson.dumps(result))] async def main(): async with stdio_server() as (read, write): await app.run(read, write, app.create_initialization_options()) if __name__ __main__: import asyncio asyncio.run(main())这个骨架跑起来之后任何 MCP 客户端都能通过 stdio 连上它。如果你想让远程客户端也能用把传输层换成 SSE 就行MCP 两种都支持。4.4 把 hindsight 逻辑接进写入流程hindsight 的关键是“事后复盘”。我的做法是在任务结束时触发一次复盘流程把这次任务的完整轨迹喂给一个模型让它输出结构化的记忆条目。async def hindsight_write(session_trace): prompt f 以下是 Agent 完成一次任务的完整轨迹 {session_trace} 请提取 1. 任务类型 2. 关键决策点及原因 3. 用户表达的偏好或约束 4. 可复用的经验教训 以 JSON 格式返回。 structured await llm_call(prompt) for item in structured[memories]: await store_memory( contentitem[content], tagsitem[tags], layeritem[layer] )这段逻辑的价值在于它把“原始轨迹”转化成了“可检索的知识”。原始轨迹又长又乱直接存进去检索效果很差经过复盘提炼之后每条记忆都是干净的、带标签的召回精度会高很多。提示复盘用的模型不需要太强小模型足够。因为这一步是离线批处理不占实时链路成本可控。4.5 参数选择embedding 维度与 top_k 怎么定embedding 模型的选择直接影响检索质量。我一般用 1536 维的模型维度太低语义区分度不够太高存储和计算成本上去了。如果你的记忆量在百万级以内1536 维完全够用。top_k的选择要看场景。实时对话里我一般取 3 到 5太多会稀释相关性。如果是离线分析或者复杂任务规划可以取到 10 到 20。另外建议加一个相似度阈值低于 0.7 的直接丢弃宁可返回空也不要返回噪声。时间衰减的公式我用的是指数衰减weight base_score * exp(-lambda * days_elapsed)lambda取 0.01 左右意味着大约 70 天后权重衰减到一半。这个值可以根据你的业务节奏调高频交互的场景可以调大让它忘得更快。5. 常见问题与排查技巧实录5.1 记忆检索不准的排查顺序检索不准是最常见的问题我一般按这个顺序排查。先看 embedding 模型是否和写入时一致换过模型但没重新索引是经典坑。再看 query 构造是否合理如果 query 太短向量检索基本靠运气。然后看标签过滤是否过严有时候标签写错了导致该召回的条目被过滤掉。最后看时间衰减参数如果衰减太快老记忆永远排不上来。5.2 Docker 网络不通的典型原因容器之间连不上九成是网络配置问题。如果用了自定义 bridge 网络容器之间应该用服务名访问用 localhost 是连不到别的容器的。另外检查端口映射容器内部端口和宿主机端口是两回事。还有一种情况是防火墙拦了容器网段这个在云服务器上比较常见。5.3 上下文爆炸的应急处理如果发现 token 消耗突然飙升先查是不是 recall 返回了太多内容。我遇到过 recall 返回了 20 条记忆每条都是长文本直接把上下文撑爆。应急处理是把 recall 的 top_k 调小同时开启摘要模式只返回摘要不返回原文。长期方案是给每条记忆加长度限制写入时就截断。5.4 常见问题速查表问题现象可能原因排查方向检索结果全是无关内容embedding 不一致或 query 太短检查模型版本丰富 query容器启动即退出依赖服务未就绪加健康检查和重试记忆写入成功但检索不到索引未刷新或标签错误检查索引状态和标签token 消耗异常高recall 返回过多调小 top_k启用摘要老记忆永远不出现时间衰减过快调小 lambda 值Docker 拉镜像超时网络或镜像源问题配置镜像加速5.5 几个我踩过的坑第一个坑是忘记给记忆加 session 隔离。早期版本所有用户的记忆混在一起检索时把别人的偏好召回了非常尴尬。后来所有查询都强制带 session_id 过滤。第二个坑是复盘模型幻觉。让模型总结轨迹时它有时会编造没发生过的决策。解决办法是要求它引用原文片段没有原文支撑的总结直接丢弃。第三个坑是向量库没做持久化。有次容器重建几个月的记忆全没了。从那以后所有数据卷都挂出来还加了定期备份。第四个坑是MCP 工具描述写得太模糊。工具描述是模型决定要不要调用的依据描述写不清楚模型就不知道该什么时候用。后来我把每个工具的适用场景都写进 description调用准确率明显提升。6. 记忆系统的演进方向与我的一点判断聊到这里我想说说我对这个方向后续演进的看法纯粹是个人观察不一定对。Agent Memory 现在最大的问题是缺乏统一的评估标准。大家都在说自己记忆做得好但没有一个公认的 benchmark 来衡量“记忆质量”。这导致很多优化是凭感觉的没法量化对比。另一个趋势是记忆和推理的融合。现在记忆和推理基本是分离的先检索再推理。未来可能会出现把记忆直接编码进模型权重或者 KV cache 的方案让“回忆”变成推理的一部分而不是一个独立步骤。热词里提到的llm ontology和本体rag其实就是在往结构化知识的方向走让记忆不只是向量而是有关系的图。还有一个我觉得很实际的方向是记忆的可解释性。当 Agent 基于某条记忆做了决定用户应该能追溯“它是根据哪条记忆做的”。这在医疗、金融这类场景里是刚需。hindsight 这个思路天然适合做这件事因为记忆本身就是从复盘里来的每条都有来源。最后分享一个我最近在用的技巧给记忆条目加一个confidence字段表示这条记忆的可信度。从用户明确表达里提取的记忆给高分从模型推断里来的给低分。检索时按 confidence 加权能有效降低幻觉记忆的干扰。这个小改动成本很低但效果挺明显。如果你也在做 Agent 记忆相关的东西我的建议是先从最小可用的版本跑起来别一上来就追求完美的架构。记忆系统的很多问题只有真正跑起来、积累了一定数据量之后才会暴露。先让它能存能取再慢慢优化检索和遗忘这个顺序比较稳。