一本397页的教材,考前真正要读的只有99页。剩下的四分之三,翻都不用翻。
这是 Portion Cut 在 OpenStax《Introduction to Python Programming》上跑出的真实结果:它从397页里圈出99页,命中9条考纲中的7条,另外两条——集合与正则表达式——被直接标注为"这本书里大概率没有"。教材目录证实,这两块确实没被收录。
做这件事的,是跑在浏览器标签页里的 EmbeddingGemma 300M。
把考纲和教材对上号,靠的是向量
流程不长。pdf.js 先把每一页的文字抽出来,空白页、目录和索引按简单规则跳过,PDF 书签转成分节的页码范围。每一页再送进 EmbeddingGemma,用文档提示词处理,截断到512个token,借助模型自带的 Matryoshka 训练压到256维,让单本书的缓存不至于太大。
这一步最慢。向量存进 IndexedDB,同一本书第二次贴考纲就是秒出。
匹配时,每条考纲用查询提示词做嵌入。逗号列表会被拆成多个侧面,比如"列表:切片、推导式"会分别去搜"列表切片"和"列表推导式",避免一个强侧面把其他侧面盖住。
判定环节是纯代码,不归模型管。当某条考纲的最佳页面得分没有从全书里跳出来——具体标准是最佳得分低于全书平均分3.35个标准差以上——就报"这本书里大概率没有"。否则保留接近最佳得分的页面,加上每个侧面的最佳页面,再吸附到完整的书签分节上。毕竟没人会单独去读第137页。
最后 pdf-lib 在标签页里拼出精简版PDF。
和关键词搜索比,差在哪
作者拿一份公立大学的 Python 考纲原文做了对照,两份考纲,第一份调参,第二份留作验证。人工先按教材目录标好每条考纲需要哪些小节。
同样的页面规则、同样的调参方式,关键词(BM25)基线在留出的那份考纲上找到16个所需小节中的8个,而且一条缺失的考纲都没标出来。关键词搜索崩掉的地方很明确:考纲和教材用词不一致的时候——而这种情况占大多数。
作者自己也把话说清楚了:只有一本书、两份考纲。"书里没有"这个阈值是看完两份考纲的得分之后定的,所以那三次命中属于拟合,不算测试。它也会漏小节,留出那份考纲16个里中12个,意思是目录页还是得瞄一眼。
为什么非要放在浏览器里跑
课程PDF经常是同学传的文件,或者院系放在网盘上的扫描件。传到别人的API上不太合适。在这里,文字提取、嵌入、缓存全程不出标签页,模型下载完之后网络面板看不到任何外发请求。
成本也是零。400页的书就是400次嵌入调用,每换一本教材又是400次。权重放在浏览器里,托管方不花钱,学生用也不花钱,整个部署就是一个静态的 GitHub Pages 站点。
首次访问之后能离线用。模型被浏览器缓存,一个小 service worker 守住应用外壳,宿舍Wi-Fi抽风或者干脆没网也能开。
开放权重还救了它一次。默认的4位模型文件用了一个 WebAssembly 运行时不支持的算子,直接加载失败。因为权重是开放的,作者换到同一仓库里的 model_no_gather_q4 变体就继续了。换成闭源端点,就只能等别人修。Matryoshka 截断同理——能选256维,是因为向量的存储方式自己说了算。
慢是真的慢。整本书第一次读,CPU 四线程 WebAssembly 跑了大约15分钟。托管API会更快,但这是每本书一次性的成本,泡杯茶的工夫,之后每份考纲都是秒出。
省下来的时间
样本考纲的阅读清单覆盖397页里的99页,约四分之三可以跳过,两条考纲在浪费时间去找之前就被标了出来。留出的那份考纲是75页。
它不总结教材,也不替你写笔记。它只做一件事:让你别去读那些本来就不用读的页。
热门跟贴