恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Agent Memory实战:从架构设计到Docker部署的LLM智能体记忆系统
首页
资讯中心
/
Agent Memory实战:从架构设计到Docker部署的LLM智能体记忆系统
Agent Memory实战:从架构设计到Docker部署的LLM智能体记忆系统
发布时间:2026/9/30 15:36:37
1. 从“hindsight”说起为什么我们需要给Agent装上“后视镜”“hindsight”这个词本身很有意思字面意思是“事后的洞察力”也就是我们常说的“后见之明”。放在LLM Agent的语境里它指向一个非常具体且棘手的问题Agent如何记住过去发生过的事情并在后续决策中真正用上这些记忆我接触过不少做Agent项目的团队大家一开始都信心满满觉得只要把LLM接上工具、配上提示词Agent就能像人一样聪明地干活。但实际跑起来就会发现Agent的“失忆”问题比想象中严重得多。同一个用户上一轮刚说过“我对花生过敏”下一轮Agent推荐餐厅时照样给你推花生酱拌面同一个任务昨天已经执行过一遍今天重新触发时Agent完全不知道之前做过什么从头再来一遍浪费token不说还可能产生冲突操作。这就是“hindsight”要解决的核心问题Agent Memory智能体记忆。它不是一个简单的“把对话历史塞进上下文”的活儿而是一套完整的记忆采集、存储、检索、注入和更新的工程体系。结合热搜词里出现的agent memory、LLM、MCP、Docker这几个关键词我理解这个项目大概率是在做一套面向LLM Agent的记忆管理方案可能涉及MCP协议作为工具调用层Docker作为部署载体最终让Agent具备跨会话、跨任务的长期记忆能力。这篇文章我会从实际落地的角度把Agent Memory这件事拆开揉碎讲清楚。不管你是刚接触Agent开发的新手还是已经在做相关项目但被记忆问题困扰的从业者都能从中找到可以直接参考的思路和操作方案。我会重点讲清楚几个问题记忆到底该怎么分类、存储层怎么选、检索策略怎么设计、MCP在其中扮演什么角色、Docker部署时有哪些坑以及我自己踩过的那些血泪教训。2. Agent Memory的整体架构设计分层才是出路2.1 为什么“一个向量库走天下”行不通很多人做Agent记忆的第一反应是搞个向量数据库把对话历史全部embedding存进去需要的时候做相似度检索就完了。我一开始也是这么想的直到实际跑了一段时间后发现这种做法在简单场景下能用一旦任务复杂度上来就崩了。问题出在几个地方。第一记忆是有类型的。用户说“我住在杭州”这是事实型记忆用户说“帮我订明天去北京的机票”这是任务型记忆Agent自己推理出“用户可能偏好靠窗座位”这是推断型记忆。这三种记忆的存储方式、检索时机、更新策略完全不同全部混在一个向量库里检索出来的结果噪音极大。第二向量相似度不等于记忆相关性。用户问“帮我推荐个餐厅”向量检索可能召回三个月前用户说过“我讨厌日料”这条记忆但实际上用户这次可能就是想找日料以外的选择这条记忆的权重应该很低。单纯靠语义相似度没法处理这种时效性和场景相关性的问题。第三记忆需要遗忘和压缩。Agent跑久了记忆库会无限膨胀检索效率下降成本上升。哪些记忆该保留、哪些该压缩成摘要、哪些该直接丢弃这是一套策略问题不是向量库能自动解决的。所以我的结论是Agent Memory必须分层设计。下面这张表是我在实际项目中总结出来的记忆分层方案供参考。记忆层级存储内容典型生命周期推荐存储方案检索触发时机工作记忆当前会话上下文、临时变量单次会话内存/Redis每轮对话直接注入情景记忆具体事件、任务执行记录数天到数周关系型数据库向量索引任务开始时检索语义记忆用户偏好、事实知识长期向量数据库图数据库需要个性化时检索程序记忆工具调用模式、操作流程长期结构化存储工具选择时检索2.2 记忆的写入、检索与更新闭环分层只是第一步更关键的是让记忆流动起来。一个完整的Agent Memory系统需要三个核心环节写入、检索、更新。这三个环节构成一个闭环任何一环出问题整个记忆系统就废了。写入环节的核心挑战是“什么值得记”。我的做法是在Agent的每轮对话结束后用一个轻量的LLM做一次记忆抽取判断这轮对话中是否产生了值得长期保留的信息。抽取的prompt大概长这样MEMORY_EXTRACTION_PROMPT 分析以下对话内容判断是否包含值得长期记忆的信息。 值得记忆的信息包括 1. 用户明确陈述的个人事实姓名、住址、偏好、禁忌等 2. 用户明确表达的任务意图和约束条件 3. Agent执行任务过程中产生的关键结果和状态变更 4. 用户对Agent行为的明确反馈满意/不满意/纠正 如果包含以上信息请以JSON格式输出 {should_remember: true, memory_type: fact|task|feedback, content: 记忆内容, confidence: 0.0-1.0} 如果不包含输出{should_remember: false} 对话内容 {conversation} 这个抽取步骤很关键它决定了记忆库的质量。我试过不做抽取直接全量存储结果记忆库里充斥着“好的”“谢谢”“没问题”这种无意义内容检索时噪音极大。加上抽取之后记忆库的信噪比至少提升了三倍。检索环节的核心挑战是“什么时候取、取多少”。我的策略是分场景触发任务开始时检索情景记忆和程序记忆需要个性化回复时检索语义记忆每轮对话都注入工作记忆。检索数量上我一般控制在3-5条太多会挤占上下文窗口太少可能漏掉关键信息。更新环节的核心挑战是“记忆冲突怎么处理”。比如用户上周说“我住在杭州”这周说“我搬到上海了”两条记忆冲突了。我的做法是给每条记忆加时间戳和置信度检索时优先返回时间近、置信度高的记忆同时在写入新记忆时检测冲突如果发现冲突则标记旧记忆为“已过期”而不是直接删除保留追溯能力。注意记忆更新千万不要直接覆盖旧数据。我踩过这个坑用户改了偏好之后旧偏好被删了结果后来用户说“还是按以前那个来吧”Agent完全不知道“以前那个”是什么。保留历史版本用状态标记来管理有效性这是更稳妥的做法。3. 核心细节解析存储选型、MCP集成与Docker部署3.1 存储层选型别被“向量数据库”绑架存储层是Agent Memory的地基选错了后面全是坑。我的建议是不要一上来就上重型向量数据库根据实际数据量和检索需求来选。对于工作记忆直接用Redis或者进程内存就够了读写快、过期策略成熟。对于情景记忆和语义记忆如果数据量在百万级以下PostgreSQL配合pgvector扩展完全够用运维成本低还能用SQL做结构化过滤。数据量再大或者需要复杂图关系查询再考虑Milvus、Qdrant这类专业向量库或者Neo4j这类图数据库。我实际项目中的组合是Redis存工作记忆PostgreSQLpgvector存情景记忆和语义记忆Neo4j存实体关系图谱。这个组合的好处是每一层都可以独立扩展不会因为某一层的问题拖垮整个系统。关于embedding模型的选择我试过OpenAI的text-embedding-3-small和开源的BGE-M3。实测下来如果预算充足且对检索精度要求高text-embedding-3-small确实更好如果考虑成本和数据隐私BGE-M3在中文场景下表现也很稳而且可以本地部署。维度上我一般用1024维再高收益递减明显存储成本却线性增长。3.2 MCP协议在记忆系统中的角色MCPModel Context Protocol在这套架构里扮演的是“记忆工具化”的角色。简单说就是把记忆的读写操作封装成MCP工具让Agent通过标准的工具调用协议来访问记忆而不是把记忆逻辑硬编码在Agent的prompt里。这样做的好处很明显。第一解耦。记忆系统的升级不影响Agent本身的逻辑Agent只需要知道“我有一个remember工具和一个recall工具”就行了。第二可复用。同一个记忆服务可以被多个Agent通过MCP接入不用每个Agent都重新实现一遍。第三可观测。所有记忆操作都走MCP协议日志和监控统一排查问题方便很多。我实现的一个简化版记忆MCP Server大概长这样from mcp.server import Server from mcp.types import Tool, TextContent server Server(memory-server) server.tool() async def remember(content: str, memory_type: str, confidence: float 0.8) - str: 将一条信息写入长期记忆 memory_id await memory_store.write( contentcontent, memory_typememory_type, confidenceconfidence, timestampdatetime.now() ) return fMemory saved with id: {memory_id} server.tool() async def recall(query: str, memory_type: str None, top_k: int 5) - str: 从长期记忆中检索相关信息 memories await memory_store.search( queryquery, memory_typememory_type, top_ktop_k ) return format_memories(memories) server.tool() async def forget(memory_id: str, reason: str) - str: 标记某条记忆为过期 await memory_store.mark_expired(memory_id, reason) return fMemory {memory_id} marked as expiredAgent端通过MCP客户端连接这个Server就可以像调用普通工具一样使用记忆功能。实测下来这种方式的开发效率比在Agent代码里直接操作数据库高很多而且换记忆后端的时候Agent侧完全不用改。提示MCP工具的description字段一定要写清楚。我一开始偷懒只写了“写入记忆”结果Agent经常把不该记的东西也往里塞。后来把description改成“仅当用户明确陈述个人事实、偏好或任务约束时调用”误写入率大幅下降。3.3 Docker部署那些文档里不会写的坑Docker部署本身不复杂但Agent Memory系统涉及多个组件数据库、向量库、MCP Server、Agent服务容器间的网络配置和资源分配容易出问题。我整理了几个实际踩过的坑和解决方案。第一个坑是容器间网络不通。默认的bridge网络下容器之间只能用IP互相访问但IP会变。我的做法是创建一个自定义网络所有相关容器都加入这个网络然后用容器名做DNS解析。docker network create agent-memory-net docker run -d --name postgres \ --network agent-memory-net \ -e POSTGRES_PASSWORDyourpassword \ -v pgdata:/var/lib/postgresql/data \ pgvector/pgvector:pg16 docker run -d --name memory-mcp \ --network agent-memory-net \ -e DB_HOSTpostgres \ -e DB_PORT5432 \ memory-mcp-server:latest第二个坑是Windows下Docker Desktop启动失败报“virtualization support not detected”。这个问题一般是BIOS里虚拟化没开或者Hyper-V/WSL2配置有问题。我的排查顺序是先确认BIOS里Intel VT-x或AMD-V已启用然后检查Windows功能里“虚拟机平台”和“适用于Linux的Windows子系统”是否勾选最后确认WSL2内核版本是否最新。这三步走完基本能解决。第三个坑是资源限制没设导致宿主机卡死。向量数据库和LLM推理服务都是吃内存大户不设限制的话容器可能把宿主机内存吃光。我的做法是给每个容器设memory limit并且用docker-compose统一管理。version: 3.8 services: postgres: image: pgvector/pgvector:pg16 deploy: resources: limits: memory: 2G volumes: - pgdata:/var/lib/postgresql/data memory-mcp: build: ./memory-mcp deploy: resources: limits: memory: 1G depends_on: - postgres environment: - DB_HOSTpostgres - DB_PORT5432第四个坑是数据持久化没做对。我见过有团队容器重启后记忆全丢的就是因为没挂volume。PostgreSQL的数据目录、向量库的索引文件、Redis的持久化文件这些都必须挂载到宿主机或者命名volume上。4. 实操过程从零搭建一套可用的Agent Memory系统4.1 环境准备与依赖安装先列一下我实际使用的环境配置供参考。操作系统我用的是Ubuntu 22.04Windows下用WSL2也可以但生产环境建议直接上Linux。Docker版本25.x以上docker-compose v2。Python 3.11因为MCP SDK对3.10支持最好。安装步骤我按顺序列一下# 1. 安装Docker和docker-compose curl -fsSL https://get.docker.com | sh sudo usermod -aG docker $USER # 重新登录使权限生效 # 2. 验证Docker安装 docker --version docker compose version # 3. 创建项目目录结构 mkdir -p agent-memory/{memory-mcp,agent,data} cd agent-memory # 4. 拉取pgvector镜像 docker pull pgvector/pgvector:pg16 # 5. 安装Python依赖 pip install mcp psycopg2-binary openai numpy这里有个细节要注意pgvector的镜像标签要选对。pg16对应PostgreSQL 16pg15对应15别选错了否则扩展装不上。我一开始用了默认的postgres镜像结果pgvector扩展死活装不上换成pgvector官方镜像才解决。4.2 数据库初始化与向量索引创建PostgreSQL启动后需要手动启用pgvector扩展并创建表结构。这一步很多人会忘导致后面写入数据时报“type vector does not exist”。-- 连接数据库后执行 CREATE EXTENSION IF NOT EXISTS vector; -- 创建记忆表 CREATE TABLE memories ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), content TEXT NOT NULL, memory_type VARCHAR(20) NOT NULL, embedding vector(1024), confidence FLOAT DEFAULT 0.8, status VARCHAR(20) DEFAULT active, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), metadata JSONB DEFAULT {} ); -- 创建向量索引用HNSW算法 CREATE INDEX ON memories USING hnsw (embedding vector_cosine_ops) WITH (m 16, ef_construction 64); -- 创建类型和状态的复合索引 CREATE INDEX idx_memories_type_status ON memories (memory_type, status);关于HNSW的参数m16和ef_construction64是我实测下来在百万级数据量下召回率和构建速度比较平衡的值。如果数据量小可以降到m8如果对召回率要求极高可以提到m32但构建时间和内存占用会明显增加。4.3 记忆写入与检索的完整实现写入逻辑我封装成了一个MemoryStore类核心方法就三个write、search、mark_expired。写入的时候先做embedding然后插入数据库。检索的时候先做embedding然后用向量相似度加结构化过滤。import psycopg2 from openai import OpenAI import numpy as np class MemoryStore: def __init__(self, db_config, embedding_client): self.conn psycopg2.connect(**db_config) self.embedding_client embedding_client def _get_embedding(self, text): response self.embedding_client.embeddings.create( modeltext-embedding-3-small, inputtext ) return response.data[0].embedding def write(self, content, memory_type, confidence0.8, metadataNone): embedding self._get_embedding(content) with self.conn.cursor() as cur: cur.execute( INSERT INTO memories (content, memory_type, embedding, confidence, metadata) VALUES (%s, %s, %s, %s, %s) RETURNING id , (content, memory_type, embedding, confidence, metadata or {})) memory_id cur.fetchone()[0] self.conn.commit() return memory_id def search(self, query, memory_typeNone, top_k5, min_confidence0.5): query_embedding self._get_embedding(query) sql SELECT id, content, memory_type, confidence, 1 - (embedding %s::vector) AS similarity FROM memories WHERE status active AND confidence %s params [query_embedding, min_confidence] if memory_type: sql AND memory_type %s params.append(memory_type) sql ORDER BY embedding %s::vector LIMIT %s params.extend([query_embedding, top_k]) with self.conn.cursor() as cur: cur.execute(sql, params) results cur.fetchall() return [ {id: r[0], content: r[1], type: r[2], confidence: r[3], similarity: r[4]} for r in results ]这里有个性能优化的点是pgvector的余弦距离操作符配合HNSW索引可以走索引扫描。但如果你在WHERE里加了太多过滤条件PostgreSQL可能选择全表扫描而不是索引扫描。我的经验是如果过滤后的结果集预计小于总数据的10%先用向量检索取top 50再在应用层做过滤这样比在SQL里直接过滤更快。4.4 Agent侧的记忆注入策略记忆检索出来之后怎么注入到Agent的prompt里也是有讲究的。我的做法是分区块注入不同类型的记忆放在不同的prompt区块里并且加上明确的标签。def build_memory_context(memories): if not memories: return sections { fact: 【已知用户信息】, task: 【相关历史任务】, feedback: 【用户历史反馈】 } grouped {} for m in memories: grouped.setdefault(m[type], []).append(m) context_parts [] for mem_type, header in sections.items(): if mem_type in grouped: items \n.join([f- {m[content]} for m in grouped[mem_type]]) context_parts.append(f{header}\n{items}) return \n\n.join(context_parts)注入的位置我一般放在system prompt的末尾、user message之前。实测下来这个位置Agent的利用率最高放在太前面容易被后续内容冲淡放在太后面又可能被截断。实操心得记忆注入的内容要精简。我一开始把检索到的记忆原文全部塞进去结果prompt长度暴涨Agent反而抓不住重点。后来改成每条记忆压缩成一句话最多注入5条效果明显更好。记忆的价值在于精准不在于多。5. 常见问题与排查技巧实录5.1 记忆检索不相关怎么办这是最常见的问题用户问A检索出来的是B。排查思路我一般按这个顺序走先看embedding质量。把query和检索结果的embedding拿出来算一下余弦相似度如果相似度低于0.7说明embedding模型可能不适合你的场景。中文场景下我推荐试试BGE-M3或者text-embedding-3-large比small版本在中文语义理解上强不少。再看检索策略。纯向量检索在有些场景下确实不够比如用户问“我上次说的那个餐厅”向量检索很难把“上次说的”这个时间指代和具体餐厅名关联起来。这时候需要混合检索向量相似度加关键词匹配加时间衰减综合排序。时间衰减的公式我用的是import math from datetime import datetime def time_decay_score(created_at, half_life_days30): days_elapsed (datetime.now() - created_at).days return math.exp(-days_elapsed / half_life_days)最终得分 0.6 * 向量相似度 0.3 * 时间衰减 0.1 * 置信度。这个权重是我调了几次之后觉得比较平衡的你可以根据自己的场景调整。5.2 记忆写入冲突与重复同一个事实被多次写入或者新旧信息冲突这是记忆系统必须处理的问题。我的做法是在写入前先做一次相似度检查如果发现已有记忆和新记忆的相似度超过0.95就判定为重复不写入如果在0.8到0.95之间判定为潜在冲突把新旧记忆都返回给LLM做一次判断决定是更新、合并还是保留两条。def write_with_dedup(self, content, memory_type, confidence0.8): existing self.search(content, memory_typememory_type, top_k3) for mem in existing: if mem[similarity] 0.95: return {action: skipped, reason: duplicate, existing_id: mem[id]} elif mem[similarity] 0.8: # 调用LLM判断冲突处理方式 decision self._resolve_conflict(content, mem[content]) if decision update: self.mark_expired(mem[id], superseded by new memory) elif decision merge: content self._merge_memories(content, mem[content]) self.mark_expired(mem[id], merged into new memory) return {action: written, id: self.write(content, memory_type, confidence)}这个冲突处理逻辑我跑了三个月记忆库的准确率明显提升。之前用户改偏好之后Agent还按旧偏好推荐的情况基本没有了。5.3 Docker环境下的性能问题排查容器化部署后性能下降是常见问题我整理了一个排查清单现象可能原因排查方法解决方案检索变慢容器内存不足docker stats查看内存使用增加memory limit或优化索引写入超时磁盘IO瓶颈iostat查看磁盘负载使用SSD或调整写入批次网络延迟高容器间走默认bridgedocker network inspect创建自定义网络数据丢失volume未挂载docker inspect查看Mounts配置命名volume启动失败端口冲突docker logs查看错误修改映射端口我遇到过一次检索突然变慢的情况排查了半天发现是容器内存限制设了512Mpgvector的HNSW索引加载不进去每次查询都走全表扫描。把内存限制提到2G之后查询时间从3秒降到了50毫秒。这个坑很隐蔽因为容器没崩只是慢不看docker stats根本发现不了。5.4 记忆系统的监控与迭代记忆系统上线不是终点持续监控和迭代才是。我一般会监控这几个指标记忆写入量、检索命中率、检索延迟、记忆库大小、冲突处理次数。其中检索命中率最重要它直接反映记忆系统有没有用。命中率的计算方式是Agent在回复中实际引用了检索到的记忆的次数除以总检索次数。如果命中率低于30%说明要么检索策略有问题要么写入的记忆质量不行。我一般会每周看一次这个指标低于阈值就调检索参数或者优化记忆抽取的prompt。最后分享一个小技巧给记忆加一个“使用次数”字段每次被检索并注入prompt就加一。使用次数高的记忆说明价值大可以适当提高其检索权重长期零使用的记忆可以考虑归档。这个简单的机制能让记忆库自动优胜劣汰省去很多手动清理的功夫。这套Agent Memory方案我在两个项目中实际跑过一个是对内的知识助手一个是对外的客服Agent。知识助手场景下记忆主要用来记住用户的查询偏好和已读文档客服场景下记忆用来记住用户的历史问题和处理状态。两个场景的共同点是记忆让Agent从“每次都是新对话”变成了“越用越懂你”用户体验的提升非常明显。如果你也在做Agent相关的东西强烈建议把记忆系统作为一等公民来设计别等到用户抱怨“怎么又忘了”的时候才想起来补。