1. 从“hindsight”说起:为什么我们需要给 Agent 装上“后视之明”
第一次看到“hindsight”这个词,我脑子里蹦出来的不是词典释义,而是自己踩过的一个坑。去年做一套基于 LLM 的自动化运维助手,用户问“上周那台出问题的机器后来怎么处理的”,模型一脸茫然——它压根不记得三天前发生过什么。那一刻我才真正意识到,Agent 的智能上限,很多时候不是被推理能力卡住的,而是被记忆卡住的。
hindsight 直译是“后见之明”,放到 Agent 语境里,它指的是一套让智能体能够回溯、检索、复用历史交互与经验的能力体系。你可以把它理解成给 Agent 装了一个“可检索的长期记忆库”,而不是每次对话都从零开始。它要解决的问题非常具体:多轮任务中上下文丢失、跨会话经验无法沉淀、工具调用结果无法被后续步骤引用。这套东西适合谁?做 Agent 应用的开发者、折腾 MCP 协议的工具党、以及所有被“模型记不住事”折磨过的人。
围绕 hindsight 这个核心,会牵扯出一串关键词:agent memory、LLM、MCP、Docker。它们不是孤立的热词,而是一条完整的落地链路——LLM 是大脑,agent memory 是记忆,MCP 是连接外部世界的协议,Docker 是让这一切跑起来的容器底座。接下来我会把这四块拆开揉碎,讲清楚它们各自扮演什么角色,以及怎么把它们拼成一个能用的 hindsight 系统。
2. hindsight 的整体设计思路:记忆到底该怎么存
2.1 为什么“塞进上下文”不是长久之计
很多人做 Agent 记忆的第一反应,是把历史对话全部拼进 prompt。我早期也这么干过,结果很惨:token 成本飙升、模型注意力被稀释、关键信息淹没在废话里。一个跑了 50 轮的任务,上下文能轻松突破几万 token,模型反而变笨了。
hindsight 的核心思路是分层记忆,而不是无脑堆上下文。我把它分成三层:
- 工作记忆(working memory):当前任务正在用的短期信息,比如最近几轮对话、当前工具调用的中间结果。这层可以放在上下文里,但要严格控制长度。
- 情景记忆(episodic memory):过去发生过的具体事件,比如“某次部署失败的原因”。这层要落库,按需检索。
- 语义记忆(semantic memory):从多次事件中提炼出的规律,比如“这台机器磁盘超过 90% 就会告警”。这层是知识,更新频率低。
这个分层不是拍脑袋来的,它对应了认知科学里人类记忆的基本结构。落到工程上,好处是检索时能按需取用,而不是全量加载。
2.2 记忆的 token 三元组:key、query、value
热词里有一句特别精辟的描述:“llm 的 token 三个点 key 我是谁、query 我在找什么、value 我能提供什么”。这其实就是注意力机制的直觉解释,也是记忆检索的设计蓝本。
在 hindsight 里,我把每条记忆都抽象成一个三元组:
| 维度 | 含义 | 工程落地 |
|---|---|---|
| key | 这条记忆“是谁” | 唯一 ID + 类型标签(事件/知识/工具结果) |
| query | “我在找什么” | 检索时的向量或关键词 |
| value | “我能提供什么” | 记忆正文 + 元数据(时间、来源、置信度) |
检索时,query 和 key 做匹配(向量相似度或关键词),命中后返回 value。这个模型简单但极其好用,后面讲 MCP 工具设计时还会用到同样的思路。
2.3 为什么选 MCP 而不是自己写一套接口
MCP(Model Context Protocol)是一个软件协议,注意,它是软件协议,不是硬件协议——热词里有人问“mcp 是软件协议,硬件协议那个概念叫什么来着”,硬件那边对应的概念通常是总线协议或接口标准,两者完全不是一个层面。MCP 的价值在于,它把“模型如何调用外部能力”这件事标准化了。
自己写接口当然可以,但你会面临:每个工具一套鉴权、一套参数格式、一套错误处理。MCP 把这些统一了,Agent 只要按协议描述工具,就能即插即用。hindsight 的记忆读写、工具调用结果回填,全都通过 MCP 暴露成标准工具,这样换模型、换框架都不用重写。
2.4 Docker 在整套方案里的位置
Docker 解决的是“环境一致性”问题。记忆库要跑数据库、MCP 服务要跑进程、LLM 网关要跑服务,如果全裸装在宿主机上,换台机器就崩。用 Docker Compose 把这些服务编排起来,一条命令拉起整套 hindsight 环境,这是最省心的做法。后面我会给出完整的 compose 配置。
3. 核心细节解析:记忆库、MCP 与 LLM 的协作要点
3.1 记忆库选型:向量库还是关系库
这是被问得最多的问题。我的结论是:两者都要,各司其职。
- 向量库(如 pgvector、Milvus)负责语义检索,解决“意思相近但用词不同”的匹配问题。
- 关系库(如 MySQL、PostgreSQL)负责结构化查询,解决“某时间段内某类型的事件”这类精确过滤。
hindsight 里我用的方案是 PostgreSQL + pgvector,一个库同时搞定两种需求,省得维护两套存储。热词里出现的 tencentdb agent memory 也是类似思路,把记忆能力做进数据库层。如果你只是做原型,SQLite + 内存向量索引也够用,但上生产还是建议上 PG。
3.2 记忆写入的时机:什么时候该记
记太勤,库会爆炸;记太懒,关键信息丢失。我总结的写入触发点有三个:
- 任务节点完成时:一个子任务结束,把输入、输出、结果状态打包写入。
- 工具调用返回异常时:失败经验比成功经验更值钱,必须记。
- 用户显式纠正时:用户说“不对,应该是这样”,这条纠正要立刻落库并提高权重。
注意:不要每轮对话都写。我见过有人把每条消息都存成记忆,结果检索时全是噪音,模型反而被带偏。
3.3 MCP 工具设计:把记忆操作暴露成标准能力
hindsight 通过 MCP 暴露的核心工具大概有这几个:
memory_write:写入一条记忆,参数含 key、value、类型、时间戳。memory_search:按 query 检索,返回 top-k 相关记忆。memory_forget:软删除或降权某条记忆。memory_summarize:把多条情景记忆压缩成一条语义记忆。
这里有个设计细节值得说:memory_search的返回结果要带置信度和时间衰减。一条三年前的记忆和一条昨天的记忆,权重不该一样。我在实现里加了个简单的时间衰减因子,越久远的记忆得分越低,实测下来检索质量提升明显。
3.4 LLM 在 hindsight 里的双重角色
LLM 在这里干两件事:一是生成记忆摘要,把冗长的工具输出压缩成一句话;二是判断记忆相关性,在检索结果里做二次筛选。热词里提到的 “llm as judge” 就是这个用法。
但要注意,让 LLM 做判断会引入延迟和成本。我的做法是:先用向量检索粗筛出 top-20,再用 LLM 精排出 top-5。这样既保证质量,又不至于每次都全量过模型。
4. 实操过程:从零搭一套 hindsight 环境
4.1 环境准备与 Docker 安装
先说 Docker 安装。Windows 用户走 Docker Desktop,安装前务必确认 BIOS 里虚拟化已开启,否则会报 “virtualization support not detected, docker desktop failed to start”。这个报错我见过太多次,九成是虚拟化没开或者和 Hyper-V/WSL2 冲突。
Linux 用户直接用官方脚本或包管理器装 docker engine + docker compose plugin。装完跑一句docker compose version确认插件在。
提示:Windows 11 装 Docker Desktop 建议用 WSL2 后端,比 Hyper-V 后端省资源,文件挂载性能也更好。
4.2 用 Docker Compose 编排整套服务
下面是我实际在用的 compose 配置,包含 PostgreSQL(带 pgvector)、MCP 服务、以及一个 LLM 网关:
version: "3.9" services: pg: image: pgvector/pgvector:pg16 environment: POSTGRES_PASSWORD: hindsight POSTGRES_DB: memory ports: - "5432:5432" volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s retries: 5 mcp-memory: build: ./mcp-memory depends_on: pg: condition: service_healthy environment: DATABASE_URL: postgres://postgres:hindsight@pg:5432/memory ports: - "8080:8080" llm-gateway: image: ghcr.io/example/llm-gateway:latest environment: UPSTREAM_URL: http://host.docker.internal:11434 ports: - "8090:8090" volumes: pgdata:几个关键点解释一下:
pgvector/pgvector:pg16这个镜像自带向量扩展,省得自己编译。healthcheck很重要,MCP 服务必须等数据库就绪再启动,否则连接会失败。host.docker.internal让容器访问宿主机上的 LLM 服务,本地跑 Ollama 时特别有用。
4.3 初始化记忆表结构
数据库起来后,建表。核心就两张:记忆主表和向量索引。
CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE memories ( id BIGSERIAL PRIMARY KEY, mem_key TEXT NOT NULL, mem_type TEXT NOT NULL, content TEXT NOT NULL, embedding vector(768), confidence REAL DEFAULT 1.0, created_at TIMESTAMPTZ DEFAULT now(), last_access TIMESTAMPTZ DEFAULT now() ); CREATE INDEX ON memories USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100); CREATE INDEX idx_mem_type ON memories (mem_type); CREATE INDEX idx_created ON memories (created_at DESC);ivfflat索引的lists参数按数据量调,一般取sqrt(行数)。数据少的时候不建索引反而更快,别急着加。
4.4 记忆写入与检索的核心代码
写入逻辑,重点是生成 embedding 和计算初始置信度:
import psycopg2 from datetime import datetime def write_memory(conn, key, content, mem_type, embedding, confidence=1.0): with conn.cursor() as cur: cur.execute( """INSERT INTO memories (mem_key, mem_type, content, embedding, confidence) VALUES (%s, %s, %s, %s, %s) RETURNING id""", (key, mem_type, content, embedding, confidence) ) return cur.fetchone()[0]检索逻辑,带时间衰减:
def search_memory(conn, query_embedding, top_k=5, decay_days=30): with conn.cursor() as cur: cur.execute( """ SELECT id, content, confidence, 1 - (embedding <=> %s::vector) AS similarity, EXTRACT(EPOCH FROM (now() - created_at)) / 86400 AS age_days FROM memories ORDER BY embedding <=> %s::vector LIMIT %s """, (query_embedding, query_embedding, top_k * 4) ) rows = cur.fetchall() scored = [] for r in rows: sim = r[3] age = r[4] decay = 0.5 ** (age / decay_days) score = sim * decay * r[2] scored.append((score, r[1])) scored.sort(reverse=True) return scored[:top_k]0.5 ** (age / decay_days)是半衰期公式,30 天衰减一半。这个参数按业务调,运维场景可以设长一点,闲聊场景设短一点。
4.5 把记忆接入 MCP 服务
MCP 服务本质是个 HTTP 服务,暴露工具描述和调用端点。核心是把上面的读写函数包成标准工具:
from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class WriteReq(BaseModel): key: str content: str mem_type: str @app.post("/tools/memory_write") def memory_write(req: WriteReq): emb = embed(req.content) mid = write_memory(conn, req.key, req.content, req.mem_type, emb) return {"id": mid, "status": "ok"} @app.post("/tools/memory_search") def memory_search(query: str, top_k: int = 5): emb = embed(query) results = search_memory(conn, emb, top_k) return {"results": [{"content": c, "score": s} for s, c in results]}工具描述要写清楚,因为 LLM 是靠描述来决定调不调、怎么调的。描述里把参数含义、返回格式、适用场景都写明白,模型调用准确率会高很多。
5. 常见问题与排查技巧实录
5.1 Docker 相关的高频故障
| 现象 | 原因 | 解决 |
|---|---|---|
| Docker Desktop 启动失败,提示虚拟化未检测到 | BIOS 虚拟化关闭或与 Hyper-V 冲突 | 进 BIOS 开 VT-x/AMD-V,Windows 关闭冲突的 Hyper-V 功能 |
| 容器间网络不通 | 不在同一 network,或用了 localhost | 用 compose 默认网络,服务名当主机名 |
| 数据库连接被拒 | 服务启动顺序问题 | 加 healthcheck + depends_on condition |
| 挂载卷权限错误 | 容器内用户 UID 与宿主机不一致 | 指定 user 或调整目录权限 |
5.2 记忆检索质量差的排查思路
检索不准,先别怪模型,按这个顺序查:
- embedding 模型是否一致:写入和检索必须用同一个 embedding 模型,换模型等于换了一套坐标系。
- top-k 是否太小:先放大到 20 看召回,再考虑精排。
- 时间衰减是否过猛:衰减太快会把有用老记忆全压下去。
- 记忆内容是否太碎:一条记忆塞太多信息,向量会“糊”,检索自然不准。
实操心得:我习惯在写入时让 LLM 顺手生成一句 20 字以内的摘要,把摘要和原文一起存,检索时用摘要做向量,原文做返回。这样向量更聚焦,命中率明显提升。
5.3 MCP 接入时的坑
热词里有人问 “codex 无法找到 mcp”“codex 接入 figma mcp 怎么授权”,这类问题本质是工具发现和鉴权。MCP 服务要先能被客户端发现(通常是配置文件里声明服务地址),再解决鉴权(token 或 OAuth)。我踩过的坑是:服务地址写成了容器内地址,客户端在宿主机根本访问不到。记住,客户端在哪,就用它能访问到的地址。
另一个常见问题是工具描述里的 schema 不合法,导致 “provider rejected the request schema or tool payload”。MCP 对参数 schema 有格式要求,JSON Schema 写错一个字段就整个工具不可用。写完用在线校验器过一遍,能省很多调试时间。
5.4 记忆污染与安全
热词里提到 “agentpoison: red-teaming llm agents via poisoning memory”,这是个真实威胁:如果攻击者能往记忆库里写脏数据,Agent 后续行为就会被带偏。防御手段有几个:
- 写入来源分级,外部输入的记忆置信度默认调低。
- 关键决策前对检索到的记忆做一次 LLM 校验,判断是否与当前任务矛盾。
- 定期跑一致性检查,把互相冲突的记忆标出来人工复核。
这套东西不是可选项,只要你的 Agent 会长期运行,记忆安全就必须考虑。
6. 我在这套方案上的一些个人体会
折腾 hindsight 这套东西大半年,最大的感受是:记忆系统的难点从来不在存储,而在“什么时候记、记什么、怎么取”。存储层用 PG 加 pgvector 已经足够,真正花时间的是调检索权重、设计写入触发点、以及处理记忆冲突。
还有一个反直觉的发现:不是所有 Agent 都需要长期记忆。短任务型 Agent 用工作记忆就够了,硬上长期记忆反而增加复杂度和出错面。判断标准很简单——如果你的任务需要“跨会话引用历史”,那才值得上 hindsight;如果每次任务都是独立的,别给自己找麻烦。
最后分享一个我一直在用的小技巧:给记忆库加一个last_access字段,每次检索命中就更新。定期把长期没被访问的记忆归档或降权,库会越来越“干净”,检索速度和质量都会稳步提升。这个动作我设了个定时任务每周跑一次,效果比任何调参都实在。