1. 从“hindsight”说起:为什么我们需要给Agent装上“后视镜”
“hindsight”这个词本身很有意思,字面意思是“事后的洞察力”,中文常翻译成“后见之明”。放在LLM Agent的语境里,它指向一个非常具体且要命的问题:Agent在完成一轮任务之后,能不能记住自己刚才做了什么、为什么这么做、结果是好是坏,并且在下一轮任务中真正用上这些经验?
我接触过不少做Agent落地的团队,大家一开始都把精力砸在工具调用、提示词工程、流程编排上,等到系统跑起来才发现,真正让Agent从“能用”变成“好用”的瓶颈,往往不在推理能力,而在记忆。一个没有记忆的Agent,每次对话都是重新开始,用户上一轮纠正过的错误,下一轮它照犯不误;一个只有短期记忆的Agent,上下文窗口一满就开始丢信息,前面聊过的关键约束全被截断;一个只有原始对话记录、没有结构化提炼的Agent,检索出来的全是噪声,反而干扰判断。
所以“hindsight”这个项目标题,我理解它要解决的核心命题就是:如何为LLM Agent构建一套具备“事后复盘”能力的记忆系统。这套系统不只是把对话存下来,而是要在任务结束后主动提炼经验、识别模式、形成可复用的知识,并在后续任务中精准调用。它要回答三个问题:刚才发生了什么(episodic memory)、我从中学到了什么(semantic memory)、下次遇到类似情况我该怎么做(procedural memory)。
这篇文章适合谁看?如果你正在做Agent产品、在搭RAG系统、在用MCP协议对接各种工具、或者单纯对LLM记忆机制感兴趣,那接下来的内容应该能给你一些可以直接抄作业的思路。我会从整体设计、核心细节、实操落地、问题排查四个层面展开,尽量把“为什么这么设计”讲透,而不是只丢一堆概念。
2. 整体设计与思路拆解:Agent记忆到底该怎么分层
2.1 为什么“存下来”不等于“记住了”
很多人做Agent记忆的第一反应是:把对话历史塞进向量数据库,需要的时候检索一下不就行了?我一开始也这么干过,结果很快撞墙。问题出在三个地方。
第一,原始对话的粒度太细。用户说“帮我查一下北京明天的天气”,Agent调用天气API返回结果,用户说“那后天呢”,Agent再查一次。这三轮对话如果原封不动存进去,下次用户问“上海天气”的时候,检索出来的可能是“北京”“后天”这些无关片段,因为向量相似度匹配的是字面语义,不是任务意图。
第二,没有时间衰减和重要性加权。三个月前的一次闲聊和昨天的一次关键纠错,在向量空间里可能距离差不多,但显然后者对当前任务的价值高得多。原始存储没有机制去区分这个。
第三,缺乏主动提炼。人类记忆不是录像机,我们会自动把经历压缩成经验。Agent也需要这个压缩过程:把“用户说X,我做了Y,结果Z”提炼成“当遇到X类情况时,做Y比做Z更有效”。这个提炼动作,就是hindsight的核心。
所以我的设计思路是:分层存储 + 主动提炼 + 场景化检索。三层记忆各司其职,写入和读取走不同路径,避免一锅粥。
2.2 三层记忆的职责划分与数据流
我把Agent记忆分成三层,这个划分参考了认知科学里对人类记忆的分类,但在工程上做了简化,确保可落地。
第一层:工作记忆(Working Memory)。这一层就是当前会话的上下文窗口,生命周期最短,容量受限于模型的token上限。它的职责是保证当前这轮任务能连贯执行。我通常会把最近N轮对话、当前任务的目标、已经调用过的工具及结果放在这里。N的取值要看模型窗口大小,一般留出30%的余量给系统提示词和工具定义。工作记忆不持久化,会话结束就丢弃,但会在丢弃前触发一次“提炼”动作,把有价值的部分下沉到下一层。
第二层:情景记忆(Episodic Memory)。这一层存储的是“发生过什么”,以任务为单位组织,而不是以对话轮次为单位。一个任务从开始到结束,会形成一条情景记录,包含:任务目标、关键决策点、调用的工具序列、最终结果、用户反馈。这条记录会被向量化存储,同时附带时间戳、任务类型标签、成功率等元数据。检索时不仅看语义相似度,还看时间新鲜度和任务类型匹配度。
第三层:语义记忆(Semantic Memory)。这一层存储的是“学到了什么”,是从多条情景记录中提炼出来的抽象知识。比如从十次“查询天气”的任务中提炼出“用户问天气时通常关心温度和降水概率,不关心风速”,这就是一条语义记忆。语义记忆不直接对应某次具体交互,而是跨任务的模式总结。它的更新频率低,但一旦形成,对Agent行为的指导意义最强。
数据流是这样的:工作记忆在会话结束时,由LLM做一次“复盘提炼”,生成一条情景记录写入第二层;第二层定期(比如每积累10条同类型情景)触发一次“模式提炼”,由LLM总结出语义记忆写入第三层;检索时,优先查第三层,如果没有匹配,再查第二层,同时把检索结果注入工作记忆。
2.3 为什么选择MCP作为工具对接层
热词里反复出现MCP,这不是偶然。MCP(Model Context Protocol)本质上是一个标准化协议,让LLM能够以统一的方式调用外部工具和数据源。在hindsight项目里,Agent需要调用各种工具来完成任务,同时需要把工具调用结果写入记忆系统。如果每个工具都用不同的对接方式,代码会变得极其臃肿。
MCP的价值在于:它把“工具调用”和“记忆写入”解耦了。Agent通过MCP调用天气API,返回结果后,记忆系统通过监听MCP的消息流自动捕获这次调用的输入输出,不需要在业务代码里到处埋点。我实测下来,用MCP对接Playwright、Chrome DevTools、数据库这些工具,记忆捕获的覆盖率能到90%以上,剩下10%是那些不走MCP的内部函数调用,需要手动埋点。
另外,MCP的协议设计天然适合做“工具调用链”的记录。一次任务可能涉及多个工具的串联调用,MCP的消息格式里保留了调用ID和父子关系,这让情景记忆在还原“决策路径”时非常方便。相比之下,如果直接用函数调用,你得自己维护调用栈,很容易漏。
2.4 Docker在其中的角色:环境一致性与快速复现
热词里Docker出现频率很高,这反映了一个现实问题:Agent记忆系统依赖的组件不少——向量数据库、LLM推理服务、MCP工具服务、可能还有Redis做缓存。如果每个开发者都在自己机器上手动装一遍,版本冲突和环境差异会浪费大量时间。
我的做法是:用Docker Compose把整个记忆系统的依赖打包。向量数据库用Qdrant或Weaviate的官方镜像,Redis用官方镜像,MCP工具服务自己写Dockerfile,LLM推理如果走本地就用Ollama的镜像,如果走API就不需要。这样新成员拉下代码,一条docker compose up就能跑起来,记忆系统的行为在所有机器上一致。
这里有个坑要注意:Docker Desktop在Windows上偶尔会报“Virtualization support not detected”或者“Docker Desktop failed to start because virtualization...”,这通常是BIOS里虚拟化没开,或者WSL2没装好。我在“常见问题”那节会详细说排查步骤。
3. 核心细节解析与实操要点:记忆的写入、提炼与检索
3.1 工作记忆的窗口管理:别让上下文爆掉
工作记忆的核心矛盾是:窗口有限,但任务可能需要很长的上下文。我的策略是动态摘要 + 关键信息锚定。
具体做法:当对话轮次超过阈值(比如10轮)或者token用量超过窗口的60%时,触发一次摘要。摘要不是简单截断,而是让LLM做一次“压缩提炼”,输出三部分内容:当前任务目标(一句话)、已确认的关键约束(列表)、已排除的错误路径(列表)。然后把原始对话中最早的那部分替换成这个摘要,保留最近几轮原文。
这样做的理由是:任务目标和约束是后续推理必须依赖的,不能丢;错误路径是防止Agent重复犯错的,也很重要;而中间的详细对话过程,大部分是一次性的,压缩掉不影响后续决策。
注意:摘要的提示词要明确要求“只保留对后续决策有影响的信息”,否则LLM容易把摘要写成流水账,压缩效果大打折扣。
我实测下来,用这个策略,一个原本只能撑8轮对话的窗口,可以撑到25轮以上,而且关键信息不丢。代价是每次摘要会多消耗一次LLM调用,但相比上下文爆掉导致任务失败,这个成本完全值得。
3.2 情景记忆的写入:任务边界怎么判定
情景记忆以任务为单位,那“任务什么时候开始、什么时候结束”就成了关键问题。如果边界判错了,要么把多个任务混成一条记录,要么把一个任务拆成好几条,都会影响后续检索质量。
我的判定逻辑是显式信号 + 隐式信号结合。显式信号包括:用户说“好了”“谢谢”“下一个问题”这类结束语,或者Agent调用了标记为“终结型”的工具(比如提交表单、发送邮件)。隐式信号包括:连续N轮没有工具调用且用户没有新指令,或者话题向量发生显著偏移。
实际操作中,我会给每个任务记录打一个task_boundary_confidence分数,高于0.8的才正式写入情景记忆,低于0.8的先放在缓冲区,等后续信号确认。这个分数由几个因子加权:显式结束信号权重0.5,工具调用终结标记权重0.3,话题偏移权重0.2。
写入的内容结构我固定成JSON格式,包含这些字段:
{ "task_id": "uuid", "task_type": "weather_query | code_debug | doc_search | ...", "goal": "用户想要达成的目标,一句话", "key_decisions": [ {"step": 1, "action": "调用天气API", "reason": "用户问天气", "result": "成功"} ], "tools_used": ["weather_api", "calendar_api"], "outcome": "success | partial | failure", "user_feedback": "positive | negative | none", "timestamp": "ISO8601", "embedding": [0.1, 0.2, ...] }这个结构的好处是:检索时可以按task_type过滤,按outcome排序,按timestamp做时间衰减,比纯向量检索精准得多。
3.3 语义记忆的提炼:从情景中抽象出规则
语义记忆的提炼是hindsight最有价值也最难做好的部分。我的做法是定期批处理 + LLM归纳。
具体流程:每积累10条同task_type的情景记录,触发一次提炼。把这10条记录的goal、key_decisions、outcome喂给LLM,让它输出3到5条“如果...那么...”形式的规则。比如:
- 如果用户问天气且没有指定城市,那么先查用户历史位置,不要直接问。
- 如果天气API返回超时,那么重试一次,仍失败则告知用户并建议稍后再试。
- 如果用户连续问两天的天气,那么第二次查询时主动对比温度变化。
这些规则会被存入语义记忆,附带置信度(基于支持它的情景记录数量和成功率)。置信度低于0.6的规则标记为“待验证”,在检索时降权。
实操心得:LLM归纳出来的规则有时候会过于具体,比如“如果用户问北京天气且时间是下午3点,那么...”,这种规则泛化能力差。我通常会在提示词里加一句“规则要能适用于同类任务的所有情况,不要包含具体城市、具体时间等一次性信息”,效果会好很多。
3.4 检索策略:三层记忆怎么协同
检索发生在每次新任务开始时。我的策略是语义记忆优先,情景记忆兜底,工作记忆实时注入。
第一步,用当前任务的目标描述去查语义记忆,取置信度最高的3条规则,注入工作记忆的系统提示词区域。这些规则是“行为指导”,告诉Agent该怎么做。
第二步,如果语义记忆没有匹配(新任务类型),或者匹配到的规则置信度都低于0.5,就去查情景记忆。情景记忆检索用混合策略:向量相似度占60%,任务类型匹配占25%,时间新鲜度占15%。取Top 5条,把它们的key_decisions和outcome注入工作记忆,作为“参考案例”。
第三步,工作记忆本身的内容(当前对话历史、已调用工具结果)始终在上下文里,不需要额外检索。
这个三层协同的检索逻辑,我封装成了一个MemoryRetriever类,对外只暴露一个retrieve(query, task_type)方法,内部自动决定查哪层、怎么融合。这样业务代码不用关心记忆分层的细节。
3.5 MCP工具调用的记忆捕获:监听消息流
前面提到用MCP做工具对接层,记忆捕获就靠监听MCP的消息流。MCP的消息格式里,每次工具调用都有request_id、tool_name、input、output、status这些字段。我写了一个中间件,挂在MCP客户端和工具服务之间,把所有消息异步写入一个队列,然后由后台worker消费队列,把工具调用记录关联到当前任务的情景记忆中。
这里有个细节:工具调用的input和output可能很大,比如Playwright截图返回的base64、数据库查询返回的大结果集。直接存进情景记忆会撑爆存储。我的做法是:只存input的摘要(比如SQL语句的前200字符)和output的元信息(比如“返回12行”“截图大小2MB”),完整数据存在对象存储里,情景记忆里只放引用ID。这样既保留了决策路径,又不会让记忆库膨胀。
4. 实操过程与核心环节实现:从零搭一套hindsight记忆系统
4.1 环境准备:Docker Compose一键拉起依赖
先把依赖服务用Docker Compose管起来。我用的组件清单如下:
| 组件 | 镜像 | 端口 | 用途 |
|---|---|---|---|
| Qdrant | qdrant/qdrant:latest | 6333 | 向量存储 |
| Redis | redis:7-alpine | 6379 | 工作记忆缓存、消息队列 |
| PostgreSQL | postgres:16 | 5432 | 情景记忆元数据、语义记忆规则 |
| MCP工具服务 | 自建Dockerfile | 8080 | 工具调用 |
| Ollama(可选) | ollama/ollama | 11434 | 本地LLM推理 |
docker-compose.yml的关键片段:
version: '3.8' services: qdrant: image: qdrant/qdrant:latest ports: - "6333:6333" volumes: - qdrant_data:/qdrant/storage redis: image: redis:7-alpine ports: - "6379:6379" postgres: image: postgres:16 environment: POSTGRES_PASSWORD: hindsight POSTGRES_DB: hindsight ports: - "5432:5432" volumes: - pg_data:/var/lib/postgresql/data volumes: qdrant_data: pg_data:启动命令就一句:docker compose up -d。等十几秒,docker compose ps看到所有服务healthy,就可以进下一步。
注意:Windows上如果Docker Desktop起不来,先检查BIOS虚拟化是否开启,再确认WSL2是否安装。具体排查在下一节展开。
4.2 工作记忆模块的实现:Redis + 摘要触发
工作记忆我用Redis的List结构存,每个会话一个key,比如wm:session:{session_id}。每次新消息LPUSH进去,同时更新一个token_count计数器。当计数器超过阈值,触发摘要任务。
摘要任务的伪代码:
def maybe_summarize(session_id): token_count = redis.get(f"wm:token:{session_id}") if int(token_count) < THRESHOLD: return history = redis.lrange(f"wm:session:{session_id}", 0, -1) summary = llm.summarize(history, prompt=SUMMARY_PROMPT) # 保留最近5轮,其余替换为摘要 recent = history[:5] redis.delete(f"wm:session:{session_id}") redis.rpush(f"wm:session:{session_id}", summary) for msg in reversed(recent): redis.lpush(f"wm:session:{session_id}", msg) redis.set(f"wm:token:{session_id}", estimate_tokens(summary) + sum(estimate_tokens(m) for m in recent))SUMMARY_PROMPT的关键内容是:“请把以下对话压缩成三部分:1. 当前任务目标(一句话);2. 已确认的关键约束(列表);3. 已排除的错误路径(列表)。只保留对后续决策有影响的信息,不要保留寒暄和无关内容。”
4.3 情景记忆的写入与检索:Qdrant + PostgreSQL双写
情景记忆的元数据(task_type、outcome、timestamp等)存PostgreSQL,向量存Qdrant。写入时先写PostgreSQL拿到自增ID,再用这个ID作为Qdrant的point ID,保证两边能关联。
写入代码的核心逻辑:
def write_episodic(task_record): # 1. 写PostgreSQL with pg_conn.cursor() as cur: cur.execute(""" INSERT INTO episodic_memory (task_id, task_type, goal, key_decisions, tools_used, outcome, user_feedback, timestamp) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) RETURNING id """, (...)) pg_id = cur.fetchone()[0] # 2. 写Qdrant qdrant.upsert( collection_name="episodic", points=[{ "id": pg_id, "vector": embed(task_record["goal"] + " " + " ".join(task_record["key_decisions"])), "payload": {"task_type": task_record["task_type"], "outcome": task_record["outcome"]} }] )检索时,先用Qdrant做向量搜索拿Top 20,再用PostgreSQL过滤task_type匹配和时间范围,最后按混合分数排序取Top 5。混合分数的计算:
score = 0.6 * vector_similarity + 0.25 * task_type_match + 0.15 * time_decaytime_decay用指数衰减:exp(-days_since / 30),30天半衰期。这个参数可以根据业务调整,高频任务可以设短一点,低频任务设长一点。
4.4 语义记忆的提炼:定时任务 + LLM归纳
语义记忆的提炼我放在一个定时任务里,每小时跑一次。逻辑是:查PostgreSQL,找出过去一小时新增的、同task_type数量达到10条的组合,对每组触发一次LLM归纳。
归纳的提示词模板:
你是一个Agent经验总结助手。以下是10条同类型任务的情景记录,每条包含目标、关键决策和结果。 请总结出3-5条“如果...那么...”形式的规则,用于指导未来同类任务。 要求: 1. 规则要能适用于同类任务的所有情况,不要包含具体城市、具体时间等一次性信息。 2. 每条规则附带一个0到1的置信度,基于支持它的记录数量和成功率。 3. 如果发现某些决策总是导致失败,也总结成“避免...”形式的规则。归纳结果写入PostgreSQL的semantic_memory表,字段包括:规则文本、置信度、支持记录数、创建时间、最后验证时间。置信度低于0.6的标记为pending,检索时降权。
4.5 MCP中间件的部署与配置
MCP中间件我写成一个独立的Python服务,用FastAPI暴露HTTP接口,内部维护MCP客户端连接。部署时用Dockerfile打包:
FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]requirements.txt里主要就几个包:fastapi、uvicorn、mcp、redis、qdrant-client、psycopg2-binary。
中间件的核心逻辑是拦截MCP消息,异步写入Redis队列:
@app.middleware("http") async def capture_mcp(request, call_next): body = await request.body() response = await call_next(request) # 异步写入队列,不阻塞响应 asyncio.create_task(enqueue_mcp_event(body, response)) return response后台worker消费队列,把工具调用记录关联到当前任务。关联的依据是session_id和task_id,这两个ID在MCP请求的header里带上。
5. 常见问题与排查技巧实录:踩过的坑和填坑方法
5.1 Docker Desktop启动失败:虚拟化检测问题
Windows上最常见的报错是“Virtualization support not detected”或者“Docker Desktop failed to start because virtualization...”。这个问题的根因通常是三个:BIOS里Intel VT-x或AMD-V没开、WSL2没安装或版本太旧、Hyper-V和WSL2冲突。
排查步骤我整理成表:
| 现象 | 可能原因 | 排查方法 | 解决 |
|---|---|---|---|
| 提示虚拟化未检测到 | BIOS虚拟化关闭 | 任务管理器→性能→CPU,看“虚拟化”是否为“已启用” | 重启进BIOS,开Intel VT-x/AMD-V |
| WSL2相关报错 | WSL2未安装 | 命令行跑wsl --status | wsl --install,重启 |
| Hyper-V冲突 | Hyper-V占用 | bcdedit /enum看hypervisorlaunchtype | 设为off,或改用WSL2后端 |
| 启动卡在starting | 镜像损坏 | 看Docker Desktop日志 | 重置Docker Desktop到出厂设置 |
实操心得:如果BIOS里找不到虚拟化选项,可能是厂商隐藏了,试试在BIOS里按Ctrl+F1或Fn+某键解锁高级选项。另外,Windows家庭版没有Hyper-V,只能用WSL2后端,这个在Docker Desktop设置里要选对。
5.2 记忆检索不准:向量相似度不是万能的
我遇到最多的问题是:明明存了相关的情景记忆,但检索时就是查不出来。排查下来,大部分情况是embedding模型和任务描述不匹配。比如用通用embedding模型去编码技术文档类的任务目标,相似度区分度很低。
解决办法有两个:一是换领域适配的embedding模型,比如技术类任务用代码embedding模型;二是在检索时加入关键词过滤,先用task_type缩小范围,再做向量搜索。我通常两个都用,效果最稳。
还有一个隐蔽的坑:情景记忆的goal字段写得太长。如果goal写了200字,embedding会被稀释,相似度计算不准。我的规范是goal控制在50字以内,只保留核心意图,细节放在key_decisions里。
5.3 语义记忆规则冲突:新旧规则打架
随着情景记录积累,语义记忆里会出现互相冲突的规则。比如早期总结出“用户问天气时先问城市”,后期总结出“用户问天气时先查历史位置”。两条规则置信度都高,检索时都注入,Agent就懵了。
我的处理方式是规则版本化 + 冲突检测。每条规则有version和supersedes字段,新规则如果和旧规则冲突,把旧规则标记为deprecated,检索时不再注入。冲突检测用LLM做,提示词是“以下两条规则是否会在同一场景下给出矛盾的行为指导?如果是,哪条更通用?”
另外,我加了一个人工审核环节:置信度高于0.8且涉及核心行为的规则,写入前推送到审核队列,人工确认后才生效。这个环节看起来麻烦,但能避免Agent行为漂移。
5.4 工作记忆摘要丢信息:关键约束被压缩掉
摘要触发太频繁或者提示词不够精确,会导致关键约束被压缩掉。我遇到过一次:用户明确说了“不要用红色”,摘要后这条约束消失了,Agent后续又用了红色,用户很生气。
修复方法是在摘要提示词里强制要求保留否定性约束。具体加了这句:“所有以‘不要’‘禁止’‘避免’开头的约束,必须原样保留,不得改写或省略。”另外,我把摘要后的结果和原始对话做一次一致性检查,用LLM判断“摘要是否遗漏了任何否定性约束”,如果有遗漏就重新摘要。
这个检查会增加一次LLM调用,但相比用户不满意的代价,完全值得。
5.5 MCP工具调用记录丢失:异步队列的可靠性
MCP中间件用异步队列写记忆,如果worker挂了或者Redis满了,记录就丢了。我踩过一次坑:Redis没设maxmemory-policy,内存满了之后新写入直接被拒,丢了一整天的工具调用记录。
修复方案:Redis设maxmemory-policy allkeys-lru,同时给队列加持久化,用Redis的Stream结构而不是List,Stream支持消费者组和ACK机制,worker处理失败可以重试。另外加了一个监控告警,队列长度超过1000就发通知。
5.6 常见问题速查表
| 问题 | 现象 | 根因 | 解决 |
|---|---|---|---|
| 检索不到相关记忆 | 明明存了却查不出 | embedding不匹配或goal太长 | 换领域模型,goal限50字 |
| 规则冲突 | Agent行为矛盾 | 新旧规则同时注入 | 规则版本化,冲突检测 |
| 摘要丢约束 | 否定性约束消失 | 提示词未强制保留 | 加保留指令+一致性检查 |
| 工具记录丢失 | 情景记忆不完整 | 队列无持久化 | 用Redis Stream+ACK |
| Docker起不来 | 虚拟化报错 | BIOS/WSL2问题 | 按5.1表排查 |
| 记忆库膨胀 | 存储增长过快 | 大output直接存 | 只存摘要+引用ID |
6. 记忆系统的扩展方向:从hindsight到更远的未来
6.1 多Agent共享记忆:怎么避免互相污染
单Agent的记忆系统跑通之后,自然会想到多Agent场景。多个Agent共享一套记忆库,好处是经验复用,坏处是容易互相污染——一个Agent的错误经验被另一个Agent学去了。
我的思路是记忆隔离 + 可信度传播。每个Agent有自己的私有记忆区,同时有一个共享记忆区。私有记忆只对自己可见,共享记忆需要经过“可信度评估”才能写入。评估的依据是:这条经验在多少个Agent上验证成功过、成功率多少。只有成功率高于阈值(比如70%)且验证次数超过3次的,才进入共享区。
另外,共享记忆的检索要带“来源Agent”标签,如果当前Agent和来源Agent的任务类型差异大,检索时降权。这个降权系数可以根据历史数据学习,也可以手动设。
6.2 记忆的遗忘机制:不是所有东西都值得记
人类会遗忘,Agent也需要。如果什么都记,记忆库会膨胀,检索质量会下降。我设计了一个遗忘评分,综合三个因子:最后访问时间、访问频率、任务成功率。评分低于阈值的记忆,先标记为“冷存储”,不再参与常规检索;再过一段时间还没被访问,就物理删除。
遗忘评分的公式:
forget_score = 0.5 * exp(-days_since_access / 60) + 0.3 * log(access_count + 1) + 0.2 * success_rate低于0.2的进入冷存储,低于0.05的删除。这个阈值可以根据存储成本调整,存储便宜就放宽,存储贵就收紧。
6.3 与RAG系统的融合:记忆和知识库的边界
很多人会问:记忆系统和RAG知识库有什么区别?我的理解是:知识库存的是“世界的事实”,记忆存的是“Agent的经历”。知识库相对静态,更新频率低;记忆动态增长,和Agent行为强相关。
但在实际系统里,两者会融合。比如Agent在任务中查了知识库,这个查询行为本身应该被记入情景记忆;而知识库的检索结果,如果被证明对任务有帮助,可以反向标注,提升该知识片段的权重。我目前的实现是:知识库检索走独立通道,但检索的query和结果摘要会写入情景记忆的key_decisions,这样后续复盘时能看到“Agent当时查了什么知识”。
6.4 记忆的可解释性:让Agent说清楚“我为什么这么做”
最后一个扩展方向是可解释性。当Agent做出一个决策时,能不能追溯到这个决策是基于哪条记忆?我的做法是在决策日志里记录记忆引用。每次Agent调用LLM生成决策,如果注入了记忆,就在日志里记下注入的记忆ID。这样出问题时,可以回溯“Agent当时参考了哪条经验,这条经验是从哪次任务总结来的”。
这个追溯链对调试非常有用。我遇到过Agent反复犯同一个错误,追溯后发现是一条错误的语义记忆在作祟,删掉那条规则后问题就解决了。没有追溯链的话,这种问题很难定位。
最后分享一个小技巧:记忆系统的日志一定要结构化,每条日志包含
session_id、task_id、memory_id、action、timestamp。这样可以用SQL直接查“某个记忆影响了哪些任务”,排查效率比翻文本日志高一个数量级。
我个人在实际操作中的体会是,hindsight这类记忆系统的价值不在于技术多复杂,而在于持续运行和迭代。一开始规则可能不准,检索可能漏,但只要坚持记录、定期复盘、人工审核关键规则,系统的质量会随时间提升。最怕的是搭完就不管了,那样记忆库很快会变成垃圾场,反而拖累Agent表现。