尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Hindsight实战:为LLM Agent构建高效记忆系统

Hindsight实战:为LLM Agent构建高效记忆系统 1. 从“hindsight”说起为什么我们需要给Agent装上“后视镜”第一次看到“hindsight”这个词是在一个做LLM Agent的朋友群里。有人丢了一张截图说他们的Agent在连续对话到第37轮的时候突然把用户三小时前说过的偏好设置忘得一干二净回复开始胡言乱语。底下有人回了一句“这就是没有hindsight的代价。”Hindsight直译过来就是“后见之明”或者更通俗一点——“事后复盘的能力”。放在LLM Agent的语境里它指的是一套让Agent能够回溯、检索、利用历史交互信息的记忆机制。你可以把它理解成给Agent装了一面后视镜让它不光能看到当前的路况还能随时瞥一眼身后发生了什么。这个项目标题之所以值得单独拿出来聊是因为它切中了当前Agent开发中最要命的一个痛点记忆管理。现在市面上讲Agent架构的文章十篇有八篇在讲Planning、Tool Use、Reflection但真正落到“Agent怎么记住东西、怎么忘掉东西、怎么在需要的时候把东西捞出来”这一层的少之又少。而hindsight恰恰是冲着这个缺口去的。我花了大概两周时间把hindsight相关的思路、实现方案、以及它跟MCP协议、Docker部署、LLM Wiki知识库这些热词之间的关联梳理了一遍。这篇文章不会给你讲什么“Agent记忆的重要性”这种正确的废话而是直接拆解hindsight到底解决什么问题、它的核心机制怎么运作、你如果想自己搭一套类似的系统需要踩哪些坑、以及在实际操作中怎么跟MCP Server、Docker容器、LLM网关这些东西配合起来用。适合谁看如果你正在做LLM Agent开发或者你手头有一个需要长期记忆的对话系统再或者你只是对“Agent Memory”这个概念感兴趣想动手试试那这篇内容应该能给你省下不少查文档的时间。我会尽量把每个技术决策背后的“为什么”讲清楚而不是只丢一堆配置代码让你自己猜。2. Hindsight的核心设计思路不是简单的“存下来”而是“存得聪明”2.1 Agent记忆的三个层次Working Memory、Episodic Memory、Semantic Memory在聊hindsight的具体实现之前得先把Agent记忆的分类搞清楚。这不是学术上的分类而是你在实际写代码时会真实遇到的三种不同需求。Working Memory工作记忆是最短命的。它对应的是当前这一轮对话或者当前这个任务执行周期内的上下文。比如用户说“帮我订一张明天去上海的机票”Agent需要记住“明天”“上海”“机票”这三个关键信息然后在调用订票工具的时候把它们塞进参数里。这个层次的记忆通常就是LLM的context window本身或者是一个临时的键值存储。它的特点是生命周期极短任务结束就可以丢掉。Episodic Memory情景记忆是中等寿命的。它记录的是“什么时候发生了什么”。比如用户上周三问过“你们家退货政策是什么”Agent当时回答了什么用户有没有追问。这些信息不需要永远保留但在接下来几天或几周内如果用户再次提到退货Agent应该能想起来“哦这个人之前问过类似的问题”。这个层次的记忆需要时间戳、需要会话ID、需要一种能按时间范围检索的存储结构。Semantic Memory语义记忆是长期甚至永久的。它存储的是从多次交互中提炼出来的事实性知识。比如“这个用户偏好顺丰快递”“这个用户对价格敏感”“这个用户的技术栈是Python”。这些信息不是从单次对话里直接拿到的而是需要Agent在后台做归纳和抽象。这个层次的记忆最值钱但也最难维护因为一旦归纳错了后面所有的决策都会跑偏。Hindsight的设计思路本质上是在这三个层次之间建立一套流转机制。它不是简单地把所有对话历史往向量数据库里一扔就完事了而是要根据信息的类型和时效性决定它应该待在哪个层次、以什么形式存储、什么时候该被提升到更高层次、什么时候该被降级或丢弃。2.2 为什么选择“后视镜”而不是“全景摄像头”这里有一个很关键的设计取舍。你完全可以做一个“全景摄像头”式的记忆系统把Agent所有的输入输出、工具调用、中间状态全部记录下来然后每次决策的时候把所有历史都塞进context。这种做法简单粗暴但有两个致命问题。第一是成本。LLM的context window是有限的而且越长越贵。你把三个月的对话历史全塞进去token消耗直接爆炸。第二是噪声。历史信息里大部分是无关紧要的寒暄和重复确认真正有价值的信息可能只占5%。把这些噪声一起喂给LLM反而会干扰它的判断。Hindsight选择的是“后视镜”模式平时不主动显示但你需要的时候一扭头就能看到。具体来说它维护一个记忆索引层这个索引层不存储完整的对话内容而是存储“什么信息在什么地方”的指针。当Agent需要回忆某个信息时它先查索引找到相关的记忆片段再按需加载。这样既控制了context的长度又保证了关键信息不会丢失。这个设计思路跟LLM Wiki知识库的做法有异曲同工之妙。LLM Wiki的核心思想也是“不把所有知识塞进模型而是让模型学会查知识库”。Hindsight把同样的逻辑应用到了Agent的交互历史上。2.3 与MCP协议的天然契合点MCPModel Context Protocol是最近半年在Agent圈子里讨论度很高的一个协议。它的核心作用是让LLM能够以标准化的方式访问外部工具和数据源。Hindsight跟MCP的契合点在于记忆系统本身就可以被抽象成一个MCP Server。你可以这样理解Agent通过MCP协议向“记忆服务器”发起请求说“帮我查一下用户上次提到退货是什么时候”记忆服务器返回结果。Agent不需要知道记忆存在哪里、用什么数据库、索引怎么建它只需要按照MCP的接口规范发请求就行。这种解耦带来的好处是你可以随时替换记忆系统的后端实现而不用改动Agent的核心逻辑。在实际操作中我见过有人用Docker把记忆服务单独跑在一个容器里然后通过MCP协议跟主Agent通信。这样做的好处是记忆服务可以独立扩缩容而且不同Agent可以共享同一个记忆池。当然这也带来了新的问题比如网络延迟、容器间通信的配置、以及数据一致性的保证。这些后面会详细讲。3. 核心细节解析Hindsight的存储结构与检索策略3.1 记忆片段的切分粒度按轮次、按话题还是按实体这是我在实操中踩过的第一个大坑。一开始我按照“每轮对话”来切分记忆片段用户问一句、Agent答一句就算一个片段。结果发现检索效果很差因为很多关键信息是跨轮次才能拼凑出来的。比如用户先说“我下周要去北京出差”Agent问“需要帮你订酒店吗”用户说“不用公司有协议酒店”。如果你把这三轮拆成三个片段检索“北京出差”的时候可能只命中第一个片段后面的协议酒店信息就丢了。后来我改成了按话题切分。具体做法是用一个轻量级的LLM或者规则引擎检测对话中的话题边界。当话题发生切换时把之前积累的对话打包成一个记忆片段。这个片段的粒度比单轮大比整个会话小检索的时候既能保证信息完整又不会引入太多噪声。再后来我又加了一层按实体索引。每个记忆片段在存储的时候会提取出其中的关键实体人名、地名、时间、产品名等然后把这些实体作为索引键。这样检索的时候可以走“实体→片段”的路径比纯语义检索更精准。比如用户提到“顺丰”我直接查实体索引就能找到所有跟顺丰相关的记忆片段不需要做向量相似度计算。这三种粒度不是互斥的而是配合使用。我的做法是按话题切分存储按实体建索引按轮次保留原始记录作为兜底。这样既保证了检索效率又不会丢失任何原始信息。3.2 向量检索与关键词检索的混合策略纯向量检索的问题是它对“精确匹配”不敏感。比如用户说“我的订单号是SF123456”向量检索可能会返回一堆跟“订单”相关的片段但就是找不到那个精确的订单号。纯关键词检索的问题是它无法处理语义相似但用词不同的情况。比如用户说“我想退掉那个东西”关键词检索可能匹配不到“退货”相关的记忆。Hindsight采用的是混合检索策略先用关键词检索快速缩小范围再用向量检索在候选集里做语义排序。具体实现上我推荐用Elasticsearch或者PostgreSQL的全文检索功能做关键词层用FAISS或者Milvus做向量层。两层的结果通过一个加权公式合并权重可以根据实际效果调。这里有一个参数需要特别注意关键词检索的召回数量。如果你设得太小比如只取Top 5可能会漏掉关键信息如果设得太大比如取Top 100向量层的计算量会爆炸。我的经验值是关键词层召回20到50条向量层再从中选出Top 5到10条喂给LLM。这个比例在大多数场景下都能取得不错的平衡。3.3 记忆的时效性衰减与重要性加权不是所有记忆都同等重要也不是所有记忆都值得永久保留。Hindsight引入了一个记忆评分机制每个记忆片段都有一个动态分数分数由三个因素决定时效性、访问频率、情感强度。时效性很好理解越久远的记忆分数越低。但衰减曲线不是线性的而是指数衰减。具体公式可以简化为score base_score * exp(-lambda * days_since_access)。这里的lambda需要根据你的业务场景调。如果是客服场景可能lambda设大一点一周前的记忆就基本不看了如果是个人助理场景lambda可以设小一点几个月前的偏好设置仍然有效。访问频率是指这个记忆片段被检索了多少次。被频繁访问的记忆说明它跟当前任务的相关性高应该加分。这个机制可以防止重要信息因为时间久远而被淹没。情感强度是一个比较主观的维度但在实际应用中很有用。如果用户在对话中表达了强烈的情绪比如愤怒、惊喜、强调相关的记忆片段应该获得更高的权重。实现上可以用一个简单的情感分类器给每轮对话打分然后聚合到记忆片段上。这三个因素加权求和之后得到每个记忆片段的最终分数。检索的时候分数高的片段优先返回。同时系统会定期清理分数低于阈值的片段释放存储空间。4. 实操过程从零搭建一套Hindsight风格的记忆系统4.1 环境准备Docker与MCP Server的部署我假设你已经有了基本的Docker使用经验。如果没有先去装一个Docker DesktopWindows和Mac都有图形化安装包一路下一步就行。Linux的话用命令行安装具体步骤网上教程很多这里不展开。重点讲一下MCP Server的部署。目前MCP协议的生态还在快速演进中我推荐用官方提供的Python SDK来写一个自定义的Memory Server。核心代码结构大概是这样的from mcp.server import Server, NotificationOptions from mcp.server.models import InitializationOptions import mcp.server.stdio import asyncio server Server(hindsight-memory) server.list_tools() async def handle_list_tools(): return [ { name: store_memory, description: 存储一条记忆片段, inputSchema: { type: object, properties: { content: {type: string}, entities: {type: array, items: {type: string}}, timestamp: {type: string} } } }, { name: retrieve_memory, description: 检索相关记忆, inputSchema: { type: object, properties: { query: {type: string}, top_k: {type: integer, default: 5} } } } ] async def main(): async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): await server.run( read_stream, write_stream, InitializationOptions( server_namehindsight-memory, server_version0.1.0 ) ) if __name__ __main__: asyncio.run(main())这个Server跑起来之后你的Agent就可以通过MCP协议调用store_memory和retrieve_memory两个工具。存储后端我建议用PostgreSQL pgvector这样关键词检索和向量检索可以在同一个数据库里完成省去了维护两套系统的麻烦。Docker Compose的配置大概长这样version: 3.8 services: memory-db: image: pgvector/pgvector:pg16 environment: POSTGRES_DB: hindsight POSTGRES_USER: agent POSTGRES_PASSWORD: your_password ports: - 5432:5432 volumes: - memory_data:/var/lib/postgresql/data memory-server: build: ./memory-server depends_on: - memory-db environment: DATABASE_URL: postgresql://agent:your_passwordmemory-db:5432/hindsight ports: - 8080:8080 volumes: memory_data:注意Docker Desktop在Windows上有时会遇到“Virtualization support not detected”的报错这通常是因为BIOS里的虚拟化选项没开。重启进BIOS找到Intel VT-x或者AMD-V设为Enabled就行。4.2 记忆写入流程从对话流到结构化存储记忆写入不是简单地把对话文本往数据库里一塞。完整的写入流程包括四个步骤切分、提取、索引、存储。切分就是前面说的话题边界检测。我用的方法比较简单维护一个滑动窗口计算相邻两轮对话的语义相似度当相似度低于阈值时认为话题发生了切换。阈值设0.6到0.7之间比较合适具体值需要根据你的数据调。提取是从切分好的片段里抽取结构化信息。至少需要提取三类实体人名、地名、时间、产品名、意图用户想干什么、情感用户的情绪倾向。实体提取可以用spaCy或者HanLP意图和情感可以用一个小型的LLM来做比如Qwen的1.8B版本跑在本地就行不需要调API。索引就是给每个片段建立检索入口。我建了三个索引实体索引倒排索引实体→片段ID、时间索引B树索引时间范围→片段ID、向量索引HNSW索引向量→片段ID。这三个索引在PostgreSQL里都可以用扩展实现。存储的时候原始文本和结构化信息分开存。原始文本存在一个JSONB字段里结构化信息拆成独立的列。这样检索的时候可以只查结构化列速度快需要展示原文的时候再取JSONB字段。4.3 记忆检索流程多路召回与重排序检索流程比写入流程更复杂因为它直接决定了Agent的回复质量。我的实现是三路召回 重排序。第一路是实体召回。从当前对话中提取实体然后查实体索引拿到所有相关片段。这一路的优点是精准缺点是如果当前对话没有明确实体就召不回东西。第二路是时间召回。根据当前对话中提到的时间信息比如“上次”“之前”“上周”查时间索引拿到对应时间范围内的片段。这一路对处理“回顾型”问题特别有用。第三路是语义召回。把当前对话的向量跟所有记忆片段的向量做相似度计算取Top K。这一路覆盖面最广但噪声也最大。三路召回的结果合并去重后进入重排序阶段。重排序用一个交叉编码器Cross-Encoder模型把当前对话和每个候选片段拼在一起打分。交叉编码器比向量相似度更准但计算量也更大所以只对合并后的候选集通常20到50条做重排序而不是对所有记忆做。重排序之后取Top 5到10条按照记忆评分做最终排序然后格式化成LLM能理解的上下文。格式大概是这样[相关记忆] - 2024-01-15: 用户提到下周要去北京出差公司有协议酒店不需要订酒店。 - 2024-01-10: 用户询问过退货政策对7天无理由退货表示满意。 - 2023-12-20: 用户偏好顺丰快递要求所有订单默认发顺丰。这种格式比直接把原始对话塞进去更紧凑信息密度更高LLM也更容易理解。4.4 与LLM网关的集成怎么让Agent“自然地”使用记忆记忆系统建好了但Agent怎么知道什么时候该查记忆、什么时候不该查如果每轮对话都查一次延迟会很高如果从来不查记忆系统就白建了。我的做法是在Agent的System Prompt里加一段记忆使用指南明确告诉它什么情况下应该调用retrieve_memory工具。比如当用户提到“上次”“之前”“以前”“还记得吗”等回溯性词汇时先调用retrieve_memory检索相关记忆。当用户表达偏好、设置、重要事实时调用store_memory存储。其他情况下优先直接回复不要频繁调用记忆工具。这段Prompt需要根据实际效果反复调。我试过大概十几个版本最后发现关键是给出具体的触发词和场景而不是泛泛地说“需要的时候查记忆”。LLM对具体指令的遵循度远高于抽象指令。另外LLM网关层面可以做一个缓存层。如果当前对话的向量跟最近一次检索的向量相似度很高比如超过0.95直接复用上次的检索结果不用重新查数据库。这个优化在高频对话场景下能省不少时间。5. 常见问题与排查技巧实录5.1 记忆检索不准确从“查不到”到“查得准”的排查路径这是最常见的问题。用户明明之前说过某个信息但Agent就是检索不到。排查的时候按以下顺序检查第一步确认记忆有没有存进去。直接查数据库看对应的片段是否存在。如果不存在说明写入流程有问题检查切分逻辑是不是把关键信息切掉了或者提取环节是不是漏了实体。第二步确认索引有没有建对。如果片段存在但检索不到查一下实体索引里有没有对应的条目。我遇到过因为实体提取用了英文模型中文实体全部漏掉的情况。换成多语言模型或者中文专用模型就好了。第三步确认检索参数是否合理。如果索引没问题但召回结果不理想调一下Top K和相似度阈值。有时候是召回数量太小关键片段排在Top 10之外有时候是阈值太高把相关片段过滤掉了。第四步确认重排序有没有帮倒忙。交叉编码器虽然准但也不是万能的。如果重排序后的结果反而比原始召回差可能是模型跟你的数据分布不匹配。这时候可以暂时关掉重排序看看原始召回的效果。下面这张表是我整理的一个速查表覆盖了大部分常见症状和对应的排查方向症状可能原因排查方法解决方案完全查不到任何记忆写入失败或数据库连接异常直接查数据库确认数据存在检查Docker容器日志确认数据库连接字符串正确查到的记忆不相关向量模型不匹配或索引参数不当手动计算查询向量与片段的相似度更换嵌入模型调整HNSW的efSearch参数关键信息被切分到不同片段话题切分阈值过低检查切分边界看关键信息是否被截断提高相似度阈值或改用固定窗口重叠的切分方式旧记忆覆盖新记忆时间索引排序错误检查时间戳字段的格式和时区统一使用UTC时间戳检索时按时间倒序检索延迟过高向量索引未优化或候选集过大用EXPLAIN ANALYZE分析查询计划减少关键词召回数量增加向量索引的构建参数5.2 Docker环境下的网络与存储坑用Docker跑记忆服务网络配置是最容易出问题的地方。我踩过的坑包括容器间DNS解析失败、端口映射冲突、以及数据卷权限问题。容器间通信最稳妥的方式是自定义bridge网络。不要用默认的bridge因为默认bridge不支持自动DNS解析。在docker-compose.yml里定义一个networks字段把所有相关服务都挂到这个网络下这样容器之间可以直接用服务名互相访问。数据卷权限问题在Linux上特别常见。PostgreSQL容器默认以postgres用户运行如果你挂载的宿主机目录权限不对容器启动时会报“Permission denied”。解决办法是在宿主机上把目录owner改成UID 999PostgreSQL容器里的postgres用户UID或者直接在compose文件里指定user字段。还有一个坑是Docker Desktop的资源限制。默认情况下Docker Desktop只分配2GB内存给容器。如果你的向量索引比较大或者同时跑了多个LLM推理服务很容易OOM。在Docker Desktop的设置里把内存调到8GB以上会稳定很多。5.3 记忆膨胀与性能衰减的应对策略系统跑了一段时间之后记忆片段会越来越多检索速度会越来越慢。这是必然的关键是怎么应对。我的策略是分层存储 定期归档。最近一个月的记忆放在热存储PostgreSQL一个月到三个月的放在温存储还是PostgreSQL但单独建表索引更简单三个月以上的冷记忆导出到对象存储比如MinIO只在需要的时候按需加载。归档的触发条件可以按时间也可以按容量。我设的是两个条件满足其一就触发热存储超过10万条片段或者最早的热记忆超过30天。归档的时候不是简单删除而是把原始文本和向量都保留只是从主索引里移除。这样既控制了主索引的大小又不会丢失历史信息。另外定期做记忆去重也很重要。用户可能在不同时间说了同样的话这些重复记忆会浪费存储和检索资源。去重的方法是用MinHash或者SimHash计算片段之间的相似度超过阈值的合并成一个片段时间戳取最早的访问频率累加。6. 从Hindsight延伸出去Agent记忆系统的未来形态6.1 记忆的主动遗忘与隐私保护现在大部分记忆系统只考虑“怎么记住”很少考虑“怎么忘掉”。但在实际应用中遗忘机制同样重要。用户可能明确要求“忘掉我刚才说的话”或者系统需要遵守数据保留期限的规定。Hindsight的思路是引入一个遗忘策略层。这个层独立于存储层负责根据规则标记或删除记忆片段。规则可以包括用户主动请求删除、超过保留期限、敏感信息检测比如身份证号、银行卡号。标记删除比物理删除更安全因为可以保留审计日志同时确保检索时不会返回被标记的片段。隐私保护方面我建议在写入之前做一次敏感信息脱敏。用正则表达式或者NER模型识别出敏感实体替换成占位符。比如“我的手机号是138xxxx1234”存成“我的手机号是[PHONE]”。这样即使数据库泄露也不会暴露用户隐私。6.2 多Agent共享记忆的架构设计单个Agent的记忆系统相对简单但如果你有多个Agent需要共享记忆架构就复杂了。核心问题是记忆的归属权和访问控制。我的设计是引入一个记忆命名空间的概念。每个Agent有自己的私有命名空间同时可以订阅其他Agent的公共命名空间。写入的时候指定命名空间检索的时候可以跨命名空间查询但需要检查访问权限。这种架构下MCP协议的优势就体现出来了。记忆服务作为一个独立的MCP Server所有Agent都通过统一的接口访问。权限控制可以在Server层做Agent不需要关心底层实现。Docker的容器隔离也为多租户提供了天然的支持每个租户的记忆服务可以跑在独立的容器里互不干扰。6.3 与LLM Wiki知识库的融合可能性LLM Wiki的思路是把结构化的知识库以Wiki的形式组织起来让LLM能够像查百科全书一样查询知识。Hindsight的思路是让Agent记住自己的交互历史。这两者其实可以融合。融合的方式是把Hindsight积累的语义记忆从交互中提炼出的事实性知识定期导出到LLM Wiki的知识库里作为公共知识供所有Agent查询。同时LLM Wiki里的知识也可以被Hindsight引用作为Agent决策的背景信息。这种融合的挑战在于知识的一致性维护。如果Hindsight提炼出的事实跟LLM Wiki里已有的知识冲突以哪个为准我的做法是引入一个置信度评分Hindsight提炼的知识初始置信度较低随着被多次验证而提升。当置信度超过阈值时才允许覆盖LLM Wiki里的知识。这样可以避免单次错误归纳污染整个知识库。7. 一些实操心得与避坑建议先说一个最容易被忽视的点记忆系统的测试。很多人搭完记忆系统之后随便试几句对话就上线了结果遇到边界情况就崩。我的建议是建一个记忆测试集包含至少50个场景覆盖跨会话检索、时间范围查询、实体精确匹配、语义相似匹配、冲突记忆处理、遗忘请求。每次修改检索逻辑之后跑一遍测试集确保没有回归。另一个心得是日志要打全。记忆系统的调试比普通业务系统难因为问题往往出在“该召回的时候没召回”或者“召回了不该召回的”。我的做法是在每次检索时记录查询文本、召回的各路结果、重排序后的结果、最终返回给LLM的片段。这些日志在排查问题时非常有用。关于工具选型我试过用Redis做记忆缓存、用Milvus做向量检索、用Elasticsearch做关键词检索最后还是回到了PostgreSQL pgvector。原因很简单运维成本低。一个数据库搞定所有事情不需要维护三套系统的数据同步。性能上对于百万级以下的记忆片段PostgreSQL完全够用。等到真的到了千万级再考虑拆也不迟。最后分享一个关于Prompt的小技巧。在告诉LLM“什么时候该查记忆”的时候不要只给正向指令还要给反向指令。比如“当用户只是打招呼或者闲聊时不要调用记忆工具”。我试过只给正向指令结果LLM变得过度积极每句话都要查记忆延迟高得离谱。加上反向指令之后行为就正常多了。这个方向后续还可以往记忆的可解释性上做。现在检索出来的记忆片段是直接喂给LLM的用户看不到Agent到底“想起了什么”。如果能在回复里加上“根据你之前提到的...”这样的引用用户会更有信任感。实现上可以在检索结果里带上片段ID然后在生成回复时让LLM引用这些ID最后在前端渲染成可点击的引用链接。这个功能我还在试验阶段效果好的话再单独写一篇分享。
返回列表