面试知识库

M7.2 · 偏好提取放进 reasoning:三点捕获、单一落库口 —— 开发文档(面试向)#

⚠️ 已被 M7.4 取代:本篇的「三点捕获」(remember 工具 + 入口兜底 + 终结捕获)后来被收敛成一个独立的记忆管家 curator。原因是真实实测暴露了两个问题——① 三个捕获点都在替别的事忙(summary 忙着排清单),导致本轮一次性约束被误判成长期偏好;② 终结捕获路径有个 canonical_key 字段名 bug,让「单一落库口去重」一直半失效。M7.4 还补上了本篇没有的「会话级短期状态 P_t」。下文保留作演进记录——理解「为什么最终没这么做」比结论本身更有价值。

把 M7 的长期记忆「写回」侧从只在收尾抽一次,升级成模型可在任意 reasoning 步自主沉淀 + 机制兜底。讲为什么这么设计、边界在哪,不讲代码。

一句话概括#

M7 把偏好落库接好了,但提取只发生在一个时刻:主链路走到 shopping_summary 终结时,由那一步的 LLM 顺带吐出 new_preferences。后果是——没走到收尾的路径(闲聊 / 澄清 / 用户取消)里说的「不要塑料」,全丢。M7.2 把提取从「终结工具的附带产物」升级为三个互补捕获点:模型在 reasoning 里自主调的 remember_preference 工具 + 入口规则兜底 + 保留的终结捕获,三者汇进同一个落库口

1. 先厘清一个概念陷阱:「Reflect 阶段」在代码里并不存在#

教学框架讲 Think→Act→Observe→Reflect,很容易让人以为代码里有个「Reflect 钩子」,可以在里面挂偏好提取。没有。 主 loop 是 create_react_agent 的一次 ainvoke,内部只有 model 节点 ⇄ tool 节点 两个节点转圈:model 节点一次 LLM 调用同时干了 Think(往前规划)和 Reflect(回看够没够),它们本就是同一次推理的两面,拆不成两个代码阶段。

所以「Reflect 何时发生」的答案是:每一次 model 节点被唤醒时(每个工具执行完、模型下一次睁眼那刻)。这直接决定了 M7 的写回为什么只能搭在终结那一步——代码里没有逐轮的 Reflect 钩子能让「用户一说不要塑料就落库」。要把提取放进 reasoning,只有一条路:给模型一个它能在任意一步调用的工具。模型 reasoning 唯一能产生副作用的方式,就是调工具。

2. 顺带答一个高频面试题:文档里的 NLU 是什么#

refdocs/06 把「记忆自动提取」推给「高级章节」,理由是「涉及 NLU」。NLU = Natural Language Understanding(自然语言理解):从用户自由文本里判断「有没有表达偏好、是喜欢还是排斥、针对什么(材质/品牌/预算)、能抽出哪些可过滤的关键词」——把非结构化的话变成结构化槽位。

关键认知:本项目其实早就在用 LLM 充当 NLU 提取器planner(把「便宜抗造不要塑料」拆成结构化字段)、shopping_summary(识别 NewPreference)干的就是 NLU 的活。文档里「生产环境可用 NLU 模型」对照的是它自己给的关键词匹配("不要" in msg)那种最原始版本。所以对我们而言 NLU 不是空白,而是「已用 LLM 实现,只差覆盖非终结路径」——这正是 M7.2 补的。

3. 解法:三点捕获,各司其职,汇进一个口#

捕获点时机定位
A. remember_preference 工具模型任意一步 reasoning 识别到持久偏好即调主力:把提取真正放进 reasoning,路径无关、非终结、记完继续跑
B. 入口规则兜底每轮入口扫本轮原文机制兜底:模型忘了调 A、且没走到收尾时,兜住强 dislike
C. 终结捕获(保留)shopping_summary 收尾那步最可靠的兜底点,零改动

为什么是「工具」而非「prompt 让模型脑内记住」:偏好要跨会话生效就必须落库,而 reasoning 本身写不了 Store——它只能调工具。所以「in-reasoning 提取」的唯一可落地形态就是一个非终结工具。它进 FULL_TOOL_SET(同质 fork 硬约束),主 / 子 Agent 共享。

三者汇进同一个落库口(persist_new_preferences):稳定去重键(polarity:category:内容哈希)让三处重叠只走「覆盖 + 提置信」,不堆重复条目。这带来一个刻意的设计决定——见下节。

4. 三个工程决策#

① 不删终结捕获(C),接受「三写幂等」而非「单写去重」。 直觉会想「加了 A,就该删掉 C 的 new_preferences 避免双写」。没删。因为:(a)双写在这里幂等无害——同 content 落同一 key,merge 只是置信度 +0.2 封顶 1.0;(b)删它要改 4 个测试、还拆掉最可靠的捕获点(模型收尾必调 shopping_summary,终结捕获命中率最高)。目标「提取放进 reasoning」由 A 达成,不需要靠删 C 实现。能用幂等消解的冲突,就别用删除去换。

② B 定位成「高召回、低精度」,靠「结果无害」而非「抽得准」兜底。 入口规则扫强负向线索(不要 / 别推 / 讨厌…)抽被排斥的对象词。中文没分词,抽出来的词难免带脏(「不要担心」抽出「担心」)。为什么敢用这么糙的规则:抽错的词对 item_picker 硬过滤天然无害——硬过滤只淘汰「标题/属性里命中该词」的商品,脏词(担心)根本不匹配任何商品 → 不会误杀。所以 B 的安全性来自结果无害(inert),不来自「抽得准」。再叠一层极小 stopword 兜掉最刺眼的闲聊误命中,少落脏条目。真正的高质量抽取仍归 A 的 LLM 路。

③ B 放入口而非 middleware 逐步钩子——躲开置信度虚高。 最初设想把 B 挂 post-step(每个 model 步后扫)。问题:一次 ainvoke 里 human 消息恒定不变,post-step 每步都命中同一句 → 同一偏好被反复写 → 置信度一轮内被灌到封顶。改成入口扫一次:每轮 query 只处理一次,跨轮重复表达才正常累加置信度(多次提及→更可信),语义正确。这是「逐步 hook 看着更实时,实则制造重复」的一个具体反例。

5. 诚实度与薄弱点#

  • A 依赖模型自觉调用。纯 A 是 prompt 依赖的——模型忘了调就不记。这正是 B 存在的理由:把「用户明说的强 dislike 绝不丢」当正确性保证,用机制(规则兜底)兜,不押在 prompt 上。这与项目既有原则一致(子 Agent 边界靠机制而非 prompt)。
  • B 只兜 dislike、只兜强线索。like 偏好(「喜欢小众」)语义太软,规则抽不出,只能靠 A / C 的 LLM 路;漏了 like 的危害也远小于漏 dislike(前者少一点个性化,后者违背明确禁令)。这是按危害不对称做的取舍,不是遗漏。
  • 本轮提取的收益是跨会话,不是当轮。当轮说的偏好已在消息历史里、模型直接看得到,不需要走 Store;更不能中途把它重注入 system prompt——那会打断前缀、毁掉 M5/M6 辛苦保住的 prompt cache。所以「写」在当轮任意点都行,「读 / 注入」永远只在下一个会话开头做一次。

6. 验证#

  • B 抽取正确性:强 dislike 抽对象词 + 剥拖尾助词(「不要塑料的」→塑料)、多线索去重、闲聊/无线索不误命中(stopword 挡掉「担心」)、抽出的词经落库口能被 dislike_exclude_terms 当硬过滤词取到。
  • A 工具:登录用户落库 + 回显 saved、匿名会话不沉淀(no-op)、reasoning 内落库即进本轮累加器。
  • 累加器:落库成功才记、非 run_agent 上下文(离线脚本/直调)自动 no-op(躲开「模块级可变默认列表跨任务串台」经典坑)、fork 子里记的偏好冒泡回主轮。
  • 新增 7 条测试。自动门:ruff / format 干净、pytest 325 全过、改动文件 mypy 无新增错误(仓库另有 4 处既有历史 mypy 告警,与本次无关)。

7. 交付与边界#

交付:新增 remember_preference 非终结工具(进 FULL_TOOL_SET)+ prompt 触发规则;入口规则兜底 extract_dislikes_by_rule;persist_new_preferences 收口为唯一落库口 + 本轮「已沉淀偏好」累加器(供 learned_preferences 返回汇总,含 A/B/C 三源去重);落库口签名改 Sequence(协变,消掉 list 不变性的类型噪音)。C 的终结捕获零改动。

边界 / 下一步:

  • B 是规则、不是 NLU:强负向线索硬编码,覆盖不了「我对塑料过敏」这种无线索的表达——那类只能靠 A 的 LLM 判断。B 只保底最高频、最明确的一类,不追求召全。
  • history 类记忆仍未做(已在 M7.3 补上):refdocs/06 的 Store 里有「上次买了 X / 上次搜了 Y」的 history 段——连同 forget 撤回入口、极性矛盾消解一起,见 M7.3
  • A 触发率待评测:模型多大比例会主动调 remember_preference、会不会过度触发(把一次性约束当长期偏好),是 M11 评测该盯的一类 case;prompt 已写「只记跨会话仍成立的稳定偏好」做约束,实效待测。
  • 极性误标的自动纠偏(把「不要 X」误标 like → 绕过硬过滤错进向量)沿用 M7.1 结论,靠评测闭环定位,本次不做在线拦截。

附:对「披着 loop 外壳的 workflow」质疑的补强#

这次改动顺带强化了「控制流归模型」的实证:remember_preference 是一个模型在 reasoning 里自主决定何时调的非终结工具——偏好沉淀不是流水线末尾写死的一步,而是模型思考中随时可触发的自主动作,外加一层不依赖 prompt 的机制兜底。可作为「本系统的 agent 属性体现在决策点而非固定次序」的又一个佐证。