1. 为什么“事后复盘”这件事值得单独做成一个项目
第一次看到“hindsight”这个标题,我脑子里蹦出来的不是某个具体工具,而是一种很朴素的需求:事情发生之后,我们到底能从中学到什么,以及怎么让机器也学会这件事。这个词本身的意思是“后见之明”,放在当下的技术语境里,它指向的是一个非常具体的问题——大语言模型驱动的智能体,在完成任务之后,能不能回过头来审视自己走过的路,把有用的经验沉淀下来,下次遇到类似情况时不再从零开始。
这就是agent memory这个方向最核心的命题。我们平时用 LLM 做对话、做任务编排,最大的痛点不是模型不够聪明,而是它记不住。你昨天跟它聊了三个小时,把项目的来龙去脉、踩过的坑、最终选定的方案都讲清楚了,今天开一个新会话,它又是一张白纸。上下文窗口再大,也架不住会话结束就清空。于是大家开始琢磨:能不能给 agent 配一套记忆系统,让它像人一样,把经历过的关键信息存下来,需要的时候再取出来用。
“hindsight”这个项目,从标题和关联的热词来看,大概率就是围绕agent memory做文章的一套方案。它涉及 LLM、MCP、Docker 这几个关键词,说明它不是纯理论探讨,而是一个可以跑起来、可以集成、可以部署的工程化项目。MCP 是模型上下文协议,Docker 是容器化部署手段,LLM 是底层推理引擎,这三者凑在一起,基本可以勾勒出一个典型的现代 AI 应用架构:用 Docker 把环境封装好,用 MCP 把工具和数据源接进来,用 LLM 做推理和决策,中间加一层记忆管理,让整个系统具备“回头看”的能力。
这篇文章适合谁来读?如果你正在做 AI 应用开发,尤其是涉及多轮对话、任务型 agent、知识管理这类场景,那这套思路对你直接有用。如果你只是听说过 agent memory 但不知道具体怎么落地,这篇文章会从设计思路讲到实操细节,尽量让你能照着搭出一个能跑的原型。如果你对 Docker 和 MCP 还不熟,也没关系,我会把关键概念用生活化的方式解释清楚,保证你能看懂每一步在干什么。
我自己的经验是,记忆系统这个东西,听起来很玄,但拆开来看无非就是三件事:存什么、怎么存、怎么取。存什么决定了信息密度,怎么存决定了检索效率,怎么取决定了最终效果。hindsight 这个项目名本身就暗示了一种设计哲学:不是让 agent 在事前就预知一切,而是让它在事后能够复盘、提炼、归档,把“后见之明”变成“前见之明”。下面我就按这个逻辑,把整套东西拆开来讲。
2. 记忆系统的整体设计与核心思路拆解
2.1 从“上下文窗口”到“持久化记忆”的必然演进
早期做 LLM 应用,大家的做法很直接:把历史对话拼成一个长字符串,塞进 prompt 里。上下文窗口小的时候,聊几轮就满了;窗口大了之后,成本又上去了,而且模型对超长上下文的注意力分配并不均匀,中间部分的信息容易被忽略。这就是所谓的“lost in the middle”现象。所以单纯靠扩大窗口来解决记忆问题,是一条走不通的路。
于是就有了分层记忆的思路。短期记忆放在上下文窗口里,保证当前对话的连贯性;长期记忆落到外部存储,需要的时候再检索回来。这个思路跟人脑的工作方式很像:你正在跟人说话,脑子里装着当前话题的活跃信息,这是工作记忆;而你知道自己家的地址、公司的路线、某个朋友的生日,这些是长期记忆,平时不占脑子,需要的时候才调出来。
hindsight 这个项目,从命名来看,重点可能放在长期记忆的形成机制上。不是简单地存聊天记录,而是要在任务完成后,对整个过程做一次“复盘”,提取出可复用的经验。这就涉及到几个关键设计决策:记忆的粒度是什么?是一条完整的任务轨迹,还是拆成一个个知识点?记忆的表示形式是什么?是原始文本,还是结构化摘要,还是向量?记忆的检索策略是什么?是按时间倒序,还是按语义相似度,还是按某种重要性评分?
这些决策没有标准答案,取决于具体场景。但有一个原则是通用的:记忆系统的好坏,不取决于它存了多少,而取决于它取的时候准不准。存了一万条记忆,每次检索都召回一堆无关的,那还不如不存。所以设计重心应该放在检索质量上,存储反而是次要的。
2.2 为什么选 MCP 作为集成层
MCP 这个词最近出现频率很高,全称是 Model Context Protocol,翻译过来叫模型上下文协议。你可以把它理解成一套标准化的接口规范,让 LLM 应用能够以统一的方式连接外部工具和数据源。在没有 MCP 之前,每接一个工具就要写一套适配代码,接十个工具就是十套,维护成本很高。MCP 的出现,相当于给所有工具定了一个“插座标准”,只要工具实现了这个标准,任何支持 MCP 的客户端都能直接插上去用。
hindsight 选择 MCP 作为集成层,逻辑很清晰:记忆系统本身就是一个需要被 agent 访问的数据源,它应该以标准化的方式暴露自己的能力。比如 agent 需要“写入一条记忆”,就调用 MCP 的某个工具;需要“检索相关记忆”,就调用另一个工具。这样记忆系统就跟具体的 LLM 框架解耦了,今天用这个框架,明天换那个框架,记忆层不用动。
这里有个容易混淆的点:MCP 是软件协议,不是硬件协议。硬件协议比如 USB,规定了插头的形状、针脚的定义、电压的范围,是物理层面的标准。MCP 是软件层面的,规定了请求和响应的数据结构、调用的方式、错误的格式。两者都是“让不同东西能对接”的规范,但抽象层级不同。理解这一点,对后面配置 MCP 服务很有帮助。
2.3 Docker 在其中的角色:环境一致性的保障
Docker 解决的是一个古老的问题:在我机器上能跑,在你机器上跑不起来。记忆系统通常依赖向量数据库、嵌入模型、可能还有 Redis 做缓存,这些组件的安装和配置在不同操作系统上差异很大。用 Docker 把整个环境打包成一个镜像,别人拉下来就能跑,省去了大量“配环境”的时间。
而且 Docker Compose 可以一次性编排多个服务:记忆服务本身、向量数据库、缓存、可能还有 MCP 网关。一个docker compose up就把整套东西拉起来,这对开发和测试阶段特别友好。生产环境可能用 K8s 或者别的编排工具,但本地开发和演示,Docker Compose 是最省事的方案。
我自己的习惯是,任何涉及多个组件的项目,第一步就是写 Dockerfile 和 compose 文件。先把环境跑通,再写业务逻辑。这样后面调试的时候,至少能排除“环境问题”这个变量。hindsight 这个项目涉及 LLM、MCP、记忆存储,组件不少,用 Docker 来管理是合理的选择。
2.4 记忆的三种类型与 hindsight 的定位
在 agent memory 的讨论里,通常会把记忆分成几类。一种是工作记忆,就是当前任务进行中活跃的信息,相当于人脑的短期记忆。一种是情景记忆,记录具体发生过的事件,比如“上周三我帮用户订了一张去上海的机票”。还有一种是语义记忆,是从多个事件中抽象出来的通用知识,比如“这个用户偏好靠窗座位”。
hindsight 从名字来看,最可能聚焦的是情景记忆向语义记忆的转化过程。也就是:先把任务过程记下来,这是情景记忆;然后通过某种复盘机制,从中提炼出可复用的规则或偏好,这是语义记忆。这个过程不是自动发生的,需要设计触发条件和提炼算法。
触发条件可以是任务结束、用户反馈、或者定时批处理。提炼算法可以用 LLM 来做摘要和归纳,也可以用规则引擎做模式匹配。hindsight 大概率是用 LLM 来做这件事,因为热词里有 LLM,而且用 LLM 做归纳比较灵活,不需要预先定义所有模式。
3. 核心细节解析与实操要点
3.1 记忆的写入:存什么比怎么存更重要
很多人做记忆系统,第一反应是把所有对话都存下来。这是个陷阱。全量存储会导致两个问题:一是存储成本高,二是检索噪声大。你存了一万条对话,用户问一个简单问题,检索出来五十条相关记录,其中四十九条是废话,反而干扰了模型判断。
hindsight 的思路应该是有选择地存。选择的标准可以包括:任务是否成功完成、用户是否给出了明确反馈、信息是否具有跨会话的复用价值。比如用户说“我下周要去北京出差”,这条信息值得存,因为它可能影响后续的机票、酒店、日程安排。用户说“今天天气不错”,这条就没必要存,除非用户明确表示要记录。
写入的粒度也需要考虑。太细,比如每句话存一条,检索时拼出来的上下文很碎;太粗,比如整个会话存一条,检索时又不够精准。折中方案是按事件存,一个事件可以是一次工具调用、一轮问答、一个任务阶段。每个事件包含时间戳、参与方、动作、结果、可能的反思。这样检索的时候可以按事件粒度召回,既不过碎也不过粗。
提示:写入记忆时一定要带时间戳和来源标记。时间戳用于时效性排序,来源标记用于追溯和审计。没有这两个字段,后面排查问题会很痛苦。
3.2 记忆的表示:文本、向量还是图
记忆存下来之后,用什么形式表示,直接决定了检索方式。纯文本最简单,检索靠关键词匹配或者全文索引。向量表示适合语义检索,把文本转成嵌入向量,用余弦相似度找最接近的。图表示适合有关系结构的信息,比如“A 是 B 的同事”“C 项目依赖 D 项目”。
hindsight 大概率是文本加向量的混合方案。原始文本存一份,用于展示和审计;嵌入向量存一份,用于语义检索。检索的时候先用向量召回一批候选,再用关键词或者规则做精排。这种混合检索在实践中的效果通常比单一方式好。
如果项目涉及更复杂的知识结构,可能会引入图数据库。但图数据库的运维成本比较高,除非确实需要多跳推理,否则用关系型数据库加向量扩展就够了。热词里出现了 tencentdb agent memory,说明有人在做数据库层面的记忆支持,这可能是 hindsight 的存储后端选项之一。
3.3 记忆的检索:三个关键问题
检索是记忆系统里最考验设计的部分。我把它归纳成三个问题:key 是什么、query 是什么、value 是什么。这个说法来自热词里的一条,我觉得总结得很到位。
key 是“我是谁”,也就是检索的发起方身份。不同 agent、不同用户、不同会话,能访问的记忆范围可能不同。检索时必须带上身份信息,做权限过滤。query 是“我在找什么”,也就是当前的检索意图。这个意图可能来自用户当前的问题,也可能来自 agent 当前的任务状态。value 是“我能提供什么”,也就是记忆条目本身的内容和元数据。
好的检索系统,要在这三者之间做匹配。不是简单地把 query 和 value 做相似度计算,还要考虑 key 的约束。比如用户 A 问了一个问题,检索时只能召回用户 A 有权访问的记忆,不能把用户 B 的隐私信息带出来。这个权限层如果没做好,会出大问题。
注意:检索结果一定要做去重和截断。向量检索容易召回内容高度相似的条目,如果不去重,上下文里会出现大量重复信息,浪费 token 还干扰模型。截断则是控制召回数量,一般 5 到 10 条就够了,太多反而稀释了关键信息。
3.4 MCP 工具的设计:接口要少而精
用 MCP 暴露记忆能力,工具设计要克制。不要一上来就设计二十个工具,agent 会懵。核心工具其实就几个:写入记忆、检索记忆、更新记忆、删除记忆。每个工具的输入参数要清晰,输出格式要稳定。
写入工具的参数可以包括:内容、类型、时间戳、来源、重要性评分。检索工具的参数可以包括:查询文本、返回数量、时间范围、类型过滤。更新和删除工具主要用于修正错误记忆,参数里要有记忆 ID。
工具的描述文本很重要,LLM 是靠描述来决定什么时候调用哪个工具的。描述要写清楚“这个工具在什么场景下使用”,而不是只写“写入记忆”。比如“当用户提供了值得跨会话记住的信息时,调用此工具写入长期记忆”,这样 LLM 更容易判断调用时机。
3.5 Docker 编排:把依赖管起来
用 Docker Compose 编排 hindsight 相关服务,典型的 compose 文件会包含这几个服务:记忆服务本身、向量数据库、可能还有 Redis 做缓存、以及一个 MCP 网关。每个服务定义镜像、端口、环境变量、数据卷。
数据卷特别重要。记忆数据是持久化的,不能容器一删就没了。向量数据库的数据目录要挂载到宿主机,记忆服务的数据库文件也要挂载。环境变量里放连接字符串、API key、模型名称这些配置。端口映射要避免冲突,向量数据库默认端口可能跟本机已有服务冲突,改一下映射就行。
启动顺序也有讲究。记忆服务依赖向量数据库,所以 compose 里要配 depends_on,或者用健康检查确保数据库先起来。不过 depends_on 只保证启动顺序,不保证服务就绪,更稳妥的做法是在记忆服务里做重试连接。
4. 实操过程与核心环节实现
4.1 环境准备:Docker 安装与验证
第一步是把 Docker 装好。Windows 用户装 Docker Desktop,Mac 用户也是 Docker Desktop,Linux 用户装 Docker Engine 加 Compose 插件。Windows 上装 Docker Desktop 有个常见坑:需要开启虚拟化支持。如果 BIOS 里没开,或者跟 Hyper-V、WSL2 有冲突,Docker Desktop 会启动失败,报 “virtualization support not detected” 之类的错误。解决办法是进 BIOS 开启虚拟化,然后在 Windows 功能里确保 WSL2 或 Hyper-V 是启用的。
装完之后验证一下:
docker --version docker compose version docker run hello-world三条命令都能正常输出,说明环境没问题。如果docker run hello-world卡住或者报网络错误,大概率是镜像拉取的问题,可以配置一下镜像加速器。这个配置在 Docker Desktop 的设置里就能改,不同地区的可用加速地址不一样,选一个延迟低的就行。
提示:Windows 11 装 Docker Desktop 时,建议用 WSL2 后端,比 Hyper-V 后端性能好,而且跟 Linux 容器的兼容性更好。安装过程中如果提示要更新 WSL 内核,按提示操作即可。
4.2 编写 Docker Compose 文件
下面是一个典型的 compose 配置,包含记忆服务、向量数据库和缓存三个组件。具体镜像名和端口根据实际项目调整,这里给的是结构参考。
services: memory-service: build: . ports: - "8080:8080" environment: - VECTOR_DB_URL=http://vector-db:8000 - REDIS_URL=redis://cache:6379 - LLM_API_KEY=${LLM_API_KEY} volumes: - ./data/memory:/app/data depends_on: - vector-db - cache vector-db: image: your-vector-db-image:latest ports: - "8001:8000" volumes: - ./data/vector:/var/lib/vector cache: image: redis:7-alpine ports: - "6380:6379" volumes: - ./data/redis:/data几个细节说明一下。端口映射左边是宿主机端口,右边是容器内端口。向量数据库容器内监听 8000,映射到宿主机 8001,避免跟记忆服务的 8080 冲突。Redis 映射到 6380,也是同样的考虑。数据卷把容器内的数据目录挂到宿主机的./data下,这样容器重建数据还在。
环境变量里的LLM_API_KEY从宿主机环境读取,不写死在文件里。启动前先export LLM_API_KEY=你的密钥,或者写一个.env文件放在 compose 文件旁边,Docker Compose 会自动加载。
4.3 实现记忆写入的 MCP 工具
MCP 工具的实现方式取决于你用的框架。这里用 Python 伪代码展示核心逻辑,重点是参数设计和处理流程。
from mcp.server import Server from mcp.types import Tool, TextContent import time import uuid server = Server("hindsight-memory") @server.tool() async def write_memory( content: str, memory_type: str = "episodic", importance: float = 0.5, source: str = "agent" ) -> list[TextContent]: """ 当用户提供了值得跨会话记住的信息时,调用此工具写入长期记忆。 content: 记忆内容 memory_type: episodic(情景)或 semantic(语义) importance: 重要性评分,0 到 1 之间 source: 记忆来源标记 """ memory_id = str(uuid.uuid4()) timestamp = time.time() # 生成嵌入向量 embedding = await get_embedding(content) # 写入向量数据库 await vector_db.insert( id=memory_id, vector=embedding, payload={ "content": content, "type": memory_type, "importance": importance, "source": source, "timestamp": timestamp } ) return [TextContent( type="text", text=f"记忆已写入,ID: {memory_id}" )]这个工具的关键点在于:参数有默认值,降低调用门槛;返回记忆 ID,方便后续更新或删除;嵌入向量在写入时生成,检索时直接用,避免重复计算。
4.4 实现记忆检索的 MCP 工具
检索工具比写入工具复杂一些,因为要处理查询、过滤、排序、截断。
@server.tool() async def search_memory( query: str, limit: int = 5, memory_type: str = None, time_range_hours: int = None ) -> list[TextContent]: """ 当需要回忆过去的相关信息时,调用此工具检索长期记忆。 query: 检索查询文本 limit: 返回数量,默认 5 条 memory_type: 可选,过滤记忆类型 time_range_hours: 可选,只检索最近 N 小时内的记忆 """ query_vector = await get_embedding(query) filters = {} if memory_type: filters["type"] = memory_type if time_range_hours: cutoff = time.time() - time_range_hours * 3600 filters["timestamp"] = {"$gte": cutoff} results = await vector_db.search( vector=query_vector, limit=limit * 2, # 多召回一些用于去重 filters=filters ) # 去重:内容相似度超过阈值的只保留一条 deduped = deduplicate(results, threshold=0.95) # 按重要性和时间做二次排序 ranked = sorted( deduped, key=lambda x: x.payload["importance"] * 0.7 + normalize_time(x.payload["timestamp"]) * 0.3, reverse=True )[:limit] formatted = "\n---\n".join([ f"[{r.payload['type']}] {r.payload['content']}" for r in ranked ]) return [TextContent(type="text", text=formatted or "未找到相关记忆")]这里有几个设计决策值得说明。多召回再截断,是为了给去重留余量。去重阈值设 0.95,是因为向量相似度超过这个值的基本可以认为是重复内容。二次排序把重要性和时间结合起来,重要性权重 0.7,时间权重 0.3,这个比例可以根据场景调整。如果场景对时效性要求高,就把时间权重调大。
4.5 复盘机制的实现:从情景记忆到语义记忆
hindsight 的核心特色应该在复盘机制上。任务完成后,触发一次复盘,把这次任务的情景记忆拿出来,让 LLM 做归纳,生成语义记忆。
async def retrospect(session_id: str): """ 任务结束后调用,对本次会话的情景记忆做复盘, 提炼出可复用的语义记忆。 """ # 取出本次会话的所有情景记忆 episodes = await vector_db.search( filters={"session_id": session_id, "type": "episodic"}, limit=100 ) if not episodes: return # 拼接成复盘提示 episode_text = "\n".join([ f"- {e.payload['content']}" for e in episodes ]) prompt = f"""以下是一次任务过程中的情景记录: {episode_text} 请从中提炼出 1 到 3 条可复用的经验或偏好,每条用一句话表述。 只输出经验本身,不要解释。如果没有任何值得复用的内容,输出"无"。""" response = await llm.generate(prompt) if response.strip() == "无": return # 把提炼出的经验写入语义记忆 for line in response.strip().split("\n"): line = line.strip().lstrip("- ") if line: await write_memory( content=line, memory_type="semantic", importance=0.8, source=f"retrospect:{session_id}" )这个复盘机制的关键在于提示词的设计。要求 LLM 输出简洁的经验条目,而不是长篇大论。限制条数,避免语义记忆膨胀过快。重要性给高一点,因为经过提炼的信息通常比原始情景更有价值。
触发时机可以是会话结束、任务完成、或者用户主动触发。也可以做成定时任务,每天凌晨对前一天的所有会话做批量复盘。批量复盘的好处是可以跨会话归纳,发现更通用的模式。
4.6 启动与验证
所有代码写完后,启动整套服务:
docker compose up -d --build-d是后台运行,--build是重新构建镜像。启动后查看日志:
docker compose logs -f memory-service看到服务正常监听的日志后,用 MCP 客户端连接测试。如果用的是支持 MCP 的 IDE 插件或者桌面客户端,在配置里加上 MCP 服务地址,然后试着调用写入和检索工具。
验证流程可以这样设计:先写入一条测试记忆,比如“用户偏好用中文交流”,然后换一个会话,问一个需要用到这个偏好的问题,看 agent 是否能检索到这条记忆并正确应用。如果能,说明整条链路是通的。
提示:第一次跑的时候,建议把日志级别调到 DEBUG,方便观察每一步的执行情况。等稳定了再调回 INFO,减少日志量。
5. 常见问题与排查技巧实录
5.1 Docker 相关问题速查
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| Docker Desktop 启动失败,提示虚拟化未检测到 | BIOS 虚拟化未开启,或与 Hyper-V/WSL2 冲突 | 任务管理器查看虚拟化状态 | 进 BIOS 开启虚拟化,确保 WSL2 启用 |
| 容器间网络不通 | 服务不在同一网络,或端口配置错误 | docker network inspect查看网络 | 确保 compose 中服务在同一默认网络 |
| 镜像拉取超时 | 网络问题或镜像源不可达 | docker pull手动测试 | 配置镜像加速器 |
| 数据卷挂载后权限错误 | 容器内用户与宿主机用户 UID 不匹配 | 查看容器日志中的权限报错 | 调整目录权限或指定用户运行 |
| compose 启动顺序问题 | 依赖服务未就绪 | 查看依赖服务日志 | 加健康检查或应用层重试 |
Docker 网络这块,compose 默认会创建一个 bridge 网络,所有服务都在这个网络里,服务名就是主机名。所以记忆服务里写http://vector-db:8000就能访问到向量数据库,不需要写 IP。如果自己手动创建了网络,要确保所有服务都加入同一个网络。
5.2 MCP 集成常见坑
MCP 服务启动后,客户端连不上,最常见的原因是传输方式不匹配。MCP 支持多种传输方式,stdio 是标准输入输出,适合本地进程;SSE 是服务器推送事件,适合远程服务。客户端配置里写的传输方式要跟服务端一致。如果服务端用 SSE,客户端也配 SSE,地址写对,一般就能连上。
另一个坑是工具描述不清晰导致 LLM 不调用。有时候你明明实现了检索工具,但 agent 就是不调用它,而是自己瞎编答案。这时候要检查工具的描述文本,是不是写得太模糊。把“检索记忆”改成“当需要回忆用户之前提到的信息时调用此工具”,调用率会明显提升。
还有参数类型不匹配的问题。MCP 工具的参数有类型定义,如果客户端传了字符串但服务端期望整数,会报 schema 错误。排查方法是看服务端日志里的请求体,对比参数类型。修复就是在工具定义里把类型写清楚,客户端调用时做类型转换。
5.3 记忆检索效果差的排查思路
检索效果差,通常有三个层面的原因:写入质量、向量质量、检索策略。
先看写入质量。如果存进去的就是一堆无意义的对话碎片,检索出来自然没用。解决办法是在写入前做一次过滤,只存有价值的信息。可以加一个判断逻辑:如果内容长度小于某个阈值,或者不包含任何实体和动作,就跳过不存。
再看向量质量。嵌入模型的选择很关键。不同模型对不同语言、不同领域的文本表现差异很大。如果发现检索结果跟查询意图偏差大,可以换一个嵌入模型试试。另外,嵌入的维度也要跟向量数据库的配置匹配,维度不对会直接报错。
最后看检索策略。纯向量检索有时候会召回语义相似但实际无关的内容。加入关键词过滤或者元数据过滤能改善。比如检索时限定时间范围,或者限定记忆类型,都能提高精准度。混合检索,也就是向量加关键词,通常比单一方式好。
5.4 性能与成本优化
记忆系统跑起来之后,性能和成本是两个绕不开的问题。向量检索本身很快,但如果记忆量到了百万级,检索延迟会上升。解决办法是加索引,大多数向量数据库都支持 HNSW 或 IVF 索引,建好索引后检索速度能提升一个数量级。
嵌入计算是另一个成本点。每次写入和检索都要调嵌入模型,如果量大,费用不低。优化方法是做嵌入缓存,相同文本不重复计算。检索时如果查询文本跟之前某次相同,直接复用缓存的向量。
LLM 调用是最大的成本项,尤其是复盘环节。如果每次任务结束都调一次 LLM 做归纳,费用会累积得很快。优化策略是批量复盘,攒一批情景记忆一起处理,减少调用次数。另外可以用小模型做复盘,大模型只用在关键决策上。
注意:成本优化不要牺牲效果。如果为了省钱把检索数量砍到 1 条,导致 agent 经常缺信息,那省下来的钱还不够弥补体验下降的损失。找到效果和成本的平衡点,需要根据实际数据做 A/B 测试。
5.5 记忆冲突与更新
记忆存多了之后,会出现冲突。比如用户上个月说“我喜欢喝咖啡”,这个月说“我戒咖啡了”。两条记忆都存着,检索时可能同时召回,agent 就懵了。解决办法是给记忆加时效性标记,新记忆覆盖旧记忆,或者用 LLM 做冲突检测,发现矛盾时以新记忆为准。
更新记忆比写入记忆更复杂,因为要找到旧记忆并修改。实践中可以给每条记忆加一个superseded_by字段,新记忆写入时把旧记忆标记为已取代,检索时过滤掉已取代的记忆。这样既保留了历史记录,又不会干扰当前决策。
删除记忆要谨慎。除非用户明确要求删除,否则不建议物理删除。用软删除,加一个deleted标记,检索时过滤掉。这样万一误删还能恢复。
6. 从 hindsight 延伸出去的几个方向
6.1 记忆的可视化与调试
记忆系统跑起来之后,你会想知道它到底存了什么、检索时召回了什么。做一个简单的可视化界面,列出所有记忆条目,支持按类型、时间、重要性筛选,对调试非常有帮助。更进一步,可以在每次检索时记录召回的条目和最终使用的条目,对比看哪些被召回了但没被用上,哪些被用上了但没被召回,据此优化检索策略。
6.2 多 agent 共享记忆
单个 agent 的记忆系统做好之后,自然会想到多个 agent 能不能共享记忆。比如一个客服 agent 和一个售后 agent,共享用户的历史交互记录。这涉及到权限控制和记忆隔离的问题。不同 agent 能访问的记忆范围不同,需要一套细粒度的权限模型。MCP 的 key 机制可以在这里发挥作用,每个 agent 有自己的身份标识,检索时按身份过滤。
6.3 记忆的遗忘机制
人脑会遗忘,记忆系统也需要遗忘。不是所有记忆都值得永久保留。可以设计一个衰减机制,记忆的重要性随时间递减,低于阈值时自动归档或删除。这样能控制记忆总量,保持检索效率。衰减曲线可以用指数衰减,半衰期根据记忆类型设定,情景记忆衰减快一些,语义记忆衰减慢一些。
6.4 与知识库的融合
记忆系统和知识库看起来是两回事,但边界其实模糊。知识库存的是通用知识,记忆库存的是个性化经验。两者可以融合检索,先查记忆再查知识库,或者反过来。融合之后,agent 既能知道通用规则,又能知道这个用户的特殊偏好,回答会更精准。
我在实际搭建这套东西的过程中,最大的体会是:记忆系统的难点不在技术,而在产品判断。存什么、什么时候存、怎么用,这些决策没有标准答案,需要根据具体场景反复调整。技术组件都是现成的,Docker、MCP、向量数据库,拼起来就能跑。但要让记忆真正有用,得花时间观察 agent 的行为,看它在哪些地方因为缺记忆而表现不好,然后针对性地补。这个过程没有捷径,就是不断迭代。另外一个小技巧是,初期可以把重要性阈值调低,多存一些,等数据积累起来再分析哪些记忆被检索得多、哪些从来没被用过,据此调整写入策略。用数据驱动的方式优化,比拍脑袋决定存什么要靠谱得多。