1. 从“hindsight”这个词说起:为什么它值得单独拿出来做一套系统
“hindsight”直译过来是“后见之明”,但在 AI Agent 和 LLM 工程语境里,它指向一个非常具体、也非常痛的问题:Agent 在任务执行完之后,能不能回头把这次经历沉淀成可复用的记忆,而不是每次都从零开始。
做过 Agent 项目的人大概都有这种体会:你花了两周调好一个能自动查资料、写报告、调工具的 Agent,结果第二天换个任务,它表现得像第一次上岗。上下文窗口塞满了历史对话,token 烧得飞快,但真正有用的经验——比如“上次这个 API 返回 429 时应该退避 3 秒重试”“这个数据库的字段名和文档写的不一致”——全都没留下来。这就是典型的“有执行、无记忆”。
hindsight 要解决的就是这件事。它不是一个模型,也不是一个框架,而是一套围绕Agent Memory(智能体记忆)构建的存储与检索层。你可以把它理解成给 Agent 装了一个“工作记忆 + 长期记忆”的双层结构:working memory 负责当前任务的即时状态,long-term memory 负责跨会话、跨任务的经验沉淀。关键词里出现的agent 存储 working memory、tencentdb agent memory、agent memory这些热词,本质上都在讨论同一个问题——Agent 的记忆到底该存在哪、怎么存、怎么取。
这篇文章适合三类人看:第一类是在做 Agent 产品、被“上下文爆炸”和“记忆丢失”折磨的工程师;第二类是想理解 MCP(Model Context Protocol)在记忆场景里怎么落地的人;第三类是单纯对 LLM 记忆机制好奇、想动手跑一个最小可用版本的技术爱好者。我会从设计动机讲到 Docker 部署,再讲到 MCP 接入和实际踩坑,尽量把每一步的“为什么”说清楚,而不是只丢一堆命令。
先说一个反直觉的结论:Agent 记忆的难点从来不是“存”,而是“忘”和“取”。存谁都会,写个 JSON 落盘就行;但什么时候该把一条记忆标记为过期、检索时怎么在几千条记忆里捞出最相关的那三条、怎么避免旧记忆污染新任务——这些才是 hindsight 这类系统真正要回答的问题。后面几个章节会围绕这个核心展开。
2. hindsight 的记忆分层设计:working memory 与 long-term memory 到底怎么分工
2.1 working memory:任务执行期间的“草稿纸”
working memory 的概念借自认知心理学,放到 Agent 场景里,它对应的是当前任务生命周期内需要临时保存的状态。比如 Agent 正在执行一个“查订单→算退款→发通知”的流程,那么“订单号是多少”“退款金额算到哪一步了”“通知发没发出去”这些信息就属于 working memory。
它有几个硬性特征。第一是生命周期短,任务结束就该清理或归档,不能无限堆积。第二是读写频繁,几乎每一步工具调用都要读一次、写一次。第三是结构相对固定,通常是 key-value 或者带 schema 的结构化数据,而不是自由文本。
为什么不能直接用 LLM 的上下文窗口当 working memory?因为上下文窗口有三个致命问题:贵、慢、不可靠。一个 128K 上下文的模型,每轮对话都把这 128K 重新喂一遍,token 成本是线性增长的;而且模型对上下文中间部分的注意力会衰减(业界常说的“lost in the middle”),放在中间的关键状态很可能被忽略。所以正确的做法是:上下文窗口只放当前最相关的片段,完整状态存在外部 working memory 里,按需注入。
hindsight 在这块的思路是提供一个轻量的状态存储接口,Agent 每步执行后把关键状态写进去,下一步执行前按 key 取出来。这样上下文里永远只有“当前需要的那几行”,而不是整段历史。
2.2 long-term memory:跨会话的经验沉淀
long-term memory 解决的是另一个维度的问题:这次任务学到的东西,下次能不能用上。它存储的是经过提炼的经验、事实、偏好和模式,而不是原始对话流水。
这里有个关键设计决策:long-term memory 里到底存什么?我的经验是存三类东西最划算。第一类是事实性记忆,比如“用户偏好用中文回复”“这个项目的数据库是 PostgreSQL 不是 MySQL”。第二类是程序性记忆,比如“调用这个接口前必须先拿 token”“这个工具在超时后重试两次成功率最高”。第三类是反思性记忆,比如“上次这个方案失败是因为忽略了分页,下次要先检查 total 字段”。
注意,原始对话记录不应该直接进 long-term memory。原因很简单:噪声太大、检索命中率低、还容易把一次性信息当成通用规律。正确的做法是加一个“提炼”环节——任务结束后让 LLM 总结出可复用的条目,再写入长期存储。这个提炼环节本身也是 hindsight 这类系统区别于“简单向量库”的核心。
2.3 两层之间怎么流转
working memory 和 long-term memory 不是孤立的,它们之间有一条明确的流转链路。任务开始时,系统根据任务描述从 long-term memory 检索相关经验,注入到 working memory 作为初始上下文;任务执行中,新产生的状态写入 working memory;任务结束时,一个“归档器”读取 working memory 的完整轨迹,提炼出值得长期保留的条目,写入 long-term memory,然后清理 working memory。
这条链路里最容易出问题的是归档时机。如果任务失败也归档,可能把错误经验固化下来;如果只在成功时归档,又会丢掉“失败教训”这种高价值信息。我的做法是:成功和失败都归档,但打上不同的标签,检索时根据当前任务的风险偏好决定要不要召回失败经验。这个细节后面在踩坑章节会再展开。
3. 为什么记忆检索不能只靠向量相似度
3.1 纯向量检索在记忆场景的三个失效点
很多人一想到“记忆检索”,第一反应就是上向量数据库,把记忆 embed 成向量,查询时算余弦相似度取 top-k。这个方案在文档问答里很好用,但直接搬到 Agent 记忆场景会出问题。
第一个失效点是时间维度丢失。向量相似度只看语义,不看时间。一条三年前的记忆和一条五分钟前的记忆,只要语义相近,得分可能一样。但 Agent 场景里,新鲜度往往是决定性的——“用户上周说他搬家了”比“用户两年前说他住在 A 小区”重要得多。所以检索必须引入时间衰减因子。
第二个失效点是重要性无法体现。有些记忆是“一次性事实”,比如某次查询的临时参数;有些记忆是“核心偏好”,比如用户明确说过“所有报告都要用 Markdown”。这两类在向量空间里可能很近,但重要性天差地别。纯向量检索会把它们平等对待,导致核心偏好被淹没。
第三个失效点是结构化查询缺失。Agent 经常需要“取最近 5 条关于订单的记忆”或者“取所有标记为失败的经验”,这类查询用向量做很别扭,用结构化过滤却很自然。所以实际系统里,向量检索和结构化过滤应该是组合关系,而不是替代关系。
3.2 hindsight 的混合检索思路
基于上面三个问题,hindsight 这类系统的检索层通常是混合的:向量相似度 + 时间衰减 + 重要性权重 + 结构化过滤,最后加权排序。
具体来说,每条记忆在写入时会带上几个元字段:created_at(创建时间)、last_accessed_at(最后访问时间)、importance(重要性评分,0-1)、tags(标签)、source_task(来源任务)。检索时先做结构化过滤(比如只要tags包含user_preference的),再算向量相似度,然后乘上时间衰减和重要性权重,最后取 top-k。
时间衰减函数我一般用指数衰减:decay = exp(-λ * Δt),其中 Δt 是距今天数,λ 控制衰减速度。λ 取 0.01 意味着大约 70 天后权重降到一半,这个节奏对大多数 Agent 场景比较合适。重要性评分可以让 LLM 在归档时打分,也可以人工规则设定,比如“用户明确表达的偏好”直接给 0.9。
提示:不要一上来就调复杂的加权公式。先用最简单的“向量相似度 × 时间衰减”,跑通了再逐步加重要性权重。很多团队死在过度设计上,检索层还没跑通就开始调参。
3.3 一个具体的检索打分示例
假设现在有三条记忆,查询是“用户对报告格式的偏好”:
| 记忆内容 | 向量相似度 | 距今天数 | 重要性 | 综合得分 |
|---|---|---|---|---|
| 用户要求报告用 Markdown | 0.92 | 3 | 0.9 | 0.92×0.97×0.9≈0.80 |
| 用户提过一次喜欢表格 | 0.85 | 60 | 0.5 | 0.85×0.55×0.5≈0.23 |
| 用户说报告别太长 | 0.78 | 10 | 0.7 | 0.78×0.90×0.7≈0.49 |
按综合得分排序,第一条明显胜出。如果只看向量相似度,三条差距不大,但加上时间和重要性后,真正该被召回的那条就浮出来了。这就是混合检索的价值。
4. 用 Docker 把 hindsight 跑起来:从环境准备到第一个记忆写入
4.1 环境准备里最容易被忽略的两件事
Docker 部署本身不复杂,但有两个坑几乎每个新手都会踩。第一个是Windows 上的虚拟化支持。热词里出现的virtualization support not detected docker desktop failed to start就是典型症状——Docker Desktop 启动时报“未检测到虚拟化支持”。解决办法是进 BIOS 打开 VT-x/AMD-V,然后在 Windows 功能里确认“虚拟机平台”和“适用于 Linux 的 Windows 子系统”都已启用。注意,开了 Hyper-V 之后某些安卓模拟器会冲突,这是已知的取舍。
第二个坑是Docker 网络不通。热词里的docker网络不通也是高频问题。容器之间要通信,必须放在同一个自定义 bridge 网络里,而不是默认的 bridge。默认 bridge 不支持自动 DNS 解析,容器名互相 ping 不通。正确做法是先docker network create hindsight-net,然后所有容器都--network hindsight-net接入。
# 创建专用网络 docker network create hindsight-net # 验证网络创建成功 docker network ls | grep hindsight4.2 用 docker compose 编排记忆服务
单容器跑不起来完整的记忆系统,因为通常需要三个组件:记忆存储(可以是 PostgreSQL 或专门的向量库)、记忆服务(hindsight 本体)、以及可选的缓存(Redis)。用 docker compose 编排最省心。
version: "3.9" services: hindsight-db: image: postgres:16 environment: POSTGRES_USER: hindsight POSTGRES_PASSWORD: hindsight_pass POSTGRES_DB: hindsight volumes: - hindsight_data:/var/lib/postgresql/data networks: - hindsight-net healthcheck: test: ["CMD-SHELL", "pg_isready -U hindsight"] interval: 10s timeout: 5s retries: 5 hindsight-cache: image: redis:7-alpine networks: - hindsight-net hindsight-core: image: hindsight/core:latest depends_on: hindsight-db: condition: service_healthy environment: DB_URL: postgresql://hindsight:hindsight_pass@hindsight-db:5432/hindsight REDIS_URL: redis://hindsight-cache:6379 MEMORY_TTL_DAYS: 90 ports: - "8080:8080" networks: - hindsight-net volumes: hindsight_data: networks: hindsight-net: external: true这里有几个参数值得解释。MEMORY_TTL_DAYS: 90控制长期记忆的默认存活天数,超过这个天数的低重要性记忆会被归档或清理。healthcheck那段很关键——如果不等数据库就绪就启动 core,core 会因为连不上库直接退出,然后你会在日志里看到一堆连接拒绝,排查半天才发现是启动顺序问题。depends_on配合condition: service_healthy就是解决这个的。
4.3 写入第一条记忆并验证
服务起来之后,先别急着接 Agent,用 curl 手动写一条记忆,确认链路通。
# 写入一条 working memory curl -X POST http://localhost:8080/memory/working \ -H "Content-Type: application/json" \ -d '{ "task_id": "demo-001", "key": "current_step", "value": "fetching_user_profile", "ttl_seconds": 3600 }' # 写入一条 long-term memory curl -X POST http://localhost:8080/memory/longterm \ -H "Content-Type: application/json" \ -d '{ "content": "用户偏好报告使用 Markdown 格式,且不超过 500 字", "importance": 0.9, "tags": ["user_preference", "report_format"] }' # 检索验证 curl -X POST http://localhost:8080/memory/search \ -H "Content-Type: application/json" \ -d '{ "query": "报告格式偏好", "top_k": 3 }'如果第三条命令返回了你刚写入的那条记忆,说明存储、索引、检索链路全通了。这一步看起来简单,但它能帮你把 80% 的环境问题挡在接入 Agent 之前。我见过太多人跳过这步,直接接 Agent,结果 Agent 报错时根本分不清是记忆服务的问题还是 Agent 代码的问题。
注意:
ttl_seconds只对 working memory 生效,long-term memory 用的是MEMORY_TTL_DAYS那套清理策略。别把两者搞混,否则会出现“明明设了 TTL 但记忆没被清理”的困惑。
5. MCP 接入:让 Agent 用标准协议读写记忆
5.1 MCP 到底解决什么问题
热词里反复出现mcp、mcp协议、mcp 是软件协议 硬件协议那个概念叫什么来着,说明很多人对 MCP 的定位还比较模糊。简单说,MCP(Model Context Protocol)是一套让 LLM 应用以统一方式访问外部工具和数据源的协议。在它出现之前,每个 Agent 框架都有自己的工具调用格式,你为 A 框架写的记忆插件,换到 B 框架就得重写。MCP 把这个接口标准化了:记忆服务暴露成 MCP Server,任何支持 MCP 的客户端(Agent 框架)都能直接调用。
对 hindsight 来说,MCP 接入意味着记忆能力可以跨框架复用。你今天用某个框架跑 Agent,明天换另一个,记忆层不用动。这是它比“写死在某个框架里的记忆模块”更有价值的地方。
5.2 把 hindsight 暴露成 MCP Server
hindsight 的 MCP Server 通常暴露三个工具:memory_write、memory_search、memory_forget。前两个是读写,第三个是显式删除(对应前面说的“忘”的能力)。
{ "mcpServers": { "hindsight": { "command": "docker", "args": [ "exec", "-i", "hindsight-core", "hindsight-mcp-server", "--transport", "stdio" ] } } }这段配置的意思是:MCP 客户端通过 stdio 和运行在容器里的 hindsight MCP Server 通信。用docker exec -i而不是暴露 TCP 端口,是因为 stdio 更简单、更安全,不需要额外处理认证和网络暴露。
配置好之后,Agent 在需要记忆时就会自动调用memory_search,在任务结束时调用memory_write。你不需要在 Agent 代码里写任何记忆逻辑,协议层帮你处理了。
5.3 接入时最容易卡住的三个点
第一个卡点是MCP Server 找不到。热词里的codex无法找到mcp就是这类问题。常见原因是命令路径不对,或者容器名写错。排查方法很简单:先在宿主机手动执行一遍docker exec -i hindsight-core hindsight-mcp-server --transport stdio,看能不能正常启动。如果这步就失败,问题在容器侧;如果这步成功但客户端还是找不到,问题在客户端配置。
第二个卡点是工具调用返回 schema 不匹配。热词里的llm request failed: provider rejected the request schema or tool payload说的就是这个。MCP 工具的参数 schema 必须严格符合 JSON Schema 规范,少一个required字段或者类型写错,模型侧就会拒绝。我的经验是:写完 schema 后用在线 JSON Schema 校验器过一遍,别靠肉眼。
第三个卡点是stdio 缓冲问题。有些 MCP 客户端对 stdio 的输出缓冲处理不好,导致响应延迟或截断。如果遇到“调用成功但拿不到结果”,可以在 MCP Server 启动参数里加--no-buffer或者手动 flush。这个坑比较隐蔽,日志里往往看不出明显错误。
6. 实测中踩过的坑与记忆污染问题
6.1 记忆污染:旧经验怎么毁掉新任务
这是我在实际项目里踩过最狠的一个坑。当时 Agent 在处理一类任务时表现很好,我就让它把成功经验都归档了。结果换到另一类相似但关键细节不同的任务时,Agent 检索到了旧经验,直接套用,导致连续失败。
问题的本质是:相似不等于适用。向量检索会把“语义相近”的记忆召回,但语义相近的任务,约束条件可能完全不同。比如“生成周报”和“生成月报”语义很近,但周报的格式要求、数据范围、受众都可能不一样。旧经验一旦被当成通用规律,就会变成负资产。
我的解决方案是给记忆加适用条件标签。归档时不只存“怎么做”,还存“在什么条件下这么做”。检索时先匹配条件,条件不满足的记忆即使语义再近也不召回。这个改动让记忆的误用率下降了一大截。
6.2 记忆膨胀:为什么“全存下来”是错的
另一个坑是记忆无限增长。一开始我想着“多存点总没坏处”,结果三个月后长期记忆库里堆了几万条,检索延迟从几十毫秒涨到几百毫秒,而且召回质量明显下降——因为噪声太多了。
后来我加了两道闸。第一道是归档时的去重:新记忆写入前先检索一遍,如果已有高度相似的记忆,就更新旧记忆的last_accessed_at和importance,而不是新增一条。第二道是定期清理:每周跑一次任务,把importance低于阈值且超过 60 天没被访问的记忆归档到冷存储。这两道闸加上之后,记忆库规模稳定在一个可控范围,检索质量也回来了。
提示:清理策略一定要可回滚。别直接删,先标记为
archived,观察一段时间确认没影响再真删。我吃过直接删的亏,后来想找回某条记忆发现已经没了。
6.3 并发写入的竞态问题
当多个 Agent 实例同时操作同一个记忆库时,会出现竞态。典型场景是两个实例同时检索到“没有相关记忆”,然后各自写入一条相似记忆,导致重复。解决办法是在写入路径上加乐观锁或者唯一约束——用记忆内容的哈希值做唯一键,重复写入直接忽略。这个改动很小,但能省掉后面大量的去重工作。
7. 关于 hindsight 这类系统,我个人的几点使用体会
跑了一段时间之后,我最大的体会是:Agent 记忆系统的价值不在于“记住多少”,而在于“在对的时候想起对的那一条”。一个只有 50 条高质量记忆的系统,往往比一个有 5000 条杂乱记忆的系统表现更好。所以如果你刚开始做,别急着堆功能,先把“写入质量”和“检索精度”这两件事做扎实。
第二个体会是记忆的读写要解耦。写入可以异步、可以批量、可以慢;但检索必须快、必须同步、必须在 Agent 决策路径上低延迟返回。我见过把写入和检索放在同一个同步链路上的设计,结果每次写记忆都要等索引更新,Agent 响应慢得没法用。正确的做法是写入进队列,索引异步更新,检索走缓存。
第三个体会是别迷信自动化提炼。让 LLM 自动总结记忆听起来很美,但实际跑下来,自动提炼的记忆质量参差不齐,有些总结得过于笼统,有些又丢掉了关键细节。我的折中方案是:自动提炼 + 人工抽检。每周抽 20 条自动生成的记忆人工过一遍,发现模式后调整提炼 prompt。这个投入不大,但能显著提升记忆库的整体质量。
最后一个建议是从最小可用版本开始。别一上来就搞向量库 + 图数据库 + 混合检索的全家桶。先用 PostgreSQL 加一个简单的向量扩展,把写入、检索、清理三条链路跑通,接一个真实 Agent 用两周,看看哪里疼再针对性优化。hindsight 这类系统的复杂度应该由真实需求驱动,而不是由架构图驱动。