
1. 从“hindsight”这个词说起为什么记忆是Agent落地的最后一公里“hindsight”这个词本身很有意思字面意思是“事后的聪明”也就是我们常说的“后见之明”。把这个词拿来命名一个跟 agent memory 相关的项目起名的人显然是想表达一个核心痛点大多数 Agent 在当下这一刻是“失忆”的只有事后回看才知道自己错过了什么。我接触过不少做 LLM 应用落地的团队大家一开始都把精力砸在 prompt 调优、工具调用、RAG 检索上等到真正要跑一个长周期任务的时候才发现Agent 记不住东西这件事比模型能力不足还要致命。你让它帮你跟进一个持续两周的项目第一天它记得你的偏好第三天就开始重复问你已经回答过的问题第七天它甚至忘了自己之前做过什么决策。这不是模型笨是记忆架构没搭对。这篇内容我想聊的就是围绕 agent memory 这一整套东西——从 working memory 的存储设计到 MCP 协议怎么把记忆能力标准化再到用 Docker 把整套环境跑起来。关键词里出现的 hindsight、agent memory、LLM、MCP、Docker基本覆盖了当前 Agent 记忆系统从概念到落地的完整链路。不管你是刚听说 MCP 想搞清楚它到底解决什么问题还是已经在做 Agent 项目但被记忆问题卡住这篇都能给你一些能直接上手的东西。需要先说明一点hindsight 这个标题本身信息量很少正文和关键词都是空的所以我会基于 agent memory 这个领域当前的主流实践来展开把“事后回看”这个核心隐喻拆解成可落地的记忆架构设计。如果你手上正好有一个需要长期记忆的 Agent 项目这篇的很多思路可以直接抄。2. Agent 的 working memory 到底该存什么token 三元组的启示2.1 从“我是谁、我在找什么、我能提供什么”理解记忆的本质热词里有一条特别值得琢磨“llm的token三个点key我是谁、query我在找什么、value我能提供什么”。这句话把 Transformer 里 attention 机制的 QKV 三元组翻译成了 Agent 记忆设计的三个基本问题。我觉得这个类比非常精准值得展开讲。在 attention 机制里Query 是“我在找什么”Key 是“我有什么可以被匹配的”Value 是“匹配上之后我实际提供的内容”。Agent 的记忆系统本质上也在做同样的事当前任务状态是 Query历史记忆条目是 Key具体记忆内容是 Value。区别在于attention 里的 QKV 是每次前向计算时临时生成的而 Agent 的记忆需要跨会话、跨任务持久化。所以 working memory 的设计第一个要回答的问题就是存什么。我的经验是至少要分三层身份层我是谁Agent 的角色设定、用户画像、长期偏好。这部分变化频率极低但每次对话都要加载适合放在最外层做缓存。任务层我在找什么当前正在进行的任务目标、子任务拆解、进度状态。这部分变化频率中等任务切换时需要更新。交互层我能提供什么最近几轮对话的上下文、工具调用结果、临时变量。这部分变化频率最高需要做滑动窗口或摘要压缩。很多团队一上来就把所有东西塞进一个向量库结果检索的时候噪声极大因为身份层和交互层混在一起语义相似度根本区分不开。正确的做法是分层存储、分层检索身份层用结构化存储任务层用图结构或关系型存储交互层才用向量存储。2.2 working memory 和 long-term memory 的边界在哪里这里有个常见的误区很多人把 working memory 理解成“短期记忆”把 long-term memory 理解成“长期记忆”然后按时间长短来划分。这个划分方式在实际工程里会出问题因为“短期”和“长期”的边界非常模糊一周算短期还是长期我更倾向于按访问模式来划分。working memory 是当前推理步骤需要直接访问的记忆它的特点是高频读写、容量有限、生命周期跟任务绑定。long-term memory 是跨任务复用的记忆特点是低频写入、容量大、生命周期跟 Agent 实例绑定。举个具体例子。你在做一个代码助手 Agent用户说“帮我重构这个函数”。working memory 里存的是当前文件路径、函数签名、用户之前提到的编码风格偏好、最近三次工具调用的结果。long-term memory 里存的是这个用户历史上所有项目的技术栈偏好、他常犯的代码坏味道类型、他团队内部的命名规范。当任务结束时working memory 里有一部分内容需要“晋升”到 long-term memory。比如用户这次明确说“我们团队禁止用 var”这条信息就应该从交互层提升到身份层。这个晋升机制是 hindsight 这个概念的核心——事后回看把值得记住的东西固化下来。2.3 记忆写入的时机比存储格式更重要我见过太多项目在存储格式上纠结半天用什么向量库、什么 embedding 模型、什么索引结构但真正决定记忆系统好不好用的是写入时机。如果你每轮对话都往记忆里写很快就会被噪声淹没。如果你只在任务结束时写又会丢失过程中的关键决策。我的做法是设置三个写入触发点显式触发用户明确说“记住这个”或“以后都这样”立即写入 long-term memory。决策触发Agent 做出一个影响后续步骤的决策时比如选择了某个方案、排除了某个选项写入任务层记忆。异常触发工具调用失败、用户纠正 Agent 的错误、出现预期外的结果时写入交互层并标记为高优先级。这三个触发点覆盖了绝大多数需要记忆的场景同时避免了无差别写入带来的噪声。实测下来记忆检索的准确率比全量写入能提升一大截。3. MCP 协议在记忆系统里扮演什么角色别把它当成硬件协议3.1 MCP 是软件协议解决的是“记忆怎么被访问”的问题热词里有一条问得特别实在“mcp 是软件协议 硬件协议那个概念叫什么来着”。这个问题背后反映的是很多人第一次听到 MCP 时的困惑。MCP 全称是 Model Context Protocol它是一个软件层面的通信协议跟硬件协议比如 I2C、SPI、USB 这些完全是两码事。硬件协议解决的是物理设备之间怎么传电信号MCP 解决的是 LLM 应用和外部能力之间怎么传结构化数据。在 agent memory 这个场景里MCP 的价值在于把记忆能力标准化成一种可插拔的服务。在没有 MCP 之前你要给 Agent 加一个记忆模块得自己定义接口、自己处理序列化、自己管理连接。有了 MCP 之后记忆服务可以做成一个独立的 MCP ServerAgent 作为 MCP Client 通过标准协议去调用。这意味着什么意味着你的记忆后端可以从本地文件换成 Redis再换成向量数据库Agent 侧的代码几乎不用改。因为 MCP 协议把“记忆的存取”抽象成了工具调用Agent 只需要知道“有一个叫 memory_write 的工具可以写记忆有一个叫 memory_search 的工具可以查记忆”具体后端是什么它不关心。3.2 用 MCP 封装记忆服务的具体做法我拿一个实际跑通的方案来说。假设你要做一个基于 MCP 的记忆服务核心工具定义大概是这样的{ tools: [ { name: memory_write, description: 写入一条记忆, inputSchema: { type: object, properties: { content: {type: string}, layer: {type: string, enum: [identity, task, interaction]}, priority: {type: integer, minimum: 1, maximum: 5}, tags: {type: array, items: {type: string}} }, required: [content, layer] } }, { name: memory_search, description: 检索相关记忆, inputSchema: { type: object, properties: { query: {type: string}, layer: {type: string}, top_k: {type: integer, default: 5} }, required: [query] } } ] }这个定义看起来简单但有几个设计决策值得说明。第一layer 字段是必填的强制调用方明确记忆属于哪一层避免所有记忆混在一起。第二priority 用 1-5 的整数而不是布尔值因为记忆的重要性是有梯度的检索时可以根据 priority 做加权。第三tags 用数组方便后续做多维度过滤。MCP Server 的实现可以用任何语言Python 的话用mcp这个官方库核心就是实现list_tools和call_tool两个方法。后端存储我建议先用 SQLite 跑通因为零配置、单文件、支持全文检索等验证完逻辑再换向量库也不迟。3.3 MCP 记忆服务的性能陷阱这里有个坑我必须提前说。MCP 是基于 JSON-RPC 的每次工具调用都有序列化和网络往返的开销。如果你把记忆检索做成每轮对话都调用一次延迟会很明显。我的做法是在 Agent 侧加一层本地缓存把最近用过的记忆条目缓存在内存里只有缓存未命中时才走 MCP 调用。另外MCP Server 的memory_search如果直接做向量检索embedding 计算本身就很耗时。实测下来用本地的小模型做 embedding比如 bge-small 这类单次检索在 50ms 左右可以接受。如果用远程 API 做 embedding延迟会到 200ms 以上在交互式场景里体感就很差了。还有一个容易被忽略的点MCP 协议本身不定义记忆的过期策略。你得自己在 Server 侧实现 TTL 或者 LRU 淘汰否则记忆库会无限膨胀检索质量也会随着噪声增加而下降。我一般给交互层记忆设 7 天 TTL任务层设 30 天身份层永久保留但定期做去重合并。4. 用 Docker 把记忆系统跑起来从安装到编排的完整路径4.1 Docker Desktop 安装中最容易卡住的几个点热词里关于 Docker 的问题特别多什么“docker安装失败”“virtualization support not detected”“windows11 安装docker desktop”说明这是很多人的第一道坎。我把最常见的几个问题梳理一下。第一个坑是虚拟化没开。Windows 上装 Docker Desktop 需要开启 Hyper-V 或 WSL2如果 BIOS 里虚拟化被禁用了Docker Desktop 启动时会直接报 “virtualization support not detected”。解决办法是进 BIOS 把 Intel VT-x 或 AMD-V 打开然后在 Windows 功能里启用“虚拟机平台”和“适用于 Linux 的 Windows 子系统”。第二个坑是 WSL2 内核没更新。即使开了虚拟化如果 WSL2 内核版本太旧Docker Desktop 还是会启动失败。这时候需要手动下载 WSL2 内核更新包安装或者用wsl --update命令更新。第三个坑是端口冲突。Docker Desktop 默认会用一些端口如果你本机已经装了 MySQL、Redis 之类的服务端口被占用会导致容器启动失败。我一般会在docker-compose.yml里显式指定端口映射避开常用端口。安装完成之后验证是否正常的最快方式是跑一个 hello-worlddocker run hello-world如果能看到 “Hello from Docker!” 的输出说明基础环境没问题。接下来就可以开始编排记忆系统了。4.2 用 docker-compose 编排记忆服务的完整配置一个典型的 agent memory 系统我建议至少包含三个服务记忆存储比如 Redis 或 PostgreSQL、向量检索比如 Qdrant 或 Milvus、MCP Server。用 docker-compose 编排的配置大概长这样version: 3.8 services: memory-store: image: redis:7-alpine ports: - 6380:6379 volumes: - ./data/redis:/data command: redis-server --appendonly yes restart: unless-stopped vector-db: image: qdrant/qdrant:latest ports: - 6333:6333 - 6334:6334 volumes: - ./data/qdrant:/qdrant/storage restart: unless-stopped mcp-memory-server: build: ./mcp-server ports: - 8080:8080 environment: - REDIS_URLredis://memory-store:6379 - QDRANT_URLhttp://vector-db:6333 - EMBEDDING_MODELbge-small-zh depends_on: - memory-store - vector-db restart: unless-stopped这个配置里有几个细节值得说。Redis 端口映射到 6380 而不是 6379是为了避开本机可能已经运行的 Redis 实例。Qdrant 同时暴露 6333 和 6334前者是 HTTP API后者是 gRPCMCP Server 用哪个都行。volumes 挂载到本地目录这样容器重建数据不丢调试的时候也能直接看文件。MCP Server 的 Dockerfile 我一般这么写FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8080 CMD [python, -m, mcp_server.main]requirements.txt里核心就几个包mcp、redis、qdrant-client、sentence-transformers。注意sentence-transformers会拉 PyTorch镜像会比较大如果嫌大可以换成onnxruntime加 ONNX 格式的模型。4.3 容器网络不通的排查链路“docker网络不通”是热词里另一个高频问题。在记忆系统这种多容器场景里网络不通的表现通常是 MCP Server 连不上 Redis 或 Qdrant。排查链路我一般按这个顺序走确认容器是否在同一网络。docker-compose 默认会创建一个 bridge 网络所有服务都在里面。用docker network ls看网络列表用docker network inspect 网络名看容器是否都接入了。确认服务名解析。在 compose 网络里容器之间用服务名互相访问比如memory-store:6379。如果 MCP Server 里写的是localhost:6379那肯定连不上因为 localhost 指向容器自己。确认端口监听地址。有些服务默认只监听 127.0.0.1容器间访问需要监听 0.0.0.0。Redis 默认是监听所有地址的但如果你改过配置就要注意。用docker exec进容器测试。在 MCP Server 容器里执行ping memory-store或nc -zv memory-store 6379能快速定位是 DNS 问题还是端口问题。我踩过最坑的一次是 Qdrant 的 gRPC 端口没暴露MCP Server 用 gRPC 连一直超时换成 HTTP API 就通了。所以配置里我习惯把 HTTP 和 gRPC 端口都暴露出来省得后面折腾。5. 记忆检索的质量调优从“能查到”到“查得准”5.1 混合检索比纯向量检索靠谱得多很多人做记忆检索第一反应就是上向量数据库觉得语义相似度能解决一切。实际跑下来你会发现纯向量检索在记忆场景里问题很大。因为记忆条目往往很短语义信息不充分embedding 的质量很不稳定。用户说“上次那个方案”向量检索可能返回一堆不相关的“方案”相关记忆但真正要找的那条可能因为表述差异排在第 10 位。我的做法是混合检索向量检索 关键词检索 结构化过滤三路结果做融合排序。具体来说向量检索负责语义召回用 embedding 相似度取 top 20。关键词检索用 BM25 或全文索引取 top 20。结构化过滤根据 layer、tags、时间范围做硬过滤。三路结果用 RRFReciprocal Rank Fusion融合公式很简单score sum(1 / (k rank))k 一般取 60。这个融合方式不需要调参实测效果比加权求和稳定得多。5.2 记忆摘要把长对话压缩成可检索的条目原始对话记录直接存进记忆库检索效果很差因为一条对话可能几百字embedding 之后语义被稀释了。我的做法是在写入前做一层摘要把对话压缩成 1-2 句话的记忆条目。摘要的 prompt 我一般这么写请将以下对话压缩成一条记忆条目要求 1. 保留关键决策、用户偏好、事实性信息 2. 去掉寒暄、重复确认、无关内容 3. 用第三人称陈述不超过 50 字 4. 如果对话中没有值得记忆的内容返回空 对话内容 {conversation}这个摘要步骤会增加写入延迟但换来的是检索质量的大幅提升。实测下来摘要后的记忆条目检索准确率比原始对话高 40% 以上。如果对延迟敏感可以把摘要做成异步任务写入时先存原始内容后台慢慢摘要替换。5.3 记忆冲突的处理策略长期运行的 Agent 一定会遇到记忆冲突用户上个月说“我喜欢简洁的代码风格”这个月说“帮我写详细一点”。两条记忆都存着检索时都返回Agent 就懵了。处理冲突有几种策略我常用的是时间加权 显式覆盖。时间加权是在检索排序时给新记忆更高的权重比如final_score similarity * (1 0.1 * recency_factor)。显式覆盖是在写入时检测冲突如果新记忆和旧记忆在同一主题上矛盾就把旧记忆标记为 superseded检索时默认不返回。检测冲突可以用 LLM 做把新旧两条记忆一起丢给模型问“这两条记忆是否矛盾”。这个判断的准确率挺高的成本也不大因为只在写入时做一次。6. 几个实际项目里踩过的坑和对应解法6.1 记忆膨胀导致检索变慢项目跑了一个月之后记忆库从几百条涨到几万条检索延迟从 50ms 涨到 500ms。原因是向量检索是 O(n) 的条目多了自然慢。解法是给向量库建索引Qdrant 支持 HNSW 索引建好之后检索能回到 50ms 以内。另外要定期做记忆合并把相似度极高的条目合并成一条减少总量。6.2 MCP Server 重启导致记忆丢失早期我把记忆存在 MCP Server 进程的内存里结果每次重启容器记忆就没了。后来改成 Redis 持久化 Qdrant 持久化容器重启数据还在。这里要注意 Redis 要开 AOF 持久化Qdrant 要挂载 volume否则数据还是在容器层重建就丢。6.3 embedding 模型选型影响检索质量一开始用 OpenAI 的 embedding API效果不错但延迟高、成本高。后来换成 bge-small-zh 本地模型检索质量下降了一点但延迟从 200ms 降到 30ms综合体验反而更好。如果对质量要求极高可以用 bge-large但需要 GPU 才能跑得动。选型的核心是平衡质量、延迟、成本没有绝对最优解。6.4 记忆写入的幂等性问题Agent 有时候会重复调用 memory_write 写入相同内容导致记忆库里有大量重复条目。解法是在写入前做去重检查用 embedding 相似度判断是否已有相似记忆超过阈值就跳过写入或更新已有条目。这个检查会增加写入延迟但能显著控制记忆库的膨胀速度。7. 关于 hindsight 这个命名的一点个人理解回到标题本身。“hindsight”这个词在 agent memory 语境下我理解它想强调的是事后回看的能力。大多数 Agent 是“当下驱动”的只根据当前输入做决策不会主动回看历史。而真正好用的 Agent 应该具备 hindsight——在做出决策之前先回看过去有没有类似的场景、有没有相关的偏好、有没有踩过的坑。这个能力的实现技术上就是记忆检索 上下文注入。但设计理念上它要求 Agent 的推理循环里有一个“回看”步骤而不是直接进入“行动”步骤。我在自己的项目里是在 system prompt 里加了一段指令要求 Agent 在回答前先调用 memory_search 检索相关记忆把结果作为上下文的一部分再生成回复。这个改动很小但效果提升很明显。如果你正在做 Agent 项目我建议先把记忆系统搭起来哪怕只是最简单的 SQLite 关键词检索也比没有强。等跑通了再逐步升级到向量检索、混合检索、MCP 标准化。记忆这件事早做早受益越晚做迁移成本越高。