hindsight 实战:LLM Agent 事后记忆提炼与 MCP 服务搭建
发布时间:2026/10/2 19:38:55来源:尧图网络
1. 从“事后诸葛亮”说起hindsight 到底想解决什么问题第一次看到 “hindsight” 这个词我脑子里蹦出来的就是“事后诸葛亮”。但在 Agent Memory 这个语境里它其实指向一个非常具体、也非常痛的技术问题当 LLM Agent 已经执行完一轮任务之后我们如何让它“回头看”从历史交互中提炼出可复用的记忆而不是每次都从零开始这个问题的背景是这样的。现在大部分 LLM Agent 的架构本质上还是“无状态”的——每次对话或任务执行上下文窗口里塞进去的东西任务结束就丢了。下次再来一个类似的任务Agent 还是白纸一张。你可能会说不是有 RAG 吗不是有向量数据库吗对但 RAG 解决的是“检索外部知识”它不解决“Agent 自己经历过什么、学到了什么”这个问题。Agent Memory 要解决的是后者让 Agent 拥有跨会话、跨任务的记忆能力。hindsight 这个项目我理解它的核心定位就是Agent Memory 的“事后提炼层”。它不负责实时记忆写入也不负责向量检索它负责的是在一段交互结束之后回过头去分析这段交互提取出值得长期保留的记忆片段然后以结构化的方式存下来。这个“回头看”的动作就是 hindsight 的精髓。为什么这个方向值得做因为现在 Agent 的短期记忆working memory和长期记忆long-term memory之间是断层的。短期记忆靠上下文窗口长期记忆靠人工预置的知识库或向量库中间缺了一个“自动转化”的环节。hindsight 补的就是这个环节。它适合谁适合正在做 LLM Agent 应用、已经踩过“Agent 记不住事”这个坑、想认真解决记忆问题的开发者。如果你只是做个 demo 玩玩可能用不上但如果你要让 Agent 在真实业务里持续运行、持续进化hindsight 这类思路是绕不过去的。2. 核心设计思路拆解为什么是“事后提炼”而不是“实时写入”2.1 实时记忆写入的三大坑在讲 hindsight 的设计之前我先说说为什么“实时写入记忆”这条路不好走。我试过在 Agent 每轮对话后直接把内容塞进向量库结果踩了三个坑。第一个坑是噪声污染。Agent 的交互里大量内容是废话、中间步骤、试错过程。你把这些全写进记忆库检索的时候就会被噪声淹没。比如 Agent 在规划阶段说“我先试试方案 A不行再换 B”这句话本身没有记忆价值但如果你实时写入它就会变成一条“记忆”下次检索时可能被召回干扰判断。第二个坑是上下文缺失。实时写入的时候你只有当前这一轮的信息不知道这个信息在整体任务中处于什么位置。一条“用户说要用 MySQL”的记录如果不知道这是在讨论数据库选型还是在做数据迁移它的记忆价值是完全不同的。实时写入拿不到这个全局视角。第三个坑是写入频率与成本。每轮都写入意味着每轮都要调用 LLM 做摘要或提取token 成本线性增长。而且写入太频繁记忆库膨胀极快检索效率直线下降。2.2 hindsight 的“事后提炼”逻辑hindsight 的思路是反过来的不急着写等一段交互结束后拿着完整的上下文回头做一次高质量的提炼。这个“一段交互”可以是一次完整对话、一个任务执行周期、或者人为划分的一个 session。提炼的输入是完整的交互历史输出是若干条结构化记忆。这个设计的好处很明显。首先有全局视角提炼时可以判断哪些信息是真正重要的、哪些是中间过程可以丢弃的。其次写入频率低一次交互只提炼一次成本可控。第三记忆质量高因为提炼时可以用更强的模型、更复杂的 prompt做更精细的抽取和归纳。我打个比方。实时写入就像开会时每分钟记一次笔记记下来的全是碎片事后提炼就像开完会后写会议纪要只记结论、决策和待办事项。后者显然更有价值。hindsight 做的就是“写会议纪要”这件事。2.3 与 MCP 协议的关系热词里出现了 MCP这里也说一下 hindsight 和 MCP 的关系。MCP 是 Anthropic 推出的模型上下文协议本质上是给 LLM 应用提供一个标准化的工具调用和数据接入方式。hindsight 如果要做成一个通用的 Agent Memory 服务通过 MCP Server 的形式暴露接口是很自然的选择。这样任何支持 MCP 的客户端比如 Claude Desktop、各种 IDE 插件都能直接调用 hindsight 的记忆提炼能力而不需要每个应用自己实现一套。从架构上看hindsight 作为 MCP Server 提供的能力大概包括提交一段交互历史、触发记忆提炼、查询已有记忆、更新或删除记忆。客户端只需要把交互数据传进来剩下的提炼逻辑由 hindsight 内部完成。这种解耦设计让 hindsight 可以独立演进不被具体应用绑架。3. 核心细节解析记忆提炼的四个关键环节3.1 交互历史的预处理与分片hindsight 拿到的原始输入是一段交互历史可能是几十轮对话也可能是几百条工具调用记录。直接扔给 LLM 提炼是不现实的token 放不下而且噪声太多。所以第一步是预处理和分片。预处理包括几个动作。去重把重复的工具调用结果、重复的系统提示去掉。脱敏如果交互里包含敏感信息比如用户隐私数据需要在提炼前做脱敏处理这个后面会细说。格式化把不同来源的交互记录统一成一种结构化格式比如 JSON Lines每条记录包含时间戳、角色、内容类型、内容体。分片策略取决于交互的长度和复杂度。我的经验是按“任务边界”分片比按“固定长度”分片效果好得多。一个任务从开始到结束中间的所有交互作为一个分片这样提炼出来的记忆有完整的任务上下文。如果交互太长可以在任务内部按“子目标”再分片。固定长度分片比如每 50 轮切一刀的问题是可能把一件事切成两半提炼出来的记忆不完整。3.2 记忆类型的分类体系hindsight 提炼出来的记忆不是一锅粥而是有分类的。根据我的实践Agent Memory 至少可以分为以下几类每类的提炼策略不同。记忆类型说明提炼重点示例事实记忆关于世界、用户、领域的客观事实准确性、去重“用户所在公司使用 MySQL 8.0”偏好记忆用户的喜好、习惯、约束稳定性、时效性“用户偏好简洁的回答风格”经验记忆Agent 自己总结的成功/失败经验可复用性、条件性“在这个 API 上先查文档再调用成功率更高”任务记忆具体任务的执行记录和结果可检索性、关联性“2024-01-15 完成了数据迁移任务用了方案 B”关系记忆实体之间的关系图结构、一致性“项目 X 的负责人是张三”这个分类不是死的你可以根据业务需要调整。但核心思想是不同类型的记忆提炼时的 prompt 和存储结构应该不同。事实记忆要的是准确经验记忆要的是可迁移任务记忆要的是可检索。用一套 prompt 打天下效果一定打折扣。3.3 提炼 Prompt 的设计要点提炼 Prompt 是 hindsight 的核心资产。我踩过的坑是一开始用很简单的 prompt比如“请从以下对话中提取重要信息”结果 LLM 提取出来的东西要么太泛“用户问了问题”要么太细“用户在第一轮说了‘你好’”。好的提炼 Prompt 应该包含这几个要素。角色设定明确告诉 LLM 它是一个记忆提炼专家不是对话摘要器。分类指令告诉它要按哪几类提取每类的标准是什么。正反例给几个“应该提取”和“不应该提取”的例子这个对效果提升非常明显。输出格式强制 JSON 输出每个记忆条目包含类型、内容、置信度、来源片段。去重指令告诉它如果多条记忆表达的是同一件事合并成一条。还有一个技巧是分步提炼。不要指望一次 LLM 调用就完成所有提炼。可以先让 LLM 做一轮“候选记忆生成”再做一轮“记忆筛选与合并”最后做一轮“记忆分类与结构化”。多轮调用成本高一些但质量提升是值得的。3.4 记忆的存储与索引提炼出来的记忆存哪里这取决于你的规模。小规模用 SQLite 就够了中等规模上 PostgreSQL pgvector大规模才需要考虑专门的向量数据库。我的建议是不要一上来就上重型基础设施先用最简单的方案跑通流程等记忆量真的上来了再迁移。存储结构上每条记忆至少包含这些字段记忆 ID、类型、内容、向量表示、来源交互 ID、创建时间、最后访问时间、访问次数、置信度。向量表示用于语义检索来源交互 ID 用于追溯访问统计用于后续的记忆淘汰和权重调整。索引方面除了向量索引关键词索引和类型索引也很重要。很多查询是“找出所有关于用户偏好的记忆”或者“找出所有和项目 X 相关的记忆”这种查询用向量检索反而不如结构化过滤来得准。混合检索向量 关键词 结构化过滤是实际生产环境里最稳的方案。4. 实操过程从零搭建一个 hindsight 风格的记忆提炼服务4.1 环境准备与依赖安装我以 Docker 部署为例讲一下完整的搭建过程。为什么用 Docker因为 hindsight 这类服务通常依赖多个组件LLM 调用、向量库、数据库用 Docker Compose 编排最省心。首先确保 Docker Desktop 已经装好。Windows 用户如果遇到 “Virtualization support not detected” 的报错需要进 BIOS 开启虚拟化支持Intel VT-x 或 AMD-V。这个坑我踩过折腾了半天才发现是 BIOS 里没开。# 检查 Docker 是否正常运行 docker --version docker compose version # 创建工作目录 mkdir -p hindsight-service cd hindsight-service目录结构大概是这样hindsight-service/ ├── docker-compose.yml ├── .env ├── app/ │ ├── main.py │ ├── extractor.py │ ├── storage.py │ └── prompts/ │ ├── extract.txt │ └── merge.txt └── data/ └── (持久化数据)4.2 Docker Compose 编排配置docker-compose.yml 里我放了三个服务hindsight 主服务、PostgreSQL带 pgvector、Redis做任务队列和缓存。version: 3.9 services: hindsight: build: ./app ports: - 8080:8080 environment: - DATABASE_URLpostgresql://hindsight:hindsightdb:5432/hindsight - REDIS_URLredis://cache:6379/0 - LLM_API_KEY${LLM_API_KEY} - LLM_BASE_URL${LLM_BASE_URL} depends_on: db: condition: service_healthy cache: condition: service_started volumes: - ./data:/app/data db: image: pgvector/pgvector:pg16 environment: - POSTGRES_USERhindsight - POSTGRES_PASSWORDhindsight - POSTGRES_DBhindsight volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: [CMD-SHELL, pg_isready -U hindsight] interval: 5s timeout: 5s retries: 5 cache: image: redis:7-alpine volumes: - redisdata:/data volumes: pgdata: redisdata:这里有个细节pgvector 的镜像要用pgvector/pgvector:pg16不要用官方的postgres:16否则向量扩展装不上。这个坑很隐蔽我第一次搭的时候用了官方镜像结果建向量索引时报错 “type vector does not exist”查了半天才发现是镜像选错了。4.3 数据库表结构设计记忆存储的表结构我设计了三张核心表。-- 记忆主表 CREATE TABLE memories ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), memory_type VARCHAR(32) NOT NULL, content TEXT NOT NULL, embedding vector(1536), source_session_id UUID, confidence FLOAT DEFAULT 0.8, created_at TIMESTAMP DEFAULT NOW(), last_accessed_at TIMESTAMP DEFAULT NOW(), access_count INT DEFAULT 0, metadata JSONB DEFAULT {} ); -- 交互会话表 CREATE TABLE sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), agent_id VARCHAR(128), started_at TIMESTAMP, ended_at TIMESTAMP, raw_history JSONB, processed BOOLEAN DEFAULT FALSE ); -- 记忆关联表用于关系记忆 CREATE TABLE memory_relations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), from_memory_id UUID REFERENCES memories(id), to_memory_id UUID REFERENCES memories(id), relation_type VARCHAR(64), weight FLOAT DEFAULT 1.0 ); -- 向量索引 CREATE INDEX ON memories USING ivfflat (embedding vector_cosine_ops) WITH (lists 100); -- 类型索引 CREATE INDEX idx_memories_type ON memories(memory_type); -- 时间索引 CREATE INDEX idx_memories_created ON memories(created_at DESC);embedding 维度 1536 是 OpenAI text-embedding-3-small 的维度如果你用别的 embedding 模型改这个数字就行。ivfflat 索引的 lists 参数经验值是sqrt(总行数)初期数据少的时候设 100 够用数据量上百万了再调。4.4 记忆提炼核心逻辑实现提炼逻辑我用 Python 写核心是一个 Extractor 类。这里展示关键部分。import json from openai import OpenAI class MemoryExtractor: def __init__(self, api_key, base_url, modelgpt-4o): self.client OpenAI(api_keyapi_key, base_urlbase_url) self.model model def preprocess(self, raw_history): 预处理去重、格式化、分片 # 去重相同内容的工具调用结果只保留一次 seen set() cleaned [] for item in raw_history: key (item.get(role), item.get(content, )[:200]) if key not in seen: seen.add(key) cleaned.append(item) return cleaned def extract_candidates(self, history_chunk): 第一轮生成候选记忆 prompt self._load_prompt(extract.txt) history_text self._format_history(history_chunk) response self.client.chat.completions.create( modelself.model, messages[ {role: system, content: prompt}, {role: user, content: history_text} ], response_format{type: json_object}, temperature0.3 ) return json.loads(response.choices[0].message.content) def merge_and_filter(self, candidates): 第二轮合并去重、筛选 prompt self._load_prompt(merge.txt) response self.client.chat.completions.create( modelself.model, messages[ {role: system, content: prompt}, {role: user, content: json.dumps(candidates, ensure_asciiFalse)} ], response_format{type: json_object}, temperature0.2 ) return json.loads(response.choices[0].message.content) def _format_history(self, history): lines [] for item in history: role item.get(role, unknown) content item.get(content, ) lines.append(f[{role}] {content}) return \n.join(lines) def _load_prompt(self, filename): with open(fprompts/{filename}, r, encodingutf-8) as f: return f.read()temperature 设 0.3 和 0.2 是有讲究的。提炼任务要的是稳定和准确不是创意温度太高会导致同一段历史每次提炼出来的记忆不一样这对记忆系统是灾难。我试过 temperature0.7结果同一段对话跑两次一次提取了 5 条记忆一次提取了 3 条内容还不一样完全没法用。4.5 提炼 Prompt 模板extract.txt 的内容大概是这样你是一个 Agent 记忆提炼专家。你的任务是从一段 Agent 交互历史中提取出值得长期保留的记忆。 记忆分为五类 1. fact关于世界、用户、领域的客观事实 2. preference用户的喜好、习惯、约束 3. experienceAgent 总结的成功或失败经验 4. task具体任务的执行记录和结果 5. relation实体之间的关系 提取原则 - 只提取有长期价值的信息忽略寒暄、中间步骤、试错过程 - 每条记忆必须能独立理解不依赖上下文 - 如果多条信息表达同一件事合并为一条 - 不确定是否值得保留的宁可不提取 输出 JSON 格式 { memories: [ { type: fact|preference|experience|task|relation, content: 记忆内容一句话说清楚, confidence: 0.0-1.0, source_quote: 原文中支撑这条记忆的片段 } ] } 不应该提取的例子 - 用户说了你好寒暄无价值 - Agent 调用了搜索工具中间步骤无价值 - 用户问了一个问题太泛无信息量 应该提取的例子 - 用户所在团队使用 PostgreSQL 作为主数据库事实 - 用户偏好用中文交流回答要简洁偏好 - 调用某 API 时需要先获取 token 再请求数据经验merge.txt 的内容你是一个记忆合并专家。输入是一批候选记忆你的任务是 1. 合并表达同一件事的记忆 2. 删除置信度低于 0.5 的记忆 3. 删除内容太泛、没有实际价值的记忆 4. 对每条保留的记忆重新评估置信度 输出格式与输入相同只保留合并后的记忆列表。这两个 prompt 我迭代了十几版才稳定下来。最关键的是正反例那部分加了之后提取质量提升非常明显。LLM 对例子的敏感度远高于对规则描述的敏感度。5. 常见问题与排查技巧实录5.1 记忆提炼质量不稳定的排查问题表现同一段交互历史多次提炼结果差异大有时提取 5 条有时提取 2 条内容还不一样。排查思路先检查 temperature 是否设得太高。提炼任务 temperature 建议 0.2-0.3超过 0.5 就会不稳定。如果 temperature 已经很低还是不稳定检查 prompt 里是否有模糊表述比如“提取重要信息”这种没有明确标准的指令。把标准量化比如“提取包含具体实体、具体动作、具体结果的信息”。还有一个容易被忽略的点是输入格式。如果交互历史的格式不统一有时是 JSON有时是纯文本LLM 的理解会受影响。统一格式后稳定性会好很多。5.2 记忆库膨胀过快的处理问题表现跑了一段时间后记忆库从几百条涨到几万条检索变慢而且很多记忆是重复或低价值的。排查思路首先检查提炼 prompt 是否太宽松。如果 prompt 里没有明确的“宁可不提取”原则LLM 倾向于多提取。其次检查是否有去重机制。我建议在写入前做一次向量相似度检查如果新记忆和已有记忆的余弦相似度超过 0.95就合并而不是新增。另外记忆淘汰机制是必须的。我的做法是超过 90 天未被访问、且访问次数少于 3 次的记忆降权处理超过 180 天未被访问的归档到冷存储。这个策略不一定适合所有场景但核心思想是记忆库不是只进不出的要有淘汰。5.3 向量检索召回不准的调优问题表现查询“用户的数据库偏好”召回的记忆却是“用户提到过 MySQL”但实际用户偏好是 PostgreSQL。排查思路这是典型的向量检索语义漂移问题。解决方案是混合检索。先用向量检索召回 Top 20再用关键词过滤比如必须包含“偏好”或“preference”类型最后用 LLM 做一次重排序。我实测下来混合检索的准确率比纯向量检索高 30% 以上。还有一个技巧是查询改写。用户查询往往很短信息量不足。可以在检索前用 LLM 把查询改写成更完整的描述比如“用户的数据库偏好”改写成“用户喜欢或习惯使用哪种数据库系统”。改写后的查询向量表示更准确召回质量明显提升。5.4 常见问题速查表问题可能原因解决方案提炼结果不稳定temperature 过高 / prompt 模糊降低 temperature 至 0.2-0.3量化提取标准记忆库膨胀过快提取太宽松 / 无去重 / 无淘汰加严 prompt写入前去重设置淘汰策略检索召回不准纯向量检索语义漂移混合检索 查询改写 LLM 重排序提炼成本过高每轮都提炼 / 模型太大改为事后批量提炼小模型做初筛记忆冲突新旧记忆矛盾加时间戳权重新记忆覆盖旧记忆需人工确认Docker 启动失败虚拟化未开启 / 端口冲突检查 BIOS 虚拟化设置检查端口占用5.5 几个我踩过的坑第一个坑是忘了做脱敏。有一次测试时把包含用户真实手机号的交互历史直接扔给 LLM 提炼提炼出来的记忆里带了手机号。虽然只是测试环境但这是个严重的安全隐患。后来我在预处理环节加了正则脱敏手机号、邮箱、身份证号这类敏感信息在提炼前就替换掉。第二个坑是embedding 模型换了没重建索引。我一开始用 text-embedding-ada-002后来换成 text-embedding-3-small维度从 1536 变成 1536巧合一样但向量空间变了旧索引全部失效。换 embedding 模型一定要重建所有向量索引这个没有捷径。第三个坑是MCP Server 的超时设置。hindsight 作为 MCP Server 暴露接口时提炼操作可能耗时几十秒如果客户端超时设置太短默认可能 30 秒请求会失败。解决方案是把提炼做成异步任务客户端提交请求后立即返回任务 ID然后轮询或通过回调获取结果。这个设计改动不大但体验提升明显。6. 记忆系统的扩展方向与个人体会hindsight 这套思路跑通之后我发现在它基础上还能做不少扩展。比如记忆的时效性管理不是所有记忆都永久有效用户偏好可能随时间变化任务记忆可能过期。可以给每条记忆加一个“有效期”字段过期自动降权或归档。再比如跨 Agent 的记忆共享。如果多个 Agent 服务同一个用户它们的记忆应该能互通。这需要一套记忆权限和隔离机制实现起来不复杂但设计时要考虑清楚哪些记忆可以共享、哪些必须隔离。还有一个方向是记忆的可解释性。当 Agent 基于某条记忆做出决策时应该能追溯到这条记忆的来源交互。这在调试和审计时非常重要。我在存储结构里保留了 source_session_id 和 source_quote就是为了这个。我个人在实际操作中的体会是Agent Memory 这件事提炼质量比存储技术重要得多。很多人一上来就纠结用哪个向量数据库、用什么索引算法但真正决定效果的是提炼环节的 prompt 设计和分类体系。存储层用最朴素的方案都能跑提炼层做不好再好的存储也是垃圾进垃圾出。最后分享一个小技巧定期人工审查提炼结果。我每周会随机抽 20 条新提炼的记忆人工判断是否准确、是否有价值。这个习惯帮我发现了很多 prompt 里的问题也让我对“什么样的记忆真正有用”有了更具体的感知。自动化系统需要人工校准完全放任不管质量会慢慢漂移。
网站建设高端定制企业官网