拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

hindsight 实战:基于 MCP 与 Docker 构建 LLM Agent 持久化记忆系统

hindsight 实战:基于 MCP 与 Docker 构建 LLM Agent 持久化记忆系统

1. 从“事后诸葛亮”说起:hindsight 到底想解决什么问题

第一次看到 “hindsight” 这个词,我脑子里蹦出来的就是“事后诸葛亮”。但在 agent memory 这个语境里,它其实指向一个非常具体、非常工程化的痛点:当 LLM Agent 执行完一段任务之后,我们能不能让它回过头去,把这段经历真正“记住”,并且在下次遇到类似场景时用上?

我接触过不少基于 LLM 的 Agent 项目,从最早的纯 prompt 拼接,到后来的 RAG 检索增强,再到最近一年火起来的 MCP 协议生态。说实话,大部分 Agent 的“记忆”都是假的。它们所谓的记忆,无非是把对话历史塞进 context window,或者把几段文本丢进向量库做相似度检索。这两种做法在简单场景下能用,但一旦任务链条变长、工具调用变多、跨会话需求出现,立刻就露馅了。

hindsight 这个项目标题,加上 agent memory、LLM、MCP、Docker 这几个关键词,基本可以判断它要做的是一套面向 Agent 的持久化记忆系统,而且大概率是以 MCP 服务的形式对外暴露能力,用 Docker 做部署封装。这个组合在当下的技术栈里非常合理:MCP 负责标准化工具调用接口,Docker 负责环境一致性,LLM 负责理解和生成,agent memory 负责把“经历”沉淀下来。

那它到底解决了什么问题?我举个例子你就明白了。假设你有一个 Agent 帮你处理日常的代码审查任务。第一次它审查了一个 Python 项目,发现了几类常见问题:类型注解缺失、异常处理过于宽泛、日志级别用错。如果没有 hindsight 这样的记忆机制,第二次它审查另一个 Python 项目时,这些经验就全部归零了,它还是会从头开始“摸索”。而有了 hindsight,它可以把第一次的审查结论、修复建议、甚至你当时的反馈都存下来,第二次直接调用这些经验,效率和质量都会有明显提升。

适合谁来参考这篇文章?我认为有三类人:第一类是正在做 LLM Agent 应用开发的工程师,尤其是那些被“记忆”问题折磨过的;第二类是对 MCP 协议感兴趣、想了解怎么把自定义能力接入 Agent 生态的开发者;第三类是习惯用 Docker 做本地开发和部署、想快速跑通一个完整示例的技术爱好者。不管你是哪一类,下面的内容都会尽量把原理、操作和踩坑经验讲透。

2. 整体设计思路:为什么是 MCP + Docker + 持久化存储

2.1 为什么选 MCP 而不是直接写 API

MCP 全称 Model Context Protocol,是一个软件协议,不是硬件协议。它的核心价值在于把“工具”和“模型”解耦。在没有 MCP 之前,如果你想让 LLM 调用一个自定义函数,通常有两种做法:一种是在 prompt 里描述函数签名,让模型输出特定格式的文本,然后你自己解析;另一种是用各家平台自己的 function calling 格式,比如 OpenAI 的 tools 参数。这两种做法的问题都很明显:前者不稳定,模型经常输出格式错误的文本;后者绑平台,换一个模型供应商就得重写一遍。

MCP 的思路是定义一个标准化的协议,工具提供方实现一个 MCP Server,模型调用方实现一个 MCP Client,双方通过 JSON-RPC 通信。这样一来,你写的记忆服务只需要实现一次 MCP Server,就能被所有支持 MCP 的客户端使用。我实测下来,这个解耦带来的好处在长期维护中非常明显:你不需要关心客户端是 Codex、是 Dify、还是别的什么,只要它支持 MCP,你的服务就能接进去。

hindsight 选择 MCP 作为对外接口,说明它的定位不是一个孤立的记忆库,而是一个可以被各种 Agent 调用的记忆能力层。这个定位很聪明,因为记忆本身不是目的,被用起来才是。

2.2 为什么用 Docker 做部署封装

Docker 在这个项目里的角色是环境一致性保障。记忆系统通常依赖数据库(比如 PostgreSQL、Redis、或者向量数据库)、依赖特定的 Python 或 Node 运行时、依赖一些系统库。如果让每个用户自己配环境,光是版本冲突就能劝退一大半人。用 Docker 封装之后,用户只需要一条docker compose up就能把整套服务跑起来,这对项目的传播和落地至关重要。

而且 Docker 还有一个隐性好处:资源隔离。记忆系统可能会跑 embedding 模型、可能会做大量的向量检索,这些操作对 CPU 和内存的占用不小。用容器隔离之后,即使记忆服务跑崩了,也不会影响你本机的其他开发环境。我在自己的机器上就遇到过向量检索把内存吃满导致整个系统卡死的情况,后来把所有重资源服务都放进 Docker 并限制内存之后,世界清净了。

2.3 记忆的分层设计:working memory 与 long-term memory

热词里出现了 “agent 存储 working memory”,这其实点出了记忆系统的一个关键设计:记忆不是一坨,而是分层的。我在实际项目里通常会把记忆分成至少三层:

层级存储内容生命周期典型实现
Working Memory当前任务的中间状态、临时变量单次会话内存 / Redis
Episodic Memory具体经历、任务执行记录数天到数月关系库 + 向量库
Semantic Memory抽象出的规律、偏好、知识长期向量库 + 图数据库

hindsight 如果要做一套完整的 agent memory,大概率也会采用类似的分层。Working memory 负责让 Agent 在单次任务中不“失忆”,episodic memory 负责让 Agent 记住“我做过什么”,semantic memory 负责让 Agent 记住“我学到了什么”。这三层的读写频率、存储介质、检索方式都不一样,混在一起做必然出问题。

2.4 与 RAG 的区别:记忆不是检索

很多人会把 agent memory 和 RAG 混为一谈,觉得都是“存文本、查文本”。这个理解是片面的。RAG 的核心是从静态知识库中检索相关信息,它假设知识是不变的、外部的。而 agent memory 的核心是从 Agent 自身的经历中积累和调用经验,它假设记忆是动态增长的、与 Agent 的行为强相关的。

举个具体的区别:RAG 检索出来的是一段文档,Agent 用它来回答问题;agent memory 检索出来的是一段“我上次遇到类似情况时是怎么做的”,Agent 用它来指导行动。前者是知识,后者是经验。hindsight 这个名字本身就暗示了“回头看”的动作,它更偏向后者。

3. 核心细节解析:记忆的写入、存储与检索

3.1 记忆写入:什么时候该记,记什么

记忆系统的第一个难点不是存储,而是决定记什么。如果什么都记,存储会爆炸,检索会变慢,而且大量噪声会干扰后续的召回。如果记得太少,又起不到“经验积累”的作用。

我在实践中总结了一个简单的判断标准:只记录那些“下次可能用到”的信息。具体来说,以下几类内容值得写入记忆:

  • 任务的目标和最终结果(成功或失败)
  • 执行过程中做出的关键决策及其理由
  • 遇到的异常情况以及处理方式
  • 用户的显式反馈(比如“这个做法不对”“以后都这样处理”)
  • 可复用的参数配置或代码片段

而以下几类内容通常不值得记:

  • 中间过程的琐碎日志
  • 可以从其他来源轻易重建的信息
  • 一次性的、明显不会复现的临时状态

hindsight 如果实现了自动记忆写入,那它内部一定有一套类似的过滤逻辑。这套逻辑的质量直接决定了记忆系统的可用性。我见过一些项目为了“显得智能”,把所有对话都塞进向量库,结果检索出来的全是无关内容,反而拖累了 Agent 的表现。

3.2 存储选型:关系库、向量库还是图数据库

存储选型是另一个关键决策。热词里出现了 tencentdb agent memory,说明国内也有团队在做类似的事情。从工程角度看,记忆存储通常需要满足几个需求:结构化查询、语义检索、关系推理。

  • 结构化查询:比如“找出所有失败的任务记录”,这需要关系库
  • 语义检索:比如“找出与当前任务相似的经历”,这需要向量库
  • 关系推理:比如“A 任务依赖 B 任务,B 任务又依赖 C 任务”,这需要图数据库

一个成熟的记忆系统往往会组合使用多种存储。我的建议是:不要一开始就上全套。先用 PostgreSQL 加 pgvector 扩展,把结构化和向量检索都覆盖了,等真正遇到关系推理的需求再引入图数据库。过早引入复杂存储只会增加运维负担。

hindsight 用 Docker 封装,很可能已经把存储依赖打包好了。如果你要自己部署,我建议至少给 PostgreSQL 分配 2GB 内存,给向量索引预留足够的磁盘空间。向量维度如果是 1536(OpenAI embedding 的常见维度),100 万条记忆大约需要 6GB 左右的原始向量存储,加上索引开销会更多。

3.3 检索策略:相似度不是唯一标准

检索记忆时,很多人第一反应就是算余弦相似度,取 Top-K。这个做法在简单场景下能用,但在记忆系统里往往不够。因为记忆的价值不仅取决于“像不像”,还取决于“新不新”“重不重要”“可不可信”。

我在实际项目里会综合以下几个维度来排序:

  • 语义相似度:基础分,用向量检索得到
  • 时间衰减:越新的记忆权重越高,可以用指数衰减函数计算
  • 重要度:写入时打的分,比如用户显式反馈的记忆重要度更高
  • 使用频率:被召回次数多的记忆,说明它确实有用
  • 置信度:记忆的来源是否可靠,比如来自用户直接输入的记忆比模型自己总结的更可信

最终的排序分数可以是这些维度的加权和。权重怎么定?没有标准答案,需要根据你的具体场景调。我的经验是先用一组拍脑袋的权重跑起来,然后根据实际召回效果慢慢调。别指望一次调好。

3.4 MCP 工具设计:暴露哪些能力给 Agent

hindsight 作为 MCP Server,需要定义一组工具(tools)供 Agent 调用。从记忆系统的功能出发,至少应该包含以下几类工具:

  • memory_write:写入一条记忆,参数包括内容、类型、重要度、元数据
  • memory_search:检索记忆,参数包括查询文本、返回数量、过滤条件
  • memory_update:更新已有记忆,比如修正错误或补充信息
  • memory_forget:删除记忆,用于隐私清理或纠错
  • memory_summarize:对一段记忆做摘要,减少存储和检索开销

工具的参数设计很讲究。比如memory_search如果只接受一个查询字符串,那 Agent 就没法做精细过滤。更好的设计是接受一个结构化的查询对象,包含文本查询、时间范围、记忆类型、重要度阈值等字段。这样 Agent 可以根据具体需求灵活组合。

提示:MCP 工具的 description 字段非常重要,它是 LLM 决定是否调用这个工具的主要依据。description 要写清楚“这个工具做什么”“什么时候该用”“参数是什么意思”,不要写得太简略。

4. 实操过程:从零把 hindsight 跑起来

4.1 环境准备:Docker 安装与常见问题

假设你用的是 Windows 11,第一步是安装 Docker Desktop。这个过程本身不复杂,但有几个坑我踩过,这里提前说。

首先,Docker Desktop 依赖 WSL2(Windows Subsystem for Linux 2)。如果你之前没启用过 WSL,安装程序会提示你启用。启用之后需要重启电脑,这一步别跳过。重启之后,打开 PowerShell 运行wsl --update确保 WSL 内核是最新的。

其次,可能会遇到 “Virtualization support not detected” 的错误。这通常是因为 BIOS 里的虚拟化技术(Intel VT-x 或 AMD-V)没有开启。你需要重启进入 BIOS 设置,找到虚拟化相关的选项并启用。不同主板的选项名称不一样,常见的有 “Intel Virtualization Technology”“SVM Mode”“AMD-V” 等。

安装完成后,打开 PowerShell 运行:

docker --version docker compose version

如果两条命令都能正常输出版本号,说明安装成功。如果docker compose报错,可能是你的 Docker Desktop 版本较老,需要更新到较新版本,因为 compose 现在是内置的插件形式。

注意:国内网络环境下拉取 Docker 镜像可能会很慢甚至超时。建议在 Docker Desktop 的设置里配置镜像加速器。具体配置方法因网络环境而异,这里不展开,你可以根据自己的实际情况处理。

4.2 获取 hindsight 项目并理解目录结构

假设你已经拿到了 hindsight 的项目文件(通常是一个 Git 仓库或者一个压缩包),解压后进入项目根目录。一个典型的 MCP + Docker 项目结构大概长这样:

hindsight/ ├── docker-compose.yml ├── Dockerfile ├── .env.example ├── src/ │ ├── server.py # MCP Server 主入口 │ ├── memory/ │ │ ├── writer.py # 记忆写入逻辑 │ │ ├── searcher.py # 记忆检索逻辑 │ │ └── storage.py # 存储层封装 │ └── config.py # 配置加载 ├── requirements.txt └── README.md

先别急着启动,花五分钟看一下docker-compose.yml和.env.example。这两个文件决定了服务怎么跑、依赖什么、需要配哪些环境变量。常见的环境变量包括数据库连接串、embedding 模型的 API Key、服务监听端口等。

4.3 配置环境变量与启动服务

复制.env.example为.env,然后根据你的实际情况填写。几个关键配置项:

# 数据库连接 DATABASE_URL=postgresql://hindsight:hindsight@db:5432/hindsight # embedding 模型配置 EMBEDDING_MODEL=text-embedding-3-small EMBEDDING_API_KEY=your_api_key_here # 服务端口 MCP_SERVER_PORT=8080 # 记忆检索默认返回数量 DEFAULT_SEARCH_LIMIT=10

这里解释一下为什么数据库连接串里的 host 是db而不是localhost。因为在 Docker Compose 里,各个服务在同一个网络中,服务之间通过服务名互相访问。db就是数据库服务的名字,Docker 内置的 DNS 会把它解析到对应的容器 IP。如果你写成localhost,容器会尝试连接自己内部的 5432 端口,而不是数据库容器的,必然失败。

配置好之后,启动服务:

docker compose up -d

-d表示后台运行。启动之后用docker compose ps查看各容器状态,确保都是running或healthy。如果有容器反复重启,用docker compose logs <服务名>看日志排查。

4.4 验证 MCP Server 是否正常工作

服务起来之后,需要验证 MCP Server 是否能正常响应。最直接的方式是用一个 MCP Client 去连接它。如果你用的是支持 MCP 的编辑器或工具,在配置里添加这个 Server 的地址即可。

如果手头没有现成的 MCP Client,也可以用 curl 手动发一个 JSON-RPC 请求测试:

curl -X POST http://localhost:8080/mcp \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} }'

如果返回的 JSON 里包含memory_write、memory_search等工具定义,说明 Server 正常。如果返回连接拒绝,检查端口映射是否正确;如果返回方法不存在,检查 MCP 协议的版本是否匹配。

4.5 写入第一条记忆并检索验证

验证工具列表之后,下一步是实际写入一条记忆,然后检索出来,确认整个链路通畅。

写入记忆的请求大概长这样:

curl -X POST http://localhost:8080/mcp \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "memory_write", "arguments": { "content": "在处理 Python 项目时,发现类型注解缺失是最常见的问题,建议优先检查函数签名。", "type": "semantic", "importance": 0.8, "metadata": {"language": "python", "category": "code_review"} } } }'

然后检索:

curl -X POST http://localhost:8080/mcp \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "memory_search", "arguments": { "query": "Python 代码审查常见问题", "limit": 5 } } }'

如果检索结果里包含刚才写入的那条记忆,说明写入、存储、向量化、检索这条链路全部打通了。这一步看起来简单,但实际上是整个系统最核心的验证点。很多问题(比如 embedding 维度不匹配、向量索引没建好、数据库权限不足)都会在这一步暴露出来。

5. 常见问题与排查技巧实录

5.1 Docker 相关的高频问题

问题一:docker compose up卡在拉取镜像

这通常是网络问题。先确认你的镜像加速器配置是否生效,可以尝试docker pull一个基础镜像测试速度。如果加速器也不行,考虑换一个时间段重试,或者使用离线镜像包。

问题二:数据库容器启动失败,日志显示权限错误

PostgreSQL 容器对数据目录的权限很敏感。如果你把宿主机目录挂载到容器里,需要确保目录的属主和容器内的 postgres 用户匹配。最简单的做法是不挂载宿主机目录,用 Docker volume 管理数据。如果一定要挂载,在宿主机上执行chown -R 999:999 ./data(999 是 postgres 容器内用户的常见 UID)。

问题三:容器之间网络不通

先确认它们在同一个 Docker network 里。docker compose默认会创建一个网络,所有服务都在里面。如果你手动指定了 network,检查服务是否都加入了。另外,容器之间用服务名通信,不要用localhost或127.0.0.1。

5.2 MCP 协议相关的排查

问题:Client 找不到 MCP Server

热词里出现了 “codex无法找到mcp”,这是个典型问题。排查顺序是:先确认 Server 进程在跑,再确认端口在监听,然后确认 Client 配置的地址和端口正确,最后确认协议版本匹配。MCP 协议在快速演进,不同版本的 Client 和 Server 可能不兼容。如果 Server 支持多个协议版本,在初始化握手时会协商;如果不支持,就会直接失败。

问题:工具调用返回 schema 错误

热词里有 “llm request failed: provider rejected the request schema or tool payload”,这通常是工具的参数 schema 定义有问题。检查 JSON Schema 是否符合规范,必填字段是否标注了required,类型是否匹配。有些模型对 schema 的严格程度不一样,建议用最宽松、最明确的 schema 定义。

5.3 记忆质量相关的调优

问题:检索出来的记忆不相关

先检查 embedding 模型是否适合你的语言和领域。有些 embedding 模型对中文支持一般,换一个针对中文优化的模型会有明显改善。其次检查检索的排序策略,纯相似度排序容易召回“看起来像但实际没用”的记忆,加入时间衰减和重要度加权通常能改善。

问题:记忆越存越多,检索越来越慢

这是必然的,需要做记忆的“遗忘”和“压缩”。定期把低重要度、长期未被召回的 memory 归档或删除。对于 episodic memory,可以定期做摘要,把多条相关记忆合并成一条更高层的 semantic memory。这个过程可以手动触发,也可以做成定时任务。

5.4 常见问题速查表

现象可能原因排查方向
容器启动后立即退出配置错误或依赖缺失docker compose logs看日志
数据库连接超时网络不通或认证失败检查连接串、网络配置、用户权限
检索结果为空向量索引未建或维度不匹配检查 embedding 维度和索引配置
MCP 工具列表为空Server 未正确注册工具检查 Server 启动日志和工具注册代码
写入记忆报错字段类型或长度超限检查 schema 定义和实际数据
检索速度慢索引缺失或数据量过大检查索引、考虑分片或归档

6. 记忆系统的扩展方向与个人经验

6.1 从单 Agent 记忆到多 Agent 共享记忆

hindsight 如果只服务单个 Agent,价值是有限的。真正有意思的是多 Agent 共享记忆:一个 Agent 学到的经验,其他 Agent 也能用。这需要解决几个问题:记忆的命名空间隔离、权限控制、冲突解决。比如 Agent A 和 Agent B 对同一件事有不同的记忆,检索时该信谁?我的做法是给每条记忆打上来源标签,检索时根据当前 Agent 的信任策略做加权。

6.2 记忆的主动遗忘与隐私保护

记忆系统必须支持“遗忘”。用户可能要求删除某些记忆,或者某些记忆因为时效性已经不再适用。遗忘不只是删除数据,还要清理相关的向量索引、缓存、摘要。如果用了图数据库,还要处理节点和边的删除。这块做不好,轻则检索出过时信息,重则造成隐私泄露。

6.3 与 LLM 微调的配合

热词里出现了 “使用聊天记录模型精调llm”,这其实和记忆系统是互补的。记忆系统解决的是“运行时调用经验”的问题,微调解决的是“把经验内化到模型参数”的问题。两者可以配合:先用记忆系统积累高质量的经验数据,定期把这些数据整理成微调数据集,微调后的模型再配合记忆系统使用。这样模型本身越来越强,记忆系统的负担也越来越轻。

6.4 我踩过的几个坑

第一个坑是过度依赖向量检索。早期我做的记忆系统只有向量检索,结果发现很多明明存过的记忆检索不出来,因为用户的查询措辞和记忆的原始措辞差异太大。后来加入了关键词检索和结构化过滤,召回率明显提升。混合检索(hybrid search)现在是标配。

第二个坑是忽视写入时的去重。同一个经验被反复写入,导致检索结果里全是重复内容。后来在写入前加了一步相似度检查,超过阈值的就不重复写入,而是更新已有记忆的权重和时间戳。

第三个坑是没有做记忆的版本管理。记忆被更新后,旧版本直接丢了,导致无法追溯。后来给记忆加了版本字段,每次更新保留历史版本,需要时可以回滚。

6.5 一个实用的小技巧

如果你在本地开发时觉得每次都要启动完整的 Docker 栈太慢,可以做一个“轻量模式”:用一个内存版的存储替代数据库,用一个简单的关键词匹配替代向量检索。这样启动只要几秒钟,适合快速迭代 MCP 工具的逻辑。等逻辑稳定了,再切回完整模式做集成测试。这个技巧帮我省了大量等待容器启动的时间。

记忆系统这个东西,说起来概念不复杂,但真正做好用、做稳定,需要在写入策略、存储选型、检索排序、遗忘机制这几个环节反复打磨。hindsight 这个项目名起得好,它提醒我们:Agent 的智能不仅来自向前看的能力,也来自回头看的能力。把经历沉淀成记忆,把记忆转化成经验,这条路还很长,但方向是对的。

返回列表