习得贴纸:贴纸识别流水线 + search_stickers 检索 #75

Merged
Yushu merged 2 commits from feature/70-learned-stickers into main 2026-07-27 02:31:00 +00:00
Member

Closes #70

What

机器人记住群里常用贴纸的交际含义(不是画面描述),供它之后自己发贴纸时用得上。识别流水线独立于两级门控异步跑,per-chat 默认关(opt-in)。ADR-0024 落地。

实现要点

  • 契约层MediaKind 新增 "sticker"MediaRef 新增 file_id(平台稳定标识,既是精确去重键也是模型之后要发出去的引用)。传输层与图片同形,复用同一内容块构造器。
  • fire-and-forget_spawn_sticker_learning 同步做完零成本过滤,只在真有候选时起后台任务;刻意不挂进 _pending_flushes(那些会被下一条消息 cancel,而识别一旦开始就该跑完)。
  • 三道闸都在花钱之前capabilities.sticker → per-chat 开关 → 去重查询,任一不过零 VLM 调用。
  • 写入治理复用 Knowledge 形状:per-chat_id 作用域、sensitivity fail-closed、落库前过人设漂移守护否决闸(ADR-0021 管辖范围扩至本类目)。
  • 检索search_stickers 向量语义检索(新 Qdrant collection),不整份常驻注入(ADR-0024 规避不可预测的库增长)。工具按 capabilities.sticker 门控,计入 run_limit。

两处偏离 AC 字面的决定(已 grill 确认)

  1. 识别用结构化工具调用,而非 AC2 字面的「无工具描述请求」:AC2 与 AC5 不能同时字面满足——fail-closed 打标需要模型声明的结构化字段(仓里 delta._resolve_sensitivity 就是这条规则)。保住了「无工具」那句真正在争的东西:新鲜上下文、旁路调用、不与主推理链共享、单次调用(Spec 轴已逐项复核成立)。
  2. 判敏感即不入库,而非 AC5 字面的「打标」:检索本就是 per-chat 作用域,存下来再标记 sensitive=True 没有任何行为差别、会让这个字段成为摆设;不入库才真正兑现「不要复用别人的私人照片」。

Review

/code-review 两轴都抓到真问题:

  • Spec 轴发现本片引入了一个真实回归MediaKind 新增 sticker 后,既有部署(arise_llm_multimodal_kinds 只声明 "image")一旦 host 开始上报 kind="sticker"正常对话里的贴纸会退化成 [贴纸] 占位。已修:新增 _TRANSPORT_ALIAS 让声明 image 的模型同样覆盖 sticker(两者线格式本来完全一致),既消回归也免了部署方为纯语义区分再写一遍配置。别名刻意不对称。
  • Spec 轴变异验证抓出两条空转测试:① 「无贴纸不起任务」断言的是零 VLM 调用,而这在任何变异下都成立(reviewer 删掉过滤后 e2e 仍全绿)——改为断言 _pending_sticker_learning 集合并补反向对照;② 「沉默轮识别照常」压根没跑门控,只是不调用 run()——改为真跑一轮以沉默收场的对话,同时断言 egress 零发送与贴纸已落库。
  • Spec 轴指出识别本身未受 capabilities.sticker 门控(此前只门控工具可见性),发不出贴纸的平台照付识别成本。已补。
  • Standards 轴两处重复:两个检索工具的 query 守卫逐字节相同 → _sanitized_query_arg_learn_stickers 手工重建的兜底多模态路由与 _build_runtime_loop 重复 → 提取 _build_fallback_multimodal_models(避免「声明顺序即优先级」有两个实现)。
  • 补文档:并发去重的残余竞态(同 #56 先例如实记录不加锁)、被拒贴纸不留痕迹因而重复付费的刻意取舍、EgressPort.send_stickersticker_ref 双来源契约(此前从未声明,host 照静态目录解析会发不出学到的贴纸)。

Standards 轴判定为不改的两条(记录理由):learn_sticker 七个参数与 run_delta_compression_cycle/run_reflection_cycle 同形、全是依赖零调参,不是 Data Clump;render_sticker_hitsrender_search_hits 键集语义都不同,合并需引入字段映射抽象反而更糟。

已知范围边界

per-chat 开关的 setter 已就位但尚无生产开启途径——/settool 那类管理命令属 ADR-0010 运营面,PRD 已明确排除在本片之外。运营面接上即可驱动。

Tests

全仓库 1027 个测试通过(新增 50 个:纯函数 16、识别流水线 11、search_stickers 工具 8、e2e 12、传输层别名 3)。

变异复核四处守卫,逐个都有测试挂:贴纸过滤、per-chat 开关闸、能力位闸、传输层别名。

Closes #70 ## What 机器人记住群里常用贴纸的**交际含义**(不是画面描述),供它之后自己发贴纸时用得上。识别流水线独立于两级门控异步跑,per-chat 默认关(opt-in)。ADR-0024 落地。 ## 实现要点 - **契约层**:`MediaKind` 新增 `"sticker"`;`MediaRef` 新增 `file_id`(平台稳定标识,既是精确去重键也是模型之后要发出去的引用)。传输层与图片同形,复用同一内容块构造器。 - **fire-and-forget**:`_spawn_sticker_learning` 同步做完零成本过滤,只在真有候选时起后台任务;刻意不挂进 `_pending_flushes`(那些会被下一条消息 cancel,而识别一旦开始就该跑完)。 - **三道闸都在花钱之前**:`capabilities.sticker` → per-chat 开关 → 去重查询,任一不过零 VLM 调用。 - **写入治理复用 Knowledge 形状**:per-chat_id 作用域、sensitivity fail-closed、落库前过人设漂移守护否决闸(ADR-0021 管辖范围扩至本类目)。 - **检索**:`search_stickers` 向量语义检索(新 Qdrant collection),不整份常驻注入(ADR-0024 规避不可预测的库增长)。工具按 `capabilities.sticker` 门控,计入 run_limit。 ## 两处偏离 AC 字面的决定(已 grill 确认) 1. **识别用结构化工具调用,而非 AC2 字面的「无工具描述请求」**:AC2 与 AC5 不能同时字面满足——fail-closed 打标需要模型声明的结构化字段(仓里 `delta._resolve_sensitivity` 就是这条规则)。保住了「无工具」那句真正在争的东西:新鲜上下文、旁路调用、不与主推理链共享、单次调用(Spec 轴已逐项复核成立)。 2. **判敏感即不入库,而非 AC5 字面的「打标」**:检索本就是 per-chat 作用域,存下来再标记 `sensitive=True` 没有任何行为差别、会让这个字段成为摆设;不入库才真正兑现「不要复用别人的私人照片」。 ## Review `/code-review` 两轴都抓到真问题: - **Spec 轴发现本片引入了一个真实回归**:`MediaKind` 新增 sticker 后,既有部署(`arise_llm_multimodal_kinds` 只声明 `"image"`)一旦 host 开始上报 `kind="sticker"`,**正常对话里的贴纸会退化成 `[贴纸]` 占位**。已修:新增 `_TRANSPORT_ALIAS` 让声明 image 的模型同样覆盖 sticker(两者线格式本来完全一致),既消回归也免了部署方为纯语义区分再写一遍配置。别名刻意不对称。 - **Spec 轴变异验证抓出两条空转测试**:① 「无贴纸不起任务」断言的是零 VLM 调用,而这在任何变异下都成立(reviewer 删掉过滤后 e2e 仍全绿)——改为断言 `_pending_sticker_learning` 集合并补反向对照;② 「沉默轮识别照常」压根没跑门控,只是不调用 `run()`——改为真跑一轮以沉默收场的对话,同时断言 egress 零发送与贴纸已落库。 - **Spec 轴指出识别本身未受 `capabilities.sticker` 门控**(此前只门控工具可见性),发不出贴纸的平台照付识别成本。已补。 - **Standards 轴两处重复**:两个检索工具的 query 守卫逐字节相同 → `_sanitized_query_arg`;`_learn_stickers` 手工重建的兜底多模态路由与 `_build_runtime_loop` 重复 → 提取 `_build_fallback_multimodal_models`(避免「声明顺序即优先级」有两个实现)。 - 补文档:并发去重的残余竞态(同 #56 先例如实记录不加锁)、被拒贴纸不留痕迹因而重复付费的刻意取舍、`EgressPort.send_sticker` 的 `sticker_ref` 双来源契约(此前从未声明,host 照静态目录解析会发不出学到的贴纸)。 **Standards 轴判定为不改的两条**(记录理由):`learn_sticker` 七个参数与 `run_delta_compression_cycle`/`run_reflection_cycle` 同形、全是依赖零调参,不是 Data Clump;`render_sticker_hits` 与 `render_search_hits` 键集语义都不同,合并需引入字段映射抽象反而更糟。 ## 已知范围边界 per-chat 开关的 setter 已就位但**尚无生产开启途径**——`/settool` 那类管理命令属 ADR-0010 运营面,PRD 已明确排除在本片之外。运营面接上即可驱动。 ## Tests 全仓库 1027 个测试通过(新增 50 个:纯函数 16、识别流水线 11、`search_stickers` 工具 8、e2e 12、传输层别名 3)。 变异复核四处守卫,逐个都有测试挂:贴纸过滤、per-chat 开关闸、能力位闸、传输层别名。
机器人记住群里常用贴纸的**交际含义**(不是画面描述),供它之后自己发贴纸时
用得上。ADR-0024 落地。

契约层:`MediaKind` 新增 `"sticker"`(与 `"image"` 分开的唯一理由是只有真贴纸
进流水线,用户随手发的自拍/截图不触发);`MediaRef` 新增 `file_id` 作为平台
稳定标识——既是精确去重键也是模型之后要发出去的引用。传输层形状与图片一致,
复用同一个内容块构造器。

识别流水线(fire-and-forget):`_spawn_sticker_learning` 同步做完零成本过滤
(有没有值得学的贴纸),只在真有候选时才起后台任务;刻意不挂进
`_pending_flushes`(那些会被下一条消息 cancel,而识别一旦开始就该跑完)。
per-chat 开关是协程里的第一道闸,未开启零 VLM 调用。识别用的模型按
`route_media_kind` 现算,主模型不支持 sticker 就走兜底多模态模型。

识别调用形状:一次**结构化工具调用**同时拿 `meaning`+`sensitive`,而非 AC
字面写的"无工具描述请求"——两者不能同时字面满足(fail-closed 打标需要模型
声明的结构化字段,仓里 `delta._resolve_sensitivity` 就是这条规则)。保住了
"无工具"那句话真正在争的东西:新鲜上下文、旁路调用、不与主推理链共享;也只
花一次调用。

sensitive 判真即**不入库**(不是入库但不检索):把某人的私人照片留在库里等着
以后被检索到,与"采集他人内容默认保守"的立意相反;这也让这个字段对 per-chat
作用域的贴纸库有了真实作用,而不是照搬 Knowledge 跳语境语义后变成摆设。

写入治理复用 Knowledge 形状:per-chat_id 作用域、fail-closed 打标、落库前过
人设漂移守护否决闸(ADR-0021 管辖范围扩至本类目)。

检索走 `search_stickers` 向量语义检索(新 Qdrant collection),不整份常驻注入
(ADR-0024 规避不可预测的库增长)。选向量而非关键词:贴纸含义本质是自由文本,
模型查"尴尬"要能命中"一只猫捂住脸表示尴尬",关键词集合难穷举。工具按
`capabilities.sticker` 门控(能力为假=模型看不到,同 poke_send 先例),计入
run_limit。

已变异复核两处守卫:去掉 per-chat 开关闸挂 3 条测试,去掉去重挂 1 条。
**最重要:修掉本片引入的真实回归(Spec 轴指出)**。`MediaKind` 新增 sticker
后,既有部署(`arise_llm_multimodal_kinds` 只声明 `"image"`)一旦 host 开始
上报 `kind="sticker"`,正常对话里的贴纸会突然退化成 `[贴纸]` 占位——而贴纸
的传输层与图片完全同形(两者共用 `_image_block`),分开纯粹是语义需要。新增
`multimodal._TRANSPORT_ALIAS` 让声明 image 的模型同样覆盖 sticker:既消掉这个
回归,也免了部署方为一个纯语义区分再写一遍配置才能启用识别。别名刻意不对称
(只声明 sticker 不能推断出支持普通图片,那是另一个能力主张)。

**两条空转测试(Spec 轴变异验证抓到)**:
① `test_a_message_with_no_sticker_spawns_no_task_at_all` 断言"零 VLM 调用",
而这在任何变异下都成立(协程即便起了也会在开关/路由那步早退)——reviewer 删掉
过滤后 e2e 仍 10/10 全绿。改为断言 `_pending_sticker_learning` 集合(唯一能
区分"压根没起"与"起了但没做事"的可观察量),并补一条反向对照防"这个集合永远
是空的"式假通过。
② `test_recognition_survives_a_turn_that_stays_silent` 压根没跑门控,只是"不
调用 RuntimeLoop.run()",等价于隔壁那条非阻塞测试。改为真的跑一轮以沉默收场
的对话,同时断言 egress 零发送与贴纸已落库。

**新增 `capabilities.sticker` 门控识别本身**(Spec 轴指出):发不出贴纸的平台
学了也用不上,此前只门控了工具可见性、识别成本照付。

**Standards 轴两处重复**:`search_memory`/`search_stickers` 的 query 守卫逐字节
相同,收敛成 `_sanitized_query_arg`;`_learn_stickers` 手工重建的兜底多模态
路由与 `_build_runtime_loop` 重复,提取 `_build_fallback_multimodal_models`
两处共用(避免"声明顺序即优先级"这条契约有两个实现)。

补文档:`learn_sticker` 的并发去重残余竞态(同 #56 先例如实记录不加锁)+ 被拒
贴纸不留痕迹因而重复付费的刻意取舍;`EgressPort.send_sticker` 的 `sticker_ref`
双来源契约(静态目录 vs 入站 file_id),此前从未声明,host 照静态目录解析会
发不出学到的贴纸。

四处守卫均已变异复核:过滤/开关闸/能力位闸/传输层别名,逐个变异都有测试挂。
Yushu merged commit 6d90c9234c into main 2026-07-27 02:31:00 +00:00
Yushu deleted branch feature/70-learned-stickers 2026-07-27 02:31:01 +00:00
Sign in to join this conversation.
No description provided.