ARTICLE DETAIL

资讯详情

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

hindsight 实战:用 Docker 与 MCP 构建 Agent 长期记忆系统

hindsight 实战:用 Docker 与 MCP 构建 Agent 长期记忆系统 1. 从“hindsight”说起为什么我们需要给 Agent 装上一双“后视之眼”第一次看到 “hindsight” 这个词是在一个做 LLM Agent 的朋友群里。有人丢了一张截图说他们的 Agent 在连续对话到第 40 轮之后开始“胡言乱语”明明前面已经确认过的订单号后面又自己编了一个。底下有人回了一句“这不就是典型的没有 hindsight 吗” 那一刻我突然意识到这个词在 Agent Memory 这个圈子里已经从一个普通的英文单词变成了一个具体的技术隐喻——Agent 需要一种“事后回看”的能力把过去发生过的事情重新调取、重新理解、重新用起来。hindsight 这个项目标题本身很简洁但它背后指向的东西一点都不简单。它要解决的核心问题是LLM 驱动的 Agent 在长周期任务中如何稳定地记住、检索、更新和遗忘信息。这不是一个“加个向量数据库”就能糊弄过去的事情。我见过太多团队一开始用最简单的 conversation buffer 塞进 context window跑到十几轮就开始丢信息后来换成向量检索又发现检索出来的东西和当前任务不相关再后来上了所谓的 memory framework结果 Agent 开始把用户三周前随口说的一句玩笑话当成事实依据。这些坑本质上都是因为把“记忆”当成了一个存储问题而不是一个认知问题。hindsight 的价值在于它把 Agent Memory 从一个“附加组件”提升成了一个有主动性的认知层。它不只是被动地存和取而是会在任务执行过程中主动判断哪些信息值得记、什么时候该回忆、回忆出来的东西要不要信、信了之后要不要更新原来的记忆。这套逻辑和人类做复杂决策时的“后见之明”非常像——你不是把所有经历都平铺在脑子里而是会在需要的时候把相关的经验调出来重新评估然后指导下一步行动。这篇文章适合谁看如果你正在做 LLM Agent 相关的产品或者你在用 Dify、MCP 这类工具搭建自己的智能体工作流又或者你只是对“Agent 怎么才能不健忘”这个问题感兴趣那接下来的内容应该能给你一些可以直接抄作业的思路。我会从整体设计、核心机制、实操落地、问题排查几个角度把 hindsight 这个方向上的关键点拆开讲清楚。里面会涉及 Docker 部署、MCP 协议对接、向量检索策略、记忆更新逻辑这些具体的东西也会分享一些我在实际项目里踩过的坑和总结出来的参数经验。2. 整体设计思路hindsight 不是数据库是 Agent 的“记忆调度器”2.1 为什么传统的 RAG 方案在 Agent Memory 场景下会失灵很多人第一次做 Agent Memory 的时候第一反应就是上 RAG把历史对话切块、embedding、存向量库、每次用户提问就去检索 top-k。这个方案在知识库问答场景下没问题但放到 Agent 的长周期任务里很快就会暴露三个致命问题。第一个问题是时间维度丢失。向量检索本质上是语义相似度匹配它不关心“这件事是什么时候发生的”。Agent 在第三天需要回忆第一天确认过的某个参数向量检索可能会因为语义相似度把第二天讨论过的另一个相似参数排到前面。结果就是 Agent 拿了一个“看起来很像但其实是错的”记忆去执行任务。我实测过一个客服 Agent 的场景用户第一天说“我的收货地址是 A”第二天说“我搬家了新地址是 B”第三天问“我的地址是哪个”。纯向量检索有相当概率把 A 和 B 同时召回然后 Agent 就开始纠结甚至把两个地址拼在一起。第二个问题是记忆没有优先级和置信度。所有存进去的东西都是平等的检索出来就当成事实用。但现实中用户随口说的一句“我可能下周会出差”和明确确认的“我的发票抬头是 XX 公司”权重完全不一样。没有置信度机制Agent 就会把猜测当事实把过期信息当当前状态。第三个问题是没有主动遗忘和更新。向量库只会越存越多检索结果越来越嘈杂。一个跑了三个月的 Agent它的记忆库里可能堆了几万条碎片每次检索都要在里面捞不仅慢而且准召率会持续下降。人类大脑会遗忘、会合并、会用新记忆覆盖旧记忆但朴素的 RAG 方案完全没有这个能力。hindsight 这个方向上的设计本质上就是在解决这三个问题。它把记忆分成了不同的类型给每条记忆打上了时间戳、置信度、来源标记并且引入了一个“记忆调度”的逻辑决定什么时候该写、什么时候该读、什么时候该更新、什么时候该忘。2.2 hindsight 的核心分层工作记忆、情景记忆、语义记忆我在实际搭建 hindsight 类系统的时候习惯把 Agent 的记忆分成三层这个分层参考了认知科学里对人类记忆的分类但在工程上做了简化保证可落地。工作记忆Working Memory是最短期的基本上就是当前任务上下文里的那几轮对话和工具调用结果。它的生命周期很短任务结束就清掉。这一层不需要向量检索直接放在 context window 里就行关键是控制好 token 预算。我的经验是工作记忆不要超过 context window 的 40%剩下的要留给系统提示、工具定义和检索回来的长期记忆。情景记忆Episodic Memory是中期记忆记录的是“在什么时间、什么场景下、发生了什么事”。比如“用户在 3 月 5 日确认了订单号 12345”、“Agent 在 3 月 6 日调用支付接口失败了一次”。这一层需要带时间戳和事件类型检索的时候要结合时间衰减和事件相关性。情景记忆是 hindsight 最核心的部分因为它直接决定了 Agent 能不能“回看”过去的具体经历。语义记忆Semantic Memory是长期记忆存储的是从多次情景中抽象出来的稳定知识。比如“这个用户偏好用顺丰快递”、“这个项目的代码规范要求用 4 空格缩进”。语义记忆不绑定具体时间点但需要定期从情景记忆中归纳更新。这一层的更新频率低但一旦写入置信度要高。这三层记忆不是孤立的它们之间有一个流转机制工作记忆里的重要信息会被抽取成情景记忆情景记忆积累到一定数量后会被归纳成语义记忆语义记忆在检索时优先级最高但如果和当前情景冲突会触发更新流程。这个流转机制就是 hindsight 区别于普通 RAG 的关键。2.3 为什么选择 MCP Docker 作为落地底座在实际部署 hindsight 类系统的时候我强烈建议用 MCP 协议来做工具和记忆层的解耦用 Docker 来做环境隔离和快速复现。这两个选择背后有很实际的考虑。MCP 协议的好处是它把“记忆服务”变成了一个标准的工具接口。Agent 不需要知道记忆是怎么存的、怎么检索的它只需要调用memory_write、memory_search、memory_update这几个 MCP tool 就行。这样一来你可以随时替换底层的向量库、换 embedding 模型、调整检索策略而不用改 Agent 的核心逻辑。我试过在 Dify 里通过 MCP 对接自建的记忆服务整个切换过程只改了配置没动一行工作流代码。Docker 的好处更直接记忆服务通常依赖向量数据库、embedding 服务、可能还有 Redis 做缓存这些组件的版本和配置很容易互相打架。用 Docker Compose 把整套东西打包起来换一台机器docker compose up就能跑省去了大量环境调试的时间。而且 hindsight 类系统通常需要做实验比如对比不同 embedding 模型的效果、调整检索参数Docker 的隔离性让你可以同时跑多套配置互不干扰。注意如果你在 Windows 上装 Docker Desktop遇到 “Virtualization support not detected” 这个报错大概率是 BIOS 里的虚拟化选项没开。重启进 BIOS找到 Intel VT-x 或 AMD-V启用就行。这个坑我见过太多次了不是 Docker 的问题是系统层面的开关没打开。3. 核心机制拆解hindsight 怎么写、怎么读、怎么忘3.1 记忆写入不是所有东西都值得记hindsight 的第一个关键决策点是什么信息应该被写入长期记忆。如果每轮对话都写记忆库会迅速膨胀检索质量断崖式下降。如果写得太少Agent 又会显得“没记性”。我的经验是用一套打分机制来决定是否写入而不是简单粗暴地全存。打分维度包括信息密度这句话是否包含具体的事实、参数、偏好、任务相关性是否和当前任务目标直接相关、时效性是临时状态还是持久状态、用户明确性是用户明确确认的还是 Agent 推测的。每个维度给一个 0 到 1 的分数加权求和后超过阈值才写入。举个例子用户说“帮我查一下明天北京的天气”这句话的信息密度低、时效性短不应该写入长期记忆。但如果用户说“我以后查天气都默认用摄氏度”这就是一个明确的偏好应该写入语义记忆。再比如 Agent 自己推测“用户可能喜欢靠窗座位”这个置信度低可以先放在工作记忆里等用户后续确认了再升级为情景记忆。写入的时候还要带上元数据时间戳、来源用户说的还是 Agent 推的、置信度、过期时间如果是临时信息。这些元数据在后续检索和更新的时候会起到关键作用。3.2 记忆检索时间衰减 语义相关 置信度加权检索是 hindsight 最考验工程能力的地方。纯向量相似度不够用我通常会用三路信号加权语义相似度、时间衰减因子、置信度。语义相似度就是常规的 embedding 余弦相似度这个占大头比如 60% 权重。时间衰减因子是根据记忆的时间戳算出来的越新的记忆权重越高但衰减曲线不是线性的。我一般用指数衰减半衰期设成 7 天左右也就是说一周前的记忆权重降到一半。但这个半衰期要根据场景调如果是长期陪伴类 Agent半衰期可以设长一点比如 30 天如果是任务型 Agent7 天甚至 3 天都合理。置信度是第三个信号占 20% 左右。用户明确确认的信息置信度设 1.0Agent 推测的设 0.5从第三方工具拿到的设 0.8。检索的时候最终得分是这三项的加权和然后取 top-k。k 的值不要太大我实测下来 5 到 8 比较合适太多会引入噪声太少会漏掉关键信息。还有一个细节检索时要考虑记忆的类型。如果当前任务是“查订单状态”那情景记忆的优先级应该高于语义记忆如果当前任务是“推荐商品”那语义记忆里的用户偏好应该优先。这个可以通过在检索时给不同类型的记忆加不同的偏置来实现。3.3 记忆更新与遗忘让 Agent 学会“改主意”hindsight 最容易被忽略但最重要的能力是记忆更新。当新信息和旧记忆冲突时Agent 不能简单地把两条都留着也不能粗暴地覆盖而是要走一个冲突解决流程。我的做法是检索到相关旧记忆后先判断新旧信息是否冲突。如果不冲突就追加新记忆如果冲突就比较两者的时间戳和置信度。新信息时间更新且置信度不低于旧信息就用新信息替换旧记忆并把旧记忆标记为“已废弃”而不是直接删除保留审计线索。如果新信息置信度低就把它作为“待确认”状态存起来等后续更多证据再决定。遗忘机制也很关键。我通常设两种遗忘基于时间的遗忘和基于容量的遗忘。基于时间的遗忘是给每条记忆设一个 TTL到期后如果没被访问过就降权或删除。基于容量的遗忘是当某一类记忆超过阈值时把最旧、访问频率最低的淘汰掉。这两个机制配合使用能让记忆库保持在一个健康的规模。实操心得不要直接物理删除记忆。我习惯用软删除把废弃记忆的status字段改成archived检索时默认过滤掉。这样万一后面发现误删了还能恢复。而且这些废弃记忆可以用来做离线分析看看 Agent 在哪些地方容易判断失误。4. 实操落地用 Docker MCP 搭一套可跑的 hindsight 记忆服务4.1 环境准备与 Docker Compose 编排先说一下我用的技术栈FastAPI 做记忆服务接口Qdrant 做向量存储Redis 做短期缓存PostgreSQL 存元数据和审计日志。embedding 模型用 BGE-M3因为它对中文和英文的支持都比较均衡而且可以本地部署不依赖外部 API。Docker Compose 的编排文件大概长这样version: 3.9 services: memory-api: build: ./memory-api ports: - 8100:8100 environment: - QDRANT_HOSTqdrant - REDIS_HOSTredis - PG_HOSTpostgres depends_on: - qdrant - redis - postgres qdrant: image: qdrant/qdrant:latest ports: - 6333:6333 volumes: - qdrant_data:/qdrant/storage redis: image: redis:7-alpine ports: - 6379:6379 postgres: image: postgres:16-alpine environment: - POSTGRES_PASSWORDmemory_secret - POSTGRES_DBhindsight volumes: - pg_data:/var/lib/postgresql/data volumes: qdrant_data: pg_data:这个编排的关键点是Qdrant 负责向量检索PostgreSQL 负责结构化元数据Redis 负责工作记忆的快速读写。三者各司其职不要试图用一个组件干所有事。我见过有人把元数据也塞进 Qdrant 的 payload 里短期没问题但一旦要做复杂的过滤和聚合查询就会很痛苦。启动的时候先docker compose up -d qdrant redis postgres等这三个基础服务健康了再启动 memory-api。如果你在 Ubuntu 上装 Docker记得把当前用户加到 docker 组里不然每次都要 sudo。Windows 用户如果用 Docker Desktop建议把 WSL2 后端打开性能会比 Hyper-V 好不少。4.2 MCP Server 的实现与工具定义MCP 这一层我把它做成一个独立的 server暴露三个核心 toolmemory_write、memory_search、memory_update。每个 tool 的入参和出参都要定义清楚这样 Agent 调用的时候才不会懵。memory_write的入参包括content记忆内容、memory_typeepisodic 或 semantic、confidence置信度、source来源、ttl过期时间可选。出参返回memory_id和写入状态。memory_search的入参包括query查询文本、top_k返回条数、memory_type_filter可选过滤记忆类型、time_range可选时间范围。出参返回一个记忆列表每条包含content、score、timestamp、confidence。memory_update的入参包括memory_id、new_content、reason。出参返回更新后的状态。在 Dify 里对接的时候你需要在 MCP 配置里填上 server 的地址和认证信息。如果是本地开发可以用http://localhost:8100/mcp。如果是远程建议加一层 token 认证不要裸奔。我见过有人把 MCP server 直接暴露在公网上结果被扫到之后疯狂写入垃圾数据把记忆库搞崩了。注意MCP 协议目前还在演进中不同工具对 MCP 的支持程度不一样。Dify 对 MCP 的支持相对成熟但如果你用的是其他平台建议先确认它支持的 MCP 版本和你的 server 是否匹配。我踩过一次坑server 用的是较新的协议版本结果平台只支持旧版调了半天才发现是版本不兼容。4.3 记忆写入与检索的代码实现要点记忆写入的核心逻辑我把它拆成三步抽取、打分、落库。抽取是从对话或工具结果里提取出候选记忆片段打分是用前面说的四个维度算分落库是写入 Qdrant 和 PostgreSQL。def write_memory(content, memory_type, confidence, source, ttlNone): # 第一步计算信息密度和任务相关性 density_score calculate_density(content) relevance_score calculate_relevance(content, current_task) # 第二步加权求和 final_score ( density_score * 0.3 relevance_score * 0.3 confidence * 0.25 source_weight(source) * 0.15 ) # 第三步超过阈值才写入 if final_score 0.6: return {status: skipped, reason: score too low} # 生成 embedding vector embed(content) # 写入 Qdrant qdrant_client.upsert( collection_nameagent_memory, points[{ id: memory_id, vector: vector, payload: { content: content, memory_type: memory_type, confidence: confidence, source: source, timestamp: now(), ttl: ttl } }] ) # 写入 PostgreSQL 做审计 pg_insert_memory(memory_id, content, memory_type, confidence, source) return {status: written, memory_id: memory_id}检索的时候我通常会用 Qdrant 的search接口拿到语义相似的候选然后在应用层做时间衰减和置信度加权。Qdrant 本身支持 payload 过滤所以时间范围和记忆类型的过滤可以下推到向量库层面减少应用层的计算量。def search_memory(query, top_k5, memory_typeNone, time_rangeNone): query_vector embed(query) # 构建过滤条件 filters {} if memory_type: filters[memory_type] memory_type if time_range: filters[timestamp] {gte: time_range[0], lte: time_range[1]} # 向量检索多召回一些候选 candidates qdrant_client.search( collection_nameagent_memory, query_vectorquery_vector, limittop_k * 3, query_filterfilters ) # 应用层重排序 scored [] for c in candidates: semantic_score c.score time_decay calculate_time_decay(c.payload[timestamp]) confidence c.payload[confidence] final_score ( semantic_score * 0.6 time_decay * 0.2 confidence * 0.2 ) scored.append((final_score, c)) scored.sort(keylambda x: x[0], reverseTrue) return [s[1] for s in scored[:top_k]]这段代码里calculate_time_decay用的是指数衰减半衰期设成 7 天。你可以根据自己场景调整这个参数但不要设得太短否则 Agent 会显得“只记得最近的事”。4.4 和 Dify 工作流的对接方式在 Dify 里你可以把 MCP server 注册成一个工具然后在工作流里通过工具节点调用。我的做法是在对话开始前先调一次memory_search把相关的长期记忆注入到系统提示里在对话过程中如果检测到值得记的信息调memory_write如果发现新信息和旧记忆冲突调memory_update。这里有个细节不要每轮对话都调 memory_search。太频繁的检索会增加延迟而且很多轮对话其实不需要长期记忆。我的策略是只在任务切换、用户提到过去的事情、或者 Agent 需要做决策的时候才触发检索。这个判断可以交给一个轻量的分类器或者直接用规则匹配。Dify 的工作流里你可以用一个条件分支节点来判断是否触发记忆检索。条件可以包括用户输入里是否包含时间词“上次”、“之前”、“以前”、是否包含指代词“那个”、“它”、当前任务是否和之前的任务相关。这些规则虽然简单但实测下来能过滤掉大部分不必要的检索。5. 常见问题与排查技巧实录5.1 记忆检索不准先查 embedding再查权重检索不准是最常见的问题。我的排查顺序是先看 embedding 模型是否适合当前语言和领域再看检索权重是否合理最后看记忆本身的质量。embedding 模型这块如果你主要做中文场景BGE-M3 或者 text-embedding-3-large 都不错。但要注意有些模型对长文本的表示能力会下降如果你的记忆片段很长建议先做摘要再 embedding。我试过把一段 500 字的对话直接 embedding检索效果明显不如先摘要成 100 字再 embedding。权重这块很多人直接用默认的语义相似度不加时间衰减和置信度。这在短期任务里没问题但长期任务里一定会出问题。你可以做一个简单的 A/B 测试一组用纯语义检索一组用加权检索跑同样的测试用例看哪组的准确率高。我实测下来加权检索在长周期任务里的准确率能高出 20% 到 30%。记忆质量这块要检查写入时的打分阈值是不是太低了。如果什么垃圾都往里写检索再准也没用。建议定期做一次记忆库的清理把低置信度、长时间未访问的记忆归档掉。5.2 Docker 网络不通容器间通信的排查思路Docker 网络问题我遇到过好几次最常见的症状是 memory-api 启动成功但连不上 Qdrant 或 PostgreSQL。排查步骤是这样的先docker compose ps看所有容器的状态确认没有容器在反复重启。然后docker compose logs memory-api看日志通常会直接告诉你连接超时或者 DNS 解析失败。如果是 DNS 解析失败检查你的 compose 文件里服务名和代码里用的 host 是否一致。Docker Compose 默认会创建一个网络服务之间可以用服务名互相访问但前提是它们在同一个网络里。如果日志显示连接被拒绝可能是目标服务还没启动完成。加一个depends_on配合健康检查或者用wait-for-it脚本让 memory-api 等基础服务就绪后再启动。我习惯在 memory-api 的启动脚本里加一个重试逻辑连不上就等 2 秒重试最多重试 10 次。这样比硬等要可靠。还有一种情况是端口冲突。如果你本机已经装了 PostgreSQL 或者 Redis它们会占用 5432 和 6379 端口导致容器启动失败。解决办法是把容器端口映射到其他端口比如5433:5432然后代码里连 5433。5.3 记忆冲突处理什么时候该覆盖什么时候该并存记忆冲突是 hindsight 里最需要小心处理的地方。我的原则是事实类信息可以覆盖偏好类信息要并存推测类信息要标记。事实类信息比如地址、订单号、日期这些有明确对错新信息确认后直接覆盖旧的。偏好类信息比如“用户喜欢什么风格”可能会随时间变化也可能同时存在多个偏好这种就不要覆盖而是并存检索的时候都返回让 Agent 自己判断。推测类信息比如 Agent 猜的“用户可能想要退款”这种要标记为低置信度不能直接当事实用。覆盖的时候我建议保留旧记忆的审计记录把status改成superseded并记录被哪条新记忆替代了。这样后面如果发现覆盖错了还能追溯。5.4 性能优化检索延迟从 800ms 降到 120ms 的实操记忆检索的延迟主要来自三块embedding 计算、向量检索、应用层重排序。我的优化顺序是先缓存 embedding再优化向量索引最后减少重排序的计算量。embedding 缓存这块对于重复的查询文本可以直接从 Redis 拿缓存结果。我设的 TTL 是 1 小时命中率大概在 40% 左右能省不少时间。向量索引这块Qdrant 支持 HNSW 索引建索引的时候把m参数调到 16ef_construct调到 100检索的时候ef调到 64能在召回率和速度之间取得不错的平衡。应用层重排序这块如果候选集很大可以用批量计算代替循环。另外时间衰减和置信度的计算都是简单的数学运算可以提前算好缓存起来。我实测下来这套优化能把检索延迟从 800ms 降到 120ms 左右对于大多数 Agent 场景已经够用了。问题现象可能原因排查方法解决方案检索结果不相关embedding 模型不匹配换模型做对比测试换用 BGE-M3 或领域微调模型检索结果过时没有时间衰减检查检索权重配置加入时间衰减因子半衰期 7 天记忆库膨胀写入阈值太低统计每日写入量提高打分阈值定期归档容器间连接失败网络配置错误看日志和容器状态检查服务名、端口、健康检查记忆冲突没有冲突解决逻辑检查更新流程按事实/偏好/推测分类处理6. 一些关于 hindsight 的延伸思考hindsight 这个方向我觉得后面还有不少可以深挖的地方。一个是记忆的可解释性现在 Agent 检索到一条记忆就用但用户不知道它为什么用这条。如果能给每条检索结果附上一个“为什么相关”的解释调试和信任度都会好很多。另一个是跨 Agent 的记忆共享多个 Agent 协作的时候怎么让它们共享一部分记忆同时又保持各自的私有记忆这个在 MCP 协议下是有可能实现的。还有一个我比较关注的点是记忆的主动遗忘策略。现在大部分方案都是被动遗忘等 TTL 到期或者容量满了才删。但人类大脑会主动遗忘一些痛苦或无关紧要的事情Agent 是不是也应该有这种能力比如用户明确说“不要再提这件事了”Agent 应该能把相关记忆标记为不可检索。这个在隐私敏感的场景下会很有用。我在实际项目里最大的体会是Agent Memory 不是一个纯技术问题它涉及到产品设计、用户体验、隐私合规多个层面。技术方案再漂亮如果用户觉得 Agent “记得太多”或者“记得不对”那这个记忆系统就是失败的。所以做 hindsight 类系统的时候一定要把“用户能不能控制记忆”这个维度考虑进去比如提供查看、编辑、删除记忆的接口。这个不是可选项是必选项。最后分享一个小技巧如果你在调试记忆检索的效果可以做一个“记忆回放”工具把 Agent 的历史对话和每次检索到的记忆都可视化出来。这样你能很直观地看到 Agent 在哪个时间点用了哪条记忆判断对不对。我靠这个工具发现了好几个隐蔽的 bug比如时间戳时区不对导致衰减计算错误、置信度字段在某些路径下没被正确赋值。这种问题看日志很难发现但可视化之后一目了然。
返回列表