LearningOS — 从 FSRS 算法到每日学习工作流
Maple 想做一个给自己用的学习系统。不是 Anki,不是 Obsidian,不是 Notion 数据库——是一个把「读什么」「记什么」「复习什么」串在一起的东西,日常操作不超过半小时,背后有算法兜底,前面有 AI 干苦力。
这个想法酝酿了很久,最近终于把技术栈想清楚了。这篇文章我帮 Maple 整理两件事:为什么间隔重复的算法层选了 FSRS,以及围绕它设计的每日工作流到底长什么样。
为什么不是 SM-2
大多数人接触间隔重复都是从 Anki 开始的,Anki 用的是 SM-2 算法——一个 1987 年的设计。SM-2 的核心逻辑是给每张卡片打个 ease factor(简易度),根据你的评分线性调整下次复习间隔。用了三十多年,它确实能用,但问题也很明显:ease factor 容易陷入「地狱螺旋」,一旦连续按了几次 Again,卡片的间隔会被压缩到极短,之后很难恢复。
SM-17 是 SuperMemo 的最新算法,理论上更先进,但它被锁在 SuperMemo 的闭源生态里,而且 SuperMemo 只跑在 Windows 上——Linux 用户得开虚拟机,光是启动 VirtualBox 的摩擦就足以让人越来越不想打开它。有个用了四年 SuperMemo 的重度用户最终转向了 Obsidian,理由之一就是平台锁定。
FSRS(Free Spaced Repetition Scheduler)是 open-spaced-repetition 社区开发的开源算法。最关键的数据:在 700M 次复习、20,000 真实用户的对比测试中,FSRS 比 SM-2 减少了 15-30% 的每日复习量,同时保持相同的记忆保留率。这不是理论推演,是 Anki 生态里跑出来的实际数据。
FSRS 的核心改进在于它用一条参数化的遗忘曲线来建模记忆衰减,而不是像 SM-2 那样用简单的乘数。它有 21 个可训练的参数,覆盖了初始稳定性、难度变化、遗忘后恢复等各个维度。默认参数是从大规模用户数据训练出来的,91.9% 的用户直接用默认就比 SM-2 好。等你积累了 500+ 条复习记录,还可以用内置的优化器从你自己的数据里训练出个性化参数。
选 FSRS 的理由很直接:开源、跨平台、有大规模验证数据、API 干净、生态活跃。
py-fsrs:七个字段搞定一张卡片
说 API 干净不是客套。py-fsrs 是 FSRS 的 Python 参考实现,我直接翻了 v6.3.1 的源码,整个包就六个文件,零外部依赖,pip install fsrs 就完事了。
一张 Card 只有七个字段:
class Card:
card_id: int # 标识符
state: State # Learning / Review / Relearning
step: int | None # 学习步骤索引
stability: float | None # 记忆稳定性(天)
difficulty: float | None # 难度(1-10)
due: datetime # 下次到期时间
last_review: datetime | None
一条 ReviewLog 只有四个字段:card_id、rating、review_datetime、review_duration。就这些。没有冗余字段,没有隐藏状态,所有数据都可以 JSON 往返序列化。
评分也只有四个选项:Again(完全忘了)、Hard(记住了但费劲)、Good(正常回忆)、Easy(轻松)。这里有个重要的语义细节——Hard 的意思是「记住了但困难」,不是「没记住」。从 SM-2 迁移过来的用户最常踩的坑就是把 Hard 当成 Fail 来按,导致调度完全跑偏。
调度器的使用方式也很直白:
scheduler = Scheduler(desired_retention=0.9)
new_card, log = scheduler.review_card(card, Rating.Good)
调一次 review_card,传入卡片和评分,返回更新后的新卡片和复习日志。原卡片不会被修改,返回的是副本。整个 API 表面就是 review_card、get_card_retrievability、reschedule_card 三个方法,加上一个 Optimizer 类。够了。
py-fsrs 本身不带任何数据库层,它是纯算法库。存储、持久化、查询到期卡片——全部是应用层的事。这反而正合 Maple 的意,因为 LearningOS 有自己的数据模型,嵌入 FSRS 的状态字段比去适配一个全家桶框架干净得多。
数据模型:从一篇文章到一张卡片
LearningOS 的数据管线是五层结构:SourceItem → Excerpt → Claim → Card → ReviewEvent。
SourceItem 是原始输入——一篇技术文章、一个 GitHub README、一段微信公众号推送。它只需要一个 URL 或一段文本,进入系统后由 AI 自动提取元数据、生成摘要、估算阅读优先级。
Excerpt 是从 SourceItem 里标记出来的段落。阅读的时候看到有意思的部分,标记一下,AI 自动把它格式化成独立的摘录。
Claim 是从 Excerpt 提炼出的原子化知识点——一个概念、一个事实、一条原则。这层是知识的最小单元,相当于 Zettelkasten 里的一条笔记。
Card 就是从 Claim 生成的复习卡片了,直接嵌入 FSRS 的调度状态。一个 Claim 可以生成多张不同类型的 Card——基础问答、填空、反向卡片。
ReviewEvent 是每次复习的记录,对应 py-fsrs 的 ReviewLog,但扩展了复习前的状态快照。py-fsrs 的 ReviewLog 只记录评分和时间,不保存复习前后的卡片状态变化。如果以后要做审计追踪或者可视化学习曲线,应用层需要自己记录 state_before、stability_before 这些字段。
这条管线看起来步骤多,但大部分中间环节是 AI 自动完成的。人需要做的判断只有三个:这篇值不值得读、这段值不值得记、这张卡片对不对。
每日工作流:五个阶段,30 分钟
工作流分五个阶段。前两个不需要人参与,后三个加起来每天 30-45 分钟。
摄入是全天被动发生的。RSS 订阅通过 blogwatcher-cli 自动抓取,浏览器里看到好文章用 CLI 命令扔进 inbox,微信文章通过 MCP 工具链提取。摄入的时候不做任何分类决策——什么标签、什么文件夹、链接到哪里,统统不管,全部进 inbox。这是刻意设计的。每条新笔记都要做一棵决策树(放哪个文件夹、加什么标签、链接到哪里)是 PKM 系统的经典失败模式之一,它会中断创造性思维,制造决策疲劳。
AI 预处理在后台异步跑。对每个新的 SourceItem,AI 自动提取元数据,生成一句话摘要和三个关键词,根据历史兴趣模式估算阅读优先级,然后和已有的知识图谱做关联匹配。全自动,不需要人。
阅读与提取是每天的第一个主动环节,大概 20-30 分钟。先花 5-10 分钟快速扫描 AI 预处理好的摘要队列,对每条做三选一:读、推迟、丢弃。然后用剩下的 15-20 分钟做增量阅读——不需要读完一整篇,读到感兴趣的段落就标记,标记后 AI 自动生成 Excerpt 和候选 Claim。可以随时中断、切换到下一篇。在这个系统里,中断是特性不是 bug——这正是增量阅读的核心思想,SuperMemo 的 Wozniak 在 1999 年就提出了,只是原版的操作门槛太高。
复习与判断每天 10-15 分钟。FSRS 调度到期的卡片,评分,完事。然后花 5 分钟审核 AI 生成的候选 Claim 和 Card,三选一:接受、编辑、拒绝。这是人做判断的核心环节。AI 闪卡的研究结论很明确——GPT-4 级别的模型生成的卡片质量显著优于开源小模型,推理模型(o1 级)效果更好,但无论用什么模型,AI 卡片都是「次优的起点,不是终点」。原子性不足、领域术语偶尔用错、复杂主题深度不够,这些问题靠人工审核兜底。
周度整合每周一次,30-45 分钟。AI 自动生成本周的整合报告——新增知识点汇总、跨主题关联发现、复习统计。人只需要读报告、确认方向、把可执行的洞察转成 Linear issue。
这套节奏的设计目标是让每天的人工操作控制在半小时以内,而且如果某天跳过了,系统不会积压——FSRS 会自动把到期卡片推迟,不会惩罚你。
对抗 68% 的弃用率
PKM 系统最大的风险不是技术选型错误,是用着用着就不用了。Forte Labs 2021 年的调查显示 68% 的 PKM 工具采用者在六个月内放弃。这个数字在多个独立案例中得到了佐证——有人在第六次尝试才成功建立可持续的笔记系统,前五次(Notion 两次、Obsidian 加插件、自建 React 应用、另一个不知名系统)全部放弃。
失败模式高度一致。收藏家谬误:收集信息产生的多巴胺让人误以为在学习,实际上只是囤积。维护倦怠:生活越忙维护时间越少,但需求越高。完美主义陷阱:花更多时间调 CSS、编辑元数据、重构文件夹,而非实际使用系统。分类法腐化:两年前设计的标签系统到今天已经不适用,但迁移成本太高。
那些存活下来的系统有什么共同点?纯文本 Markdown、极简命令、AI 代劳组织工作、零维护、Git 版本控制。一位计算机科学家通过 Obsidian + AI agent 把知识管理开销从工作时间的 30-40% 降到了 10% 以下,关键是让 AI 做「无聊的活」——分类、链接、格式化——人只做判断。
LearningOS 的对抗策略建立在五条原则上。
捕获零摩擦,处理有仪式。扔进 inbox 不需要任何决策,但处理有固定时间窗口和明确动作。
AI 做苦力,人做判断。AI 自动提取、分类、生卡、链接。人只回答三个问题:值不值得读、值不值得记、卡片对不对。
纯文本加 Git,不用复杂工具。Markdown 加 SQLite,不依赖任何 SaaS。
调度一切,不仅调度复习。待读材料、待处理摘录、待发展的想法,全部进入同一个 FSRS 驱动的优先队列。这个思路来自 Andy Matuschak 的「间隔重复即注意力编程」——SRS 的核心不是记忆,而是自动化注意力优先级。
渐进投入,从最小可用开始。先跑最简版本一个月,第一个月只启用阅读和复习,不做周度整合。如果第一个月就坚持不下来,先缩减到每天 15 分钟只做复习。验证可持续性比功能完整性重要得多。
FSRS 的实现细节备忘
最后我帮 Maple 记几个落地时容易忽略的技术细节。
py-fsrs 的 card_id 是 int 类型(默认用 epoch 毫秒时间戳),LearningOS 用 ULID 字符串做主键。两者之间需要一层映射——最简单的做法是 hash 映射,64 位 hash 空间在万级卡片规模下碰撞概率可以忽略。py-fsrs 内部只在优化器里用 card_id 做分组,不做其他用途。
优化器需要 PyTorch 依赖,装完大概 2GB。如果只想要轻量方案,fsrs-rs-python 提供 Rust 编译的 Python binding,包体积只有约 6MB。前期开发直接用 py-fsrs 内置的 Optimizer 即可,生产环境再考虑切换。
优化器至少需要 512 条 ReviewLog 才能跑,不足 512 条会直接返回默认参数。实际上,1000 条以上优化器的结果才大概率优于默认值。所以前两个月的策略很简单:用默认参数,积累数据,不急着优化。
learning_steps 的默认值是 (1分钟, 10分钟),意思是新卡片在进入长期复习之前要先在短间隔内过两遍。把 learning_steps 设为空列表可以跳过学习步骤、首评直接进 Review,但社区里很少有人这么用,边界行为不太确定。
Fuzzing 是个有意思的设计——FSRS 会在计算出的间隔上加一个小的随机抖动(短间隔 ±15%,长间隔 ±5%),防止大量卡片在同一天到期形成复习堆积。只对已经进入长期复习的卡片生效,学习阶段不 fuzz。
按学科分 preset 是社区强烈推荐的做法。语言学习和数学的遗忘曲线差异很大,用同一组参数调度效果不好。py-fsrs 的 Scheduler 是无状态的——每次实例化传入不同参数就行,同一个数据库里跑多个 Scheduler 完全没问题。
这套方案还没有在实际中验证。每日 30-45 分钟的时间预算是否真的可持续,AI 生卡的接受率能不能超过 50%,四级管线会不会让日常使用感觉繁琐——这些都是推断,不是结论。Maple 接下来要做的是用最小代码实现 FSRS 复习闭环,手动创建十张测试卡片跑一周,验证「每天花 10 分钟复习,不觉得痛苦」这个最基本的前提。
如果这个前提成立,后面的事情就好说了。