☰
从零构建Agent记忆系统:分层架构、MCP协议与Docker部署实战
2026/9/30 8:20:51 网站建设 项目流程

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

第一次看到“hindsight”这个词,我脑子里蹦出来的不是词典释义,而是自己踩过的一个坑。去年我搭了一个基于LLM的客服Agent,跑单轮问答时表现堪称完美,可一旦用户隔了三天回来追问“上次那个订单后来怎么处理的”,它就彻底失忆,像个刚入职的新人一样从头问起。那一刻我意识到,Agent的智能程度,很大程度上不取决于它有多会“回答”,而取决于它有多会“记住”。hindsight这个项目标题,翻译过来就是“后见之明”,说白了就是让Agent拥有回看历史、从过往交互中提取经验的能力——这正是当前agent memory领域最核心的命题。

这篇文章我想聊的,就是围绕hindsight这类Agent记忆系统,怎么从零把它落地。涉及的关键词包括agent memory、LLM、MCP、Docker,我会把记忆分层设计、MCP协议接入、Docker容器化部署这几块串起来讲透。适合谁看?如果你正在做LLM应用、被Agent的“金鱼记忆”折磨过、或者想搞清楚MCP到底怎么和记忆系统配合,那这篇就是写给你的。哪怕你只是刚接触Docker,我也会把每一步拆到能直接抄作业的程度。

先说清楚hindsight要解决的根本问题。大模型的上下文窗口再大,也是有限的,而且每次请求都是无状态的——你不主动把历史喂给它,它就当什么都没发生过。传统的做法是把对话历史一股脑塞进prompt,但这样有两个致命伤:一是token成本随对话轮次线性暴涨,二是无关信息会稀释模型的注意力,导致“记了一堆废话,关键信息反而丢了”。hindsight的思路是把记忆从“临时上下文”升级为“可检索、可分层、可持久化的独立系统”,让Agent像人一样,有短期的工作记忆,也有长期的归档记忆,需要的时候再精准调取。

2. Agent记忆系统的整体架构设计思路

2.1 为什么不能只靠一个向量数据库

很多人一提Agent记忆,第一反应就是“上个向量库不就行了”。我早期也这么干过,把所有对话切片丢进向量数据库,检索时按相似度召回。结果发现几个问题:第一,时间维度丢失,三天前的闲聊和昨天的关键决策在向量空间里可能挨得很近,但重要性天差地别;第二,缺乏结构化,用户说“我下周三要去北京出差”,这句话里的时间、地点、事件是结构化的,纯向量检索很难精确利用;第三,没有遗忘机制,记忆只增不减,越跑越臃肿。

所以hindsight这类系统通常采用分层记忆架构。我把它类比成人的记忆:工作记忆(working memory)就像你脑子里此刻正在处理的信息,容量小、易失;情景记忆(episodic memory)是具体发生过的事件,带时间戳;语义记忆(semantic memory)是从事件中抽象出的规律和事实。这三层各司其职,检索时按需组合,而不是一锅乱炖。

2.2 三层记忆的职责划分与选型考量

具体到工程实现,我是这么划分的:

  • 工作记忆层:存当前会话的最近N轮对话,直接放在内存或Redis里,读写极快,会话结束或超时后按策略落盘或丢弃。这一层解决的是“刚才聊了什么”。
  • 情景记忆层:存带时间戳和元数据的交互事件,用支持结构化过滤的存储,比如PostgreSQL配合pgvector,或者专门的向量库加元数据字段。这一层解决的是“某年某月某日发生了什么”。
  • 语义记忆层:存从多次交互中提炼出的事实、偏好、结论,比如“这个用户偏好简洁回复”“该订单的退款政策是7天”。这一层解决的是“关于这个用户/这件事,我长期该知道什么”。

选型上我踩过的坑是:别一上来就追求全自动。早期我想让LLM自动决定什么进语义层,结果它把一堆临时信息也固化了,污染了长期记忆。后来改成“规则+LLM”混合:明确的用户偏好、确认过的事实走规则直接入库,模糊的才交给LLM判断,准确率一下子上来了。

2.3 记忆的写入、检索与遗忘闭环

一个完整的记忆系统必须形成闭环,否则就是只进不出的死水。我的设计是:

写入侧,每次交互结束后触发一个异步任务,把本轮对话做三件事——切分、打标签(时间、实体、重要性评分)、分发到对应层级。这里重要性评分很关键,我一般让LLM给每轮对话打个1到5分,低于阈值的只进工作记忆,不进长期层。

检索侧,用户新请求进来时,先做query改写,提取出“我是谁、我在找什么、我能提供什么”这三个要素(这正好对应热词里提到的token三个点:key、query、value),然后用改写后的query同时查三层记忆,按相关性和时间衰减加权排序,取Top-K拼进prompt。

遗忘侧,我设置了两条规则:一是时间衰减,超过一定天数的低重要性记忆自动降权;二是容量上限,每层设最大条数,超了就淘汰最旧或最低分的。这套机制跑下来,记忆库能长期保持“精而不杂”。

3. 核心细节解析:MCP协议如何打通记忆与Agent

3.1 MCP到底是什么,为什么它适合做记忆接口

MCP(Model Context Protocol)这个词最近热度很高,但很多人第一次听会懵:它到底是软件协议还是硬件协议?简单说,MCP是一套让LLM应用和外部工具/数据源标准化通信的软件协议。你可以把它理解成“AI世界的USB接口”——以前每个工具都要为每个模型单独适配,现在大家都按MCP的规范来,插上就能用。

为什么记忆系统特别适合用MCP暴露?因为记忆的读写本质上是“工具调用”:Agent需要“存一条记忆”“查相关记忆”,这天然就是工具接口的形态。用MCP把记忆系统封装成一个server,任何支持MCP的客户端(比如各种IDE、Agent框架)都能直接调用,不用改一行记忆系统的代码。这就是解耦的价值。

3.2 用MCP封装记忆服务的接口设计

我实际封装时,暴露了这么几个核心工具:

{ "tools": [ { "name": "memory_write", "description": "写入一条记忆", "parameters": { "content": "记忆内容", "layer": "working|episodic|semantic", "importance": "1-5", "metadata": "时间、实体等结构化字段" } }, { "name": "memory_search", "description": "检索相关记忆", "parameters": { "query": "检索query", "layers": "要检索的层", "top_k": "返回条数" } }, { "name": "memory_forget", "description": "按条件删除或降权记忆", "parameters": { "filter": "删除条件" } } ] }

设计这几个接口时有个心得:参数要留足扩展位。比如metadata我一开始只放了时间,后来发现实体、来源、置信度都得存,如果当初写死了就得改协议。所以宁可一开始字段宽松点,用JSON存扩展信息。

3.3 记忆检索的query改写与重排策略

检索质量直接决定记忆系统的成败。我实测下来,原始用户query直接拿去检索,召回质量很差,因为用户的话往往口语化、指代多。所以中间必须加一层query改写。

我的做法是让LLM把用户输入改写成三个部分:key(我是谁,即用户/会话标识)、query(我在找什么,即检索意图)、value(我能提供什么,即可能的答案线索)。这个三要素框架特别好用,因为它强迫模型把“检索意图”和“已知线索”分开,检索时用query去匹配,用key做过滤,用value做重排参考。

重排阶段我用的是“向量相似度+时间衰减+重要性”的加权公式:

final_score = 0.6 * vector_sim + 0.25 * importance_norm + 0.15 * time_decay

权重是我根据自己业务调出来的,向量相似度占大头,但重要性和新鲜度也不能忽视。你可以根据自己的场景调这三个系数,比如客服场景时间衰减要快,知识库场景重要性权重可以更高。

4. 实操过程:用Docker把整套记忆系统跑起来

4.1 环境准备与Docker安装的坑

这部分我要重点讲,因为Docker安装是新手最容易卡住的地方。Windows上装Docker Desktop,最常见的报错就是“virtualization support not detected”或者“Docker Desktop failed to start because virtualization...”。这不是Docker的锅,是你主板的虚拟化没开。

解决步骤:重启进BIOS,找到Intel VT-x或AMD-V选项,设为Enabled。如果是Windows,还要确认“Hyper-V”和“虚拟机平台”这两个Windows功能是勾选的。我见过有人折腾一下午,最后发现就是BIOS里一个开关没开。

Linux上装Docker相对省心,用官方脚本:

curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh sudo systemctl enable docker sudo systemctl start docker

装完记得把当前用户加进docker组,不然每次都要sudo:

sudo usermod -aG docker $USER

然后重新登录一次生效。这个细节文档里经常一笔带过,但不做的话后面跑容器会一直权限报错。

4.2 用docker-compose编排记忆系统的各个组件

整套系统我拆成几个容器:向量库(用Qdrant或pgvector)、关系库(PostgreSQL存结构化记忆)、Redis(工作记忆)、以及记忆服务本身。用docker-compose一把编排:

version: '3.8' services: postgres: image: pgvector/pgvector:pg16 environment: POSTGRES_PASSWORD: yourpassword POSTGRES_DB: agent_memory ports: - "5432:5432" volumes: - pgdata:/var/lib/postgresql/data redis: image: redis:7-alpine ports: - "6379:6379" memory-service: build: ./memory-service depends_on: - postgres - redis environment: DB_URL: postgresql://postgres:yourpassword@postgres:5432/agent_memory REDIS_URL: redis://redis:6379 ports: - "8080:8080" volumes: pgdata:

这里有个关键点:容器间通信用服务名,不是localhost。我见过新手在memory-service里写DB_URL=localhost:5432,结果死活连不上,因为localhost在容器里指的是容器自己。要用compose里定义的服务名postgres。

4.3 记忆服务的核心代码实现

记忆服务的写入逻辑,我用Python写了个简化版:

import json from datetime import datetime def write_memory(content, layer, importance, metadata): # 1. 生成embedding embedding = get_embedding(content) # 2. 按层分发 if layer == "working": redis_client.lpush(f"wm:{metadata['session_id']}", json.dumps({"content": content, "ts": datetime.now().isoformat()})) redis_client.ltrim(f"wm:{metadata['session_id']}", 0, 19) # 只留最近20条 elif layer == "episodic": db.execute(""" INSERT INTO episodic_memory (content, embedding, importance, metadata, created_at) VALUES (%s, %s, %s, %s, %s) """, (content, embedding, importance, json.dumps(metadata), datetime.now())) elif layer == "semantic": # 语义层先查重,避免重复固化 existing = search_similar(content, layer="semantic", threshold=0.92) if existing: update_memory(existing['id'], content, importance) else: db.execute("INSERT INTO semantic_memory ...")

注意工作记忆用了ltrim只保留最近20条,这就是前面说的容量上限机制。语义层的查重也很重要,不然同一个事实会被反复写入,检索时全是重复结果。

检索逻辑:

def search_memory(query, layers, top_k=5): # query改写 rewritten = rewrite_query(query) # 返回 key, query, value results = [] for layer in layers: if layer == "working": items = redis_client.lrange(f"wm:{rewritten['key']}", 0, -1) results.extend([json.loads(i) for i in items]) else: emb = get_embedding(rewritten['query']) rows = db.execute(f""" SELECT *, 1 - (embedding <=> %s) AS sim FROM {layer}_memory WHERE metadata->>'user_id' = %s ORDER BY embedding <=> %s LIMIT %s """, (emb, rewritten['key'], emb, top_k)) results.extend(rows) # 重排 for r in results: r['final_score'] = (0.6 * r.get('sim', 0) + 0.25 * (r.get('importance', 3) / 5) + 0.15 * time_decay(r.get('created_at'))) return sorted(results, key=lambda x: x['final_score'], reverse=True)[:top_k]

4.4 把记忆服务接入MCP并验证

服务跑起来后,用MCP的server SDK把它包一层。我用的是官方Python SDK,核心就是注册前面设计的三个工具,然后启动stdio或SSE传输。启动后,在支持MCP的客户端里配置连接,就能看到memory_write、memory_search这些工具出现在可用列表里。

验证环节我建议分三步:第一步,手动调memory_write写一条,再调memory_search查出来,确认读写通;第二步,模拟多轮对话,看工作记忆是否正确滚动;第三步,隔一段时间再查,确认持久化生效。我当初就是漏了第三步,结果重启容器后发现记忆全没了——因为volume没挂对,数据写在容器里,容器一删就没了。

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

5.1 记忆检索召回不准的排查思路

召回不准是最常见的问题,我整理了一个排查顺序:

现象可能原因排查方法
检索结果完全不相关embedding模型不匹配确认写入和检索用的是同一个embedding模型
相关记忆排不到前面权重系数不合理调大vector_sim权重,或检查importance是否都默认成3了
该召回的没召回query改写失败打印改写后的query,看是否丢失了关键信息
召回一堆重复语义层没查重检查写入时是否做了相似度去重
时间近的排后面时间衰减函数有问题检查created_at时区是否一致

我踩过最坑的一个是时区问题:写入时用本地时间,检索时用UTC,结果时间衰减算出来是负数,新记忆反而被降权。后来统一全用UTC存储,展示时再转本地。

5.2 Docker网络不通与容器启动失败

Docker网络问题我遇到两类。一类是容器间不通,通常是没在同一个network里。docker-compose默认会创建一个网络,所有服务都在里面,但如果你手动docker run的容器想连compose的服务,就得手动docker network connect。另一类是容器访问外网不通,这个多半是DNS问题,可以在compose里指定dns:

services: memory-service: dns: - 8.8.8.8 - 114.114.114.114

容器启动失败,先看日志docker logs <container>,九成问题日志里都写清楚了。如果日志没输出就退出了,用docker run -it <image> sh进去手动跑命令,看具体报什么错。

5.3 记忆膨胀与性能下降的治理

系统跑久了,记忆库会膨胀,检索变慢。我的治理经验是三条:

第一,定期归档。超过30天的低重要性情景记忆,导出到冷存储,从主库删掉。第二,语义层合并。定期跑一个任务,把语义层里相似度超过0.9的记忆合并成一条,减少冗余。第三,索引优化。向量库的索引参数要调,比如HNSW的ef_construction和M参数,默认值在小数据量下够用,数据上百万就得重新调。

提示:治理任务一定要做成幂等的,我见过有人跑合并任务跑一半挂了,重跑时把已经合并的记忆又合并了一遍,数据全乱了。加个任务状态标记,跑之前先检查。

5.4 几个我踩过的独家坑

第一个坑:别把API key硬编码进镜像。我早期图省事,把LLM的key写进Dockerfile,结果镜像一推送到仓库就泄露了。正确做法是用环境变量或secret管理。

第二个坑:工作记忆的TTL别设太长。我一开始设了24小时,结果用户第二天回来,Agent还在提昨天的临时话题,显得很诡异。后来改成会话结束即清,或者最多保留2小时。

第三个坑:MCP连接要处理断线重连。MCP的SSE连接不是永远稳定的,网络抖动会断。我在客户端加了重连逻辑,断了自动重试,不然用户会突然发现Agent“失忆”了。

第四个坑:embedding要缓存。同一段文本反复算embedding很浪费,我在写入前先查缓存,命中就直接用,省了不少token和时间。

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

这套系统跑稳定之后,我做了几个扩展,效果不错。一个是记忆的可视化,把三层记忆用时间轴和关系图展示出来,调试时一眼就能看出Agent“记住了什么、忘了什么”,排查问题效率翻倍。另一个是记忆的主动回顾,让Agent在空闲时定期扫描情景记忆,提炼新的语义记忆,相当于“睡前整理今天学到的东西”,这个机制让长期记忆的质量明显提升。

还有个方向是多Agent共享记忆。多个Agent协作时,如果各自记各自的,就会出现信息孤岛。我试过用一个共享的语义记忆层,配合权限控制,让不同Agent既能共享公共知识,又能保留私有记忆。这块还在打磨,但初步效果已经能看出价值。

我个人在实际操作中的体会是,Agent记忆这件事,工程复杂度远高于算法复杂度。模型能力现在都够用,真正难的是怎么设计一套既不过度设计、又能长期稳定运行的存储和检索机制。我的建议是,别一上来就追求大而全,先把工作记忆和情景记忆跑通,让Agent能记住最近发生的事,这一步就能解决80%的体验问题。等这套稳了,再往上叠语义记忆和自动提炼。记忆系统是养出来的,不是一次设计出来的,边跑边调,比一开始就画大饼靠谱得多。

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

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

立即咨询