☰
基于MCP与Docker的Agent长期记忆系统hindsight架构设计与实现
2026/9/29 16:38:00 网站建设 项目流程

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

第一次看到hindsight这个词,我脑子里蹦出来的就是“事后诸葛亮”。英文里 hindsight 的意思就是“事后的洞察力”,说白了就是回头看的时候才发现“哦,原来当时应该这么干”。把这个词用在 agent memory 这个方向上,其实非常精准——因为现在绝大多数 LLM Agent 的记忆系统,本质上都是“事后记录”,而不是“事前预判”。

我接触 agent memory 这个方向大概有一年多时间,从最早的简单对话历史拼接,到后来的向量数据库检索,再到最近围绕 MCP 协议做的一整套记忆中间件,踩过的坑可以说相当多。hindsight这个项目标题,结合热搜词里的agent memory、LLM、MCP、Docker,我判断它要解决的核心问题是:让 Agent 在跨会话、跨任务、跨工具调用的场景下,拥有一套可持久化、可检索、可推理的记忆层,而不是每次都从零开始。

为什么这个问题现在变得这么重要?因为 LLM 本身是无状态的。你每次调用 API,模型都是“第一次见你”。上下文窗口再大,也只是一个临时的 working memory,会话一结束就烟消云散。Agent 要真正像一个“助手”而不是“一次性问答机器”,就必须有长期记忆。而 hindsight 这个词暗示的,是记忆不只是存下来,还要能在后续决策中“回头看”,把过去的经验转化为当前的判断依据。

这套东西适合谁来参考?我的判断是三类人:一是正在做 Agent 产品的开发者,尤其是用 MCP 协议做工具编排的;二是对 LLM 应用架构感兴趣、想自己搭一套记忆系统的技术爱好者;三是已经在用 Docker 部署各种服务、想把记忆层也容器化管理的运维向同学。如果你只是调用一下 ChatGPT 的 API 做个聊天机器人,那这套东西可能有点重;但只要你开始做多轮任务、多工具协作的 Agent,记忆层就是绕不过去的坎。

2. 整体架构设计:为什么是 MCP + Docker + 向量存储这套组合

2.1 核心思路拆解:记忆分三层,各司其职

我在设计任何 agent memory 系统的时候,都会先把记忆分层。hindsight 这个项目虽然标题只有一个词,但从热搜词agent 存储 working memory、llm wiki知识库、MCP这些线索来看,它大概率采用的是三层记忆架构:

  • Working Memory(工作记忆):当前会话内的短期上下文,生命周期就是一次任务执行期间。这部分通常直接放在内存里,或者用 Redis 这类高速缓存。
  • Episodic Memory(情景记忆):跨会话的历史交互记录,包括用户说过什么、Agent 做过什么决策、结果如何。这部分需要持久化,通常落到关系型数据库或文档数据库。
  • Semantic Memory(语义记忆):从大量交互中提炼出来的知识、规则、偏好,以向量形式存储,支持语义检索。这部分是llm wiki知识库这类概念的落地形态。

为什么要这么分?因为不同层的记忆,访问频率、数据量级、检索方式完全不同。Working memory 要求毫秒级读写,Episodic memory 要求可靠持久化,Semantic memory 要求高维向量相似度检索。如果混在一起用一套存储,要么性能拉胯,要么成本爆炸。我试过把所有记忆都塞进向量库,结果就是每次简单查询都要走一遍 embedding,延迟高得离谱。

2.2 为什么选 MCP 作为记忆层的接入协议

MCP(Model Context Protocol)这两年在 Agent 圈子里热度很高,热搜词里mcp是什么、mcp协议、mcp server、mcp教程频繁出现,说明很多人还在搞明白它到底是什么。我的理解是:MCP 本质上是一套标准化的“工具/资源暴露协议”,让 LLM 能够以统一的方式调用外部能力。

把记忆层做成 MCP Server,好处非常明显。第一,解耦。记忆系统不用关心上层是哪个 LLM 框架,只要遵循 MCP 协议,Claude、GPT、本地模型都能接。第二,可组合。你可以同时挂多个 MCP Server,一个管记忆,一个管搜索,一个管代码执行,Agent 自己编排。第三,可复用。写一次 MCP Server,所有支持 MCP 的客户端都能用。

我实测下来,用 MCP 做记忆层最大的坑在于工具描述的粒度。如果你把“存记忆”和“取记忆”做成两个粗粒度工具,模型经常不知道该什么时候调用。更好的做法是拆细:store_episodic、store_semantic、recall_by_similarity、recall_by_time_range、forget,每个工具的描述里写清楚适用场景。这样模型的选择准确率会高很多。

2.3 Docker 化部署:为什么不用裸机跑

热搜词里docker、docker desktop、docker安装教程、docker安装mysql8.0、docker安装redis主从一大堆,说明容器化部署已经是标配。hindsight 这类记忆系统涉及多个组件——向量库、关系库、缓存、MCP Server 本身——用 Docker Compose 编排是最省心的。

裸机部署的问题在于依赖冲突。向量库可能依赖特定版本的 Python,关系库要特定版本的 glibc,缓存又是另一套。我早期在一台机器上手动装过 Milvus + PostgreSQL + Redis,光是版本对齐就折腾了一整天。后来全部容器化,docker compose up -d一条命令搞定,迁移的时候把 compose 文件和 volume 一打包,换台机器照样跑。

注意:Windows 上装 Docker Desktop 经常遇到virtualization support not detected或者docker desktop failed to start的问题,本质是 BIOS 里虚拟化没开,或者 WSL2 没装好。这个后面排查章节会细说。

3. 核心组件拆解与实操要点

3.1 向量存储选型:Milvus、Qdrant 还是 pgvector

语义记忆的核心是向量检索,选型直接决定系统上限。我把几个主流方案拉出来对比过:

方案部署复杂度检索性能过滤能力适用场景
pgvector低中等强(SQL)数据量 < 100万,已有 PG
Qdrant中高强中等规模,需要复杂过滤
Milvus高极高中大规模,亿级向量
Chroma极低低弱原型验证

我的建议是:如果你已经在用 PostgreSQL,直接上 pgvector。理由很简单,少维护一个组件。记忆数据量在百万级以下时,pgvector 的 HNSW 索引性能完全够用,而且可以用 SQL 做元数据过滤,比如“只检索最近 7 天、属于某个用户、标签为偏好类的记忆”。这种复合查询在纯向量库里反而不好写。

如果你追求极致性能,或者向量量级要上千万,那就上 Milvus。但要做好心理准备,Milvus 的依赖组件多(etcd、MinIO、Pulsar),Docker Compose 文件能写上百行。我一般只在确实需要的时候才上。

3.2 Embedding 模型选择:本地还是 API

记忆系统要把文本转成向量,就得选 embedding 模型。这里有个关键权衡:用 API 还是本地部署。

API 方案(比如各家云服务商的 embedding 接口)省事,但有两个问题。一是成本,记忆系统是高频写入的,每次对话都要 embedding,量大起来费用不低。二是延迟,多一次网络往返,在实时交互场景里能明显感觉到卡顿。

本地部署方案我推荐bge-m3或者nomic-embed-text,这两个模型在多语言和长文本上表现都不错,用 ONNX 或者 sentence-transformers 都能跑。热搜词里onnx部署llm模型也印证了这个方向。本地部署的代价是需要 GPU 或者至少够用的 CPU,但一旦跑起来,写入延迟能压到 10ms 以内。

实操心得:embedding 模型一旦选定,千万不要中途换。因为不同模型的向量空间不兼容,换了之后历史记忆全部失效,只能重新 embedding 一遍。我踩过这个坑,换模型那天整个记忆库重建,花了六个小时。

3.3 MCP Server 的工具设计:粒度决定成败

前面提到工具粒度,这里展开说。一个记忆型 MCP Server,我通常会暴露这些工具:

# 伪代码示意工具定义 tools = [ { "name": "store_memory", "description": "存储一条记忆。当用户表达了偏好、事实或重要决策时调用。", "parameters": { "content": "记忆内容", "memory_type": "episodic | semantic", "tags": "标签列表", "importance": "1-5 重要度" } }, { "name": "recall_memory", "description": "根据语义相似度检索相关记忆。在回答需要历史上下文的问题前调用。", "parameters": { "query": "检索查询", "top_k": "返回条数", "time_range": "可选时间范围" } }, { "name": "forget_memory", "description": "删除指定记忆。当用户要求遗忘或记忆被判定为过时调用。", "parameters": { "memory_id": "记忆 ID" } } ]

关键在于description要写清楚什么时候调用,而不只是这个工具做什么。模型是根据描述来决定是否调用的,描述里带上触发场景,准确率能提升一大截。

3.4 记忆的写入策略:什么时候该记

这是最容易被忽略但最影响效果的一环。如果 Agent 每句话都往记忆里塞,很快就会被噪声淹没;如果什么都不记,又失去了长期记忆的意义。

我的策略是分级触发:

  • 强制写入:用户明确说“记住”“以后都这样”“我的偏好是”这类指令时,必须写。
  • 规则写入:检测到特定模式时写,比如用户纠正了 Agent 的错误、用户提供了个人信息、任务产生了明确结论。
  • 模型判断写入:让 LLM 自己判断这条信息是否值得长期记忆,用一个轻量 prompt 做二分类。
  • 不写入:闲聊、寒暄、临时性指令,这些不写。

实测下来,规则写入 + 模型判断的组合效果最好。纯靠模型判断会漏,纯靠规则会僵。两者结合,召回率和准确率都能接受。

4. 完整实操流程:从零搭一套 hindsight 记忆层

4.1 环境准备与 Docker 编排

先把基础环境搭起来。假设你在 Linux 或者 macOS 上,Windows 的话建议用 WSL2。

# 检查 Docker 是否就绪 docker --version docker compose version # 创建工作目录 mkdir -p hindsight/{data,config} cd hindsight

然后写docker-compose.yml,把记忆系统需要的组件编排起来:

version: "3.9" services: postgres: image: pgvector/pgvector:pg16 environment: POSTGRES_PASSWORD: hindsight_secret POSTGRES_DB: memory volumes: - ./data/pg:/var/lib/postgresql/data ports: - "5432:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 10s retries: 5 redis: image: redis:7-alpine command: redis-server --appendonly yes volumes: - ./data/redis:/data ports: - "6379:6379" mcp-memory: build: ./mcp-server environment: DATABASE_URL: postgresql://postgres:hindsight_secret@postgres:5432/memory REDIS_URL: redis://redis:6379/0 EMBEDDING_MODEL: bge-m3 depends_on: postgres: condition: service_healthy redis: condition: service_started ports: - "8080:8080"

这里选pgvector/pgvector:pg16镜像,是因为它自带 pgvector 扩展,省得自己编译。Redis 用来做 working memory 的高速缓存,开 AOF 持久化防止重启丢数据。

启动:

docker compose up -d docker compose ps

三个服务都 healthy 之后,进数据库建表:

CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE memories ( id BIGSERIAL PRIMARY KEY, content TEXT NOT NULL, memory_type VARCHAR(20) NOT NULL, tags TEXT[], importance INT DEFAULT 3, embedding vector(1024), created_at TIMESTAMPTZ DEFAULT NOW(), accessed_at TIMESTAMPTZ DEFAULT NOW(), access_count INT DEFAULT 0 ); CREATE INDEX ON memories USING hnsw (embedding vector_cosine_ops); CREATE INDEX ON memories (memory_type); CREATE INDEX ON memories USING gin (tags);

vector(1024)里的 1024 是 bge-m3 的输出维度,换模型的话这个数字要跟着改。HNSW 索引是近似最近邻检索,比暴力扫描快几个数量级,代价是有一点点精度损失,但在记忆检索场景里完全可接受。

4.2 MCP Server 核心逻辑实现

MCP Server 用 Python 写最顺手,官方有 SDK。核心逻辑分三块:写入、检索、遗忘。

写入的时候,先算 embedding,再入库:

import asyncpg from sentence_transformers import SentenceTransformer model = SentenceTransformer("BAAI/bge-m3") async def store_memory(conn, content, memory_type, tags, importance): embedding = model.encode(content, normalize_embeddings=True).tolist() row = await conn.fetchrow( """ INSERT INTO memories (content, memory_type, tags, importance, embedding) VALUES ($1, $2, $3, $4, $5) RETURNING id """, content, memory_type, tags, importance, embedding ) return row["id"]

检索的时候,同样先算 query 的 embedding,然后用余弦距离排序:

async def recall_memory(conn, query, top_k=5, memory_type=None): q_emb = model.encode(query, normalize_embeddings=True).tolist() sql = """ SELECT id, content, memory_type, tags, importance, 1 - (embedding <=> $1) AS similarity FROM memories WHERE ($2::text IS NULL OR memory_type = $2) ORDER BY embedding <=> $1 LIMIT $3 """ rows = await conn.fetch(sql, q_emb, memory_type, top_k) # 更新访问记录,用于后续的重要性衰减 ids = [r["id"] for r in rows] await conn.execute( "UPDATE memories SET accessed_at = NOW(), access_count = access_count + 1 WHERE id = ANY($1)", ids ) return [dict(r) for r in rows]

<=>是 pgvector 的余弦距离操作符,1 - 距离就是相似度。这里有个细节:检索完要更新accessed_at和access_count,这两个字段后面做记忆衰减和重要性排序时会用到。

4.3 记忆衰减与重要性排序

记忆不是存进去就一劳永逸的。时间久了,不常访问的记忆应该逐渐“淡出”,否则检索时会被大量过时信息干扰。我用的衰减公式是:

effective_score = similarity * importance_weight * recency_weight * access_weight

其中:

  • importance_weight= importance / 5,把 1-5 映射到 0.2-1.0
  • recency_weight= exp(-days_since_access / 30),30 天半衰期
  • access_weight= log(1 + access_count) / log(1 + max_access),访问越多权重越高

这个公式不是拍脑袋来的。指数衰减是记忆研究里的经典模型,30 天半衰期是我根据实际使用频率调的——太短了记忆消失太快,太长了噪声太多。访问次数用对数是因为边际效应递减,访问 100 次和 200 次的差别不应该线性放大。

实际检索时,可以先按向量相似度取 top 50,再用这个综合分数重排取 top 5。这样既保证了语义相关性,又兼顾了时效性和重要性。

4.4 与 Agent 框架对接

MCP Server 跑起来之后,在 Agent 侧配置连接。以常见的 MCP 客户端配置为例:

{ "mcpServers": { "hindsight-memory": { "url": "http://localhost:8080/sse", "description": "长期记忆服务,提供存储和检索能力" } } }

Agent 在每轮对话开始前,先调用recall_memory把相关记忆拉出来,拼进 system prompt。对话结束后,根据写入策略决定是否调用store_memory。这个流程听起来简单,但实际调优空间很大——检索几条、怎么拼进 prompt、写入时机怎么把握,每个环节都影响最终效果。

实操心得:检索回来的记忆不要直接堆进 prompt,最好做一次去重和摘要。我遇到过检索出 5 条高度相似的记忆,全塞进去反而让模型困惑。后来加了一步:如果多条记忆相似度都超过 0.9,只保留最新的一条。

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

5.1 Docker 相关故障速查

容器化部署虽然省心,但出问题的时候排查起来也有套路。我把遇到过的典型问题整理成表:

现象根因解决
virtualization support not detectedBIOS 虚拟化未开进 BIOS 开 VT-x/AMD-V
docker desktop failed to startWSL2 未安装或版本旧wsl --update后重启
容器间网络不通不在同一 networkcompose 里显式定义 network
端口冲突宿主机端口被占改映射端口或停占用进程
数据丢失volume 没挂载检查 compose 的 volumes 配置

docker网络不通这个问题我遇到最多。默认情况下,同一个 compose 文件里的服务在同一个 network 里,可以用服务名互相访问。但如果你手动docker run起的容器,就得先docker network create再--network指定。排查的时候用docker exec -it <容器> ping <目标服务名>,能 ping 通说明网络没问题,ping 不通就是网络配置的事。

5.2 记忆检索效果差的排查思路

检索效果差是最常见的问题,但原因可能有很多层。我的排查顺序是:

  1. 先看 embedding 是否正常。随便取两条语义相近的文本,算一下余弦相似度,正常应该在 0.7 以上。如果只有 0.3,说明 embedding 模型有问题,或者文本预处理有问题。
  2. 再看索引是否生效。EXPLAIN ANALYZE一下检索 SQL,如果走的是 Seq Scan 而不是 Index Scan,说明 HNSW 索引没建好或者没被用上。
  3. 然后看数据质量。把检索出来的记忆人工过一遍,如果内容本身就是垃圾,那检索再准也没用。这时候要回头优化写入策略。
  4. 最后看排序逻辑。如果相似度高的记忆排在后面,检查综合分数的权重配置是否合理。

我踩过的一个坑是:embedding 的时候忘了normalize_embeddings=True,导致向量没归一化,余弦相似度算出来全是乱的。这个参数一定要加,尤其是用余弦距离的时候。

5.3 MCP 工具调用失败的典型场景

热搜词里有个llm request failed: provider rejected the request schema or tool payload,这个错误在 MCP 场景里很典型。原因通常是工具的参数 schema 和模型实际传的不匹配。

比如你定义tags是数组类型,但模型传了个字符串;或者你要求importance是整数,模型传了"high"。解决办法有两个:一是把 schema 写得更宽松,用oneOf兼容多种类型;二是在 Server 侧做参数校验和转换,容错处理。

还有一个常见问题是工具描述太长,超出了模型的上下文预算。MCP 工具描述会占用 token,如果你挂了十几个 MCP Server,每个都有一堆工具,光描述就能吃掉几千 token。这时候要做取舍,不常用的工具就别挂,或者把多个相关工具合并成一个带action参数的复合工具。

5.4 记忆膨胀与性能下降

系统跑久了,记忆表会越来越大,检索变慢,成本上升。这时候需要做记忆整理。我的做法是定期跑一个后台任务:

  • 把access_count = 0且超过 90 天的记忆归档到冷存储表
  • 把高度相似的记忆(相似度 > 0.95)合并,保留信息量最大的那条
  • 把 episodic memory 里重复的模式提炼成 semantic memory,然后删掉原始记录

这个整理任务我一般放在凌晨低峰期跑,用pg_cron或者外部调度器触发。整理完之后VACUUM ANALYZE一下,回收空间并更新统计信息。

注意:归档和删除要谨慎。我建议先归档不删除,观察一段时间确认没问题再清理。记忆这东西,删错了就找不回来了。

6. 记忆系统的扩展方向与个人实践体会

hindsight 这套架构搭起来之后,能扩展的方向其实很多。我目前在做的一个尝试是记忆的主动推理——不只是被动检索,而是让 Agent 定期“回顾”自己的记忆,从中发现模式、生成新的洞察。比如发现用户最近三次都问了同类问题,就主动生成一条“用户关注 X 领域”的语义记忆。这其实就是 hindsight 这个词的本意:从过去的经验中提炼出对未来的指导。

另一个方向是多 Agent 共享记忆。多个 Agent 协作时,如果各自有独立的记忆,就会出现信息孤岛。把记忆层做成共享服务,Agent A 学到的经验 Agent B 也能用,整体效率会高很多。但这里有个权限和隔离的问题,不同 Agent 的记忆不能无差别共享,需要做访问控制。

我在实际使用中最大的体会是:记忆系统的价值不在于存了多少,而在于检索时能不能把对的那条捞出来。我见过太多项目,记忆库塞了几十万条,但检索准确率一塌糊涂,最后还不如不用。所以与其追求记忆的量,不如把写入策略和检索排序打磨好。少而精,永远比多而杂强。

最后分享一个小技巧:给记忆加一个source字段,记录这条记忆是从哪次对话、哪个任务来的。排查问题的时候,能顺着 source 回溯到原始上下文,定位效率高很多。这个字段平时用不上,但真出问题的时候,它就是救命稻草。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询