尧图精选

Hindsight:让LLM Agent从失败中学习的记忆系统设计与MCP实战

🕒 发布时间:2026/10/1 5:06:18 📁 来源:尧图网络
1. 从 hindsight 这个词说起为什么它值得单独拿出来做hindsight 这个词本身的意思很简单——事后的聪明、后见之明。放在日常生活里就是那句老话早知道我就……。但把它放到 LLM Agent 的语境里它立刻变成了一个非常具体、非常工程化的命题一个 Agent 在完成一轮任务之后能不能把当时不知道、事后才明白的信息沉淀下来变成下一次决策的依据。我最初注意到这个方向是因为一个很实际的问题。我手上跑着几个基于 MCP 协议的 Agent日常帮我在本地 Docker 环境里做一些自动化的事情——查数据库、跑脚本、整理文件、调外部工具。跑得多了就发现一个规律同一个坑Agent 会反复踩。比如某次它调用一个工具时参数格式传错了报错重试改对了。下一次遇到同样的任务它还是先传错一次。因为它的上下文窗口在会话结束后就清空了它没有上次我是怎么解决的这个记忆。这就是 hindsight 要解决的核心问题。它不是简单的对话历史存储也不是把聊天记录塞进向量库做 RAG 检索那么粗暴。它关注的是决策层面的经验沉淀哪些判断是对的哪些是错的错在哪里下次遇到类似情况应该怎么调整。换句话说它给 Agent 装了一个复盘机制。这个项目适合谁来参考三类人。第一类是正在做 Agent 应用的开发者尤其是用 MCP 协议接工具、用 Docker 做本地部署的那批人你们大概率已经遇到了记忆管理的瓶颈。第二类是对 LLM 记忆机制感兴趣的研究型选手想搞清楚 working memory、episodic memory、semantic memory 在工程上怎么落地。第三类是纯粹被Agent 老是犯同样的错折磨过的实践者你想找一个系统性的解法而不是每次手动往 prompt 里塞提示。我下面要拆的就是 hindsight 这个思路背后的完整设计逻辑、核心实现要点、实操流程以及我在实际搭建过程中踩过的坑。内容会涉及 MCP 协议、Docker 部署、Agent 记忆分层、以及一些具体的参数设计。不管你是刚接触 Agent 开发还是已经跑过几轮项目应该都能从中拿到可以直接用的东西。2. 核心设计思路拆解hindsight 到底在记什么2.1 记忆不是日志是决策快照很多人做 Agent 记忆第一反应是存对话历史。用户说了什么Agent 回了什么工具返回了什么全部按时间顺序存下来。这个做法能用但效率极低。因为对话历史里 90% 的内容是噪音——寒暄、确认、中间过程的冗余输出。真正有价值的信息可能只占 5%。hindsight 的思路不一样。它不存发生了什么它存当时我基于什么信息做了什么判断结果如何。我把它叫做决策快照。一个决策快照至少包含四个字段情境特征当时面对的是什么任务类型、什么输入特征、什么工具可用。决策内容Agent 选择了哪个工具、传了什么参数、走了哪条推理路径。执行结果成功还是失败失败的话错误类型是什么。事后评估这个决策在事后看来是否合理有没有更优解。这四个字段里最有价值的是第四个。因为前三个是客观记录第四个是主观反思。而 hindsight 的精髓就在于它强迫 Agent 在任务结束后做一次反思把反思结果结构化存储供未来检索。注意反思这一步不能交给同一个上下文窗口里的 Agent 自己做因为它会有确认偏误——它会倾向于认为自己的决策是合理的。实践中更可靠的做法是另起一个轻量的评估调用用不同的 prompt 模板专门做批判性复盘。2.2 为什么选择 MCP 作为记忆的接入层MCP 协议在这里的角色很关键。它是一个标准化的工具调用协议让 Agent 可以通过统一的接口访问外部能力。hindsight 把记忆系统本身也封装成一个 MCP Server这意味着任何支持 MCP 的 Agent 框架都能直接接入不需要改 Agent 的核心代码。这个设计选择的好处是解耦。记忆系统独立运行Agent 通过 MCP 调用它就像调用任何一个普通工具一样。你可以今天用这个 Agent 框架明天换另一个记忆层不用动。而且 MCP Server 可以跑在 Docker 容器里和 Agent 的运行环境隔离数据持久化也更好管理。我实测下来这种架构的另一个好处是可以单独调试记忆系统。你可以不启动 Agent直接用 MCP 客户端去调记忆服务的接口测试存储、检索、更新这些操作是否正常。这在排查问题时非常省事。2.3 三层记忆结构的工程落地hindsight 的记忆不是单一存储它分了三个层次这个分层直接对应认知科学里的记忆模型记忆层对应概念存储内容生命周期检索方式Working Memory工作记忆当前会话的临时上下文会话级结束即清直接读取Episodic Memory情景记忆具体任务的决策快照长期可衰减相似度检索Semantic Memory语义记忆从多个快照中提炼的通用规则长期稳定关键词语义混合Working Memory 最简单就是当前对话的上下文窗口管理。但 hindsight 在这里做了一个优化它不是把所有历史都塞进上下文而是根据当前任务动态加载相关的 Episodic Memory 摘要。这样既保证了信息的连续性又不会把上下文撑爆。Episodic Memory 是核心。每次任务结束后的决策快照都存在这里。存储时会给每个快照打上多维标签任务类型、工具名称、错误类型、成功标志等。检索时用向量相似度加标签过滤先粗筛再精排。Semantic Memory 是最高层。它不存具体案例存的是从多个案例中归纳出来的规则。比如当调用数据库查询工具时如果返回空结果先检查时间范围参数是否设置正确——这种规则就是从多次失败快照中提炼出来的。Semantic Memory 的更新频率低但一旦形成就很稳定可以直接注入到 Agent 的系统提示里。2.4 为什么不用纯 RAG 方案有人会问这不就是 RAG 吗把历史记录向量化检索相似内容注入上下文。表面上看确实像但有几个本质区别。纯 RAG 检索的是相似文本hindsight 检索的是相似决策情境。前者关注内容相似度后者关注决策结构的相似度。举个例子两个任务一个是查询用户表里最近七天的注册数另一个是查询订单表里最近三十天的成交额。文本相似度不高但决策情境高度相似——都是时间范围查询都可能遇到时间参数格式问题。hindsight 能检索到这种结构相似性纯 RAG 很难。另外hindsight 有反思和归纳机制纯 RAG 没有。RAG 只是把原始文本拿出来用hindsight 会把原始经验加工成规则。这个加工过程是它真正的价值所在。3. 核心细节解析与实操要点3.1 决策快照的数据结构设计数据结构设计是整个系统的地基这里偷懒后面会非常痛苦。我建议用 JSON 作为快照的序列化格式字段设计如下{ snapshot_id: uuid, timestamp: ISO8601, task_context: { task_type: database_query, input_summary: 查询最近七天注册用户数, available_tools: [mysql_query, redis_get, file_read] }, decision: { tool_selected: mysql_query, parameters: {sql: SELECT COUNT(*) FROM users WHERE created_at ?, params: [2024-01-01]}, reasoning_path: 用户要求时间范围查询选择关系型数据库工具 }, outcome: { status: success, error_type: null, error_message: null, execution_time_ms: 45 }, reflection: { is_optimal: true, alternative: null, lesson: 时间范围查询需确认时区设置 }, tags: [time_range, count_query, mysql] }这个结构里task_context和decision是检索的主要依据outcome和reflection是价值所在。tags是人工和自动结合生成的用于快速过滤。实操心得reflection字段不要留空。哪怕任务成功了也要写一句这个决策为什么是对的。因为成功经验同样需要沉淀否则 Agent 只知道什么不能做不知道什么应该做。3.2 反思环节的 Prompt 设计反思环节的质量直接决定整个系统的上限。我试过几种 prompt 模板最后稳定下来的版本大概是这个结构你是一个任务复盘专家。以下是一个 Agent 执行任务的完整记录 任务情境{task_context} 决策内容{decision} 执行结果{outcome} 请从以下角度进行批判性复盘 1. 这个决策在当时的信息条件下是否合理 2. 如果结果失败根本原因是什么是信息不足、工具选择错误、还是参数错误 3. 如果结果成功是否存在更优的决策路径 4. 提炼一条可复用的经验规则用一句话表述。 输出格式为 JSON包含 is_optimal、root_cause、alternative、lesson 四个字段。这个模板的关键在于它明确要求批判性。如果不强调这一点模型很容易给出这个决策基本合理这种和稀泥的结论。另外要求输出结构化 JSON 是为了后续自动处理避免每次都要解析自然语言。3.3 记忆检索的混合策略检索环节我踩过最大的坑是纯向量检索不够用。向量检索擅长语义相似但对精确匹配不敏感。比如我想找所有涉及 mysql_query 工具且失败类型是 timeout 的快照纯向量检索给不出准确结果。最后的方案是混合检索分三步走标签粗筛根据当前任务的 task_type 和可用工具先过滤出候选集。这一步用数据库索引速度极快。向量精排对候选集做向量相似度计算取 top-K。向量用的是任务情境和决策内容的拼接嵌入。规则加权对包含 Semantic Memory 规则命中的快照加权对时间较近的快照加权对失败案例适当加权因为失败教训比成功经验更值得记住。这个三步策略实测下来检索准确率比纯向量方案高出一大截。而且因为第一步就做了粗筛整体延迟反而更低。3.4 Docker 部署的关键配置记忆系统我建议独立部署用 Docker 容器跑。这样数据持久化、网络隔离、版本管理都清晰。核心的 docker-compose 配置大概是这样version: 3.8 services: hindsight-memory: image: hindsight-memory:latest container_name: hindsight-memory ports: - 8765:8765 volumes: - ./data/snapshots:/app/data/snapshots - ./data/vectors:/app/data/vectors - ./config:/app/config environment: - VECTOR_DB_TYPEchroma - SNAPSHOT_RETENTION_DAYS90 - REFLECTION_MODELgpt-4o-mini - EMBEDDING_MODELtext-embedding-3-small restart: unless-stopped networks: - agent-net networks: agent-net: driver: bridge几个关键点。volumes必须把快照数据和向量数据挂出来否则容器一删数据就没了。SNAPSHOT_RETENTION_DAYS控制快照保留天数太短会丢失长期经验太长会拖慢检索90 天是个比较平衡的值。REFLECTION_MODEL用轻量模型就行反思任务不需要太强的推理能力用大模型纯属浪费。注意如果你在 Windows 上跑 Docker Desktop挂载卷的路径要用绝对路径而且要注意文件权限问题。我遇到过容器内写入失败的情况最后发现是 Windows 文件系统的权限映射问题改成命名卷就解决了。4. 实操过程与核心环节实现4.1 环境准备与依赖安装先把基础环境搭起来。我假设你用的是 Ubuntu 或者 macOSWindows 的话建议用 WSL2 配合 Docker Desktop。第一步确认 Docker 和 Docker Compose 可用docker --version docker compose version如果 Docker 没装Ubuntu 上的标准流程是sudo apt update sudo apt install -y docker.io docker-compose-plugin sudo systemctl enable --now docker sudo usermod -aG docker $USER最后一行是把当前用户加入 docker 组避免每次都要 sudo。执行完要重新登录一次才生效。第二步准备项目目录结构mkdir -p hindsight/{data/snapshots,data/vectors,config,src} cd hindsight第三步拉取记忆系统的镜像。如果你是自己构建需要先写 Dockerfile如果用现成的直接 pull。我建议自己构建因为要改配置和加自定义逻辑。FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY src/ ./src/ COPY config/ ./config/ EXPOSE 8765 CMD [python, -m, src.server]requirements.txt里核心依赖是 MCP 的 Python SDK、向量库客户端、以及一个嵌入模型调用库。具体版本根据你用的向量库和模型服务来定。4.2 MCP Server 的核心接口实现记忆系统作为 MCP Server需要暴露几个核心工具接口。我用 Python 写一个简化版的骨架from mcp.server import Server from mcp.types import Tool, TextContent import json app Server(hindsight-memory) app.list_tools() async def list_tools(): return [ Tool( namestore_snapshot, description存储一个决策快照, inputSchema{ type: object, properties: { task_context: {type: object}, decision: {type: object}, outcome: {type: object} }, required: [task_context, decision, outcome] } ), Tool( nameretrieve_similar, description检索相似决策情境的历史快照, inputSchema{ type: object, properties: { current_context: {type: object}, top_k: {type: integer, default: 5} }, required: [current_context] } ), Tool( nameget_rules, description获取适用于当前任务的语义记忆规则, inputSchema{ type: object, properties: { task_type: {type: string} }, required: [task_type] } ) ]这三个接口对应了记忆系统的三个核心操作存、查、取规则。store_snapshot在任务结束后调用retrieve_similar在任务开始前调用get_rules在构建系统提示时调用。4.3 存储流程的完整实现存储流程不只是写数据库那么简单它包含反思生成、向量化、标签提取、规则更新四个步骤。我按顺序说。第一步接收原始快照。Agent 通过 MCP 调用store_snapshot传入任务情境、决策内容、执行结果。这时候还没有反思字段。第二步生成反思。系统内部调用反思模型用前面说的 prompt 模板生成反思内容。这一步是异步的不阻塞 Agent 的主流程。反思完成后回填到快照里。第三步向量化。把task_context和decision拼接成一段文本调用嵌入模型生成向量。向量维度根据模型定text-embedding-3-small是 1536 维。向量存到向量库和快照 ID 关联。第四步标签提取。从task_context.task_type、decision.tool_selected、outcome.error_type等字段自动提取标签。也可以让模型额外生成几个语义标签但要注意控制数量标签太多反而影响检索效率。第五步规则更新。检查 Semantic Memory 里是否已有相关规则。如果有看这条新快照是支持还是反驳这条规则。支持则增加置信度反驳则降低。如果没有相关规则且这条快照的教训足够通用就生成一条新规则。async def store_snapshot(task_context, decision, outcome): snapshot { snapshot_id: str(uuid.uuid4()), timestamp: datetime.now().isoformat(), task_context: task_context, decision: decision, outcome: outcome, reflection: None, tags: extract_tags(task_context, decision, outcome) } # 异步生成反思 asyncio.create_task(generate_reflection(snapshot)) # 向量化并存储 embedding embed(concat_context_decision(task_context, decision)) vector_db.add(snapshot[snapshot_id], embedding, snapshot[tags]) # 持久化快照 save_snapshot(snapshot) return {status: stored, snapshot_id: snapshot[snapshot_id]}4.4 检索流程与上下文注入检索发生在任务开始前。Agent 拿到用户请求后先构造一个current_context然后调retrieve_similar。async def retrieve_similar(current_context, top_k5): # 第一步标签粗筛 candidate_tags extract_tags_from_context(current_context) candidates vector_db.filter_by_tags(candidate_tags, limit50) # 第二步向量精排 query_embedding embed(current_context) scored [] for cid in candidates: score cosine_similarity(query_embedding, vector_db.get_embedding(cid)) scored.append((cid, score)) scored.sort(keylambda x: x[1], reverseTrue) # 第三步规则加权 top_results scored[:top_k] weighted [] for cid, score in top_results: snapshot load_snapshot(cid) weight 1.0 if snapshot[outcome][status] failure: weight * 1.2 # 失败案例加权 if is_recent(snapshot[timestamp], days30): weight * 1.1 # 近期案例加权 weighted.append((snapshot, score * weight)) weighted.sort(keylambda x: x[1], reverseTrue) return [format_for_context(s) for s, _ in weighted]检索结果不是直接塞进上下文要格式化。我用的格式是【历史经验参考】 情境查询最近七天注册用户数 决策使用 mysql_query 工具SQL 为 SELECT COUNT(*) FROM users WHERE created_at ? 结果成功 教训时间范围查询需确认时区设置这种格式简洁明了模型能快速抓住要点。不要塞太多条3 到 5 条足够太多会干扰当前任务的判断。4.5 语义规则的自动归纳规则归纳是 hindsight 最有意思的部分。它不是简单地把快照堆在一起而是从多个快照中提炼共性。实现逻辑是定期比如每天一次扫描新增的快照按task_type和error_type分组。如果某个分组下的失败案例超过阈值比如 3 次就触发规则生成。规则生成的 prompt 大概是以下是同一类任务下的多个失败案例 {snapshots} 请分析这些失败的共同原因提炼一条可复用的规则。 规则要求 1. 用一句话表述 2. 明确指出在什么情况下适用 3. 给出具体的操作建议 输出 JSON 格式{rule: ..., condition: ..., action: ...}生成的规则存到 Semantic Memory并关联到对应的task_type。下次 Agent 处理同类任务时get_rules接口会把这些规则返回注入到系统提示里。实操心得规则不要生成太多。我一开始没控制跑了一周生成了两百多条规则结果系统提示被撑爆了而且很多规则互相矛盾。后来加了去重和冲突检测把规则数量控制在每个 task_type 不超过 5 条效果反而更好。5. 常见问题与排查技巧实录5.1 记忆检索不准的排查思路这是最常见的问题。Agent 明明有相关经验但检索不出来。排查按这个顺序走先看标签是否匹配。检查extract_tags函数对当前任务的标签提取结果和快照存储时的标签做对比。如果标签体系不一致粗筛阶段就把正确结果过滤掉了。解决办法是统一标签生成逻辑最好用同一套规则或同一个模型。再看向量模型是否一致。存储和检索必须用同一个嵌入模型。我遇到过中途换模型的情况旧向量和新向量不在同一空间相似度计算完全失效。换模型必须重新向量化所有历史数据。最后看加权是否过度。失败案例加权 1.2 倍如果失败案例特别多可能会把真正相关的成功案例挤下去。加权系数要调不能拍脑袋定。问题现象可能原因排查方法解决措施检索结果完全不相关向量模型不一致检查存储和检索的模型配置统一模型重新向量化检索结果漏掉关键案例标签体系不匹配对比标签提取逻辑统一标签生成规则检索结果偏向失败案例加权系数过高查看加权后的排序降低失败案例权重检索延迟高候选集太大查看粗筛后的候选数量增加标签过滤条件5.2 Docker 环境下的网络问题MCP Server 跑在 Docker 里Agent 跑在宿主机上两者通信容易出问题。最常见的症状是 Agent 连不上记忆服务。先确认容器是否正常启动docker ps | grep hindsight docker logs hindsight-memory --tail 50如果容器在跑但连不上检查端口映射。docker-compose.yml里写的8765:8765表示宿主机 8765 映射到容器 8765。Agent 配置的地址应该是http://localhost:8765或者http://127.0.0.1:8765。如果 Agent 也跑在容器里那就不能用 localhost要用 Docker 网络里的服务名。比如http://hindsight-memory:8765。这个坑我踩过排查了半天才发现是网络命名的问题。注意Windows 上 Docker Desktop 的网络模式和 Linux 有差异。如果遇到virtualization support not detected这类启动错误先确认 BIOS 里虚拟化选项是否开启再确认 WSL2 是否正常安装。5.3 反思质量不稳定的处理反思环节偶尔会产出废话比如这个决策是合理的这种没有信息量的结论。原因通常是 prompt 不够具体或者模型太弱。我的处理办法是加 few-shot 示例。在 prompt 里放两三个高质量的反思样例让模型模仿。另外对反思结果做一次质量校验如果lesson字段长度小于 10 个字符或者包含合理正常这类空泛词汇就重新生成一次。还有一个技巧是换角度提问。不要问这个决策合理吗而是问如果让你重新做一次你会改变什么。后者更容易逼出具体内容。5.4 记忆膨胀的控制策略跑久了快照会越来越多检索变慢存储变大。控制策略有几个时间衰减超过一定天数的快照降低检索权重但不删除。因为老经验可能仍然有效只是优先级降低。去重合并如果多条快照的情境和决策高度相似合并成一条保留最新的结果和反思。规则替代当某个 task_type 下的规则已经稳定形成可以把对应的原始快照归档只保留规则。规则占用的存储和检索成本远低于原始快照。定期清理对outcome.status为 success 且reflection.is_optimal为 true 的快照如果超过 180 天没有被检索命中过可以安全删除。我现在的配置是保留 90 天全量快照90 天到 180 天的只保留失败案例和规则180 天以上的全部归档到冷存储。这样检索性能一直很稳定。5.5 与 Agent 主流程的集成注意事项最后说几个集成层面的坑。不要在 Agent 的每一轮对话都调检索。检索是有成本的而且频繁注入历史经验会干扰 Agent 对当前任务的判断。我的做法是只在任务开始时检索一次任务过程中如果需要额外信息由 Agent 主动调用。存储快照的时机要选对。不是任务一结束就存而是等 Agent 给出最终结果、用户确认或系统判定完成后才存。否则会把中间过程的错误决策也存进去污染记忆。给记忆系统设一个开关。调试 Agent 逻辑的时候有时候需要关掉记忆注入看 Agent 的原始表现。在 MCP 配置里加一个环境变量控制是否启用记忆检索会方便很多。监控记忆命中率。记录每次检索返回的结果是否被 Agent 实际使用。如果命中率长期低于 20%说明检索策略有问题或者记忆内容和当前任务类型不匹配需要调整。这套东西我从零搭到现在稳定运行前后大概花了三周时间其中一半时间在调检索策略和反思质量。但一旦跑通效果是肉眼可见的——同一个 Agent接入 hindsight 之后重复错误的次数下降了大概七成。这个投入产出比我觉得很值。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →