news 2026/9/30 3:49:45

LLM Agent记忆系统实战:三层架构、写入决策与MCP集成

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LLM Agent记忆系统实战:三层架构、写入决策与MCP集成

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

“hindsight”这个词本身很有意思,字面意思是“事后的洞察力”,也就是我们常说的“后见之明”。放在LLM Agent的语境里,它指向一个非常具体且棘手的问题:Agent如何记住过去发生过的事情,并在后续决策中真正用上这些经验。

我接触过不少做Agent项目的团队,大家一开始都信心满满,觉得只要把LLM接上工具调用,再套一个ReAct循环,就能做出一个能自主完成复杂任务的智能体。但实际跑起来之后,几乎所有人都会撞上同一堵墙——Agent没有记忆。它每次对话都像失忆一样,用户上周告诉它的偏好、上次任务失败的原因、某个工具在特定场景下会返回异常,这些信息全部丢失。结果就是Agent永远在“重新开始”,永远在犯同样的错误。

这就是hindsight要解决的核心问题。它不是一个具体的开源项目名称,而是一类能力的统称:让Agent具备对历史交互的回顾、提炼和复用能力。你可以把它理解成Agent的“长期记忆系统”,但它比简单的对话历史存储要复杂得多。它需要决定什么值得记、怎么存、什么时候取出来用、用完怎么更新。这几个环节每一个都有坑。

这篇文章适合谁看?如果你正在做LLM Agent相关的开发,或者你已经在用MCP协议搭建工具链,又或者你只是对“Agent记忆”这个概念感兴趣想了解落地细节,那接下来的内容应该能给你不少可参考的东西。我会从整体设计思路讲到具体实现,再把我自己踩过的坑和排查经验一并分享出来。

2. Agent记忆系统的整体设计与核心思路拆解

2.1 为什么简单的向量数据库不够用

很多人一提到Agent记忆,第一反应就是“上个向量数据库不就行了”。我一开始也是这么想的,拿Chroma或者Milvus把对话历史embedding一下存进去,需要的时候做相似度检索。但实际用下来发现,这套方案在Agent场景下有几个致命问题。

第一个问题是检索粒度失控。向量检索返回的是一段一段的文本块,但Agent需要的是结构化的信息。比如用户说“我不喜欢在周五下午安排会议”,向量检索可能返回整段对话,但Agent真正需要的是“用户偏好:周五下午不排会”这个结构化事实。你让LLM每次从一堆文本块里自己提取,不仅浪费token,还容易提取错。

第二个问题是没有优先级和时效性。所有记忆平等地存在向量库里,但实际上一周前的临时任务细节和用户的核心偏好,重要性完全不一样。向量相似度只能衡量语义相关性,衡量不了重要性。

第三个问题是写入策略缺失。什么信息值得存?每轮对话都存吗?那记忆库很快就爆炸了。只存用户明确说“记住这个”的内容?那又漏掉太多隐含信息。

所以hindsight这类记忆系统的设计,核心不是“存什么”,而是“怎么组织”和“怎么决策”。它更像是一个信息管理系统,而不是一个存储系统。

2.2 三层记忆架构的设计逻辑

我目前采用的方案是三层记忆架构,这个设计参考了认知科学里人类记忆的分类方式,但在工程上做了简化。三层分别是:工作记忆(Working Memory)、情景记忆(Episodic Memory)、语义记忆(Semantic Memory)。

工作记忆就是当前对话轮次内的上下文,存在内存里,对话结束就丢弃。这部分不需要持久化,就是LLM的context window里直接维护的内容。情景记忆是具体的事件记录,比如“2024年3月15日,用户要求查询北京天气,Agent调用了weather API,返回晴天25度”。语义记忆是从情景记忆中提炼出来的抽象知识,比如“用户经常查询北京天气,可能居住在北京”或者“weather API在查询中国城市时需要用中文城市名”。

为什么要分三层?因为不同层的读写频率、存储介质、检索方式完全不一样。工作记忆追求速度,情景记忆追求完整,语义记忆追求精炼。混在一起存,检索效率会急剧下降。

注意:三层架构不是必须的。如果你的Agent场景很简单,比如只是客服问答,可能两层就够了。架构复杂度要匹配业务复杂度,不要为了架构而架构。

2.3 记忆的写入决策:什么值得记

这是整个系统里最容易被忽视但最关键的一环。我的做法是引入一个记忆评估器,它本身也是用LLM实现的,但prompt经过精心设计。每轮对话结束后,评估器会判断这轮交互是否包含值得写入长期记忆的信息。

评估器主要看几个维度:新颖性(这个信息之前是否已经存在)、重要性(对后续任务是否有影响)、稳定性(这个信息是临时的还是长期的)。比如用户说“帮我查一下今天的天气”,这是临时需求,不需要写入长期记忆。但用户说“我以后查天气都默认用摄氏度”,这就是一个稳定的偏好,必须记下来。

这里有个实操技巧:评估器的prompt里一定要给正反例。我一开始只写了判断标准,结果LLM把什么都往里存,记忆库很快就臃肿了。后来加了几个few-shot例子,比如“用户说‘你好’→不存”、“用户说‘我对花生过敏’→存”,准确率立刻上来了。

2.4 记忆的检索策略:什么时候取出来用

检索策略的核心是在正确的时间把正确的记忆注入到Agent的context里。我的做法是在每次Agent准备调用工具或者生成回复之前,先做一次记忆检索。检索的query不是简单的用户输入,而是结合了当前任务状态的复合query。

具体来说,检索query由三部分组成:当前用户输入+当前任务类型+最近几轮对话摘要。这样检索出来的记忆既相关又不会太发散。比如用户说“帮我订个餐厅”,当前任务类型是“预订”,最近对话摘要是“用户在讨论周末聚餐”,那检索出来的记忆可能包括“用户偏好川菜”、“用户之前提过周六晚上有空”等。

检索数量也要控制。我一般取top-5,最多不超过8条。太多记忆注入会挤占context window,而且LLM在处理大量记忆时反而容易忽略关键信息。这跟人开会一样,给你一堆参考资料,你反而抓不住重点。

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

3.1 记忆的数据结构设计

记忆存什么格式,直接决定了后续检索和更新的效率。我试过几种方案,最后稳定下来的结构是这样的:

{ "memory_id": "uuid", "type": "episodic | semantic", "content": "用户偏好周五下午不安排会议", "embedding": [0.123, 0.456, ...], "metadata": { "created_at": "2024-03-15T10:30:00Z", "last_accessed": "2024-03-20T14:00:00Z", "access_count": 5, "source": "conversation_123", "confidence": 0.92, "tags": ["preference", "schedule"] } }

这个结构里,content是给LLM看的自然语言描述,embedding是给检索用的向量,metadata里的字段各有用途。access_count和last_accessed用来做记忆的衰减和淘汰,confidence表示这条记忆的可信度(有些信息是用户随口说的,可信度就低一些),tags用来做分类过滤。

提示:content字段的写法很讲究。不要存原始对话,要存提炼后的事实。原始对话“用户:我周五下午一般都有会,别给我排东西”应该转成“用户偏好:周五下午不安排会议”。前者检索出来LLM还要再理解一遍,后者直接就能用。

3.2 记忆的更新与冲突处理

记忆不是只写不更新的。用户上周说“我喜欢川菜”,这周说“我最近吃不了辣”,这两条记忆就冲突了。怎么处理?

我的方案是新记忆覆盖旧记忆,但保留历史版本。具体操作是:写入新记忆时,先检索是否有语义相似的旧记忆。如果有,比较时间戳和confidence,新记忆时间更新且confidence不低于旧记忆时,将旧记忆标记为superseded,新记忆正常写入。检索时默认只返回非superseded的记忆。

但这里有个坑:有些冲突不是真正的冲突,而是补充。比如“用户喜欢川菜”和“用户最近吃不了辣”并不矛盾,只是时效性不同。所以我在metadata里加了一个valid_until字段,对于临时性信息设置过期时间。过期后自动降权,但不删除。

3.3 记忆衰减与淘汰机制

记忆库不能无限增长。我的淘汰策略是基于访问频率和时间的加权评分。每条记忆有一个分数,计算公式大致是:

score = access_count * 0.4 + recency_score * 0.3 + confidence * 0.3

其中recency_score是最近一次访问距今时间的衰减函数,我用的是指数衰减,半衰期设为7天。分数低于阈值的记忆会被归档到冷存储,不再参与常规检索,但保留以备审计。

这个机制的效果是:常用的记忆会一直保留,偶尔用到的会慢慢降权,从来不用的最终被归档。实测下来,一个中等复杂度的Agent运行一个月,活跃记忆数量稳定在200-500条左右,不会无限膨胀。

3.4 与MCP协议的集成方式

MCP(Model Context Protocol)是现在Agent工具调用的事实标准之一。把记忆系统做成MCP Server是一个很自然的选择。这样做的好处是:任何支持MCP的Agent框架都可以直接接入,不需要改Agent本身的代码。

我实现的记忆MCP Server暴露了三个tool:memory_write、memory_search、memory_update。Agent在需要的时候调用这些tool,就像调用其他MCP工具一样。Docker部署也很简单,一个容器跑Server,一个容器跑向量数据库,通过Docker network互联。

version: '3.8' services: memory-server: build: ./memory-server ports: - "8080:8080" environment: - VECTOR_DB_URL=http://vector-db:6333 depends_on: - vector-db vector-db: image: qdrant/qdrant:latest volumes: - ./data:/qdrant/storage

这个compose文件是我实际在用的,Qdrant做向量存储,memory-server做业务逻辑。两个容器通过内部网络通信,对外只暴露memory-server的端口。

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

4.1 环境准备与依赖安装

先说环境。我是在Ubuntu 22.04上做的开发和部署,Windows用户建议用WSL2,Mac用户直接跑就行。Docker和Docker Compose是必须的,版本不要太老,Docker 24以上、Compose v2以上。

安装Docker的步骤我就不赘述了,网上教程很多。重点说一下Docker Desktop在Windows上常见的启动问题。如果你遇到“Virtualization support not detected”这个报错,大概率是BIOS里的虚拟化支持没开。重启进BIOS,找到Intel VT-x或者AMD-V,设为Enabled。如果还不行,检查一下Hyper-V和WSL2是否冲突,有时候两个都开着会打架。

Python环境我用的3.11,主要依赖这几个包:

pip install fastapi uvicorn qdrant-client openai tiktoken

qdrant-client是Qdrant的Python SDK,tiktoken用来算token数,方便控制记忆注入的量。OpenAI的SDK用来调LLM做记忆评估和提炼,如果你用其他模型,换成对应的SDK就行。

4.2 记忆写入的完整流程实现

写入流程分四步:接收对话 → 评估是否值得记 → 提炼结构化记忆 → 写入存储。

第一步接收对话很简单,MCP tool被调用时传入对话内容就行。关键是第二步的评估。我的评估器prompt大致是这样的:

你是一个记忆评估器。判断以下对话内容是否包含值得长期记忆的信息。 值得记忆的信息包括:用户偏好、重要事实、任务关键上下文、工具使用经验。 不值得记忆的信息包括:寒暄、临时查询、已经过时的信息。 对话内容:{conversation} 请输出JSON格式:{"should_remember": true/false, "reason": "..."}

第三步提炼结构化记忆,我用另一个prompt来做:

将以下对话内容提炼为一条简洁的记忆事实。 要求:用第三人称陈述,不超过50字,包含关键实体和关系。 对话内容:{conversation} 输出格式:{"content": "...", "type": "episodic/semantic", "tags": [...]}

第四步写入Qdrant,同时把embedding也算好存进去。embedding我用的是OpenAI的text-embedding-3-small,1536维,性价比不错。

实操心得:评估和提炼可以合并成一次LLM调用,省token也省时间。我一开始分开做,后来发现合并后效果差不多,但速度快了一倍。prompt里让LLM同时输出should_remember和content就行。

4.3 记忆检索的注入时机与格式

检索的触发时机很关键。我的做法是在Agent的system prompt里加一段动态内容,每次Agent准备生成回复前,先调用memory_search,把返回的记忆格式化后插入system prompt。

格式大概是这样:

## 相关历史记忆 1. [偏好] 用户偏好周五下午不安排会议(置信度:0.92) 2. [事实] 用户对花生过敏(置信度:0.98) 3. [经验] weather API查询中国城市需用中文城市名(置信度:0.85)

这个格式的好处是LLM一眼就能看懂每条记忆的类型和可信度,在做决策时会自然考虑这些因素。我试过用JSON格式注入,效果反而不好,LLM对JSON里的信息敏感度不如自然语言列表。

检索的query构造也有讲究。我一开始直接用用户输入做query,发现检索出来的记忆经常不相关。后来改成用户输入 + 最近三轮对话的摘要 + 当前任务类型,相关性明显提升。摘要用LLM生成,任务类型从Agent的状态机里取。

4.4 记忆更新的触发与执行

更新有两个触发点:定时任务和事件驱动。定时任务每天跑一次,做记忆的衰减计算和归档。事件驱动是在写入新记忆时触发,检查是否有冲突需要处理。

冲突检测的逻辑是:新记忆写入前,先用新记忆的embedding做一次检索,取top-3。如果某条旧记忆的相似度超过0.85,就认为可能冲突。然后让LLM判断这两条记忆是否真的矛盾。如果矛盾,按前面说的覆盖策略处理。

def check_conflict(new_memory, existing_memories): for old in existing_memories: similarity = cosine_sim(new_memory.embedding, old.embedding) if similarity > 0.85: prompt = f"判断以下两条记忆是否矛盾:\n新:{new_memory.content}\n旧:{old.content}\n输出:矛盾/不矛盾" result = llm.invoke(prompt) if "矛盾" in result: return old return None

这个逻辑跑下来,误判率大概在5%左右,主要是LLM有时候会把补充信息误判为矛盾。后来我在prompt里加了“如果新记忆是旧记忆的补充或细化,不算矛盾”这个说明,误判率降到了2%以下。

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

5.1 记忆检索不相关怎么办

这是最常见的问题。你明明存了相关记忆,但检索的时候就是出不来。排查思路按优先级来:

先看embedding模型是否合适。如果你用的是通用embedding模型,在特定领域(比如医疗、法律)可能效果不好。可以考虑用领域数据微调一个embedding模型,或者换一个在该领域表现更好的模型。

再看检索query的构造。前面说了,直接用用户输入做query效果不好。试试加上任务类型和对话摘要。如果还不行,可以试试多query检索,就是用不同的query分别检索,然后合并去重。

最后看相似度阈值。Qdrant默认返回top-k,但有些结果相似度很低,注入了反而干扰LLM。我一般设一个0.7的阈值,低于这个值的不返回。

5.2 记忆库膨胀太快怎么控制

如果你的记忆库一周就涨了几千条,说明写入评估太宽松了。检查评估器的prompt,是不是把太多临时信息判为值得记忆。另外可以加一个写入频率限制,比如同一个session最多写入10条记忆,超过的排队或者丢弃。

还有一个技巧是记忆合并。如果多条记忆讲的是同一件事,可以合并成一条。比如“用户喜欢川菜”、“用户喜欢火锅”、“用户喜欢麻辣烫”可以合并成“用户喜欢川菜和火锅类食物”。合并用LLM做,定期跑一次。

5.3 Docker网络不通导致记忆服务不可用

这个坑我踩过好几次。表现是memory-server容器启动正常,但Agent调用时报连接超时。排查步骤:

先docker exec进memory-server容器,curl一下vector-db的地址,看能不能通。如果不通,检查两个容器是否在同一个Docker network里。docker network inspect看一下。

如果网络通但服务不可用,检查端口映射。Qdrant默认端口是6333,memory-server连的时候要用容器名+端口,不是localhost。这个新手很容易搞错。

还有一个隐蔽的问题:Docker Desktop在Mac上有时会有网络延迟,容器间通信偶尔超时。解决办法是给memory-server加一个重试机制,或者把两个服务放到同一个容器里用supervisor管理。

5.4 LLM评估器返回格式不稳定

用LLM做评估器,最头疼的就是它有时候不按格式返回。你让它输出JSON,它给你输出一段解释文字。解决办法有几个:

一是用function calling。OpenAI的function calling可以强制LLM按schema输出,稳定性高很多。二是加格式校验和重试。解析失败就重试,重试三次还失败就跳过这条记忆。三是用更小的模型做评估。大模型虽然聪明但有时候“话多”,小模型反而更听话。我试过用GPT-3.5-turbo做评估,格式稳定性比GPT-4还好。

5.5 常见问题速查表

问题现象可能原因排查方法解决方案
检索不到相关记忆embedding模型不匹配手动测试几条已知相关记忆的相似度换模型或微调
记忆库增长过快写入评估太宽松检查评估器prompt和few-shot例子收紧标准,加频率限制
容器间连接超时Docker网络配置错误docker network inspect确保同网络,用容器名通信
LLM返回格式错误prompt不够明确打印原始返回内容用function calling或加重试
记忆冲突误判冲突检测prompt不完善人工审核误判案例补充说明和反例
检索结果太多top-k设置过大检查检索参数降低top-k,加相似度阈值

最后分享一个小技巧:记忆系统的调试一定要有可视化界面。我一开始全靠日志,排查问题很痛苦。后来用Streamlit搭了一个简单的管理页面,可以浏览、搜索、手动编辑记忆,效率提升巨大。这个页面不需要多好看,能看能改就行。

这个记忆系统我跑了大概三个月,中间迭代了七八个版本,从最开始简单的向量存储到现在三层架构加冲突处理,踩的坑基本都在这了。后续还可以扩展的方向包括:记忆的跨Agent共享、基于记忆的主动学习、记忆的可解释性分析等。不过那是另一个话题了,先把基础打牢再说。

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

Vue项目代码混淆实战:自定义插件与关键配置指南

交付一个前端项目,最尴尬的时刻莫过于客户随口一句"你们这个包我看了下,接口地址和加密逻辑都写在里面了"。不少 Vue 团队的第一反应是加个uglify就完事,结果发现变量名变成a、b、c之后,业务逻辑依然能被轻松还原——因…

作者头像 李华
网站建设 2026/9/30 3:49:15

MySQL数据出海同步方案:主从复制与CDC实战解析

做数据出海(或者说跨地域数据同步)这个方向时,我最深的感触是:数据库同步链路出问题,往往不是某一台机器宕机,而是“一切看起来都正常,但数据就是少了半小时”。尤其当源端是 MySQL,…

作者头像 李华
网站建设 2026/9/30 3:48:35

MATLAB搭建CNN-LSTM-SE注意力模型实现时序数据分类

Matlab里把CNN-LSTM-SE注意力机制串起来做数据分类预测,这件事我在不同数据集上反复折腾过好几轮。说实话,网上搜"CNN-LSTM-SE"十个结果九个是Python写的,剩下一个还是从PyTorch翻译过来的,MATLAB能直接跑的完整资料非常…

作者头像 李华
网站建设 2026/9/30 3:47:27

hindsight实战:基于MCP与Docker的LLM Agent记忆系统设计与部署

1. 从“hindsight”说起:为什么我们需要给Agent装上“后视镜”“hindsight”这个词,直译过来就是“后见之明”,或者更通俗一点——“事后诸葛亮”。放在人类身上,它指的是我们回顾过去、从经历中提炼教训的能力。而当我第一次看到…

作者头像 李华
网站建设 2026/9/30 3:46:48

2022年408真题解析:磁盘物理结构与DMA方式综合计算

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

作者头像 李华
网站建设 2026/9/30 3:46:37

MIMO卫星信道均衡:RLS算法原理与Matlab实现解析

我们需要先明确一件事:这篇博文我不会像教科书那样先列一大段“研究背景”,而是直接讲清楚这个项目到底在解决什么问题、代码怎么组织、踩过哪些坑。你搜到的“MIMO卫星信道均衡”“RLS算法”“Matlab代码”这些关键词,背后对应的是一类典型的…

作者头像 李华