基于MCP协议的LLM Agent记忆系统:事后复盘与三层记忆架构实践 1. 项目缘起为什么“事后复盘”值得被单独做成一个系统“hindsight”这个词本身很有意思字面意思是“事后的洞察”中文里最贴切的翻译大概是“后见之明”。但在 LLM Agent 的语境下它指向的是一个非常具体、非常工程化的问题Agent 在完成一次任务之后能不能把这次经历沉淀下来变成下一次可以调用的记忆我最初接触这个概念是因为在做一个基于 MCP 协议的多工具 Agent 项目。当时遇到一个很典型的问题Agent 每次执行任务都像失忆一样上一轮踩过的坑、试过的有效路径、用户明确表达过的偏好下一轮全部归零。你可能会说这不是有 RAG 吗把历史对话塞进向量库不就行了但实际跑下来你会发现原始对话记录和“可复用的经验”之间隔着一道巨大的鸿沟。对话里充斥着大量噪音、试错、重复确认直接做向量检索召回的内容往往是低质量的。hindsight 这个项目要解决的就是这个问题。它不是一个简单的“对话历史存储”而是一套Agent 记忆的提炼、组织与召回机制。核心思路是在 Agent 完成一个任务周期后触发一次“事后复盘”流程把这次任务中的关键决策、有效路径、失败教训、用户偏好抽取出来结构化成可检索、可组合的记忆单元再通过 MCP 协议暴露给后续的 Agent 调用。这套东西适合谁如果你正在做 Agent 应用尤其是那种需要跨会话保持上下文、需要从历史经验中学习、需要多 Agent 共享知识的场景那 hindsight 的思路值得你花时间研究。如果你只是用 LLM 做单轮问答那这个项目的价值对你来说可能没那么直接。但只要你开始碰“Agent 记忆”这个方向hindsight 提供的这套“事后提炼”的视角会比单纯的向量库方案扎实得多。2. 核心设计拆解Agent 记忆到底该怎么存、怎么取2.1 为什么不能直接把对话历史当记忆用我先说一个我踩过的坑。早期做 Agent 记忆我的做法很粗暴每轮对话结束后把完整的 message 列表序列化存进一个 JSON 文件或者向量库。检索的时候用当前 query 去匹配历史 message。听起来没问题对吧实际跑起来问题一大堆。第一个问题是信噪比极低。一次任务执行可能有二三十轮工具调用其中大部分是中间状态比如“正在查询数据库”“返回结果为空换一个参数再试”。这些内容对后续任务几乎没有参考价值但它们会占据向量库的空间还会在检索时被召回干扰判断。第二个问题是缺乏结构。原始对话是线性的、时序的但 Agent 需要的记忆往往是按“实体”“意图”“约束条件”来组织的。比如用户说“以后所有报表都用 UTC 时间”这是一条约束它应该被单独抽出来而不是淹没在某轮对话的中间。第三个问题是无法组合。后续任务可能需要同时用到“用户偏好”“上次失败的原因”“某个工具的正确调用方式”这三条记忆但如果它们散落在不同的对话片段里检索系统很难把它们有效地聚合起来。hindsight 的设计正是针对这三个问题。它的核心洞察是记忆不是存出来的是提炼出来的。原始对话是矿石hindsight 要做的是冶炼。2.2 记忆的三层结构Working Memory、Episodic Memory、Semantic Memory参考认知科学里对记忆的分类hindsight 把 Agent 记忆分成三层这个分层是我认为整个项目里最值得借鉴的设计。Working Memory工作记忆是最短期的只服务于当前任务周期。它保存的是当前任务的上下文、中间结果、临时变量。任务结束这一层基本就清空了或者只保留极少量关键状态。这一层的实现通常就是内存里的一个字典或者队列不需要持久化也不需要向量检索。Episodic Memory情景记忆是中期层保存的是“某一次任务是怎么完成的”。它记录的是具体的事件序列用户提出了什么需求、Agent 做了哪些尝试、哪些成功了、哪些失败了、最终结果是什么。这一层需要持久化通常用结构化存储比如 SQLite 或者文档数据库加上向量索引。检索的时候既支持按时间范围查也支持按语义相似度查。Semantic Memory语义记忆是长期层保存的是从多次情景记忆中抽象出来的“知识”。比如“用户偏好 UTC 时间”“调用某个 API 时需要先获取 token”“某类任务的平均执行步骤是五步”。这一层是跨任务、跨会话的更新频率低但价值密度最高。它的存储形式更接近知识图谱或者规则库检索时以精确匹配和规则推理为主。这三层的划分解决了我前面说的“信噪比”和“结构”问题。原始对话进入 Episodic Memory 之前会经过一次提炼把噪音过滤掉而 Semantic Memory 则是从多条 Episodic Memory 中归纳出来的天然具有更高的抽象层级。2.3 MCP 协议在其中的角色让记忆成为可调用的工具hindsight 选择用 MCP 协议来暴露记忆能力这个选择很关键。MCP 本质上是一套工具调用协议它让 Agent 可以像调用普通工具一样调用“记忆服务”。具体来说hindsight 会注册几个 MCP tool比如memory_store、memory_recall、memory_forget。Agent 在执行任务时可以通过这些 tool 来主动存取记忆。这比“自动注入上下文”的方式灵活得多因为 Agent 可以根据当前任务的需要决定什么时候去查记忆、查哪一层记忆、要不要写入新记忆。我实测下来这种“主动记忆”的模式比“被动注入”效果好很多。被动注入的问题是你很难控制注入的内容和时机容易把不相关的记忆塞进上下文浪费 token 还干扰判断。而主动模式把决策权交给 Agent配合好的 prompt 引导Agent 会自己判断“这个信息值得记”或者“我需要查一下之前有没有类似任务”。MCP 的另一个好处是跨 Agent 共享。如果你有多个 Agent 在同一个环境里工作它们可以通过同一个 MCP server 来共享记忆。比如一个 Agent 学会了某个工具的正确用法写入 Semantic Memory另一个 Agent 在遇到类似任务时就能直接查到。这种共享机制在多 Agent 协作场景里非常有用。2.4 Docker 化部署为什么记忆服务需要独立容器hindsight 的部署方式推荐用 Docker这个选择背后有实际考量。记忆服务通常需要持久化存储数据库、向量库还需要一定的计算资源embedding 模型、检索排序。把它和 Agent 主程序放在同一个进程里会导致资源耦合、升级困难、扩展性差。用 Docker 独立部署好处有几个。第一是环境隔离记忆服务依赖的 Python 版本、数据库驱动、向量库版本不会和 Agent 主程序冲突。第二是持久化方便通过 volume 挂载记忆数据可以独立于容器生命周期存在容器重启不丢数据。第三是扩展灵活如果记忆检索压力大可以单独给这个容器加资源或者横向扩展多个实例。我自己的部署方案是hindsight 跑在一个 Docker 容器里内部用 SQLite 做结构化存储用 Chroma 或者 Qdrant 做向量索引embedding 模型用一个小型的本地模型比如 bge-small通过 MCP 协议对外暴露接口。Agent 主程序通过 stdio 或者 SSE 连接到这个 MCP server。整套东西跑在一台 4C8G 的机器上完全够用。3. 实操落地从零搭一套 hindsight 记忆系统3.1 环境准备与依赖安装先说基础环境。我假设你用的是 Linux 或者 macOSWindows 的话建议用 WSL2因为 Docker Desktop 在 Windows 上的网络配置有时候会比较折腾。Docker 的安装这里不展开网上教程很多你只要确保docker --version和docker compose version都能正常输出就行。Python 环境我建议用 3.11 或以上因为 hindsight 用到的一些库比如新版 pydantic对版本有要求。用 conda 或者 venv 都行我个人习惯用 venv轻量。python -m venv hindsight-env source hindsight-env/bin/activate pip install mcp chromadb sentence-transformers sqlalchemy fastapi uvicorn这里解释一下几个关键依赖的选择理由。mcp是官方 SDK用来实现 MCP server。chromadb是我选的向量库轻量、易用、支持持久化适合中小规模记忆场景。sentence-transformers用来做 embedding我选的是BAAI/bge-small-zh-v1.5中文效果好模型小推理快。sqlalchemy做结构化存储的 ORMfastapi和uvicorn用来提供一个可选的 HTTP 管理接口方便调试和查看记忆状态。注意如果你在国内网络环境下载 sentence-transformers 模型可能会比较慢。建议提前用 huggingface-cli 或者 modelscope 把模型下载到本地然后通过本地路径加载。3.2 记忆数据模型设计在写代码之前先把数据模型定清楚。hindsight 的三层记忆在存储上对应三张表或者三个 collection。Working Memory我用一个内存字典实现key 是 session_idvalue 是一个列表保存当前任务周期的临时状态。这个不需要持久化进程重启就丢符合工作记忆的定位。Episodic Memory用 SQLite 存结构化字段用 Chroma 存向量。结构化字段包括episode_id、session_id、task_description、start_time、end_time、outcome、key_stepsJSON、failuresJSON、user_feedback。向量部分存的是 task_description 加上 key_steps 的拼接文本的 embedding。Semantic Memory也用 SQLite 加 Chroma。结构化字段包括memory_id、memory_type偏好/规则/事实、content、confidence、source_episodesJSON 列表、created_at、updated_at。向量部分存 content 的 embedding。这里有个设计细节值得说Semantic Memory 的 confidence 字段。不是所有从 Episodic Memory 归纳出来的知识都同样可靠。比如用户只说过一次“用 UTC”那这条偏好的 confidence 就低如果用户在多个任务里都强调了confidence 就高。检索的时候confidence 可以作为排序的一个因子避免低置信度的记忆干扰判断。3.3 记忆提炼流程的实现这是 hindsight 最核心的部分。每次任务结束后触发一次提炼流程。流程分三步抽取、归纳、写入。抽取阶段把当前 session 的完整对话历史和工具调用记录拿出来用 LLM 做一次结构化抽取。Prompt 大概是这样你是一个记忆提炼助手。请从以下任务记录中抽取关键信息输出 JSON 格式。 需要抽取的字段 - task_description: 用一句话描述这次任务的目标 - key_steps: 完成任务的关键步骤列表每步一句话 - failures: 失败的尝试及原因如果没有则为空列表 - user_preferences: 用户在这次任务中表达的偏好或约束 - reusable_knowledge: 从这次任务中可以抽象出的可复用知识 任务记录 {conversation_history}这个 prompt 的关键在于明确输出结构。LLM 如果不加约束会输出一大段自然语言后续处理很麻烦。用 JSON schema 约束之后解析就简单了。归纳阶段把抽取出来的reusable_knowledge和已有的 Semantic Memory 做比对。如果发现相似的知识就更新 confidence 和 source_episodes如果是新的就创建一条新记忆。这个比对用向量相似度做初筛再用 LLM 做一次确认避免误合并。写入阶段把 Episodic Memory 和 Semantic Memory 分别写入对应的存储。注意写入之前要先做 embeddingChroma 的 add 操作需要传入 embedding 向量。import chromadb from sentence_transformers import SentenceTransformer model SentenceTransformer(BAAI/bge-small-zh-v1.5) client chromadb.PersistentClient(path./hindsight_db) episodic_collection client.get_or_create_collection(episodic) semantic_collection client.get_or_create_collection(semantic) def write_episodic(episode): text episode[task_description] .join(episode[key_steps]) embedding model.encode(text).tolist() episodic_collection.add( ids[episode[episode_id]], embeddings[embedding], metadatas[{ session_id: episode[session_id], outcome: episode[outcome], start_time: episode[start_time] }], documents[text] )这段代码里documents存的是原始文本embeddings存的是向量metadatas存的是结构化字段。检索的时候可以用向量相似度查也可以用 metadata 过滤很灵活。3.4 MCP Server 的注册与工具定义MCP server 的实现核心是定义 tool 的 schema 和对应的处理函数。hindsight 暴露三个 toolmemory_store、memory_recall、memory_forget。memory_store接收一个记忆对象根据类型写入 Episodic 或 Semantic。memory_recall接收一个 query 和可选的 memory_type 过滤条件返回最相关的若干条记忆。memory_forget接收一个 memory_id删除对应记忆用于处理过时或错误的记忆。from mcp.server import Server from mcp.types import Tool, TextContent server Server(hindsight) server.list_tools() async def list_tools(): return [ Tool( namememory_recall, description检索 Agent 记忆。输入查询文本和可选的记忆类型返回相关记忆列表。, inputSchema{ type: object, properties: { query: {type: string}, memory_type: {type: string, enum: [episodic, semantic, all]}, top_k: {type: integer, default: 5} }, required: [query] } ), # ... 其他 tool 定义 ]这里有个实操心得tool 的 description 要写得足够详细。Agent 决定要不要调用某个 tool很大程度上依赖 description 里的信息。如果你只写“检索记忆”Agent 可能不知道什么时候该用。写清楚“当你需要回忆之前的任务经验、用户偏好或可复用知识时调用”Agent 的调用准确率会高很多。3.5 Docker Compose 编排与持久化配置最后把整套东西用 Docker Compose 编排起来。我的配置大概是这样version: 3.8 services: hindsight: build: . volumes: - ./data:/app/data - ./models:/app/models environment: - EMBEDDING_MODEL_PATH/app/models/bge-small-zh-v1.5 - DB_PATH/app/data/hindsight.db - CHROMA_PATH/app/data/chroma ports: - 8765:8765 restart: unless-stopped关键点是 volume 挂载。./data挂载到容器内的/app/data这样 SQLite 数据库和 Chroma 的持久化文件都在宿主机上容器删了重建数据还在。./models挂载本地模型文件避免每次启动都去下载。注意如果你在 Windows 上用 Docker Desktopvolume 挂载的路径要用绝对路径而且要注意文件权限问题。我遇到过容器内写入失败的情况最后是把宿主机目录权限改成 777 才解决。生产环境当然不能这么干但本地开发阶段这样最省事。4. 避坑指南我在实操中遇到的五个典型问题4.1 Embedding 模型选型不是越大越好我一开始用的是text-embedding-ada-002效果确实好但有两个问题一是需要联网调用延迟高二是成本随记忆量增长而增长。后来换成bge-small-zh-v1.5本地推理延迟从 200ms 降到 20ms 以内效果在中文场景下差距不大。选 embedding 模型的核心考量是你的记忆内容以什么语言为主。如果是中文为主bge 系列的中文模型性价比很高。如果是多语言混合可以考虑paraphrase-multilingual-MiniLM-L12-v2。模型大小方面small 级别100MB 左右在大多数场景下够用base 级别400MB 左右效果更好但推理慢一倍。我的建议是先用 small如果检索准确率不达标再升级。4.2 记忆冲突处理新记忆和旧记忆矛盾怎么办这是实际跑起来一定会遇到的问题。比如用户上周说“报表用 UTC”这周说“报表用北京时间”。两条 Semantic Memory 冲突了。我的处理策略是时间优先 置信度加权。新记忆写入时如果检测到与旧记忆冲突向量相似度高但内容矛盾就把旧记忆的 confidence 降低新记忆的 confidence 设为较高值。检索时confidence 低于阈值的记忆不返回。同时保留旧记忆但在 metadata 里标记superseded_by字段方便追溯。这个策略不是完美的但在实践中够用。更复杂的方案可以用 LLM 做冲突仲裁但成本高而且仲裁结果也不一定对。我的观点是记忆系统要允许一定程度的模糊和矛盾不要追求绝对一致因为用户本身就可能改变主意。4.3 检索结果排序相似度不是唯一因子单纯按向量相似度排序效果往往不好。我加了几个排序因子时间衰减越新的记忆权重越高、confidence高置信度记忆优先、使用频率被召回次数多的记忆权重略高。具体实现是在 Chroma 返回相似度分数后做一次重排序def rerank(results, now): scored [] for r in results: similarity 1 - r[distance] age_days (now - r[metadata][created_at]).days time_decay 0.99 ** age_days confidence r[metadata].get(confidence, 0.5) usage min(r[metadata].get(recall_count, 0) / 10, 1.0) final_score similarity * 0.6 time_decay * 0.2 confidence * 0.15 usage * 0.05 scored.append((final_score, r)) return [r for _, r in sorted(scored, keylambda x: -x[0])]权重是我拍脑袋定的你可以根据自己的场景调。核心思路是不要让相似度一家独大多因子加权能让检索结果更符合直觉。4.4 MCP 连接稳定性stdio 还是 SSEMCP 支持两种传输方式stdio 和 SSE。stdio 适合本地进程间通信简单、低延迟但 Agent 和 MCP server 必须在一台机器上。SSE 适合远程调用但需要处理连接断开、重连、心跳等问题。我一开始用 SSE因为想让多个 Agent 共享一个记忆服务。但实际跑下来SSE 的连接稳定性是个坑。网络抖动、容器重启、长时间空闲都可能导致连接断开。后来我改成 stdio 本地连接每个 Agent 启动时拉起一个 hindsight 进程通过共享的 SQLite 文件来同步记忆。这样虽然每个 Agent 有独立的 MCP server 进程但数据是共享的稳定性好很多。提示如果你确实需要远程共享SSE 也不是不能用但一定要加心跳和自动重连逻辑。我试过用sse-starlette加自定义心跳效果还行但复杂度明显上升。4.5 记忆膨胀怎么控制存储规模跑了一段时间之后Episodic Memory 会积累得很快。每次任务都写一条一个月下来可能几千条。检索延迟会上升存储也会膨胀。我的做法是定期归档和压缩。每周跑一次归档任务把超过 30 天的 Episodic Memory 做一次聚类相似的任务合并成一条“典型情景”原始记录移到冷存储比如单独的文件或者另一个 collection。Semantic Memory 不做归档因为它的增长速度慢得多而且价值密度高。另外memory_forget这个 tool 要真的用起来。Agent 如果发现某条记忆明显错误或者过时应该主动调用 forget。我在 prompt 里加了引导“如果你发现检索到的记忆与当前事实不符请调用 memory_forget 删除它。” 实测下来Agent 确实会偶尔主动清理错误记忆效果不错。5. 效果验证与扩展方向5.1 怎么判断记忆系统有没有用我设计了一个简单的评测方法准备一组任务每个任务执行两次。第一次不启用记忆第二次启用记忆。对比两次的任务完成率、平均步数、用户满意度人工打分。实测数据基于我自己的 20 个测试任务启用记忆后任务完成率从 65% 提升到 85%平均步数从 12 步降到 8 步用户满意度从 3.2 提升到 4.15 分制。提升最明显的场景是重复性任务和有明确用户偏好的任务。对于完全新颖的任务记忆的帮助有限因为确实没有相关经验可参考。这个评测方法不严谨但足够说明问题。如果你要做更正式的评测可以参考 LLM Agent 记忆相关的论文里面有更系统的指标设计。5.2 后续可以怎么扩展hindsight 目前的实现是单机、单用户的。如果要扩展到多用户或者团队场景有几个方向可以走。多租户隔离在记忆的 metadata 里加user_id或team_id检索时强制过滤。这个改动不大但要注意 embedding 的共享问题——不同用户的记忆最好不要混在一起做向量检索否则容易串味。记忆图谱把 Semantic Memory 从扁平的列表升级成图谱结构用实体和关系来组织。比如“用户 A” --偏好-- “UTC 时间”“UTC 时间” --适用于-- “报表任务”。这样检索的时候可以做多跳推理找到更间接相关的记忆。这个方向可以参考 GraphRAG 的思路。主动记忆现在的记忆写入是任务结束后触发的。更进一步的做法是让 Agent 在任务执行过程中主动判断“这个信息值得记”实时写入 Working Memory任务结束后再决定要不要提升到 Episodic 或 Semantic。这样能捕捉到更多细节但对 Agent 的判断能力要求更高。记忆压缩与抽象随着 Semantic Memory 增多可以考虑用 LLM 定期做一次“记忆整理”把多条相关记忆合并成一条更抽象的规则。比如“用户喜欢 UTC”“用户不喜欢本地时间”“用户要求时间戳统一”可以合并成“用户在时间相关配置上偏好 UTC 且要求全局统一”。这样能降低记忆数量提高检索效率。5.3 一个容易被忽略的点记忆的可解释性最后说一个我觉得很重要但容易被忽略的点。Agent 的记忆系统可解释性和可审计性很关键。当 Agent 做出一个决策时你最好能追溯它是基于哪条记忆做出的。这在调试和信任建立阶段特别重要。我的做法是在每次memory_recall返回结果时附带记忆的 ID 和来源 episode。Agent 在最终输出里可以引用这些 ID这样人工审查时就能看到“Agent 之所以这么做是因为它回忆起了第 123 号情景记忆”。这个机制不复杂但对建立对 Agent 的信任很有帮助。我在实际使用中发现当用户能看到 Agent 是“基于什么经验”做出判断时他们对 Agent 的容错度会高很多。即使 Agent 做错了用户也能理解“哦它是被某条过时记忆误导了”然后手动去清理那条记忆。这种透明性是记忆系统从“能用”到“好用”的关键一步。