资讯动态

TypeScript实现SM-2间隔重复算法:构建高效记忆引擎

发布时间:2026/8/24 4:06:18 来源:尧图企业网站定制
1. 项目概述当记忆遇见算法“间隔重复记忆之SM-2”这个标题一出来很多朋友可能会觉得有点陌生但如果说“背单词软件的核心算法”你可能就恍然大悟了。没错我们每天用的Anki、SuperMemo这类软件它们之所以能让你在快要忘记的时候恰到好处地提醒你复习背后站着的就是像SM-2这样的间隔重复算法。它不是什么高深莫测的AI而是一个将心理学研究成果艾宾浩斯遗忘曲线工程化、数学化的经典模型。简单说它通过计算你每次复习后的记忆强度动态决定下一次复习的最佳时间点目标是让你用最少的复习次数达到最牢固的长期记忆效果。我之所以对这个项目感兴趣是因为市面上虽然应用很多但真正把算法核心剥离出来做成一个干净、可复用、类型安全的库并且用现代前端工具链包装好的并不多。很多开发者想在自己的应用里集成记忆功能要么得从头实现算法容易出错要么得引入一个庞大的、附带复杂UI的库。这个项目的价值就在于它用TypeScript精准地实现了SM-2算法内核打包成npm包让你可以像安装lodash一样用一行命令npm install就把“记忆引擎”装进你的项目里无论是开发一个语言学习App、一个知识点闪卡工具还是一个内部培训系统都变得轻而易举。2. 核心原理SM-2算法深度拆解SM-2算法诞生于上世纪80年代是SuperMemo系列软件的第二代算法。它的精妙之处在于用几个简单的参数和规则模拟了人脑记忆的巩固过程。理解它是正确使用和潜在改进它的基础。2.1 算法核心参数与状态算法围绕“卡片”一个待记忆的信息单元比如一个单词的几个核心状态运转易度因子Ease Factor, EF这是算法的“记忆引擎”。它是一个大于1.3的浮点数默认值通常是2.5。它直接决定了下次复习间隔的增长率。如果你觉得这张卡片“简单”EF会微微上调下次间隔就更长如果觉得“困难”EF会下调下次间隔就缩短。EF是算法实现“个性化”记忆调度的关键。间隔Interval当前这次复习后到下一次复习应该等待的天数。重复次数Repetitions这张卡片被连续成功回忆的次数。一张卡片初始状态通常是EF 2.5,Interval 0,Repetitions 0。2.2 复习流程与调度逻辑算法的核心是一个决策循环每次你复习一张卡片并给出评分比如1-5分代表从“完全忘记”到“轻而易举”后算法就会运行以下逻辑评分判定评分低如1-2分视为本次复习失败。算法会将卡片的Repetitions重置为0Interval重置为1或一个很小的值比如1天但EF保持不变或进行小幅惩罚。这意味着卡片将“降级”到最初的频繁复习状态。评分合格如3-5分视为本次复习成功。算法进入间隔计算流程。间隔计算针对成功复习如果Repetitions 0第一次成功复习则新间隔设为1天。如果Repetitions 1第二次成功复习则新间隔设为6天。这是一个经验值对应记忆从短期转入中期巩固的关键点。如果Repetitions 2则新间隔 旧间隔 * EF。这就是“间隔重复”的核心每次成功复习后间隔时间按易度因子成倍增长。易度因子更新EF EF (0.1 - (5 - 评分) * (0.08 (5 - 评分) * 0.02))这个公式看起来复杂但其意图很直观评分越高5分公式后半部分值越小甚至是负值EF增加得越多上限通常有限制如2.5。评分越低3分公式后半部分值越大EF减少得越多。EF被限制在一个合理范围内如不低于1.3防止间隔无限缩短或增长失控。重复次数更新成功复习后Repetitions加1。注意上述公式和参数如1天、6天、EF更新公式是SM-2的经典版本。在实际的库实现中这些可能作为可配置项允许开发者微调以适应不同的学习材料特性。2.3 为什么是SM-2相比更早期的算法SM-2引入了“易度因子EF”这个动态调节器使得算法不仅能根据“遗忘”来调度还能根据“记忆难度”来个性化调整这是一个巨大的进步。虽然SuperMemo后续版本SM-4, SM-5...直到SM-18算法越来越复杂考虑了更多因素但SM-2因其简单、有效、易于实现和理解成为了间隔重复领域事实上的“标准模型”和入门基石。绝大多数开源和商业应用其核心调度逻辑都能看到SM-2的影子。3. 工程化实现从算法到TypeScript库理解了原理我们来看看如何把它变成一个可靠的、开发者友好的库。这正是“间隔重复记忆之SM-2”项目的工程价值所在。3.1 技术选型为什么是TypeScript类型安全记忆调度涉及多个数值参数EF间隔评分和状态流转。TypeScript的静态类型系统可以在编译阶段就捕获大量的潜在错误比如传入错误的评分枚举值、对未定义的状态进行计算等这对于保证算法核心的健壮性至关重要。良好的生态与分发TypeScript编译为JavaScript后可以同时被Node.js和浏览器环境使用。通过npm分发能无缝集成到现代前端、后端或全栈项目中。出色的开发体验清晰的接口定义Interface能让库的使用者一目了然地知道如何调用函数、传递什么参数、得到什么结果大大降低了集成成本。未来友好越来越多的开源项目和公司内部项目采用TypeScript用TS编写库具有更好的长期维护性和社区接受度。3.2 核心模块设计与接口定义一个设计良好的库应该有清晰的职责划分。我设想的核心模块可能包括Card接口/类定义卡片的数据结构。interface SMCard { // 核心算法状态 easeFactor: number; // 易度因子 interval: number; // 当前间隔天 repetitions: number; // 连续成功次数 // 实用信息 dueDate: Date; // 下次到期日由interval计算得出 lastReviewed?: Date; // 上次复习日期 // ... 其他业务字段如卡片内容ID等 }Scheduler调度器类算法的核心实现。class SM2Scheduler { // 可配置参数 private initialEF: number; private easyBonus: number; // 评分5分时的额外奖励因子 private hardIntervalFactor: number; // 评分3分时的间隔乘数 constructor(config?: PartialSchedulerConfig) { // 初始化配置提供默认值 this.initialEF config?.initialEF ?? 2.5; // ... 其他配置 } // 核心方法处理一次复习 review(card: SMCard, rating: ReviewRating): SMCard { // 1. 深拷贝或创建新卡片对象避免副作用 const newCard { ...card }; // 2. 根据评分应用SM-2算法逻辑更新 newCard 的 EF, interval, repetitions // 3. 计算并更新 newCard.dueDate (例如Date.now() newCard.interval * 86400000) // 4. 更新 lastReviewed // 5. 返回更新后的卡片对象 return newCard; } // 工具方法初始化一张新卡片 initializeCard(): SMCard { return { easeFactor: this.initialEF, interval: 0, repetitions: 0, dueDate: new Date(), // 立即到期 lastReviewed: null }; } }ReviewRating枚举定义标准的评分等级。export enum ReviewRating { Again 1, // 完全忘记 Hard 2, // 困难/想起错误答案 Good 3, // 一般/需要努力回忆 Easy 4 // 简单/轻而易举 // 有些实现会有5分Very Easy对应更大的EF奖励 }3.3 实现中的关键细节与陷阱不可变数据review方法应该返回一个全新的卡片对象而不是修改传入的卡片。这是函数式编程的常见实践能避免共享状态引发的难以追踪的Bug也便于状态管理和历史回溯。时间处理interval单位是“天”但JavaScript的Date操作是基于毫秒的。计算dueDate时要小心时区问题和浮点数精度。一个稳健的做法是使用dayjs或date-fns这类日期库或者至少用interval * 86400000毫秒/天来计算。参数边界检查评分值必须在有效范围内如1-4或1-5EF在更新后需要钳制clamp在合理范围如[1.3, 2.5]防止极端值导致间隔爆炸几百年后复习或坍缩几乎每天复习。配置化将经典SM-2公式中的魔法数字如第一次成功间隔1第二次6EF更新公式系数暴露为可配置选项。这能让库更加灵活适应不同的学习场景比如儿童学习和成人专业学习的最佳间隔可能不同。4. 开发、测试与发布全流程有了清晰的设计我们就可以动手搭建项目了。这里我会结合最新的工具链分享一个高效的开发流程。4.1 项目初始化与工具链配置首先确保你的Node.js版本在18以上推荐最新的LTS版本。然后初始化项目# 创建项目目录并进入 mkdir sm2-spaced-repetition cd sm2-spaced-repetition # 初始化npm项目使用默认配置或按需填写 npm init -y # 安装TypeScript和类型定义作为开发依赖 npm install -D typescript types/node # 初始化TypeScript配置 npx tsc --init接下来编辑生成的tsconfig.json。对于库项目关键配置如下{ compilerOptions: { target: ES2020, module: CommonJS, declaration: true, // 关键生成.d.ts类型声明文件 outDir: ./dist, rootDir: ./src, strict: true, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true, moduleResolution: node }, include: [src/**/*], exclude: [node_modules, dist, **/*.test.ts] }为什么这么配置target: ES2020平衡了现代特性和兼容性module: CommonJS确保在Node环境和大多数打包工具中能良好运行declaration: true是库项目的生命线它为使用者提供完整的类型提示。4.2 构建、测试与质量保障构建脚本在package.json中添加构建命令。scripts: { build: tsc, prepublishOnly: npm run build npm test }prepublishOnly钩子确保在发布到npm前一定会先执行构建和测试。单元测试使用Jest作为测试框架它能很好地支持TypeScript。npm install -D jest ts-jest types/jest npx ts-jest config:init在src/__tests__/目录下为Scheduler编写测试用例覆盖所有评分路径、边界情况如EF降到下限、升到上限以及初始状态。// scheduler.test.ts 示例 import { SM2Scheduler, ReviewRating } from ../scheduler; describe(SM2Scheduler, () { let scheduler: SM2Scheduler; beforeEach(() { scheduler new SM2Scheduler(); }); test(首次复习评分“Good”间隔应变为1天, () { const card scheduler.initializeCard(); const newCard scheduler.review(card, ReviewRating.Good); expect(newCard.interval).toBe(1); expect(newCard.repetitions).toBe(1); }); test(连续两次“Good”后第三次“Good”应使用EF计算间隔, () { let card scheduler.initializeCard(); card scheduler.review(card, ReviewRating.Good); // 间隔1 card scheduler.review(card, ReviewRating.Good); // 间隔6 const newCard scheduler.review(card, ReviewRating.Good); // 间隔 6 * EF expect(newCard.interval).toBeCloseTo(6 * 2.5, 1); // 约15天 }); test(评分“Again”应重置重复次数和间隔, () { let card scheduler.initializeCard(); card scheduler.review(card, ReviewRating.Good); card scheduler.review(card, ReviewRating.Good); // 此时 repetitions2, interval6 const newCard scheduler.review(card, ReviewRating.Again); expect(newCard.repetitions).toBe(0); expect(newCard.interval).toBe(1); // 或你定义的初始失败间隔 }); });运行测试npm test。代码风格与质量使用ESLint和Prettier保证代码一致性。npm install -D eslint typescript-eslint/parser typescript-eslint/eslint-plugin prettier eslint-config-prettier配置好.eslintrc.js和.prettierrc并添加lint: eslint src --ext .ts和format: prettier --write src/**/*.ts到package.json的脚本中。4.3 发布到npm与GitHub准备package.json这是库的“身份证”。{ name: sm2-spaced-repetition, // 名字要唯一先去npm官网查重 version: 1.0.0, description: A TypeScript implementation of the SM-2 spaced repetition algorithm., main: dist/index.js, types: dist/index.d.ts, // 指向类型声明文件 files: [dist], // 指定发布到npm的文件目录 keywords: [spaced-repetition, sm2, memory, learning, typescript], author: Your Name, license: MIT, repository: { type: git, url: https://github.com/your-username/sm2-spaced-repetition.git }, homepage: https://github.com/your-username/sm2-spaced-repetition#readme, bugs: { url: https://github.com/your-username/sm2-spaced-repetition/issues }, scripts: { ... }, devDependencies: { ... }, dependencies: {} // 纯TS/JS实现通常没有运行时依赖 }创建入口文件在src/index.ts中导出所有公共API。export { SM2Scheduler } from ./scheduler; export { ReviewRating } from ./types; export type { SMCard, SchedulerConfig } from ./types;发布流程在 npm官网 注册账号。在终端登录npm login按提示输入用户名、密码和邮箱。构建项目npm run build。执行发布npm publish。如果是第一次发布默认是公开包。如果想发布测试版可以使用npm publish --tag beta。GitHub仓库管理初始化Git仓库关联GitHub远程仓库。创建清晰的README.md包含简介、安装、快速开始、API文档、示例和贡献指南。添加.gitignore文件忽略node_modules和dist目录。建议使用GitHub Actions配置CI/CD实现提交代码后自动运行测试和lint检查。5. 实战集成在Web应用中嵌入记忆引擎库发布后我们来看看如何在一个真实的前端项目比如一个React闪卡应用中使用它。5.1 安装与引入在你的React/Vue/Next.js等项目根目录下npm install sm2-spaced-repetition然后在你的组件或状态管理文件中引入import { SM2Scheduler, ReviewRating, type SMCard } from sm2-spaced-repetition;5.2 状态管理与复习逻辑假设我们有一个简单的闪卡应用卡片数据存储在状态中。// 在组件或Store中 import { useState, useMemo } from react; const scheduler useMemo(() new SM2Scheduler(), []); // 创建单例调度器 const [cards, setCards] useStateSMCard[](() { // 从本地存储加载或初始化新卡片 return loadCardsFromStorage().map(cardData ({ ...scheduler.initializeCard(), // 初始化算法状态 ...cardData, // 合并业务数据id, question, answer等 })); }); // 处理复习动作 const handleReview (cardId: string, rating: ReviewRating) { setCards(prevCards prevCards.map(card { if (card.id cardId) { // 核心调用调度器根据评分更新卡片状态 const updatedCard scheduler.review(card, rating); return { ...updatedCard, lastReviewed: new Date() }; } return card; }) ); }; // 获取今日待复习的卡片 const getDueCards () { const now new Date(); return cards.filter(card card.dueDate now); };5.3 数据持久化策略卡片状态尤其是easeFactor,interval,repetitions,dueDate需要持久化否则页面刷新后学习进度就丢失了。本地存储对于纯前端应用localStorage或IndexedDB是简单选择。每次cards状态更新后同步写入。useEffect(() { try { localStorage.setItem(sm2-cards, JSON.stringify(cards)); } catch (e) { console.error(Failed to save cards to localStorage, e); } }, [cards]);注意Date对象序列化后会变成字符串反序列化时需要重新转为Date对象。可以使用JSON.parse的reviver函数或库如date-fns来处理。后端同步对于多设备同步或团队应用需要将卡片状态保存到后端数据库。设计数据表时除了业务字段务必包含算法所需的几个核心字段。API接口需要提供更新卡片复习结果的端点。5.4 界面交互设计建议评分按钮提供清晰对应ReviewRating枚举的按钮如“生疏/错误”Again、“困难”Hard、“良好”Good、“简单”Easy。给每个按钮配上直观的图标和颜色。进度可视化展示今日复习进度X/Y张以及未来卡片到期情况的日历视图或图表能极大提升用户坚持的动力。批量操作允许用户对多张卡片进行“全部标记为Good”等操作方便快速复习已知内容。6. 常见问题、优化与扩展方向在实际使用和开发过程中你肯定会遇到一些典型问题。这里我分享一些踩过的坑和进阶思路。6.1 常见问题排查问题现象可能原因解决方案卡片复习间隔增长过快很快就要几年后才复习。易度因子EF增长失控可能评分“简单”给的奖励过高或EF上限未设置。检查EF更新公式确保EF有上限如2.5。可以调低“简单”评分对应的EF增量系数。卡片复习间隔几乎不增长总是频繁出现。评分持续偏低如总是“困难”导致EF不断被惩罚降至下限。或者初始间隔设置过小。检查学习材料是否过难。可以适当提高“良好”评分对应的初始间隔如从1天调为2天。确保EF有下限如1.3。dueDate计算出现一天误差。时区处理问题。new Date()和interval * 86400000计算可能受本地时区影响。使用UTC时间进行计算或使用dayjs等库的add(interval, day)方法它们能更好地处理日期加减。TypeScript项目引入后找不到类型声明。package.json中的types字段未正确指向.d.ts文件或tsconfig.json的moduleResolution设置有问题。确保库的package.json包含types: dist/index.d.ts。在使用方项目中检查node_modules下该库的dist目录是否存在声明文件。npm安装失败提示EBADENGINE。库的package.json中engines字段指定的Node.js版本高于你当前的版本。升级本地Node.js版本或联系库作者放宽版本限制。对于自己发布的库谨慎设置engines字段。6.2 算法优化与变体经典SM-2已经很有效但仍有优化空间间隔抖动在计算出的间隔上增加一个小的随机浮动如±10%避免大量卡片在同一天到期形成“复习高峰”这更符合真实的、分散的学习节奏。难度因子初始化不同卡片初始难度不同。可以允许用户在创建卡片时设置一个初始难度系数影响初始EF值。学习阶段与复习阶段SM-2主要针对复习阶段。可以引入一个单独的“学习阶段”对于新卡片或复习失败的卡片采用更短、更密集的间隔如20分钟后、1小时后、9小时后然后再进入SM-2主导的复习阶段。Anki就采用了这种两阶段模型。基于历史表现的动态参数可以根据用户整体的复习表现平均正确率动态微调算法中的全局参数实现更自适应的学习。6.3 项目扩展方向支持更多算法将SM-2作为第一个实现然后抽象出统一的Scheduler接口未来可以轻松加入Anki的改进算法、FSRS一个较新的、基于神经网络的算法等让库成为一个“间隔重复算法工具箱”。提供React/Vue Hooks封装像useSpacedRepetition(cardId)这样的Hook让在UI组件中集成复习逻辑变得无比简单。开发CLI工具做一个命令行工具可以导入CSV格式的问答对然后每天在终端里进行复习适合喜欢极简和键盘操作的用户。数据导出与分析提供将复习历史导出为JSON或CSV的功能方便用户进行数据分析了解自己的记忆规律和薄弱环节。从一行算法公式到一个可复用的TypeScript库再到集成到真实应用中“间隔重复记忆之SM-2”这个项目完整地展示了一个想法如何通过工程化手段产生实际价值。它剥离了复杂的外壳将最核心、最经典的记忆引擎交付给开发者。无论你是想快速为自己的应用添加记忆功能还是想深入学习间隔重复算法的原理并动手实践这个项目都是一个绝佳的起点和可靠的工具。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价