跳到正文

book-to-skill

一个转换器:把一个文档——单文件、文件夹、glob 或一串路径,格式涵盖 PDF、EPUB、DOCX、HTML、RTF、MOBI 与纯文本——读成一份 agent 技能:一份讲心智模型、带章节索引的核心文件,每章一个只在问题碰到它时才加载的文件,旁边配术语表、模式库与决策速查。

Screenshot of book-to-skill
编辑截图, 1 Oct 2026book-to-skill ↗

这是什么

book-to-skill 把文档变成 Agent Skills——也就是 GitHub Copilot CLI、Amp、Claude Code、Hermes Agent、OpenCode 与 OpenClaw 都读的那套 SKILL.md 目录格式。它是仓库拒绝混为一谈的两半。一半是确定性的 Python 抽取器,接受一个文件、一个文件夹、一个 glob 或一串路径——PDF、EPUB、DOCX、HTML、RTF、MOBI 或纯文本——产出合并后的 full_text.txt 与一份记着页数、词数、token、章数与目录的 metadata.json;每种格式都有标准库退路,一个读不动的来源会被跳过而不是拖垮整批。另一半是读者自己那个编码 agent 照着 55,009 字节的规格去蒸馏:一份讲心智模型与主题索引的核心(约 4,000 token)、每章一个约 1,000 token 的文件,外加术语表、模式库与决策速查。它旁边还有 analyze-only、fold-in 与发布三种模式。这一切底下是一笔 token 账:会读 PDF 的 agent 每一轮都要重新处理一遍目录,而它把这份结构化的成本只在转换时付一次。

谁做的一位个人维护者,其账号写了 202 次提交中的 89 次。GitHub 一共列出 41 位贡献者,除作者之外最多的是 dex0shubham 的 23 次,其后是 Hotragn 的 11 次、Stamina9 的 10 次,以及一个依赖机器人的九次。账号本人的提交署过三个名字——Virgilio Junior、Virgilio Borges 与账号名本身——所以按名字分开看会比总数小。86 条提交带共同作者尾注,其中多数名字是工具而不是人:Claude Opus 5 二十一条、Claude Opus 4.8 十六条(另有七条标注百万 token 上下文)、Copilot 九条、Amp 三条、Cursor 一条。

它是怎么搭起来的

组成 · 6

两个程序加一份它们之间的契约,就是全部设计,而这条分界是刻意划的:凡是能确定性化、能测试的部分都写成 Python 程序,凡是必须真的读一本书的部分都交给读者本来就在用的那个 agent,由一份写下来的规格驱动。这样一来,有风险的那一半——抽取、章计数、清洗——留在模型判断之外,而生成那一半留在代码之外,于是转换行为可以靠改 SKILL.md 里的某一步来调整,不必发版。产物的形状则来自一句经济学断言:贵的不是读书,而是反复重新导航,所以导航只在转换时做一次,并存成索引。这也是为什么一份生成出来的技能是一小页心智模型、一个主题索引和一叠章节文件,也是为什么这个项目用省下的 token、而不是用转换过的书来衡量自己。它周围还有两个塑造代码的习惯:每种格式都有标准库退路,于是缺一个可选依赖是降级而不是失败;以及仓库里每一个说法都必须可复现,所以这里测试与评测脚手架比产品代码还多。

SKILL.md
生成的那一半:55,009 字节,也是这棵树里当散文读的最大一份文件。按 Steps 0–10 编号,外加一个把新材料并进已有技能的 fold-in 流程。它会在每一次转换时被加载进 agent,所以仓库把它的任何净增长都当成一件必须自己挣回上下文成本的事。
book_to_skill/
抽取器包:九个模块、97 KB,其中 utils.py 就有 65,957 字节——命令行解析、多来源解析、章与目录检测、运行器——另有配置、一个回答 --check 的可选依赖探测、一个让单个来源失败不至于拖垮整批的异常模块,以及 6,357 字节的文本清洗。
book_to_skill/parsers/
一个格式一个模块,八个文件、38 KB:PDF(含 Docling 路径与各层退路)、EPUB、DOCX、HTML、RTF、Calibre 与纯文本。每个都优先用某个库、再退回标准库,而报告里修掉的大多数 bug 就长在这些退路分支上。
tools/ 与 evals/
三个程序加一个子包:14,278 字节的 scan_generated_skill.py、11,707 字节、带按 host 分镜头的 validate_skill.py、9,177 字节、用来量「整本塞进上下文」与「按索引查一次」差多少 token 的 discovery_tax.py,以及一个含清单、打分器、回放工具与平铺论文基线的 evals/ 包。
tests/
四十二个文件、314 KB,其中一个单文件测试模块就有 111,646 字节——比仓库里任何源码文件都大。夹具是合成书与录下来的轨迹;另有五个测试被可选依赖挡在门外,而这件事本身就开着一个 issue:持续集成只装 pytest,于是这五个每次运行都被跳过。
docs/ 与仓库根
docs/ 下十个文件——领头的是一份 25,559 字节、放在 docs/research/ 里的评测台账——外加一份 MkDocs 配置与一个部署工作流;这一带里有 1,106 KB 是这个项目那位法师吉祥物的七张图。仓库根上放着 5,198 字节的执行契约 AGENTS.md、三份 README、一份 15,915 字节、被明确要求不许手改的 changelog、安全政策与安全通告,以及三个工作流。

取舍,以及它替代了什么

  • 先问这是什么书,再为对的抽取器付钱 替代 所有文档共用一个抽取器

    仓库自己量出来的那张表就是理由:Docling 要 164 秒,而快的那条只要 0.1 秒,产出的 token 数一模一样,所以它不是质量上的默认选择——它是为带表格和代码的书买的,在那里它救回 48 张表和 36 段代码块,而快的那条一张都不剩。这个选择在抽取之前的 Step 1.5 就问清楚。

  • 存一份结构索引,而不是摘要,也不是原文 替代 把段落存起来,提问时再去检索

    架构文档与规格从两头说同一件事——抽取具名的框架、决策规则与反模式,绝不抽原文段落——而版权那一节就是它的收益:一份生成出来的技能被描述成读者自己的笔记,工具因此可以声称自己不装载任何书籍内容。禁止抄录的那条质量规则在许可证论证里被点名引用,所以法律立场是长在生成规则上,而不是摆在它旁边。

  • 章节文件按需加载 替代 一本书一个大文档

    成本只在问题落到的地方付:核心文件约 4,000 token,每章约 1,000,而只有主题索引把 agent 指过去时才会读那一章。布局写明的理由是压缩——核心写成前面重、后面轻,因为截断是从末尾开始的,这是 host 的性质,不是书的性质。

  • 保留退路,但让它说出来 替代 要么砍掉可选依赖,要么继续让退路一声不吭

    每种格式都能退到标准库,而一份报告说明了这样做的代价:在技术模式下,Docling 装了却在运行时失败时会被当成没装,整轮以退出码 0 走过平铺文本、没有表格也没有代码块,而元数据里仍然写着 technical。修法保留了退路和退出码,改为给每个来源记一个机器可读的原因——不可用、为空、失败——于是生成器在拿纯文本造技术技能之前可以先问一句。

  • 假设在通过关卡之前不进产品 替代 因为听起来有道理就把它加上

    仓库的 agent 契约禁止在关卡通过之前把论文来的假设变成生产行为,点名了不许凭「听起来对」添加的东西——某种形状的元数据键、library 模式、更深的 routing、更多 SKILL.md 内容——并要求任何关于质量、token、路由或成本的说法都要有可复现证据,而不是断言。评测纪律也写了下来:先跑单元测试再跑真机实验,先跑小而有判别力的样本再铺开,按来源与配置缓存生成结果,运行前先把成本上限登记好。

依据docs/architecture.md 与 docs/how-it-works.md(两份都在 recon 报告里全文打印)、同样全文的 AGENTS.md 与 README.md、日志里引用到的各 issue 与 pull request 正文,以及完整的 119 个文件树及其体积。

制作过程

6 个阶段
  1. 01

    两半:一个确定性的抽取器,与一份 55,009 字节的规格

    架构文档把这条分界写成规则,而不是一句描述:一半是确定性的 Python 抽取器,另一半是由读者手上那个编码 agent 执行的规格驱动生成器。SKILL.md 就是后一半,也是这棵树里当散文读的最大一份文件——55,009 字节,只比抽取器那份 65,957 字节的 utils.py 和一个 111,646 字节的测试模块小。它按编号步骤写成:Step 1.5 先问这本书是技术书还是文本书,Step 2.6 是给大部头用的读-求值-打印循环,用 grep 和切片代替整本重读,Step 7 按「书类型 × 深度」的预算写每章摘要,Step 9.5 反过来扫描刚写出来的文件。抽取器那一半更常规:parsers/ 是八个模块、38 KB,sanitize.py 6,357 字节,而 scripts/extract.py 作为一个 1,333 字节的壳留着,好让旧的调用方式继续可用。它的产物是每次运行的工作目录里两个文件——带来源标记的合并文本,与一份装着各项计数的元数据——转换结束后这个目录会被删掉。命令面比一条命令宽:analyze-only、generate-from-analysis,以及把新材料并进已有技能的 update/fold-in 模式,另有一个可选的发布步骤,把结果放进一个私有 GitHub 仓库,于是任何 host 都能用 npx skills add 装上。

  2. 02

    读一本书要花多少,转换一本书要花多少

    抽取器按书的类型挑工具,而不是每个都试一遍。散文走 pdftotext,退一步是 pypdf 与 pdfminer.six,三者都快到可以忽略。带表格和公式的书走 Docling,大约每页 1.5 秒;仓库把这笔交换写成了一张表:在一本 103 页的技术书上只用 CPU 跑,pdftotext 用 0.1 秒产出 27K token,表格和代码块都是零,而 Docling 用 164 秒产出同样多的 token(多 1.2%),却保住了 48 张表与 36 段代码块,且都转成 markdown。接着是四次真实转换,列出页数、抽出的 token、自动识别到的章数,以及按 Claude Sonnet 4.5 每百万 token 三美元进、十五美元出估算的一遍成本:Think Python 2 是 244 页、119K token、十九章、约八十八美分;Working Backwards 是 371 页、175K token、十章、约九十六美分;Pro Git 是 501 页、229K token;Moby-Dick 是 EPUB、301K token。最后两本带着那句诚实的脚注:自动识别章需要明确的「Chapter N」或「Capítulo N」标题,而 Pro Git 用的是小节标题、Moby-Dick 用的是章名与罗马数字,所以两本都不会自动分段——抽取与转换照常可用,只是要由人点章节。

  3. 03

    章检测才是这个仓库真正生活的地方

    章的数量决定了生成出来的技能长什么样,而报告里三十条 issue 与 pull request 中有十三条都在讲「什么算一章」或「怎么数」。多语言那一半是同一份补丁反复提交:马拉雅拉姆语、古吉拉特语、奥里亚语的章标题,每一次都加一条正则、一张本族数字到拉丁数字的映射、一个紧挨着已有婆罗米系分支的派发分支,以及挨着邻居放的测试;其中三条出自同一位贡献者,他有 23 次提交,仅次于作者本人。另一半是误判,也更有意思。「Chapter 6.」——一句在列宽处被折断的话,续行正好以交叉引用开头——被当成了一章;一段被围栏包起来的 unified diff 也被当成了一章;而在结构化那条路径上,规则是取「至少有两个不同标题的最浅标题层级」,于是前言、目录、附录、术语表、参考书目与索引会和真正的二十章一起被数进去。反过来,两个叫「Part」的标题又会把数字压塌:一本十章的书被报成两章。维护者用合成输入把每一条都复现了一遍,然后把它们拆开:数字误判进一个改动,结构化选择以及「到底是哪些标题产生了这个数」的样本进另一个,而那种按标题前缀一刀切的排除被拒绝了,因为它会把真正在讲课的「Introduction」一章删掉。

  4. 04

    一个晚上八份报告,每一份都拿真实依赖复现

    2026-09-30 到 2026-10-01 之间,同一个账号提交了报告里最后八条中的全部八条,每一条都用真实依赖而不是 mock 做出复现。ebooklib 0.20 按 manifest 顺序抽取 EPUB、忽略声明的 spine,把章节顺序打乱,而标准库那条退路反而是对的;python-docx 1.2.0 会把所有顶层段落排在所有表格之前,于是夹在第一章与第二章之间的表格跑到文末;在 Windows 上,一个普通 CRLF 换行的 Markdown 源文件会被写成双回车,之后普通的 Python 读取会在每一行之间看到一行空白,而写入之前打印的分段数仍然是对的;BeautifulSoup 会在每个文本节点之间插入换行,包含行内节点,于是一份合成的两章文本只要写了 Chapter <span>1</span> 就检出零章,并且照样干净退出。另外三条打的是那个让中断的运行复用已抽取语料的判断:抽出的文本被删掉、清空或换成无关内容时,reuse_is_safe() 依然回答工作目录完好、所有来源都与当前输入一致;而当两个不同目录下的输入同名又同哈希时,被替换掉的第三个文件根本不会被检查。报告者对结论很克制——失败的是这个辅助函数的判断,而不是已知生成过错误的技能——而这个缺口正好是上一轮改动留下的缝:每个来源的文件名与哈希都被记下并核对,却没有东西核对输出。

  5. 05

    它拒绝拿文件做的事,与它拒绝出厂的东西

    扫描件是用「停下来」处理的,而不是用「再努力一点」:PDF 没有文本层会被开头几页挡下来,带着一句说明退出,而不是把整本书跑一遍产出一个空技能;README 让读者先自己去跑 ocrmypdf。版权也是同样的处理方式,而整个设计正建立在它上面:这个工具不装载任何书籍内容,抽取在读者自己的机器上跑,一份生成出来的技能被定义成结构化的派生物——框架名、定义、要点——因为规格直接禁止抄录原文段落;仓库对自己用同一套规矩,绝不提交受版权保护的书籍原文,只用合成的或有明确许可的夹具,原因就在这里。安全工作沿着文档进入 agent 的同一条路径铺开:每个解析器的输出都会先剥掉不可见字符与零宽字符,再进入任何计数;声明了 DTD 的 DOCX 部件在解析前就被拒绝;传给 pdftotext 或 ebook-convert 的路径会先绝对化,好让以短横线开头的文件名不会被当成参数;生成器最后还会做一次劝告性扫描,而它只报规则与位置,从不报匹配到的原文。当一位读者证明有九个 Default_Ignorable 码点穿过了这道清洗时,维护者确认了这个缺口,拒绝了按格式字符一刀切的做法,理由是那会连语义控制字符一起删掉,并把这件事称为一处覆盖缺口、而不是一次成功的提示注入,同时请对方把后续细节走私密漏洞报告渠道。

  6. 06

    五个 release、安静的九月,以及材料没有解释的 33,182 个星

    第一次提交的时间戳比仓库本身还早六秒——它是从一个本地工作副本推上来的,而不是在平台上从零开始。此后是 151 天里的 202 次提交,而月度曲线更像是对事情作出的反应,而不是排好的日程:五月二十次、六月六十二次、七月十九次、八月七十四次、九月二十七次。头三个月出了五个 release,之后再没有过:v1.0.0(2026-06-08)、四天后的 v1.1.0、2026-06-17 的 v1.2.0(自我描述为可安装的包加多语言章检测)、2026-07-30 的 v1.3.0,以及 2026-08-10 的 v1.4.0。九月换来的是二十七次提交、一大段 triage 和零个 release——最后一次提交是一位贡献者的 EPUB spine 修复,2026-09-29 合入——所以这份记录写的是活跃,而不是已完成。文档这边,一位评审指出几份译文 README 已经偏离了英文版的落点政策,每种语言都钉着它翻译时所依据的那次修订,而没有任何东西在管这个钉子,修法被接受,但带两个条件:钉子只能移到真正被审阅过的那个来源上,而且不得有任何检查依赖 git 历史,因为浅克隆会让它失败。材料里没有的,是对 33,182 个星的任何解释:关于它怎么传开的,仅有的痕迹是 README 头部那两枚指向 27038 号仓库的 Trendshift 徽章、一个索引只收一行 pull request 的用例仓库,以及 3,466 个 fork 对 41 位贡献者这个比例。

相关档案

全部档案 →