艾宾浩斯复习工具 Recall
写在前面
你有没有过这种经历:背了一上午的英语单词,第二天醒来全忘了?或者学了一个技术点,过两周再看跟第一次见一样?
这不是你记忆力差,是人脑的遗忘曲线决定的。德国心理学家艾宾浩斯早在 1885 年就通过实验发现:人类对无意义音节的记忆保持率在学习后 20 分钟、1 小时、1 天、6 天分别约为 58%、44%、33%、25%。
那有没有办法对抗遗忘?有——间隔重复(Spaced Repetition)。简单说就是"在你快要忘掉的时候刚好复习一下",每次复习之后间隔可以拉长,直到真正进入长期记忆。
市面上已经有 Anki、Quizlet 这类产品,但 Anki 配置太折腾、UI 不够友好,Quizlet 在线使用体验好但离线要翻墙。我想要一个轻量、好看、纯本地、数据完全自己掌控的复习工具——所以就搞了个Recall。
GitHub 项目地址 👉 https://github.com/suger-cheng/memory-tool
一、先看成品长什么样
📊 今日概览
打开应用第一眼就能看到今天要复习多少张、留存率、总卡片数、连续打卡天数、XP 和等级进度。
🗂 卡片管理
三种卡片类型:记忆卡片、知识点笔记、语录收藏。支持按卡组分类,嵌套卡组也没问题。
📚 卡组管理
卡组树结构,默认卡组 + 自建卡组,每日新卡数和复习上限独立配置。
⚙️ 设置页
主题切换、每日限额、托盘提醒、JSON 导入导出、数据库备份、自定义数据目录(改完自动迁移)。
二、核心算法:9 阶段间隔 + 三级反馈 + 长期记忆毕业
2.1 艾宾浩斯遗忘曲线
艾宾浩斯的实验公式是R(t) = e^(-t/S),其中 R 是记忆保持率,t 是经过的时间,S 是记忆强度(和复习次数正相关)。我们的目标是在 R 降到某个阈值之前恰好复习一次,把 S 拉高,下次间隔可以更长。
2.2 九阶段间隔表
我把复习间隔设计成 9 个递进阶段,从第一次复习的 5 分钟到第 9 次的 30 天:
| 阶段 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|
| 基础间隔 | 5 分钟 | 30 分钟 | 12 小时 | 1 天 | 2 天 | 4 天 | 7 天 | 15 天 | 30 天 |
但这不是死板的——每张卡还有一个ease(难度因子,1.3~3.0),实际间隔 =基础间隔 × (ease / 2.5)。容易的卡(ease 大)间隔自动拉长,难的卡(ease 小)间隔自动压缩。
2.3 三级反馈机制
每次复习完一张卡,用户只需要点三个按钮之一:
| 反馈 | quality | 阶段变化 | ease 变化 | 熟练度变化 | 获得 XP |
|---|---|---|---|---|---|
| 🟢 记住了 | 2 | +1(最高到第 9 阶段) | +0.08 | +100/9 × (ease/2.5) | +10 |
| 🟡 模糊 | 1 | -1(最低到 0) | -0.05 | -5 | +4 |
| 🔴 忘了 | 0 | 重置为 0 | -0.3 | ×0.4 | +1 |
"忘了"惩罚最重:阶段清零 + ease 大减 0.3 + 熟练度砍到 40%——但还是能拿到 1 XP,哪怕只是打开应用标记"忘了"也是好的,鼓励用户保持习惯。
2.4 熟练度与毕业系统
熟练度范围 0~100,达到 100 时卡片毕业,从普通复习流程进入长期记忆池:
长期记忆卡片使用 60 天基础间隔,按 ease 增长,上限 180 天
长期记忆卡片若再被标"忘了",退回普通流程,熟练度降至当前值的 40%
毕业就像通关——你不会再被一张已经记住的卡反复打扰。
2.5 复习队列优先级
一天到期的卡可能有几十上百张,按什么顺序复习?我的设计:
逾期普通卡 > 今日到期 > 即将到期 > 逾期长期卡
同优先级内:逾期天数降序 → ease 升序 → 熟练度升序
简单说就是越该复习的卡越靠前,让每张卡都在"最佳遗忘临界点"被复习。
2.6 核心算法代码
算法集中在review_engine.py,核心就一个函数:
defapply_feedback(card:dict,feedback:str)->dict:"""根据用户反馈更新卡片的阶段、ease、熟练度。"""q={"forgot":0,"fuzzy":1,"remembered":2}[feedback]# 三级反馈更新逻辑ifq==2:# 记住了card["stage"]=min(card["stage"]+1,9)card["ease"]=min(card["ease"]+0.08,3.0)card["mastery"]+=100/9*(card["ease"]/2.5)elifq==1:# 模糊card["stage"]=max(card["stage"]-1,0)card["ease"]=max(card["ease"]-0.05,1.3)card["mastery"]=max(card["mastery"]-5,0)else:# 忘了card["stage"]=0card["ease"]=max(card["ease"]-0.3,1.3)card["mastery"]*=0.4# 熟练度达标 → 毕业ifcard["mastery"]>=100andnotcard["is_long_term"]:card["is_long_term"]=1returncard三、技术栈 & 架构设计
3.1 为什么选 PySide6?
| 框架 | 优点 | 缺点 |
|---|---|---|
| PySide6(我选的) | Qt 官方 Python 绑定、文档全、跨平台、QSS 样式能力强、社区成熟 | 打包体积大(但 PyInstaller 可解决) |
| Tkinter | 自带、轻量 | 样式丑、功能弱、图表支持差 |
| PyQt5/6 | 功能和 PySide6 类似 | GPL/商业双授权,商用要花钱 |
| Electron/CEF | UI 最好看 | 体积大(动辄 100MB+)、内存占用高 |
PySide6 是目前 Python 桌面 GUI 的性价比之王——免费(LGPL 协议)、跨平台、样式系统灵活、图表渲染可以接 pyqtgraph。
3.2 整体架构
┌──────────────────────────────────────────────┐ │ PySide6 界面层 │ │ dashboard_view / review_view / card_manager │ │ deck_manager / stats_view / settings_view │ └──────────────────┬───────────────────────────┘ │ 调用 ┌──────────────────▼───────────────────────────┐ │ 业务逻辑层 │ │ review_engine.py 复习调度算法 │ │ gamification.py XP/等级/成就 │ │ scheduler.py 定时提醒 │ └──────────────────┬───────────────────────────┘ │ 读写 ┌──────────────────▼───────────────────────────┐ │ SQLite 数据层 │ │ database.py 连接管理 + 路径解析 │ │ utils/import_export.py JSON 导入导出 │ └──────────────────────────────────────────────┘3.3 数据模型
全部 7 张表,职责清晰:
| 表 | 职责 |
|---|---|
cards | 卡片数据 + 复习调度状态(stage/ease/mastery/is_long_term) |
decks | 卡组(支持嵌套,parent_id 自关联) |
review_log | 每次复习的完整记录(含前后状态快照 + 反应时间) |
daily_stats | 按天聚合的统计 |
settings | 键值对配置 |
user_progress | XP 与等级 |
achievements | 成就定义与解锁状态 |
用 SQLite WAL 模式 + 单连接复用,性能和稳定性都够用。
3.4 数据路径的坑
这里有个我花了点心思的设计:数据目录可配置 + 自动迁移。
问题:如果把路径硬编码成E:\data\xxx,别人 Windows 机器上 E 盘不存在就崩了。但如果放在安装目录,升级/卸载会丢数据。
方案:
- 默认路径跨平台:Windows 用
%APPDATA%、macOS 用~/Library/Application Support、Linux 用$XDG_DATA_HOME - 引导配置独立:在
~/.recall_config.json存data_dir——放在用户 home 而不是 data_dir 里,否则改了 data_dir 后下次启动找不到自己在哪 - 一键迁移:设置里选新目录 → 自动关闭连接 → 迁
recall.db+-wal+-shm→ 写引导配置。还做了目标目录可写校验、已存在数据库拒绝覆盖、迁移中途失败自动回滚 - 便携版支持:设环境变量
RECALL_BOOT_CONFIG=./boot.json就能把引导配置也挪走,整个 Recall 可以跟着 U 盘走
# database.py —— 路径解析核心逻辑def_get_data_dir()->str:"""默认路径按系统选择。"""ifsys.platform=="win32":base=os.environ.get("APPDATA")oros.path.expanduser("~")elifsys.platform=="darwin":base=os.path.expanduser("~/Library/Application Support")else:base=os.environ.get("XDG_DATA_HOME")oros.path.expanduser("~/.local/share")returnos.path.join(base,"Recall")def_resolve_data_dir()->str:"""优先级:引导配置 > 系统默认。"""cfg=_load_boot_config()# 读 ~/.recall_config.jsoncustom=(cfg.get("data_dir")or"").strip()ifcustomandos.path.isabs(custom):returncustomreturn_default_data_dir()四、游戏化:让坚持下去变得容易
纯工具很难坚持,加了一点游戏化元素:
XP 与等级:每次复习拿 XP(记住 +10、模糊 +4、忘了 +1),升级公式
100 × (n-1) × n / 28 种成就徽章:初次复习、勤奋学习者(10/100 张)、连续打卡(3/7 天)、完美一轮、长期记忆、小有所成(Lv.5)
连续打卡天数:断一天就清零——但这也是动力
XP 系统其实是个副产品:算法本身已经很科学了,但加了数字反馈之后,用户会更愿意打开应用——这对坚持来说比算法精度更重要。
五、快速上手
方式一:源码运行
# 克隆仓库gitclone https://github.com/suger-cheng/memory-tool.gitcdmemory-tool# 安装依赖pipinstall-rrequirements.txt# 运行python main.py方式二:打包 exe 分发
pipinstallpyinstaller# 单目录模式(推荐,启动快)pyinstaller--onedir--windowed--nameRecall main.py# 产物:dist/Recall/Recall.exe分发时把dist/Recall/整个文件夹压缩给对方即可。对方双击就能用,数据目录自动按系统选择,无需配置。
依赖清单
PySide6 >= 6.5.0 pyqtgraph >= 0.13.0Python 3.10+ 即可。
也可以直接安装exe文件,无需安装各种依赖,直接运行。
有任何问题或建议,欢迎在 GitHub 提 Issue / PR,也可以在评论区交流。
🔗项目地址:https://github.com/suger-cheng/memory-tool
⭐ 如果觉得有用,记得给项目点个 Star~