牌组爆炸、复习积压:我从 Anki 弃用边缘爬回来的踩坑实录
【免费下载链接】ankiAnki is a smart spaced repetition flashcard program项目地址: https://gitcode.com/GitHub_Trending/an/anki
如果你从未体会过"打开 Anki 看到 1,200 张到期卡片"的窒息感,恭喜你,这篇踩坑实录或许可以帮你永远不用体会。我的故事并不特殊:从下载第一份共享牌组时的兴奋,到疯狂新建二十多个主题牌组,再到某天清晨发现整个系统已经变成一台失控的"复习永动机"——新卡永远放不完,旧账永远还不清,每天在"刷不完→点简单→更记不住→再次→更多积压"的循环里越陷越深,最终差点删库弃坑。
这篇文章不是劝退指南,而是我从崩溃边缘爬回来的完整复盘:前兆怎么识别、积压为什么会滚雪球、以及基于 Anki 源码机制设计的三步止血与长期治理方案。所有结论都锚定在 Anki 仓库的真实实现上,希望能帮你避开我踩过的每一个坑。
一、牌组爆炸的三个前兆
回头看,牌组爆炸从来不是一夜之间发生的,它有三个非常清晰的前兆信号。
前兆一:每天都在"插队"的新卡
Anki 每次构建学习队列时,并不是"今天该看什么就取什么"这么简单。在队列构建器 rslib/src/scheduler/queue/builder/gathering.rs 中,收集顺序是这样的:先收日内学习卡,再收到期学习卡与到期复习卡,最后才收集新卡:
pub(super) fn gather_cards(&mut self, col: &mut Collection) -> Result<()> { self.gather_intraday_learning_cards(col)?; self.gather_due_cards(col, DueCardKind::Learning)?; self.gather_due_cards(col, DueCardKind::Review)?; self.gather_new_cards(col)?; Ok(()) }真正的问题不在"新卡排最后",而在于配额。默认配置下,Anki 每天放行的新卡数量是有限的(对应 proto/anki/deck_config.proto 里的new_per_day字段),但当你不断新建牌组、不断导入内容,而某个牌组的"插入顺序"又设成了随机或按牌组优先时,新卡会以你意想不到的方式"插队"进队列。更隐蔽的是new_mix选项——新卡可以被混在复习卡中间(REVIEW_MIX_MIX_WITH_REVIEWS),也可以排在复习之前(REVIEW_MIX_BEFORE_REVIEWS)。如果你为了"先把新内容过一遍"把新卡排在前面,那么每天真正能留给复习的时间就被新卡挤占了,到期卡片越积越多。
我当时的状态是:每天新卡 +20,复习 -20,看起来收支平衡,实际上新卡每天都在挤压复习配额——这就是牌组爆炸的第一个前兆:新卡永远在"插队"。
前兆二:一张笔记,长出三张卡
第二个前兆是我当时完全没察觉的:笔记与卡片的 1:N 关系。在 Anki 的数据模型里,一张笔记(Note)可以对应多张卡片(Card),取决于笔记类型模板的数量。一个"单词"笔记如果挂了三个模板(正面释义、拼写、例句填空),它就会变成三张卡。
这本身不是坏事,问题出在"兄弟卡片"的互相遮蔽。在队列构建的遮蔽逻辑 rslib/src/scheduler/queue/builder/burying.rs 中,如果开启bury_new/bury_reviews,同一张笔记下的兄弟卡片会被隐藏起来:
self.context .seen_note_ids .entry(card.note_id()) .and_modify(|entry| { previous_mode = Some(*entry); entry.bury_new |= new_mode.bury_new; entry.bury_reviews |= new_mode.bury_reviews; entry.bury_interday_learning |= new_mode.bury_interday_learning; }) .or_insert(new_mode);表面上看,埋没兄弟卡是在"保护"你——同一天不重复刷同一知识点的多张卡。但它带来一个经典陷阱:你每天刷完了"今天的队列",以为自己复习了 50 个知识点,实际上很多只是被埋没了,真正的到期卡根本没有出现。等到埋没解除的那天,你会发现涌出来的是两倍、三倍的卡片。我当时就是在这种"虚假的清零感"中不断加牌组、不断导新卡,直到某天埋没解除,积压数字直接翻倍。
前兆三:从共享牌组下载的"大礼包"
第三个前兆最致命:共享牌组。下载一个 5,000 张卡的"考研词汇大礼包",感觉像捡到了宝藏,但这份宝藏的每一张卡都是未来某天的债务。社区里关于"下载牌组"的讨论几乎一致地指出:共享牌组是结构性缺陷的重灾区——卡片粒度不统一、干扰项设计粗糙、大量低质量卡最终变成永久滞涨的"僵尸卡"。
更麻烦的是嵌套牌组的配额传导。Anki 的限额是按牌组树逐层递减的(decrement_deck_and_parent_limits会同时扣减父牌组配额),一个"大礼包"如果被拆成几十个子牌组,每层都在争抢配额,队列构建时甚至会出现子牌组抢光父牌组配额、导致其他牌组当天颗粒无收的情况。牌组爆炸的第三个前兆,就是你的牌组树里躺着超过三个"此生不可能刷完"的巨型牌组。
二、复习积压的滚雪球机制
如果说牌组爆炸是"入口失控",复习积压就是"出口堵塞"。理解积压的滚雪球机制,必须先看 Anki 是怎么决定"先刷哪张卡"的。
逾期优先:越欠越多,越排越前
到期复习卡的收集顺序由review_order决定,proto/anki/deck_config.proto 中枚举了十多种排序策略:按到期日、按牌组、按间隔升/降序、按简洁度、甚至按"相对逾期程度"(REVIEW_CARD_ORDER_RELATIVE_OVERDUENESS)。默认情况下,逾期越久的卡优先级越高——这本来是合理的,但它的副作用是:只要你缺席一天,第二天出现在你面前的永远是"最烂的账",而那些本可以轻松刷掉的近期待复习卡被不断往后推。
于是积压有了第一个自我强化的特征:一旦开始拖欠,你面对的不再是"均匀分布的复习任务",而是一条越来越长的"逾期长尾"。刷完今天新增的到期卡,昨天的逾期卡还在等你;清掉昨天的,前天的又沉底了。队列永远是满的,你永远在还债,且利息——以逾期天数——持续增长。
点"再次"的代价:每张失误卡都变成三次债
积压的第二个引擎是"再次(Again)"按钮。在 rslib/src/scheduler/states/review.rs 中可以看到,每点一次 Again,卡片的间隔会被压缩、进入重学队列,同时失误次数(lapses)+1:
fn answer_again(self, ctx: &StateContext) -> CardState { let lapses = self.lapses + 1; let leeched = leech_threshold_met(lapses, ctx.leech_threshold); ... ease_factor: (self.ease_factor + EASE_FACTOR_AGAIN_DELTA).max(MINIMUM_EASE_FACTOR),注意一个残酷的数字:EASE_FACTOR_AGAIN_DELTA = -0.2。每点一次 Again,这张卡未来复习的间隔上限就被永久削减 20%。我当时的典型操作是:积压太多 → 时间不够 → 点"简单(Easy)"赶进度 → 大脑根本没记牢 → 下次到期直接 Again → 间隔被压缩 → 更频繁地出现在队列里。一次"赶进度",换来的是未来三到四次的复习债。
更隐蔽的是 rslib/src/scheduler/answering/mod.rs 里的 leech 机制:当一张卡的失误次数达到阈值(默认 8 次,见 rslib/src/deckconfig/mod.rs 中的leech_threshold: 8),它会被标记为 leech,而如果配置了"挂起(Suspend)",这张卡会直接从队列中消失——这不是清除,而是隐藏。被挂起的卡不再出现在统计里,你的"待复习数"因此显得没那么可怕,但它们并没有消失,只是变成了未来某次清理时才会浮出水面的暗礁。
FSRS 时代的积压新变量:期望保留率
如果你已经切到 FSRS(Free Spaced Repetition Scheduler),积压的第三个引擎是"期望保留率(desired retention)"。FSRS 的核心思想是:不预设固定间隔,而是基于记忆稳定性模型,为每个目标保留率计算最优间隔。目标保留率设得越高(默认 0.9),系统就越"保守",间隔越短、复习频率越高、每日负荷越大。
在 rslib/src/scheduler/fsrs/retention.rs 中,Anki 甚至内置了一个"最优保留率"计算器,通过模拟未来若干天的复习流程求解目标值,并把结果收敛在 0.70 ~ 0.95 之间:
Ok(fsrs::optimal_retention( &config, &req.params, ... )? .clamp(0.7, 0.95))这组数字本身就是个信号:0.95 的保留率几乎必然带来不可持续的每日负荷。如果你的积压数字像我当时一样直逼四位数,先别急着怪自己意志力差——先想想是不是把期望保留率设到了不现实的档位。
三、我的止血三步与长期治理方案
从崩溃边缘爬回来的过程,我把它抽象成三步:止血、清账、重建系统。每一步都有对应的源码机制支撑。
止血第一步:停掉新卡流入
止血的第一动作不是"多刷一点",而是"不再新增"。把新卡配额new_per_day直接设为 0,等于从源头切断了插队入口。这一步在 rslib/src/scheduler/queue/builder/gathering.rs 中的效果很直观——新卡收集阶段检查limit_reached(deck_id, LimitKind::New),配额为 0 时新卡一张都不会进队列,复习队列反而获得完整的配额窗口。
同时建议检查"新卡忽略复习上限"开关:如果开着它,即使复习配额已满,新卡仍会强行入队。止血期务必关掉,让"复习优先"成为唯一原则。
止血第二步:用数据调参,而不是硬扛
止血期最有效的参数调整有三个,全部有源码依据:
调整期望保留率。在 rslib/src/scheduler/fsrs/retention.rs 里跑一次最优保留率模拟,或手动把
desired_retention从 0.9 降到 0.85 甚至 0.8。代价是长期记忆率轻微下降,收益是每日复习负荷显著下降——对处于积压漩涡中的人来说,后者远比前者重要。重新优化 FSRS 参数。Anki 会把你的历史复习记录(revlog)喂给训练器重新拟合参数,训练逻辑在 rslib/src/scheduler/fsrs/params.rs。注意它要求足够的数据量:代码注释明确说明"少于 400 条复习记录时不会直接报错,需要由调用方自行判断结果可信度"。所以如果历史记录不够,优先攒数据,别急着调参。优化后还有个健康检查(health check),只有当调整后的模型损失显著优于当前参数时才会采纳新参数——这保证了"越调越差"的情况不会发生。
处理 ignore 日期。如果你的 revlog 里混着早期不科学的复习史(比如我下载大礼包后的无脑 Easy 阶段),可以设置"忽略某日期之前的复习记录",让 FSRS 从靠谱的时间点重新起算记忆状态。
清账第三步:给积压卡分诊,而不是蛮干
积压数字摆在那里,靠每天硬刷是清不完的——我试过,三天后情绪就崩了。真正的清账手段是 Anki 的"自定义学习(Custom Study)"机制,入口逻辑在 rslib/src/scheduler/filtered/custom_study.rs:
- 找回"忘记"的卡:
ForgotDays选项会把你指定天数内点过 Again 的卡拉出来重刷,其搜索条件正是"按按钮评分=1 的卡":
fn forgot_config(deck_name: String, days: u32) -> FilteredDeck { let search = SearchNode::Rated { days, ease: RatingKind::AnswerButton(1), } .and(SearchNode::from_deck_name(&deck_name)) .write(); ... }这比我当时"把所有过期卡全部重排"温和得多——它只针对真正没记住的卡。
预览(Preview):把最近新加入的卡集中过一遍,不做进度改动,只做"混个脸熟",防止新卡积压成未来的逾期债务。
建立过滤牌组做"分诊":用搜索条件把积压卡分成"救得回来的"和"该放弃的"。过滤牌组构建时默认排除挂起与埋没的卡(
-is:suspended -is:buried),这意味着你分诊出来的每一张卡都是真实需要决策的对象。我当时做了三件事:真正有价值的卡留下并调整间隔;反复 Again 的 leech 卡直接挂起——与其让它们每天出现在队列里消耗意志力,不如先冷冻起来;低质量共享牌组的卡批量删除。删除不是失败,是回收自己的注意力。
长期治理:把 Anki 当成系统来运维
清账之后,我给自己立了几条不再破防的规矩,每一条都能在源码里找到对应:
- 新卡配额用"可完成"而非"可接受"的标准。每天只放行自己确定能刷完的数量,宁可保守。配额字段就在 proto/anki/deck_config.proto 的
new_per_day/reviews_per_day,改一次成本极低。 - 默认关闭"新卡插队"。把
new_mix设为"复习之后",新卡永远只能出现在复习完成后的空隙里,从机制上杜绝插队。 - 理解埋没,善用埋没。兄弟卡埋没不是 bug,是防重复机制;但要意识到"今日清零"不等于"任务清零",把统计页的预测曲线当作真实工作量看待。
- 定期体检 leech 清单。默认 8 次失误阈值会持续生产 leech 卡,rslib/src/scheduler/answering/mod.rs 中挂起动作只是默认策略之一。对长期治理来说,leech 是信号:要么卡片粒度太粗,要么释义太差,要么就是该删的卡——诊断卡,而不是跟它死磕。
- 让 FSRS 参数优化成为季度例行。数据攒够后定期重训,让模型贴合自己真实遗忘规律,而不是永远用一份通用的初始参数。
回头看,我最深的教训不是"没用对工具",而是把 Anki 当成内容仓库而不是记忆系统:无节制地导入,无规划地放行,无原则地赶进度。Anki 的队列、配额、遮蔽与 FSRS 模型都不是摆设——它们是一整套节流阀,唯一的开关在你的设置里。牌组爆炸与复习积压,本质上都是"入口流量超过出口处理能力"的系统性失衡。
如果你此刻正被红色数字压得喘不过气:停掉新卡,调低一点期望保留率,给积压卡分诊,然后接受"清账需要以周为单位"的节奏。这个从弃用边缘爬回来的过程,本身就是一次关于自我认知与自我管理的复利——而这一次,复利的方向是好的。
【免费下载链接】ankiAnki is a smart spaced repetition flashcard program项目地址: https://gitcode.com/GitHub_Trending/an/anki
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考