M7.4 · 会话级短期记忆 P_t 与记忆管家 curator —— 开发文档(面试向)#
M7.5 更新提醒:本篇 §1 发现 2 描述的
canonical_key字段名 bug 修法(落库口key/canonical_key两个字段名都认)已在 M7.5 被替换——canonical_key/key整个概念都废弃了,去重键改为polarity/category/domain/slug四个结构化字段代码派生。本篇正文按当时的历史叙事保留不改,最新字段现状见 M7.5。把记忆判定从购物工作流里彻底剥离成一个独立模块「记忆管家 curator」,并补上论文原本就有、我们此前省略的会话级短期偏好状态 P_t;顺带修掉一个让「记忆合并三原则」一直半失效的字段名 bug。讲为什么这么改、边界在哪,不讲代码。
取代 M7.2 的「三点捕获」:那篇用三个互补捕获点(remember 工具 + 入口兜底 + 终结捕获)把提取放进 reasoning;本篇把这三点收敛成一个独立 curator,原因见下。
一句话概括#
M7.2 把偏好提取铺成了三个捕获点,但它们有个共同问题:都在替别的事忙——shopping_summary 主职是生成清单文案,识别偏好只是它顺手捎带的 afterthought。真实实测里,「今天想要蓝色的」这种本轮一次性约束被它误判成跨会话长期偏好写进了库,污染以后所有品类的检索。M7.4 的判断是:判「本轮约束 vs 一贯取向」是一个需要专注的独立认知任务,不该塞给一个正忙着购物的调用。于是把记忆判定整个剥离出来,由会话结束后独立运行的 curator 专门做;同时补上一层专门承接「本轮约束」的会话级状态 P_t,让它有结构化归宿、不再靠 LLM 在写库瞬间赌对。
1. 触发这次重构的两个真实实测发现#
不是拍脑袋改的。用真实 LLM(deepseek-v4-flash)+ 真实检索跑了两轮实测,暴露出两个性质不同的问题:
发现 1(设计缺陷):本轮约束泄漏成长期。 一句话「这次预算300 / 我一直讨厌塑料 / 今天想要蓝色」跑完,长期库里落了 like:color:blue。「今天想要蓝色」带着极强的本轮信号词,shopping_summary 的 Preservation 纪律本该挡住,它还是漏了——因为那一步模型精力在排清单,记忆判断是附带的。这不是偶发,是结构性的:让一个忙着别的事的调用兼职做需要专注的判断,判准就不稳。
发现 2(确认的 bug):记忆合并三原则一直半失效。 shopping_summary 的偏好字段叫 canonical_key,但落库口读的是 key——字段名对不上,导致 summary 路径生成的 canonical_key 被 100% 静默丢弃、退化成 content 哈希。后果:同一条「不要塑料」经 remember 工具(用 key)和 summary(用 canonical_key)两条路径写入却生成不同 key → 去重失效、重复落库;更糟的是注入给模型看的是 dislike:material:3ed27e94957c 这种无语义哈希,模型想做冲突消解(keys_to_supersede)也对不上号。M7.2 引以为傲的「三点汇进单一落库口、稳定去重键幂等」,在 summary 这条路上其实一直没生效。
两个发现合起来指向同一个结论:记忆这摊事散在购物流程里、和购物逻辑焊在一起,既判不准也容易埋 bug。该拆出来。
2. 三层记忆的分层错位:我们把论文的「短期机制」搬到了「长期库」上#
论文(RecBot, arXiv:2509.21317)的核心记忆机制其实是会话级短期工作记忆 P_t:把一次浏览会话内用户逐轮下的命令,映射成一个会话内稳定累积、逐轮 merge 的结构化偏好状态 {P⁺,P⁻}×{hard,soft};它所谓的「长期」是隐式行为序列,不是结论性偏好库。
我们此前的实现把论文用于会话级短期状态的「合并三原则」,搬到了跨会话长期库上。这对对话式购物是合理适配,但它把「本轮约束 vs 一贯取向」的全部区分压力,压在了「写不写长期库」这一个决策上——中间缺了一层专门装「本轮约束」的东西。发现 1 的泄漏,本质就是这层缺失:蓝色没有会话级归宿,只能在「写不写长期」这道闸上赌,赌输了就污染长期库。
M7.4 补上这层:
| 层 | 载体 | 生命周期 | 装什么 |
|---|---|---|---|
| 短期 / 会话(新增 P_t) | pt.json(会话目录) | 一段 thread + TTL | 本次选购的约束(这次预算/今天要蓝) |
| 长期 | PreferenceEntry Store | 跨会话持久 | 一贯取向(我一直不要塑料) |
| 行为历史 | HistoryEntry | 跨会话 TTL | 上次搜了/选了什么 |
3. 解法:一个独立的记忆管家 + 一层会话级状态#
记忆管家 curator —— 唯一的偏好判定 / 写入口。 会话结束后独立跑一次(时机见下),它的唯一职责就是读本轮对话、判偏好分流:
- 「本轮约束」(预算300、蓝色)→ merge 进 P_t,本会话内累积生效、会话结束自然清理,不进长期库。
- 「一贯取向」(讨厌塑料)→ 经单一落库口提升为跨会话长期偏好,带 canonical_key + keys_to_supersede。
它有独立的 prompt,专教「本轮 vs 一贯」的判别 + 记忆三原则 + hard/soft 分档——不再和购物逻辑混在一个 prompt 里抢注意力。购物主 prompt 因此卸掉了整套记忆三原则,更短、更聚焦。
会话级状态 P_t —— 本轮约束的结构化归宿。
一个 {正/负}×{硬/软} 的约束集 + 预算/品类锚点,落 pt.json(与 turns.json 同处会话目录)。turn 开局读入、注入 system prompt;curator 在其上按会话内三原则(Integration 累积 / Resolution 顶替)merge 本轮新约束再存回。
4. 关键工程决策#
① 时机:后处理异步(主回复下发之后才跑),不在关键路径。
curator 是一次额外 LLM 调用。把它放在 report_task_result 之后——用户已经拿到回复了,curator 的延迟对用户零感知。而且长期偏好本来就服务于未来轮次,晚几百毫秒落库毫无影响。附带好处:后处理扫的是整轮对话轨迹,闲聊 / 澄清 / 取消所有路径统一覆盖,比 M7.2 要模型主动调 remember_preference 更全——这恰好也把 M7.2 里「A 依赖模型自觉调用」那条薄弱点一并消掉了。
② 彻底剥离,不搞并存。 直觉会想「先加 curator、旧捕获点留着并存验证」。没这么做。因为「混在工作流里」正是发现 1 的根因,留着就没根治;而且并存 = 两套语义各写各的、又要防重复,复杂度更高。所以直接删掉 remember_preference 工具、shopping_summary 停止产偏好、主 prompt 删掉记忆三原则——curator 成为唯一写入口。发现 2 那种「两条路径字段名不一致」的 bug,也因为路径收敛成一条而根除。(唯一保留的记忆相关工具是 forget_preference:用户明说「忘掉 X」时即时撤回,与记忆判定正交。)
③ P_t 双通道消费:prompt 注入 + 工具硬 enforce。
- 通道一:P_t render 进 system prompt,让主 loop 把已累积约束折进传给 planner 的 intent。
- 通道二:P_t 塞进 ContextVar,
item_picker直接读——硬 dislike 并入 exclude 淘汰、软并入减分、预算兜底。这一条把「续聊里上一轮说过的不要塑料 / 预算≤X」从prompt 建议升为机制硬保证,不靠模型每轮把旧约束转述进本次调用。它和长期库 dislike 的确定性硬过滤是对称的,item_picker 本就有这个模式,加 P_t 是自然扩展。
④ 会话边界靠 TTL,不强造信号。 没有真实的「会话结束」事件(用户不会点「结束选购」)。P_t 靠 thread_id 天然隔离 + pt.json 的 TTL(默认 24h)兜底:同一 thread 隔很久再来,视为新一段选购,旧约束不复活。没有去造「新意图就重置 P_t」这类启发式——那太脆,容易把正在累积的约束误清。这是「宁可用粗糙但可预测的 TTL,不用聪明但易误伤的启发式」。
5. 诚实度与薄弱点#
- P_t 的续聊携带靠「注入 + 主 loop 折进 planner」+ item_picker 硬 enforce 两道。前者仍是模型行为(可能偶尔不折),但后者是机制保证——所以「上一轮的预算/排除」在精挑阶段是确定性生效的,不押在 prompt 上。实测续聊第 2 轮只说「要金属精致」、不重述,item_picker 仍带
budget_usd=42+ 排除塑料,验证了这一点。 - curator 是模型判断,分流不是 100% 准。清晰两端稳(「这次预算300」不写长期、「我一直讨厌塑料」写);拿不准时 prompt 明确要求倾向判成本轮约束(进 P_t)——因为错放本轮约束只影响这一会话、会话结束即消,错放长期库要污染所有品类,代价不对称。
- 会话边界不精确:TTL 是务实答案,不是精确信号。这是 P_t 无真实 session-end 信号下的固有限制,不是遗漏。
- cache 末尾化未做:P_t 是又一个每轮变的注入块,理想是把长期偏好 / 行为历史 / 会话约束三个易变块一起挪到 prompt 末尾以保住前缀缓存。分析后发现这些块其实已在 prompt 靠后位置(大块稳定协议之后),cache 损失有界(只波及尾部 termination/constraints),且与「硬约束放结尾(位置敏感)」有真实设计张力,故留作配 cache_hit 前后测量的独立跟进,不在本次草率动。
- curator 的 token 不计入本轮成本树——但这与 planner / shopping_summary / category_insight 等所有工具内部 LLM 调用历来一致(成本树只记 agent 中间件的模型调用),不是新增缺口。
6. 验证#
- 单元测试(新增两个文件):P_t 的 merge 三原则(Integration/Resolution)、pt.json roundtrip、TTL 过期当空、损坏降级不崩;curator 的分流(把「预算/蓝色」归 P_t、「塑料」归长期)、匿名不落长期、LLM 失败降级返 None 不抛;item_picker 从 ContextVar 硬 enforce P_t(调用不传 exclude/budget 也照样淘汰);发现 2 的回归测试(canonical_key 必须原样采用、同 key 两写只留一条)。
- 真实 LLM 端到端:①混三种偏好一句话 → 长期库只落「讨厌塑料」,预算/蓝色不入长期,pt.json 完整含预算+蓝色(发现 1 泄漏根治);②同 thread 续聊两轮 → 第 2 轮不重述,检索仍带预算+排除(P_t 携带)。
- 自动门:ruff / format 干净、pytest 381 全过、改动文件 mypy 无新增错误(仓库另有 4 处既有历史 mypy 告警,与本次无关)。发现 2 修复前留下的污染数据(测试残留用户 + hash-slug 条目)用一次性 dry-run 脚本扫描确认后清除。
7. 交付与边界#
交付:
- 新增
app/memory/session_state.py(会话级 P_t:结构 + pt.json 持久化 + merge + TTL)、app/memory/curator.py(记忆管家:唯一判定/写入口,后处理异步、全兜底)。 - 主 prompt 新增会话约束注入位、删除记忆三原则整套;新增独立的 curator prompt。
- 删除
remember_preference工具;shopping_summary停产偏好;main_agent入口 load + 注入 P_t、收尾跑 curator;item_picker从 ContextVar 硬 enforce P_t。 - 修掉
canonical_key字段名 bug(落库口两个字段名都认;Protocol 改只读 property 消掉 Literal 子类型的类型噪音)。 - 一次性污染清理脚本(dry-run 默认)。
边界 / 下一步:
- cache 末尾化是明确的独立跟进项(见上,配前后 cache_hit 测量再做)。
- curator 触发质量待评测:它把本轮 vs 一贯分对的比例、会不会偶尔把一贯误判成本轮(那样长期偏好会漏记),是 M11 评测该盯的一类 case;prompt 已用「拿不准倾向判本轮」的不对称策略兜,实效待测。
- P_t 尚未接 item_search 的召回向量:目前 P_t 只在 item_picker(精挑硬筛)机制生效、在主 loop 经 prompt 影响检索词;把会话约束也喂进召回阶段是更大改动、收益边际,留作后续,ContextVar 口子已预埋。
附:这次改动对「记忆分层」框架的意义#
它把三层记忆的边界第一次理清了:短期 P_t(本次选购,会话态)/ 长期 Store(一贯取向,跨会话)/ 行为 History(上次做了什么),各有独立载体、独立生命周期、独立写入时机。而「谁进哪层」的判定,从散在购物流程里的三个兼职捕获点,收敛成一个专职的 curator——记忆判定不再是购物流水线的附带产物,而是一个独立、专注、可单独评测和迭代的子系统。