FlashCard 智能学习系统设计(MVP版)¶
文档目标¶
重新梳理FlashCard系统设计,聚焦MVP目标:跑通完整的学习流程。算法优化、策略配置等留待后续迭代。
一、系统概述¶
1.1 核心定位¶
基于间隔重复(Spaced Repetition)的智能卡片学习系统,通过多维度能力评估帮助用户高效掌握词汇。
1.2 MVP核心特性¶
- ✅ 动态卡片生成:根据用户学习状态实时生成卡片
- ✅ 多题型支持:先实现3种核心题型(CHOICE, SPELLING, SELECT_WORDS)
- ✅ 多维度追踪:分别追踪听、读、拼、说四项能力
- ✅ 间隔重复调度:根据答题表现自动调整复习时间
- ✅ 客户端判题:答案随卡片下发,客户端即时反馈,服务端批量评分
1.3 MVP不包含的功能¶
- ❌ 可配置的学习策略(ReviewPlanConfig)
- ❌ 智能干扰项生成(MVP使用完全随机选择)
- ❌ 复杂的算法优化(如FSRS)
- ❌ 语音评分(SPEAKING题型)
- ❌ 复杂题型(ARRANGE、MATCHING,先实现3种核心题型)
二、核心概念¶
2.1 多维度掌握度(MasteryBreakdown)¶
proto定义:learning/v1/learning.proto
message MasteryBreakdown {
int32 listening = 1; // Listening mastery (0-5)
int32 reading = 2; // Reading mastery (0-5)
int32 spelling = 3; // Spelling mastery (0-5)
int32 speaking = 4; // Pronunciation mastery (0-5)
int32 overall = 5; // Overall mastery score (0-500, stored as *100)
}
Overall 计算公式(MVP固定权重):
2.2 题型与能力映射¶
proto定义:learning/v1/flashcard.proto(已完整定义6种CardType)
| 题型 | 主训练能力 | 次要能力 | MVP实现状态 |
|---|---|---|---|
| CHOICE | Read | - | ✅ MVP实现 |
| SPELLING | Listen, Spell | - | ✅ MVP实现 |
| SELECT_WORDS | Read, Spell | - | ✅ MVP实现 |
| ARRANGE | Read | - | ⏳ 后续实现 |
| MATCHING | Read | - | ⏳ 后续实现 |
| SPEAKING | Pronounce, Listen | - | ❌ 暂不实现 |
2.3 间隔重复核心参数(MVP固定值)¶
InitialInterval = 1 天 // 首次复习间隔
PassingScore = 0.6 // 及格线(60%)
EasyBonus = 1.5 // 简单题奖励因子
HardPenalty = 0.5 // 困难题惩罚因子
MaxInterval = 180 天 // 最大间隔
三、MVP核心流程¶
3.1 完整学习流程(时序图)¶
用户 客户端 服务端
| | |
|--[1. 开始学习]-------->| |
| |--GetFlashCards(planId)--->|
| | | 查询待复习单词
| | | 生成卡片+答案
| |<--FlashCardSet------------|
| | |
|<--[2. 展示题目]--------| |
|--[3. 答题]----------->| |
|<--[4. 即时反馈]--------| |
| | (本地存储答题记录) |
| | |
|--[5. 完成/退出]------->| |
| |--SubmitAnswer(批量结果)-->|
| | | 1. 接收 AnswerResult (lword_id, type, correct)
| | | 2. 根据 CardType 查找评分策略
| | | 3. 更新对应维度的 Mastery
| | | 4. 计算下次复习时间
| |<--ACK---------------------|
|<--[6. 查看报告]--------| |
3.2 服务端:生成卡片 (GetFlashCards)¶
设计模式:批次获取 (Batch Fetching)
- 本接口不支持传统分页 (Offset/Page),因为复习队列是动态变化的。
- 客户端应采用 "做完一批取一批" 的模式。
- 当用户完成当前批次并提交后,已复习的词不再满足 next_review_at <= now 条件,自然会从队列中消失。下一次请求将自动获取后续的词。
输入:
- review_plan_id: 复习计划ID
- limit: 本次需要的卡片数量(建议 10-20 张,避免一次生成过多浪费资源)
核心逻辑:
1. 加载ReviewPlan关联的所有Wordbook
2. 查询用户的LearnedWords
3. 分类单词:
- 到期复习词:next_review_at <= now 且 overall > 0
- 新词:overall == 0
4. 优先级排序(到期词):
- 优先级1: fail_count 降序(失败越多越优先)
- 优先级2: overall 升序(掌握度越低越优先)
- 优先级3: next_review_at 升序(逾期越久越优先)
5. 选择单词:
- 优先填充到期词
- 不足时补充新词(随机打乱)
- 限制总数为 limit
6. 为每个单词生成卡片:
- 选题型:根据最弱能力选择
- listen 最弱 → SPELLING(听写)
- read 最弱 → CHOICE(选择)
- spell 最弱 → SELECT_WORDS(填空)
- pronounce 最弱 → CHOICE(MVP阶段降级为选择题)
- 生成内容:题目、选项、答案
- 干扰项:从同Wordbook完全随机选择其他单词(不做任何过滤)
7. 打乱卡片顺序
8. 返回 FlashCardSet
输出(proto定义见 review_service.proto):
message FlashCardSet {
repeated FlashCard flash_cards = 2;
FlashCardStats stats = 3; // 包含实时统计信息
}
message FlashCardStats {
// Fixed totals for progress calculation
int32 today_due_total = 3; // 今日到期词总数(固定)
int32 today_new_total = 6; // 新词配额(固定)
// Remaining tasks (dynamic)
int32 today_due_remaining = 7; // 剩余到期词数
int32 today_new_remaining = 1; // 剩余新词配额
// Progress
int32 today_reviewed_count = 4; // 今天已复习的卡片数
// Other
int32 estimated_minutes = 5; // 完成本批次的预计时间(分钟)
reserved 2; // 已删除: review_words
}
**客户端进度条计算公式**:
```javascript
// 总任务 = 到期词总数 + 新词配额(都是固定值)
const totalTask = stats.today_due_total + stats.today_new_total;
// 当前进度 = 今天已完成
const currentProgress = stats.today_reviewed_count;
// 渲染
ProgressBar.render(currentProgress, totalTask); // 例如: 50/120
### 3.3 客户端:答题与本地存储
**职责**:
- ✅ 渲染题目UI
- ✅ 收集用户答案
- ✅ 基于下发的answer即时反馈对错
- ✅ 本地存储答题进度(localStorage/IndexedDB)
- ✅ 批量提交答题记录
- ✅ **错题重练机制**:当用户答错时,客户端**不应立即提交**,而是将该卡片重新插入待答队列末尾(Re-queue),直到答对为止。
**本地数据结构**(参考):
```javascript
{
reviewPlanId: 123,
startedAt: "2025-01-04T10:00:00Z",
cards: [...], // 完整卡片数据
answers: [ // 已答题记录
{
cardId: "xxx",
term: "apple",
userAnswer: {...},
timeSpent: 5,
answeredAt: "...",
clientIsCorrect: true // 客户端判断(仅供参考)
}
],
currentIndex: 1,
status: "in_progress"
}
提交策略:
- 同一个词在本次 Session 中可能被回答多次(先错后对)。
- 最终提交规则:只要该词在本次 Session 中出现过错误,SubmitAnswer 时标记为 correct=false(或低分)。只有一次性答对的词,才标记为 correct=true。
中途退出处理:
- 监听 beforeunload 事件自动提交已答题目
- 下次打开时提示"继续上次的练习?"
3.4 服务端:接收分数与更新 (SubmitAnswer,单题异步)¶
输入(proto定义见 learning/v1/review_service.proto):
- SubmitAnswerRequest: 包含 review_plan_id、AnswerRecord
- AnswerRecord: 单条答题记录,包含 lword_id、AnswerScore(客户端计算的分数,0-10 刻度)、time_spent_seconds、answered_at
- AnswerScore: listening/speaking/reading/writing 四项能力分数,范围 0-10
核心逻辑:
1. 依赖客户端判题,直接接收分数(0-10);建议配合 client_answer_id 做幂等以避免重试重复累计。
2. 分数归一化与能力映射(示例,可在实现时调整权重):
- normalized = score / 10.0
- 针对不同能力,计算 mastery_delta = normalized * weight,并限制最终 mastery 在 [0,5]
- overall = avg(listen, read, spell, pronounce) * 100
3. 更新 LearnedWord:
- 更新 mastery / overall
- 依据 normalized 分数更新 review_timing(见 3.5)
4. 更新 DailyStats(本日学习统计,单题累加)
5. 当前 RPC 返回 google.protobuf.Empty;如需前端同步掌握度,可在后续扩展反馈结构
3.5 间隔重复算法(MVP简化版)¶
输入:
- 当前 interval_days(首次为0)
- 本次 score_normalized(0-1,客户端 0-10 分数 / 10 得到)
算法:
1. 确定当前间隔:
currentInterval = interval_days > 0 ? interval_days : InitialInterval(1天)
2. 根据得分选择因子:
- score_normalized >= 0.9 → easeFactor = 2.0 (非常熟练,大幅延长)
- score_normalized >= 0.6 → easeFactor = 1.5 (及格,正常延长)
- score_normalized < 0.6 → easeFactor = 0.5 (不及格,缩短)
3. 计算新间隔:
newInterval = ceil(currentInterval * easeFactor)
newInterval = clamp(newInterval, 1, 180)
4. 更新失败计数:
- score_normalized >= 0.6 → fail_count = 0
- score_normalized < 0.6 → fail_count++
5. 失败惩罚:
if fail_count >= 3:
newInterval = InitialInterval
所有能力值 -= 1.0(最低为0)
6. 计算下次复习时间:
next_review_at = now + newInterval * 24小时
关键点: - MVP使用简化的SM-2算法 - 固定的得分-因子映射关系 - 连续失败3次触发降级
四、API定义¶
4.1 Proto定义¶
所有API定义已实现在 api/proto/learning/v1/review_service.proto,包括:
核心RPC:
- GetFlashCards: 获取复习卡片
- SubmitAnswer: 单题提交答题记录(客户端判题,0-10 刻度上报)
关键Message:
- FlashCardSet: 卡片集合,包含flash_cards和stats统计信息
- FlashCardStats: 卡片统计(固定总数:到期词总数、新词配额;动态数据:剩余到期词、剩余新词配额、已复习数;预估时长)
- SubmitAnswerRequest: 提交答题请求(单条)
- AnswerRecord: 单条答题记录,含 AnswerScore(0-10)与 time_spent_seconds
详细定义请参考proto文件。
五、数据库设计¶
5.1 核心表(已有)¶
learned_words(对应 LearnedWord entity):
- id, user_id, term, language
- mastery_listen, mastery_read, mastery_spell, mastery_pronounce, mastery_overall
- last_review_at, next_review_at, interval_days, fail_count
- queried_count, created_at, updated_at
review_plans(对应 ReviewPlan entity):
- id, user_id, name, description
- pending_words, mastered_words, learning_words, unknown_words
- created_at, updated_at
review_plan_wordbooks(关联表):
- review_plan_id, wordbook_id
5.2 需要新增的表¶
daily_stats(每日学习统计):
CREATE TABLE daily_stats (
id BIGSERIAL PRIMARY KEY,
user_id UUID NOT NULL,
date DATE NOT NULL,
cards_reviewed INT DEFAULT 0,
new_words INT DEFAULT 0,
time_spent_seconds INT DEFAULT 0,
average_score FLOAT DEFAULT 0,
words_mastered INT DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE(user_id, date)
);
用途: - 学习日历 - 连续学习天数统计 - 学习曲线图表
六、关键设计决策(ADR)¶
6.1 客户端判题 + 单题异步提交¶
- 决策:答案随卡片下发,客户端即时反馈并计算 0-10 分,逐题异步上报;服务端不再判题,只做掌握度/调度更新
- 理由:减轻服务端压力,降低网络重试的影响(可配合幂等键),保持离线/弱网体验
- 权衡:完全信任客户端分数;需在服务端定义分数到掌握度/间隔的映射以确保行为一致
6.2 无Session机制¶
- 决策:GetFlashCards不返回sessionID,SubmitAnswer不验证session
- 理由:进度在客户端管理,服务端无状态更简单
- 多设备重复问题:通过立即更新
next_review_at自然解决(设备B不会获取设备A已做的词)
6.3 新词定义¶
- 决策:
mastery.overall == 0即为新词 - 说明:系统评判的掌握度,非用户主观声明
- 初始状态:新收藏的词
overall=0,next_review_at=now
6.4 干扰项生成(MVP)¶
- 决策:从同Wordbook完全随机选择,不做任何过滤
- 理由:最简单实现,快速验证产品流程
- 权衡:可能出现明显错误的选项(如同义词同时出现),但不影响流程验证
- 后续优化:基于词频、词性、排除同义词、用户错误历史等
6.5 例句来源(MVP)¶
- 决策:优先使用
LearnedWord.contexts(用户收藏时的上下文) - 降级方案:如果contexts为空,从
Lexeme表获取 - 后续扩展:AI生成个性化例句
6.6 SPEAKING题型暂不实现¶
- 决策:MVP阶段跳过语音评分
- 理由:需要对接语音识别API,复杂度高
- 后续实现:可对接Google/Azure语音服务
七、MVP实现检查清单¶
7.1 Proto定义¶
- [x] 在
review_service.proto新增SubmitAnswerRequest - [x] 在
review_service.proto新增AnswerRecord,AnswerScore - [x] 在
review_service.proto新增FlashCardStats - [x] 在
ReviewPlanService新增SubmitAnswerRPC
7.2 Entity层¶
- [ ] 新增
DailyStatsentity(ent schema) - [ ]
LearnedWord确保包含 mastery和review相关字段
7.3 Repository层¶
- [ ]
LearnedWordRepository.GetByReviewPlan(planID, dueOnly)- 查询待复习词 - [ ]
LearnedWordRepository.UpdateMasteryAndReview(id, mastery, review)- 更新掌握度和复习时间 - [ ]
DailyStatsRepository.GetOrCreate(userID, date)- 获取或创建本日统计 - [ ]
DailyStatsRepository.Update(stats)- 更新统计
7.4 Usecase层¶
- [ ]
ReviewPlanUsecase.GetFlashCards() - [ ] 实现单词选择逻辑
- [ ] 实现题型选择逻辑(根据最弱能力,只支持CHOICE/SPELLING/SELECT_WORDS)
- [ ] 实现卡片生成逻辑:
- [ ] CHOICE题型生成器
- [ ] SPELLING题型生成器
- [ ] SELECT_WORDS题型生成器
- [ ] 实现干扰项生成(完全随机选择)
- [ ]
ReviewPlanUsecase.SubmitAnswer() - [ ] 将 0-10 分数映射到 mastery/overall
- [ ] 实现间隔重复算法
- [ ] 实现DailyStats更新
7.5 Adapter层¶
- [ ] gRPC handler:
GetFlashCards - [ ] gRPC handler:
SubmitAnswer
7.6 测试验证¶
- [ ] 单词选择优先级测试(到期词优先)
- [ ] 题型选择测试(最弱能力映射)
- [ ] 分数映射测试(0-10 → mastery/overall)
- [ ] 间隔重复算法测试(得分-间隔映射)
- [ ] 掌握度更新测试(增减逻辑)
- [ ] 多设备场景测试(不重复获取)
八、后续迭代方向(非MVP)¶
8.1 算法优化¶
- 使用FSRS替代简化SM-2
- 智能干扰项生成(基于词性、词频、错误历史)
- 难度自适应调整
8.2 题型扩展¶
- 实现ARRANGE题型(排序组句)
- 实现MATCHING题型(配对匹配)
- 实现SPEAKING题型(语音评分)
8.3 策略配置¶
- ReviewPlanConfig(卡片分布、难度策略、每日学习量)
- 用户可自定义学习偏好
8.4 功能扩展¶
- 多语言差异化策略
- 成就系统
- 学习统计可视化