Skip to content

短语识别统一 LLM 重构 plan(floor/ceiling 两层 → 单层「AI 短语分析」)

物理仓库:RB(/Users/larry/reading-browser)。纯 RB 侧、无 RVH(content-script + 一个 edge function + settings + Rust 命令)。 会话边界:单开 RB 会话实施。本 plan 是 bootstrap 真相源。 创建:2026-06-28。 取代phrase-deep-mode-plan.md 的 floor/ceiling 两层模型(该 plan 的 P1 已落地记录保留作审计;本 plan 是其演进的重写)。 前置真相源:phrase-eval-log.md(全 9 轮台账 + 本次 (10) 决策);phrase-deep-mode-plan.md §P1(已落地件 + gate-C ①);backlog.md §「🔭 最后一公里」(本 plan 的讨论起点)。 彻底重构纪律(用户拍板):不考虑兼容、不留历史包袱,朝最高准确度重写;删干净旧路径。


0. 这次重构是什么 + 为什么

模型转变:两层 → 单层

旧模型(phrase-deep-mode-plan.md)= floor(本地 always 桶静态高亮,默认常驻)+ ceiling(opt-in LLM 复判 context 桶)。 经 9 轮多体裁评测 + 本次讨论,敲定静态层不该作为一个能用的短语功能端出去——短语非组合性本质是 occurrence 级问题,type 级静态标签构造上收敛不到可信(gate-A 残留 FP 全是同短语不同句的 occurrence 陷阱)。

新模型 = 单层「AI 短语分析」

  • 一个 opt-in 开关。 = 全量 LLM(judge-before-paint,always ∪ context 候选都送 LLM 判,判完只画确认的); = 不画任何短语高亮。
  • 没有「次一点的静态档」可选——要么用最好的(LLM),要么不用(用户拍板:短语学习比单词更依赖 LLM,不提供半档)。
  • 预装短语三档库不浪费,角色降级:从「显示层 floor」退成候选生成 + 路由层——matcher 仍用它框候选,literal 桶仍不送 LLM(省钱省延迟),always/context 合并成「送 LLM 判」。前几轮 retag/tiering 投资转化成路由价值。

为什么:信任 = 一致 + 诚实(不是追求完美)

旧设计的信任 bug:高亮走 judge-phrases-batch(lean 二分类),点击弹窗走 explain_phrase(richer,会判 COMPOSITIONAL)——两个不同的 judge 各判一次 → 点击时第二个 judge 可能推翻第一个 → dismissPhraseHighlight 当着用户面撤掉高亮("打脸")。always 桶静态没过 LLM,点击也会被 explain_phrase 撤。这教会用户「标注不可信」。

修法两条腿(缺一不可):

  • 一致:judge-before-paint(判完才上色,拒绝 = 从来没画过,不可见)+ 弹窗读缓存(不重判)→ 构造上不可能自相矛盾
  • 诚实:定位/文案明示「AI 驱动、概率性、极准但非绝对」→ 残留误差(释义未覆盖 / 模型 ~7% ceiling)落在用户预期内,是「AI 就这样」而非「app 骗我」。

两条合起来 = 可信,且不需要追求完美 → 不必为完美上全局 batch=1batch~10 + 诚实框架已够)。


✅ 实施状态(2026-06-28)

P0–P4 代码全落地,cargo check ✓ / pnpm build(前端+content-script)✓ / i18n 三件套(zh/en/locale)✓ / arch-check ✓。

  • P1 Rust+edgejudge-phrases-batch edge 重写为 richer(返回 idiomatic+pos+sense_index,服务端校验接地);Rust judge_phrases_batch 送按 pos 分组候选 + PhraseVerdict 加 pos/sense_index;删 explain_phrase 命令 + PhraseResolution + explain-phrase edge 目录 + lib.rs 注册。
  • P2 content-scriptphrase-highlight.js 合并为 analyzePhrasesOnPage()(策略 3,judge-before-paint,always∪context);wrapPhrasesInTextNode 单标记 + 存 data-rb-pos/data-rb-senseindex.js phrase-first await;popup.js 点击读 span attr 高亮(applyPhraseSenseFromSpan),删 maybeResolvePhrase/dismissPhraseHighlight/showPhraseExplanation/phraseResolveCachestate.js deepCache 值升级。
  • P3 settings 合并phrase_auto_highlight+phrase_deep_mode → 单键 phrase_analysis(seed false);GeneralSettings 并开关 + 删 4C implication + 诚实文案;maybeDisambiguate gate 解耦为仅 context_disambiguation;init_data/settings.js/useContentTools/strings×3 全改。
  • P4 收尾:选区按钮(已是死代码)highlightPhrasesInSelection/collectRangeTextNodes/clipNodeToRange + i18n toolbar.phrases* 删除;showPhraseExplanation 相关死 i18n(8 条)+ 死 CSS(~45 行)清除;telemetry dismissed(撤高亮)随 dismiss 删除而消失;统一「分析中」提示(Rust emit_phrase_analysis_status + content-script try/finally emit + DiscoveryPanel 本地监听 phrase-analysis-status + phraseAnalyzing 文案,"正在分析此页…(短语·生词·潜在词)")。bloom fade-in 可选 polish 暂略(留 P5 实机调)。

待 P5(需用户/部署):① /edge-deploy 部署新 judge-phrases-batch(outward-facing,手工触发);② 实机 gate-C 复测(dev app + 开 phrase_analysis + 多体裁,验一致性=点击零撤销 + precision + 召回)。 遗留:Supabase 上 explain-phrase 函数已成孤儿(本地删、无调用方)→ 可在 Dashboard 删除(cosmetic)。


1. 已敲定决策(用户拍板,2026-06-28)

#决策出处
D1彻底合并两个 judge 成一个 richer batch judge(返回 idiomatic + sense),删 explain-phrase 全链路。不留兼容。用户点 1
D2判完再上色,只画确认的(所有桶)。用户确认
D3弹窗无「分析中」态——没判完就没高亮 → 没有可点的短语 span,场景不存在。只保留全局进度提示。用户点 3(化简)
D4batch~10 维持;记牢 batch 是唯一准确度旋钮,极端可降到 1。用户点 4
D5不分好/次档:开短语分析 = 全量 LLM;关 = 不画短语。静态 floor 不再端出。用户点 5
D6开关粒度:短语分析一个开关;双击查词消歧(context_disambiguation)保持独立(给用户多一点控制)。用户点 1(本轮)
D7产品定位文案先不改,仅记录(开关处知情同意文案要体现「借助 AI」)。用户点 2(本轮)
D8诚实承认依赖 LLM 的概率性局限是用户使用此功能的大前提。用户点 6
D9删选区「短语」按钮(无 LLM 静态降级档,D5 否决)。2026-06-28
D10召回走策略 3:pipeline 改 phrase-first(短语 match+判+画全在 vocab 前,vocab 排其后),满召回+零撤销+改动面最小;代价 = 开关开时 vocab 高亮晚 ~2-3s(正文不阻塞)。详 §3.6。2026-06-28
D11统一「分析中」提示:panel 一条 正在分析此页…(短语·生词·潜在词),可扩展后台任务队列;浏览不阻塞、增强层等一等。详 §3.6。2026-06-28

2. 目标架构

正文渲染 = 立即可读(增强层不阻塞浏览)
phrase_analysis = on(opt-in,默认关)→ pipeline 走 phrase-first(策略 3,§3.6)
  ↓ panel 挂统一「正在分析此页…(短语·生词·潜在词)」
【匹配·vocab 前】collectBodyTextNodes(完整节点,满召回)→ match_phrases_in_node_texts
        → 候选 = idiomaticity ∈ {always, context}(literal 不收)+ 各自所在句

【判定·await·judge-before-paint】一个 batch judge(richer,sense-aware)
        items=[{id, phrase, sentence, glosses_by_pos}] → 每条 {id, idiomatic, pos?, sense_index?}
        · idiomatic=true 且 sense 命中本地义项 → 画(统一 .rb-phrase 高亮,sense 入缓存)
        · idiomatic=false / 无 sense 命中 → 不画(拒绝 = 不可见)

【vocab → discoverable】排在短语之后(~2-3s 后与短语一起绽放)→ 提示清除

【点击】读 per-page 缓存(phrase+sentence → verdict)→ applyDisambiguation(pos, sense_index)
        秒出、零 LLM、永不撤销

(关 phrase_analysis 时:pipeline 走今天的 vocab-first 分支,vocab 瞬时、无短语高亮、无外发。)

关键纪律

  • 判完才上色——没有「先画静态、再 LLM 撤」;拒绝 = 从未画过。
  • 弹窗纯读缓存——点击一个已画短语 = 它必然 idiomatic=true 且 sense 已知 → 直接高亮该义项,没有任何能说「其实不是短语」的路径
  • 浏览不阻塞、增强层等一等——正文立即可读;短语+生词+潜在词作为一次几秒的诚实后台分析(panel 统一提示),开 phrase_analysis 时 vocab 也排在短语后。
  • context_disambiguation 独立——双击查词消歧仍是它单独门控;短语分析开关不再 imply 它(撤销 4C 的 implication)。

3. 全面影响分析(逐层触点 + 风险)⭐

用户点 3:本次涉及大量既有逻辑(高亮/pipeline/缓存/设置/埋点),影响面要全。下面是 grep + 阅读核实出的完整触点清单

3.1 content-script

文件现状改动
features/phrase-highlight.jshighlightPhrasesOnPage()(floor,always,vocab 前画,auto=true)、deepResolvePhrasesOnPage()(ceiling,context,vocab 后判+画,deep=true)、wrapPhrasesInTextNode(node,matches,sentence,auto,deep)emitDiscoveryPhrases()(扫 [data-rb-phrase-auto])、highlightPhrasesInSelection()DEEP_PHRASE_BATCH=10/MAX=120重写为单一 analyzePhrasesOnPage():匹配 always∪context 候选 → batch judge → 判完画确认的。wrapPhrasesInTextNodeauto/deep 两标记合并成一种(统一 .rb-phrase,去掉 data-rb-phrase-auto/-deep 二分)。emitDiscoveryPhrases.rb-phrasedeepCache 值从 bool 改为 {idiomatic, pos, sense_index}
index.js(~80-95)pipeline:highlightPhrasesOnPage()(vocab 前)→ vocab → discoverable → deepResolvePhrasesOnPage()(vocab 后,fire-and-forget)策略 3 改 phrase-first(§3.6):if (phraseAnalysis) await analyzePhrasesOnPage()(match→judge→画确认的,await)→ vocab → discoverable。关 phrase_analysis 时走今天的 vocab-first 分支。期间挂统一「分析中」提示。
features/popup.jsmaybeResolvePhrase()(gate contextDisambiguation||phraseDeepMode,调 explain_phrase)→ applyPhraseResolutiondismissPhraseHighlight(撤高亮)/ showPhraseExplanation(组合义文案);phraseResolveCache maybeResolvePhrase 的 LLM 调用 + dismissPhraseHighlight + showPhraseExplanation 的 compositional 分支 + phraseResolveCache:点击 .rb-phrase → 读 state.phrase.deepCache(键 = data-rb-phrase + data-rb-sentence)→ applyDisambiguation(pos, sense_index)永不撤销
features/popup.js maybeDisambiguate()(~408)单词消歧,gate contextDisambiguation||phraseDeepModegate 改为contextDisambiguation(D6 独立)。去掉 || phraseDeepMode
features/settings.js(33-124)batch keys 含 phrase_auto_highlight/phrase_deep_mode;set state.settings.phraseAutoHighlight/phraseDeepMode + state.phrase.highlightEnabled合并成单键(见 §3.4);highlightEnabled 改由新键驱动。
features/styles.js(384-411).rb-phrase/.rb-phrase-pv/.rb-phrase-idiom/.rb-phrase-flashauto/deep 无视觉区分基本不动(统一高亮本就一种样式)。可选:去掉 deep 专属 CSS(若有)。
index.js/features/discovery.js/features/highlight.js/events.jsTreeWalker 排除 .rb-phrase(不走进已包短语区);events 可能接选区「短语」按钮排除过滤保持(统一类名后仍 .rb-phrase)。选区按钮去留见 §4。
core/state.jsstate.phrase.highlightEnabled/deepCache/state.settings.*deepCache 值类型升级;settings 键改名。

3.2 Rust

文件现状改动
commands/supabase.rs judge_phrases_batch(~543)items=[{id,phrase,sentence}],Rust 补全 sense gloss,返回 Vec<PhraseVerdict{id,idiomatic}>重写为 richer:Rust 补按 pos 分组的候选义项(同 explain_phrasecandidates 形态),edge 返回 {id, idiomatic, pos?, sense_index?}PhraseVerdictpos/sense_index
commands/supabase.rs explain_phrase(~431)+ PhraseResolution/DisambiguateCandidate单条短语解析命令 explain_phrase 命令 + PhraseResolution struct(短语路径不再用;DisambiguateCandidate 若单词消歧 disambiguate_sense 仍用则保留——核实)。
commands/vocabulary/query.rs match_phrases_in_node_texts返回 PhraseMatch{phrase,char_start,char_end,is_phrasal_verb,is_idiom,is_basic,idiomaticity},按 word_tags idiomaticity:<tier> 路由不改(候选生成 + 路由层照旧)。消费侧(content-script)改为收 always∪context。
lib.rs注册 judge_phrases_batch/explain_phrase/match_phrases_in_node_texts/emit_discovery_phrases/toggle_phrase_highlight/log_phrase_interaction去掉 explain_phrase 注册;judge_phrases_batch 签名变(或更名 analyze_phrases_batch)。
commands/reading.rs/report.rs/auth.rs/mod.rsgrep 命中疑似 log_phrase_interaction 注册 / 无关 substring核实为非核心(多半只是 phrase substring 巧合)。

3.3 Edge Functions

文件改动
supabase/functions/judge-phrases-batch/index.ts重写:prompt 改 richer(对照按 pos 分组的候选义项,返回 {id, idiomatic, pos?, sense_index?})。融合 explain-phrase 的 sense 选择语义 + judge-phrases-batch 的非组合性/位移/POS 碰撞/guardrail 规则。MAX_ITEMS 保留。部署走 /edge-deploy(ref jdtbyteiwnciqnfppztz)。
supabase/functions/explain-phrase/index.ts删除(短语点击不再用;单词消歧用的是 disambiguate-sense,不是这个)。

⚠️ 删 edge function 前确认 explain-phrase 无其它调用方(grep 已确认仅 explain_phrase Rust 命令调它;删命令即断)。

3.4 Settings / 前端(开关合并,D5/D6)

目标phrase_auto_highlight(seed true)+ phrase_deep_mode(seed false)→ 一个键 phrase_analysis(seed false,默认关)。context_disambiguation 独立不动

文件改动
assets/sql/init_data.sql(24/26)删两行旧 seed;加 phrase_analysis='false'。(旧键残留行无害,可选 migration 清理。)
features/settings.js(33-35,114-124)batch keys 去掉两旧键、加 phrase_analysis;set state.settings.phraseAnalysis + state.phrase.highlightEnabled = phraseAnalysis;去掉 phraseAutoHighlight/phraseDeepMode
components/settings/GeneralSettings.tsx两个短语开关并一个「AI 短语分析」(Switch + ConfirmDialog);删 4C implication(phrase_analysis 不再联动 context_disambiguation——D6 独立);context_disambiguation 开关独立保留。
hooks/useContentTools.ts(38,92-94)短语发现 per-page 默认值从 phrase_auto_highlight 改读 phrase_analysis
lib/strings/{locales/zh,locales/en,}/panels/settings.ts合并文案:删 phraseAutoHighlight*phraseDeepMode* → 改写为「AI 短语分析」+ 诚实文案(D8:明示借助第三方 AI、按句判断、AI 概率性局限、只发候选句不发整页、可随时关)。
lib/commands.tsinvoke 封装:去 explainPhrasejudgePhrasesBatch 签名/返回更新。

word 路径独立性核实(D6)maybeDisambiguate(单词)+ 个性化例句 ④(ReviewSession/ClozeRevealCard/PersonalizedExampleAction)+ generate_personalized_example 都门控 context_disambiguation → 这些完全不动,短语重构与它们解耦。

3.5 Telemetry(phrase_interaction_log,migration v20)

  • dismissed(点击→撤高亮 = 误报分子)在新模型不存在(永不撤销)→ 该埋点死掉。
  • shown(CTR 分母 = 本页 always 数)语义变(现在 = 判完确认的高亮数,post-LLM)。
  • clicked 仍有意义(点击查看)。
  • 决策:v20 表/migration 不动(不 churn schema),但停写 dismissedshown 继续记(语义注释更新)。thin-slice v1 验证使命已完成,CTR/误报率指标退役(评测改用 /phrase-eval 野外多体裁 + gate-C)。

3.6 ⭐ 上色时机 vs 召回 —— 定为策略 3(用户拍板 2026-06-28)

问题:新模型「judge-before-paint」要求判完才画(LLM 返回后数秒);而短语满召回要求在完整文本节点上匹配(vocab 高亮会切碎节点 → 单节点匹配漏掉被切的短语)。三个策略:

策略召回复杂度代价
1 vocab 先、短语判+画在后(= 现 deepResolve 扩到 always∪context)漏被切碎的短语(有界召回损失)召回损失
2 vocab 前包不可见 marker → vocab 跳过 → 判完确认的加样式/拒绝 unwrap(DOM 编排 + 抑制被拒候选的 vocab 高亮)复杂、blast radius 大
3 ✅(采纳)短语匹配+判+画全在 vocab 之前;vocab/discovery 排在短语之后(短语 + vocab 都在完整节点匹配)(vocab 高亮器零改动,只是排在后面)vocab/discovery 高亮晚 ~2-3s

采纳策略 3(pipeline 改 phrase-first)

  • 满召回 + 零撤销 + 改动面最小——短语在完整节点匹配(满召回),judge-before-paint 只画确认的(零撤销),vocab 高亮器完全不改(只是排在短语 await 之后)。
  • 唯一代价 = 当 phrase_analysis 开时,vocab/discovery 高亮也要等短语 LLM(~2-3s)。但:① 阅读正文不阻塞(正文立即可读,高亮是叠加增强层);② 关 phrase_analysis 时 vocab 瞬时如今天(pipeline 分支:开→phrase-first,关→今天的 vocab-first)。
  • 几秒成立的依据:vocab/discovery 是本地瞬时,唯一慢的是短语 LLM;所有批 Promise.all 并行 → 墙钟 ≈ 最慢一批(~2-3s),长文亦然(caveat:LLM provider 并发限流可能拉长,诚实提示兜底)。短语+生词+潜在词一起绽放(比交错闪烁干净)。
  • pipeline 改动:短语 pass 从 fire-and-forget 改为 await(vocab 在其后);短语 pass 内部 = match(完整节点)→ batch judge → 画确认的。

配套:统一「分析中」提示(用户拍板)——把整个增强层做成一次诚实的几秒后台分析:

  • panel 一条统一状态 正在分析此页…(短语 · 生词 · 潜在词),完成清除 / 转「已标注 N 短语 · M 生词」。不做分项进度条(现实时间线是"全卡短语 LLM→一起完成",granular 无信息量)。
  • 做成可扩展后台任务队列:未来 analyzePageDifficulty / 封面抓取等后台活也能挂进同一提示。模型 = 浏览正文不阻塞,增强层等一等
  • 可选 polish:高亮 bloom 时极轻 fade-in。

未来解耦(不进 v1):若"vocab 跟着等"成困扰 → 短语 + vocab 合并成一次协调上色(两边完整节点匹配 → 判完一起画、短语优先生词让位),满召回且 vocab 不等短语;但要动 vocab 高亮器(blast radius 大)。v1 先走简单顺序版。


4. 决策点(已确认 2026-06-28)

  1. 选区「短语」按钮 → 删除(D9)。涉及 toolbar.js phrasesBtn + highlightPhrasesInSelection + handleSelectionPhrases + i18n toolbar.phrases + collectRangeTextNodes/clipNodeToRange(仅选区路径用,一并删)。理由:无 LLM、露全部桶 = D5 否决的静态降级档;backlog §选区工具栏早有删意。
  2. 召回策略 → 策略 3(D10,phrase-first + 统一分析提示),详见 §3.6。
  3. 分析提示 → panel 统一状态 + 可扩展后台队列(D11),详见 §3.6。
  4. 旧 settings 键清理phrase_auto_highlight/phrase_deep_mode 残留行不加 migration(cosmetic,停读即可)。

5. 分阶段实施

Phase范围产出
P0契约定稿:新 edge judge-phrases-batch(richer)I/O + Rust judge_phrases_batch(richer,补 pos 分组候选)+ deepCache 值结构接口冻结,便于并行改前后端
P1Rust + edge 重写:新 batch judge(返回 idiomatic+sense);删 explain_phrase 命令 + explain-phrase edge;cargo check/edge-deploy后端就位
P2content-script 重写:analyzePhrasesOnPage()策略 3 phrase-first);wrapPhrasesInTextNode 单标记;index.js 改 phrase-first await(vocab 排后);popup 点击读缓存 + 删撤销链;deepCache 升级核心行为
P3settings 合并:单键 + 文案(诚实)+ GeneralSettings 改 + useContentTools + strings;word 路径 gate 解耦opt-in 收口
P4删选区按钮(D9)+ telemetry 停写 dismissed + 统一「分析中」提示(D11,panel 后台任务队列,含 vocab/discovery)+ 可选 bloom fade-in收尾
P5gate-C 复测(§6):precision + 召回 + 一致性(点击零撤销)+ 多体裁验证

/arch-check + /build-check + /code-review 按 CLAUDE.md §8 标准流程跑。


6. 验证(gate-C 复验,新口径)

复用 /phrase-eval(A 实机多体裁 + rb-debug 驱动)。新增一致性口径:

  1. precision:opted-in,多体裁(新闻/科学/论说/文学/小说)取样,.rb-phrase 高亮里真习语义占比(目标延续 gate-B ~90%;fiction ~80-85% 可接受 ceiling)。
  2. 召回(策略 1 的有界损失量化):抽查正文真习语用法被 vocab 切碎而漏的比例;判断是否需策略 2。
  3. 一致性(本次核心):点击任意已画短语,永不出现撤高亮 / 「字面用法」反转(结构上保证,实测确认)。
  4. 诚实/鲁棒:关开关→零短语高亮零外发;断网/超时→graceful(不画,不报错);长文「AI 正在分析」如实显示。

判据:一致性 100%(零撤销)+ precision 不低于旧 deep 路径 + 召回损失可接受 → 重构成立。


7. 不做 / 边界

  • case-1(是短语但本地释义未覆盖该义项)这次治不了——它是词库覆盖问题(缓冲池 / RVH pipeline 补义项),不在本重构。新 judge 若判 idiomatic=true 但无本地 sense 命中 → 保守不画(不画一个画不出准释义的短语),避免「自信标错」。
  • 不发整页——只发含候选短语的句子(隐私 + token 双控)。
  • 不做持久缓存(隐私含用户句 + 按 VOCABULARY_SEED_VERSION 失效,投入产出不成立;沿消歧 follow-up 同结论)。per-page 内存缓存照旧。
  • 不上全局 batch=1(D4:batch~10 + 诚实框架已够;batch 作准确度旋钮记牢)。
  • 产品 slogan/对外定位文案不动(D7,仅记录;只改开关 + 设置页文案)。
  • context_disambiguation 全链路不动(D6 独立;单词消歧 + 个性化例句 ④ 与短语解耦)。

8. 文件锚点(实施速查)

supabase/functions/explain-phrase/index.tscommands/supabase.rs explain_phrase+PhraseResolution;popup maybeResolvePhrase/dismissPhraseHighlight/showPhraseExplanation(compositional)/phraseResolveCache;(按 §4)选区 highlightPhrasesInSelection+toolbar phrasesBtn。 重写supabase/functions/judge-phrases-batch/index.ts(richer);commands/supabase.rs judge_phrases_batch(+pos/sense);phrase-highlight.js(单 analyzePhrasesOnPage,策略 1);index.js 接线;popup 点击读缓存。 features/settings.js(单键);GeneralSettings.tsx(并开关 + 删 4C implication);useContentTools.tsstrings/.../settings.ts×3;lib/commands.tsinit_data.sqlmaybeDisambiguate gate 解耦;emitDiscoveryPhrases.rb-phrasestate.js deepCache 升级。 不动query.rs match_phrases_in_node_texts(路由层);phrase_interaction_log(v20) schema;disambiguate-sense 全链路;个性化例句 ④。