主题
CEFR LLM 评估 + cefr_inferred/cefr_source 透传 + UI 标识
Context
预装库 v5(12291 词)已部署成功(2026-05-12 凌晨)。但 CEFR 数据可信度问题暴露:
| CEFR | Oxford 权威 | CEFR-J 权威 | Google freq 反推(估算) | 估算占比 |
|---|---|---|---|---|
| A1 | 1040 | 74 | 0 | 0% ✅ |
| A2 | 972 | 267 | 0 | 0% ✅ |
| B1 | 1209 | 712 | 0 | 0% ✅ |
| B2 | 876 | 1442 | 0 | 0% ✅ |
| C1 | 548 | 0 | 2233 | 80.3% ⚠️ |
| C2 | 0 | 0 | 4992 | 100% ⚠️ |
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'(这些是其他维度状态,详见 memoryproject_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):- 必须有
pos_definitions(Kaikki 找得到结构化定义) - 至少一个 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 用isReferencegetter 派生处理
- 数据沉淀,不丢失:
- 2026-05-12 本次部署分流 2047 + 27 = 2074 词(其中
empty_pos_definitions2047 /no_zh_translation27) - 已写到
tools/vocabulary_builder_v3/output/reference_words_candidates.jsonl - 等"演进 4:candidates → reference_words 表"任务消费(详见下文)
- 2026-05-12 本次部署分流 2047 + 27 = 2074 词(其中
- 设计意图:
- 不要把"参考词"塞进 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
- 迁移路径:
vocabulary表加 3 列:cefr_oxford TEXT/cefr_from_freq TEXT/cefr_from_llm TEXT- Pipeline 已有这些数据(在
master_wordlist.csv+translated_vocabulary.jsonl),只需db_generator.dart补写入 + Supabase 表镜像 +1 次性 backfill 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 交叉验证 — 一个模型异常评估时另一个模型大概率不会同样异常
- 实施方案:
- 加 audit 字段(不破坏现有 schema):在
translated_vocabulary.jsonl现有字段基础上加cefr_claude/word_tags_claude - 跑 Claude Sonnet 二轮评估:复用现有 evaluator 框架 + checkpoint,但写入 audit 字段而非 primary
- 对比报告(新 CLI
bin/compare_cefr_evaluations.dart):- 一致率统计(按 CEFR 差距 0/1/2/3 级分布)
- 最大分歧样本列表(≥2 级差)
- 用户报告词的双模型结果
- 合并策略(依据报告决定):
- 选 1 保守:取较高 CEFR(学习者倾向高估难度)
- 选 2 Claude 优先:默认 Claude,DeepSeek 仅 sanity check
- 选 3 一致 + 高分歧 review:一致词保留,差 ≥2 级人工修
- 重新部署预装库(升 v11)
- 加 audit 字段(不破坏现有 schema):在
- 成本:
- 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 候选
- 已沉淀候选数据:
- 设计要点:
- 数据源合并:candidates.jsonl (2074) ∪ vocabulary 中 word_tags ≠ NULL (945) = ~2900-3000 词
- 去重 + 归一:用 lemmatizer 跑一遍(参考红线 #5e),生成
reference_words表归一形式 - 元数据保留:把
cefr_inferred、LLM 评估的 CEFR、word_tags 信息也带进 reference_words 表 - 跨端:reference_words 表已在三端(RVH/Supabase/RB),导入数据三端同步
- vocabulary 主表:被收编到 reference_words 的词从 vocabulary 删除?还是保留双表存在?— 需要决策
- 风险:与"演进 1:CEFR enum 6 值化重构"(PENDING/UNKNOWN/REFERENCE 拆字段)有依赖关系,应先做演进 1 还是直接演进 4,需评估
- 预期工作量:1-2 天(含跨端同步契约验证)
- 关联 memory:
reference_words 扩充(MEMORY pending tasks 段)
Schema 设计 Audit(2026-05-12 复盘发现 — 暂不处理)
在 Step A1.5 之后 Step A2 之前做的整体 vocabulary 表 schema review。发现的 7 个观察点按 "有意冗余(接受)" 和 "待迭代修复" 两类分组,记录在此避免遗忘。本 plan 不动 schema 设计层,所有项标记为"未来演进/不做"。
接受为"有意冗余"(遵守原则 4:三端 schema 一致性优先)
| ID | 字段 | 现象 | 决策 |
|---|---|---|---|
| OBS-1 | audio_local_path | RB / Supabase 端访问不到 RVH 本地路径 | ✅ 保留三端一致,非 RVH 端写 NULL。Track B 上传脚本要显式 NULL 此列 |
| OBS-2 | last_accessed_at | LRU 仅 RVH 本地行为,跨端无意义 | ✅ 保留三端一致,非 RVH 端写 NULL。Track B 上传脚本要显式 NULL 此列 |
| OBS-3 | word_family | v13 起一直为空,等 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-4 | source vs cefr_source 命名冲突(指代"什么的来源"不同维度) | 🟡 Low | 加第 3 个 *_source 字段(如 etymology_source)时 | 把 source 改名 entry_source 或 record_origin;跨端契约影响 |
| OBS-5 | cefr_inferred 可由 cefr_source 派生(冗余) | 🟡 Low | 出现 inferred 与 source 不一致 bug 时 | 选 1:删 cefr_inferred 走 source 派生 选 2:改为 SQLite 3.31+ generated column 当前:代码层手动同步 |
| OBS-6 | Supabase cefr_inferred BOOLEAN ↔ SQLite INTEGER (0/1) 类型转换 | 🟢 Info | Track B1 ALTER TABLE 落地时 | PostgREST 会自动 cast 0/1 ↔ true/false;push/pull 时验证一次即可,已写在 supabase_vocabulary_service.dart |
| OBS-7 | pos_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.sql加cefr_source TEXT列 + 注释 - [x] A0.2
lib/shared/data/database/app_database.dart- [x]
_schemaVersion 48 → 49 - [x]
_insertVocabulary写入cefr_source
- [x]
- [x] A0.3 App 层 model/entity 加字段
- [x]
lib/features/vocabulary_filtering/data/models/vocabulary_item_model.dart加cefrSource: String? - [x]
lib/features/vocabulary_filtering/domain/entities/vocabulary_entity.dart加cefrSource - [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]
- [x] A0.4 Pipeline 加字段
- [x]
tools/vocabulary_builder_v3/lib/models/vocabulary_entry.dart加String? cefrSource - [x]
lib/data_merger.dart:Oxford 词标'oxford',CEFR-J 词标'cefr_j',freq 反推标'freq' - [x]
lib/db_generator.dartSQL CREATE + INSERT 加列 - [x]
bin/build_all.dart/bin/enrich_translations.dart/bin/generate_db.dartJSON 序列化加字段 - [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]
- [x] A0.5 commit
bbedeab:feat(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.sql加word_tags TEXT列 + v50 注释 + Schema版本号 v49→v50 - [x]
assets/sql/02_create_indexes.sql+03_init_data.sqlSchema版本号 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.dart加wordTags: String?(JSON 字符串)+ fromDatabase / toDatabase - [x]
vocabulary_entity.dart加wordTags(同左)+ 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.dart加Set<String> wordTags = {}+ JSON 序列化辅助 - [x]
lib/db_generator.dartCREATE TABLE + INSERT 加 word_tags 列(jsonEncode 写入) - [x]
bin/build_all.dart/bin/enrich_translations.dart/bin/generate_db.dartJSON 读写透传 - [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 词跑完后
--resume0 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(SHA256d09d5978ab...,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/... ← 兜底词写入逻辑风险 & 回滚
风险点
- evaluate_cefr 全量跑挂:参考 enrich_translations 的崩溃-retry 经验,已在 cefr_evaluator.dart 加 try-catch + 3 次重试 + resume 模式
- LLM 输出质量低:Step A1 试水阶段 review 50 词,不满意可调 prompt 或换 provider(Claude Sonnet ~$5)
- 预装库版本号撞车:当前 v5,本 plan 升 v6。其他平行任务若也升版本号需协调
- 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 上传时需遵守