news 2026/6/19 20:51:23

23 级山东大学软件学院创新实训 - 个人纪录(五)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
23 级山东大学软件学院创新实训 - 个人纪录(五)

LinguaSpark 单词复习算法的缺陷发现与改进实践

在 LinguaSpark 智能外语学习平台上线运行后,我通过日志监控与数据分析,发现了单词背诵模块中 SM-2 间隔重复算法存在的严重缺陷——用户可以通过高频刷同一个单词,使间隔天数指数膨胀至数百天,导致遗忘曲线算法完全失效。针对这一问题,我深入排查了根因,设计了基于实际经过天数的改进方案,并重新规划了推送接口与主动复习接口的职责边界。改进后的算法从根本上杜绝了数据膨胀问题,同时使间隔计算更精准地反映用户真实的记忆周期。


一、核心业务场景

系统涉及两个关键接口:

接口路径功能
更新复习记录/api/words/user-words/update用户背诵单词后提交评估结果(记得住/模糊/没记住),系统计算下次复习时间
推送需复习单词/api/words/user-words/review根据遗忘曲线推送已到复习时间的单词,供用户回顾

此外,平台还提供了一个单词书自由复习功能——用户可自主选择任意单词进行背诵,不受推送机制约束。这一设计带来了额外的数据安全挑战。


二、初版实现与问题发现

2.1 初版算法设计

初版实现采用了经典的 SM-2 公式,以repetitions(连续背诵成功次数)作为间隔计算的核心驱动因子:

如果状态为"记得住": repetitions += 1 如果 repetitions == 1:interval = 1 天 如果 repetitions == 2:interval = 6 天 否则:interval = int(interval × ease_factor) # 指数增长 如果状态为"模糊": interval = max(1, int(interval × 0.8)) # 略微缩短 # repetitions 不变 如果状态为"没记住": repetitions = 0 # 清零连续正确次数 interval = 1 # 重置为1天

这一设计的理论假设是:用户严格按系统推送的节奏复习,repetitions的递增能准确反映记忆巩固的进程。

2.2 上线后的数据异常

系统上线后,我通过日志监控发现了两个严重的数据异常:

异常一:间隔天数异常膨胀

某测试用户在 5 天内连续复习同一个单词,每次选择"记得住",间隔天数出现了非理性增长:

第 N 天实际间隔repetitionsinterval(计算值)问题分析
111 天正常
21 天26 天⚠️ 实际才隔 1 天,却跳到 6 天
31 天315 天⚠️ 仍在每天刷,间隔却到 15 天
41 天437 天❌ 严重脱节
51 天592 天❌ 数据完全失真

5 天后,该单词的间隔被计算为92 天(超过 3 个月),而用户实际每天只隔了1 天。这种指数膨胀使遗忘曲线算法完全失去意义——用户的数据被"刷"高了。

异常二:迟到复习未获正向激励

与此相反,另一个场景中,某单词的预计复习日期已过 20 天,用户才回来复习且仍然记得住。但初版算法不考虑这个"20 天"的实际间隔:

实际经过:20 天 用户表现:记得住(说明记忆很强) 算法计算:repetitions=3 → interval=15 天 应得间隔:20 × 2.5 = 50 天 差距:15 天 vs 50 天,相差 3.3 倍

用户用 20 天的实际表现证明了对这个词的掌握,但算法只给了 15 天间隔,这是一种负向反馈——好的记忆表现没有获得应有的奖励。

2.3 根因分析

经过深入排查,我确认了三个根本缺陷:

缺陷一:算法完全依赖repetitions计数,不包含时间维度信息。无论用户是隔了 1 天还是 30 天才来复习,只要repetitions++,间隔就按指数增长。

缺陷二:推送接口存在兜底逻辑,当到期单词不足时会推送未到期单词。这导致用户可能在单词尚未到期时就被推送,从而在不合理的短间隔内反复更新记录。

缺陷三update接口缺少防刷保护,结合前端单词书的自由复习功能,用户可以反复刷同一个单词使数据膨胀。


三、算法改进方案设计

3.1 核心改进思路:引入实际经过天数

改进的核心思想是将计算驱动因子从"虚的"repetitions切换为"实的"actual_days(实际经过天数):

实际经过天数 (actual_days) = max(1, 今天 - 上次复习日期) 新间隔 = int(actual_days × ease_factor) 保护机制: 如果 new_interval ≤ 记录中已有的 interval_days, 则拒绝更新 interval 和 repetitions(防止刷数据)

为什么使用max(1, ...)保证至少为 1?这是为了处理用户在同一天内多次复习的极端情况——即使同一天内,也按 1 天计算,避免出现除零等问题。

3.2 改进后的全量算法

actual_days = max(1, (today - last_review_date).days) 状态 = "记得住" (status == 1): ┌─────────────────────────────────────────────────────┐ │ if actual_days <= 1: new_interval = 1 │ │ elif actual_days == 2: new_interval = max(3, int(2×ef)) │ │ else: new_interval = int(actual_days × ease_factor) │ │ │ │ new_ease_factor = min(2.5, ease_factor + 0.1) │ │ │ │ ★ 保护判断: │ │ if new_interval <= record.interval_days: │ │ interval = 不变 (保护) │ │ repetitions = 不变 (保护) │ │ ease_factor = new_ease_factor (可以微增) │ │ else: │ │ interval = new_interval (更新) │ │ repetitions += 1 (正常递增) │ │ ease_factor = new_ease_factor │ └─────────────────────────────────────────────────────┘ 状态 = "模糊" (status == 2): repetitions = 0 interval = 1 ease_factor = max(1.3, ease_factor - 0.15) 状态 = "没记住" (status == 3): repetitions = 0 interval = 1 ease_factor = max(1.3, ease_factor - 0.2)

3.3 推送接口的职责分离

推送接口/user-words/review也进行了两次迭代优化:

第一轮:去掉了other_words兜底逻辑。原本当到期+即将到期单词不足时,会用 7 天外的未到期单词补齐,这破坏了记忆曲线的节奏。

第二轮(最终版):去掉了upcoming_words(7 天内即将到期单词),只推送真正已到期的单词next_review_date <= today)。

设计理念如下:

┌─────────────────────────────────────────────────────────┐ │ 用户背单词的两条路径 │ ├──────────────────────┬──────────────────────────────────┤ │ 推送复习(被动提醒) │ 主动复习(单词书自由选择) │ │ /user-words/review │ /user-words/update │ ├──────────────────────┼──────────────────────────────────┤ │ 只推送已到期单词 │ 接受任意单词的复习请求 │ │ 保证用户按科学节奏复习 │ 保护机制防止数据被刷 │ │ next_review_date≤today │ new_interval≤记录interval→不更新 │ ├──────────────────────┴──────────────────────────────────┤ │ 两条路径互补:推送控制节奏,主动复习允许自由但数据安全 │ └─────────────────────────────────────────────────────────┘

推送是被动提醒——系统告诉用户"该复习了"。如果推送未到期单词,就会打破遗忘曲线的科学节奏。而主动复习是用户自主行为——想多学几个是合理的,但通过保护机制确保数据不被刷坏。


四、改进效果验证

4.1 场景一:用户每天刷同一个单词(刷数据行为)

天数 实际经过 new_interval 记录中interval 是否更新 repetitions ──────────────────────────────────────────────────────────────────── 1 1天 1 1(初始) ✓ 更新 1 2 1天 1×2.5=2.5→2 1 ✗ 保护 1 3 1天 1×2.6=2.6→2 1 ✗ 保护 1 4 1天 1×2.6=2.6→2 1 ✗ 保护 1 5 1天 1×2.6=2.6→2 1 ✗ 保护 1 6 1天 1×2.6=2.6→2 1 ✗ 保护 1 7 1天 1×2.6=2.6→2 1 ✗ 保护 1

结论:保护机制生效。从第 2 天开始,new_interval始终为 2,小于等于记录中的 1,触发保护。interval 和 repetitions 保持稳定,不会被刷高。

4.2 场景二:用户按正常间隔复习(正常使用)

天数 实际经过 状态 new_interval 记录中interval 是否更新 repetitions ──────────────────────────────────────────────────────────────────────────── 1 - 创建 1 1 - 1 2 1天 记得住 1×2.5=2→2 1 ✗ 保护 1 8 6天 记得住 6×2.5=15 1 ✓ 更新 2 23 15天 记得住 15×2.6=39 15 ✓ 更新 3

结论:当用户真正等了足够天数(第 8 天复习时实际经过 6 天),new_interval=15大于记录中的1,触发正常更新。间隔天数从 1→15→39,合理反映记忆巩固过程。

4.3 场景三:到期后很久才复习,但还记得住

条件:上次 interval=6 天,next_review_date=第10天 用户第30天才复习(逾期20天),选择"记得住" 实际经过 = 30 - 2 = 28 天 旧算法:repetitions=3 → interval = int(6×2.5) = 15 天 新算法:28 × 2.5 = 70 天 差距:70 天 vs 15 天(4.7 倍)

结论:新算法正确奖励了用户的强记忆。间隔 28 天仍记住 → 说明掌握非常好 → 给予 70 天的长间隔,减少不必要的复习负担。

4.4 场景四:推送接口行为

假设用户有 20 条记录(难度=cet4,每日目标=10): 状态 数量 next_review_date 推送? ───────────────────────────────────────────────── 已到期(逾期3天) 3条 2026-06-02 ✓ 已到期(今天到期) 2条 2026-06-05 ✓ 即将到期(明天) 5条 2026-06-06 ✗ 即将到期(3天后) 4条 2026-06-08 ✗ 未到期(8天后) 6条 2026-06-13 ✗ 推送结果:只推送 5 条已到期单词,不补齐到 10 条

结论:推送接口严格只推送已到期单词,不会为了让数量凑够而推送未到期内容。前端可引导用户"当前没有需要复习的单词,可以主动去单词书学习"。


五、改进前后对比总结

对比维度改进前改进后
间隔计算驱动因子repetitions(纯计数,无时间维度)actual_days(实际经过天数,精确到日)
刷数据行为可被利用(间隔指数膨胀:1→6→15→37→92…)被阻止(保护机制拒绝更新)
逾期后仍记住间隔不足(仅 15 天,未奖励强记忆)间隔充足(70 天,准确反映记忆水平)
模糊/没记住处理间隔略微减少直接清零,间隔回到 1 天
推送已到期单词
推送即将到期单词✗(已移除)
推送未到期单词会(other_words 兜底)✗(已移除)
主动复习保护无(数据可被刷)有(保护机制兜底)
整体数据真实性低(与实际间隔脱节)高(直接反映实际间隔)

六、工程实现要点

6.1 技术栈

  • 后端框架:FastAPI(Python 3.10+)
  • ORM:SQLAlchemy
  • 数据库:MySQL(AWS RDS)
  • 缓存:Redis(打卡状态、每日计数、分布式锁)
  • 容器化:Docker + Docker Compose

6.2 核心代码实现

@router.post("/user-words/update")asyncdefupdate_user_word_review(data:WordUserUpdate,db:Session):"""基于实际天数的 SM-2 改进算法"""record=db.query(WordUser).filter(...).first()ifnotrecord:raiseHTTPException(status_code=404)today=datetime.now().date()# 1. 计算实际经过天数(核心改进点)ifrecord.last_review_date:actual_days=max(1,(today-record.last_review_date).days)else:actual_days=1ease_factor=record.ease_factor/100.0interval=record.interval_days repetitions=record.repetitionsifdata.status==1:# 记得住# 2. 基于实际天数计算新间隔ifactual_days<=1:new_interval=1elifactual_days==2:new_interval=max(3,int(actual_days*ease_factor))else:new_interval=int(actual_days*ease_factor)new_ease_factor=min(2.5,ease_factor+0.1)# 3. 保护机制:间隔不足则不更新ifnew_interval<=record.interval_days:interval=record.interval_days# 保持不变# repetitions 保持不变ease_factor=new_ease_factorelse:interval=new_interval repetitions+=1ease_factor=new_ease_factorelifdata.status==2:# 模糊repetitions=0interval=1ease_factor=max(1.3,ease_factor-0.15)else:# 没记住repetitions=0interval=1ease_factor=max(1.3,ease_factor-0.2)# 4. 持久化更新record.ease_factor=int(ease_factor*100)record.interval_days=interval record.repetitions=repetitions record.next_review_date=today+timedelta(days=interval)record.last_review_date=today record.last_status=data.status db.commit()db.refresh(record)return{"message":"复习记录更新成功","ease_factor":ease_factor,"interval_days":interval,"repetitions":repetitions,"next_review_date":record.next_review_date.isoformat(),"actual_days":actual_days}

七、总结

7.1 项目成果

  • 完成了 SM-2 算法的工程化实现与改进,解决了初版中"间隔与实际脱节"、"数据可被刷高"两个核心缺陷
  • 引入了actual_days作为间隔计算驱动因子,使算法具备真实的时间维度感知能力
  • 设计了保护机制,确保推送复习和主动复习两条路径数据安全
  • 编写了完整的技术文档,涵盖算法原理、缺陷分析、改进方案和效果验证

7.2 技术亮点

  1. repetitionsactual_days的驱动因子迁移:这是整个改进的核心——让算法从"计数驱动"变为"时间驱动",从根本上解决了数据失真问题
  2. 保护机制的巧妙设计:仅用一个不等式new_interval <= record.interval_days即可实现防刷保护,无需额外状态标记,简洁且高效
  3. 推送与主动复习的职责分离:通过不同的接口行为约束,既保证了科学复习节奏,又保留了用户的学习自由度
  4. 全面的测试验证:通过多个场景的模拟验证,确保改进后的算法在各种边界条件下都能正确运行

以下为提示词

需求:修改/user-words/update接口的复习逻辑

一、核心问题

当前接口存在以下问题:

  • 即使所有单词都未到期,调用接口仍会推送并更新未到期的单词
  • 导致:
    • 初始单词被反复推送,间隔天数过度增长
    • 无法利用用户逾期后仍记得清晰的真实表现

二、改进方案

1. 改用“实际天数”计算下次间隔

公式

实际天数 = 原间隔天数 + (当前日期 − 过期日期)

处理逻辑

比较结果操作
实际天数 ≤ 原间隔天数不更新间隔、不更新连续成功次数
实际天数 > 原间隔天数按实际天数更新间隔

2. 用户选择“模糊 / 忘记”时

  • 连续成功次数 → 清零
  • 下次间隔 → 重置为 1 天

3. 最终更新

  • 根据最终确定的间隔天数,重新计算并更新过期时间

三、为什么这样设计

  • 防止用户通过单词书反复背诵同一单词来刷数据
  • 真实反映用户记忆水平
  • 不推送未到期单词,避免间隔失控

四、效果对比示例

场景原逻辑新逻辑
单词应 10 天后复习,用户 20 天后才复习且记得仍按原间隔增长使用实际间隔 20 天
反复调用接口背同一单词间隔不断增长实际间隔未超过记录时不更新
逾期后仍记得清晰仍按原计划增长可延长间隔,减少冗余复习

五、输出要求

  1. 接口修改说明(字段、计算逻辑)
  2. 改进前后数据对比示例
  3. 能避免的异常行为说明(如刷数据、间隔失控)
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/19 20:50:19

Dev-C++一键运行的C语言进销存控制台程序(含源码+exe+工程文件)

本文还有配套的精品资源&#xff0c;点击获取 简介&#xff1a;直接在Windows下用Dev-C打开就能编译运行的商品进销存管理程序&#xff0c;纯C语言编写&#xff0c;不依赖任何外部库。启动后通过数字菜单操作&#xff1a;录入进货或销售记录&#xff08;自动同步更新库存和销…

作者头像 李华
网站建设 2026/6/14 3:39:43

099、YOLO + LLM/VLM 多模态探索:检测结果用大模型做语义理解和描述

099、YOLO + LLM/VLM 多模态探索:检测结果用大模型做语义理解和描述 一、从一次离谱的误检说起 上个月做智慧零售项目,摄像头对着货架,YOLOv8检测到一瓶“可乐”——置信度0.92,框得贼准。但客户反馈说:“你们系统把一瓶零度可乐识别成经典可乐,这会导致库存统计出错。”…

作者头像 李华