hindsight:为LLM agent构建可回溯、可检索、可分层记忆系统
1. 为什么“事后复盘”这件事值得单独做一个项目
“hindsight”这个词本身的意思就是“事后的领悟”——事情发生完了,回头一看,才明白当时到底发生了什么、哪里做对了、哪里踩了坑。把这个词拿来命名一个跟 agent memory 相关的项目,我觉得起名的人挺懂行的:因为 agent 的记忆问题,本质上就是一个“事后才看得清”的问题。
你让一个 LLM agent 跑一个长任务,比如连续处理几十轮对话、调用十几个工具、跨好几个会话去完成一件事。跑完之后你问它:“上次我们聊到哪了?那个报错最后怎么解决的?”它一脸茫然。不是它笨,是它根本没有一套像样的记忆机制。上下文窗口就那么大,塞满了就丢,丢了就再也找不回来。这跟人熬夜加班到凌晨三点、第二天完全不记得昨晚干了啥是一个道理。
hindsight要解决的核心问题,就是给 LLM agent 装一套可回溯、可检索、可分层的记忆系统。它不是一个简单的“把对话历史存进向量库”就完事的方案,而是把 agent 的记忆拆成 working memory(工作记忆)、episodic memory(情景记忆)和 semantic memory(语义记忆)几个层次,再通过 MCP 协议把这些能力暴露给上层的 agent 框架去调用。整个东西用 Docker 打包,一条命令拉起来就能用。
这篇文章适合谁看?如果你正在做 LLM agent 相关的开发,被“agent 记不住东西”这个问题折磨过;或者你在研究 MCP 协议怎么落地到实际项目里;又或者你只是想找一个能本地跑起来的 agent memory 方案,不想依赖任何外部云服务——那这篇内容应该能给你不少可直接抄作业的东西。
我下面会从整体设计思路、核心机制拆解、Docker 实操部署、MCP 接入配置、常见问题排查几个角度,把hindsight这个项目从头到尾讲一遍。里面涉及参数计算和配置的地方,我会把“为什么这么选”也一并说清楚,避免你照着抄完还不知道自己在干嘛。
2. 整体设计思路:agent memory 到底该怎么分层
2.1 从“上下文窗口不够用”这个根本矛盾说起
所有 agent memory 方案要解决的第一个问题,都是上下文窗口的物理限制。当前主流 LLM 的上下文窗口从 8K 到 200K token 不等,看起来很大,但实际跑起来消耗极快。一轮带工具调用的对话,system prompt 加上工具定义就能吃掉两三千 token,再叠几轮历史对话和工具返回结果,一万 token 转眼就没了。
最粗暴的做法是“截断”——只保留最近 N 轮对话。这个方案的问题在于,早期关键信息会被无声无息地丢掉。比如用户在第三轮说过“我的数据库密码放在环境变量 DB_PASS 里”,到第二十轮 agent 需要连数据库时,这条信息早就被截掉了,agent 只能瞎猜或者报错。
稍微好一点的做法是“摘要压缩”——把旧对话用 LLM 总结成一段短文本。但这又引入新问题:摘要是有损的,而且摘要本身也要消耗 token 去生成。更麻烦的是,摘要做多了会形成“摘要的摘要”,信息一层层失真,最后剩下的东西跟原意可能已经差了十万八千里。
hindsight的思路不是在这两个方案里二选一,而是把记忆分层,不同层用不同的存储和检索策略。这跟人脑的记忆机制其实很像:你不需要记住今天中午吃了什么(working memory,用完就丢),但你会记住“上次在这家店吃坏肚子了”(episodic memory,情景记忆),还会记住“生冷食物容易吃坏肚子”(semantic memory,语义记忆)。三层各司其职,检索的时候按需调用。
2.2 三层记忆的职责划分与选型逻辑
具体到hindsight的实现,三层记忆的分工是这样的:
Working memory(工作记忆)负责当前会话内的短期上下文。它本质上就是一个滑动窗口,但比简单的“保留最近 N 轮”要聪明一些——它会根据消息的重要程度做加权,工具调用的返回结果如果被标记为关键,会获得更高的保留优先级。这一层通常放在内存里,读写速度最快,进程重启就清空。
Episodic memory(情景记忆)负责跨会话的事件记录。每一次完整的任务执行、每一次工具调用链、每一次用户交互,都会被序列化成一条“情景”存起来。这一层用向量数据库做语义检索,你给它一个 query,它返回最相关的若干条历史情景。选向量库而不是传统数据库的原因很直接:agent 检索记忆时用的是自然语言描述,不是精确的 ID 或时间戳,语义相似度匹配才是对的检索方式。
Semantic memory(语义记忆)负责沉淀那些从多次情景中抽象出来的“知识”。比如 agent 在十次任务里都遇到了同一个 API 的限流问题,每次都做了重试,那么“这个 API 容易限流,需要加重试逻辑”就应该被抽象成一条语义记忆,而不是每次都要去翻十次情景记录。这一层的更新频率最低,但价值密度最高。
三层之间的数据流向是:working memory 满了之后,重要的部分沉淀到 episodic memory;episodic memory 积累到一定量之后,通过一个后台的“反思”过程抽象出 semantic memory。这个反思过程可以定时触发,也可以在特定事件(比如任务失败)后触发。
2.3 为什么选 MCP 作为对外接口
hindsight把记忆能力通过 MCP 协议暴露出来,这个选择我觉得是整个项目里最关键的设计决策之一。
MCP(Model Context Protocol)本质上是一套标准化的“工具调用协议”,它定义了 agent 框架怎么发现工具、怎么调用工具、怎么拿返回结果。在没有 MCP 之前,每个 agent 框架都有自己的工具接入方式,你要把一套记忆系统接进 LangChain、接进 AutoGPT、接进各种自研框架,得写 N 套适配代码。有了 MCP,你只需要实现一次 server,任何支持 MCP 的客户端都能直接调用。
这对hindsight这种“基础设施型”项目来说太重要了。它的定位不是某个特定 agent 框架的插件,而是一个独立的记忆服务。用 MCP 做接口,意味着它的用户群体不会被绑定在某个框架上。你今天用 A 框架,明天换 B 框架,hindsight照样能用。
而且 MCP 的 tool 定义方式天然适合记忆操作。记忆的增删改查可以很自然地映射成几个 tool:store_memory、retrieve_memory、list_memories、forget_memory。agent 在需要的时候主动调用这些 tool,而不是被动地等框架把记忆塞进上下文。这种“主动检索”的模式,比“被动注入”要高效得多——agent 只在真正需要的时候才去查记忆,平时不占用上下文窗口。
2.4 Docker 打包带来的部署便利性
把整个东西用 Docker 打包,解决的是一致性和可移植性问题。hindsight依赖向量数据库、可能还依赖 Redis 做缓存、依赖 Python 运行时和一堆库。如果让用户自己装,光是版本兼容问题就能劝退一半人。
Docker 方案把所有这些依赖封在一个镜像里,用户只需要docker compose up就能跑起来。对于本地开发和测试场景,这个便利性怎么强调都不过分。而且 Docker 的网络隔离特性,让hindsight可以跟其他服务(比如 agent 框架本身)跑在同一台机器上但互不干扰,通过内部网络通信。
提示:如果你是在 Windows 上跑 Docker,需要先确认 BIOS 里开启了虚拟化支持(Intel VT-x 或 AMD-V),否则 Docker Desktop 会报 “Virtualization support not detected” 然后启动失败。这个坑我踩过,排查了半天才发现是 BIOS 设置问题。
3. 核心机制拆解:记忆的写入、检索与反思
3.1 记忆写入:什么该记,什么不该记
记忆系统的第一个难题不是“怎么存”,而是“存什么”。如果什么都存,向量库很快就会被垃圾数据淹没,检索质量断崖式下降。如果存得太少,又起不到记忆的作用。
hindsight在写入侧做了几层过滤。第一层是显式标记:agent 在调用store_memory的时候,可以给记忆打上 importance 标签(high/medium/low)。这个标签直接影响后续的检索排序和淘汰策略。第二层是去重检测:新写入的记忆会先跟已有记忆做相似度比对,如果相似度超过阈值(默认 0.92),就不重复写入,而是更新已有记忆的时间戳和访问计数。第三层是容量控制:每一层记忆都有最大条数限制,超限时按“重要性 × 最近访问时间”的加权分数淘汰低分记忆。
这里面的参数选择是有讲究的。相似度阈值设成 0.92 而不是 0.95,是因为自然语言的语义相似度很难做到非常精确,0.95 会导致大量本该去重的记忆被重复写入,0.92 是一个实测下来比较平衡的值。淘汰策略里“最近访问时间”的权重不能太高,否则那些很重要但很少被访问的记忆(比如“这个用户的时区是 UTC+8”)会被误删。
写入时的另一个关键点是记忆的粒度。一条记忆不应该太长,也不应该太短。太长了检索时匹配不精准,太短了信息量不够。hindsight的建议是把单条记忆控制在 50 到 200 个 token 之间。超过 200 token 的内容应该拆成多条关联记忆,用 metadata 里的related_ids字段建立关联。
3.2 检索机制:token 三个点 key、query、value 的映射
热词里有一条“llm的token三个点key我是谁、query我在找什么、value我能提供什么”,这个说法其实很形象地概括了检索时的核心逻辑。在hindsight的检索流程里,这三个点对应的是:
- Key(我是谁):当前 agent 的身份和角色上下文。同一个记忆库,不同角色的 agent 去查,应该返回不同的结果。比如一个“客服 agent”和一个“运维 agent”去查“用户投诉”相关的记忆,前者应该看到处理话术,后者应该看到系统日志。
- Query(我在找什么):当前任务的自然语言描述。这是检索的主要输入,会被编码成向量去跟记忆库做相似度匹配。
- Value(我能提供什么):当前 agent 可用的工具和能力。这个信息用来做检索结果的二次过滤——如果检索到的记忆需要调用某个 agent 没有的工具才能用上,那这条记忆的排序就应该降低。
实际检索时,hindsight会先做向量相似度召回(取 top 50),然后用一个轻量级的 rerank 模型做精排(取 top 5 到 10),最后根据 key 和 value 的匹配度做加权调整。这个多阶段检索的流程,比单纯依赖向量相似度要靠谱得多。
检索的返回结果不是原始记忆文本,而是经过格式化的“记忆卡片”,包含记忆内容、时间戳、重要性标签、关联记忆 ID。这样 agent 拿到之后可以直接用,不需要再做额外的解析。
3.3 反思机制:从情景到语义的抽象过程
反思(reflection)是hindsight里最容易被忽视但价值最高的部分。它的作用是把零散的情景记忆抽象成更高层的语义记忆。
具体实现上,反思是一个定时任务,默认每 6 小时跑一次,也可以在手动触发。它会做这几件事:
- 扫描最近新增的 episodic memory,按主题聚类。
- 对每个聚类,用 LLM 生成一条概括性的语义记忆。
- 检查新生成的语义记忆是否跟已有的冲突,冲突时以新的为准并标记旧记忆为 deprecated。
- 更新记忆之间的关联图谱。
这个过程的成本不低,因为要调用 LLM。所以反思频率不能太高,6 小时是一个折中值。如果你的 agent 使用频率很低,可以调成 24 小时;如果很高,可以调到 2 小时,但要相应增加 LLM 的调用预算。
反思生成的语义记忆质量,很大程度上取决于聚类做得好不好。hindsight用的是基于向量相似度的层次聚类,聚类阈值默认 0.85。这个值调低会让聚类更粗,生成的语义记忆更泛化;调高会让聚类更细,生成的语义记忆更具体。没有绝对的好坏,取决于你的使用场景。
注意:反思过程会消耗 LLM token,如果你的 agent 本身就在用按 token 计费的 API,记得给反思任务单独设一个预算上限,避免它悄悄烧掉大量额度。
4. Docker 部署实操:从零把 hindsight 跑起来
4.1 环境准备与 Docker 安装确认
在开始之前,先确认你的机器上 Docker 已经装好并且能正常工作。Linux 上装 Docker 相对直接,用官方脚本或者包管理器都行。Windows 和 macOS 上建议用 Docker Desktop,省去手动配置的麻烦。
装完之后跑一下这个命令确认:
docker --version docker compose version两个命令都能正常输出版本号,说明环境没问题。如果docker compose报错说找不到命令,可能是你的 Docker 版本太老,需要升级到支持 Compose V2 的版本。
Windows 用户特别注意:Docker Desktop 启动时如果报 “Virtualization support not detected”,去 BIOS 里找 “Intel Virtualization Technology” 或 “SVM Mode” 之类的选项,把它设成 Enabled。不同主板这个选项的位置不一样,一般在 CPU 配置或者高级设置里面。
还有一个常见问题是 Docker 网络不通。如果你在公司内网环境,可能有代理或者防火墙限制。测试方法是跑一个最简单的容器看能不能拉镜像:
docker run --rm hello-world如果这个都跑不起来,先解决网络问题再往下走。
4.2 拉取镜像与目录结构规划
hindsight的镜像可以从公共镜像仓库拉取。假设镜像名是hindsight/server,拉取命令是:
docker pull hindsight/server:latest拉下来之后,先别急着跑。规划一下目录结构,把需要持久化的数据挂载出来。我一般会建这样一个目录:
hindsight-data/ ├── vector-db/ # 向量数据库的持久化数据 ├── config/ # 配置文件 ├── logs/ # 日志 └── backups/ # 定期备份向量数据库的数据一定要挂载出来,否则容器一删数据就没了。配置文件也挂载出来,方便修改不用进容器。日志挂载出来是为了排查问题方便。
4.3 docker-compose 配置详解
单跑一个容器也能用,但hindsight通常还需要一个向量数据库和一个缓存服务,用 docker-compose 编排更省事。下面是一个参考配置:
version: "3.8" services: hindsight: image: hindsight/server:latest container_name: hindsight-server ports: - "8765:8765" volumes: - ./hindsight-data/config:/app/config - ./hindsight-data/logs:/app/logs environment: - HINDSIGHT_VECTOR_DB_URL=http://vector-db:6333 - HINDSIGHT_CACHE_URL=redis://cache:6379 - HINDSIGHT_LOG_LEVEL=info - HINDSIGHT_REFLECTION_INTERVAL=21600 depends_on: - vector-db - cache restart: unless-stopped vector-db: image: qdrant/qdrant:latest container_name: hindsight-vector-db volumes: - ./hindsight-data/vector-db:/qdrant/storage restart: unless-stopped cache: image: redis:7-alpine container_name: hindsight-cache volumes: - ./hindsight-data/cache:/data restart: unless-stopped几个关键配置说明一下:
HINDSIGHT_REFLECTION_INTERVAL设成 21600 秒,也就是 6 小时,这是前面提到的反思间隔。restart: unless-stopped保证容器挂了会自动重启,但手动停掉之后不会自己起来,避免调试时反复重启干扰。
端口映射8765:8765是hindsight的 MCP 服务端口。如果你本机这个端口被占用了,改成别的,比如9765:8765,冒号左边是宿主机端口,右边是容器内端口,改左边就行。
向量数据库我选的是 Qdrant,原因是它单机部署简单、性能好、支持过滤查询。你也可以换成 Milvus 或者 Weaviate,但配置会复杂一些。对于hindsight这个场景,Qdrant 足够了。
4.4 启动与健康检查
配置写好之后,在 docker-compose.yml 所在目录执行:
docker compose up -d-d是后台运行。启动之后用这个命令看状态:
docker compose ps三个服务都显示running或者healthy就对了。如果哪个服务一直重启,用docker compose logs <服务名>看日志。
hindsight启动后,可以调一下健康检查接口确认服务正常:
curl http://localhost:8765/health返回{"status": "ok"}之类的 JSON 就说明服务起来了。如果连不上,先确认端口映射对不对,再确认容器是不是真的在跑。
提示:第一次启动时向量数据库需要初始化,可能会花十几秒。如果
hindsight启动时报连接向量库失败,等一会儿再重启一下hindsight容器就行。
5. MCP 接入配置:让 agent 真正用上记忆
5.1 MCP 协议基础与 hindsight 的 tool 定义
MCP 协议的核心概念是 server 和 client。hindsight作为 server,对外暴露一组 tool;agent 框架作为 client,通过 MCP 协议调用这些 tool。
hindsight暴露的 tool 主要有这几个:
| Tool 名称 | 功能 | 关键参数 |
|---|---|---|
store_memory | 写入一条记忆 | content, importance, layer, metadata |
retrieve_memory | 检索记忆 | query, layer, top_k, min_score |
list_memories | 列出记忆 | layer, limit, offset |
forget_memory | 删除记忆 | memory_id |
reflect | 手动触发反思 | layer, force |
store_memory的layer参数指定写入哪一层记忆,可选值working、episodic、semantic。retrieve_memory的top_k默认 5,min_score默认 0.7,低于这个相似度分数的结果会被过滤掉。
5.2 在主流 agent 框架中配置 MCP 连接
不同框架配置 MCP 的方式不太一样,但核心都是填 server 的地址和认证信息。以某个支持 MCP 的框架为例,配置文件里大概是这样:
{ "mcpServers": { "hindsight": { "url": "http://localhost:8765/mcp", "transport": "sse" } } }transport指定传输方式,hindsight支持 SSE 和 stdio 两种。SSE 适合服务端部署,stdio 适合本地进程调用。如果你是在浏览器扩展里配置 MCP 连接,通常是在扩展的设置页面里找到“MCP 连接”选项,填入上面的 URL。
配置好之后,agent 框架启动时会自动发现hindsight提供的 tool,并在需要的时候调用。你可以在框架的日志里看到 tool 调用的记录,确认记忆功能真的在工作。
5.3 记忆检索的 prompt 工程技巧
光把 MCP 接上还不够,agent 得知道什么时候该去查记忆、怎么查。这需要在 system prompt 里做引导。
一个比较有效的 prompt 模板是这样的:
你有一个长期记忆系统,可以通过 retrieve_memory 工具访问。 在以下情况下,你应该先检索记忆: 1. 用户提到“上次”、“之前”、“以前”等时间相关的词 2. 你需要回忆某个配置、密码、路径等具体信息 3. 当前任务跟之前做过的任务类似 检索时,query 应该用自然语言描述你在找什么,而不是关键词堆砌。这个引导的关键在于给出具体的触发条件,而不是笼统地说“需要时去查”。LLM 对具体条件的响应比抽象指令好得多。
另外,检索回来的记忆怎么用也有讲究。不要把检索结果直接塞进上下文就完事,最好让 agent 先判断一下“这些记忆跟当前任务相关吗”,不相关的就忽略。这能避免旧记忆干扰当前任务。
5.4 多 agent 场景下的记忆隔离与共享
如果你同时跑多个 agent,记忆的隔离和共享就需要考虑。hindsight通过 namespace 机制支持这个需求。每个 agent 可以有自己的 namespace,也可以共享一个公共 namespace。
配置方式是在 MCP 连接 URL 里加 namespace 参数:
http://localhost:8765/mcp?namespace=agent-a公共记忆放在sharednamespace 里,所有 agent 都能读写。私有记忆放在各自的 namespace 里,互相隔离。检索时默认只查自己的 namespace,需要查公共记忆时显式指定。
这个机制在团队协作场景下特别有用。比如一个客服团队有多个 agent,公共的产品知识放在 shared namespace,每个 agent 跟用户的私人对话记录放在各自 namespace,既共享了知识又保护了隐私。
6. 常见问题与排查技巧实录
6.1 启动类问题速查
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 容器启动后立即退出 | 配置错误或依赖服务未就绪 | docker compose logs hindsight看报错 |
| 端口被占用 | 宿主机 8765 端口已被其他程序使用 | netstat -tlnp | grep 8765找到占用进程 |
| 向量库连接失败 | 向量库还没初始化完 | 等 30 秒后重启 hindsight 容器 |
| Windows 上 Docker 启动失败 | 虚拟化未开启 | 进 BIOS 开启 VT-x/AMD-V |
| 拉镜像超时 | 网络问题 | 配置镜像加速器或检查网络 |
6.2 记忆检索质量差的排查思路
检索质量差是最常见的问题,表现是 agent 查记忆时返回一堆不相关的结果,或者该查到的查不到。排查顺序是这样的:
先看写入侧。如果记忆本身写得不好,检索再好也没用。检查一下写入的记忆是不是太短(少于 20 token)或者太长(超过 300 token),太短的信息量不够,太长的向量表示会模糊。另外检查 importance 标签是不是都设成了 low,那样检索排序会很靠后。
再看检索参数。min_score设得太高会过滤掉太多结果,设得太低会引入噪音。默认 0.7 是个起点,如果你的记忆内容比较专业(比如大量技术术语),可以调到 0.65;如果内容比较通用,可以调到 0.75。
最后看 embedding 模型。hindsight默认用的 embedding 模型对通用文本效果不错,但如果你记忆的内容是特定领域(比如医学、法律),可能需要换一个在该领域微调过的模型。这个在配置文件里可以改。
6.3 反思任务失败的常见原因
反思任务失败通常有几个原因:LLM API 调用超时、token 超限、聚类结果为空。
API 超时的话,检查一下你的 LLM 服务商是不是有速率限制,反思任务批量调用时容易触发限流。解决办法是降低反思频率,或者在配置里设置每次反思处理的最大记忆条数。
token 超限的话,说明单次反思处理的记忆太多了。把reflection_batch_size调小,默认是 100,可以调到 50 甚至 30。
聚类结果为空的话,说明最近新增的情景记忆太少,或者相似度阈值设得太高导致聚不出类。这种情况不用管,等记忆积累多了自然会好。
6.4 性能调优的几个实操心得
跑了一段时间之后,如果发现检索变慢,可以从这几个方向调优:
向量库的索引类型对性能影响很大。Qdrant 默认用的是 HNSW 索引,对于百万级以下的记忆量够用了。如果记忆量超过百万,可以考虑调大 HNSW 的m参数(默认 16,可以调到 32),代价是内存占用增加。
缓存命中率是另一个关键指标。hindsight会把高频检索的 query 和结果缓存到 Redis 里。如果缓存命中率低于 30%,说明 query 的重复度不高,可以考虑调大缓存容量或者延长缓存过期时间。
还有一个容易被忽视的点是记忆的定期清理。working memory 会随着会话结束自动清理,但 episodic memory 和 semantic memory 会一直累积。建议每隔一段时间(比如一个月)做一次手动清理,把那些 importance 为 low 且超过 90 天没被访问过的记忆删掉。这个操作可以通过forget_memorytool 批量做,也可以直接操作向量库。
注意:清理记忆之前一定要先备份。向量库的数据目录直接打包备份就行,恢复的时候解压回去重启容器即可。
7. 我对这套方案的实际使用体会
hindsight这套东西我用了一段时间,最大的感受是:agent memory 这件事,难点不在“存”,而在“取”和“忘”。存进去容易,但要在正确的时候取出正确的东西,并且在该忘的时候果断忘掉,这才是真正考验设计的地方。
三层记忆的分法我觉得是这套方案里最值钱的部分。working memory 保证了当前会话的流畅性,episodic memory 保证了跨会话的连续性,semantic memory 保证了知识的沉淀。三层各管各的,互不干扰,但又通过反思机制打通。这个结构比那种“所有记忆一锅炖”的方案要清晰得多。
MCP 接口的选择也很务实。我试过把它接进两个不同的 agent 框架,配置改改就能用,不用改代码。这种可移植性对于基础设施型项目来说太重要了。
如果让我给一个建议,我会说:先把 working memory 和 episodic memory 跑通,semantic memory 的反思机制可以晚点再开。反思机制虽然价值高,但它依赖 LLM 调用,成本和复杂度都上一个台阶。先用前两层把基本流程跑顺,确认记忆检索确实能帮到 agent,再考虑上反思。
另外一个小技巧:在 system prompt 里明确告诉 agent “检索记忆时,如果第一次没查到有用的东西,换个说法再查一次”。LLM 有时候第一次 query 写得不好,换个角度重新检索往往能命中。这个简单的重试策略,实测能把检索命中率提高不少。