主题
第 17 周 · Day 7:周复盘——跨会话记忆验收,让「记得你」变成有证据的事
手册任务:周复盘 + 整理。测试「用户第二次对话时 Agent 记得偏好」,写 300 字周记,当日产出:测试记录 + 周记。 本篇解决的问题:Day 6 总装完的 Agent 看起来已经记得你了,但「看起来」不算数。记忆链路是四段接力,任何一环都可能静默失败而不报错。今天用一场可复现的验收,把三层记忆各自生效的证据拿到手。
今日目标
- 独立跑通跨会话验收剧本:会话 A 自报家门,新开会话 B 提问,Agent 答对且日志链完整
- 掌握不达标排查表:偏好没抽出来、注入没生效、情景没命中,三类故障各自怎么定位
- 按四段模板写 300 字周记,过 10 题自检,答不上的回读对应 Day 的教程
概念讲解:为什么记忆系统必须专门验收
复盘的方法第 1 周 Day 7 已经讲透:识别不是提取,输出倒逼输入,画图、周记、自检三件套每周照用,细节回第 1 周教程。今天概念部分讲本周特有的事:为什么「记忆做完了」必须配一场验收,光跑通代码不算数。进度对照本周日程,Day 1 到 Day 6 的零件今天全部上台受检。
因为记忆链路是四段接力:抽取 → 入库 → 注入与检索 → 生成。它和普通代码最大的区别是:任何一环断了,系统都不报错。抽取器返回 {},是一次「成功」的调用;入库事务没提交,程序照常退出;system prompt 的占位符没替换,模型照样侃侃而谈。用户看到的只是 Agent 客客气气地说「请问您想问什么」。整条链没有一行代码抛异常,但记忆整体失效。
所以验收标准不能是「Agent 答对了」。答对可能靠碰巧:会话 B 的提问方式恰好引导模型猜了个职业;也可能靠作弊:你其实在同一个会话里接着问,窗口里还躺着原文。合格的验收要同时满足四条:
- 跨会话:新开会话、最好新起进程,短期层从零开始,排除窗口内原文作弊
- 答案正确:问「我是做什么的」,回答必须含「后端」;顺带看称呼是不是 Jerry,这是偏好注入的第二证据
- 日志链完整:长期层有 profile 注入日志,情景层有检索命中日志,各自能对上
- 可反证:把 PG 和向量库里这个用户的数据清空再问一遍,Agent 应当答不上来。答不上来,才证明刚才答对靠的是记忆
第四条最容易漏,也最值钱。它把「答对」和「靠记忆答对」区分开。
核心知识
1. 跨会话记忆验收实录
场景剧本固定:会话 A 说「叫我 Jerry,我是做后端开发的」,正常结束;新开会话 B,问「我是做什么的?」。下面是完整终端记录,日志行都是真实链路该有的样子:
text
# ── 会话 A:自报家门 ─────────────────────────────────
$ python agent.py --user u_001
[graph] 会话 sid=a8f31c 启动,入口节点执行
[memory.load] user_profiles(u_001) 为空,按新用户初始化
你: 叫我 Jerry,我是做后端开发的
Agent: 好的 Jerry,记住了,后面都这么称呼。
你: /exit
[redis] sid=a8f31c 共 2 条消息,TTL 生效
# ── 会话 A 收尾,save 节点异步执行 ───────────────────
[memory.save] 偏好抽取 → {"称呼": "Jerry", "职业": "后端开发"}
[memory.save] user_profiles(u_001) upsert ✓ 流水表 +2 条(confidence 0.5)
[memory.save] 情景归档 → 摘要「用户自述名叫 Jerry,从事后端开发」
embedding 入库 ✓ metadata={user_id: u_001, source: conversation}
# ── 会话 B:新起进程,两手空空 ───────────────────────
$ python agent.py --user u_001 # 没有 resume,全新会话
[graph] 会话 sid=c0ffee2 启动,入口节点执行
[memory.load] user_profiles(u_001) → {"称呼": "Jerry", "职业": "后端开发"} ✓ 注入偏好区
[redis] sid=c0ffee2 无历史,短期层为空
你: 我是做什么的?
[memory.load] 情景检索「我是做什么的」→ 命中 1 条
sim=0.81「用户自述名叫 Jerry,从事后端开发」(source=conversation)
Agent: Jerry,你是做后端开发的。对照验收四条:跨会话 ✓(两个不同 sid,两次独立进程);答案正确 ✓(「后端」和 Jerry 都对上了);日志链下面逐层核对;反证你自己在库里跑。
三层记忆各自命中的证据,一张表说死:
| 记忆层 | 命中证据 | 对应日志行 |
|---|---|---|
| PG 长期(档案) | profile 注入 | [memory.load] user_profiles(u_001) → {…} ✓ 注入偏好区 |
| 向量库(情景) | 相似度检索命中 | sim=0.81「用户自述名叫 Jerry…」(source=conversation) |
| Redis 短期(窗口) | 空仓反证 | [redis] sid=c0ffee2 无历史,短期层为空 |
第三行是排除法:会话 B 的短期层是空的,答案不可能来自窗口,只能来自下面两层。短期记忆在这次验收里的角色不是「命中」,而是「证明自己没参与」。整套证据和 Day 1 的架构图完全咬合:读的时机(会话开始注入、提问时检索命中)、写的时机(收尾提炼归档),一条条都能在日志里指出来。指不出来的那条,就是没做对的那条。
2. 验收不达标排查表
剧本跑挂了别急着改代码,先定位断在哪一环。排查总原则:顺着链路查,每环只看一样东西。抽取看日志、注入看最终 prompt、检索看过滤条件。
| 症状 | 病根大概率在 | 排查动作 |
|---|---|---|
会话 A 收尾没有抽取日志,或抽出来是 {} | 偏好没抽出来 | 单独跑 extract_preferences(transcript),print 出 LLM 原始输出:多半输出了讲解文字而不是 JSON,回 EXTRACT_PROMPT 把「只输出 JSON」重申一遍;确认 response_format 和 temperature=0 都在 |
抽到了,但 SELECT kv FROM user_profiles 查出来是空 | 入库断了 | 看 save 节点的日志时间戳,确认异步任务真的执行了;查事务有没有 commit;查 user_id 是不是写串了 |
| 库里有,会话 B 还是失忆 | 注入没生效 | 打印最终发给模型的 messages[0],注意是组装后的最终产物,不是 build_system_prompt 的返回值。两个常见病:模板占位符 {preferences} 没替换;Day 2 的压缩管线把 system 消息动了(system 永不参与压缩,Day 2 坑 5) |
| 情景检索 0 命中 | 向量没入库或过滤错了 | 先查会话 A 的归档日志和入库条数;再打印检索时的过滤条件,user_id 和 source=conversation 两个都要有。高频错误:metadata 的 key 和过滤字段名对不上;过滤值引用了别的变量 |
| 命中了,但返回的是知识库文档 | source 过滤没加 | Day 1 坑 4 原样复发,检索时把 source=knowledge 挡在门外 |
| 没有日志但 Agent 答对了 | 假阳性 | 会话 B 的提问别带任何线索(别自己说「我这个后端项目」)。验收以日志链为准,不以答案为准 |
表里最值得强调的是第三行。注入是四段链路里最隐蔽的一环:函数返回值是对的,模板渲染是错的,中间没有任何报错。借这个养成一个习惯:验收时永远打印最终产物,发出去什么就打印什么,别打印「应该发出去的东西」。
3. 300 字周记(第 17 周示例)
四段模板不变:最大收获、卡得最久、还含糊、下周前补。第 17 周示例,照这个密度写:
text
① 本周最大收获:记忆不是给 Agent 加一张「存对话」的表,是按生命周期分
层放:窗口管当下,档案管「你是谁」,向量库管「发生过什么」。context rot
最颠覆认知,窗口 200k 也不许放开聊,token 是注意力预算,塞满等于用差。
② 卡得最久:会话 B 死活答不上「我是做什么的」,数据明明在库里。最后打
印最终 system prompt 才看到:模板的 {preferences} 占位符压根没替换,注
入了个寂寞。教训:查注入看组装后的最终产物,不看函数返回值。
③ 还含糊:隐式偏好怎么和置信度联动没想透;情景检索时间衰减的半衰期
拍不出依据。
④ 下周前补:读 Generative Agents 的三因子检索;窗口 N 拿第 16 周的评
估集跑一轮对比,让数据选 N。4. 第 17 周知识自检清单
规则同前:每题先口头回答,说完整了再点开对照。说不出的记下题号,回读对应 Day 的教程。
问题 1:记忆四分类是哪四类?各自的 Agent 对应物是什么?(Day 1)
答案
短期(工作记忆)→ 当前会话最近 N 轮对话窗口;长期·事实 → 用户档案(偏好、约束、关键事实);长期·情景 → 历史对话事件,可检索回忆;长期·语义 → RAG 知识库,第 14 周已经建好。最容易混的是情景和语义:情景的主语是「我」,带时间带当事人;语义和经历者无关,对谁都成立。
问题 2:三层存储各自放什么?生命周期分别是什么?(Day 1)
答案
Redis 放当前会话最近 N 轮,TTL,会话结束即过期;PG 放提炼后的用户事实档案,一用户一行,永久、可更新覆盖;向量库放情景和语义两类(历史对话摘要、知识库文档),长期、只增可标废弃,用 metadata 的 source 字段区分两块数据。
问题 3:什么是 context rot?它和成本各是哪笔账?(Day 2)
答案
上下文腐烂:窗口越长,模型注意力越稀释,中段信息召回变差、跨段推理出错、指令遵循下滑,这是质量账。另一笔是经济账:聊天 API 无状态,每轮重发全部历史,成本和延迟随轮次线性膨胀。两笔账指向同一个结论:token 是注意力预算,不是塞满就好,是塞对才好。
问题 4:Deep Agents 的 compaction 三招是什么?各自管什么?(Day 2)
答案
① 超长工具结果卸载:超过阈值的结果原文进虚拟文件,上下文只留占位符(文件名、行数、开头摘要、read_file 取回句柄);② 子 Agent 上下文隔离:重活的中间过程死在子 Agent 的独立窗口里,主上下文只收结论;③ 外置任务清单:规划状态放外部文件,不占对话历史。共同思想:把「模型可能要看的」和「现在就要看的」分开。
问题 5:显式偏好和隐式偏好差在哪?为什么先做显式?(Day 3)
答案
显式是用户亲口说过的(称呼、职业、回复风格),对话原文就是证据;隐式是从行为推断的(总追问底层原理 → 偏好深度内容)。先做显式,因为它抽对了就是对、抽错了好排查;隐式要靠猜,行为信号可能只是那次任务的需要而非稳定偏好,猜错一次 Agent 就永久把人认错。
问题 6:情景记忆入库为什么存摘要,不存逐字原文?(Day 4)
答案
三个原因。一,整段原文做 embedding 语义被稀释,一段对话聊了五件事,向量哪个都不像;摘要一段一主题,检索才准。二,命中后注入的是检索结果,摘要短,不会一命中就往上下文塞几千 token,这是 Day 2「按需调入」思想在记忆层的延续。三,回忆要的是事件三要素(时间、当事人、结论),不是逐字稿,原文留在日志里,要细节凭 ID 回取。
问题 7:情景检索的相似度得分为什么要乘时间衰减?(Day 4)
答案
情景有保鲜期。用户去年说自己写 React,今年早换 Vue 了,旧情景不衰减就和新鲜记忆同分,注入的「回忆」其实是过时噪声。做法是最终得分 = 相似度 × 衰减因子,时间越久权重越低。少了这一项,检索会变成把用户的历史原样倒回来,越用越像背旧账。
问题 8:System Prompt 模板的分区顺序有什么讲究?(Day 5)
答案
稳定区在前(角色、工具说明),动态区在后(记忆摘要、用户偏好、本次情景检索结果)。两个理由:一,prompt 缓存按前缀命中,稳定内容放头部,前缀不变缓存才有效,每会话都变的记忆内容放尾部,失效的只有尾部;二,离生成点越近的指令遵循越好,偏好和本次检索结果贴着对话区,模型不容易当耳旁风。
问题 9:checkpoint 和记忆的分工边界是什么?(Day 1)
答案
checkpoint 绑会话线程,存运行状态(消息、工具调用轨迹),管同一个对话中断后能续命;记忆绑用户,存提炼后的信息(事实、事件、知识),管不同对话共享认知。互补不是替代:会话开始注入档案、每轮读写窗口、结束提炼归档,底下 checkpoint 继续管中断恢复。一句话:checkpoint 让对话接着聊,记忆让 Agent 越用越懂你。
问题 10:每轮收尾的 save 节点为什么做成异步?(Day 6)
答案
三个理由。一,保存链路里有一次 LLM 抽取调用,同步等它,用户下一句回复要平白多等几秒;二,保存失败不能拖垮主对话,异步隔离后失败了能重试,主链路无感;三,晚几百毫秒入库无害,下一轮注入的是 profile 不是刚抽出来的这条,但主链路慢半秒用户立刻有体感。优先级排序:主对话延迟的优先级高于记忆时效性。
动手任务:完成本周复盘
按顺序做完五步,预计 60 到 90 分钟。
第一步:跑验收剧本(20 分钟)。 原样照核心知识第 1 节的场景跑:会话 A 自报家门,正常退出;新起进程开会话 B,提问。终端输出完整保存成 week17-acceptance.log,这是今天的产出之一。
第二步:核对三层证据(10 分钟)。 对着证据表逐层打勾:profile 注入日志、情景检索日志、短期层空仓反证。再做反证:清空 user_profiles 和向量库里 u_001 的数据,重跑会话 B,Agent 应当答不上来。做完把数据恢复(或干脆重跑一遍会话 A)。
第三步:不达标就排查(时间看造化)。 按排查表顺链路定位,一次只改一个地方,改完重跑整个剧本。四条标准全过才叫达标,别看见答案对就收手。
第四步:写 300 字周记(15 分钟)。 四段模板,对照示例的密度。「还含糊」那段别敷衍,它是下周的输入。
第五步:自检 + git 收口(30 分钟)。 10 题逐个口头回答,答不上来的记下题号,回读对应 Day 的教程。然后分类型提交,打 tag:
bash
git add week17/
git commit -m "feat(memory): 三层记忆总装"
git add week17-acceptance.log
git commit -m "test(memory): 跨会话记忆验收记录"
git tag week17-done原则照旧:一个提交只做一件事。代码、测试记录、周记各走各的提交。
常见踩坑
在同一个会话里验收。 问完「叫我 Jerry」接着问「我是做什么的」,Agent 答对了,你高高兴兴收工。可这什么都没证明:窗口里还躺着你的原话,长期记忆根本没参与。必须新开会话,最好新起进程,让短期层从零开始。
只看答案不看日志链。 答案正确加日志链完整,两条都满足才算过。没有日志的答对可能是碰巧,你修也不是、不修也不是。验收的产出是证据,不是截图里那一句回复。
user_id 前后不一致。 会话 A 用 u_001,会话 B 手滑传了 u_002,Agent 当然失忆。排查表全过一遍也查不出毛病,因为链路本身没病。验收脚本里把 user_id 提成常量,两个会话共用。
只测 happy path。 改口场景(「别叫 Jerry 了,叫我老王」,画像要覆盖、流水要留痕)和隐私场景(对话里夹一句病史,确认它没被抽进库)也是本周链路承诺过的行为,一并过一遍。第 16 周说的道理在这周同样成立:测试集里只有 happy path,等于没有测试。
测试数据污染。 验收完 u_001 的假档案留在库里,哪天接入真实用户,他从第一天起就「叫 Jerry、做后端」。测试统一用 test_ 前缀的 user_id,验收完清掉。
延伸阅读
- Generative Agents: Interactive Simulacra of Human Behavior,斯坦福小镇论文,记忆流加三因子检索(recency × importance × relevance),时间衰减和重要性的理论老家,本周的记忆设计多处取材于此
- LangGraph:Memory 概念文档,官方对短期/长期记忆的切分,Day 6 整合进 LangGraph 之后再通读一遍,看官方抽象和本周三层怎么对上
- Anthropic:Effective context engineering for AI agents,Day 2 读过一遍,学完记忆再读:compaction 三招和记忆分层其实是同一件事的两面,都在决定什么配进上下文
下周开始前,把周记第四段留的补课动作清掉。第 17 周到此收口:Agent 从「每次见面都失忆」走到了「第二次开口就喊对名字」,而且这一次,你手里有整条证据链。