基于MongoDB与MCP协议构建AI智能体持久化记忆层
1. 项目概述:为AI智能体构建一个持久、可搜索的记忆层
在构建AI智能体时,我们常常面临一个核心挑战:如何让智能体记住过去?无论是多轮对话中的上下文,还是长期任务中的关键决策,一个可靠的记忆系统是智能体从“单次反应”走向“持续智能”的关键。mcp-memory-layer正是为了解决这个问题而生。它是一个用Go语言编写的MCP服务器,为LLM智能体提供了一个基于MongoDB的持久化、可搜索的记忆层。
简单来说,你可以把它想象成智能体的“外置大脑”。这个大脑不仅能存储海量的自然语言记忆片段,还能通过向量搜索和结构化查询,在需要时精准地调取相关记忆。它通过标准的MCP协议和REST API暴露功能,这意味着无论是Claude Desktop、VS Code Copilot这样的桌面应用,还是LangChain、CrewAI、AutoGen这样的开发框架,甚至是AWS Bedrock AgentCore,都能以统一的方式接入并使用这个记忆层,而无需修改服务器端的代码。
这个项目的设计哲学非常务实:存储层的职责是快速、准确地将正确的文档呈现在LLM面前,而理解这些文档含义的工作,则完全交给LLM本身。它不试图在存储层做复杂的语义理解,而是专注于优化检索效率。同时,它倡导“自由存储,精确过滤,让LLM解释”的模式,认为存储冗余的成本很低,但因过度过滤而丢失关键信息导致错误结果的代价却很高。
2. 核心架构与设计思路拆解
2.1 为什么选择MongoDB + MCP的组合?
在技术选型上,mcp-memory-layer做出了几个关键决策,这些决策背后是经过实战检验的权衡。
首先是数据库选择MongoDB Atlas。这不仅仅是跟风,而是基于几个硬性需求:第一,需要原生支持向量搜索(Atlas Vector Search),以实现基于语义相似度的模糊召回。第二,需要灵活的文档模型,因为记忆的结构可能随着智能体能力的进化而变化,固定的表结构会成为枷锁。第三,需要强大的聚合管道和全文检索能力,以支持复杂的、基于标签、实体和时间的结构化查询。MongoDB的JSON文档模型完美契合了记忆数据半结构化、易扩展的特性。
其次是采用MCP作为核心协议。MCP是一个新兴但势头强劲的协议,旨在为LLM工具调用提供一个标准化的接口。它的优势在于“一次编写,处处运行”。我为我的记忆服务器实现一套MCP工具后,任何支持MCP的客户端(如Claude Desktop)都能立即发现并使用这些工具,无需为每个客户端编写特定的适配器代码。同时,提供REST API作为补充,确保了与不支持MCP的传统框架或自定义脚本的兼容性。这种双协议支持极大地扩展了项目的适用性。
2.2 三层会话模型与记忆生命周期
项目文档中提到的“三层会话模型”是理解其架构的关键。这不是一个随意的分层,而是为了清晰地区分记忆的时效性和作用范围。
工作记忆:这相当于智能体的“短期记忆”或“上下文窗口”。它存储当前会话中最新、最活跃的记忆片段,检索优先级最高,用于维持对话的连贯性。通常,这部分记忆的生命周期与一次LLM调用或一个简短的子任务绑定。
会话记忆:对应一个完整的、有明确目标的任务会话。例如,用户要求智能体“帮我规划一个三天的旅行行程”,从开始到产出完整计划的全过程,相关的记忆都归属于这个会话。会话记忆在任务完成后会被归档,但依然可以为了解任务全貌而被检索。
长期记忆:这是智能体的“知识库”或“经验库”。它存储跨会话的、被判定为有价值、可复用的知识。例如,从多次编程任务中总结出的“用户偏好使用Python的f-string格式化字符串”这条经验,就应该被提升到长期记忆。长期记忆的检索策略更偏向于精确匹配和重要性加权,避免被大量临时信息淹没。
这种分层管理带来了几个好处:首先是检索效率,系统可以优先扫描工作记忆和当前会话记忆,缩小搜索范围。其次是记忆的“新陈代谢”,系统可以定义不同的留存策略,比如工作记忆随上下文滚动,会话记忆在闲置一段时间后自动降级或清理,而长期记忆则需要显式的“提升”操作才能进入。
2.3 混合检索策略:向量搜索与TurboQuant压缩扫描
检索的准确性和速度是记忆系统的命脉。mcp-memory-layer采用了一种混合检索策略,结合了广度语义召回和深度精确匹配。
Atlas Vector Search(广度语义召回):这是第一道关卡。当智能体提出一个查询(如“上次我们讨论的关于用户认证的最佳实践是什么?”),系统会先将查询文本转换为向量嵌入,然后在MongoDB的向量索引中进行近似最近邻搜索。这一步能召回所有在语义上相关的文档,不管它们是否包含了查询中的关键词。它解决了传统关键词搜索“用词不同但意思相同”就找不到的痛点。
TurboQuant压缩扫描(深度精确匹配):这是项目的创新点。向量搜索召回的可能是一个较大的相关文档集合。接下来,系统会利用TurboQuant技术对这些文档的向量进行高比例压缩(比如从1024维压缩到64字节),并将它们缓存在内存中。当需要进行更精确的、带过滤条件的查询时(比如“在
project_x这个会话中,关于error_handling标签的记忆”),系统可以直接在内存中高速扫描这些压缩后的向量和元数据,进行快速的初步过滤和排序,而无需反复访问数据库。这相当于在内存中建立了一个超轻量级的向量索引副本,专门用于二次筛选。
这种“向量搜索广撒网,TQ扫描精捕捞”的组合,在实践中取得了很好的平衡。既保证了语义搜索的召回率,又通过内存计算大幅提升了复杂查询的响应速度。
注意:TurboQuant压缩是有损的,会损失一些向量精度,因此它主要用于快速预筛选,而不是最终的相似度计算。最终的相关性排序,通常会结合压缩向量的近似分数和原始向量的精确分数(如果需要的话)。
3. 核心功能解析与实操要点
3.1 记忆的存储结构:不仅仅是文本
一个记忆条目远不止一段文本。mcp-memory-layer为每个记忆文档设计了一套丰富的元数据字段,这使得记忆变得可组织、可连接、可推理。
{ “_id”: ObjectId(“…”), “agent_id”: “coding_assistant”, “session_id”: “session_20231027_planning”, “content”: “用户倾向于在编写API时优先考虑使用FastAPI,因为它自动生成的交互式文档Swagger UI对他们团队协作很有帮助。”, “embedding”: [0.12, -0.05, …], // 1024维向量 “memory_type”: “user_preference”, “tags”: [“backend”, “framework”, “documentation”], “entities”: [{“type”: “technology”, “value”: “FastAPI”}, {“type”: “feature”, “value”: “Swagger UI”}], “related_docs”: [ObjectId(“…”), …], “created_at”: ISODate(“…”), “accessed_at”: ISODate(“…”), “importance_score”: 0.8 }memory_type:这是一个强大的命名空间机制。你可以定义如fact、preference、decision、lesson_learned等类型。在检索时,你可以指定memory_type来缩小范围,例如只检索“经验教训”类的记忆。tags与entities:标签用于自由分类,实体用于结构化提取。例如,一条记忆可以被打上#bug、#performance标签,同时提取出{“type”: “module”, “value”: “auth”}实体。实体信息可以用于构建隐式的知识图谱(通过共现分析),而无需维护复杂的图数据库。related_docs:允许手动或自动建立记忆之间的链接。例如,一条“问题”记忆可以关联到多条“解决方案”记忆,形成因果链。importance_score:这是一个由策略子系统或LLM评估的动态分数,用于在检索结果排序时加权。频繁访问、被关联多的记忆分数会更高。
3.2 MCP工具集:智能体与记忆交互的接口
智能体通过调用MCP工具来与记忆层交互。这些工具设计得尽可能符合LLM的“思考”模式。
memory_intake:记忆摄入。智能体决定将哪段对话、哪个观察结果存入记忆。它需要提供content,并可以可选地添加memory_type、tags、entities等。关键在于,决定“记住什么”的逻辑在智能体侧,这赋予了智能体管理自身记忆的自主权。memory_recall:记忆召回。这是最常用的工具。智能体提交一个自然语言查询,系统返回最相关的记忆列表。背后是混合检索策略在起作用。调用时可以附加过滤器,如{“agent_id”: “me”, “memory_type”: “fact”, “tags”: {“$in”: [“python”]}}。memory_query:结构化查询。当智能体需要非常精确地查找信息时使用,例如“查找session_abc中所有带有error标签的记忆”。它使用MongoDB的查询语法,进行确定性的字段查找。memory_reflect:记忆反思与整理。这是智能体进行“记忆管理”的高级工具。它可以:promote:将一条记忆从会话记忆提升为长期记忆。merge:将两条内容相似或互补的记忆合并成一条更完整的记忆。tag/untag:为记忆添加或删除标签。delete:删除不再相关或错误的记忆。summarize:对一组相关记忆生成一个摘要,并存储为新记忆。
memory_shard_scan:这是一个底层优化工具,它直接利用内存中的TurboQuant压缩数据进行快速扫描,适用于对延迟要求极高的实时过滤场景。memory_strategy_store/memory_strategy_recall:策略管理。智能体可以存储一套“记忆策略”(例如:“所有关于用户偏好的记忆,重要性分数自动加0.1”),并在后续的存储或检索中应用这些策略,实现个性化的记忆管理风格。
3.3 身份认证与安全隔离
在多智能体环境中,记忆隔离至关重要。项目使用JWT Bearer Token进行认证。每个智能体在数据库的mcp_config.agent_identities集合中有一个身份记录,包含一个私钥。服务器用这个私钥验证JWT签名。
实操要点:agent_id是记忆隔离的核心。所有记忆操作都隐式或显式地与一个agent_id绑定。这意味着智能体A无法访问智能体B的记忆,除非系统特意设计了共享逻辑。在生成JWT时,务必确保payload中包含正确的api_key字段(对应身份记录中的标识),并且Token不会泄露。
4. 从零开始部署与集成实战
4.1 环境准备与配置
假设我们从一个干净的Linux开发环境开始。
第一步:基础设施准备你需要一个MongoDB Atlas集群(免费层即可)。在Atlas控制台中:
- 创建一个项目和一个集群(例如,选择M10共享集群)。
- 在集群中,为
mcp-memory-layer创建一个数据库用户,记录用户名和密码。 - 获取集群的连接主机名(如
cluster0.abc12.mongodb.net),注意不要包含mongodb+srv://前缀。 - 在Atlas中,你需要为存储记忆的集合创建向量搜索索引。项目提供了
configs/atlas_indexes.json文件,你可以参考其内容,在Atlas UI的“Search”页面创建索引。通常,索引会建立在embedding字段上,使用cosine相似度度量。
第二步:获取嵌入模型API密钥项目支持多种嵌入模型。以默认的VoyageAI为例:
- 访问VoyageAI官网注册并获取API密钥。
- 你也可以选择OpenAI(需要
OPENAI_API_KEY)或本地运行的Ollama(需要启动Ollama服务并拉取如nomic-embed-text模型)。
第三步:克隆项目并配置
git clone https://github.com/chapmancl/mcp-memory-layer.git cd mcp-memory-layer cp env.example .env编辑.env文件,填入你的核心配置:
# 必须配置 MONGO_URL=cluster0.abc12.mongodb.net MONGO_USERNAME=your_db_user MONGO_PASSWORD=your_db_password EMBED_PROVIDER=voyageai VOYAGEAI_API_KEY=sk-你的voyageai密钥 # 可选配置 MCP_TOOL_NAME=MyMemoryServer # 你的MCP端点名称 SERVER_PORT=80004.2 启动服务器
你有两种主要运行方式:本地Go运行和Docker容器运行。
方式一:本地Go运行(适合开发)
go mod download go run .如果一切顺利,终端会输出服务器启动日志,并显示MCP和REST端点信息。
方式二:Docker运行(适合部署)
docker compose up --build这会根据docker-compose.yml构建镜像并启动容器。如果你想使用支持AWS Bedrock嵌入的版本,需要使用docker-compose.aws.yml并确保你的环境已配置AWS凭证。
验证服务:启动后,打开浏览器或使用curl访问:
curl http://localhost:8000/health应返回{"status":"ok"}。访问http://localhost:8000/会列出所有可用的REST端点。
4.3 生成认证Token并进行首次测试
在调用需要认证的端点前,你需要为你的智能体生成一个JWT Token。项目仓库的mongo-examples子模块或相关文档中通常会有生成Token的脚本示例。这里提供一个概念性的Python示例:
import jwt import time import base64 # 这是你在 mcp_config.agent_identities 集合中为智能体创建的记录中的 `pvk` 字段 # 它是一个base64编码的HMAC-SHA256密钥 agent_pvk_base64 = “你的base64编码私钥” agent_pvk = base64.b64decode(agent_pvk_base64) # JWT Payload,其中 `api_key` 必须对应 agent_identities 文档中的标识 payload = { “api_key”: “my_agent_identity_key”, “agent_id”: “my_cool_agent”, # 这个会用于记忆隔离 “iat”: int(time.time()), “exp”: int(time.time()) + 86400 # 1天后过期 } token = jwt.encode(payload, agent_pvk, algorithm=“HS256”) print(f“Bearer {token}”)将生成的Token设置为环境变量,然后测试记忆的存储与召回:
export TOKEN=”上面生成的JWT Token” # 存储一条记忆 curl -X POST http://localhost:8000/memory/intake \ -H “Authorization: Bearer $TOKEN” \ -H “Content-Type: application/json” \ -d ‘{ “content”: “用户Alice在今天的会议中明确表示,她希望下周发布的报告重点突出转化率数据,而不是总访问量。”, “agent_id”: “my_cool_agent”, “session_id”: “meeting_20231027”, “memory_type”: “user_requirement”, “tags”: [“report”, “priority”, “stakeholder”], “entities”: [{“type”: “person”, “value”: “Alice”}, {“type”: “metric”, “value”: “conversion_rate”}] }’ # 召回相关记忆 curl -X POST http://localhost:8000/memory/recall \ -H “Authorization: Bearer $TOKEN” \ -H “Content-Type: application/json” \ -d ‘{ “query”: “Alice对报告有什么要求?”, “agent_id”: “my_cool_agent”, “limit”: 5 }’如果配置正确,召回请求应该能返回刚刚存储的那条记忆。
4.4 与智能体框架集成
与Claude Desktop集成:这是最丝滑的体验之一。Claude Desktop原生支持MCP。你只需要在Claude Desktop的配置文件中添加你的mcp-memory-layer服务器信息。配置完成后,Claude就能直接看到并调用memory_intake,memory_recall等工具,仿佛这些能力是内置的一样。
与LangChain/CrewAI集成:这些框架通常通过自定义Tool类来集成。你需要创建一个封装了REST API调用的Tool。以LangChain为例:
from langchain.tools import BaseTool import requests class MemoryRecallTool(BaseTool): name = “memory_recall” description = “Search through the agent’s past memories based on a query.” def _run(self, query: str) -> str: resp = requests.post( “http://localhost:8000/memory/recall", headers={“Authorization”: f“Bearer {TOKEN}”}, json={“query”: query, “agent_id”: “my_agent”} ) memories = resp.json() # 将记忆列表格式化为LLM可读的文本 return format_memories(memories)然后将这个Tool加入到你的Agent工具列表中即可。
与VS Code Copilot集成:可以通过配置Copilot的指令文件来实现。在项目的examples/目录下通常有copilot-instructions.example.md,你可以参考它编写指令,告诉Copilot何时以及如何使用记忆工具,例如:“当你需要参考之前关于当前文件的讨论时,使用memory_recall工具进行搜索。”
5. 高级配置、优化与故障排查
5.1 嵌入模型选型与性能调优
选择不同的EMBED_PROVIDER会对成本、速度和效果产生直接影响。
- VoyageAI(默认):通过MongoDB Atlas的托管服务调用,性能稳定,向量维度高(默认1024),语义区分度好,是生产环境的推荐选择。注意其API调用成本。
- OpenAI:
text-embedding-3-large模型效果顶尖,但成本较高,且API调用存在延迟和限流风险。适合对效果要求极高且预算充足的场景。 - Ollama:完全本地运行,零成本,数据隐私性最好。但需要自己维护模型服务,且大多数本地嵌入模型的效果和维度(通常为384或768)与商用API有差距。适合开发、测试或对数据隐私极度敏感的离线环境。
- Bedrock:适合已经深度集成在AWS生态中的团队。Titan Embeddings模型效果不错,且与其他AWS服务(如Secrets Manager)集成方便。
调优建议:
- 维度匹配:确保你选择的嵌入模型维度与
configs/atlas_indexes.json中定义的向量索引维度一致,否则索引无法正常工作。 - 批量处理:在摄入大量历史数据时,不要逐条调用API。应该在应用层实现一个批量嵌入生成和写入的脚本,以节省成本和提升速度。
- 缓存层:对于高频的、重复的查询词,可以在应用层(如Redis)缓存其嵌入向量,避免重复调用嵌入模型API。
5.2 TurboQuant压缩配置详解
TurboQuant是提升内存扫描性能的秘密武器。它的配置主要在configs/turboquant_config.json中。
dimensions:必须与你的嵌入向量原始维度一致(如VoyageAI是1024)。seed:用于生成随机旋转矩阵的种子。重要:在生产环境中,一旦开始写入数据,就不要更改这个种子,否则之前压缩存储的向量将无法正确解码。compressed_bytes:压缩后的字节数。这决定了压缩率和精度损失。通常设置为16、32或64。字节数越少,内存占用越小,扫描越快,但精度损失越大。需要通过实验在速度和召回质量间取得平衡。
压缩流程:当一条记忆被摄入时,其原始向量除了存入MongoDB,还会被TurboQuant压缩,然后与文档的_id、tags、memory_type等关键元数据一起,存入一个内存中的“分片缓存”。这个缓存按agent_id和session_id等维度组织,方便快速定位扫描范围。
5.3 索引策略与查询性能
MongoDB的索引是查询性能的基石。除了必备的向量搜索索引,你还需要考虑以下复合索引:
{agent_id: 1, session_id: 1, created_at: -1}:这是最常用的查询模式之一——按智能体和会话查看时间线。降序排列可以快速获取最新记忆。{agent_id: 1, tags: 1}:加速按标签过滤的查询。{agent_id: 1, memory_type: 1}:加速按记忆类型过滤的查询。{agent_id: 1, “entities.type”: 1, “entities.value”: 1}:如果你经常按实体进行查询,这个索引会很有帮助。
使用explain()命令分析你的慢查询,并据此创建或调整索引。记住,索引会占用存储空间并降低写入速度,需要权衡。
5.4 常见问题与排查实录
问题1:服务器启动失败,提示MongoDB连接错误。
- 排查:首先检查
.env文件中的MONGO_URL、MONGO_USERNAME、MONGO_PASSWORD是否正确。确保网络可以访问MongoDB Atlas(可能需要配置IP白名单)。尝试用mongosh命令行工具直接连接,验证凭证。 - 解决:修正环境变量。如果是网络问题,检查防火墙和Atlas的网络访问列表。
问题2:执行memory_recall查询,返回空数组或无关结果。
- 排查:
- 确认向量搜索索引已正确创建,且索引定义的维度与嵌入模型输出维度匹配。
- 检查查询时传递的
agent_id是否与存储记忆时的agent_id一致。记忆是严格隔离的。 - 尝试一个非常简单的查询,比如存储“apple”,查询“apple”,看是否能召回。如果不能,可能是嵌入模型API调用失败或向量生成有问题。查看服务器日志。
- 如果使用了过滤器(如
tags),检查过滤器语法是否正确,以及记忆文档中是否存在对应的标签。
- 解决:根据排查结果,修复索引、核对agent_id、检查嵌入服务状态或修正查询参数。
问题3:记忆摄入速度很慢。
- 排查:
- 瓶颈可能在嵌入模型API调用。查看服务器日志中嵌入步骤的耗时。
- 也可能是MongoDB写入延迟。检查Atlas集群的监控指标。
- 解决:
- 对于嵌入慢:考虑使用更快的模型(本地Ollama虽然效果稍逊,但延迟极低),或实现异步批量嵌入队列。
- 对于写入慢:检查是否在频繁创建索引,或者文档体积过大。确保
embedding数组字段没有被意外地索引多次。
问题4:TurboQuant扫描似乎没有生效,或者结果不准确。
- 排查:
- 确认
turboquant_config.json中的dimensions设置正确。 - 检查服务器启动日志,看TurboQuant初始化是否成功,以及分片缓存是否被加载。
- 在调用
memory_shard_scan工具或相关接口时,确认传入的shard_key(通常是agent_id和session_id的组合)能正确命中缓存的分片。
- 确认
- 解决:核对配置,重启服务以重新初始化缓存。如果怀疑压缩导致精度问题,可以临时调整
compressed_bytes为一个更大的值(如64)进行测试,权衡性能与精度。
问题5:如何清理或归档旧记忆?
- 方案:项目本身不提供自动清理策略,这需要你根据业务逻辑来实现。一个常见的模式是:
- 定期运行一个后台任务,使用
memory_query查找created_at早于某个时间点且importance_score低于阈值的记忆。 - 对于这些记忆,可以调用
memory_reflect工具的delete操作进行删除,或者将其memory_type改为archived并移动到另一个归档集合。 - 更复杂的策略可以结合访问频率 (
accessed_at)、关联度等因素。关键是将这些策略逻辑实现为一个可调用的管理工具或定时任务,而不是硬编码在核心服务中。
- 定期运行一个后台任务,使用
