拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

基于MCP协议的LLM Agent长期记忆框架hindsight设计与Docker部署实战

基于MCP协议的LLM Agent长期记忆框架hindsight设计与Docker部署实战

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

“hindsight”这个词,直译过来就是“后见之明”,或者更通俗一点——“事后诸葛亮”。但在LLM Agent的开发语境里,它指的是一套让智能体能够回顾、检索并利用过往交互记忆的机制。你可以把它理解成给Agent装上了一面后视镜,让它不再每次对话都像失忆一样从零开始,而是能“想起”之前发生过什么、用户偏好是什么、哪些操作踩过坑。

我最初接触这个概念,是因为在做一个基于MCP协议的多工具Agent项目时,发现一个很尴尬的问题:用户上午刚告诉我“以后所有报表都用A4纸横向打印”,下午再问“帮我打印一份销售报表”,Agent依然会弹出默认的纵向设置。这不是模型不够聪明,而是它根本没有“记住”上午的对话。Agent的working memory在会话结束后就被清空了,下一次请求对它来说就是全新的世界。

这就是hindsight要解决的核心痛点:让Agent拥有跨会话、跨任务的长期记忆能力。它不仅仅是简单的对话历史存储,而是一套完整的记忆管理框架,涉及记忆的写入、索引、检索、衰减和更新。结合当前热门的agent memory、MCP协议、Docker容器化部署等技术,hindsight实际上是一个融合了存储层、检索层和协议层的系统工程。

这篇文章适合谁看?如果你正在开发LLM Agent,或者对MCP协议、Agent记忆机制感兴趣,又或者你只是单纯好奇“为什么我的Agent总是记不住事”,那接下来的内容应该能给你一些可以直接抄作业的思路和代码。我会从整体设计、核心细节、实操部署到问题排查,把hindsight这套东西拆开了揉碎了讲清楚。

2. 整体设计思路:hindsight的记忆架构是怎么搭起来的

2.1 为什么不能只用对话历史做记忆

很多人第一反应是:记忆嘛,把对话历史存下来不就行了?我一开始也是这么想的,直接用SQLite存了所有message,每次请求把最近N条塞进context。但很快问题就来了:token消耗爆炸、检索精度极差、旧信息干扰新任务。

举个例子,用户在一个月前问过“帮我查一下北京天气”,今天问“帮我订一张去北京的机票”。如果把一个月前的天气对话也塞进context,模型可能会莫名其妙地在订票回复里加一句“北京今天晴,适合出行”。这就是典型的记忆污染——无关的旧记忆干扰了当前任务。

所以hindsight的设计核心不是“存更多”,而是“存得更聪明”。它需要解决三个问题:第一,什么信息值得记;第二,怎么快速找到相关的记忆;第三,怎么让过时或错误的记忆逐渐淡出。

2.2 三层记忆架构:working memory、episodic memory、semantic memory

参考认知科学里的人类记忆模型,hindsight把Agent记忆分成了三层:

  • Working Memory(工作记忆):当前会话的上下文,生命周期最短,通常就是最近几轮对话。它存在内存里,会话结束就释放。这一层不需要持久化,但需要保证低延迟。
  • Episodic Memory(情景记忆):具体的事件记录,比如“2024年3月15日,用户要求用A4横向打印报表”。它带有时间戳和场景标签,存在持久化存储里,检索时按时间衰减加权。
  • Semantic Memory(语义记忆):从情景记忆中抽象出来的通用知识,比如“该用户偏好横向打印”。它不依赖具体时间,是经过多次情景记忆归纳后形成的稳定认知。

这个分层的好处是:检索时可以先查semantic memory拿稳定偏好,再查episodic memory拿具体事件,最后结合working memory的当前上下文,三层融合后注入prompt。这样既保证了相关性,又控制了token量。

2.3 为什么选择MCP协议做记忆接口

MCP(Model Context Protocol)是当前Agent工具调用领域的一个热门协议,它定义了一套标准化的上下文交互方式。hindsight选择MCP作为记忆读写的接口层,主要考虑是解耦。

如果没有MCP,记忆模块和Agent框架是强耦合的——换一个Agent框架,记忆模块就得重写。而通过MCP,记忆服务可以作为一个独立的Server运行,任何支持MCP协议的Agent都可以通过标准接口调用它。这就像USB接口一样,不管你是键盘、鼠标还是U盘,只要插上就能用。

具体来说,hindsight暴露的MCP工具包括:

  • memory_write:写入一条记忆
  • memory_search:语义检索相关记忆
  • memory_forget:标记某条记忆为过期
  • memory_summarize:对情景记忆做归纳,生成语义记忆

Agent在每轮对话结束后调用memory_write,在每轮对话开始前调用memory_search,整个流程对Agent框架透明。

2.4 Docker化部署的考量

记忆服务需要持久化存储,可能用到向量数据库、关系数据库、缓存等多个组件。如果直接裸机部署,环境依赖会非常麻烦。Docker化之后,整个hindsight可以打包成一个compose文件,一键拉起所有依赖。

我实测下来,用Docker Compose部署hindsight大概需要以下容器:

  • hindsight-core:核心记忆服务,跑MCP Server
  • postgres:存情景记忆和语义记忆的元数据
  • redis:做working memory的缓存和会话状态
  • qdrant:向量数据库,存记忆的embedding

这样一套下来,资源占用大概2GB内存起步,对于个人开发机来说完全可以接受。

3. 核心细节解析:记忆的写入、检索与衰减机制

3.1 记忆写入:不是所有对话都值得记

hindsight在写入记忆时做了一个关键判断:这条信息是否具有跨会话价值。如果只是“帮我查一下现在几点”这种一次性请求,记下来毫无意义。所以写入前会经过一个轻量级的价值评估。

我的做法是用一个小模型(比如7B级别的LLM)做快速分类,判断当前对话是否包含以下信号:

  • 用户明确表达的偏好(“我喜欢…”、“以后都…”)
  • 重要的实体信息(人名、项目名、截止日期)
  • 任务执行结果(成功/失败、关键参数)
  • 纠错反馈(“不对,应该是…”)

如果命中任一信号,就触发写入流程。写入时会把原始对话压缩成一条结构化记忆,格式大概是:

{ "type": "episodic", "timestamp": "2024-03-15T10:30:00Z", "summary": "用户要求报表打印使用A4横向", "entities": ["报表", "A4", "横向"], "source_session": "sess_abc123", "confidence": 0.92 }

这里confidence字段很关键,它表示这条记忆的可信度。如果用户后来纠正了,新记忆的confidence会更高,旧记忆会被标记为低置信度,检索时权重降低。

3.2 记忆检索:token的三个关键问题

检索是hindsight最核心的部分。我在设计检索逻辑时,参考了LLM token的经典三问:key我是谁、query我在找什么、value我能提供什么。

具体到hindsight的检索流程:

  1. Query理解:把当前用户输入和working memory拼在一起,用LLM提取检索意图。比如用户说“打印报表”,提取出的query是“打印偏好 报表格式”。
  2. 多路召回:一路走向量检索(qdrant),找语义相似的记忆;一路走关键词检索(postgres全文索引),找实体匹配的记忆;一路走时间衰减检索,找最近的相关记忆。
  3. 重排序:把三路召回的结果合并,用一个cross-encoder模型做精排,输出top-K条记忆。
  4. 注入prompt:把top-K条记忆格式化成自然语言,插入system prompt的指定位置。

这里有个细节:检索结果不是越多越好。我实测下来,top-5条记忆的注入效果最好,超过8条反而会干扰模型。而且每条记忆要带上时间戳和置信度,让模型自己判断是否采信。

3.3 记忆衰减:让过时的信息自然淡出

记忆衰减机制是hindsight区别于普通RAG的关键。普通RAG是静态的,存进去就不变。但Agent记忆需要动态更新——用户三个月前说“我喜欢红色”,今天说“我现在喜欢蓝色”,那红色这条记忆应该逐渐失效。

hindsight的衰减公式参考了艾宾浩斯遗忘曲线:

retention = exp(-λ * Δt) * confidence

其中λ是衰减系数,Δt是距今天数,confidence是写入时的置信度。检索时,retention低于阈值的记忆会被过滤掉。λ的取值需要根据场景调整:对于用户偏好类记忆,λ可以小一点(衰减慢);对于临时任务类记忆,λ可以大一点(衰减快)。

我一般把λ设成0.01到0.05之间,具体看业务对“记忆新鲜度”的要求。如果发现Agent总是引用过时信息,就把λ调大;如果发现Agent忘得太快,就把λ调小。

3.4 记忆归纳:从情景到语义的抽象

当同一类情景记忆积累到一定数量(比如5条以上),hindsight会触发归纳流程。用LLM把这些情景记忆总结成一条语义记忆,然后降低原始情景记忆的权重。

比如积累了5条“用户要求横向打印”的情景记忆后,归纳出一条语义记忆:“该用户偏好横向打印”。之后检索时,优先返回这条语义记忆,而不是5条重复的情景记忆。这样既节省了存储,又提高了检索效率。

归纳的触发时机很重要。太频繁会导致语义记忆不稳定,太稀疏又起不到压缩作用。我的经验是:同类情景记忆达到5条,且时间跨度超过3天,就触发一次归纳。

4. 实操部署:从零搭建一套hindsight记忆服务

4.1 环境准备与Docker Compose配置

先说环境要求。我是在Windows 11上跑的Docker Desktop,需要确保虚拟化支持已开启。如果你在安装Docker Desktop时遇到“virtualization support not detected”的报错,去BIOS里把Intel VT-x或AMD-V打开就行。Linux环境下直接装Docker Engine和Docker Compose插件即可。

下面是我用的docker-compose.yml,可以直接抄:

version: '3.8' services: hindsight-core: build: ./hindsight-core ports: - "8080:8080" environment: - POSTGRES_URL=postgresql://hindsight:hindsight@postgres:5432/hindsight - REDIS_URL=redis://redis:6379/0 - QDRANT_URL=http://qdrant:6333 - EMBEDDING_MODEL=text-embedding-3-small depends_on: - postgres - redis - qdrant postgres: image: postgres:16-alpine environment: - POSTGRES_USER=hindsight - POSTGRES_PASSWORD=hindsight - POSTGRES_DB=hindsight volumes: - pg_data:/var/lib/postgresql/data redis: image: redis:7-alpine volumes: - redis_data:/data qdrant: image: qdrant/qdrant:latest volumes: - qdrant_data:/qdrant/storage volumes: pg_data: redis_data: qdrant_data:

这里解释几个关键点。Postgres用来存记忆的元数据和全文索引,Redis做working memory的缓存,Qdrant存向量。三个存储各司其职,不要试图用一个数据库全搞定,否则后期扩展会很痛苦。

4.2 MCP Server的启动与配置

hindsight-core本身是一个MCP Server,启动后监听8080端口。Agent端需要配置MCP连接,以Claude Desktop为例,在配置文件里加上:

{ "mcpServers": { "hindsight": { "url": "http://localhost:8080/mcp", "transport": "sse" } } }

如果你用的是支持MCP的浏览器扩展或IDE插件,配置方式类似,核心就是填对URL和transport类型。SSE是Server-Sent Events,适合长连接场景;如果Agent端只支持stdio,那就需要用一个适配器把HTTP转成stdio。

启动顺序很重要:先起postgres、redis、qdrant,等它们健康检查通过后再起hindsight-core。我踩过一次坑,core起太快连不上postgres,导致初始化失败。后来在compose里加了depends_on和健康检查才解决。

4.3 记忆写入的代码实现

写入逻辑我用Python写了一个简化版,核心是调用LLM做价值评估和摘要生成:

import json from openai import OpenAI client = OpenAI() def evaluate_memory_value(conversation: str) -> dict: prompt = f"""判断以下对话是否包含值得长期记忆的信息。 值得记忆的信号包括:用户偏好、重要实体、任务结果、纠错反馈。 如果值得记忆,输出JSON格式:{{"worthy": true, "summary": "...", "entities": [...], "confidence": 0.0-1.0}} 如果不值得,输出:{{"worthy": false}} 对话内容: {conversation} """ response = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": prompt}], response_format={"type": "json_object"} ) return json.loads(response.choices[0].message.content)

这个函数返回worthy为true时,就把summary和entities写入postgres,同时把summary做embedding后写入qdrant。注意confidence字段要让LLM自己评估,不要固定值,否则后期衰减机制会失真。

4.4 记忆检索的完整流程

检索的代码稍微复杂一点,需要协调三路召回:

def search_memories(query: str, top_k: int = 5) -> list: # 第一路:向量检索 query_embedding = get_embedding(query) vector_results = qdrant.search( collection_name="memories", query_vector=query_embedding, limit=top_k * 2 ) # 第二路:关键词检索 keyword_results = postgres.execute( "SELECT * FROM memories WHERE summary ILIKE %s OR entities && %s LIMIT %s", (f"%{query}%", extract_entities(query), top_k * 2) ) # 第三路:时间衰减检索 recent_results = postgres.execute( "SELECT *, exp(-0.03 * EXTRACT(DAY FROM NOW() - timestamp)) * confidence AS retention " "FROM memories WHERE retention > 0.3 ORDER BY retention DESC LIMIT %s", (top_k * 2,) ) # 合并去重后重排序 merged = deduplicate(vector_results + keyword_results + recent_results) reranked = rerank(query, merged, top_k) return reranked

这里的关键是retention的计算,我用了0.03作为λ值,你可以根据业务调整。重排序我用了一个轻量级的cross-encoder,如果资源有限,也可以直接用RRF(Reciprocal Rank Fusion)做简单融合。

4.5 与Agent框架的集成

最后一步是把hindsight接入Agent。以LangChain为例,在Agent执行前调用memory_search,把结果注入system prompt:

def build_system_prompt(base_prompt: str, user_input: str) -> str: memories = search_memories(user_input, top_k=5) memory_text = "\n".join([ f"- [{m['timestamp']}] {m['summary']} (置信度: {m['confidence']})" for m in memories ]) return f"""{base_prompt} 以下是与当前任务相关的历史记忆,供参考: {memory_text} 注意:如果记忆与当前任务无关,请忽略。 """

Agent执行完后,调用memory_write把本轮对话写入。整个流程对Agent框架透明,换框架只需要改MCP配置。

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

5.1 记忆检索不准确怎么办

这是最常见的问题。我遇到过一次,用户问“帮我订机票”,检索出来的却是三个月前“帮我查天气”的记忆。排查后发现是embedding模型对“机票”和“天气”的向量区分度不够。

解决办法有三个:第一,换一个更强的embedding模型,比如从text-embedding-ada-002换成text-embedding-3-large;第二,在检索前加一层query改写,用LLM把“帮我订机票”改写成“机票预订 航班 出行”,提高检索精度;第三,调整重排序模型的权重,让关键词匹配的权重更高。

我最后用的是组合方案:query改写加关键词权重提升,检索准确率从60%左右提到了85%以上。

5.2 Docker网络不通的排查思路

Docker Compose部署时,容器间网络不通是高频问题。我遇到过一次hindsight-core连不上qdrant,报错是connection refused。排查步骤:

  1. 先docker compose ps看所有容器是否正常运行
  2. 进core容器docker exec -it hindsight-core sh,用ping qdrant测试网络
  3. 如果ping不通,检查compose文件里是否在同一个network下
  4. 如果ping通但连不上端口,检查qdrant的端口映射和防火墙

最后发现是qdrant容器启动慢,core启动时qdrant还没ready。解决办法是在compose里加healthcheck,让core等qdrant健康后再启动。

5.3 记忆写入过多导致token爆炸

初期我没做价值评估,所有对话都写入记忆。结果跑了一周,检索时返回的记忆条数太多,注入prompt后token直接超限。

后来加了价值评估和归纳机制,情况好转很多。另外我还加了一个硬限制:每次检索最多返回5条记忆,每条记忆的summary不超过100字。这样即使记忆库很大,注入的token量也是可控的。

5.4 常见问题速查表

问题现象可能原因排查方法解决方案
检索结果不相关embedding区分度低检查query和记忆的向量相似度换embedding模型或加query改写
容器间连接失败网络配置错误docker exec进容器ping测试检查network配置和healthcheck
token超限记忆注入过多统计每次注入的token数限制返回条数和summary长度
记忆不更新衰减系数太小检查retention计算公式调大λ值或降低旧记忆confidence
归纳不触发阈值设置过高检查同类记忆计数降低触发阈值或手动触发

5.5 几个我踩过的坑

第一个坑是时间戳时区问题。我一开始用UTC存时间,但检索时用本地时间比较,导致衰减计算偏差了8小时。后来统一用UTC,只在展示时转本地时间。

第二个坑是并发写入冲突。多个Agent同时写入记忆时,postgres出现了死锁。解决办法是给写入操作加一个Redis分布式锁,或者用消息队列串行化写入。

第三个坑是embedding模型版本不一致。我中途换了一次embedding模型,但旧记忆的向量还是用旧模型生成的,导致检索时新旧向量不在同一空间。后来加了一个迁移脚本,把所有旧记忆重新embedding了一遍。

6. 记忆安全与防御:a-memguard思路的借鉴

Agent记忆有一个容易被忽视的风险:记忆投毒。如果攻击者能往记忆库里写入恶意记忆,比如“用户说所有文件都可以删除”,那Agent后续的行为就可能被操控。虽然hindsight本身是内部服务,但如果MCP接口暴露在外,就需要考虑防御。

a-memguard是一个针对LLM Agent记忆的前瞻性防御框架,核心思路是在记忆写入前做安全审查,在记忆检索后做一致性校验。我在hindsight里借鉴了它的两个机制:

第一,写入审查。所有记忆写入前,用一个安全分类器判断是否包含危险指令。如果命中,直接拒绝写入并告警。

第二,检索校验。检索出的记忆在注入prompt前,用LLM做一次一致性检查,判断这些记忆是否与当前任务冲突。如果冲突,降低权重或直接过滤。

这两个机制增加了一点延迟,但安全性提升明显。对于生产环境的Agent,我觉得是值得的。

7. 后续扩展方向:从hindsight到更完整的记忆生态

hindsight目前解决的是单Agent的记忆问题。如果扩展到多Agent协作场景,还需要考虑记忆共享和权限控制。比如Agent A的记忆是否对Agent B可见,不同用户的记忆如何隔离。

另一个方向是记忆的可视化。我现在用Grafana加了一个简单的面板,展示记忆库的增长曲线、检索命中率、衰减分布等指标。后续想做一个记忆图谱,把实体和记忆的关系可视化出来,方便调试和优化。

还有一个有意思的方向是记忆的主动遗忘。现在衰减是被动的,等时间到了自然淡出。但如果用户明确说“忘掉之前关于X的所有信息”,就需要一个主动遗忘接口,批量删除相关记忆。这个在隐私合规场景下会很有用。

我个人在实际操作中的体会是,Agent记忆这件事,存储和检索只是基础,真正的难点在于判断什么该记、什么该忘、什么时候该想起来。hindsight这套框架给了我一个可落地的起点,但具体参数和策略还需要根据业务场景反复调优。如果你也在做类似的事情,建议先从最简单的版本跑起来,再逐步加机制,不要一上来就追求大而全。

返回列表