Skip to content

CEFR LLM 评估 + cefr_inferred/cefr_source 透传 + UI 标识

Context

预装库 v5(12291 词)已部署成功(2026-05-12 凌晨)。但 CEFR 数据可信度问题暴露:

CEFROxford 权威CEFR-J 权威Google freq 反推(估算)估算占比
A110407400% ✅
A297226700% ✅
B1120971200% ✅
B2876144200% ✅
C15480223380.3% ⚠️
C2004992100% ⚠️

C1/C2 共 7225 词(占总数 58.7%)由 wordlist_builder.dart::c1Threshold = rank < 10000 ? 'C1' : 'C2' 推断 —— 频率反推存在已知偏差:

  • 低频但简单的词被高估(如 aardvark → C2)
  • 专有名词残留(部分被 db_generator filter 拦下)
  • 高频但难的词被低估(如 albeit / nonetheless
  • 领域偏向(Google web 频率偏向 IT/电商)

决策

三方案对比 → 选 A(LLM 评估)

维度A. LLM 评估B. SUBTLEX 频率C. 学习者语料
Pipeline 改造中(加新 step)极小(换数据源)大(数据接入)
工作量4-6 小时2-3 小时1-2 天
金钱成本$0.5-5$0可能付费
准确度中-高(语义+难度)中(仍是频率)高(如能拿到 EFCAMDAT)
解决根本问题✅ 是❌ 否✅ 是
当前阶段适用⭐⭐⭐ 推荐⭐⭐ 可选⭐ 不推荐

A 的关键性质:纯叠加架构 — 不破坏现有 Google web 频率反推 pipeline,仅在 LLM 评估失败时 fallback 到原值。

Audit 字段方案 → 选 B(单字段 cefr_source)

方案DB 列数分析能力跨端同步成本
A. 全 audit 入库(cefr_oxford / cefr_from_freq / cefr_from_llm 3 列)+3 列🟢 SQL 直接 join🟡 列膨胀
B. 单字段 source(cefr_source)+1 列🟡 能分组但不能 cross-compare🟢 1 列
C. 仅 pipeline JSON+0 列🟡 离线分析🟢 不影响
D. A + B+4 列🟢 最强🟡 列最多

理由:DB 层只关心"当前用哪个值 + 来源",audit 真相留在 pipeline 中间文件(master_wordlist.csv + translated_vocabulary.jsonl)。

UI 标识 → "约 C2" + 灰色 + tooltip

权威 CEFR 正常显示 C2;LLM/freq 推断显示 约 C2(灰色,长按 tooltip "基于词频/AI 估算,非官方分级")。纯 UI 渲染层字符串拼接,不引入新 enum 值。

不在本 plan 范围

  • CEFR enum 6 值化重构(PENDING/UNKNOWN/REFERENCE 拆字段):59 处引用 / 14 个文件 / 跨端契约影响,单独立项。详见 ~/.claude/projects/.../memory/project_cefr_orthogonal_dimensions.md
  • i18n / 多语言 UI 标识:本期硬编码中文 "约",遵循 memory feedback_i18n_chinese_only.md

关键设计原则(红线级,未来改动必须遵守)

本 plan 落地后,未来任何 CEFR 相关改动都必须遵守以下原则。违反 = 数据语义污染 + 跨端契约风险。

原则 1:CEFR 的"值"和"可信度"是正交维度

  • primary_cefr_level 严格 6 值(A1/A2/B1/B2/C1/C2)或 NULL(未分级)
  • 可信度cefr_inferred (bool) + cefr_source (string) 表达
  • ❌ 不要在 enum 里塞"约 C2"/"近 C2"/"APPROX_C2"等组合值
  • ❌ 不要在 cefr_source 里塞"oxford_high_confidence" / "freq_low_confidence"等混合语义
  • ✅ 想表达"这个 C2 不太确定",用 cefr_inferred=true + cefr_source 标具体来源

原则 2:CEFR 字段语义纯粹

  • primary_cefr_level 只表达"难度等级",不表达"是否分级"/"是否可学"/"是否参考词"
  • ❌ 不要在 cefr_source 里塞 'pending' / 'unknown' / 'reference'(这些是其他维度状态,详见 memory project_cefr_orthogonal_dimensions.md
  • ❌ 不要在重构 PENDING/UNKNOWN/REFERENCE 时"顺手"把它们塞进 cefr_source(应建独立字段如 cefr_status / data_status / word_type
  • ✅ 任何新维度("可信度区间"/"评估时间"/"评估者"等)都建独立字段,不挤现有字段

原则 3:UI 字符串拼接 ≠ 数据建模

  • "约 C2" 是 UI 渲染层的视觉决定(presentation layer 字符串拼接)
  • DB 层永远存 primary_cefr_level='C2' + cefr_inferred=1
  • 跨端同步只同步底层数据;UI 表达可双端独立演进(RVH 用"约",RB 可用"≈"或不显示,互不影响)
  • ❌ 不要把 "约 C2" 当成可枚举值往 DB / 跨端契约里塞

原则 4:三端 vocabulary schema 一致性优先(接受有意冗余)

  • 背景:RVH (SQLite) / Supabase (PostgreSQL) / RB (SQLite via Rust) 三端共享同一份 vocabulary 表结构
  • 决策:三端字段集合保持一致,即使某字段在某端无业务意义
  • 理由
    • 维护心智负担小(一处改 schema,三端跟着对齐)
    • 跨端 sync 契约简单(不用按端区分列集)
    • 数据迁移工具不用做端特定字段映射
  • 代价(已接受):
    • audio_local_path 在 RB / Supabase 端无意义但仍有此列(写 NULL)
    • last_accessed_at 在 Supabase 端无意义但仍有此列(写 NULL)
    • 个别字段空着但占 schema 行(如 word_family 等 Phase 5)
  • 执行规则
    • ✅ 加字段:三端同步加,即使某端用不到也加(写 NULL 即可)
    • ✅ 删字段:三端同步删,避免某端单方面缺列
    • ❌ 不要"某端简化版":例如"Supabase 不需要 LRU 字段,去掉 last_accessed_at"是违反此原则的,理由不够强
    • ⚠️ 例外:跨端语义分裂(如旧 cover_image_path 一端 URL 一端本地路径)必须打破对称性 — 此时按红线级处理(详见 RVH CLAUDE.md §6c)

原则 5:vocabulary 主表 vs reference_words 参考表的边界

  • 核心区分
    • vocabulary 表 = 学习词库 — 用户要学的词,参与 CEFR 筛选 / 间隔重复 / 复习
    • reference_words 表 = 参考词库 — 阅读时识别但不参与学习(专有名词、缩写、月份名等)
  • 入主表的双门槛db_generator.dart::_isVocabularyGrade):
    1. 必须有 pos_definitions(Kaikki 找得到结构化定义)
    2. 至少一个 sense 有 zh 翻译(用户能看懂)
  • 不满足任一门槛 → 流到 reference_words_candidates.jsonl(候选文件),等 reference_words 表消费
  • 正交于 word_tags
    • db_generator filter 在入主表前跑,关心"能不能学"
    • word_tags(LLM 评估打的)关心"词类型是什么"
    • 两者各管一段,可以同时触发:例 africa 被 filter 分流 ∧ 被 LLM 标 proper_noun
    • 注意:被 LLM 标了 proper_noun 但有 zh 翻译的词(例 aaron→亚伦仍留主表,UI 用 isReference getter 派生处理
  • 数据沉淀,不丢失
    • 2026-05-12 本次部署分流 2047 + 27 = 2074 词(其中 empty_pos_definitions 2047 / no_zh_translation 27)
    • 已写到 tools/vocabulary_builder_v3/output/reference_words_candidates.jsonl
    • 等"演进 4:candidates → reference_words 表"任务消费(详见下文)
  • 设计意图
    • 不要把"参考词"塞进 vocabulary 主表后再用字段过滤——会污染主表语义、增加 SQL 查询心智负担
    • 不要丢弃 filtered-out 数据——这些是后续 reference_words 表的输入材料

未来演进路径(不在本 plan,但记录避免遗忘)

这两条都是非破坏性的平滑升级。本 plan 的 cefr_inferred + cefr_source 设计已为它们留好出口。

演进 1:CEFR enum 6 值化重构

  • 触发条件:跨端 RB 对接需要 CEFR 严格 6 值时 / 数据分析需要清晰语义时 / 跨端 sync 报错涉及 PENDING/UNKNOWN/REFERENCE 时
  • 影响范围:59 处引用 / 14 个文件 / 跨端契约
  • 与 cefr_source 协同:重构后 cefr_source 不变,但原 enum 中的 PENDING/UNKNOWN/REFERENCE 会迁到独立字段:
    • cefr_status ∈ {'graded', 'pending', 'unknown'} — 分级状态
    • word_type ∈ {'standard', 'reference'} — 词类型(标准学习词 vs 参考词)
  • 详见:memory project_cefr_orthogonal_dimensions.md

演进 2:Audit 字段从 B 升级到 A(全 audit 入库)

  • 触发条件:发现需要在 app 内做 LLM vs freq 准确度对比 / 想做"重置 CEFR"功能(用户回退到 freq 推导值)/ 学术分析需要 SQL 直接 cross-compare
  • 迁移路径
    1. vocabulary 表加 3 列:cefr_oxford TEXT / cefr_from_freq TEXT / cefr_from_llm TEXT
    2. Pipeline 已有这些数据(在 master_wordlist.csv + translated_vocabulary.jsonl),只需 db_generator.dart 补写入 + Supabase 表镜像 +1 次性 backfill
    3. cefr_source 字段保留,作为"当前用了哪个值"的快捷索引
  • 非破坏性:cefr_inferred / cefr_source 的语义不变,新列纯叠加;可平滑升级

演进 3(潜在):词典优先 4 层 lemmatizer 给 CEFR 也来一遍

  • 思路:参考 lib/core/nlp/lemmatizer.dart 的"词典优先"架构(详见 RVH 红线 #5e)— 用一份双端共享的 SHA256 byte-equal CEFR 字典作为权威,CC0/MIT 数据集(如 EVP 公开版本)打底,pipeline 估算只对字典 miss 的词跑
  • 触发条件:找到合规可用的全量 CEFR 数据集(当前 EVP 商用付费 / Octanove SA 传染)
  • 暂搁置原因:数据源问题未解决;本 plan 的 LLM 评估已是当前最佳方案

演进 5:双模型 CEFR 交叉验证(提升 LLM 评估准确度)

  • 触发条件:用户报告 LLM 评估明显错误的词,或抽样发现错误率超出可接受阈值
  • 背景(2026-05-13 实证):DeepSeek 单模型评估有 ~5-10% 噪声。用户报告样例:
    • abbreviation 被评 B1(社区共识 B2/C1),且被错误地打 word_tags=["abbreviation"](它本身是普通名词,不是缩写)
    • 5193 词级 review 不可行,需要工程化方案
  • 核心思路:双 LLM 交叉验证 — 一个模型异常评估时另一个模型大概率不会同样异常
  • 实施方案
    1. 加 audit 字段(不破坏现有 schema):在 translated_vocabulary.jsonl 现有字段基础上加 cefr_claude / word_tags_claude
    2. 跑 Claude Sonnet 二轮评估:复用现有 evaluator 框架 + checkpoint,但写入 audit 字段而非 primary
    3. 对比报告(新 CLI bin/compare_cefr_evaluations.dart):
      • 一致率统计(按 CEFR 差距 0/1/2/3 级分布)
      • 最大分歧样本列表(≥2 级差)
      • 用户报告词的双模型结果
    4. 合并策略(依据报告决定):
      • 选 1 保守:取较高 CEFR(学习者倾向高估难度)
      • 选 2 Claude 优先:默认 Claude,DeepSeek 仅 sanity check
      • 选 3 一致 + 高分歧 review:一致词保留,差 ≥2 级人工修
    5. 重新部署预装库(升 v11)
  • 成本
    • API:~$4-5(Claude Sonnet 5193 词,$3/M input + $15/M output)
    • 时间:~15-30 分钟(API call) + 人工 review 报告 15-30 分钟
  • 依赖ANTHROPIC_API_KEY 环境变量
  • 预期收益:错评率 ~10% → ~3% (双模型一致 ~75%,剩余 25% 标出来 review)
  • 关联:cefr_evaluator.dart 已支持 Claude provider(switch case 'claude' || 'anthropic'),无需新 provider 实现

演进 4:candidates → reference_words 表导入(本 plan 主任务完成后立即启动)

  • 触发条件:本 plan Track A + B + C 全部完成
  • 当前状态
    • 已沉淀候选数据:tools/vocabulary_builder_v3/output/reference_words_candidates.jsonl(2074 词,2026-05-12 生成)
    • 已存在 reference_words 表(系统预装 216 词,详见 MEMORY pending tasks 段)
    • LLM 评估副产物 945 词带 word_tags(含 proper_noun / abbreviation / symbol)也是 reference 候选
  • 设计要点
    1. 数据源合并:candidates.jsonl (2074) ∪ vocabulary 中 word_tags ≠ NULL (945) = ~2900-3000 词
    2. 去重 + 归一:用 lemmatizer 跑一遍(参考红线 #5e),生成 reference_words 表归一形式
    3. 元数据保留:把 cefr_inferred、LLM 评估的 CEFR、word_tags 信息也带进 reference_words 表
    4. 跨端:reference_words 表已在三端(RVH/Supabase/RB),导入数据三端同步
    5. vocabulary 主表:被收编到 reference_words 的词从 vocabulary 删除?还是保留双表存在?— 需要决策
  • 风险:与"演进 1:CEFR enum 6 值化重构"(PENDING/UNKNOWN/REFERENCE 拆字段)有依赖关系,应先做演进 1 还是直接演进 4,需评估
  • 预期工作量:1-2 天(含跨端同步契约验证)
  • 关联 memoryreference_words 扩充 (MEMORY pending tasks 段)

Schema 设计 Audit(2026-05-12 复盘发现 — 暂不处理)

在 Step A1.5 之后 Step A2 之前做的整体 vocabulary 表 schema review。发现的 7 个观察点按 "有意冗余(接受)" 和 "待迭代修复" 两类分组,记录在此避免遗忘。本 plan 不动 schema 设计层,所有项标记为"未来演进/不做"。

接受为"有意冗余"(遵守原则 4:三端 schema 一致性优先)

ID字段现象决策
OBS-1audio_local_pathRB / Supabase 端访问不到 RVH 本地路径✅ 保留三端一致,非 RVH 端写 NULL。Track B 上传脚本要显式 NULL 此列
OBS-2last_accessed_atLRU 仅 RVH 本地行为,跨端无意义✅ 保留三端一致,非 RVH 端写 NULL。Track B 上传脚本要显式 NULL 此列
OBS-3word_familyv13 起一直为空,等 Phase 5 填充✅ 保留,避免来回改 schema;新增 issue 跟踪 Phase 5 填充计划

Track B 实施提醒(B2.1 上传脚本必须显式排除这两列):

dart
// 上传 vocabulary → Supabase 时
final remoteRow = {
  ...localRow,
  'audio_local_path': null,     // RVH 本地路径,跨端无效
  'last_accessed_at': null,     // LRU 本地行为,跨端无意义
};

待迭代修复(非阻塞,但日后改更好)

ID问题严重度触发条件处理方案
OBS-4source vs cefr_source 命名冲突(指代"什么的来源"不同维度)🟡 Low加第 3 个 *_source 字段(如 etymology_source)时source 改名 entry_sourcerecord_origin;跨端契约影响
OBS-5cefr_inferred 可由 cefr_source 派生(冗余)🟡 Low出现 inferred 与 source 不一致 bug 时选 1:删 cefr_inferred 走 source 派生
选 2:改为 SQLite 3.31+ generated column
当前:代码层手动同步
OBS-6Supabase cefr_inferred BOOLEAN ↔ SQLite INTEGER (0/1) 类型转换🟢 InfoTrack B1 ALTER TABLE 落地时PostgREST 会自动 cast 0/1 ↔ true/false;push/pull 时验证一次即可,已写在 supabase_vocabulary_service.dart
OBS-7pos_definitions JSON 无法 SQL 查询单 sense🟢 Info需要 SQL 层做 sense-level 搜索时已是 v34 决策(dense storage > query flex),不动

设计的良好之处(不要倒退)

  • ✅ 正交建模:cefr_level / cefr_inferred / cefr_source / word_tags 四维度各司其职
  • word 升 PK + COLLATE NOCASE(v43)解决跨端 UUID 漂移
  • ✅ v34 大瘦身(删 9 个冗余字段)
  • ✅ pos_definitions JSON 紧凑(密集存储)
  • ✅ 字段都有版本号注释(v13/v34/v48/v49/v50)
  • ✅ 索引覆盖(cefr / source / freq / last_accessed)

Audit Trail(cross-session 备查)

  • 2026-05-12 复盘人:用户 + Claude
  • 审视范围:17 列 vocabulary 表 + 索引 + 跨端语义
  • 结论:3 个接受为有意冗余 / 4 个待迭代修复;整体设计合理,本 plan 不动 schema
  • 新增原则:「原则 4:三端 vocabulary schema 一致性优先」纳入「关键设计原则」段

Schema 设计

v48 已落(前置工作)

  • vocabulary.cefr_inferred INTEGER NOT NULL DEFAULT 0 — true 表示 primary_cefr_level 是推断值

v49 新增(本 plan Step 1)

  • vocabulary.cefr_source TEXT — 取值约定:'oxford' / 'cefr_j' / 'llm' / 'freq' / NULL(NULL = 历史数据/未标记)

为什么 cefr_source 不用 NOT NULL? 现有 backfilled 词(348 个)和未来 Edge Function 兜底词的来源标记可能缺失,允许 NULL 避免迁移阻塞。新预装库每词都会写入。

Supabase 端镜像

vocabulary 表(远端)需补 cefr_inferred BOOL + cefr_source TEXT 两列(Track B Step B1 落地)。

阶段进度(跨会话锚点)

⚠️ 跨会话恢复时,从首个未勾选项继续。每完成一项立即勾选并写实施 commit hash。

Track A:Pipeline + DB + 评估闭环

Step A0:前置 schema/model/pipeline 改造(加 cefr_source)✅ 完成 commit bbedeab

  • [x] A0.1 assets/sql/01_create_tables.sqlcefr_source TEXT 列 + 注释
  • [x] A0.2 lib/shared/data/database/app_database.dart
    • [x] _schemaVersion 48 → 49
    • [x] _insertVocabulary 写入 cefr_source
  • [x] A0.3 App 层 model/entity 加字段
    • [x] lib/features/vocabulary_filtering/data/models/vocabulary_item_model.dartcefrSource: String?
    • [x] lib/features/vocabulary_filtering/domain/entities/vocabulary_entity.dartcefrSource
    • [x] lib/features/vocabulary/data/services/supabase_vocabulary_service.dart 加字段
    • [x] 跑 dart run build_runner build --delete-conflicting-outputs 重生 freezed
    • [x] flutter analyze lib/ 0 error
  • [x] A0.4 Pipeline 加字段
    • [x] tools/vocabulary_builder_v3/lib/models/vocabulary_entry.dartString? cefrSource
    • [x] lib/data_merger.dart:Oxford 词标 'oxford',CEFR-J 词标 'cefr_j',freq 反推标 'freq'
    • [x] lib/db_generator.dart SQL CREATE + INSERT 加列
    • [x] bin/build_all.dart / bin/enrich_translations.dart / bin/generate_db.dart JSON 序列化加字段
    • [x] bin/evaluate_cefr.dart 评估成功后写 entry.cefrSource = 'llm'
    • [x] 额外修:bin/merge_data.dart::_loadWordlist 用简单 split(',') 无法解析 pos_cefr JSON 列,对齐 build_all.dart 的稳健实现
    • [x] 额外新增:bin/backfill_cefr_source.dart — 把 cefr_inferred + cefr_source 补回已生成的 translated_vocabulary.jsonl(14365 词全 matched,源分布 oxford 4645 / cefr_j 2495 / freq 7225)
  • [x] A0.5 commit bbedeabfeat(schema): v48/v49 加 cefr_inferred + cefr_source + pipeline 全链路透传

Step A1:evaluate_cefr 试水(50 词,DeepSeek)✅ 已跑

  • [x] A1.1 --dry-run 看成本估算(5193 词 / $0.21)
  • [x] A1.2 --limit 50 --output translated_vocabulary.eval50.jsonl — 跑 50 词
  • [x] A1.3 人工 review LLM 输出质量
    • 28 词 LLM 重评(70%):方向对,C1→B1/B2 大量降级修正 freq 反推错误
    • 12 词 LLM 识别为 PROPER_NOUN / ABBREVIATION(保留原 CEFR)
    • 10 词 posDefinitions 空被跳过
  • [x] A1.4 决策(2026-05-12 用户拍板):
    • LLM 评估质量"满意" — 比 freq 反推准 ~20 个百分点
    • PROPER_NOUN / ABBREVIATION 信号不能丢,需引入 v50 word_tags 字段
    • 暂停 Step A2 全量,先做 Step A1.5 schema 升级

Step A1.5:v50 加 word_tags 字段 + LLM evaluator 升级

⚠️ 新增步骤(2026-05-12 决策)。理由:LLM 已能识别 proper_noun / abbreviation, 但当前 evaluator 只统计不存储。设计 word_tags 用 tags 数组(JSON),正交干净, 未来可扩展 symbol / archaic / medical 等 0 schema 改动。详见 plan 顶部 「关键设计原则」段。

Schema 设计

  • vocabulary.word_tags TEXT — JSON 数组,例如 '["proper_noun"]' / '["proper_noun","abbreviation"]' / NULL
  • NULL = 普通学习词(无标签);非 NULL = 至少有一个标签
  • App 层派生 bool get isReference => wordTags?.any({"proper_noun","abbreviation","symbol"}.contains) ?? false

为什么不用单 enum:NASA = proper_noun ∧ abbreviation 双标签场景,单字段 enum 强制选一个会丢信息。

A1.5.1 Schema + App 端
  • [x] assets/sql/01_create_tables.sqlword_tags TEXT 列 + v50 注释 + Schema版本号 v49→v50
  • [x] assets/sql/02_create_indexes.sql + 03_init_data.sql Schema版本号 v49→v50
  • [x] docs/database/schema.md 加 v50 变更摘要 + 头部版本号
  • [x] lib/shared/data/database/app_database.dart _schemaVersion 49→50_insertVocabulary 写入 word_tags
  • [x] vocabulary_item_model.dartwordTags: String?(JSON 字符串)+ fromDatabase / toDatabase
  • [x] vocabulary_entity.dartwordTags(同左)+ Equatable + copyWith
  • [x] supabase_vocabulary_service.dart 加 word_tags 读取
  • [x] dart run build_runner build --delete-conflicting-outputs
  • [x] flutter analyze lib/ 0 error
A1.5.2 Pipeline 端
  • [x] lib/models/vocabulary_entry.dartSet<String> wordTags = {} + JSON 序列化辅助
  • [x] lib/db_generator.dart CREATE TABLE + INSERT 加 word_tags 列(jsonEncode 写入)
  • [x] bin/build_all.dart / bin/enrich_translations.dart / bin/generate_db.dart JSON 读写透传
  • [x] bin/evaluate_cefr.dart 读写 word_tags
  • [x] bin/backfill_cefr_source.dart:本次不需要补 word_tags(无来源数据,全 NULL 即可)
A1.5.3 LLM evaluator 升级
  • [x] lib/cefr_evaluator.dart 改 prompt:要求 LLM 同时输出 {"word": {"cefr": "C1", "tags": ["proper_noun"]}} 而非裸 CEFR 字符串
  • [x] 改 _applyEvaluations:解析新 JSON 形态;tags 写入 entry.wordTags(去重 union);CEFR 单独写 primaryCefr
  • [x] 处理向后兼容:旧 prompt 输出 "PROPER_NOUN"/"ABBREVIATION" 也接受,转为 tags
A1.5.4 重跑 50 词试水验证
  • [x] dart bin/evaluate_cefr.dart --limit 50 --output output/translated_vocabulary.eval50_v2.jsonl
  • [x] 验证 word_tags 写入正确(PROPER_NOUN 词在 entry 里有 ["proper_noun"]
  • [x] 用户 review 质量没回退
A1.5.5 Commit
  • [x] commit:feat(schema): v50 加 word_tags + LLM evaluator 同步输出 tags

Step A2:evaluate_cefr 全量评估 ✅ 完成 2026-05-12

  • [x] A2.0 前置改造:evaluator 加 checkpoint 机制(避免 hang/中断丢失全部进度)
    • 旧版 5193 词跑 38.7% 时网络 TCP 僵死 hang 14+ 分钟,无 timeout 不可恢复
    • 改造:HTTP 加 120s timeout;evaluator 加 checkpointEveryNBatches + CheckpointFn;resume 改为数据驱动(cefrSource == 'llm' 跳过);SIGINT 触发 graceful flush;atomic rename 写盘
    • Resume 验证:72 词跑完后 --resume 0 API 调用,全部正确跳过
  • [x] A2.1 备份 output/translated_vocabulary.jsonl.before-evalcefr-fullrun-20260512-123302
  • [x] A2.2 dart bin/evaluate_cefr.dart --resume --checkpoint-every 15 — 5193 词 / 520 API 调用 / 10 分钟 / $0.34
  • [x] A2.3 统计 ✅:
    • CEFR 变更:4686/5193 = 90.2%;最大转移 C2→B2 (1580) / C2→B1 (785) / C1→B2 (653)
    • word_tags:proper_noun 765 / abbreviation 199 / symbol 8(unique 956 词)
    • 残留 cefr_source='freq' 2034 词(pos_definitions 空,无法评估)
    • 分布大幅修正:C2 从 4992 缩到 1910(-60%),B1+B2 从 4239 增到 7706(+82%)

Step A3:重新生成预装库 + 部署验证 ✅ 完成 2026-05-12

  • [x] A3.1 dart bin/generate_db.dart 重新生成 output/reading_vocab.db — 12291 词入库 / 2074 候选分流 / CEFR 分布 A1:1131 A2:1730 B1:3148 B2:4538 C1:1261 C2:483
  • [x] A3.2 SQLite 抽查:17 列含 cefr_inferred/cefr_source/word_tags;word_tags 945 词;cefr_source 分布 oxford 4636 / cefr_j 2488 / llm 5165 / freq 2(残留 corner case);cefr_inferred 7124 false / 5167 true
  • [x] A3.3 拷贝到 assets/databases/reading_vocab.db(SHA256 d09d5978ab...,42188800 bytes)
  • [x] A3.4 升 preinstalledVocabularyVersion:v6 → v7(上次会话已升到 v6,本次再升触发新内容导入)
  • [x] A3.5 flutter run -d 6DL76DBEQSYXA6BI 验证:Schema v50 启动 → v6→v7 upgrade 触发 → 12291 词导入 4.379s → 0 flutter 异常 → CEFR 分布与离线 SQLite 一致
  • [x] A3.6 commit(含 assets/databases/reading_vocab.db + app_database.dart 升 v7)

Step A4:UI 加 "约" 标识 + tooltip

  • [ ] A4.1 调研 cefr_badge.dart / cefr_section_header.dart 等显示 CEFR 的位置(grep level.label / cefrLevel.label
  • [ ] A4.2 加显示工具:String formatCefrLabel(CefrLevel level, {required bool inferred})
  • [ ] A4.3 主显示位置接入 formatCefrLabel(cefr_badge / 词卡 / 列表项)
  • [ ] A4.4 加 tooltip "基于词频/AI 估算,非官方分级"(仅 inferred 词显示)
  • [ ] A4.5 灰色处理(inferred 词 CEFR 标签用 colorScheme.onSurfaceVariant)
  • [ ] A4.6 flutter run 验证视觉效果(找几个典型词截图对比)
  • [ ] A4.7 commit:feat(ui): inferred CEFR 显示 "约 X" + tooltip + 灰色

Track B:跨端同步 + Edge Function ✅ 完成 2026-05-13

实际执行 超出原计划——做了一次性彻底重建(用户决策):

  • Supabase 不再上传本地 12291 预装词(设计转向「共享缓存」模型)
  • 同时改 FK CASCADE → RESTRICT(消除红线 #6b 同源 footgun)

Step B1:Supabase migration v51 ✅

  • [x] 写 SQL migration supabase/migrations/20260513_v51_vocabulary_rebuild_fk_restrict.sql
  • [x] 双目标:vocabulary 重建(清 94063 旧 Kaikki 词 + 加 3 新列)+ FK CASCADE→RESTRICT
  • [x] 用户在 Supabase Studio 执行 migration
  • [x] 验证:vocabulary/user_notebook_entries/user_excluded_words 都是 0 行;2 条 FK delete_rule=RESTRICT
  • [x] commit 8a471a6

Step B2:Supabase 数据上传 → 设计转向(不上传)

  • [x] 决策:Supabase 当「共享缓存」用,不镜像本地预装库
  • [x] 用户查词流程:本地 12291 命中(90%+)→ Supabase(首次空)→ Edge Function(兜底)→ 写回 Supabase
  • [x] 管理员可定期 SELECT word FROM vocabulary WHERE source='backfilled' ORDER BY created_at DESC 看新增词决定纳入下版本预装库

Step B3:Edge Function 兜底词改造 ✅

  • [x] B3.1 改 supabase/functions/lookup-or-fetch-word/index.ts
  • [x] B3.2 兜底词字段:primary_cefr_level='C2' + cefr_inferred=true + cefr_source='fallback' + word_tags=null
  • [x] 修复 bug:pos_definitions[pos].cefr 也同步 'C2'(不是 'PENDING')— affca84
  • [x] B3.3 用户部署 Edge Function
  • [x] B3.4 E2E 测试 obfuscate:顶层 + pos.cefr 都 'C2',cefr_inferred=true,cefr_source='fallback' ✓

设备验证 ✅

  • [x] flutter run 触发 schema v50 → v51 升级(删库重建)
  • [x] Preinstalled v0 → v10(首次 import 路径)
  • [x] Upserted 12291 words in 2771ms
  • [x] FK on_delete: RESTRICT 确认
  • [x] 17 列 schema 含 cefr_inferred / cefr_source / word_tags

Track C:文档同步 ✅ 完成 2026-05-13

  • [x] C1 CLAUDE.md 数字更新 4953 → 12291 + 字段/架构说明同步
  • [x] C2 schema.md 已含 v48/v49/v50/v51 变更摘要(前序 commits 已落地,本步无需追加)
  • [x] C3 decisions.md 加 ADR:
    • 决策16:CEFR 评估改用 LLM 重评 + 正交字段建模
    • 决策17:vocabulary → user 表 FK CASCADE → RESTRICT

关键文件位置(cross-session 备查)

Pipeline 端:
  tools/vocabulary_builder_v3/
    bin/evaluate_cefr.dart                  ← Step 3.7 CLI(已存在,未提交)
    bin/build_all.dart                      ← 主流程
    bin/generate_db.dart                    ← Step 6 SQLite 生成
    bin/enrich_translations.dart            ← Step 3.5 LLM 翻译
    lib/cefr_evaluator.dart                 ← LLM 评估器(已存在,未提交)
    lib/data_merger.dart                    ← 数据合并 + cefr_source 标记
    lib/db_generator.dart                   ← SQLite 生成
    lib/llm_translation_enricher.dart       ← LLM provider 抽象(复用)
    lib/models/vocabulary_entry.dart        ← Entry 模型 + cefrInferred/cefrSource
    lib/wordlist_builder.dart               ← Step 1 Oxford+CEFR-J+freq 合并 → master_wordlist.csv
    config.yaml                             ← LLM provider 配置
    output/master_wordlist.csv              ← Step 1 输出
    output/translated_vocabulary.jsonl      ← Step 3.5 输出(evaluate_cefr 输入/输出)
    output/reading_vocab.db                 ← Step 6 输出

App 端:
  assets/sql/01_create_tables.sql           ← v49 加 cefr_source
  assets/databases/reading_vocab.db         ← 预装库
  lib/config/cefr_levels.dart               ← CefrLevel enum(不动)
  lib/shared/data/database/app_database.dart ← _schemaVersion + 写入逻辑
  lib/features/vocabulary_filtering/
    data/models/vocabulary_item_model.dart  ← Freezed model
    domain/entities/vocabulary_entity.dart  ← Domain entity
  lib/features/vocabulary/data/services/supabase_vocabulary_service.dart
  lib/shared/presentation/widgets/cefr_badge.dart  ← UI "约 X" 主修改点

Edge Function(待调研):
  supabase/functions/...                    ← 兜底词写入逻辑

风险 & 回滚

风险点

  1. evaluate_cefr 全量跑挂:参考 enrich_translations 的崩溃-retry 经验,已在 cefr_evaluator.dart 加 try-catch + 3 次重试 + resume 模式
  2. LLM 输出质量低:Step A1 试水阶段 review 50 词,不满意可调 prompt 或换 provider(Claude Sonnet ~$5)
  3. 预装库版本号撞车:当前 v5,本 plan 升 v6。其他平行任务若也升版本号需协调
  4. Supabase migration 锁表:12291 词 INSERT 不大,但 ALTER TABLE 加列在 production 用户读时可能短暂阻塞,建议低峰期

回滚策略

阶段回滚操作
Step A0 schema 改坏git revert + 删除 .freezed.dart 文件 + build_runner 重生
Step A2 评估结果不满意备份的 translated_vocabulary.jsonl 还原
Step A3 预装库导入有问题降回 preinstalledVocabularyVersion v5,用户端重新导入
Step B1 Supabase 加列失败ALTER TABLE vocabulary DROP COLUMN ...
Step B3 Edge Function 部署失败Supabase Functions 历史版本回滚

Session Resume 备查清单

跨会话恢复时按此 checklist 找到当前位置:

bash
# 1. 看 git status 检查未提交改动
git status --short

# 2. 看 plan 勾选项找到首个未完成步骤
grep -n "^\- \[ \]" docs/plans/cefr-llm-evaluation-and-ui-passthrough.md | head -5

# 3. 验证 schema 状态
grep "_schemaVersion" lib/shared/data/database/app_database.dart
grep "cefr_inferred\|cefr_source" assets/sql/01_create_tables.sql

# 4. 验证 pipeline 状态
grep -n "cefr_source" tools/vocabulary_builder_v3/lib/db_generator.dart

# 5. 检查 evaluate_cefr 输出(如已跑过)
ls -la tools/vocabulary_builder_v3/output/translated_vocabulary.jsonl 2>/dev/null

# 6. 验证预装库版本
grep "preinstalledVocabularyVersion" lib/shared/data/database/app_database.dart

关联

  • memory: ~/.claude/projects/-Users-larry-reading-vocab-helper/memory/
    • MEMORY.md — 主索引
    • project_cefr_orthogonal_dimensions.md — CEFR enum 重构(本 plan 不做,单独立项)
    • feedback_i18n_chinese_only.md — 新功能 i18n 暂只做中文
  • previous plans: docs/plans/vocabulary-supabase-authoritative-10k-pivot.md(10k 词 pivot 的源头计划)
  • CLAUDE.md §跨端 Sync 协议红线 #5b/#5d/#5e/#5f/#6b/#6c — Track B 上传时需遵守