记忆分层与会话级短期记忆(P_t)—— 讨论稿#
这份文档的用途:交接给一个新对话讨论「要不要给 ShoppingX 引入一层显式的会话级短期记忆」。 文档自包含,读完即可开始讨论,无需前序对话上下文。核心结论先说:论文的核心记忆机制其实是 「会话级短期工作记忆」,而我们把它搬到了「跨会话长期库」上——中间缺一层显式的会话级短期记忆。
✅ 已落地(结论回填,2026-07):讨论已收敛并实现——引入了 P_t + 独立记忆管家 curator。 触发点不是空想,而是两次真实实测发现(本轮约束「蓝色」泄漏成长期 + 一个让记忆三原则半失效的
canonical_key字段名 bug)。落地结论见文末 §10,完整开发文档见docs/milestones/M7.4。下文 §1–§9 保留作 讨论原貌(问题定义、取舍推演),理解推演过程比只看结论更有价值。
1. 触发这次讨论的问题#
用户场景:在一次选购对话里,用户逐步说「我要蓝色的」「不要塑料的」「喜欢精致一点的(比如金属制)」。
问题:这些偏好该进跨会话长期记忆,还是应该有一种短期/会话记忆,专门承接「单次对话内」的约束?
2. 论文 RecBot《Interactive Recommendation Agent with Active User Commands》(arXiv:2509.21317) 的记忆实现#
论文把记忆分成两块(§2 问题定义、§3.2 Parser):
-
P_t(Explicit Preferences,显式偏好)= 会话级短期结构化偏好状态。- 定义
P_t = φ(c_1, c_2, …, c_t):把一次浏览会话内用户下的一系列自然语言命令c_1..c_t,映射成结构化偏好。 - 结构:
P_t = {P⁺, P⁻} × {hard, soft}(正/负 × 硬约束/软倾向)。 - 动态记忆合并三原则(Preservation / Integration / Resolution)作用的正是这个
P_t:P_{t+1} = φ(P_t, c_t, R_t),会话内每一轮根据新命令 merge 更新。 - 生命周期:
t ∈ {1…T_max},T_max是「用户疲劳阈值」。会话结束,P_t即止——论文没有把P_t持久化到跨会话长期库的机制。它是 sequential decision 的 state。
- 定义
-
H_t(Implicit Preferences,隐式偏好)= 用户历史交互序列(长期行为)。- 这是用户过往点击/购买的行为轨迹,隐式、长期,喂给 Matcher 的协同通道。
结论:论文真正的「记忆合并机制」是短期的、会话级的工作记忆(P_t);它所谓的「长期」是隐式行为序列 H_t,不是我们理解的「结论性偏好长期库」。
3. 我们系统(ShoppingX)的记忆现状:三层#
| 层 | 载体 | 生命周期 | 装什么 |
|---|---|---|---|
| 短期 / 会话 | planner 当轮解析出的结构化字段(budget / exclude_keywords / soft_dislikes / prefer_keywords / material_pref…)+ 主 loop 的 messages 上下文 | 一次 run_agent / 一个 thread | 本次选购的约束 |
| 长期 | PreferenceEntry Store(app/memory/store.py,本地 JSON / Redis) | 跨会话持久 | 用户一贯取向(like/dislike,带 strength、canonical_key) |
| 行为历史 | HistoryEntry(last_search 等,TTL 30 天) | 跨会话 | 上次搜了/选了什么 |
关键错位:我们最近做的「记忆合并三原则 + dislike 的 hard/soft strength 双档 + keys_to_supersede 冲突消解」——全部落在「长期库」上(见 commit 693079c)。也就是说,我们把论文用于「会话级短期状态」的合并机制,搬到了「跨会话长期库」上。适配对我们的对话式购物是合理的,但它把「本轮约束」和「一贯取向」的区分压力,全压在了「写不写长期库」这个决策上。
中间那层(短期/会话)没有独立的结构化持久对象——它只是「planner 当轮输出 + messages 上下文」,跑完即弃(除非被 remember_preference 提升为长期)。
4. 场景分析:那三句话现在会去哪#
判断标准只有一条:这是「关于这次购买的约束」还是「关于用户这个人的一贯取向」?
| 用户说 | 性质 | 现在的归宿 |
|---|---|---|
| 「我要蓝色的」 | 本次购买约束(不是”买任何东西都要蓝”) | 短期:planner 当轮字段 → item_search/item_picker,不落长期库 ✅ |
| 「我喜欢金属精致的风格」 | 跨品类的一贯风格取向 | 长期:应 remember 进 Store |
| 「我不要塑料的」 | 歧义(本次约束 or 一贯取向?) | 现在靠 LLM 结合语境判断 + strength 分档 |
我们上一轮做的 Preservation 纪律(「对当前结果的满意评价 / 本轮一次性约束一律不写长期」)+ strength 双档,就是在长期库这一层,靠 prompt 纪律 + LLM 判断,去挡住「本轮约束」污染长期。
5. 缺口:我们没有论文那种「显式的会话级结构化偏好状态 P_t」#
现状用「每轮 planner 重新解析当前 intent + messages 自然累积」来代替会话级记忆。这在续聊多轮时不稳:
例:第 1 轮说「不要塑料」,第 2 轮说「再看看别的颜色」。续聊靠
main_agent.py的load_prior_turns回喂精简对话对,第 2 轮的planner要从对话历史里重新捞出「不要塑料」——依赖 LLM 每轮重解析,可能漏、不稳定。
论文的 P_t 恰好解决这个:一个会话内稳定累积、逐轮 merge 的结构化状态,让「本轮约束」有明确归宿——既不必每轮靠 LLM 从 messages 重捞,又能在会话结束时自然清理、不污染长期库(而不是像现在靠 Preservation 纪律去挡)。
6. 待讨论的核心问题#
要不要给 ShoppingX 引入一层显式的「会话级短期偏好状态 P_t」? 如果引入,需要定的设计维度:
- 存哪 / 生命周期:进程内会话状态?绑 thread_id?会话(thread)结束或 TTL 到期即清?和
output/<thread_id>/会话目录的关系? - 结构:直接复用长期库的
PreferenceEntry(带 strength)结构,还是更轻的会话态结构?是否也用{正/负}×{硬/软}? - 谁写谁读:
planner每轮解析后 merge 进 P_t(用三原则)?item_search/item_picker从 P_t 取当轮约束,而不是只从当轮 planner 输出取? - 与三层的边界:
- P_t(短期)↔ 长期 Store:什么时候把 P_t 里的偏好「提升」为长期(用户一贯取向)?还是仍由
remember显式提升? - P_t ↔ 现有 planner+messages:P_t 取代「每轮重解析」,还是叠加?
- P_t(短期)↔ 长期 Store:什么时候把 P_t 里的偏好「提升」为长期(用户一贯取向)?还是仍由
- 清理时机:会话结束怎么判定(无真实 session 结束信号,靠 TTL?靠新意图重置?)
- 收益 vs 代价:
- 收益:本轮约束有稳定归宿、续聊不丢、多轮迭代精炼(「蓝色→再要金属→放宽预算」稳定累积)、长期库更干净。
- 代价:新增一层会话级状态管理,和现在「靠 messages」是两套东西,复杂度上升;YAGNI 风险(是否真到了多轮精炼是瓶颈的地步)。
倾向性判断(供讨论起点,非结论):先想清楚「本轮约束 vs 一贯取向」的分流,是否单靠现有「planner 当轮字段 + Preservation 纪律」就够;只有当续聊多轮丢约束被证明是真实痛点(最好有 bad case / 评测数据支撑)时,再引入 P_t。这与已定的「靠齐深度先浅层」一致。
7. 相关代码位置(新对话定位用,均为 commit 693079c 后的现状)#
app/tools/planner.py—— 当轮意图解析;输出PlanOutput(含soft_dislikes/exclude_keywords/prefer_keywords/ budget 等)。这是我们「短期约束」的主要产出点。app/memory/store.py——PreferenceEntry(长期,含strength: hard|soft、canonical_key、recency_weight半衰期);LocalFileStore/RedisStore。app/memory/injector.py—— 长期记忆读注入(build_preference_block/build_user_profile)+ 写沉淀(persist_new_preferences,含keys_to_supersede冲突消解)+ dislike 分流(dislike_exclude_terms硬 /dislike_attenuate_terms软)。app/tools/remember_preference.py/app/tools/shopping_summary.py—— 长期记忆的两个写入捕获点。app/agent/main_agent.py——run_agent入口注入长期记忆;load_prior_turns续聊回喂精简对话对(当前「会话记忆」的实际承载方式)。prompt/prompts.yml——<user_long_term_preferences>注入位(在文件中部);tool_policy 里的记忆三原则决策 + Preservation 纪律;planner_prompt教 soft_dislikes。
8. 已拍板的相关决策(背景,避免新对话重复讨论)#
- 记忆合并走「路A」:
planner只「感知」历史(读),落库仍走单一口persist_new_preferences——不把合并搬进 planner(planner 是按需工具、不每轮必过,挂落库会漏)。 - 靠齐深度先「浅层」:暂不引入会话级 P_t(本讨论就是要重新评估这条)。
- 已完成(commit
693079c):记忆合并三原则 + dislike 负向 hard/soft 双档(Attenuator)+ 术语keys_to_supersede。 - 不做:蒸馏 SFT、协同 AIA 训练(无 GPU);论文的 recency/forget 我们已有且更强。
- 关联的另一条待办:cache 位置隔离(记忆块每轮更新会打碎跨轮 system prompt 前缀缓存,需把易变的记忆挪到 system prompt 末尾 + 拆 content block)——与本讨论相关(若引入 P_t,其注入位置同样要考虑 cache)。
9. 一句话给新对话#
我们已经把论文的「短期合并机制」落在了「长期库」上并跑通;现在要讨论的是是否补一层论文原本就有、我们却省略了的「会话级短期结构化偏好状态 P_t」,以及它与现有 planner / messages / 长期 Store 三者的分工与边界。
10. 落地结论(回填)#
讨论的走法是「先证伪、再动土」——不空谈要不要引入,先用真实 LLM 实测缺口是否真咬人。
10.1 两次实测(决策依据)#
- 实测 A(续聊丢约束?):同 thread 续聊两轮、匿名用户(隔离长期库),第 1 轮「预算300 不要塑料」、第 2 轮只说「有没有金属的」。结果 planner 收到的 intent 仍含预算+排除——主 loop 已能从文本回喂里重构约束,没丢。§6 设的门槛(「续聊丢约束被证明是真痛点才引入」)没在动机例子上复现,一度指向「YAGNI、暂不引入」。
- 实测 B(分流闸漏不漏?):一句话混三种偏好「这次预算300 / 我一直讨厌塑料 / 今天想要蓝色」,跑一轮带真实 user_id,查 Store 落了哪几条。结果闸三判对一个半:预算300 正确没写长期 ✅;塑料正确写了长期但重复落了 2 条 ❌;「今天想要蓝色」被误固化成
like:color:blue长期偏好 ❌。
实测 B 翻转了结论:§4 表里那个「歧义中间地带无结构兜底」的担心被实锤了,而且比预期更糟——「今天」是极强的本轮信号,纯 LLM 的 Preservation 判断仍漏。同时挖出 §5 之外的一个真 bug:shopping_summary 的 canonical_key 因字段名与落库口读的 key 不匹配,被 100% 静默丢弃、退化成 content 哈希,导致重复落库、且冲突消解(keys_to_supersede)对 summary 偏好一直无效——即 §3「记忆合并三原则搬到长期库」这套,在 summary 路径上其实一直半失效。
10.2 最终方案(已实现,见 M7.4)#
不是「简单加一层 P_t」,而是把根因(记忆判定混在购物工作流里、判不准也埋 bug)一并治掉:
- 独立记忆管家 curator:把散在三处(
shopping_summary终结抽取 /remember_preference工具 / 主 prompt 三原则)的记忆判定彻底剥离成一个独立模块,会话结束后后处理异步跑(用户零感知延迟),成为唯一偏好写入口。删掉remember_preference工具、summary 停产偏好、主 prompt 卸掉三原则。§6 的设计维度「谁写谁读 / 与三层边界」在这里定案:curator 独家判定 + 单一落库口。 - 会话级 P_t(
app/memory/session_state.py):{正/负}×{硬/软}约束集,落pt.json,逐轮 merge(Integration/Resolution),TTL 兜会话边界。对应 §6 的「存哪/生命周期/结构/清理时机」。 - P_t 双通道消费:注入 system prompt(主 loop 折进 planner)+ 塞 ContextVar 供
item_picker机制性硬 enforce(硬 dislike 并 exclude、预算兜底)——对应 §6「谁读」,且比原设想更强(从 prompt 建议升为硬保证)。 - 顺带修掉 §5 之外发现的
canonical_key字段名 bug。
10.3 对当初「倾向性判断」的修正#
§6 的起点倾向是「先浅层、别急着上 P_t,等 bad case」。实测 B 就是那个 bad case,且暴露的不止「丢约束」还有「污染长期库 + 底层 bug」,故从「暂不引入」转为「引入,并借机把记忆判定整个剥离干净」。§8 那条 cache 位置隔离仍是独立待办(P_t 注入让它更紧迫,但分析后 cache 损失有界、且与「硬约束放结尾」有张力,留作配前后测量单独做);会话结束无真实信号是固有限制,靠 TTL 务实兜、不强造启发式。
一句话:这层短期记忆最终不是照着论文补的「缺失层」,而是被两个实测发现逼出来的一次记忆子系统重构——分层理清、判定专职、bug 根除。