ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Agent记忆层拆分为独立MCP服务:本地记忆架构设计与接入实践

Agent记忆层拆分为独立MCP服务:本地记忆架构设计与接入实践 1. 为什么我要把记忆层从 Agent 里拆出来先说结论把本地记忆层做成一个独立的 MCP 服务再让 Agent 通过协议去调用是我这两年折腾 Agent 项目里性价比最高的一次重构。它解决的不是能不能记住的问题而是记忆归谁管、怎么复用、怎么换 Agent 不丢数据的问题。我最早做 Agent 的时候记忆是直接塞在 Agent 进程里的——一个内存字典或者一个本地 SQLite 文件Agent 启动时读进来对话结束写回去。这套做法在单 Agent、单场景的 Demo 里跑得挺欢但只要项目稍微长大一点问题就全冒出来了。最典型的是三个第一我换了 Agent 框架记忆层要重写一遍第二我同时跑两个 Agent一个负责问答、一个负责执行任务两边记忆对不上第三我想给记忆加个向量检索、加个过期淘汰策略结果发现逻辑和 Agent 主循环缠在一起改一处崩三处。后来 MCPModel Context Protocol这个概念火起来我意识到它其实给了一个很干净的解法把记忆层当成一个独立的能力提供方Agent 只是能力消费方。记忆的存储、检索、更新、淘汰全部封装在一个本地服务里Agent 通过标准协议去读写。这样 Agent 换框架、换模型、换编排方式记忆层纹丝不动反过来记忆层升级检索算法Agent 也完全无感。这篇文章我会把整套接入实践拆开讲记忆层到底该存什么、MCP 服务怎么设计、Agent 侧怎么接、并发和一致性怎么处理、以及我踩过的那些坑。适合已经写过至少一个 Agent Demo、想把它往能长期用方向推的开发者。如果你还在纠结 Agent 是什么、MCP 是软件协议还是硬件协议这类基础概念建议先把基础补一补再回来看这篇偏实战。提示本文讲的本地记忆层指的是跑在你本机或内网、数据不出域的存储服务和云端托管记忆是两条路线。选本地还是云端取决于你的数据敏感度和部署条件本文只讨论本地这条线。2. 记忆层到底该存什么三层结构的设计取舍很多人一上来就问用哪个向量库我觉得这是把问题问反了。先想清楚记忆分几类再决定用什么存。我实践下来Agent 的记忆至少分三层每层的生命周期、检索方式、存储介质都不一样。2.1 会话记忆、事实记忆、技能记忆的分工会话记忆Session Memory当前这轮对话的上下文生命周期最短通常几十分钟到几小时。它的特点是读写极频繁、要求极低延迟而且大部分内容用完就废。这层我一般直接放内存或者带 TTL 的 Redis不落盘。事实记忆Fact Memory用户告诉过 Agent 的稳定信息比如我叫老张我偏好用 Python我们团队用 PostgreSQL。生命周期长可能几个月甚至永久。这层需要持久化而且需要按语义检索——用户下次说帮我写个脚本Agent 得能想起他偏好 Python。这层是向量库的主战场。技能记忆Skill MemoryAgent 自己积累的怎么做某件事的经验比如处理这类 CSV 要先检测编码调用某个内部接口要先拿 token。这层介于前两者之间既可以结构化存键值对也可以语义存。我倾向于结构化为主、语义为辅因为技能往往有明确的触发条件。把这三层分开之后你会发现用哪个向量库这个问题自然就有了答案只有事实记忆和部分技能记忆需要向量检索会话记忆根本不需要。我见过太多项目上来就全量向量化结果延迟高、成本高、召回还差。2.2 为什么我不建议把所有记忆都向量化向量检索有个被低估的坑它对精确匹配是无能为力的。用户说我的订单号是 A12345你把这个订单号向量化存进去下次用户问A12345 到哪了向量检索很可能召回一堆语义相似但订单号完全不同的记录。这种场景必须用精确匹配键值或全文索引。我的做法是混合检索结构化字段走精确匹配自由文本走向量检索最后做一次融合排序。具体到实现记忆条目里我会显式标注memory_type和retrieval_hint告诉检索层这条记忆该走哪条路。记忆类型存储介质检索方式典型 TTL会话记忆内存 / Redis按 session_id 直取1-24 小时事实记忆SQLite 向量索引语义 精确混合长期技能记忆SQLite键值 标签匹配长期这张表是我项目里实际用的配置你可以直接抄。SQLite 做本地记忆层的主存储我觉得非常合适——单文件、零运维、支持全文检索FTS5、还能挂向量扩展。别一上来就上 PostgreSQL本地场景 SQLite 足够撑到几十万条记忆。2.3 记忆条目的最小字段设计一条记忆该有哪些字段直接决定了后面检索和淘汰好不好做。我踩过的坑是字段设计太随意导致后面想加记忆重要性排序时发现没地方存权重。现在我的最小字段集是这样的id唯一标识建议用内容哈希天然去重content记忆正文memory_typesession / fact / skillembedding向量可选事实记忆才有tags标签数组用于精确过滤importance0-1 的权重影响淘汰和排序created_at/updated_at时间戳access_count被召回次数用于热度排序source来源标识方便追溯是哪次对话产生的importance和access_count这两个字段是我强烈建议加的。前者让你能做重要记忆优先保留后者让你能做常用记忆优先召回。没有这两个字段你的记忆层就是个只会堆积的垃圾桶。3. MCP 服务端把记忆能力封装成标准工具MCP 的核心价值在于它定义了一套标准的工具暴露方式Agent 不需要知道记忆层内部怎么实现只需要知道有这么几个工具可以调。这一节讲服务端怎么设计。3.1 工具粒度为什么我最终只暴露了四个一开始我设计得很细add_memory、update_memory、delete_memory、search_by_vector、search_by_keyword、get_by_id……一口气暴露了十几个工具。结果 Agent 调用时经常选错工具而且工具描述太长占用了大量上下文。后来我收敛到四个工具覆盖 95% 的场景memory_write写入或更新一条记忆内部按 id 去重有则更新无则插入memory_search检索记忆参数里带modesemantic / exact / hybrid和filtersmemory_forget删除记忆支持按 id 或按条件批量删memory_summarize把一段会话压缩成事实记忆这个工具很关键后面细说工具少了Agent 的选择准确率明显上升。这里有个经验MCP 工具的粒度应该对齐用户意图而不是对齐数据库操作。用户想的是记住这件事不是INSERT 一行。3.2 memory_search 的参数设计细节memory_search是整个记忆层最核心的工具参数设计直接决定召回质量。我的参数结构是这样的{ query: 用户偏好的编程语言, mode: hybrid, memory_type: [fact, skill], tags: [preference], top_k: 5, min_importance: 0.3, recency_weight: 0.2 }几个关键点解释一下。mode让 Agent 自己决定检索策略简单查询用 exact模糊查询用 semantic不确定就用 hybrid。memory_type和tags是硬过滤先缩小范围再检索比纯向量检索准得多。recency_weight是时间衰减权重让新记忆稍微占优——这个参数我调了很久0.2 是个比较平衡的值太高会导致老的重要记忆被淹没。注意top_k不要设太大。我见过有人设 20结果召回一堆弱相关记忆反而干扰 Agent 判断。5 到 8 是比较舒服的区间配合min_importance过滤效果更好。3.3 memory_summarize让记忆自己沉淀这个工具是我觉得最有价值的设计。Agent 每轮对话都往记忆里塞原始文本记忆层很快就会爆炸。memory_summarize的作用是接收一段会话原文让记忆层内部调用一次小模型把它压缩成结构化的事实条目。比如一段对话用户说他最近在做一个 Agent 项目用的是 Python部署在本地遇到了并发问题。 压缩后变成三条事实记忆用户在做一个 Agent 项目tag: project用户偏好 Pythontag: preference用户遇到并发问题tag: issue这样做的好处是记忆密度大幅提升检索时信噪比高很多。代价是要多一次模型调用所以我一般只在会话结束时触发一次 summarize而不是每轮都触发。3.4 服务端用 stdio 还是 HTTP本地场景的选择MCP 服务端有两种常见传输方式stdio 和 HTTPSSE。本地记忆层我强烈建议用 stdio。原因很简单stdio 是进程内管道通信没有网络开销没有端口占用没有鉴权烦恼Agent 启动时把记忆服务作为子进程拉起来就行。HTTP 方式适合记忆层要跨机器共享的场景比如你团队几个人共用一个记忆服务。但本地单机场景用 HTTP 纯属给自己找麻烦——你得管端口、管进程存活、管防火墙。我早期用 HTTP后来全换回 stdio启动脚本都简单了一大截。4. Agent 侧接入从能调到调得好服务端写好了Agent 侧接入其实不难难的是让 Agent用得聪明。这一节讲接入的实操和调优。4.1 接入的最小闭环先跑通再优化最小闭环就三步配置 MCP 服务、注册工具、在系统提示里告诉 Agent 什么时候用记忆。我建议先用最笨的方式跑通——手动触发记忆写入和检索确认数据能存进去、能查出来再去做自动化。配置部分不同 Agent 框架写法不同但核心都是声明一个 MCP server。以常见的配置格式为例{ mcpServers: { local-memory: { command: python, args: [-m, memory_server, --db, ./memory.db], env: { EMBEDDING_MODEL: local-mini, MAX_MEMORY_ITEMS: 100000 } } } }跑通之后你会看到 Agent 的工具列表里多了四个记忆工具。这时候先别急着让它自动用手动测几轮看看检索结果符不符合预期。4.2 系统提示怎么写Agent 才会主动用记忆这是最容易被忽略的一环。很多人接完 MCP 就指望 Agent 自己会用记忆结果 Agent 要么不用要么乱用。系统提示里必须明确三件事什么时候写告诉 Agent 遇到用户陈述稳定信息用户明确要求记住任务产生可复用经验时调用memory_write。不要让它每轮都写那样记忆会被噪音淹没。什么时候读告诉 Agent 在回答需要个性化执行任务前用户提到之前上次时先调memory_search。这里有个技巧——让 Agent 在不确定时先搜一下成本很低但收益很高。怎么写告诉 Agent 写入时带上合适的memory_type和tags。标签体系最好在提示里给几个示例否则 Agent 会自创一堆乱七八糟的标签。我实测下来把这三条写进系统提示后Agent 主动使用记忆的比例从不到 20% 提升到 70% 以上。这个投入产出比非常高。4.3 检索结果怎么喂回上下文检索到的记忆不能直接一股脑塞进上下文得做二次处理。我的做法是按importance × recency × relevance算一个综合分取 top 3 到 5 条格式化成一段简短的背景信息再注入。格式大概是这样[相关记忆] - 用户偏好 Python重要度 0.83 天前 - 用户在做 Agent 项目重要度 0.71 周前这样 Agent 一眼就能看到关键信息而不是被一堆原始文本淹没。记住记忆注入的目标是提供背景不是复述历史。4.4 一个反直觉的经验少即是多我一开始追求记忆越全越好结果 Agent 反而变笨了——因为上下文里塞了太多无关记忆干扰了它对当前任务的判断。后来我把召回数量砍到 3 条并且加了min_importance过滤Agent 的表现明显变好。这背后的道理其实很简单记忆是辅助不是主角。Agent 的核心能力还是理解和推理记忆只是给它提供必要的背景。背景太多反而喧宾夺主。5. 并发、一致性与淘汰本地记忆层的三个硬骨头前面讲的都是顺风顺水的部分这一节讲真正难的地方。本地记忆层一旦上了生产并发、一致性、淘汰这三件事一定会找上门。5.1 多 Agent 同时读写怎么办如果你只有一个 AgentSQLite 默认的串行写入就够了。但如果你像我一样同时跑多个 Agent比如一个问答、一个执行、一个定时任务就会遇到写冲突。我的方案是读操作完全并发写操作走单写者队列。具体实现是在记忆服务里起一个写队列所有memory_write和memory_forget请求排队处理读请求直接走。SQLite 开 WAL 模式后读和写可以并发性能足够。如果你用的是别的存储思路一样把写操作串行化读操作放开。别指望数据库自己搞定并发写本地场景下自己控制更可靠。5.2 记忆冲突同一个事实被写了两次不同的值这是很常见的场景用户先说我用 Python后来说我改用 Go 了。两条记忆都存进去检索时召回哪条我的处理策略是同 tag 同 key 的记忆新值覆盖旧值但旧值保留在历史里。具体做法是给记忆加一个superseded_by字段新记忆写入时把旧记忆标记为已被取代检索时默认过滤掉被取代的。这样既保证了当前状态正确又保留了变更历史方便追溯。这个设计我踩过坑才想明白。最早我是直接删旧值结果用户问我之前用什么语言时Agent 完全答不上来。保留历史之后这类问题就能回答了。5.3 淘汰策略什么时候该忘记忆不能无限增长必须有淘汰。我的淘汰策略是分层的会话记忆TTL 到期自动删不犹豫事实记忆按importance和access_count综合排序低于阈值的进入冷存储超过一定时间没被访问就删技能记忆基本不主动删但会定期合并相似条目淘汰的触发时机我选在每天凌晨低峰期跑一次批处理而不是实时淘汰。实时淘汰会引入额外的写操作和锁竞争得不偿失。提示淘汰前一定要做备份。我有一次淘汰逻辑写错把重要记忆全删了幸好有备份。现在我的记忆服务每天自动导出一次快照保留最近 7 天。5.4 记忆服务的健康检查本地服务最怕的是悄悄挂了。Agent 调用记忆工具失败如果没处理好可能直接报错中断。我的做法是给记忆服务加一个轻量健康检查Agent 启动时 ping 一次运行中每隔几分钟 ping 一次失败就降级——记忆功能不可用时Agent 继续工作只是不带记忆。这个降级设计很重要。记忆是增强功能不是核心功能不能因为它挂了就让整个 Agent 瘫痪。6. 我踩过的坑和对应的解法这一节全是血泪教训每一条都是我真金白银试出来的。6.1 坑一向量模型换了历史记忆全废我最早用的嵌入模型是某个通用模型后来想换成更小的本地模型省资源。结果一换发现新旧向量不在同一个空间历史记忆的向量全部失效检索结果乱七八糟。解法记忆条目里存embedding_model字段检索时只比对同模型的向量。换模型时要么全量重算要么新旧并存一段时间。我现在固定用一个模型换之前一定先做全量重算的预案。6.2 坑二记忆写入没有去重同一件事存了十几遍Agent 有时候会反复确认同一件事导致同一条记忆被写入多次。检索时召回一堆重复内容浪费上下文。解法用内容哈希做 id写入前先查 id 是否存在存在就更新updated_at和access_count不新增。这一招直接解决了 90% 的重复问题。6.3 坑三检索延迟拖垮了 Agent 响应有段时间 Agent 响应特别慢排查发现是记忆检索平均要 800ms。原因是向量检索没建索引全表扫描。解法给向量列建 ANN 索引SQLite 可以用向量扩展或者自己实现一个简单的 HNSW。建完索引后检索降到 30ms 以内。这个优化是必须做的别偷懒。6.4 坑四Agent 把敏感信息写进了记忆这个坑最危险。Agent 有时候会把用户随口说的密码、token 之类的东西写进记忆。虽然是本地存储但也是隐患。解法在memory_write里加一层敏感信息过滤用正则匹配常见的敏感模式密码、密钥、身份证号等命中就拒绝写入或者脱敏后再写。这层过滤必须在服务端做不能指望 Agent 自觉。6.5 坑五记忆服务的日志把磁盘写满了本地服务跑久了日志文件能涨到几个 G。我有一次磁盘满了才发现。解法日志按大小滚动保留最近几个文件。记忆服务这种长期运行的东西日志管理一定要提前做。7. 关于复用怎么把这套东西搬到下一个项目最后聊聊复用。这套记忆层我后来在三个不同项目里复用每次接入成本不到半天。能复用的关键是我做了两件事。第一件是把记忆层做成完全独立的进程不依赖任何特定 Agent 框架。它只认 MCP 协议谁来调都行。这样换 Agent 框架时记忆层零改动。第二件是把配置全部外置。数据库路径、嵌入模型、淘汰策略、召回参数全部走配置文件或环境变量。新项目接入时改几个配置就能跑不用动代码。我现在的标准做法是记忆层作为一个独立的 Python 包发布新项目pip install一下配好 MCP server 声明就能用。这套流程跑顺之后我再也没为记忆这件事操过心。如果你也在做 Agent 项目我强烈建议尽早把记忆层拆出来。越早拆后面越省事。等到记忆逻辑和 Agent 主循环缠成一团再拆那个痛苦程度会翻好几倍。
返回列表