我如何过度工程化我的书
摘要
一位开发者描述了使用 Git、Markdown、CI 管道和代码检查工具来过度工程化他们的书籍写作和发布过程,自动化检查和多种格式构建。
暂无内容
查看缓存全文
缓存时间: 2026/08/17 18:54
# 我如何过度工程化我的书
来源:https://ben.balter.com/2026/08/17/how-i-over-engineered-my-book/
我的书有个“监工”,会因为我把“open source”(开源)用连字符连接而对我大吼大叫。[1](https://ben.balter.com/2026/08/17/how-i-over-engineered-my-book/#user-content-fn-opensource) 它运行着约5,500项自动化检查,每次`git push`都会重建五种格式,并且如果我暗示自己仍然在职于一份早已离开的工作,构建就会失败。为了这本书。一本由一个人写的书。
我并非有意为之。大多数人开始写作项目时会启动Word或Google Docs,我最初也走了同样的路。我甚至尝试过一些专门为写作设计的工具,但每个工具都感觉不如我作为开发者每天使用的工具。于是我做了“唯一合理”的事情——把它们全部扔掉,用Git、Markdown和CI构建管道来写整本书。如果你读过我如何过度工程化我的家庭网络[https://ben.balter.com/2020/12/04/over-engineered-home-network-for-privacy-and-security/]——两次 [https://ben.balter.com/2021/09/01/how-i-re-over-engineered-my-home-network/]——这一切都不会让你惊讶。
我做网站已有数十年,所以跨越这一步并不困难:构建网站的同一套工具也能构建书籍,无需最后时刻变魔术般把标准Word文档变成一本完整的书。中间有过一些失误,[2](https://ben.balter.com/2026/08/17/how-i-over-engineered-my-book/#user-content-fn-latex)但最终,我无法以其他方式写出《Open and Async》[https://open-and-async.com/]。以下是方法:
## 内容\# (https://ben.balter.com/2026/08/17/how-i-over-engineered-my-book/#content)
*自然*,内容本身以Markdown文件形式存放在Git仓库中。毕竟,这是我每天花费大部分时间的地方。我使用VS Code,配合几个写作扩展插件([如下所列](https://ben.balter.com/2026/08/17/how-i-over-engineered-my-book/#real-time))。每章都是独立的Markdown文件,一个单独的`index.yml`文件定义了顺序,使得重新排列章节或添加新章节变得容易。
实际上,我大部分书稿是在iPad上写的——浏览器标签页中的Codespaces、蓝牙键盘,常常在远离办公桌的夜晚和周末——Git仓库确保无论我在哪里打开,所有内容都保持同步。我可以专注于文字,一个坏主意只需一个`git revert`就能消失。
更不用说,我在IDE中直接获得了来自各种散文检查器的实时写作反馈,就像我从ESLint或Prettier获得代码的实时反馈一样。
## 测试\# (https://ben.balter.com/2026/08/17/how-i-over-engineered-my-book/#testing)
内容即代码,下一个合乎逻辑的步骤——也是“合理”悄然溜走的地方——是建立自动化测试。[像测试代码一样测试散文](https://ben.balter.com/2015/05/22/test-your-prose/)是我多年来一直主张的;这次我把它推向了荒谬的极端。我通过两种方式实现:实时和推送时(CI)。
### 实时\# (https://ben.balter.com/2026/08/17/how-i-over-engineered-my-book/#real-time)
在本地,当我打字时,我运行多个VS Code扩展,它们都给我实时反馈。具体包括:
- [Markdownlint](https://github.com/DavidAnson/markdownlint) — Markdown语法和格式一致性
- [Harper](https://github.com/Automattic/harper)[3](https://ben.balter.com/2026/08/17/how-i-over-engineered-my-book/#user-content-fn-cspell) — 语法和用词选择,完全在设备上运行
- [LanguageTool](https://languagetool.org/) — 语法、标点和风格[4](https://ben.balter.com/2026/08/17/how-i-over-engineered-my-book/#user-content-fn-languagetool)
- [Vale](https://vale.sh/) — 我自己的风格规则和禁用词
- [Alex](https://alexjs.com/) — 不敏感或排他性的措辞
- [Write-good](https://github.com/btford/write-good) — 弱表达:被动语态、含糊词、陈词滥调
所有六个工具也在CI中运行(Alex和Write-good在那里被整合进Vale;[下文详述](https://ben.balter.com/2026/08/17/how-i-over-engineered-my-book/#on-each-push))。它们共同在完整的语法引擎之上叠加了数百条精心策划的风格规则——所有这些都实时标记我的错误,就像红色波浪线标记类型错误一样。
### 每次推送时\# (https://ben.balter.com/2026/08/17/how-i-over-engineered-my-book/#on-each-push)
除了在CI中运行这些开源检查器(其中一些是阻断性的),我还构建了自己的定制测试套件:一个独立的Node脚本用于内容验证器,一个Vitest套件,以及Playwright规范。验证器是有趣的部分。每个验证器只有几行代码,读取Markdown并推送一个带有文件和行号指针的错误。我最喜欢的一个:我不再在GitHub工作了,所以任何声称我*仍然*在职的句子都会导致构建失败。
```javascript
// 我在GitHub的时间必须用过去时态表述——
// 关于它的现在时就业声明会导致构建失败。
function validateGitHubTense(files) {
const patterns = [
// "is/are ... at GitHub" — 当前就业声明
/\b(is|are)\b[^.!?\n]{1,80}?\bat GitHub\b/i,
// "works/leads/runs at GitHub" — 当前就业活动
/\b(works?|leads?|runs?|manages?|directs?)\s+(at|for)\s+GitHub\b/i,
];
return flagLinesMatching(files, patterns);
}
```
这个验证器存在是因为我曾经犯过那个错误——这就是整个模式。第一次错误从我这里溜走时,我不仅修正了那个句子;我还写了一条规则,这样我就不必再靠眼睛去捕捉它。这是散文的[“牛群,不是宠物”](https://cloudscaling.com/blog/cloud-computing/the-history-of-pets-vs-cattle/):不要手工照料每章,用策略管理整个牛群。一次捕捉到的错误变成了一个检查,扫描每一章,并在它再次出现时导致构建失败。这只是大约三十个检查之一。其他我引以为豪的包括:
- **`validateOpenSourceHyphenation`** — “open source”是名词,不是[动词](https://ben.balter.com/2012/10/15/open-source-is-not-a-verb/),并且绝不使用连字符。
- **`validateHypotheticalHooks`** — 段落开头的公式化AI提示开头(“想象一下……”、“设想……”、“考虑一个……”)。
- **`validateSentenceStarters`** — 连续三句以上以相同单词开头。
- **`validateNoBareUrlLinkText`** — 没有链接的可见文本是原始URL。
- **`validateCalloutBalance`** — 这本书面向管理者和个人贡献者,因此一个“给管理者”的提示附近必须有一个“给个人贡献者”的对应部分。
- **`validateCrossReferences`** — 每个`\[text\]\(\#anchor\)`交叉引用都指向一个真实的标题。
……另外还有几十个用于小型大写字母、长破折号、重复单词、短破折号范围、TL;DR长度以及我能想到的其他小习惯的检查。[5](https://ben.balter.com/2026/08/17/how-i-over-engineered-my-book/#user-content-fn-validators)
其中一个抓到了*我自己*。`validateHypotheticalHooks`标记了一段我确信是自己写的文字的开头——我确实写了。但冷静地重读它,它听起来确实像代笔的;我吸收了太多生成文本的节奏,产出了一种流畅的模仿,却空洞无物。检查器无法区分好散文和坏散文。但它能标记的是当你停止思考时所依赖的模式——而这正是你在自己的草稿中无法看到的东西。同样的原因,`eslint`物有所值:它也无法区分好代码和坏代码,但它能抓住你自己眼睛一扫而过的自动驾驶错误。
总的来说,CI套件运行了约70个测试文件,包含2,204个测试用例和约3,900个`expect()`断言——另外还有来自上述验证器的每章约1,600次结构检查。
## 审计\# (https://ben.balter.com/2026/08/17/how-i-over-engineered-my-book/#audits)
检查器抓住了错误,但它们无法告诉我这本书是否重复自己,或者某章是否写得好。为此,我构建了第二层工具:审计工具,它们评判写作,而不仅仅是检查它。有一件事它们从未做过:代笔。每一个词都是我的;这些工具是最后的读者——抓住我已经无法看见的东西。
### 重复检测\# (https://ben.balter.com/2026/08/17/how-i-over-engineered-my-book/#duplication-detection)
反复阅读这本书后,我*确信*我在不同章节重复了相同的想法。我想要证据,而不是直觉——所以我构建了三层重复检测,每一层都抓住前一层遗漏的东西:
- **[`jscpd`](https://github.com/kucherenko/jscpd)** — 令牌级复制粘贴检测。抓住我在章节间粘贴的较长逐字块,但更微妙的重复则无能为力。[6](https://ben.balter.com/2026/08/17/how-i-over-engineered-my-book/#user-content-fn-dry)
- **[n-gram](https://en.wikipedia.org/wiki/N-gram)** — 对`index.yml`中的每一章进行分词,去除Markdown/Pandoc语法,构建单词n-gram(n个词的短语),并标记出现在多章中的短语(加上一个“最重复的惯用表述”排名)。两种模式:跨章节(默认`--n`)和章节内(`--scope=intra --n=8`)用于检测章节内的自我重复。
- **语义**——前两个无法做到的:用不同词语陈述的*相同要点*。一个按需运行的LLM审计,设计上从不将整本书喂给模型——三次传递,每次处理小单元:
- **章节内**——每章一次调用:“这章在哪里重复了自己?”
- **跨章节**——对每章的TL;DR进行一次调用,生成概念上重叠的章节图。用一次调用的成本扫描整本书。
- **论点**——每次调用提取每章的一个核心论点,然后在最后一次调用中将所有章节的相同论点聚类。抓住`cross`遗漏的正文中提出的论点。
我真的在重复自己吗?在最后这遍扫描中,两个机械层没有发现问题——但它们理应如此:`jscpd`和n-gram扫描已经抓住了复制粘贴和重复使用的措辞(这里的一个双倍主题,那里的一个重复的TL;DR结构),而我已经修正了每一个。
两者都看不到的是更微妙的那种——不再有任何逐字重复,只是用不同词语陈述的*相同要点*。语义传递抓住了这一点,而且它是对的:我在*五个*不同的章节中提出了相同的论点,即“将办公室习惯搬上网并不等同于远程优先工作”,此外在51章中还散布着166处较小的自我重述。**我把自己转述得太好了,以至于比LLM便宜的任何工具都无法抓住我。**[分享此引用](https://ben.balter.com/2026/08/17/how-i-over-engineered-my-book/#quote-paraphrased-myself)
我的直觉是正确的;我只是需要三个逐步升级的工具来证明,反复阅读自己写到眼花缭乱也无法证明的东西。
### 内容审计\# (https://ben.balter.com/2026/08/17/how-i-over-engineered-my-book/#content-audits)
除了重复检测,第二组工具对散文本身进行评分——根据评判方式划分。有些是**概率性**的(LLM阅读章节并形成观点;运行两次,发现可能改变),有些是**确定性**的(规则和算术——相同输入,每次输出相同)。
#### 概率性(LLM)审计\# (https://ben.balter.com/2026/08/17/how-i-over-engineered-my-book/#probabilistic-llm-audits)
“主张”视角早期就物有所值。它在一个句子上停下,该句声称GitHub的月度全体员工会议“每次会议大约一半时间用于实时问答”——这个数字我曾确信是正确的,但它是错的;更接近三分之一,而且这个比例逐年变化。没有检查器会标记这个:它是一段清晰、自信但碰巧错误的散文。只有读者问“这真的正确吗?”才能抓住它,否则我会把它发布出去。
“主张”是“散文审计”测试中的**20个单一视角**之一——一个按章节运行的LLM审计器,每个视角只问一个狭隘的问题,这样模型就无法含糊其辞地说“看起来不错”。`--lens=all`运行所有视角;`--models`/`--rounds`添加去重的多模型和自一致性小组,因此一个发现必须经受住不止一个模型(或不止一次运行)才算数。其他一些示例:[7](https://ben.balter.com/2026/08/17/how-i-over-engineered-my-book/#user-content-fn-lenses)
| 视角 | 它捕捉什么 |
|------|------------|
| AI提示 | 读起来像机器生成而非我本人风格的措辞 |
| 钩子 | 开篇是否值得其位置,还是只是清嗓子 |
| 法律 | 法律或声誉风险(点名、未经证实的说法) |
| 过时 | 会过时的参考(“最近”、“今年”) |
| 全球 | 让非美国读者困惑的习语和文化假设 |
| 双受众 | 一章是否同时服务于管理者*和个人贡献者* |
| 承诺 | 该章是否兑现了书的核心承诺 |
另外,我还进行了一项特质分析测试,根据说服力和参与度特质对每章进行评分,抓住技术上干净但平淡的散文,以便我可以再进行(人工)润色。
#### 确定性审计\# (https://ben.balter.com/2026/08/17/how-i-over-engineered-my-book/#deterministic-audits)
我在一轮编辑后运行这些,以查看书籍作为阅读体验是否在改进:章节和段落长度分布、整本书阅读时间、严格的EPUB大小预算(Kindle会对超大文件在交付时进行惩罚)、全书一致性检查器,以及一个统计仪表板,其`--check`模式只有在每个“关注项”——缺失TL;DR的章节、不平衡的提示——都为零时才通过构建。[8](https://ben.balter.com/2026/08/17/how-i-over-engineered-my-book/#user-content-fn-deterministic)
所有这些加在一起,将“这本书实际上是否变得更好?”从一种直觉变成了一个我可以观察其在草稿间移动的数字。
## 设计\# (https://ben.balter.com/2026/08/17/how-i-over-engineered-my-book/#design)
我*远*非设计师,以前也从未出版过电子书——除了读过很多电子书,我完全不知道它们是如何运作的。两个“顿悟时刻”改变了我对出版的看法:
- **[电子书是穿着风衣的网站](https://ben.balter.com/2026/08/17/how-i-over-engineered-my-book/#quote-ebook-trench-coat)** — 电子书就是HTML和CSS,尽管是非常精简的版本。[9](https://ben.balter.com/2026/08/17/how-i-over-engineered-my-book/#user-content-fn-kindlewebkit)如果你能做网站,你就能做电子书。
- 只要付出足够努力,纸质书也可以这样——CSS本身就有强大的`@media print`和`@page`规则,包括左右页样式、标题页、页码等等。
对于内页设计,我选择了[Tailwind CSS](https://tailwindcss.com/)——不是因为它专为书籍构建(它不是),而是因为这是我每天已经在使用的。`@tailwindcss/typography`给了我一个排版基线,而不是从空白页开始。
有一点:我特意请了一位*人类*设计师来设计封面。对于一本关于真实性的书,第一印象必须真实。[10](https://ben.balter.com/2026/08/17/how-i-over-engineered-my-book/#user-content-fn-cover)
### 不可见的泄漏\# (https://ben.balter.com/2026/08/17/how-i-over-engineered-my-book/#the-invisible-leak)
从一个样式表为所有格式设置样式有一个故障模式:为一种格式设计的CSS会泄漏到另一种格式。为了将我的电子阅读器调整与平装本区分开,我将它们都限定在`@media not screen`内。但是WeasyPrint([https://weasyprin
相似文章
我绕过Adobe和Microsoft,搭建了一个Git追踪的图书生产流水线
一名软件开发者描述了如何利用开源工具搭建一个由Git追踪的图书生产流水线,绕过了传统的Adobe InDesign和Microsoft Word工作流程,用于自助出版小说。
我决定回归手写代码
作者在重构一个 Kubernetes 仪表盘工具时反思道,虽然借助 AI 进行“氛围编程”(vibe-coding)能加速功能开发,但在缺乏人工监督的情况下,往往会导致架构臃肿和技术债务。
用机器重建我的博客,为机器服务·
作者重建了博客,加入了完整的结构化数据标记(JSON-LD、微格式),并配备了一个由提示词引导的AI协作写作助手,该提示词避免了常见的LLM模式,同时通过CI验证防止数据损坏。
每月110美元的自优化流水线(5分钟阅读)
一位开发者分享了他们每月110美元的自动化流水线,该流水线使用Claude AI对GitHub issue进行分类、分解、实现和测试,在两周内完成了27次合并,且故障极少。
软件界的Emacs化
作者讲述了在终端中阅读 Markdown 的烦恼,并描述了如何使用 Claude 快速构建一个自定义的 macOS Markdown 查看器(MDV.app),展示了 AI 如何让人能够迅速创建个人软件工具。