面试知识库

M7.1 · user 塔通电:only-like 偏好进个性化召回 —— 开发文档(面试向)#

把 M7 已落地的长期记忆,接进 M3 的三塔召回——让 user 塔第一次真正承重。讲为什么这么接、为什么 dislike 不能进,不讲代码。

一句话概括#

三塔召回里的 user 塔(个性化通路)此前一直空着:接口、双通道 fuse(α·query + β·user) 都在,但 user_profile 参数全库没人传过非 None,召回实质是「穿着三塔外衣的一塔 bi-encoder」。M7.1 给它通电——把已沉淀的用户偏好喂进 user 向量。但只喂 likedislike 绝不进向量:否则会触发 dense 检索最经典的「否定坍缩」,让「不要塑料」反而召回更多塑料。

1. 核心陷阱:为什么「不喜欢塑料」不能进 user 向量#

fuse 在向量空间做的是加法请求向量 = α·query + β·user。问题出在 embed("不喜欢塑料") 这个向量本身——

  • embedding 模型对**主题词「塑料」编码很强,对否定算子「不/不喜欢」**编码极弱;
  • 于是 cos(embed("不喜欢塑料"), embed("塑料")) 很高,两者在空间里几乎挨着;
  • β·user 这一项等于把请求向量往「塑料」那片区域推,COSINE 召回结果更靠近塑料商品——和用户意图正好相反

一句话:dense 向量不会做减法,你给它「不要 X」,它只听见「X」。 否定 / 排除 / 黑名单这类语义,放进向量融合里几乎必然反向召回。这和 M3.7「exact-match 是 filter 问题不是打分问题」是同一个认知的两面:精确命中和排除都不该交给「软打分 / 向量相似度」,那是 filter 的活。

2. 解法:按极性分两条路,各走各的#

用户偏好在 M7 里是结构化数据PreferenceEntrypolarity: like/dislike 枚举字段),所以「分流」不需要任何 NLP 判断,就是按枚举值切:

偏好极性走哪条路为什么
like(喜欢小众 / 木质)→ user 向量,进 fuse正向、可加语义,向量加法成立
dislike(不要塑料)→ 确定性硬过滤(早已落地)否定语义,向量会带反;filter 才能表达「排除」

dislike 那条路(dislike_exclude_terms → item_picker 硬过滤)一行没动——它在 M7 就接好了。M7.1 只补上缺失的 like 向量腿,让三塔架构第一次名副其实。

3. 三个工程决策#

① 不让模型填偏好参数,走 ContextVar 旁路。 item_search@tool,参数是大模型 tool-calling 时填的。指望模型把用户长期偏好重建成文本塞进 user_profile,既不可靠又烧 token。改成:run_agent 入口算好画像 → 写进 ContextVar → 工具内部自取。与 thread_id/session_dir 同款机制,fork 出的同质子 Agent 通过 Task 的 ContextVar 快照自动继承,不必每个子 Agent 各读一次 Store。

② 画像在入口算一次,不在工具里算。 跨平台检索会 fork N 个子 Agent,每个都调 item_search。若在工具里现算画像 = N 次 Store 读,白白拖慢热路径。入口算一次、塞 ContextVar、forks 继承,N 次读塌成 1 次。

③ β 低权重灰度起步。 即便只放 like 偏好,向量加法仍会把请求向量拽离当前 query(搜耳机时混进「喜欢木质」会污染相关性)。所以 β 默认压到 0.15(env 可调),且画像用 read_relevant(query) 只取和本次意图相关的 like 偏好——双重抑制跑题。权重该不该上调,留给真实 A/B 定,不拍脑袋。

4. 顺手补的诚实度治理(接 G2 路线)#

  • 写入侧告警dislike 偏好缺 keywords 时硬过滤要退化为「切整句」(精度下降)。落库不阻断,但记一条 warning——这是「极性误标 / 漏 keywords」的可观测点,给 M11 评测留 bad-case 靶子。
  • AGUI 状态标注item_search 上报 personalization: active/inactive,前端 / 监控不再「显得比实态大」。个性化没通电时如实说没通电。

这也点出本方案唯一的真实薄弱点:schema 保证容器规整(polarity 是枚举、不会冒出非法值),但保证不了模型把「不要塑料」分对类。一旦误标成 like,就会绕过硬过滤、错误地进向量。所以薄弱点不在「是不是结构化」,而在模型 like/dislike 分类的准确率——这是评测要专门盯的一类 case。

5. 验证#

  • 回归红线build_user_profile 只返回 like、滤掉 dislike(test_build_user_profile_only_like)——从架构上守住「否定语义不进向量」,杜绝反向召回复现。
  • 接线正确:ContextVar 有画像 → 工具走个性化通路(编码进 user 向量);ContextVar 空 → 退化纯语义、不碰 encode_user;显式传空串可覆盖上下文。
  • 新增 8 条测试(记忆层 5 + 工具层 3)。自动门全绿:ruff / format / mypy(50 文件) / pytest 168

6. 交付与边界#

交付:新增「只取 like」的画像构造 + ContextVar 旁路(入口写、工具读);item_search 接 ContextVar + 低 β fuse + AGUI 个性化标注 + 更新过时注释;写入侧 dislike 缺 keywords 告警;.envRECALL_USER_BETA。dislike 硬过滤链路零改动。

边界 / 下一步

  • β 实效未定:0.15 是灰度初值,护栏齐了但「该不该上调」要配真 embedding 跑 like-query A/B(开 / 关 user 塔对比 top_k、确认相关性没掉)才能定。本里程碑只搭好可灰度的结构。
  • 画像用入口原始 query 的相关 like:fork 子任务可能搜更窄的子 query,用原 query 相关的 like 是廉价近似,够 v1;真要更准可让子任务各自算,但要权衡那 N 次 Store 读。
  • v1 传文本、工具现编码:画像短、可后续加 lru 缓存把 encode_user 省掉,当前不做(YAGNI)。
  • 极性误标的自动纠偏留给 M11 评测闭环,本次只加告警、不做拦截。