主题
短语深度模式(Deep Phrase Mode)— §9 LLM occurrence 判断 plan
物理仓库:RB(
/Users/larry/reading-browser)。纯 RB 侧、无 RVH(content-script reader 二段 + 一个 edge function + opt-in 隐私门)。 会话边界:单开 RB 会话实施(本 plan 是 bootstrap 真相源)。 创建:2026-06-26(短语功能战略转向后定为主线)。 前置真相源:phrase-noncompositionality-tiering-crossend-plan.md§9(设计原型)+ §gate-B(flash 92.9% 实测);phrase-auto-highlight-thinslice-plan.md§gate-A 复测(28%→7%,floor 已就位);backlog.md§短语 B 🧭 战略结论。
🔁 本 plan 的 floor/ceiling 两层模型已于 2026-06-28 被
phrase-unified-llm-refactor-plan.md取代 (转为单层「AI 短语分析」:判完才上色、合并两个 judge、删 floor 静态档)。本文 §P1 落地状态 + gate-C ① + §2.1/§2.2 迭代记录保留作审计(已 ship 的 P1 是新模型的起点);后续实施走新 plan,不再按本文 floor/ceiling 推进。
✅ P1 落地状态(2026-06-26)
代码全落地 + edge function 已部署 + 实机 gate-C ① 已跑(precision 87%,下方)。实机暴露并修复 1 个 bug (judge_phrases_batch 误带 AND deleted_at IS NULL——vocabulary 共享词典无此列,修后复测无报错)。
| 件 | 状态 | 锚点 |
|---|---|---|
Edge function judge-phrases-batch(batch 二分类,复用 _shared/llm.ts + explain-phrase 骨架,flash 单次批量) | ✅ 部署到 jdtbyteiwnciqnfppztz | supabase/functions/judge-phrases-batch/index.ts |
Rust 命令 judge_phrases_batch(收 items → 从 pos_definitions 取主习语义 gloss 入 batch → anon_key 调 edge → graceful) | ✅ 注册 lib.rs,cargo check 通过 | src-tauri/src/commands/supabase.rs |
content-script deepResolvePhrasesOnPage()(context 桶救回 + per-page 内存缓存 + data-rb-phrase-deep 标记 + cap40 + withObserverPaused 应用) | ✅ index.js 二段 fire-and-forget 接线(floor 后、不阻塞) | src-tauri/src/content-script/features/phrase-highlight.js |
opt-in 开关 phrase_deep_mode(Switch + ConfirmDialog 仿 context_disambiguation;zh/en/facade strings;init_data seed false;settings.js batch key + state.phraseDeepMode) | ✅ pnpm build 通过,i18n 双守门 0 违规 | GeneralSettings.tsx 等 |
部署 smoke test(curl 占位验证,2026-06-26)——同一短语 occurrence 级判别正确:
in the black@ 财经句 →idiomatic:true✓;@ "in the black coat" →false✓(floor 做不到的按句判别)to die for@ 甜点句 →true✓
gate-C ① 实机下钻(2026-06-26,单篇文学随笔 Longreads "Living in an Alive World",opted-in)
数据来源:精度侧 = DOM 实扫 23 个高亮 span(完整);召回侧 = 临时埋点 dump 拒绝集语境(已撤回)。 单页样本(gate-A 同量级),category 分布稳健,精确计数随 cap 抽样/版本略有浮动,故部分标「约」。
总结论:引擎(occurrence 二分类)本身好——precision 87% / recall ~85%、同短语跨句能分对。拖后腿的 不是 LLM,而是它上游(matcher 过度匹配)和喂进去的料(tier 质量 + gloss/prompt 边角)。下一步重心 = 「降噪 + 喂准」,非换模型。
第 1 层 · 漏斗:context 候选 65 → cap40 丢 25 →(判 40)→ TRUE 23 高亮 / FALSE 17 拒。
- cap 丢 25(38%)= 隐藏 recall 漏(这 25 个从未被判,里面可能有真习语);根因 = 候选被 matcher 噪声撑爆。
第 2 层 · 高亮的 23 个(precision = 20/23 = 87%):
| 细类 | 分布 | 归因 | 下一步 |
|---|---|---|---|
| 2A 高价值真习语 ✅ | 14(61%)look up/come up/turn out/to do with×3/make up/win out/speak to/hang out/point out/set out×2/work on | 命中靶心,floor 此页露 0 | 无需动(价值证明) |
| 2B 透明低价值 PV ⚠️ | 6(26%)slow down/speed up/get up/pass by/cut out/not so much | 非 LLM 错:PV 义对、但太透明=噪声。根因 = RVH tier 把高组合 PV 错放进 context | RVH demote context→literal(行动 3) |
| 2C 硬 FP ❌ | 3(13%)go to×2(go to a window)/push on(pushing on env) | LLM 真误判,失败模式集中=位移/物理 PV 的字面位置用法 | prompt 外科规则 + 精准 gloss(行动 2) |
第 3 层 · 拒掉的 17 个(recall ~85%,最大发现在此):
| 细类 | 分布 | 归因 | 下一步 |
|---|---|---|---|
| 3A matcher 过度匹配,正确清掉 ✅ | 约 10:work on(work=名词)/key in(key=形容词)/in order(to)/mean to(means to)/listen in/good sense/time in/other side/call it/have get | matcher 按 lemma 串相等匹配、不分 POS/句法 → 把"名词 work+on"当 PV。LLM 替它兜底 | RB 预过滤降噪(行动 1,治本、连带救 cap 漏) |
| 3B 真 PV 字面用法,正确拒绝 ✅ | 约 4:look down/pull away/look out/be there + 教科书级 for the bird→"for the birds[真鸟]" | deep mode 本职——同短语字面 occurrence 判掉 | 无需动(引擎正确性正面证据) |
| 3C 漏报 FN ❌ | 2 清晰(on track=回正轨 / catch up="caught up in"=入迷)+ 3 边缘(let in/cut off/come to) | LLM 偏保守 + 隐喻隐晦 | 暂不单治;反向约束 2C 修法不可推它更爱拒 |
第 4 层 · 横切发现:
- 4A occurrence 判别生效 ✅:同短语跨句判出不同(
slow downtime-lapse=TRUE / "systems slow down"=FALSE;for the birds字面=FALSE)——type 级静态标签构造上做不到,核心假设证实,premium 立论基础。 - 4B cap 静默丢 25 = 隐藏 recall 漏 🔴:根因同 3A(噪声撑爆候选);解 = 行动 1 降噪后候选回落到 cap 内,漏自动消失。RB 侧最高优先。
- 4C always 桶 click-confirm 挂在另一开关:单击二次确认(
explain_phrase能撤误高亮)门控context_disambiguation,与phrase_deep_mode两个独立开关 → 只开深度模式的用户点 always 误高亮不被纠正。P3 评估合并。
汇总行动表(按 ROI 排序):
| # | 行动 | 治哪类 | 归属 | 优先级 |
|---|---|---|---|---|
| 1 | matcher 送 LLM 前预过滤(跳 basic 桶 / 功能词组前置规则) | 3A+4B(降噪+救 cap 漏+省钱) | RB | P2 最高 |
| 2 | prompt 外科规则「字面物理移动/按压→false」+ 精准 idiom gloss | 2C 三个硬 FP | edge+RB | P2 |
| 3 | 透明基础 PV demote context→literal | 2B 六个 | RVH 新会话 | P2 |
| 4 | 两个 AI opt-in 合并评估 | 4C UX 缝隙 | RB | P3 |
| 5 | 多体裁复测(新闻/技术,验证是否文学体裁特例) | 全局泛化 | RB | P2(1/2 后) |
视觉:deep 高亮(蓝波浪线 rb-phrase-pv/习语样式)与 vocab/CEFR 橙点线、floor data-rb-phrase-auto 共存可区分。 已知边界(P1):deepResolve 在 vocab 高亮之后跑,被生词高亮切碎的 context 短语单节点匹配不到(召回损失,与 always 路径同源);P2 可前移捕获到 vocab 前。
行动 2 落地(2026-06-26 prompt 外科修法):见下方「§2.1 prompt 迭代记录」。
已知边界(P1,见 §4 注 + §11):deepResolve 在 vocab 高亮之后跑,被生词高亮切碎的 context 短语单节点匹配不到(召回损失,与 always 路径"必须在 vocab 前"同源);P2 可前移捕获到 vocab 前。
§2.1 prompt 迭代记录(行动 2,2026-06-26)
v1 → v2(治 2C 三个硬 FP:位移/物理 PV 字面位置用法):judge-phrases-batch SYSTEM_PROMPT 加两条—— ① 「LITERAL PHYSICAL/SPATIAL/MOTION use → false」+ 实例(go to a window / push on a surface / look down at / pull away); ② 「GUARDRAIL」反向护栏(防过度拒绝伤 recall)+ 实例(back on track / caught up in / let in → 仍 true)。 护栏实例直接取自 gate-C 实测的 FN,让同一条 prompt 同时修 FP 和保 recall。
验证(两层):
- curl 回归(8 例 curated,新 prompt 部署后):8/8 全对——go to/push on/look down → false ✓;look up/set out/win out → true ✓;on track/catch up(原 FN)→ true ✓(护栏不仅没伤 recall,还把 2 个 FN 救回)。
- 同页实机复测(content-1 reload,真实 gloss 走 Rust):
go to×2 +push ondeep 高亮 归零 ✓;look up/set out×2/win outTP 保留 ✓;borderline FNlet in("let in the tips…into awareness")被救回 ✓。总数仍 23(FP 出、真习语进,volume 不变质量升)。
结论:prompt 外科修法成立——同页 precision 13% 硬 FP 清零、且顺带救回 ≥2 个 FN。待多体裁复测(行动 5)验证泛化。
§2.2 行动 1 落地(2026-06-26 candidate 分批不丢,治 4B 隐藏 recall 漏)
先证伪:原设想「送 LLM 前按 basic 桶/tag 预过滤降噪」被数据否决(rb_db_query)——is_basic 不区分伪/真匹配: 8/10 伪匹配非 basic(滤不掉)、5 个真高价值(look up/come up/make up/hang out/point out)是 basic(会误杀), work on 真+伪同 PK。伪/真纯 occurrence 级,无 phrase 级干净信号,只能 LLM(或没有的 POS)判 → 伪匹配交 LLM 兜底即可(precision/recall 不受影响),不预过滤。
真正改动:deepResolvePhrasesOnPage 候选超批不丢、分批并行判——DEEP_PHRASE_CAP=40 静默丢 → DEEP_PHRASE_BATCH=40(每批 ≤ edge MAX_ITEMS=60)滚入下一批 Promise.all 并行调,单页硬顶 DEEP_PHRASE_MAX=120(3 批)才丢 + log。4B 隐藏 recall 漏闭合(密页 65 候选全判、dropped→0),成本仍可忽略(密页 2-3 次 flash ≈ 0.1-0.2¢)。任一批失败 graceful(该批不补,其余照常)。
验证:build 通过 + edge gold 27/27;实机 judged 40→~65 / dropped→0 待下次 /phrase-eval 复测(落地时 dev app 卡在启动网络 fetch,未当场核;逻辑经 inspection 确认)。剩 2B 透明 PV(行动 3,RVH)+ 多体裁复测(行动 5)未动。
0. 为什么做(战略定位)
静态 idiomaticity 三档收敛不到 0——残留 FP(all along/in a fix/for the bird)全是 occurrence 级问题 (同一短语在不同句有时习语、有时字面),用 type 级标签(与上下文无关)解,构造上漏长尾。继续 retag = 边际递减。
正确架构 = floor + ceiling 两层(综合评估 2026-06-26 敲定):
| 层 | 是什么 | 状态 |
|---|---|---|
| floor(免费/默认) | 本地 always 桶:即时、离线、零外发、~93% 干净(gate-A 7% FP) | ✅ 已就位 |
| ceiling(premium/opt-in) | 本 plan = §9 LLM occurrence 判断:救回 context 桶(recall 2789→6000+)+ 洗掉 always 残留 FP(→近 0) | 🚀 本 plan |
ceiling 解决 floor 解不了的两件事:
- recall 大增:floor 只露 2789 个 always;context 桶 3450 条短语的习语用法全漏。deep mode 在它们真的是习语义时救回(gate-B 实测 0 漏报)。
- precision 到顶:opted-in 时 LLM 复判 always 桶,把
all along这类 occurrence 陷阱按句降掉 → FP 近 0。
约束是隐私不是成本:flash ≈ 0.056¢/篇(重度 50 篇/天 ≈ $0.84/月)可忽略、可定价进 premium;opt-in 的真正理由 = 整篇候选正文片段外发第三方,需显式知情同意(沿 disambiguate-sense 同口径)。
1. 架构总览(两段式,不发整页)
进入 reader 模式(或 live 页)+ phrase_deep_mode = on
↓
【第一段·本地·即时】highlightPhrasesOnPage() ← 现有,floor,不变
· always 桶本地秒出(绿/彩高亮)
↓
【第二段·LLM·异步补出】deepResolvePhrasesOnPage() ← 新增
1. 本地 matcher 返回 context 桶候选(+opted-in 可含 always 复判集),带 char 偏移 + 所在句
2. 去重 + 组 batch(含候选的句子,不发整页)→ 一次 edge function 调用
3. flash 逐条判「习语义 vs 字面/组合义」
4. fit=true → 高亮(context 桶新增);always 复判 fit=false → 移除该 always 高亮(FP 清理)
5. 结果种入 per-page 内存缓存(word+sentence 键),翻页/重扫免重调关键纪律:
- always 桶永远本地先到(deep mode 只做"补"和"减",不阻塞首屏高亮)。
- 只发含候选短语的句子(matcher 已知偏移 → 切句),不发整页正文(token + 隐私双控)。
- 失败/超时/null → graceful:context 桶就不补、always 桶就不减(floor 体验不退化)。
2. Edge Function(batch occurrence judge)
复用 explain-phrase 骨架(已存在,FITS/COMPOSITIONAL 选择式 + _shared/llm.ts 计量/限额/llm_call_log)。 现 explain-phrase 是单条(一次一个 phrase+sentence+candidates,on-click 用)。deep mode 要批量才能命中 §9 的 0.056¢/篇(~3000 in / ~500 out,一页一次调用)。两选一:
- 方案 A(推荐)新建
judge-phrases-batch:收{ items: [{ id, phrase, sentence, gloss }] }→ 返回{ verdicts: [{ id, idiomatic: bool }] }。精简 prompt(二分类,不返回 sense_index)——deep-mode 高亮只需 true/false; 弹窗的 sense 仍走现有 on-clickexplain_phrase(已工作,不动)。token 最省。 - 方案 B:给
explain-phrase加 batch 模式。复用度高但 prompt 携 candidates 更重(token 高)。
选型建议 = A:高亮决策是二分类,不需要每条带 candidates 全 glosses;轻 prompt 把每页压到 ~3000 token。 复用 explain-phrase 的 system prompt 中「COMPOSITIONAL 判定」那段语义,去掉 sense 选择部分。
prompt 要点(gate-B 教训):
- 给每条候选的 per-sense 精准 gloss(不是泛化义)——gate-B 的 2 个误报全是
work in配泛化 gloss "to insert or include something" 被多义干扰。matcher 取该短语主习语义 gloss(pos_definitions第一条 idiom 义)入 batch。 - 低置信可选择性升 pro 二判(gate-B 实测 pro 同错 → 先改 gloss 更值,pro 兜底 P2)。
secret:复用全局 LLM_PROVIDER/LLM_API_KEY/LLM_MODEL(与 explain-phrase/disambiguate-sense 同)。 model:deepseek-v4-flash(gate-B 坐实,pro 无增益)。部署走 /edge-deploy(project-ref jdtbyteiwnciqnfppztz)。
3. Rust 命令
- 新
judge_phrases_batch(commands/短语相关模块):收候选 items → anon_key 调 edge function → gracefulCommandResult<Vec<Verdict>>(错误返回空/null,不抛)。镜像现有explain_phrase的调用范式。 - matcher 复用:
match_phrases_in_node_texts(query.rs)已返回idiomaticity+ 偏移 + phrase。第二段从 perNode 里筛idiomaticity === 'context'(及 opted-in 时'always')作候选,无需新 SQL。 - 切句:content-script 已有
getSentenceAround/sentenceContaining(cloze/popup 复用),deep mode 第二段同款取候选所在句。
4. Content-script(reader 二段路径)
新增 deepResolvePhrasesOnPage()(phrase-highlight.js),在 highlightPhrasesOnPage() 之后异步触发:
- 复用第一段已 match 的 perNode 结果(避免二次
match_phrases_in_node_texts);从中取 context 桶候选(m.idiomaticity === 'context')+ 各自所在句。 - opted-in always 复判(可选 Phase 2):把已高亮的 always span(
[data-rb-phrase-auto])连句一起加入 batch。 - 去重(同 word+sentence 只判一次)+ 查 per-page 内存缓存(命中跳过)。
invoke('judge_phrases_batch', { items })→ verdicts。- 应用:
- context fit=true →
wrapPhrasesInTextNode(新增高亮,标data-rb-phrase-deep="1"区分来源 + 便于埋点/回收)。 - always fit=false → 移除该 span(unwrap,FP 清理)。
- context fit=true →
- 缓存 verdicts(word+sentence 键,仅当页、仅正向 + 负向都存,规避瞬时故障钉死)。
- 视觉:判定期可给一个克制的"• AI"脉冲(沿 disambiguation 既有提示),结果平滑补出/淡出。
作用域:findMainContentRoot()(已落地,提取为共享 helper 复用)。reader 模式优先(overlay-scoped、最干净);live 页 Phase 2。 选区路径 highlightPhrasesInSelection 不变(仍露全部,保留手动捞回)。
5. opt-in 隐私门(沿 context_disambiguation 同款)
- 新 setting key
phrase_deep_mode,init_data.sqlseed'false'(默认关)。 GeneralSettings.tsx:Switch + 开启前 ConfirmDialog(文案:"深度短语模式会把你正在读的、含候选短语的句子发送给第三方 AI 服务做习语判断,以更准地识别习语用法。是否开启?")。settings.tsgetter/setter;zh/en locale(走src/lib/strings/,禁 JSX 中文字面量,/arch-checkS19)。- content-script 第二段先查开关(invoke 读
phrase_deep_mode,关 → 直接 return,floor 照常)。 - 与
context_disambiguation关系:两者都是"正文片段外发"opt-in。可独立;亦可评估合并成一个"AI 语境增强"总开关(实施时定,倾向独立 = 语义清晰 + 各自可灰度)。
6. 成本 / 延迟 / 鲁棒护栏
- 成本:
_shared/llm.ts已有llm_call_log+ 预算闸;deep mode 调用计入。每页一次批量调用 ≈ 0.056¢。 - 页内候选上限:cap(如 >40 候选句则截断 +
log丢弃数,不静默——沿"no silent caps"纪律),防超长页 token 爆。 - 延迟:flash ~1-3s(高峰波动)。always 桶已先到,deep 异步补——不卡首屏。给加载脉冲。
- 缓存:per-page 内存(word+sentence),翻页/重扫免重调。不持久化(§9 + 消歧 follow-up 同结论:隐私含用户句 + 须按
VOCABULARY_SEED_VERSION失效,投入产出不成立)。 - graceful:任何错误/超时/null → context 不补、always 不减,floor 不退化。
7. Premium / 定价(架构留口,v1 不做 paywall)
- deep mode = 天然的 premium 锚点(成本 + 隐私同意都绑定到"愿意为质量付费 + 授权"的用户)。
- v1 = opt-in 免费(验证期,收集真实使用 + gate-C 准确率);paywall 基建(订阅/额度)独立后续,不阻塞本 plan。
- 免费用户始终有 floor(本地 always 桶,即时/私密/离线)——分层不剥夺基础体验。
8. 验证(gate-C)
复用 gate-A 同法(Claude 读 data-rb-sentence 逐句判 + rb-debug 驱动),但分两个口径分开验(契约 §8 验证分层纪律,不混算):
- context 桶 precision/recall(deep mode 新增高亮):opted-in 开 deep mode,文学/叙事体裁取样——
- precision:新增的
data-rb-phrase-deep高亮里,真习语义占比(目标 ≥90%,对齐 gate-B)。 - recall:抽查正文里 context 桶短语的真习语用法,deep mode 有没有救回(对比 floor 漏的)。
- precision:新增的
- always 桶 FP 清理(Phase 2):
all along/for the most part等 floor 残留,opted-in 后是否按句被移除。 - 延迟/鲁棒:always 先到、deep 后补的体感;断网/超时 floor 不退化。
判据:context precision ≥90% + recall 明显高于 floor + always FP 清理生效 → deep mode 成立,评估 premium 化。
9. 分阶段
| Phase | 范围 | 阻塞关系 |
|---|---|---|
| P1 | opt-in 开关 + judge-phrases-batch edge function + Rust 命令 + reader 二段 context 桶救回 + 内存缓存 + gate-C ①验证 | 核心,先做 |
| P2 | always 桶复判 FP 清理 + per-sense 精准 gloss 优化(治 work in 类)+ live 页路径 | P1 后 |
| P3 | premium/paywall 基建(订阅/额度)+ 与 context_disambiguation 开关合并评估 | 独立、不阻塞体验 |
并行非阻塞(floor 侧,RVH):1 次廉价 demote-only(all along + 同模式 homonym 长尾 + §11.4 话语子集 no matter what/pay attention/for the most part 降 literal)把 floor FP 推向 ~0,之后冻结静态治理(见 backlog 🧭 战略结论)。RVH 新会话,与本 plan 无依赖。
10. 文件锚点
| 文件 | 改动 |
|---|---|
supabase/functions/judge-phrases-batch/index.ts | 新建(复用 explain-phrase 骨架 + _shared/llm.ts,batch 二分类) |
supabase/functions/explain-phrase/index.ts | 参考骨架(不必改;on-click 弹窗仍用它) |
src-tauri/src/commands/.../ | 新 judge_phrases_batch 命令(镜像 explain_phrase) |
src-tauri/src/commands/vocabulary/query.rs | matcher 已返回 idiomaticity + 偏移,无需改(第二段筛 context) |
src-tauri/src/content-script/features/phrase-highlight.js | 新增 deepResolvePhrasesOnPage();复用 findMainContentRoot()/getSentenceAround;data-rb-phrase-deep 标记 |
src/components/.../GeneralSettings.tsx | 加 phrase_deep_mode Switch + ConfirmDialog(仿 context_disambiguation) |
src-tauri/.../init_data.sql | seed phrase_deep_mode='false' |
src/lib/strings/ | zh/en 文案(开关标题/隐私确认) |
11. 不做 / 边界
- 不发整页正文——只发含候选短语的句子。
- 不做持久缓存(隐私 + 版本失效,§6)。
- 不动 floor(always 桶本地路径)——deep 只做"补 context + 减 always FP",不替换首屏。
- 不动选区路径(手动捞回保留)。
- literal 桶不送 LLM(§2 路由:跑了也是浪费)——三档分类在此兑现"省钱省延迟路由器"价值。
- v1 不做 paywall(opt-in 免费验证先行)。