☰
基于MCP与Docker构建LLM Agent持久记忆系统实战
2026/9/29 7:40:25 网站建设 项目流程

1. 从“hindsight”说起:为什么我们需要给Agent装上记忆

“hindsight”这个词本身很有意思,字面意思是“事后的洞察力”,也就是我们常说的“后见之明”。放在LLM Agent的语境里,它指向一个非常具体且棘手的问题:Agent如何记住过去发生过的事情,并在后续决策中真正用上这些经验。

我接触过不少做Agent项目的团队,大家一开始都信心满满,觉得只要把LLM接上工具、挂上MCP协议,Agent就能自己跑起来。但实际跑一段时间就会发现,Agent每次对话都像失忆一样,用户上周告诉它的偏好、上个月踩过的坑、昨天刚纠正过的错误,它统统不记得。这不是模型能力不行,而是记忆架构没搭对。

hindsight这个项目标题,结合agent memory、LLM、MCP、Docker这几个关键词,我判断它要解决的核心问题是:为基于LLM的Agent构建一套可持久化、可检索、可演进的记忆系统,并且通过MCP协议标准化记忆的读写接口,用Docker保证部署的一致性和可移植性。

这套东西适合谁?如果你正在做以下任何一件事,这篇内容都值得你花时间看完:

  • 你在开发基于LLM的对话Agent,发现它“记性太差”,想系统性地解决记忆问题
  • 你在用MCP协议做工具集成,想搞清楚记忆模块怎么通过MCP暴露给Agent
  • 你在用Docker部署Agent服务,想了解记忆存储的容器化方案
  • 你在研究Agent的working memory和long-term memory怎么划分、怎么协同

我踩过的坑是:一开始把记忆简单理解成“把对话历史塞进context”,结果token爆炸、检索效率极低、旧信息干扰新决策。后来才明白,Agent记忆不是简单的存储问题,而是一套包含编码、存储、检索、遗忘、更新的完整工程体系。hindsight这个方向,本质上就是在做这套体系。

2. Agent记忆的核心架构拆解:从working memory到long-term memory

2.1 为什么不能把记忆等同于“对话历史”

很多人第一反应是:记忆不就是把之前的对话都存下来,下次一起塞给LLM吗?这个思路在小规模场景下能跑,但一旦对话轮次超过几十轮,就会遇到三个致命问题。

第一是token成本失控。假设每轮对话平均500 token,50轮就是25000 token,每次请求都带上全部历史,成本线性增长,响应速度也会明显下降。第二是信息稀释。LLM的注意力机制虽然强大,但当context里塞了大量无关历史时,真正关键的那几条信息反而被淹没了。第三是矛盾累积。用户上周说“我喜欢简洁的回答”,这周说“能不能详细一点”,如果两段历史都原样保留,模型会陷入困惑。

所以hindsight这类项目要做的第一件事,就是把记忆分层。我参考常见实践,把Agent记忆分为三层:

记忆层级存储内容生命周期典型实现
Working Memory当前对话轮次的临时上下文单次会话内存中的消息队列
Episodic Memory具体事件、对话片段、操作记录数天到数月向量数据库+时间索引
Semantic Memory提炼后的知识、偏好、规则长期结构化存储+知识图谱

这个分层不是拍脑袋定的,而是对应了认知科学里人类记忆的基本模型。Working memory对应你正在思考的内容,episodic memory对应你记得“昨天发生了什么”,semantic memory对应你知道的“一般性事实”。

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

MCP(Model Context Protocol)在这里的作用,是把记忆的读写能力标准化成Agent可以调用的工具。没有MCP的时候,每个Agent框架都要自己定义一套记忆接口,换一个框架就得重写。有了MCP,记忆系统可以作为一个独立的Server存在,Agent通过标准协议去调用。

具体来说,hindsight项目里MCP要暴露的核心工具大概包括这几类:

  • memory_store:写入一条记忆,需要指定内容、类型、时间戳、关联实体
  • memory_retrieve:根据查询条件检索记忆,支持语义搜索和时间范围过滤
  • memory_update:更新已有记忆的内容或权重
  • memory_forget:主动遗忘低价值或过期的记忆
  • memory_summarize:对一段时间的记忆做摘要提炼

这里有个关键设计决策:记忆的写入是同步还是异步。我试过同步写入,每次对话都要等记忆落盘,延迟明显。后来改成异步写入+本地缓冲,Agent响应速度回来了,但代价是极端情况下可能丢最近几条记忆。折中方案是用消息队列做缓冲,Docker里跑一个轻量级的Redis或者NATS,既保证速度又保证可靠性。

2.3 Docker化部署的考量

为什么记忆系统要Docker化?我总结下来有三个实际原因。

首先是依赖隔离。记忆系统通常要同时跑向量数据库、关系数据库、缓存、消息队列,这些组件版本冲突是家常便饭。Docker Compose一编排,各跑各的,互不干扰。

其次是环境一致性。开发机上跑得好好的,部署到服务器就出问题,十有八九是环境差异。Docker镜像把OS层、运行时、依赖库全部固化,换台机器docker compose up就能复现。

第三是资源控制。记忆系统里的向量检索是内存大户,不限制的话可能把整台机器拖垮。Docker可以精确限制每个容器的CPU和内存配额。

我常用的docker-compose结构大概是这样的:

version: '3.8' services: memory-api: build: ./memory-api ports: - "8080:8080" environment: - VECTOR_DB_URL=http://vector-db:6333 - REDIS_URL=redis://redis:6379 depends_on: - vector-db - redis deploy: resources: limits: memory: 2G vector-db: image: qdrant/qdrant:latest volumes: - ./data/qdrant:/qdrant/storage ports: - "6333:6333" redis: image: redis:7-alpine volumes: - ./data/redis:/data

这个结构里,memory-api是记忆系统的核心服务,vector-db负责语义检索,redis做缓存和消息缓冲。数据卷挂载到宿主机,容器重启不丢数据。

注意:Docker Desktop在Windows上安装时经常报“virtualization support not detected”,这不是Docker的问题,而是BIOS里虚拟化没开。进BIOS把Intel VT-x或AMD-V打开就行。另外Windows家庭版不支持Hyper-V,需要用WSL2后端。

3. 记忆的编码、存储与检索:核心细节与实操要点

3.1 记忆编码:从原始对话到可检索单元

原始对话是一段连续文本,直接存进去检索效率很低。hindsight项目里我建议做一层记忆编码,把对话拆解成结构化的记忆单元。

具体怎么做?我常用的流程是:

  1. 分块:按语义边界把对话切成小块,每块200-500 token。不要按固定字数切,那样会把一个完整意思切断。
  2. 实体抽取:用LLM或规则引擎抽出人名、地名、时间、事件、偏好等实体。
  3. 摘要生成:对每个块生成一句话摘要,作为检索时的粗筛依据。
  4. 向量化:用embedding模型把摘要和原文分别向量化,存到向量数据库。
  5. 元数据标注:打上时间戳、对话ID、用户ID、记忆类型等标签。

这里有个经验:摘要和原文要分开存。检索时先用摘要做粗筛,命中后再取原文做精排。这样既保证速度,又保证精度。我试过只存原文,检索延迟高了3倍;只存摘要,细节丢失严重。两者结合是最优解。

关于embedding模型的选择,如果追求效果,可以用OpenAI的text-embedding-3-large或者开源的BGE-M3;如果追求本地部署和速度,all-MiniLM-L6-v2够用,768维,单次推理几毫秒。Docker里跑一个embedding服务,用FastAPI包一下,通过HTTP调用。

3.2 存储选型:向量库、关系库、图数据库怎么配

记忆系统的存储层通常需要三种数据库配合:

向量数据库负责语义检索。Qdrant、Milvus、Weaviate、Chroma都是常见选择。我选Qdrant比较多,原因是它的过滤功能强,可以在向量搜索的同时按元数据过滤,比如“只搜最近7天的记忆”或“只搜某个用户的记忆”。Docker部署也简单,一个镜像搞定。

关系数据库负责结构化记忆。比如用户的偏好列表、规则库、任务状态,这些用PostgreSQL或SQLite存更合适。SQLite适合单机小规模,PostgreSQL适合多用户并发。

图数据库负责记忆之间的关联。比如“用户A上周提到了项目X,项目X的负责人是B,B喜欢用工具Y”,这种关系用Neo4j或NebulaGraph存,检索时可以做多跳推理。不过图数据库不是必须的,初期用关系库的外键也能凑合。

我实际项目里的数据流是这样的:

对话输入 -> 编码服务 -> 向量库(语义索引) -> 关系库(结构化字段) -> 图数据库(实体关系)

检索时,查询先走向量库拿候选集,再用关系库过滤,最后用图数据库补充关联信息。三层配合,召回率和准确率都能兼顾。

3.3 检索策略:语义搜索、时间衰减与重要性加权

记忆检索不是简单的“相似度排序”。我踩过的坑是:只按语义相似度排,结果最相关的记忆往往是最近刚说的,但真正有价值的可能是三个月前的一条关键偏好。

所以hindsight的检索评分公式我建议这样设计:

score = α * semantic_similarity + β * time_decay + γ * importance

其中:

  • semantic_similarity是查询向量和记忆向量的余弦相似度,范围0-1
  • time_decay是时间衰减因子,常用指数衰减:exp(-λ * days_ago),λ取0.01到0.05之间
  • importance是记忆的重要性权重,写入时由LLM打分或规则设定,范围0-1

α、β、γ是调节权重,我一般设α=0.6,β=0.2,γ=0.2。如果场景更看重时效性,β可以调到0.3;如果更看重长期知识,γ调到0.3。

这个公式的好处是可解释、可调节。不同业务场景可以调参,不用改代码逻辑。

实操心得:time_decay的λ不要设太大,否则旧记忆几乎被完全忽略。我试过λ=0.1,结果一周前的记忆权重就降到0.5以下,导致Agent频繁“忘记”用户长期偏好。后来改成λ=0.02,两周前的记忆还有0.75左右的权重,效果稳很多。

3.4 记忆更新与遗忘:让Agent学会“忘记”

记忆系统最容易被忽视的功能是遗忘。不是所有记忆都值得永久保留,低价值记忆会占用存储、干扰检索、增加成本。

hindsight里我设计了三种遗忘机制:

被动过期:每条记忆写入时设定TTL,到期自动标记为过期。TTL根据记忆类型定,working memory几小时,episodic memory几个月,semantic memory永久。

主动压缩:对同一主题的多条记忆,定期用LLM做摘要合并。比如用户十次提到“喜欢简洁回答”,合并成一条高权重记忆,原始记录归档。

冲突消解:当新记忆和旧记忆矛盾时,不是简单覆盖,而是保留两条并标注冲突,让Agent在决策时知道存在不同版本。这个设计参考了a-memguard的思路, proactive地处理记忆冲突,而不是等出问题再修。

遗忘策略的执行频率,我一般设每天凌晨跑一次批处理。Docker里用cron或者APScheduler定时触发,不影响在线服务。

4. 完整实操:从零搭建一个hindsight记忆服务

4.1 环境准备与Docker编排

先确保Docker和Docker Compose装好。Windows用户如果遇到“virtualization support not detected”,进BIOS开虚拟化;Linux用户确认内核版本5.10以上,docker --version能正常输出。

目录结构这样组织:

hindsight/ ├── docker-compose.yml ├── memory-api/ │ ├── Dockerfile │ ├── requirements.txt │ └── app/ │ ├── main.py │ ├── encoder.py │ ├── retriever.py │ └── mcp_server.py ├── data/ │ ├── qdrant/ │ ├── postgres/ │ └── redis/ └── config/ └── settings.yaml

docker-compose.yml在上一节基础上补充PostgreSQL:

postgres: image: postgres:16-alpine environment: POSTGRES_DB: hindsight POSTGRES_USER: agent POSTGRES_PASSWORD: agent_pass volumes: - ./data/postgres:/var/lib/postgresql/data ports: - "5432:5432"

memory-api的Dockerfile:

FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY app/ ./app/ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]

requirements.txt核心依赖:

fastapi==0.109.0 uvicorn==0.27.0 qdrant-client==1.7.0 psycopg2-binary==2.9.9 redis==5.0.1 sentence-transformers==2.3.1 mcp==0.1.0

4.2 记忆编码服务的实现

encoder.py的核心逻辑:

from sentence_transformers import SentenceTransformer import hashlib from datetime import datetime model = SentenceTransformer('all-MiniLM-L6-v2') def encode_memory(raw_text, user_id, memory_type='episodic'): # 分块 chunks = semantic_chunk(raw_text, max_tokens=400) results = [] for chunk in chunks: # 生成摘要 summary = generate_summary(chunk) # 向量化 vector = model.encode(summary).tolist() # 元数据 metadata = { 'user_id': user_id, 'type': memory_type, 'timestamp': datetime.utcnow().isoformat(), 'raw_text': chunk, 'summary': summary, 'hash': hashlib.md5(chunk.encode()).hexdigest() } results.append({'vector': vector, 'payload': metadata}) return results

semantic_chunk按句号、问号、换行符切分,再合并到接近400 token。generate_summary可以调LLM,也可以先用规则提取首句。初期为了快,我用首句+关键词拼接,效果够用。

4.3 MCP Server的暴露与Agent对接

mcp_server.py用官方mcp库定义工具:

from mcp.server import Server, NotificationOptions from mcp.server.models import InitializationOptions import mcp.server.stdio import mcp.types as types server = Server("hindsight-memory") @server.list_tools() async def handle_list_tools(): return [ types.Tool( name="memory_store", description="存储一条记忆", inputSchema={ "type": "object", "properties": { "content": {"type": "string"}, "user_id": {"type": "string"}, "memory_type": {"type": "string", "enum": ["episodic", "semantic"]} }, "required": ["content", "user_id"] } ), types.Tool( name="memory_retrieve", description="检索记忆", inputSchema={ "type": "object", "properties": { "query": {"type": "string"}, "user_id": {"type": "string"}, "top_k": {"type": "integer", "default": 5} }, "required": ["query", "user_id"] } ) ] @server.call_tool() async def handle_call_tool(name, arguments): if name == "memory_store": result = await store_memory(arguments) return [types.TextContent(type="text", text=result)] elif name == "memory_retrieve": result = await retrieve_memory(arguments) return [types.TextContent(type="text", text=result)]

Agent端通过MCP客户端连接这个Server,就能像调用普通工具一样读写记忆。Chrome DevTools MCP、Playwright MCP这些工具和记忆MCP可以并存,Agent根据任务需要选择调用。

4.4 检索服务的评分与排序实现

retriever.py的核心:

import math from datetime import datetime def compute_score(similarity, timestamp, importance, alpha=0.6, beta=0.2, gamma=0.2, lam=0.02): days_ago = (datetime.utcnow() - datetime.fromisoformat(timestamp)).days time_decay = math.exp(-lam * days_ago) return alpha * similarity + beta * time_decay + gamma * importance def retrieve(query, user_id, top_k=5): query_vector = model.encode(query).tolist() # 向量库粗筛,取top 20 candidates = qdrant_client.search( collection_name="memories", query_vector=query_vector, query_filter={"must": [{"key": "user_id", "match": {"value": user_id}}]}, limit=20 ) # 重排序 scored = [] for c in candidates: score = compute_score( similarity=c.score, timestamp=c.payload['timestamp'], importance=c.payload.get('importance', 0.5) ) scored.append((score, c)) scored.sort(key=lambda x: x[0], reverse=True) return [s[1] for s in scored[:top_k]]

这个实现里,向量库先粗筛20条,再用综合评分精排取前5。粗筛数量可以根据数据量调整,数据少时取10,数据多时取50。

4.5 定时遗忘任务的配置

用APScheduler在FastAPI启动时挂载:

from apscheduler.schedulers.asyncio import AsyncIOScheduler scheduler = AsyncIOScheduler() @scheduler.scheduled_job('cron', hour=3, minute=0) async def daily_forget(): # 过期清理 await expire_old_memories() # 摘要合并 await compress_similar_memories() # 冲突检测 await detect_conflicts() @app.on_event("startup") async def startup(): scheduler.start()

每天凌晨3点跑,业务低峰期,不影响在线服务。

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

5.1 Docker相关故障速查

问题现象可能原因解决方法
Docker Desktop启动失败,提示virtualization support not detectedBIOS虚拟化未开启进BIOS开Intel VT-x/AMD-V
容器间网络不通不在同一networkdocker-compose默认同network,检查服务名拼写
向量库容器内存溢出未限制内存或数据量过大加deploy.resources.limits,或分片存储
数据卷权限拒绝宿主机目录权限不对chmod 777 data/ 或指定user
镜像拉取慢默认源速度问题配置国内镜像加速器

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

检索效果差通常表现为:Agent答非所问、忘记关键信息、重复问同样的问题。排查顺序我一般这样走:

第一步,检查写入是否成功。直接查向量库的count,看记忆条数是否和预期一致。如果写入就丢了,后面检索肯定不行。

第二步,检查embedding质量。拿一条已知记忆的原文,用同样的embedding模型编码,和库里存的向量算余弦相似度,应该接近1.0。如果只有0.7、0.8,说明编码过程有问题,可能是分块把语义切断了。

第三步,检查检索评分。把候选集的similarity、time_decay、importance分别打印出来,看是哪一项拖了后腿。常见情况是time_decay把旧的好记忆压得太低,调小λ即可。

第四步,检查context注入。检索出来的记忆有没有正确拼接到LLM的prompt里?拼接格式是否清晰?我见过有人把记忆直接塞在system prompt末尾,模型根本没注意到。正确做法是用明确的分隔符和标题,比如:

[相关记忆] - 用户偏好:喜欢简洁回答(2024-01-15) - 项目背景:正在开发Agent记忆系统(2024-02-01) [记忆结束]

5.3 MCP对接中的典型坑

MCP协议本身不复杂,但实际对接时有几个坑我反复遇到。

坑一:工具描述太模糊。Agent选择工具时依赖description,如果写“存储记忆”这种笼统描述,Agent可能在该检索的时候调了存储。description要写清楚使用场景,比如“当用户提供新信息需要长期记住时调用”。

坑二:参数schema不严格。inputSchema里required字段没标全,Agent可能传空值。所有必填参数都要放进required数组。

坑三:返回内容太长。memory_retrieve返回10条记忆,每条500字,一次返回5000字,Agent的context直接被占满。返回时要截断或摘要,只给最关键的几条。

坑四:错误处理缺失。向量库挂了、数据库连不上,MCP工具调用直接抛异常,Agent收到一堆traceback。要捕获异常返回友好错误信息,比如“记忆服务暂时不可用,请稍后重试”。

5.4 性能优化的几个实操技巧

批量写入:单条写入Qdrant开销大,攒够100条或每隔5秒批量写一次,吞吐量能提升5-10倍。

缓存热点记忆:用户最常访问的记忆放Redis,TTL设1小时。我实测命中率能到60%以上,检索延迟从50ms降到5ms。

向量维度压缩:如果存储成本敏感,可以用PCA把768维降到256维,精度损失约5%,存储省三分之二。Qdrant支持量化,效果类似。

异步检索:多个检索请求并行发,用asyncio.gather合并结果。Agent一次需要查语义记忆、事件记忆、偏好记忆时,并行比串行快3倍。

最后分享一个我踩过的大坑:早期版本我没做记忆去重,同一句话用户说了三遍,库里存了三条几乎一样的向量。检索时这三条同时命中,把context占满了,真正有用的其他记忆反而被挤掉。后来加了hash去重和相似度合并,同样内容只保留一条并累加importance,效果立刻好转。记忆系统里,去重和压缩比存储本身更重要。

5.5 安全与合规的边界处理

记忆系统存的是用户数据,安全不能马虎。我一般做这几层防护:

  • 传输加密:MCP Server和Agent之间用TLS,Docker内部网络可以走明文但对外必须加密
  • 存储加密:敏感字段用AES加密后再入库,密钥放环境变量不写代码
  • 访问控制:每个user_id只能读写自己的记忆,MCP工具调用时校验token
  • 审计日志:所有记忆读写操作记日志,保留90天,方便追溯
  • 数据清理:提供用户主动删除记忆的接口,删除时同时清理向量库、关系库、缓存

这些不是可选项,是必选项。我见过因为没做访问控制导致用户A读到用户B记忆的案例,修复成本远高于初期投入。

6. 记忆系统的演进方向与个人体会

hindsight这类项目做完基础版本后,还有不少可以深挖的方向。比如记忆的重要性自动评估,现在靠LLM打分或规则,未来可以训练一个小模型专门做这件事,更快更准。再比如跨Agent记忆共享,多个Agent协作时如何安全地共享部分记忆,MCP协议天然适合做这个,但权限模型需要仔细设计。

还有记忆的可解释性。Agent做出一个决策,能不能追溯到是哪几条记忆影响了它?这个对调试和信任建立很重要。我现在的做法是在检索结果里带上记忆ID,Agent输出时标注引用了哪些记忆,虽然粗糙但够用。

我个人在实际操作中的体会是:记忆系统的复杂度很容易被低估。看起来就是存和取,但存什么、怎么存、取什么、怎么取、什么时候忘,每一个环节都有大量决策。我的建议是初期不要追求大而全,先把working memory和episodic memory跑通,用最简单的向量库+关系库组合,验证效果后再逐步加图数据库、加遗忘策略、加冲突消解。Docker编排也是,先单机跑起来,再考虑多节点和资源限制。

另外,MCP协议虽然好用,但不要为了用而用。如果Agent框架本身有成熟的记忆接口,直接用它也行。MCP的价值在于标准化和跨框架复用,如果你的场景不需要这些,简单方案反而更稳。

最后再分享一个小技巧:记忆系统的测试用例要覆盖“时间跨度”场景。比如写入一条记忆,然后把系统时间调到三个月后,看检索时这条记忆的权重是否合理衰减。这个测试能提前发现很多时间衰减参数的问题,比上线后用户反馈“Agent怎么忘了”要主动得多。

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

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

立即咨询