☰
Agent记忆系统实战:基于MCP与Docker构建可插拔的LLM记忆中间层
2026/9/28 14:08:12 网站建设 项目流程

1. 从“hindsight”说起:为什么我们需要给 Agent 装上一双“后视之眼”

“hindsight”这个词本身很有意思,字面意思是“事后的洞察力”,也就是我们常说的“后见之明”。放在 LLM Agent 的语境里,它指向一个非常具体且要命的问题:Agent 在完成一轮任务之后,能不能真正记住自己做过什么、踩过什么坑、下次遇到类似场景时能不能调用这些经验。

我接触过不少做 Agent 的团队,大家一开始都把精力砸在工具调用、Prompt 编排、工作流引擎上,觉得只要把链路跑通就万事大吉。结果上线跑了两周,用户反馈全是“它怎么又犯同样的错”“上次明明告诉过它不要这样”“换个会话就完全失忆”。这就是典型的 Agent Memory 缺失症。

hindsight 这个项目标题,结合 agent memory、LLM、MCP、Docker 这几个热搜词来看,它大概率是一个围绕Agent 记忆系统展开的工程实践项目。核心要解决的问题就是:让 Agent 具备跨会话、跨任务的记忆能力,并且这种记忆不是简单的“把聊天记录塞进向量库”那么粗暴,而是要有结构、有层次、有检索策略、有遗忘机制。

我个人的判断是,hindsight 要做的是一套可插拔的 Agent 记忆中间层,它可能通过 MCP 协议对外暴露记忆读写接口,用 Docker 做标准化部署,底层对接各种 LLM 来做记忆的压缩、摘要、索引和召回。这套东西适合谁?适合正在做 Agent 产品、被“失忆问题”折磨得死去活来的开发者,也适合想了解 Agent Memory 工程化落地路径的技术负责人。

下面我会从整体设计思路、核心细节、实操部署、问题排查几个维度,把这类项目的完整面貌拆开来讲。很多细节是基于我对 Agent Memory 领域的常见工程实践做的合理推演,但每一步的逻辑和取舍我都会讲清楚。

2. 整体架构设计与技术选型拆解

2.1 为什么 Agent Memory 不能只靠向量数据库

很多人一提到“给 Agent 加记忆”,第一反应就是上向量数据库,把历史对话 embedding 一下存进去,需要的时候做相似度检索。这个方案在 Demo 阶段能用,但一到生产环境就露馅。

问题出在三个地方。第一,向量检索召回的是“相似文本”,不是“有用经验”。用户上次问“帮我订明天去上海的机票”,这次问“帮我订后天去北京的机票”,向量相似度极高,但真正有价值的记忆不是那段对话本身,而是“这个用户偏好靠窗座位、习惯早上出发、报销需要电子发票”这些结构化偏好。第二,记忆没有时间衰减和重要性分级。三个月前的一次闲聊和昨天的一次关键决策,在向量空间里可能距离差不多,但显然不该同等对待。第三,纯向量方案无法做记忆的冲突消解。用户上周说“我住在杭州”,这周说“我搬到南京了”,两条记忆都存着,检索时可能同时召回,Agent 就懵了。

hindsight 这类项目的设计思路,通常是把记忆分成几个层次来处理。我把它归纳成一张表,方便你对照理解:

记忆层次存储内容典型实现检索方式
工作记忆当前会话上下文内存/Redis直接拼接
情景记忆具体事件、对话片段向量库+元数据语义检索+时间过滤
语义记忆提炼后的事实、偏好结构化存储/KV精确匹配+规则
程序记忆任务执行流程、工具调用模式图数据库/文档模式匹配

这个分层不是拍脑袋来的,它对应的是认知科学里人类记忆的基本分类。hindsight 的价值就在于把这套分层落地成工程可用的组件,而不是让每个 Agent 开发者自己从零造轮子。

2.2 MCP 协议在记忆系统中的角色定位

MCP(Model Context Protocol)这两年被讨论得很多,从蓝湖 MCP 到 Playwright MCP,各种工具都在往这个协议上靠。放到 Agent Memory 场景里,MCP 解决的是一个很实际的问题:记忆系统怎么和不同的 Agent 框架解耦。

你想想,今天团队用 LangChain 搭 Agent,明天可能换成自研框架,后天又要接入 Dify 这类平台。如果记忆模块是硬编码在业务逻辑里的,每次换框架都要重写一遍。但如果记忆系统通过 MCP Server 的方式暴露标准接口,Agent 只需要知道“我要调用一个叫 recall_memory 的工具”,具体底层是向量库还是图数据库,跟 Agent 没关系。

hindsight 如果走 MCP 路线,通常会暴露这么几个核心工具:

  • store_memory:写入一条记忆,带元数据(时间、类型、重要性、来源)
  • recall_memory:根据查询条件召回相关记忆
  • update_memory:更新已有记忆(处理冲突和修正)
  • forget_memory:主动遗忘或降权
  • summarize_session:把一段会话压缩成结构化记忆

这种设计的好处是,Agent 的 Prompt 里只需要描述“你可以使用记忆工具”,不用关心实现细节。而且 MCP Server 可以独立部署、独立扩缩容,记忆系统的负载不会拖垮 Agent 主流程。

注意:MCP 工具的定义要尽量原子化,不要把“召回+重排+摘要”塞进一个工具里。工具粒度太粗,Agent 的调用决策会变得困难,而且不利于单独调试每个环节。

2.3 Docker 化部署的必然性与坑点预判

热搜词里 Docker 相关的内容占了很大比重,从 docker 安装教程到 docker 网络不通,说明很多人在部署环节卡住了。hindsight 这类项目选择 Docker 部署是必然的,因为它依赖的组件太多了:向量数据库、关系型数据库、缓存、MCP Server、可能还有 LLM 网关。

用 Docker Compose 编排的好处是一键拉起整套环境,但坑也很集中。我见过最多的问题就是Docker Desktop 在 Windows 上启动失败,报 “virtualization support not detected”。这个问题的根源通常是 BIOS 里虚拟化没开,或者 Hyper-V 和 WSL2 冲突。另一个高频问题是容器间网络不通,表现为 MCP Server 连不上向量库,但单独进容器又能 ping 通。这多半是 Docker 网络模式选错了,或者服务启动顺序没控制好,向量库还没 ready,MCP Server 就开始连了。

我的建议是,在 docker-compose.yml 里给依赖服务加上 healthcheck,并且用depends_on的condition: service_healthy来控制启动顺序。这个细节后面实操部分会展开。

3. 核心细节解析:记忆的写入、召回与遗忘

3.1 记忆写入:不是所有对话都值得记

Agent 每轮对话都产生大量文本,如果全量写入记忆库,不出三天检索质量就会崩掉。hindsight 这类系统通常会在写入前做一层记忆筛选,判断哪些内容值得持久化。

筛选逻辑一般包含几个维度。新颖性:这条信息和已有记忆是否高度重复?如果用户只是说了句“好的”,没有任何新信息,直接丢弃。重要性:是否包含决策、偏好、事实变更、任务结果?这些是高价值记忆。可复用性:这条信息在未来类似场景下是否可能被用到?一次性的临时查询,比如“现在几点了”,没有存储价值。

具体实现上,常见做法是用一个小模型或者规则引擎做初筛,再用 LLM 做精炼。比如原始对话是“用户:我下周要去深圳出差,帮我看看天气。Agent:下周深圳有雨,建议带伞。用户:好的,那帮我订个酒店吧,要离会展中心近的。” 这段对话里,值得存的记忆是“用户下周去深圳出差”“用户需要离会展中心近的酒店”,而不是完整的对话流水。

写入时的元数据设计也很关键。我通常会建议至少包含这几个字段:

{ "memory_id": "uuid", "content": "用户下周去深圳出差,需要离会展中心近的酒店", "memory_type": "episodic", "importance": 0.8, "created_at": "2025-01-15T10:30:00Z", "last_accessed_at": "2025-01-15T10:30:00Z", "access_count": 0, "source_session": "session_abc123", "tags": ["出差", "深圳", "酒店", "会展中心"], "embedding": [0.023, -0.041, ...] }

importance这个字段很多人会忽略,但它直接决定了记忆的召回优先级和衰减速度。重要性高的记忆,衰减慢,召回时权重高;重要性低的,可能一周后就自动归档了。

3.2 记忆召回:多路召回加融合排序

召回环节是 hindsight 这类系统最能体现技术含量的地方。单一向量检索不够用,通常要做多路召回。

第一路是语义召回,用 embedding 做相似度搜索,解决“意思相近但用词不同”的问题。第二路是关键词召回,用 BM25 或全文索引,解决“专有名词、人名、地名”这类 embedding 容易失真的场景。第三路是时间召回,把最近 N 条记忆直接拉出来,保证 Agent 不会忘记刚发生的事。第四路是结构化召回,根据标签、类型、来源做精确过滤。

多路召回之后要做融合排序。最简单的做法是加权求和,但权重怎么定是个问题。我比较推荐用 RRF(Reciprocal Rank Fusion)这类无需调参的融合算法,它对不同召回路的分数尺度不敏感,工程上更稳。

def rrf_fusion(rankings, k=60): scores = {} for ranking in rankings: for rank, doc_id in enumerate(ranking): scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1) return sorted(scores.items(), key=lambda x: x[1], reverse=True)

这个k值一般取 60,是 RRF 原论文里的经验值,实测下来在记忆召回场景也够用。

召回之后还有一步重排,可以用 Cross-Encoder 或者直接让 LLM 打分。但 LLM 重排延迟高,通常只在召回数量少、精度要求高的场景用。日常场景用 RRF 融合后的 Top-K 就够了。

3.3 记忆遗忘:主动降权比删除更优雅

“遗忘”这个词听起来有点反直觉,记忆系统不是应该尽量多记吗?但实际跑下来你会发现,不遗忘的系统会越来越笨。过时信息、错误信息、低价值信息堆积,会稀释召回质量,还会让 LLM 在生成时被误导。

hindsight 的遗忘机制通常不是硬删除,而是降权+归档。每条记忆有一个动态的relevance_score,计算方式大致是:

relevance = importance × decay_factor(time) × access_boost

其中decay_factor是时间衰减函数,常见的是指数衰减:

decay_factor = exp(-λ × days_since_last_access)

λ的取值决定了记忆半衰期。如果希望记忆大约 30 天衰减到一半,那么λ = ln(2) / 30 ≈ 0.023。access_boost是每次被召回后的加成,让常用记忆保持活跃。

当relevance_score低于某个阈值时,记忆被标记为“归档”,不再参与常规召回,但保留在冷存储里,必要时可以恢复。这种设计比直接删除安全得多,因为有些记忆的价值是延迟显现的。

实操心得:遗忘阈值不要设得太激进。我见过有团队把阈值设得很高,结果 Agent 把用户三个月前说的“我对花生过敏”给忘了,差点出大事。涉及安全、健康、财务的记忆,importance 直接拉满,并且关闭衰减。

4. 实操部署:从零把 hindsight 跑起来

4.1 环境准备与 Docker 安装避坑

假设你是在一台干净的 Ubuntu 22.04 机器上部署,Windows 用户建议直接用 WSL2,别在原生 Windows 上折腾 Docker Desktop,坑太多。

Ubuntu 上安装 Docker 的标准流程:

# 卸载旧版本 sudo apt-get remove docker docker-engine docker.io containerd runc # 安装依赖 sudo apt-get update sudo apt-get install -y ca-certificates curl gnupg lsb-release # 添加官方 GPG key sudo mkdir -p /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg # 添加仓库 echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null # 安装 sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin # 验证 sudo docker run hello-world

如果你在 Windows 上遇到 “virtualization support not detected”,先去 BIOS 里确认 Intel VT-x 或 AMD-V 是开启状态。然后检查 Hyper-V 是否和 WSL2 冲突,命令行执行bcdedit /set hypervisorlaunchtype auto后重启。还不行的话,在“启用或关闭 Windows 功能”里确认“虚拟机平台”和“适用于 Linux 的 Windows 子系统”都勾上了。

4.2 docker-compose 编排文件详解

hindsight 这类记忆系统通常需要以下几个服务:MCP Server、向量数据库(Qdrant 或 Milvus)、关系型数据库(PostgreSQL)、缓存(Redis)、可选的 LLM 网关。

version: "3.9" services: qdrant: image: qdrant/qdrant:v1.7.4 ports: - "6333:6333" - "6334:6334" volumes: - qdrant_data:/qdrant/storage healthcheck: test: ["CMD", "curl", "-f", "http://localhost:6333/healthz"] interval: 10s timeout: 5s retries: 5 postgres: image: postgres:16-alpine environment: POSTGRES_USER: hindsight POSTGRES_PASSWORD: hindsight_pass POSTGRES_DB: hindsight ports: - "5432:5432" volumes: - pg_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U hindsight"] interval: 10s timeout: 5s retries: 5 redis: image: redis:7-alpine ports: - "6379:6379" healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5 hindsight-mcp: build: ./hindsight-mcp ports: - "8080:8080" environment: QDRANT_URL: http://qdrant:6333 POSTGRES_URL: postgresql://hindsight:hindsight_pass@postgres:5432/hindsight REDIS_URL: redis://redis:6379/0 LLM_API_BASE: ${LLM_API_BASE} LLM_API_KEY: ${LLM_API_KEY} depends_on: qdrant: condition: service_healthy postgres: condition: service_healthy redis: condition: service_healthy volumes: qdrant_data: pg_data:

这个编排文件里有几个关键点值得展开。healthcheck 是必须的,没有它,MCP Server 会在数据库还没 ready 的时候就启动,然后连接失败退出,你看到的就是“容器反复重启”。depends_on 的 condition 写法是 Compose V2 的特性,老版本不支持,注意你的 docker-compose-plugin 版本。

启动命令:

docker compose up -d docker compose logs -f hindsight-mcp

看到 “MCP server listening on 8080” 就说明起来了。

4.3 记忆写入与召回的实操验证

服务起来之后,先做一轮写入测试。假设 MCP Server 暴露的是 HTTP 接口(有些实现走 stdio,这里以 HTTP 为例):

# 写入一条记忆 curl -X POST http://localhost:8080/tools/store_memory \ -H "Content-Type: application/json" \ -d '{ "content": "用户偏好靠窗座位,报销需要电子发票", "memory_type": "semantic", "importance": 0.9, "tags": ["偏好", "差旅", "报销"] }' # 召回测试 curl -X POST http://localhost:8080/tools/recall_memory \ -H "Content-Type: application/json" \ -d '{ "query": "帮我订机票", "top_k": 5 }'

召回结果应该包含刚才写入的那条偏好记忆。如果没召回出来,先检查 embedding 模型是否一致——写入和查询必须用同一个 embedding 模型,否则向量空间对不上,相似度计算全是噪声。

再测一下冲突消解。写入“用户住在杭州”,再写入“用户搬到南京了”,然后查询“用户住在哪里”。好的记忆系统应该返回南京,并且把杭州那条标记为过时。如果两条都返回,说明冲突消解逻辑没生效,需要检查update_memory的实现。

4.4 接入 Agent 框架的配置要点

以常见的 Agent 框架为例,接入 MCP 记忆服务通常是在工具注册环节加一个 MCP Client:

from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client server_params = StdioServerParameters( command="docker", args=["exec", "-i", "hindsight-mcp", "python", "-m", "hindsight.server"], ) async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools = await session.list_tools() # 把 tools 注册到 Agent 的工具列表里

如果是 HTTP 方式,直接用 requests 或 httpx 封装成工具函数也行。关键是要在 Agent 的 System Prompt 里明确告诉它:在执行任务前先召回相关记忆,在任务结束后把关键信息写入记忆。很多团队接了记忆系统但效果不好,就是因为 Agent 根本不知道要去用这些工具。

注意:召回时机很关键。不要每轮对话都召回,那样延迟太高。我的经验是在任务开始时召回一次,任务过程中如果话题发生明显切换再召回一次。写入时机则是任务结束时统一写入,避免中间过程产生大量碎片记忆。

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

5.1 记忆召回不准的排查路径

召回不准是最常见的问题,排查要按顺序来,不要跳步。

第一步,确认 embedding 一致性。写入用的模型和查询用的模型必须是同一个。我见过有团队写入用 OpenAI 的 text-embedding-3-small,查询用了本地部署的 BGE,结果召回率惨不忍睹。检查方法很简单,写入一条已知内容,然后用完全相同的文本去查,如果相似度不是接近 1.0,就是模型不一致。

第二步,检查向量维度。Qdrant 的 collection 创建时指定的维度必须和 embedding 输出维度一致。text-embedding-3-small 是 1536 维,BGE-large 是 1024 维,搞错了要么写入报错,要么静默截断。

第三步,看召回数量。如果top_k设得太小,比如只取 3 条,而相关记忆排在第 5 位,就会漏掉。建议召回阶段取大一点,比如 20 条,再用 RRF 融合后取 Top 5 给 LLM。

第四步,检查元数据过滤。如果召回时带了时间范围或标签过滤,可能把相关记忆过滤掉了。先把过滤条件去掉,确认裸召回是否正常,再逐步加回过滤条件。

5.2 Docker 网络与连接问题速查

现象可能原因排查命令解决方案
MCP Server 连不上 Qdrant服务名解析失败docker exec hindsight-mcp ping qdrant确认在同一 network,用服务名而非 localhost
容器启动后立即退出依赖服务未 readydocker compose logs <service>加 healthcheck 和 depends_on condition
端口冲突宿主机端口被占用sudo lsof -i :6333改映射端口或停掉占用进程
数据丢失volume 未挂载docker volume ls检查 compose 里 volumes 配置
网络不通用了 host 网络模式docker network inspect改用默认 bridge 网络

这里重点说一个坑:在容器里连 localhost 是连不到其他容器的。很多新手在 MCP Server 的配置里写QDRANT_URL=http://localhost:6333,但 MCP Server 和 Qdrant 是两个容器,localhost 指向的是 MCP Server 自己。正确写法是用 compose 里的服务名http://qdrant:6333。

5.3 LLM 调用失败的典型错误

热搜词里有个 “llm request failed: provider rejected the request schema or tool payload”,这个错误在 Agent Memory 场景特别常见,因为记忆系统经常要把结构化数据塞进 LLM 的 tool call 里。

常见原因有三个。一是 JSON schema 不合法,比如 required 字段缺失、类型不匹配。排查方法是把 payload 打印出来,用在线 JSON schema validator 校验一遍。二是 token 超限,召回的记忆太多,拼接后超过了模型的 context window。解决方法是限制召回数量,或者先做一轮摘要压缩。三是工具名冲突,Agent 注册了多个 MCP Server,不同 Server 暴露了同名工具,LLM 不知道该调哪个。给工具加命名空间前缀可以解决,比如hindsight_store_memory。

# 工具名加前缀的示例 def register_mcp_tools(session, namespace): tools = await session.list_tools() for tool in tools: tool.name = f"{namespace}_{tool.name}" return tools

5.4 记忆膨胀与性能下降的应对

系统跑了一段时间后,如果发现召回延迟越来越高,多半是记忆库膨胀了。Qdrant 的 collection 到了百万级别,即使有 HNSW 索引,查询延迟也会明显上升。

应对策略分三层。第一层是写入时过滤,前面说的记忆筛选要做好,从源头控制增长。第二层是定期归档,写个定时任务,每天把relevance_score低于阈值的记忆移到冷 collection。第三层是分片,按用户或按时间分 collection,查询时只查相关分片。

我实测下来,单 collection 控制在 50 万条以内,查询延迟可以稳定在 50ms 以内。超过这个量级,就要考虑分片了。

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

hindsight 这类项目跑通之后,往上叠的东西其实很多。一个方向是记忆的可视化,让用户能看到 Agent 记住了什么,并且能手动修正。这个功能对建立信任特别重要,用户发现 Agent 记错了,能自己改,而不是干瞪眼。另一个方向是跨 Agent 的记忆共享,多个 Agent 共用一套记忆库,A Agent 学到的经验 B Agent 也能用。这需要解决记忆的权限和隔离问题,复杂度不低。

还有一个我觉得很有潜力的方向是记忆的主动反思。现在的记忆系统大多是被动写入、被动召回,但更高级的形态是 Agent 定期回顾自己的记忆,发现矛盾、提炼规律、生成新的高层记忆。比如它发现用户连续三次都选了早班航班,就可以主动生成一条“用户偏好早班航班”的语义记忆。这种主动反思机制,才是真正让 Agent 越用越聪明的关键。

我在实际部署这类系统时踩过最大的坑,是一开始太贪心,想把所有对话都存下来。结果两周后召回质量断崖式下跌,因为噪声太多了。后来改成严格筛选,只存高价值记忆,召回准确率立刻上来了。记忆系统的核心不是“记多少”,而是“记什么”和“怎么取”。这个道理,跟人脑其实是一样的。

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

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

立即咨询