memoquest 上线:会板书的 AI 老师
它想解决的是一个大家都遇到过、但一直没有工具正面去解的问题:学生做题、看资料时,所有 AI 答疑工具给的本质是同一样东西——一篇一次性的结构化长文。读的时候在跟着走,读完就散;内容进的是短期记忆,几天内衰减;学生全程被动,没有"该想一想"和"该记一下"的分界。
memoquest 的答案不是"讲得更好",是换一种讲课形态。
两块屏幕
| 演讲稿(下方) | 黑板(上方) | |
|---|---|---|
| 内容 | 完整、连贯、口语化的散文 | 凝练的重点、强调项、可交互点 |
| 数量 | 150–280 字,连续 | 关键词最多 5 条 + 1 条结论 + 抽问卡 |
| 类比 | 老师说的话 | 老师写在黑板上的字 |
关键在黑板不是演讲稿的镜像——老师不会把每句话都写上黑板。黑板只放"提纲 + 强调 + 可交互"。
✨ 三个核心机制
① 解题思路置顶 你输入的问题原文原样钉在黑板最上方,不挂学科标签。
② 黑板按节奏贴卡 讲解时 AI 会在讲到重点的地方内联一个标记,另一个模型负责把标记凝练成卡片。播放到那一处,卡才贴上来——不是一口气全倒出来让学生自己找。依据是工作记忆 7±2:一条卡就是一个组块。
③ 讲中抽问 讲到关键处会停下来,黑板上出现一道题。答完 + 看完解析才继续往下讲。
"讲中抽问"比"讲完给两道题"强得多:后者测的是"刚才读过",前者才是主动回忆。
答错不重试,直接给判别依据(为什么是这个,2 句以内)+ 一句话点出这题对应的记忆原理。
⚡ 可以自己选模型
页面上多了一个「模型」栏,讲解和板书分开选(会记住你的选择):
| 阶段 | 默认 | 消耗 |
|---|---|---|
| 讲解(写稿) | Qwen3 Next 80B | 🆓 免费,不扣额度 |
| 板书(编排) | Ling 2.6 Flash | 约 220 credits/次 |
全部 45 个模型都能选(20 个免费 + 25 个计费),在弹窗里按「免费 / 计费」分组,计费组从低到高排。价格每次从服务端实时拉,改价立刻生效,不用等 memoquest 更新。模型上挂 FREE / THINK 徽章。
选型时最重要的一件事是先看清这次要花多少。每次调整都会实时显示预估消耗、占今日额度的百分比、当前余额:
本次问答约 2448 credits · 占今日额度 10% · 当前还剩 18234
超过当天剩余额度会直接红字拦下并告诉怎么救。实测最贵的组合问一次是 49.5k credits,等于今日额度的 198%——不拦的话用户只会看到 API 硬失败。
免费模型被上游限流时会自动退避重试两次再放弃。重试只会重试同一个模型,不会偷偷换成贵的——那不叫容错叫烧额度。
⚡ 界面换成暖米色 + 珊瑚色
换掉了原来的黑板暗底,改用 DESIGN.md 那套:暖米色画布、珊瑚色主按钮、衬线大标题、8/12/16px 圆角层级。珊瑚色稀着用——只给主按钮和结论卡的左边条。
抽问卡是整页唯一的深色块,因为它是"讲到这里你要停一下"的那个界面,深色正好把它和讲解区分开。
🐛 修 MiniMax 系的三个坑
选了 MiniMax-M3 / M2.7 之后发现讲稿不对,对照 AI Studio 逐条排查,是三个独立的问题:
- 思考混进正文:MiniMax 要请求带
reasoning_split: true,思考才会单独拆到reasoning_details。不带就会和正文混在一起,直接污染黑板卡片。 - 讲稿被截断:MiniMax 强制思考,而思考 token 和正文共用同一份输出预算。原来按正文需要给预算,思考一开头就把额度吃光。现在已经预留余量。
- 界面上不想看到思考:K12 学生看"思考过程"只会分心,现在直接丢弃(标了
THINK徽章让人知道它会思考,但内容不显示)。
🐛 修复「讲一半就没了」——根因是输出预算给得太小
这次把根因挖出来了:原来每次请求只给 600 tokens 的输出上限。今天的模型普遍带推理,光思考就要 1600–1900 token,于是同一个请求:
| 输出上限 | 结果 |
|---|---|
| 600 | 正文 0 字(思考被砍断,正文根本没机会生成) |
| 2500 | 正常讲完 |
也就是说 600 这个数让所有计费模型都返回空内容。现在两个阶段都给 2500。
同时发现原来的两个默认模型都已经下线(免费档下架),产品开箱即用是坏的。已逐个实测换成可用的:讲解用 nemotron-3-ultra:free(免费),板书用 glm-4.7-flash。选择器里"本次消耗"的口径也按实测修正了(原来低报 2–3 倍)。
另外把 MiniMax M 系从可选列表里排除了:实测它不遵守输出上限,单次要烧 6 万多 credits,比一天的额度还多,而且思考跑十几秒正文才到。列表里会说明为什么排除,不静默隐藏。
🐛 修复「讲一半就没了」(同日的另一批)
用户反馈讲解讲到一半停住。查服务器日志先排除了两个猜测:全站 finishReason=length 出现 0 次、上游 5 分钟超时也 0 次——不是长度掐断也不是超时。真凶是三个独立问题,原来全部是静默的:
- 抽问卡被丢会导致死局:编排"成功"但抽问只有 1 个选项时,这张卡会被规则校验丢掉,而系统不认为这算"降级"。于是时钟卡在抽问上不动,学生看到讲解停在一半、黑板上写着「抽问(正在出题…)」永远出不来,暂停和跳过都是灰的,想走都走不了。现在宁可给一道模板题,也不会没有题。
- 讲稿被长度限制截断:接口其实一直在返回"被截断"这个标记,前端没读。现在会明确提示"这一讲被长度限制截断了,内容可能不完整",而不是让人以为页面卡了。
- 讲解中途断流:之前会拿已经收到的部分继续播(这是对的),但不说,半截稿子看起来就像"本来就这么短"。现在会提示"讲解中途断开了,下面是已经收到的一部分"。
⚡ 免费模型列表:改成实时拉 OpenRouter 官方接口
上面那些"默认模型双双 404"的问题,根子不在默认值选得不对,在免费模型列表本身是手工维护的。
[AI Studio](/ai/AI Studio) 和 memoquest 的免费模型清单,都来自一个 GitHub 仓库导出的 CDN JSON。今天实测那份数据双向失真:
| 数量 | |
|---|---|
| 仓库里有、实际已不是免费/已下线 | 13 个 |
| OpenRouter 现在免费、仓库里没收进来 | 14 个 |
两个方向都在造成真实故障:选到已下线的免费模型 → 判定为免费、跳过配额 → 上游回"该模型已不再是免费模型";新的免费模型不在清单里 → 判定为计费 → 免费模型被静默扣额度。
现在改成直接读 OpenRouter 官方公开接口 GET /api/v1/models(免鉴权、免 key)。由后端拉取过滤后经 GET /api/chat/free-models 下发,前后端共用同一份数据——过去两处各拉同一份 CDN、却各自漂移,才是这个问题的结构。
用户能感知的变化
- 免费模型从 20 个变成 15 个,且每一个都是当下真的免费(旧的 13 个死条目消失了)
- 新增一批之前根本没出现过的免费模型:
Qwen3.8 27B、Thinking Machines: Inkling、Nemotron 3.5 Lightning、Cohere: North Mini Code、Ling 3.0 Flash Fin等 - OpenAI / Google / Anthropic 三家整个不展示(按厂商过滤,gemma 也不放出)
- 徽章更准了:
MULTI(支持多模态)和THINK(思考关不掉)现在从官方元数据直接读,不再靠手写白名单和正则猜
顺带修掉的两个静默 bug
- 免费模型被当成计费模型:不在清单里的免费模型会走"1 credit/token"的兜底计费,用户用免费模型却在扣额度。现在免费判定用全集,免费就是免费,哪怕它不展示。
- 启动失败锁死 6 小时:原来用
setInterval(6h)刷新,部署时启动瞬间一次网络抖动就会让接口整整一轮返回兜底数据。现在失败 1 分钟后重试。
⚡ 用起来是什么样
- 播放控制条:
⏸ 暂停/1x 2x 4x/↺ 重播本段/⏭ 跳过本段 - 支持追问,接着问"那第二条呢?"它记得前面讲过什么
- 历史会话存在本机(20 条上限),点开就能重播,不上云
- 走 Oscar 账号的 AI 额度,与 [AI Studio](/ai/AI Studio) 共用同一个额度池。默认配置下讲解免费、编排约 240 credits 一次
这次没有做什么
- 不做间隔复习(复习表、遗忘曲线、提醒、跨设备同步)。产品没有回访抓手,V0 的结论只覆盖"单次体验好不好用"——先把形态验证掉,回访留给后续数据回答。
- 不做语音朗读(下一版做,需要先给公共
/api/tts路由加流式支持) - 不做图片输入、家长/老师后台、客观题自动评分
- 不接访客通道,未登录直接跳登录页
⚠️ 一条使用提醒
AI 讲错史实对 K12 来说是教学事故,所以讲解提示词里硬性要求:不确定就明说"这个我不太确定,建议查一下课本",不编造年代、数字、出处。不过它终究是 AI,重要史实还是要对一下课本。
试水阶段,欢迎反馈——特别是"黑板上那 3–5 条到底凝练不凝练",这直接决定下一步要不要改讲解和板书的分工。
实用工具站:新增两个工具
同日 工具集 从 7 个工具增加到 9 个。两个都是纯本地工具,不上传任何输入内容。
✨ 正则表达式工作台
把抽象的正则变成看得见的东西(文档):
- 测试文本里实时高亮所有命中位置
- 逐条拆解捕获组——数字组
$1和命名组<year>各自取到了什么一目了然;没参与匹配的组会明确标出而不是留空 - 替换结果预览,变化处标绿,可一键复制
- 6 个标志位可视化开关(
gimsuy),每个都写了释义 - 内置 13 条常用片段,分「网络 / 中文常用 / 文本清洗」三组
- 分享链接把正则和标志位编码进 URL,发给同事打开就是同一条
实现时踩到并已处理的几类坑:零长匹配会死循环、u 标志下 emoji 被 UTF-16 代理对截断、命名组被数字组错位读取、高亮片段未转义导致 XSS 注入。
✨ 图片压缩器
批量压图,每张都给出前后对比(文档):
- 点击或拖入,批量处理,结果逐张列出预览与体积对比
- JPEG / WebP / PNG 三种输出格式,浏览器不支持时自动回退并标注
- 质量滑块 5%–100%,最大尺寸支持
800或800x600(等比缩放,只缩不放) - 整批的原始体积、压缩后体积与总节省百分比汇总
同样处理了几个实际会遇到的坑:转 JPEG 时先铺白底(否则 PNG 透明区变黑)、PNG 无损时质量滑块置灰并说明、压完反而变大时如实标注而不是显示负节省率、HEIC 按白名单拦截并提示先转格式、超大图提前拦在画布上限之外、手机照片按 EXIF 方向自动转正。
✨ 文档站补建「实用工具」栏目
之前 teaching-tools 和 games 的工具都有文档页,唯独工具站一个都没有——9 个工具在文档站里是隐形的。今天补上 /tools/ 栏目与两篇新工具文档(概览,9 个工具一览 + 隐私说明对照表)。
✨ 颜文字大全 上线(文档)
工具站第 10 个工具。796 条颜文字、30 类情境,同时支持按情境浏览和用一句话模糊搜索。
搜「无语到不想说话」「累到不想动只想躺着」「同事让我帮忙但我真没时间」这类自然语言描述,也能找到对的颜文字——这是分类列表做不到的。
模糊搜索怎么做的
每条颜文字入库时,把「分类 + 标签 + 自然语言描述」用 qwen/qwen3-embedding-8b 转成 4096 维向量;搜索时把查询转成同样维度的向量,再和全库算余弦相似度。
刻意不把颜文字本体喂进模型:(⊙_⊙) 这类字符基本是标点组合,混进去只会让「开心」和「难过」的向量互相干扰。喂进去的是语言描述,所以描述必须写得具体——库里写的是「难题终于解决、拍着桌子说这事我能搞定」,而不是「表示开心的表情」。
搜索是混合式的
一次搜索并行走两条路:本地过滤(分类 + 字面)0 延迟先出结果,语义检索在后台跑约 2-3s 再补上,并标注「很接近 / 接近 / 有点像」。选了具体分类时不会混入语义结果。
⚡ 一些工程细节
- 向量存 BLOB 不存 JSON:4096 个 float 存 JSON 约 100KB/条,float32 二进制约 16KB/条,差 6 倍以上。全库 12.8MB,常驻内存暴力算余弦 1-3ms,不需要向量数据库
- 连续输入的竞态:每次输入换令牌让在途请求作废,否则先发后到的结果会盖掉新结果
- 换 embedding 模型要整体重灌:库里记了
emb_model和维度,维度不符的条目会被自动跳过而不是算出错误相似度
🐛 修掉开发过程中抓到的几个 bug
写测试的过程里抓到的,都属于「不测就上线会静默出错」的类型:
- 未命中的条目会混进结果:字面打分函数末尾无条件加了「标签数量」加分,没命中的条目也拿到基础分,而过滤条件是
score > 0——搜一个不存在的词会把全库都返回。前后端都有,已修 - 命名捕获组错位读取:
String.replace回调的参数顺序是[p1..pN, offset, string, groups],groups在最后而不是第二位的+1(那是 string),导致拆出来一堆字符 - 字段名混用:合并脚本产物用
category/tags/desc,而向量化的语料构造函数读的是批次文件的c/t/d简写,结果每条都拼出空字符串、被上游以 "Too small" 400 掉
⚠️ 全量测试仍有一例历史遗留失败
codec 里 htmlEntitiesDecode('&copy;') 的断言期望级联解码成 ©,实际返回 ©。查了实现确认是测试写错了、实现是对的——HTML 实体语义本就是单次解码。留着没动,等确认后再改断言。
⚡ 顺带修掉测试里的一个错误期望
跑全量测试时发现 codec 有一例早就存在的失败断言(与本次改动无关):期望 htmlEntitiesDecode('&copy;') 级联解码成 ©。
查了实现才确认是测试写错了、实现是对的——HTML 实体语义本就是单次解码,浏览器里 &copy; 渲染出来就是字面文本 ©,不级联;代码注释也明确写了"单次替换而非迭代,避免双重解码"。留着没动,等确认后再改断言。
骰子模拟器:换成真 3D 骰子
骰子模拟器(文档)原来是"假 3D"——骰子是一张 2D 方块贴图,动画是 @keyframes roll 里的平面 rotate + scale,转一圈就换一张点数贴图。现在改成真正的立方体(文档)。
✨ 变成了什么
- 每颗骰子是 6 个面拼成的实心 3D 立方体(
preserve-3d),六个面各自有独立的明暗,不是同一张贴图 - 抛掷物理:抛起 → 空中翻滚 → 落地压扁回弹,地面阴影随高度同步缩放淡化
- 固定俯视角(
rotateX(-20deg)),落定后朝上的点数直接看得清 - 多颗骰子错峰抛起(每颗延后 70ms),不再齐刷刷一起动
- 骰子面数少于 6 时,用不到的面留空(D4 就是没有 5、6 点的那两颗面)
- 不支持 WAAPI、或系统开了「减少动态效果」时,直接落定到结果,不播放动画
为什么"朝上的面"一定等于掷出的点数
这是这次重写的核心。不是先转动画再改数字,而是反着来:
- 先掷出值
v - 由
POSE[v]查出"把v这个面转到朝上"所需的欧拉角,作为落定姿态 - 再叠加一个随机的绕竖直轴偏航
竖直轴偏航只改水平朝向、不改哪个面朝上,所以朝上的面和显示的点数在数学上恒等。落定姿态直接写进 inline style,动画结束后自然停在终态。
两个坑都是这么撞出来的:偏航最初取 90° 的整数倍(0/90/180/270),骰子经常"正对镜头"——而立方体正对镜头时只能看到一个面,看上去就退回成一张 2D 方块;改成任意角度后才是自然的 3/4 视角。
🐛 圆角和背面剔除不能同时给
面的 border-radius 会在相邻面接缝处留出小缝,两种处理都会露馅:
| 做法 | 现象 |
|---|---|
圆角 + backface-visibility: hidden | 缝里漏出背景,立方体的角上出现黑缺口 |
| 圆角 + 不剔除背面 | 背面从缝里透出来,骰子中间一道亮缝,像纸盒 |
最终用直角面 + 背面剔除,6 个面才能严丝合缝拼成实心骰子。代价是失去圆角,但骰子本来就是方棱的。
⚠️ 一处行为变化:面数上限从 20 收到 6
原来的「最大点数」允许填 2–20,但立方体只有 6 个面,旧实现其实是用 1–6 的点阵循环显示、总数却按 20 算——两颗骰子画面上显示 3 + 4,结果区却可能写 15,对不上。
现在输入框改为「骰子面数」、范围 2–6,显示和总数天然一致(2–6 面即 D2 / D3 / D4 / D5 / D6)。如果确实需要 D20,那是二十面体、另一种几何体,要另做——说一声就加。