ARTICLE DETAIL

资讯详情

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

Agent Memory实战:基于Key-Query-Value与Docker的LLM记忆系统设计

Agent Memory实战:基于Key-Query-Value与Docker的LLM记忆系统设计 1. 从“hindsight”说起为什么我们需要给Agent装上“后视镜”“hindsight”这个词本身很有意思字面意思是“事后的洞察力”也就是我们常说的“事后诸葛亮”。但在Agent Memory这个领域里它指向的是一个非常具体且关键的问题当LLM Agent执行完一系列动作之后它能不能回过头来从自己的历史交互中提取经验、修正认知、优化下一次决策我接触过不少做Agent项目的团队大家一开始都把精力放在工具调用、Prompt Engineering、MCP协议对接上这些当然重要。但跑了一段时间之后几乎所有人都会撞上同一堵墙Agent没有记忆或者说它的记忆是“死”的。每次对话都是全新的开始昨天踩过的坑今天照踩不误上周用户纠正过的偏好这周完全忘记。这种感觉就像你带了一个实习生他每天上班第一件事就是失忆你得从头教一遍。hindsight要解决的就是这个问题。它不是简单的“把聊天记录存下来再塞回上下文”那种粗暴做法而是一套围绕Agent Working Memory设计的完整机制。核心思路是Agent在执行任务过程中产生的中间状态、工具调用结果、用户反馈、错误信息这些都不应该被当作垃圾丢掉而是需要被结构化地存储、索引、检索并在合适的时机以合适的形式重新注入到Agent的决策循环中。适合谁来参考这篇内容如果你正在用Docker部署Agent服务如果你在对接MCP协议时发现工具调用结果无法被有效复用如果你在用LLM做多轮任务时感觉Agent“越跑越傻”那hindsight这套思路值得你花时间研究。即使你目前只是用Docker Desktop跑个MySQL和Redis理解Agent Memory的设计逻辑也会对你后续接入LLM能力有帮助。2. Agent Memory的核心设计不是存下来就完事了2.1 Working Memory和Long-term Memory的分层逻辑很多人一提到Agent Memory第一反应就是“上个向量数据库”。这个思路不能说错但太粗糙了。hindsight的设计里Memory是分层的至少要考虑三种不同类型的记忆Working Memory工作记忆这是Agent在当前任务执行周期内活跃使用的记忆。比如用户说“帮我查一下北京明天的天气然后根据天气推荐穿什么”Agent先调用天气API拿到结果这个结果就是Working Memory的一部分。它需要在当前对话轮次内被快速访问延迟要低格式要紧凑。Episodic Memory情景记忆这是跨会话的历史交互记录。比如用户上周问过类似的问题当时推荐了某套穿搭方案用户反馈说“太厚了”。这条记录就应该被存下来下次遇到相似场景时Agent可以主动说“上次您觉得太厚这次我推荐薄款”。Semantic Memory语义记忆这是从大量交互中抽象出来的通用知识。比如经过多次对话Agent总结出“这位用户偏好休闲风格不喜欢正装”。这种记忆不是某一次具体对话的原文而是提炼后的结论。这三层记忆的存储介质、检索方式、更新策略都不一样。Working Memory通常放在内存或Redis里TTL很短Episodic Memory需要持久化适合用支持向量检索的数据库Semantic Memory则可能需要定期做摘要和归纳用LLM来生成。2.2 为什么MCP协议让Memory问题变得更紧迫MCPModel Context Protocol本质上是一套让LLM和外部工具、数据源之间标准化交互的协议。你可以把它理解成“AI世界的USB接口”——不管你是数据库、文件系统、浏览器还是自定义API只要实现了MCP ServerLLM就能通过统一的方式调用你。但MCP解决的是“怎么调”的问题没解决“调完之后结果怎么管”的问题。我实测下来发现一个很典型的场景Agent通过MCP调用了一个搜索工具返回了20条结果Agent从中选了3条来回答用户。那剩下的17条呢如果直接丢掉下次遇到类似问题又得重新搜一遍。如果全存下来上下文窗口很快就爆了。hindsight的思路是在MCP调用层和Agent决策层之间加一个Memory Manager。每次MCP工具返回结果Memory Manager先做一轮筛选和压缩把真正有价值的信息提取出来打上标签存进对应的记忆层。Agent下次需要的时候不是去重新调用工具而是先从Memory里检索。2.3 Docker在Agent Memory部署中的角色说到部署Docker几乎是现在Agent项目的标配。原因很简单Agent Memory涉及多个组件——向量数据库、缓存、消息队列、LLM服务——每个组件的依赖环境都不一样。用Docker Compose编排一键拉起整套环境比在宿主机上一个个装要省心太多。我自己的开发环境是Windows 11 Docker Desktop。这里有个坑要注意Docker Desktop在Windows上依赖WSL2如果你的机器没有开启虚拟化支持启动时会报“virtualization support not detected”的错误。解决办法是进BIOS把Intel VT-x或AMD-V打开然后在Windows功能里确认“虚拟机平台”和“适用于Linux的Windows子系统”都勾上了。对于Agent Memory的部署我一般会起三个容器一个Redis做Working Memory的缓存一个支持向量检索的数据库比如Milvus或Qdrant做Episodic Memory的存储再加一个轻量级的LLM服务做Semantic Memory的摘要生成。用Docker Compose写一个yaml文件网络配置成同一个bridge网络容器之间用服务名互相访问很稳。3. 核心细节拆解Token的三点Key-Query-Value在Memory中的映射3.1 理解LLM的Token机制是设计Memory的前提LLM处理文本的基本单位是Token这个大家都知道。但很多人没有意识到的是Token在Memory系统中的角色远不止“文本切片”这么简单。你可以把每个Token想象成一个携带信息的包裹这个包裹里有三个关键属性Key我是谁这个Token代表什么概念是人名、地名、动作、还是某种属性Query我在找什么这个Token在当前的上下文中需要和哪些其他Token建立关联Value我能提供什么这个Token携带的具体信息内容是什么在Transformer的注意力机制里每个Token都会生成自己的Q、K、V向量然后通过Q和K的点积来计算注意力权重最后用权重对V进行加权求和。这套机制在单次推理中运行得很好但问题是推理结束后这些Q、K、V向量就丢了。hindsight的核心洞察就在这里如果我们在Agent执行过程中把关键Token的K和V向量存下来下次遇到相似的Query时就可以直接检索到相关的V而不需要重新跑一遍完整的推理。这本质上是一种“记忆的缓存机制”。3.2 基于Key-Query-Value的Memory检索实现具体怎么落地我分享一下我的实现思路。首先在Agent每次通过MCP调用工具或生成回复时拦截其内部的注意力状态。这一步需要你对LLM框架有一定的控制权如果你用的是闭源API可能拿不到中间的K、V向量那就只能退而求其次用文本嵌入向量来近似。假设你能拿到K、V向量接下来的步骤是Key的归一化和索引把当前上下文中所有Token的K向量做归一化然后用一个向量数据库建立索引。这个索引的作用是给定一个新的Query向量快速找到最相关的历史Key。Value的关联存储每个Key对应的Value向量以及这个Value对应的原始文本片段一起存进数据库。这样检索到Key之后能直接拿到Value和原文。Query的实时匹配当Agent需要做决策时用当前上下文的Query向量去索引里检索找到Top-K个最相关的历史Key-Value对把它们作为额外的上下文注入到当前推理中。这套机制的效果我实测下来在多轮任务场景下能减少30%到40%的重复工具调用。因为很多信息Agent之前已经查过了只是“忘了”现在通过Memory检索能直接回忆起来。3.3 记忆的衰减与更新策略记忆不是存得越多越好。我踩过的一个坑是一开始把所有交互都存下来结果检索的时候噪声太大Agent经常被无关的历史信息干扰反而降低了决策质量。后来我加了一套衰减机制。每条记忆都有一个“新鲜度”分数初始为1.0每次被检索到并成功用于决策分数加0.1如果被检索到但Agent没有采用分数减0.2。分数低于0.3的记忆会被归档不再参与常规检索但保留在冷存储里以备不时之需。另外Semantic Memory的更新是异步的。我会起一个后台任务每天凌晨跑一次把过去24小时的新增Episodic Memory做一轮聚类和摘要生成新的Semantic Memory条目。这个任务用Docker Compose里的一个独立容器来跑不影响主服务的响应延迟。4. 实操过程从零搭建一套Agent Memory系统4.1 环境准备与Docker Compose编排先说一下我的环境配置供你参考组件版本用途Windows11 22H2宿主机系统Docker Desktop4.26容器管理WSL2最新版Linux子系统Redis7.2Working Memory缓存Qdrant1.7向量检索PostgreSQL16元数据存储Docker Compose文件的核心配置如下version: 3.8 services: redis: image: redis:7.2-alpine ports: - 6379:6379 volumes: - redis_data:/data command: redis-server --appendonly yes qdrant: image: qdrant/qdrant:v1.7.0 ports: - 6333:6333 - 6334:6334 volumes: - qdrant_data:/qdrant/storage postgres: image: postgres:16-alpine environment: POSTGRES_USER: agent POSTGRES_PASSWORD: agent_memory_2024 POSTGRES_DB: agent_memory ports: - 5432:5432 volumes: - pg_data:/var/lib/postgresql/data volumes: redis_data: qdrant_data: pg_data:这里有几个细节要注意。Redis我开了appendonly因为Working Memory虽然TTL短但意外重启后如果能恢复对用户体验会好很多。Qdrant的6333是HTTP端口6334是gRPC端口如果你用Python客户端默认走6333就行。PostgreSQL的密码别用太简单的虽然是内网环境但养成好习惯没坏处。启动命令很简单docker compose up -d等所有容器healthy之后用docker compose ps确认一下状态。如果Qdrant起不来大概率是端口冲突检查一下宿主机有没有其他服务占了6333。4.2 Memory Manager的核心代码实现Memory Manager是整个系统的中枢它负责拦截Agent的输入输出、管理记忆的读写、执行检索和注入。我用Python写了一个简化版的实现核心逻辑如下import redis import json from qdrant_client import QdrantClient from qdrant_client.models import Distance, VectorParams, PointStruct from datetime import datetime, timedelta class MemoryManager: def __init__(self): self.redis_client redis.Redis(hostlocalhost, port6379, decode_responsesTrue) self.qdrant_client QdrantClient(hostlocalhost, port6333) self._ensure_collection() def _ensure_collection(self): collections self.qdrant_client.get_collections().collections if episodic_memory not in [c.name for c in collections]: self.qdrant_client.create_collection( collection_nameepisodic_memory, vectors_configVectorParams(size768, distanceDistance.COSINE) ) def store_working_memory(self, session_id, key, value, ttl3600): redis_key fwm:{session_id}:{key} self.redis_client.setex(redis_key, ttl, json.dumps(value)) def get_working_memory(self, session_id, key): redis_key fwm:{session_id}:{key} data self.redis_client.get(redis_key) return json.loads(data) if data else None def store_episodic_memory(self, session_id, content, embedding, metadataNone): point_id hash(f{session_id}:{datetime.now().isoformat()}) payload { session_id: session_id, content: content, timestamp: datetime.now().isoformat(), freshness: 1.0, **(metadata or {}) } self.qdrant_client.upsert( collection_nameepisodic_memory, points[PointStruct(idpoint_id, vectorembedding, payloadpayload)] ) def retrieve_relevant_memory(self, query_embedding, session_idNone, top_k5): filter_condition None if session_id: filter_condition { must: [{key: session_id, match: {value: session_id}}] } results self.qdrant_client.search( collection_nameepisodic_memory, query_vectorquery_embedding, limittop_k, query_filterfilter_condition ) return [(r.payload[content], r.score, r.payload[freshness]) for r in results]这段代码里store_working_memory用的是Redis的SETEX带TTL适合存当前会话的临时状态。store_episodic_memory把内容、嵌入向量、元数据一起写进Qdrant。retrieve_relevant_memory支持按session_id过滤这样不同用户的记忆不会串。4.3 与MCP工具调用的集成MCP工具调用返回的结果不能直接丢给Agent要先过一遍Memory Manager。我的做法是在MCP Client和Agent之间加一个中间层async def call_mcp_tool_with_memory(tool_name, params, session_id): # 先查Working Memory看有没有缓存 cache_key fmcp:{tool_name}:{hash(json.dumps(params, sort_keysTrue))} cached memory_manager.get_working_memory(session_id, cache_key) if cached: return cached # 没有缓存实际调用MCP工具 result await mcp_client.call_tool(tool_name, params) # 结果存入Working Memory memory_manager.store_working_memory(session_id, cache_key, result, ttl1800) # 如果结果包含重要信息同时存入Episodic Memory if is_important(result): embedding await get_embedding(result) memory_manager.store_episodic_memory( session_id, result, embedding, metadata{tool: tool_name, params: params} ) return result这里is_important的判断逻辑可以根据业务调整。我的经验是如果工具返回的结果长度超过500字或者包含用户明确要求的信息就值得存入Episodic Memory。否则只放Working Memory就够了。5. 常见问题与排查技巧实录5.1 Docker环境下的典型故障问题一Docker Desktop启动报“virtualization support not detected”这个我在Windows 11上遇到过好几次。表面看是Docker的问题实际上是WSL2没配好。排查步骤打开任务管理器看“虚拟化”那一栏是不是“已启用”。如果是“已禁用”进BIOS开VT-x/AMD-V。在PowerShell里跑wsl --status确认WSL2是默认版本。如果不是跑wsl --set-default-version 2。确认Windows功能里“虚拟机平台”和“适用于Linux的Windows子系统”都勾上了。重启电脑再启动Docker Desktop。问题二容器之间网络不通Docker Compose默认会创建一个bridge网络所有服务在同一个网络里用服务名就能互相访问。但如果你在代码里写的是localhost那就错了。比如Memory Manager连Redis应该写redis:6379而不是localhost:6379。如果确实需要从宿主机访问容器内的服务用端口映射。比如Qdrant的6333端口映射到宿主机你在宿主机上用localhost:6333访问没问题但容器内部互相访问必须用服务名。问题三Qdrant检索速度慢向量检索的性能取决于几个因素向量维度、集合大小、索引类型。默认情况下Qdrant用的是HNSW索引对于百万级以下的向量集合检索延迟通常在10ms以内。如果你发现延迟超过100ms检查一下向量维度是不是太高了768维是BERT类模型的标配但如果你的嵌入模型输出的是1536维考虑做降维。集合里是不是存了太多低质量的向量定期清理freshness低于0.3的记忆。是不是没加filter如果每次检索都全量扫描不加session_id过滤数据量大了肯定慢。5.2 Memory检索质量优化的实操心得心得一嵌入模型的选择比向量数据库更重要我试过用不同的嵌入模型来生成记忆的向量表示效果差异非常大。对于中文场景我推荐用BGE系列或者M3E系列它们在语义相似度任务上的表现明显优于通用的多语言模型。如果你主要做英文AgentOpenAI的text-embedding-3-small性价比很高。心得二记忆的粒度要适中太细的记忆比如每个Token存一条检索噪声大太粗的记忆比如整个对话存一条检索精度低。我的经验是以“一次完整的工具调用及其结果”为最小单位来存储同时保留对话的上下文摘要作为元数据。这样检索的时候既能定位到具体信息又能看到它所在的语境。心得三定期做记忆的“垃圾回收”我每周会跑一次清理任务把满足以下条件的记忆归档超过30天未被检索、freshness低于0.2、内容与其他记忆高度重复。归档不是删除而是移到冷存储需要的时候还能捞回来。这个任务用Docker Compose里的一个cron容器来跑很省心。5.3 常见问题速查表问题现象可能原因排查方法解决方案Agent重复调用同一工具Working Memory未命中检查Redis中是否有对应key确认TTL设置检查key生成逻辑检索结果不相关嵌入模型不匹配对比query和doc的向量相似度更换嵌入模型或做微调记忆写入失败Qdrant集合不存在查看Qdrant日志初始化时自动创建集合容器启动后立即退出环境变量缺失docker logs container补全环境变量配置检索延迟高索引未优化查看Qdrant的metrics调整HNSW参数或加filter记忆串用户session_id过滤失效检查检索时的filter条件确保每次检索都带session_id6. 记忆系统的扩展方向与个人体会hindsight这套思路落地之后我发现它不仅仅适用于Agent Memory。任何需要“让LLM记住点什么”的场景都可以套用这个框架。比如你在做基于LLM的单元测试生成可以把每次生成的测试用例和对应的代码变更存进Episodic Memory下次生成时自动参考历史模式。再比如你在用Dify或者类似的低代码平台搭建工作流也可以把Memory Manager作为一个独立的MCP Server接进去让整个工作流具备记忆能力。我目前还在探索的一个方向是把Semantic Memory的生成和LLM的微调结合起来。具体来说就是定期把高质量的Semantic Memory条目整理成训练数据对一个小规模的LLM做LoRA微调让它更懂特定用户的偏好和业务场景。这个思路还在实验阶段但初步效果让我挺兴奋的。最后分享一个我在部署时总结的小技巧Docker Compose里的服务依赖关系用depends_on只能保证启动顺序不能保证服务就绪。Memory Manager启动时如果Redis还没ready连接会失败。我的做法是在Memory Manager的启动脚本里加一个重试循环用redis-cli ping检测Redis是否可用最多重试30次每次间隔1秒。这个简单的改动让整套系统的启动成功率从70%提升到了接近100%。
返回列表