news 2026/10/1 3:56:52

Agent记忆系统实战:基于MCP与Docker构建可检索的LLM长期记忆层

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Agent记忆系统实战:基于MCP与Docker构建可检索的LLM长期记忆层

1. 从"hindsight"说起:为什么Agent的记忆问题值得单独拎出来做

"hindsight"这个词本身很有意思,字面意思是"事后的洞察力",也就是我们常说的"后见之明"。把这个词用在Agent Memory(智能体记忆)这个方向上,其实点出了一个很核心的痛点:大部分基于LLM的Agent,在任务执行完之后,是没有"回头看"的能力的。它做完一件事,对话结束,上下文清空,下一次遇到类似场景,又是从零开始。

我最早接触Agent记忆这块,是因为一个很实际的问题:我搭了一个基于LLM的自动化助手,用来处理一些重复性的信息整理工作。跑单次任务的时候效果很好,但只要涉及"上次我们讨论到哪了""之前那个方案为什么被否掉了"这类需要跨会话回忆的场景,它就完全抓瞎。后来我意识到,这不是模型能力的问题,而是记忆架构的缺失——LLM本身是无状态的,它的"记忆"完全依赖于你喂给它的上下文窗口,而上下文窗口是有限的、易失的。

hindsight这个项目标题,我理解它的定位是:给LLM-based Agent补上一套可回溯、可检索、可复用的记忆层。它要解决的不是"模型聪不聪明",而是"模型记不记得住、能不能从过去的交互里提取经验"。这个方向最近热度很高,从热搜词里能看到agent memory、MCP、Docker、a-memguard这些关键词频繁出现,说明整个社区都在往"让Agent有长期记忆"这个方向使劲。

这篇文章适合谁看?如果你正在做LLM应用开发,尤其是涉及多轮对话、任务型Agent、知识库问答这类场景,那Agent记忆是你绕不开的一环。如果你只是刚接触LLM,想搞清楚"记忆"到底是怎么一回事,这篇也会从最基础的概念讲起。我会尽量把架构思路、实操步骤、踩坑经验都摊开讲,让你看完能自己动手搭一套最小可用的记忆系统。

2. Agent Memory的核心设计思路拆解

2.1 为什么LLM需要"外挂记忆"而不是靠上下文硬撑

先说一个很多人容易混淆的点:LLM的上下文窗口(context window)和"记忆"是两回事。上下文窗口是模型单次推理能看到的token上限,比如128K、200K,它确实能塞很多内容进去,但它是临时的、线性的、无结构的。你把一堆历史对话塞进去,模型能读到,但它不会自动区分哪些是重要的、哪些是过期的、哪些是互相矛盾的。

真正的记忆系统需要具备几个特征:持久化(关掉进程还在)、可检索(能按相关性捞出来)、可更新(新信息能覆盖旧信息)、有结构(不是一坨文本堆在那)。这就是为什么大家开始做Agent Memory——把记忆从"上下文里的一段文本"变成"一个独立管理的存储层"。

hindsight这个方向,本质上就是在做这层存储和检索的抽象。它要回答的问题是:Agent在什么时候该写入记忆、写入什么格式、下次怎么找到它、找到之后怎么用。

2.2 记忆的三种类型:working memory、episodic memory、semantic memory

在动手之前,得先把记忆的分类搞清楚,不然架构会乱。参考认知科学和目前主流的Agent记忆实践,一般分三类:

Working Memory(工作记忆):当前任务正在用的信息,生命周期最短,任务结束就丢。比如你现在让Agent订机票,它需要记住"出发地北京、目的地上海、日期下周三",这些信息在任务完成后就没用了。工作记忆通常直接放在上下文里,或者放在一个临时的session存储里。

Episodic Memory(情景记忆):记录"什么时候发生了什么"。比如"上周三用户让我查了A项目的进度,我返回了三个风险点"。这类记忆带时间戳,用于回溯和审计。

Semantic Memory(语义记忆):从多次交互中提炼出的稳定知识。比如"用户偏好用表格形式看数据""这个项目的负责人是张三"。这类记忆是去时间化的,是Agent对世界的"认知"。

hindsight如果要做完整,这三层都得覆盖。但实际落地时,我建议先从working memory和semantic memory入手,因为episodic memory的写入频率高、检索需求相对低,容易变成存储垃圾场。

2.3 为什么选MCP作为记忆的接入协议

热搜词里MCP出现频率极高,这里得解释一下。MCP(Model Context Protocol)是一个让LLM应用和外部工具/数据源对接的协议标准。它的核心价值是解耦:记忆系统作为一个独立的MCP Server,任何支持MCP的客户端(比如各种IDE、Agent框架)都能接进来用,不用为每个框架单独写适配。

这就像USB接口——以前每个设备一个专用口,现在统一成USB-C,谁都能插。MCP让记忆层变成了一个"可插拔的组件",这是它比"直接在代码里写个数据库调用"更优雅的地方。

用MCP做记忆接入,大致流程是:记忆服务暴露几个工具(比如write_memory、search_memory、update_memory),Agent在需要的时候调用这些工具。好处是记忆逻辑和Agent逻辑完全分离,你可以单独升级记忆系统而不动Agent代码。

2.4 Docker在这套架构里扮演什么角色

热搜词里Docker、Docker Desktop、docker安装这些词反复出现,说明很多人卡在环境这一步。记忆系统通常需要跑几个组件:向量数据库(存embedding)、关系数据库(存结构化记忆)、可能还有一个缓存层。这些组件用Docker跑是最省事的,因为依赖隔离、版本可控、迁移方便。

我自己的习惯是:所有记忆相关的服务都用docker-compose编排,一个文件拉起向量库+关系库+记忆服务本身。这样换机器的时候,docker compose up就完事了,不用重新配环境。后面实操部分我会给一个具体的compose配置。

3. 核心细节解析与实操要点

3.1 记忆的写入策略:什么时候该记,记什么

这是最容易做错的地方。很多人一上来就把所有对话都往记忆库里塞,结果检索的时候全是噪音。我的经验是:写入要有触发条件,不能无脑写。

常见的写入触发条件有这么几种:

  • 显式指令:用户说"记住这个""以后都按这个来",直接写。
  • 任务完成节点:一个任务结束时,把关键结论、决策、产出写进去。
  • 信息密度阈值:当一轮对话里出现了新的实体、新的偏好、新的约束条件时写。
  • 定期摘要:每隔N轮对话,让LLM做一次摘要,把摘要写入semantic memory。

写入的内容格式也很关键。我推荐用结构化的三元组+自然语言描述的混合格式。热搜词里有个说法很形象:"key是我谁、query我在找什么、value我能提供什么"。这其实就是把记忆拆成"主体-关系-客体"的结构。比如:

{ "key": "user_preference_format", "query": "用户喜欢什么格式的数据展示", "value": "表格形式,带对比列", "timestamp": "2025-01-15T10:30:00Z", "source": "session_20250115_001", "confidence": 0.85 }

这种结构的好处是检索时可以用key做精确匹配,也可以用value做语义匹配,两条路都通。

3.2 记忆的检索:向量检索+关键词检索的混合方案

检索是记忆系统的核心。纯向量检索的问题是:它对"精确匹配"不敏感。比如你搜"张三的电话",向量检索可能返回一堆"联系人相关"的记忆,但不一定精确命中张三那条。纯关键词检索的问题是:它无法处理语义相似但用词不同的情况。

我的做法是混合检索:先用关键词/元数据过滤缩小范围,再用向量相似度排序。具体流程:

  1. 用户query进来,先做一次实体抽取,提取出关键实体(人名、项目名、时间等)。
  2. 用实体做元数据过滤,从记忆库里捞出候选集。
  3. 对候选集做向量相似度计算,排序取Top-K。
  4. 如果候选集为空,退化为纯向量检索。

这个方案实测下来召回率和准确率都比单一方案好很多。代价是需要维护两套索引,但用现成的向量库(比如Milvus、Qdrant)都支持payload过滤,实现起来不复杂。

3.3 记忆的更新与遗忘:别让记忆库变成垃圾场

记忆系统做久了,最大的问题不是"记不住",而是"记太多"。过期的、矛盾的、低价值的记忆如果不清理,检索质量会断崖式下降。

我的更新策略是:

  • 同key覆盖:如果新记忆的key和旧记忆相同,且新记忆的confidence更高,直接覆盖。
  • 矛盾检测:如果新记忆和旧记忆在语义上矛盾(比如"用户喜欢简洁"vs"用户喜欢详细"),标记为冲突,让LLM做一次裁决,或者保留时间更新的那条。
  • TTL机制:给每类记忆设一个生存时间。working memory可能几小时就过期,episodic memory保留30天,semantic memory长期保留但定期做摘要压缩。
  • 访问频率加权:被频繁检索到的记忆权重更高,长期不被访问的记忆降权甚至归档。

这里有个坑:不要用硬删除。记忆删了就找不回来了,万一后面发现删错了很麻烦。我一般用软删除,加一个deleted_at字段,检索时过滤掉,但数据还在,需要的时候能恢复。

3.4 用MCP封装记忆服务的具体做法

把记忆系统封装成MCP Server,需要定义几个工具。我一般会暴露这几个:

工具名功能输入参数输出
memory_write写入一条记忆content, key, tags, ttlmemory_id
memory_search检索记忆query, top_k, filters记忆列表
memory_update更新记忆memory_id, new_content状态
memory_forget软删除记忆memory_id状态
memory_summarize对一段记忆做摘要memory_ids摘要文本

MCP Server的实现可以用Python的mcp库,也可以用TypeScript的SDK。核心是把上面这些操作包装成MCP的tool调用。Agent侧只需要在system prompt里告诉模型"你有这些记忆工具可用",模型就会在合适的时候调用。

注意:MCP工具的description要写清楚,因为模型是靠description来决定什么时候调用的。description写得太模糊,模型要么不调用,要么乱调用。

4. 实操过程与核心环节实现

4.1 环境准备:用Docker Compose拉起记忆系统全家桶

先把环境搭起来。我假设你已经装了Docker Desktop(Windows/Mac)或者Docker Engine(Linux)。如果没装,去官网下对应平台的安装包,一路下一步就行。Windows上如果提示"Virtualization support not detected",去BIOS里把虚拟化打开。

下面是一个docker-compose.yml,包含向量库(Qdrant)、关系库(PostgreSQL)、缓存(Redis)和记忆服务本身:

version: '3.8' services: qdrant: image: qdrant/qdrant:latest ports: - "6333:6333" volumes: - ./data/qdrant:/qdrant/storage postgres: image: postgres:16 environment: POSTGRES_USER: memory POSTGRES_PASSWORD: memory_pass POSTGRES_DB: agent_memory ports: - "5432:5432" volumes: - ./data/postgres:/var/lib/postgresql/data redis: image: redis:7-alpine ports: - "6379:6379" memory-service: build: ./memory-service ports: - "8080:8080" environment: QDRANT_URL: http://qdrant:6333 POSTGRES_URL: postgresql://memory:memory_pass@postgres:5432/agent_memory REDIS_URL: redis://redis:6379 depends_on: - qdrant - postgres - redis

启动命令就一句:

docker compose up -d

等几十秒,四个服务都起来之后,docker compose ps能看到状态都是running。

提示:第一次拉镜像会比较慢,尤其是Qdrant和Postgres的镜像。如果网络环境不好,可以配置国内镜像源加速。另外数据卷挂载到本地目录,方便备份和迁移。

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

记忆服务我用Python写,核心是三个模块:写入、检索、更新。先看写入:

import uuid from datetime import datetime, timedelta from qdrant_client import QdrantClient from qdrant_client.models import PointStruct, VectorParams, Distance class MemoryStore: def __init__(self, qdrant_url, collection_name="agent_memory"): self.client = QdrantClient(url=qdrant_url) self.collection = collection_name self._ensure_collection() def _ensure_collection(self): collections = self.client.get_collections().collections if self.collection not in [c.name for c in collections]: self.client.create_collection( collection_name=self.collection, vectors_config=VectorParams(size=768, distance=Distance.COSINE) ) def write(self, content, key=None, tags=None, ttl_hours=None): memory_id = str(uuid.uuid4()) embedding = self._embed(content) payload = { "content": content, "key": key, "tags": tags or [], "created_at": datetime.utcnow().isoformat(), "expires_at": (datetime.utcnow() + timedelta(hours=ttl_hours)).isoformat() if ttl_hours else None, "access_count": 0, "deleted": False } self.client.upsert( collection_name=self.collection, points=[PointStruct(id=memory_id, vector=embedding, payload=payload)] ) return memory_id

_embed方法负责把文本转成向量,可以用OpenAI的embedding API,也可以用本地的sentence-transformers模型。本地模型的好处是不依赖外部服务,坏处是占内存。我一般用bge-base-zh这类中文效果好的模型。

检索部分:

def search(self, query, top_k=5, filters=None): query_vector = self._embed(query) results = self.client.search( collection_name=self.collection, query_vector=query_vector, limit=top_k, query_filter=self._build_filter(filters) ) # 过滤掉已删除和已过期的 valid = [] now = datetime.utcnow().isoformat() for r in results: p = r.payload if p.get("deleted"): continue if p.get("expires_at") and p["expires_at"] < now: continue valid.append({"id": r.id, "score": r.score, **p}) return valid

_build_filter负责把tags、key这些元数据条件转成Qdrant的filter对象。这样就能实现前面说的"先过滤再排序"的混合检索。

4.3 把记忆服务接成MCP Server

MCP Server的代码结构大概是这样的:

from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent app = Server("agent-memory") store = MemoryStore(qdrant_url="http://localhost:6333") @app.list_tools() async def list_tools(): return [ Tool( name="memory_write", description="写入一条长期记忆。当用户表达了偏好、约束、重要事实时调用。", inputSchema={ "type": "object", "properties": { "content": {"type": "string", "description": "记忆内容"}, "key": {"type": "string", "description": "记忆的唯一标识,用于覆盖更新"}, "tags": {"type": "array", "items": {"type": "string"}}, "ttl_hours": {"type": "integer", "description": "过期时间,不填则永久"} }, "required": ["content"] } ), Tool( name="memory_search", description="检索相关记忆。在回答用户问题前,先检索是否有相关历史记忆。", inputSchema={ "type": "object", "properties": { "query": {"type": "string"}, "top_k": {"type": "integer", "default": 5} }, "required": ["query"] } ) ] @app.call_tool() async def call_tool(name, arguments): if name == "memory_write": mid = store.write(**arguments) return [TextContent(type="text", text=f"已写入记忆 {mid}")] elif name == "memory_search": results = store.search(**arguments) return [TextContent(type="text", text=format_results(results))]

启动方式:

python -m memory_service.server

然后在支持MCP的客户端里配置这个Server的启动命令,就能用了。实测下来,模型在收到"记住我喜欢用表格"这类指令时,会主动调用memory_write;在回答新问题前,会先调memory_search看看有没有相关记忆。

4.4 参数选择与性能调优

几个关键参数的选择经验:

向量维度:取决于你用的embedding模型。bge-base-zh是768维,text-embedding-3-small是1536维。维度越高精度越好但存储和计算成本越高。一般768维够用。

Top-K:检索返回几条。太小可能漏掉相关记忆,太大引入噪音。我一般设5-10,然后让LLM做二次筛选。

相似度阈值:低于某个分数的直接丢弃。我一般设0.6-0.7,具体看模型。太低会召回不相关的,太高会漏掉。

TTL:working memory设1-24小时,episodic设7-30天,semantic不设或设很长。

批量写入:如果一次要写多条记忆,用Qdrant的批量upsert,比逐条写快很多。

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

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

这是最高频的问题。检索不准通常有三个原因:

embedding模型不适合你的语言/领域。如果你做的是中文场景,用英文为主的embedding模型效果会很差。换成bge系列或者m3e这类中文模型。

记忆内容太短或太碎。一条记忆只有"好的"两个字,embedding出来没有区分度。写入时要做内容规范化,把上下文补全。

没有做元数据过滤。纯向量检索在记忆量大时噪音很多。加上tags、时间范围这些过滤条件。

排查方法:把检索结果打出来,人工看Top-10里有多少是真正相关的。如果低于50%,说明检索策略有问题。

5.2 Docker环境常见报错

报错信息原因解决方法
Virtualization support not detectedBIOS虚拟化未开启进BIOS开启VT-x/AMD-V
port is already allocated端口被占用改compose里的端口映射,或杀掉占用进程
no space left on device磁盘满清理docker system prune,或迁移数据目录
network not foundcompose网络问题docker compose down后重新up
permission denied挂载目录权限Linux下chown对应目录,或加user配置

5.3 记忆冲突与幻觉的处理

Agent有时候会"记错",把不存在的事写进记忆。这种情况要靠写入前的校验:让LLM在写入前先确认"这条信息是用户明确说的,还是我推断的"。推断的内容标记低confidence,检索时降权。

记忆冲突的话,我一般保留时间更新的那条,但把旧的标记为superseded,不删除。这样万一新的是错的,还能回溯。

5.4 性能瓶颈与扩展

单机跑几千条记忆没问题,上万条之后检索延迟会上升。扩展方向:

  • Qdrant开分片和副本
  • 加Redis缓存热点记忆
  • 把embedding计算异步化,写入时先落库再算向量
  • 定期做记忆压缩,把多条相关记忆合并成一条摘要

提示:不要过早优化。大部分场景几千条记忆足够了,先把功能跑通,性能问题等真遇到了再解决。

6. 关于hindsight方向的一些个人判断

我做Agent记忆这块有一段时间了,最大的体会是:记忆系统的价值不在于"记得多",而在于"记得准"。一个只记100条但每条都精准的系统,比记10000条但一半是噪音的系统有用得多。

hindsight这个方向,我觉得接下来会往几个方向走:一是记忆的自动摘要和压缩会越来越重要,因为原始记忆的增长速度远超检索能力的提升;二是记忆的权限和隔离会成为一个刚需,多用户场景下不能互相看到对方的记忆;三是记忆的可解释性,用户得能知道Agent"为什么记得这个"。

如果你现在要动手做,我的建议是:先用最简单的方案跑起来——一个向量库加一个MCP Server,能写能查就行。别一上来就搞三层记忆、冲突检测、自动摘要,那些都是后面根据实际需求加的。我见过太多人卡在架构设计上,最后一行代码没写。

最后分享一个小技巧:在system prompt里明确告诉模型"你有记忆工具,在回答前先检索",比让它自己判断要不要检索要可靠得多。模型的自驱性没你想的那么强,该给的指令要给足。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 3:55:27

Postman接口测试实战:从入门陷阱到自动化协作

1. 为什么接口测试不能只靠“点一下就完事”——Postman不是万能遥控器&#xff0c;而是你的API显微镜很多人第一次听说Postman&#xff0c;是在开发同事甩来一句“你用Postman调一下这个接口看看返回啥”。于是下载、安装、填个URL、点Send——看到{"code":200,&quo…

作者头像 李华
网站建设 2026/10/1 3:54:53

微信开源知识库WeKnora:从本地部署到RAG问答实战全攻略

如果你最近在刷 RAG、个人知识库这类技术话题&#xff0c;大概率会刷到“微信开源知识库项目”这个热词。我第一反应是去仓库里翻了翻代码&#xff0c;然后把 demo 跑了起来。这个项目叫 WeKnora&#xff0c;定位很干脆&#xff1a;把本地文档、网页链接、甚至零散的笔记&#…

作者头像 李华
网站建设 2026/10/1 3:54:52

灵活上下文并行(FCP):打破固定环瓶颈的长上下文推理新方案

1. 长上下文推理的核心矛盾长上下文今年已经不是"要不要做"的问题&#xff0c;而是"做不到就上不了牌桌"的问题。开会讨论一个百万token级别的检索增强方案&#xff0c;动辄几十轮对话的Agent任务&#xff0c;或者一段几十秒的视频要做时序理解&#xff0c…

作者头像 李华
网站建设 2026/10/1 3:54:49

YOLOv8整合包实战:11个bat脚本从数据集到摄像头推理全流程

简介&#xff1a;这份资源是面向目标检测初学者与工程实践者的YOLOv8完整整合包&#xff0c;基于开源仓库objectdetection_script整理&#xff0c;配套B站教学视频&#xff0c;帮助读者跳过繁琐的环境配置&#xff0c;直接进入训练、评估与推理全流程。压缩包共289个文件&#…

作者头像 李华
网站建设 2026/10/1 3:54:09

OpenCV与ONNX Runtime实现英文数字检测识别的完整推理指南

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

作者头像 李华
网站建设 2026/10/1 3:53:43

Flutter在OpenHarmony上的布局核心与交互式文档实践

1. 先想清楚&#xff1a;为什么要在OpenHarmony上做Flutter文档应用最近团队接到一个很有意思的需求&#xff1a;把一套在安卓和iOS上跑得很稳的交互式文档应用&#xff0c;迁移到OpenHarmony生态里&#xff0c;还得保证交互体验和渲染效果几乎不变。接到这个任务&#xff0c;第…

作者头像 李华