新闻详情

新闻详情

首页 / 资讯中心 / 详情

Agent 记忆系统实战:基于 MCP 与 Docker 的长期记忆落地

发布时间:2026/9/30 3:41:55来源:尧图网络
Agent 记忆系统实战:基于 MCP 与 Docker 的长期记忆落地
1. 从“hindsight”说起为什么记忆是 Agent 落地的最后一公里“hindsight”这个词本身很有意思字面意思是“事后的洞察力”也就是我们常说的“后见之明”。把这个词放到 LLM Agent 的语境里它指向的东西非常具体Agent 在完成一轮任务之后能不能把这一轮里发生的事、踩过的坑、验证过的结论沉淀下来在下一轮任务里真正用上。这就是 agent memory 这个方向最核心的命题。我接触过不少做 Agent 的团队模型能力其实都不差工具调用也跑得通但一旦把任务周期拉长到几十轮、上百轮问题就集中爆发了Agent 会重复犯同一个错误会忘记三天前用户明确说过的偏好会在同一个 API 上反复试错。表面上看是“模型不够聪明”实际上绝大多数情况是记忆层没设计好。模型本身是无状态的你不在上下文里给它它就真的不知道。而上下文窗口再大也有上限靠无脑堆历史消息token 成本会失控检索精度也会被稀释。所以“hindsight”这个项目标题我理解它要解决的就是一个很朴素但极难做好的问题如何让 Agent 拥有可检索、可更新、可遗忘的长期记忆并且这套记忆机制要能跑在真实的生产环境里。结合热搜词里出现的 agent memory、MCP、Docker、LLM wiki 知识库、a-memguard 这些关键词可以判断这个项目大概率是一个围绕 Agent 记忆系统的工程化实践涉及记忆的存储结构、检索策略、安全防护以及通过 MCP 协议把记忆能力暴露给不同的 Agent 客户端。这篇文章适合谁看如果你正在做 Agent 应用被“上下文爆炸”和“记忆混乱”折磨过或者你听说过 MCP 但还没真正把它和记忆系统结合起来用过那这篇内容应该能给你一些可以直接抄作业的思路。我会从整体设计、核心机制、实操部署、问题排查几个层面把“hindsight”这类 Agent 记忆系统拆开讲透尽量做到你看完就能动手搭一个能跑的版本。2. 记忆系统的整体设计与思路拆解2.1 为什么不能只靠上下文窗口做记忆很多人第一反应是现在模型上下文都到 128K 甚至 1M 了直接把所有历史塞进去不就行了我实测过这条路在 demo 阶段能跑一上生产就崩。原因有三个而且都是硬伤。第一是成本。假设每轮对话平均 2000 token跑 100 轮就是 20 万 token每次请求都要把这 20 万 token 重新发一遍按现在的 API 定价一天跑几百次任务账单会非常难看。第二是注意力稀释。上下文里塞的东西越多模型对关键信息的召回率反而会下降这是有大量实验支撑的现象业内一般叫“lost in the middle”——关键信息夹在中间位置时最容易被忽略。第三是无法跨会话。上下文窗口是会话级的用户今天聊完关掉明天再来历史就没了除非你手动持久化。所以正确的做法是分层工作记忆working memory放在上下文里长期记忆long-term memory落到外部存储通过检索按需注入。这也是热搜词里“agent 存储 working memory”指向的核心思路。2.2 记忆的三层结构working、episodic、semantic我在设计记忆系统时习惯把它拆成三层这个划分方式借鉴了认知科学但落地时非常实用。工作记忆就是当前任务链的上下文包括最近的几轮对话、当前正在调用的工具、临时变量。它存在内存里任务结束就丢弃生命周期最短。**情景记忆episodic memory**记录的是“什么时候发生了什么”比如“用户在 3 月 5 日要求把所有报告导出成 PDF”它是带时间戳的事件流。**语义记忆semantic memory**则是从情景记忆里提炼出来的稳定知识比如“这个用户偏好 PDF 而不是 Word”它不带具体时间是抽象后的结论。为什么要分这么细因为检索策略完全不同。工作记忆直接拼进 prompt情景记忆按时间范围或相似度检索语义记忆需要定期做“提炼”和“去重”否则会越积越多、互相矛盾。热搜词里提到的“LLM 的 token 三个点key 我是谁、query 我在找什么、value 我能提供什么”其实就是在讲记忆条目的结构化表示——每条记忆都要有明确的 key标识、query检索入口、value实际内容这样检索时才能精准命中。2.3 为什么选 MCP 作为记忆的对外接口MCPModel Context Protocol这两年被讨论得很多热搜词里“mcp 是什么”“mcp 协议”“agent mcp”反复出现。简单说它是一个让模型和外部工具/数据源通信的标准化协议。把记忆系统做成一个 MCP Server好处非常直接任何支持 MCP 的客户端都能接入这套记忆不用为每个 Agent 框架单独写适配层。我试过两种方案。一种是直接在 Agent 代码里 import 记忆模块耦合度高换个框架就得重写。另一种是把记忆封装成 MCP ServerAgent 通过标准协议调用memory_store、memory_search、memory_forget这些工具。实测下来第二种明显更稳尤其是当你同时用多个客户端比如桌面端、IDE 插件、命令行工具时一套记忆服务可以共享用户体验是连贯的。2.4 Docker 化部署为什么这是必选项热搜词里 Docker 相关的内容占了很大比重docker 安装、docker desktop、docker 网络不通、docker 安装 mysql8.0、docker 安装 redis 主从……这说明大家在实际部署记忆系统时绕不开容器化。我的判断是记忆系统涉及向量库、关系库、缓存、MCP 服务多个组件手工装环境迟早会乱Docker Compose 一把梭是最省心的。具体来说向量检索用 Qdrant 或 Milvus元数据和情景记忆用 PostgreSQL热数据缓存用 RedisMCP Server 本身跑一个 Python 或 Node 容器。这几个组件通过 Docker 网络互联数据卷持久化。这样一套下来换台机器只要docker compose up -d就能复现不用再折腾“为什么我本地能跑服务器上不行”。3. 核心细节解析与实操要点3.1 记忆条目的数据结构设计这是整个系统里最容易被忽视、但影响最大的部分。我见过太多人把记忆直接存成一段纯文本检索时靠向量相似度硬匹配结果召回质量惨不忍睹。正确的做法是结构化 向量化双轨存储。一条记忆条目至少包含这些字段id唯一标识、content原始文本、embedding向量、typeworking/episodic/semantic、timestamp创建时间、last_access最后访问时间、access_count访问次数、importance重要性评分、source来源比如哪个会话、哪个工具、tags标签。其中importance和access_count是记忆淘汰策略的关键依据。我一般用 PostgreSQL 存结构化字段用 pgvector 扩展存向量这样一条 SQL 就能同时做元数据过滤和向量检索不用维护两套系统。如果数据量特别大千万级以上再考虑独立的向量库。3.2 检索策略混合检索才是正解纯向量检索的问题在于它对精确匹配不敏感。用户问“我上次说的那个 Redis 配置”向量检索可能召回一堆关于数据库的记忆但就是找不到那条具体的 Redis 配置。所以我的做法是混合检索向量相似度 关键词 BM25 元数据过滤三路结果用 RRFReciprocal Rank Fusion融合排序。具体参数上向量检索取 top 20BM25 取 top 20元数据过滤比如限定 typesemantic 或时间范围后再合并最终返回 top 5 注入上下文。这个 5 不是拍脑袋定的我实测过注入超过 8 条记忆后模型对当前任务的注意力会明显下降5 条左右是精度和召回的平衡点。提示检索时一定要加时间衰减因子。三个月前的记忆和昨天的记忆即使相似度一样权重也应该不同。我一般用指数衰减半衰期设 30 天。3.3 记忆的写入与提炼流程写入不是简单地把对话存下来。我的流程是先判断是否值得记再决定记成什么类型最后做去重和冲突检测。判断是否值得记可以用一个轻量模型做分类或者用规则包含用户偏好、明确指令、验证过的结论、错误教训的才写入长期记忆。闲聊和中间过程不写。这一步能大幅减少噪音。去重和冲突检测很关键。比如用户先说“我喜欢用 MySQL”后来说“我们团队现在统一用 PostgreSQL”这两条是冲突的。我的做法是写入前先检索相似记忆如果相似度超过阈值比如 0.9就走更新逻辑而不是新增把旧记忆标记为superseded新记忆继承它的id链。这样检索时只返回最新有效的版本。3.4 安全防护a-memguard 思路的落地热搜词里出现了“a-memguard: a proactive defense framework for llm-based agent memory”这个方向非常重要。记忆系统一旦被污染危害比单次对话被误导大得多因为污染会持续影响后续所有任务。我的防护分三层。写入层做来源校验只有可信来源比如用户明确指令、经过验证的工具返回才能写入 semantic 记忆工具返回的未经确认的内容只能进 episodic。存储层做完整性校验每条记忆存一个哈希定期扫描异常。检索层做注入检测如果检索到的记忆内容和当前任务明显不相关或者包含可疑指令比如“忽略之前的指令”这类直接过滤掉不注入上下文。注意记忆里绝对不要存敏感凭证、密钥、个人隐私信息。如果业务需要存引用而不是明文实际值放在专门的密钥管理服务里。4. 实操过程与核心环节实现4.1 环境准备Docker 与依赖组件先把基础环境搭起来。Windows 用户如果遇到“virtualization support not detected”导致 Docker Desktop 起不来去 BIOS 里把虚拟化Intel VT-x 或 AMD-V打开然后在“启用或关闭 Windows 功能”里勾选 WSL2 和虚拟机平台重启即可。Linux 用户直接装 docker engine 和 docker compose plugin。目录结构我一般这样组织hindsight/ ├── docker-compose.yml ├── .env ├── mcp-server/ │ ├── Dockerfile │ ├── requirements.txt │ └── src/ │ ├── main.py │ ├── memory.py │ └── retrieval.py └── data/ ├── postgres/ └── redis/docker-compose.yml的核心内容version: 3.9 services: postgres: image: pgvector/pgvector:pg16 environment: POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_DB: hindsight volumes: - ./data/postgres:/var/lib/postgresql/data ports: - 5432:5432 networks: - hindsight-net redis: image: redis:7-alpine command: redis-server --appendonly yes volumes: - ./data/redis:/data ports: - 6379:6379 networks: - hindsight-net mcp-server: build: ./mcp-server environment: DB_URL: postgresql://postgres:${DB_PASSWORD}postgres:5432/hindsight REDIS_URL: redis://redis:6379/0 depends_on: - postgres - redis ports: - 8080:8080 networks: - hindsight-net networks: hindsight-net: driver: bridge这里用pgvector/pgvector:pg16镜像它自带向量扩展省得自己编译。Redis 开了 AOF 持久化防止重启丢缓存。4.2 数据库初始化与向量索引Postgres 起来后先建表和索引CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE memories ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), content TEXT NOT NULL, embedding vector(1536), type VARCHAR(20) NOT NULL, timestamp TIMESTAMPTZ DEFAULT NOW(), last_access TIMESTAMPTZ DEFAULT NOW(), access_count INT DEFAULT 0, importance FLOAT DEFAULT 0.5, source VARCHAR(100), tags TEXT[], superseded_by UUID ); CREATE INDEX idx_memories_embedding ON memories USING ivfflat (embedding vector_cosine_ops) WITH (lists 100); CREATE INDEX idx_memories_type ON memories(type); CREATE INDEX idx_memories_timestamp ON memories(timestamp DESC);ivfflat索引的lists参数经验值是sqrt(总行数)。数据量小的时候几万条以内不建索引直接暴力检索反而更准等数据上来了再建。4.3 MCP Server 的核心实现MCP Server 暴露三个工具memory_store、memory_search、memory_forget。核心逻辑用 Python 写关键部分如下from mcp.server import Server from mcp.types import Tool, TextContent import asyncpg, redis, json from openai import AsyncOpenAI app Server(hindsight-memory) db_pool None redis_client None embed_client AsyncOpenAI() async def get_embedding(text: str) - list[float]: resp await embed_client.embeddings.create( modeltext-embedding-3-small, inputtext ) return resp.data[0].embedding app.tool() async def memory_store(content: str, mem_type: str, importance: float 0.5, tags: list[str] None, source: str default): embedding await get_embedding(content) # 冲突检测检索高相似记忆 similar await db_pool.fetch( SELECT id, content FROM memories WHERE superseded_by IS NULL AND embedding $1 0.1 ORDER BY embedding $1 LIMIT 3, embedding ) if similar: # 标记旧记忆被取代 await db_pool.execute( UPDATE memories SET superseded_by gen_random_uuid() WHERE id $1, similar[0][id] ) await db_pool.execute( INSERT INTO memories (content, embedding, type, importance, tags, source) VALUES ($1, $2, $3, $4, $5, $6), content, embedding, mem_type, importance, tags, source ) return TextContent(typetext, textstored)memory_search用混合检索向量 关键词 时间衰减app.tool() async def memory_search(query: str, top_k: int 5, mem_type: str None): q_emb await get_embedding(query) type_filter AND type $3 if mem_type else params [q_emb, top_k] ([mem_type] if mem_type else []) rows await db_pool.fetch(f SELECT id, content, type, importance, 1 - (embedding $1) AS similarity, EXTRACT(EPOCH FROM (NOW() - timestamp)) / 86400 AS days_ago FROM memories WHERE superseded_by IS NULL {type_filter} ORDER BY embedding $1 LIMIT $2 , *params) # 时间衰减重排 scored [] for r in rows: decay 0.5 ** (r[days_ago] / 30) score r[similarity] * 0.7 decay * 0.2 r[importance] * 0.1 scored.append((score, r)) scored.sort(reverseTrue, keylambda x: x[0]) results [dict(r) for _, r in scored[:top_k]] # 更新访问计数 ids [r[id] for r in results] await db_pool.execute( UPDATE memories SET access_count access_count 1, last_access NOW() WHERE id ANY($1), ids ) return TextContent(typetext, textjson.dumps(results, defaultstr))这个打分公式similarity * 0.7 decay * 0.2 importance * 0.1是我调了很多次的结果。相似度是主权重时间衰减和重要性做微调。你可以根据自己的业务调整比如客服场景可以加大 importance 权重。4.4 接入 Agent 客户端MCP Server 跑起来后在支持 MCP 的客户端里配置连接。以常见的配置文件为例{ mcpServers: { hindsight: { url: http://localhost:8080/sse, transport: sse } } }配置好后Agent 在对话时就能自动调用记忆工具。我的经验是在 system prompt 里明确告诉模型什么时候该存、什么时候该查效果比让它自己判断好得多。比如加一句“在用户表达偏好或给出明确指令时调用 memory_store在开始新任务前先调用 memory_search 查询相关历史。”5. 常见问题与排查技巧实录5.1 Docker 网络不通怎么办这是最高频的问题。容器之间互相访问要用服务名而不是 localhost。比如 mcp-server 连 PostgresDB_URL 里写的是postgres:5432不是localhost:5432。如果还是不通按这个顺序排查先docker compose ps看容器是否都起来了再docker network inspect hindsight_hindsight-net看容器是否在同一网络最后进容器docker exec -it xxx sh用ping postgres测试连通性。5.2 向量检索召回质量差先检查 embedding 模型是否一致。写入和检索必须用同一个模型换了模型要重新生成所有向量。其次检查lists参数太小会导致聚类粗糙太大会导致检索慢。最后看数据本身如果记忆条目太短比如只有几个词向量质量会很差建议写入前把内容补全成完整句子。5.3 记忆越积越多导致检索变慢这是必然的需要淘汰策略。我的做法是每天跑一个定时任务把access_count 0且超过 90 天的 episodic 记忆归档到冷存储把语义记忆做聚类合并相似度超过 0.95 的合并成一条。importance 低于 0.2 且长期未访问的直接删除。5.4 常见问题速查表问题现象可能原因排查方向MCP 连接失败端口未映射或 transport 不匹配检查 compose 端口映射和客户端 transport 配置检索结果不相关embedding 模型不一致或数据噪音大统一模型清理低质量记忆写入报错向量维度不匹配检查表定义和模型输出维度是否一致记忆冲突缺少去重逻辑写入前做相似度检测和 supersede容器启动失败虚拟化未开启或端口占用检查 BIOS 虚拟化和端口占用情况检索慢索引缺失或数据量过大建 ivfflat 索引加淘汰策略5.5 几个踩过的坑第一个坑是embedding 维度写死。我一开始表定义写的是vector(1536)后来换了个 1024 维的模型整个表得重建。建议要么固定模型要么用vector不指定维度但这样没法建索引。第二个坑是时间戳时区。Postgres 的TIMESTAMPTZ存的是 UTC但应用层如果按本地时间算衰减会差好几个小时。统一用 UTC 处理展示时再转本地。第三个坑是MCP 工具返回格式。不同客户端对 MCP 返回的解析方式略有差异我遇到过返回 JSON 字符串被当成纯文本展示的情况。稳妥做法是返回结构化的TextContent内容用 JSON 字符串客户端一般都能正确解析。第四个坑是并发写入冲突。多个 Agent 同时写记忆时去重逻辑可能同时查到同一条旧记忆并都标记 supersede导致数据不一致。加个数据库层面的唯一约束或者用事务包起来能解决。这套记忆系统我陆陆续续迭代了大半年从最开始的一坨代码到现在能稳定跑在生产环境最大的体会是记忆系统的难点不在存储而在检索和淘汰。存进去容易什么时候取出来、取哪条、什么时候该忘才是真正考验设计的地方。如果你刚开始做建议先把写入和检索跑通淘汰策略可以后面再加但数据结构一定要提前设计好不然后期迁移会很痛苦。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Linux内核配置系统全解析:从Kconfig到.config与裁剪实战 2026/9/30 4:44:17

Linux内核配置系统全解析:从Kconfig到.config与裁剪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
新手向Linux安装指南:发行版、启动盘、分区与常见坑 2026/9/30 4:44:16

新手向Linux安装指南:发行版、启动盘、分区与常见坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Quill排障清单:录音没声音、权限不生效的6个高频问题逐一解决 2026/9/30 4:44:10

Quill排障清单:录音没声音、权限不生效的6个高频问题逐一解决

Quill排障清单:录音没声音、权限不生效的6个高频问题逐一解决 【免费下载链接】quill Ultra-minimalist macOS recording transcription. 项目地址: https://gitcode.com/gh_mirrors/quill26/quill Quill 是一款极简的 macOS 本地会议录音 转写工具&#x…

阅读更多 →
最近很火的《高性价比人生指南》,我把它做成了微信小程序 2026/9/30 4:44:10

最近很火的《高性价比人生指南》,我把它做成了微信小程序

最近受到关注的开源项目《高性价比人生指南》HowToLiveBetter,我也围绕它做了一个微信小程序,让大家在手机上查阅更方便。 这份指南,具体在讲什么? 它关注的事情很日常:健康、时间、金钱,以及生活里那些平…

阅读更多 →
Altium Designer新建工程全流程:从建项目到元件库与PCB设计技巧 2026/9/30 4:44:10

Altium Designer新建工程全流程:从建项目到元件库与PCB设计技巧

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Model-Optimizer 实战:量化、剪枝与图优化加速模型推理 2026/9/30 4:44:04

Model-Optimizer 实战:量化、剪枝与图优化加速模型推理

1. 从"模型能跑"到"模型跑得省":Model-Optimizer 到底在解决什么做模型部署的人迟早会撞上一堵墙:训练时一切正常,指标也漂亮,可一旦要上线,推理延迟、显存占用、单位算力成本这三座大山就压过来了…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉