1. 从“hindsight”这个词说起:为什么它值得单独拿出来聊
第一次看到“hindsight”作为项目标题,我脑子里蹦出来的不是某个具体框架,而是它字面那层意思——事后之明。这个词在英文里常出现在“hindsight is 20/20”这种表达里,说的是回头看什么都清楚。放到 LLM Agent 这个语境下,它指向的东西其实非常具体:Agent 在完成任务之后,能不能回过头去审视自己走过的路,把当时的决策、上下文、工具调用结果重新梳理一遍,形成可复用的记忆。
这件事为什么值得单独拎出来做?因为绝大多数 Agent 项目在“记忆”这件事上做得都很糙。我见过太多团队的做法是:把对话历史一股脑塞进向量库,检索的时候按相似度捞几条出来拼进 prompt,然后祈祷模型能从中找到有用的信息。这种做法在简单问答场景下勉强能用,一旦任务链条拉长、工具调用变多、跨会话需要延续状态,立刻就露馅——检索出来的记忆要么是无关的闲聊,要么是过期的中间状态,要么干脆把互相矛盾的几条一起塞进去让模型自己纠结。
“hindsight”这个标题配合 agent memory、LLM、MCP、Docker 这几个关键词,我判断它要解决的核心问题是:给 Agent 装一套“事后复盘式”的记忆机制。不是简单地存和取,而是在任务告一段落之后,主动对这段经历做一次结构化整理,把“发生了什么”“为什么这么决策”“结果如何”“下次遇到类似情况该怎么办”这几层信息分离出来,分别存储、分别检索。这跟人类做事后复盘的习惯是一样的——你不会把开会的全程录音当成会议纪要,你会提炼出结论、待办和关键分歧点。
适合读这篇内容的人,我大致分三类。第一类是在做 Agent 应用开发、被记忆问题折磨过的工程师,你们可能已经试过纯向量检索、试过滑动窗口、试过摘要压缩,但效果都不稳定。第二类是对 MCP 协议感兴趣、想搞清楚 Agent 记忆怎么跟工具调用体系结合的人。第三类是想用 Docker 快速把一套 Agent 记忆方案跑起来、先看效果再决定要不要深入的人。不管你是哪一类,下面这些内容都是从实际折腾里攒出来的,不是纸上谈兵。
2. Agent 记忆的真实痛点:不是存不下,是取不对
2.1 向量检索在记忆场景下的三个硬伤
很多人对 Agent 记忆的第一反应就是“上向量数据库”。这个思路本身没错,但直接用朴素向量检索来做记忆,会撞上三个绕不过去的硬伤。
第一个硬伤是语义相似不等于任务相关。用户上周问过“帮我查一下北京到上海的航班”,这周问“帮我订个酒店”,两句话在向量空间里可能因为都涉及“出行”而距离很近,但前者对后者几乎没有参考价值。向量检索按语义距离排序,它分不清“相关”和“有用”的区别。
第二个硬伤是时间维度的丢失。记忆是有时效性的。Agent 昨天查到的库存数量,今天可能已经变了;用户上个月说的偏好,这个月可能已经改了。纯向量检索把时间当成普通元数据,检索时要么完全忽略,要么只能做粗粒度的过滤,没法表达“优先用最近的、但如果旧的更权威则用旧的”这种复杂逻辑。
第三个硬伤是矛盾记忆的堆积。同一个问题,Agent 在不同时间可能得到不同答案,如果都存进去,检索时可能同时捞出来。模型面对两条互相矛盾的记忆,要么随机选一条,要么试图调和,两种结果都不可靠。hindsight 这类方案的价值就在于,它在存储阶段就做了冲突检测和版本管理,而不是把矛盾留给检索阶段去碰运气。
2.2 工作记忆和长期记忆的边界该怎么划
Agent 记忆另一个容易搞混的地方,是工作记忆和长期记忆的边界。我见过不少项目把两者混在一个存储里,结果就是工作记忆的临时状态污染了长期记忆的检索质量。
我的经验是,工作记忆应该以任务为生命周期。一个任务开始,工作记忆初始化;任务进行中,每一步的工具调用结果、中间推理、当前状态都写进工作记忆;任务结束,工作记忆做一次“沉淀”——把值得长期保留的部分提炼出来写入长期记忆,其余的直接丢弃。这个沉淀过程就是 hindsight 的核心动作。
长期记忆则应该以实体和关系为组织单位,而不是以对话轮次为单位。比如“用户偏好靠窗座位”这条记忆,它关联的实体是“用户”和“座位偏好”,关系是“偏好”。这样组织的好处是,当新任务涉及订座时,可以直接按实体检索,而不是靠语义相似度去碰。
2.3 为什么“事后复盘”比“实时记录”更有效
实时记录每一步的好处是信息全,坏处是噪音大。Agent 执行任务过程中会产生大量中间状态,大部分对后续任务没有复用价值。如果全部实时写入长期记忆,检索信噪比会急剧下降。
事后复盘的优势在于,它有一个全局视角。任务结束后,Agent 已经知道最终结果是好是坏,这时候再回头看每一步,就能判断哪些决策是关键节点、哪些是无关紧要的弯路。这种带结果标签的复盘,提炼出来的记忆质量远高于实时流水账。
打个比方,实时记录像是行车记录仪,全程都拍,但你要找某个片段得从头翻;事后复盘像是事故报告,只记录关键时间点和因果链,但信息密度高得多。hindsight 走的是后一条路。
3. 用 MCP 把记忆能力做成可插拔的模块
3.1 MCP 协议为什么适合承载记忆服务
MCP(Model Context Protocol)这几年的热度不用我多说,它本质上是一套让模型和外部能力对接的标准化协议。把 Agent 记忆做成一个 MCP Server,好处非常直接:记忆能力从 Agent 主逻辑里解耦出来,变成可替换、可组合的独立模块。
我试过两种做法。一种是把记忆逻辑直接写在 Agent 代码里,好处是调用方便,坏处是换一种记忆策略就要改主逻辑,测试也麻烦。另一种是做成 MCP Server,Agent 通过标准协议调用,好处是记忆策略可以独立迭代,甚至可以在运行时切换不同的记忆后端。实测下来,第二种做法在项目稍微复杂一点之后优势非常明显。
MCP 的另一个好处是工具调用的统一性。Agent 调用记忆服务和调用其他工具(比如搜索、计算)走的是同一套协议,不需要为记忆单独设计一套调用约定。这降低了 Agent 编排逻辑的复杂度。
3.2 记忆 MCP Server 该暴露哪些工具
一个记忆 MCP Server 暴露的工具不在多,在于覆盖记忆的完整生命周期。我建议至少包含这几个:
| 工具名 | 作用 | 关键参数 |
|---|---|---|
memory_write | 写入一条记忆 | content、memory_type、entities、ttl |
memory_search | 检索记忆 | query、memory_type、time_range、top_k |
memory_consolidate | 触发复盘沉淀 | task_id、outcome、summary |
memory_forget | 主动遗忘 | memory_id 或 entity |
memory_conflict_check | 冲突检测 | new_memory、existing_scope |
这里重点说memory_consolidate。这个工具是 hindsight 思路的落地入口。Agent 在任务结束时调用它,传入任务 ID、最终结果和一段简要总结,Server 端负责把这次任务的工作记忆做结构化提炼,决定哪些写入长期记忆、哪些丢弃、哪些需要标记为待验证。
memory_conflict_check也值得单独说。新记忆写入前先做一次冲突检测,如果发现和已有记忆矛盾,不是简单覆盖,而是标记冲突并保留两个版本,等后续有更多证据时再裁决。这个机制能有效避免记忆库被错误信息污染。
3.3 记忆写入的粒度控制:太细和太粗都是坑
记忆写入粒度是个需要反复调的参数。写得太细,比如每一步工具调用都存一条,检索时噪音大;写得太粗,比如整个任务只存一条总结,检索时又缺乏细节。
我的经验值是:一个任务沉淀 3 到 7 条长期记忆比较合适。具体怎么分?按“决策点”分。任务过程中每个需要做选择的节点,如果这个选择对结果有实质影响,就值得单独记一条。比如“用户明确拒绝了某个方案”“某个工具调用返回了异常并触发了降级策略”“最终选择了方案 B 而非方案 A,原因是成本”。
这个粒度控制没有万能公式,得根据你的任务类型调。任务链条越长、决策点越多,沉淀的记忆条数可以适当放宽,但一般不建议超过 10 条,否则检索时又会回到信噪比问题。
4. 用 Docker 把整套记忆服务跑起来
4.1 容器化记忆服务的目录结构设计
用 Docker 跑记忆服务,第一步是把目录结构设计清楚。我踩过的坑是早期把所有东西塞在一个容器里,后来想换向量库、想加缓存、想单独升级某个组件,都得推倒重来。现在的做法是拆成几个职责清晰的容器。
一个典型的组合是这样的:记忆服务主进程一个容器,向量数据库一个容器,关系型数据库一个容器(存实体和关系),缓存一个容器。四个容器通过 Docker 网络互联。这样拆的好处是每个组件可以独立扩缩容,向量库吃内存就多给内存,关系库吃 IO 就多给 IO。
目录结构上,我习惯把配置、数据、日志分开挂载:
memory-service/ ├── config/ │ ├── mcp-server.yaml │ └── memory-policy.yaml ├── data/ │ ├── vector/ │ └── relational/ ├── logs/ └── docker-compose.yamlmemory-policy.yaml这个文件值得单独提。它定义的是记忆策略——什么类型的记忆保留多久、冲突检测的严格程度、复盘沉淀的触发条件。把这些做成配置而不是硬编码,调参的时候不用改代码重新构建镜像。
4.2 docker-compose 编排里的网络与依赖顺序
多容器编排最容易出问题的地方是启动顺序和网络。记忆服务主进程启动时如果向量库还没就绪,会直接报错退出。解决办法有两个:一是用depends_on配合健康检查,二是主进程里做重试。
我更推荐第二种,因为depends_on只能保证容器启动顺序,不能保证服务真正可用。主进程里加一段带退避的重试逻辑,连接不上就等几秒再试,最多试若干次。这样即使向量库启动慢一点,整体也能正常起来。
网络方面,所有容器放在同一个自定义 bridge 网络里,用服务名互相访问。不要用默认网络,也不要用 host 网络模式,前者隔离性差,后者端口冲突风险高。自定义网络里,容器之间用服务名当主机名,配置里写vector-db:6333这种地址就行,不用关心具体 IP。
4.3 数据持久化:别让容器重启把记忆弄丢了
这是新手最容易犯的错。容器默认是无状态的,重启之后里面的数据就没了。记忆服务的数据是核心资产,必须挂载到宿主机或者命名卷上。
向量库的数据目录、关系库的数据目录、日志目录,这三个必须持久化。配置文件如果会动态改,也建议挂载出来。我一般用命名卷而不是直接挂宿主机目录,因为命名卷在不同操作系统上的行为更一致,迁移也方便。
还有一点,备份策略要提前想好。记忆库不像普通业务数据,它没有明确的主键约束,损坏之后很难重建。定期把向量库和关系库做快照,存到独立的位置。这个动作在项目早期就要做,别等出事了才想起来。
5. 复盘沉淀的具体实现:从工作记忆到长期记忆
5.1 任务结束时的触发时机怎么定
复盘沉淀的触发时机,直接决定了记忆质量。触发太早,任务还没真正结束,沉淀的是半成品;触发太晚,工作记忆可能已经被后续任务覆盖。
我的做法是双触发:一是任务显式结束(Agent 判断目标达成或明确失败),二是工作记忆容量达到阈值。前者是主路径,后者是兜底。有些任务可能因为各种原因没有明确的结束信号,容量阈值能保证工作记忆不会无限膨胀。
触发之后,不是立刻写入长期记忆,而是先进入一个“待沉淀”队列。这个队列里的内容会经过一轮质量检查——检查项包括:任务结果是否明确、关键决策点是否记录完整、是否存在未解决的冲突。检查通过才真正写入长期记忆。
5.2 提炼什么:决策链、结果标签、可复用模式
复盘沉淀提炼的内容,我总结为三类。
第一类是决策链。记录任务过程中每个关键决策点:当时面临什么选择、选了哪个、依据是什么。这类记忆的价值在于,下次遇到类似决策时,Agent 可以参考历史选择,而不是从零推理。
第二类是结果标签。给这次任务打上结果标签——成功、失败、部分成功、超时放弃。标签的作用是给后续检索提供过滤维度。检索时优先看成功案例的经验,失败案例则用来避坑。
第三类是可复用模式。如果这次任务的处理方式具有普适性,就把它抽象成一个模式存下来。比如“当用户需求模糊时,先追问澄清再执行”这种模式,可以跨任务复用。模式的抽象程度要把握好,太具体了没法复用,太抽象了没有指导意义。
5.3 冲突记忆的处理:保留、标记还是覆盖
冲突记忆的处理是记忆系统里最微妙的部分。我的原则是:默认保留,标记冲突,延迟裁决。
新记忆和旧记忆矛盾时,不要急着覆盖。先看两条记忆的时间戳和来源可信度。如果新记忆来自更近的时间且来源可靠,可以标记旧记忆为“可能过期”,但保留它。如果两条记忆来源可信度相当,就都保留,标记为冲突,等后续有第三条证据时再裁决。
这个策略的代价是记忆库会变大,检索时需要处理冲突标记。但相比错误覆盖导致的信息丢失,这个代价是值得的。我见过因为覆盖策略太激进,把用户早期明确表达的偏好覆盖掉,导致后续推荐全部跑偏的案例。
6. 实测中遇到的几个坑和应对
6.1 记忆检索的延迟毛刺
记忆检索偶尔会出现延迟毛刺,P99 延迟比 P50 高出一个数量级。排查下来,主要原因是向量检索在数据量增长后,索引没有及时重建,导致部分查询走了暴力扫描。
解决办法是定期重建索引,并且在数据量超过阈值时触发。另外,检索请求加超时和降级——超时了就返回缓存里的近似结果,不要让整个 Agent 卡在记忆检索上。记忆检索是辅助能力,不应该成为主流程的瓶颈。
6.2 复盘沉淀把噪音也沉淀了
早期版本里,复盘沉淀会把工作记忆里的所有内容都过一遍,结果把大量噪音也写进了长期记忆。后来加了过滤规则:只有带明确决策标签、带结果关联、或者被标记为可复用模式的内容才进入沉淀候选。
过滤规则不是一成不变的,需要根据实际沉淀出来的记忆质量持续调。我一般会定期抽样看长期记忆库,如果发现某类噪音反复出现,就加一条过滤规则。
6.3 多 Agent 共享记忆时的隔离问题
如果多个 Agent 共享一个记忆库,隔离问题必须提前设计。不同 Agent 的记忆混在一起,检索时会互相干扰。
我的做法是按 Agent 身份做逻辑隔离,而不是物理隔离。每条记忆带一个 owner 字段,检索时默认只查当前 Agent 的记忆,需要跨 Agent 共享时显式指定。这样既保证了隔离,又保留了共享的可能性。物理隔离(每个 Agent 一个独立库)在 Agent 数量多的时候运维成本太高,不推荐。
7. 关于这套方案后续可以怎么演进
这套基于 hindsight 思路的记忆方案,目前跑下来在中等复杂度的 Agent 任务上效果稳定。后续我打算试两个方向。
一个是记忆的主动遗忘。现在遗忘是被动的,靠 TTL 过期。但有些记忆虽然没过期,实际上已经没用了,主动识别并清理能进一步提升检索质量。识别信号可以包括:长期未被检索命中、关联的实体已经不存在、被多次标记为冲突且无后续证据。
另一个是记忆的跨 Agent 迁移。当一个 Agent 在某个领域积累了足够多的记忆,能不能把这部分记忆迁移给新 Agent,让它快速具备该领域的能力。这个方向涉及记忆的抽象和泛化,比单纯存储检索复杂得多,但价值也更大。
如果你也在做 Agent 记忆相关的东西,欢迎交流。这个领域现在还没有公认的最佳实践,大家都在摸索,多碰多试比闭门造车强。