Harper 深度拆解:Automattic 用 Rust 造了个「本地 Grammarly」,毫秒级响应、零上传,凭什么单日暴涨 880 Star
一、背景:语法检查器的「云端绑架」,终于有人受不了了
先说一个大多数程序员都经历过、但很少认真想过的场景:
你在写 README、写技术文档、写 commit message、写英文邮件。你装了 Grammarly 浏览器插件,或者在 IDE 里挂了 LanguageTool。每敲一个字符,你写的内容——包括还没发出去的邮件草稿、内部文档、甚至代码注释里的业务逻辑描述——都被完整地发送到了别人的服务器上。
Grammarly 的商业模式决定了它必须这么做:模型在云端,检查在云端,你的文本自然也得去云端。LanguageTool 虽然有自托管方案,但那是一个 Java 应用,完整部署要吃掉数百 MB 到数 GB 的内存,检查一段文本的延迟经常以秒计。对于「我只是想检查一下拼写和语法」这个朴素需求来说,这两条路一条出卖隐私,一条重得离谱。
2026 年 7 月下旬,GitHub Trending 上有个项目突然发力:Automattic/harper,一个纯 Rust 编写的离线语法检查器。7 月 24 日它的日增 Star 还是 247,25 日直接冲到 880,翻了 3.5 倍,总量突破 13,000。要知道它背后站着的是 Automattic——WordPress.com 的母公司,全球互联网 40% 以上网站所用 CMS 的商业主体。
Automattic 收编 Harper 这件事本身就很有信号意义:一家靠「内容发布」吃饭的公司,选择把语法检查这个环节彻底本地化、开源化。这不是慈善,是对「写作工具的数据主权」这个命题的正面回答。
这篇文章我会把 Harper 拆开讲透:它的架构如何设计、为什么能做到毫秒级响应、Parser/Linter 两个核心 trait 怎么玩、如何在 Neovim/VS Code/浏览器里落地、以及如何用 Rust 给它写一条自定义规则。
二、核心概念:把「英语」当成一门需要 Lint 的编程语言
Harper 的设计哲学可以用一句话概括:英语是一门语言,那就用对待编程语言的方式给它做 Lint。
这个类比不是修辞,而是实打实的工程决策。Harper 官方文档里明确说:「Harper 试图做好一件事:找出英文文本中的语法和拼写错误,并尽可能给出修正建议。一个错误加上它的候选修正,构成一个 Lint。」——这就是 ESLint、Clippy 们的心智模型,原样搬到了自然语言上。
这个模型带来三个直接好处:
1. 确定性。 传统 NLP 方案(包括 Grammarly 背后的深度模型)是概率性的,同一段文本两次检查可能给出不同建议。Harper 的规则引擎是确定性的:同样的输入永远产生同样的 Lint 列表。对于要集成进 CI、pre-commit hook 的场景,确定性是刚需。
2. 可解释、可关闭。 每条 Lint 都来自一条具体规则(Rule),每条规则都有名字、有文档、可以单独开关。这和 eslint-disable-next-line 是同一种工程体验。而云端黑盒给你的只有「建议改成这样」,你没法问为什么,更没法精确地关掉某一类误报。
3. 轻。 没有神经网络推理,没有 JVM,没有网络往返。Harper 的检查延迟在毫秒级(官方口径是对大多数文档 10ms 以内),内存占用在几十 MB 量级。作为对比,LanguageTool 本地完整部署需要约 2GB 内存——两者差了两个数量级。
当然,规则引擎也有天花板:它抓不住需要深层语义理解的错误(比如逻辑上的主谓语义不匹配)。Harper 的取舍很清醒——把 80% 的常见错误(拼写、a/an、大小写、重复词、常见搭配错误、标点空格)用 1% 的资源成本解决掉,剩下的交给人或者更重的工具。
三、架构分析:三层结构 + 两个 trait 撑起整个系统
Harper 的代码库是一个典型的 Rust workspace,核心分三层:
┌─────────────────────────────────────────────┐
│ 前端层 │
│ harper-ls (LSP) │ harper.js (WASM) │ 插件们 │
│ VS Code / Neovim / Helix / Zed / Emacs │
│ Chrome / Firefox / Obsidian / WordPress │
├─────────────────────────────────────────────┤
│ harper-core(全部魔法所在) │
│ Document ── Parser trait ── Linter trait │
│ Token 流 │ 词典 │ Brill 词性标注 │
├─────────────────────────────────────────────┤
│ 数据层 │
│ 内嵌词典(编译期打包)│ 用户词典 │ 规则配置 │
└─────────────────────────────────────────────┘
3.1 harper-core:Document、Parser、Linter 三个抽象
官方架构文档里说得很直白,你只需要关心几个类型:
Document:一篇英文文档的内存表示,本质是 Token 流加上原文的 Span 映射。它实现了TokenStringExt,提供大量查询方法(找出所有句子、所有名词短语、所有引号对……)。Parser:一个 trait,「消费文本、吐出 Token」。这里有个有意思的命名坦白——文档承认这名字起得不准,因为它其实只做英文的词法分析(lexing),真正「parse」的是宿主语言。比如 Markdown parser 负责剥掉 Markdown 语法只留正文;各编程语言的 parser 负责从源码里抽出注释,再把注释内容喂给英文分析管线。Linter:一个 trait,输入 Document,输出零或多个Lint。每个 Lint 携带位置(Span)、消息、严重级别和候选修正。
这个设计最漂亮的地方在于 Parser 层把「宿主格式」和「英文分析」彻底解耦。要支持一门新语言的注释检查,只需要写一个新的 Parser 实现,把注释文本和原始位置映射出来,后面的整条 Lint 管线原封不动复用。这就是为什么 harper-ls 能开箱支持「大多数编程语言的注释 + 所有 Markdown 文件」——支持矩阵的扩展成本是线性的,而且每个 Parser 都很薄。
3.2 词性标注:不用神经网络,用 1992 年的 Brill Tagger
语法检查绕不开词性标注(POS tagging)——你得知道一个词是名词还是动词,才能判断 "a apple" 错在哪。主流方案是跑一个 BERT 类模型,但那意味着几百 MB 的权重和几十毫秒起步的推理延迟。
Harper 的选择是 Brill Tagging——Eric Brill 在 1992 年提出的基于转换规则的标注算法。它的思路是:先用词典给每个词一个初始词性,然后应用一系列「转换规则」(比如「如果前一个词是限定词,把当前词从动词改成名词」)逐步修正。规则是可枚举、可审计的,运行速度极快,内存占用几乎可以忽略。
在 2026 年这个「万物皆大模型」的年代,看到一个新项目理直气壮地用 34 年前的经典算法,还把它写进贡献者文档当作核心机制来讲解,实在是一股清流。工程上这个选择完全站得住:Brill Tagger 在通用英文上的准确率能到 95% 左右,而 Harper 的规则引擎并不需要 100% 准确的词性——它只在特定规则触发时参考词性,误标的代价是漏报一条建议,而不是系统性崩坏。
3.3 harper-ls:用 LSP 通吃所有编辑器
harper-ls 是包在 harper-core 外面的 Language Server Protocol 实现。LSP 是微软为 VS Code 设计、后来成为行业标准的协议——编辑器和语言工具之间通过 JSON-RPC 通信,工具只需实现一次,所有支持 LSP 的编辑器全部受益。
Harper 把「英语」伪装成一门编程语言接入 LSP 生态,于是 VS Code、Neovim、Helix、Zed、Emacs、Sublime Text 全部零成本获得支持。Lint 显示为编辑器原生的诊断(diagnostics),修正建议显示为 code action,体验和 rust-analyzer 给你改代码完全一致。
3.4 harper.js:WASM 让浏览器也能全本地检查
第三个前端是 harper.js——把 harper-core 编译成 WebAssembly,包一层 TypeScript API。这是 Chrome/Firefox 插件和 Obsidian 插件的基础,也是 WordPress 集成(Automattic 的本职)的技术底座。
意义在于:浏览器里的语法检查终于可以不发网络请求了。 Grammarly 插件之所以被很多公司的安全部门禁用,就是因为它会把你在任何输入框里打的字上传。harper.js 的 WASM 方案在页面本地完成全部计算,从架构上根除了这个问题——不是「我们承诺不上传」,而是「压根没有上传的代码路径」。
四、代码实战:从安装到写一条自己的规则
4.1 安装
# macOS / Linux
brew install harper
# Windows
scoop install harper
# Arch Linux
sudo pacman -S harper
# 有 Rust 工具链的话
cargo install harper-ls --locked
4.2 Neovim 集成
Neovim 0.11+ 用内置 LSP 配置即可:
-- ~/.config/nvim/lsp/harper_ls.lua
return {
cmd = { "harper-ls", "--stdio" },
filetypes = { "markdown", "rust", "go", "python", "gitcommit", "typescript" },
settings = {
["harper-ls"] = {
userDictPath = vim.fn.expand("~/.config/harper/dict.txt"),
linters = {
SpellCheck = true,
SentenceCapitalization = true,
RepeatedWords = true,
-- 技术写作中 "an SDK" 这类缩写冠词经常误报,可单独关掉
AnA = true,
},
diagnosticSeverity = "hint", -- 别用 error 吓自己
isolateEnglish = true, -- 中英混排文档只检查英文部分
},
},
}
-- init.lua 里启用
vim.lsp.enable("harper_ls")
几个实用细节:
userDictPath指向用户词典,项目术语(比如你的产品名)加进去就不再报拼写错误;diagnosticSeverity建议设成hint或information,语法建议不该和编译错误抢视觉优先级;isolateEnglish对中文技术写作者是刚需——中英混排的 Markdown 里,它会只对英文片段做检查。
4.3 VS Code
商店搜 "Harper" 安装官方插件即可,配置项与上面同构:
// settings.json
{
"harper-ls.linters.SpellCheck": true,
"harper-ls.linters.SentenceCapitalization": false,
"harper-ls.userDictPath": "~/.config/harper/dict.txt",
"harper-ls.diagnosticSeverity": "hint"
}
4.4 harper.js:在 Node 里做批量检查
比如给你的文档站加一个 CI 检查步骤:
// check-docs.ts
import { WorkerLinter, binary } from "harper.js";
import { readFile } from "node:fs/promises";
import { glob } from "glob";
const linter = new WorkerLinter({ binary });
let totalLints = 0;
for (const file of await glob("docs/**/*.md")) {
const text = await readFile(file, "utf-8");
const lints = await linter.lint(text);
for (const lint of lints) {
const span = lint.span();
console.log(
`${file}:${span.start}-${span.end} ` +
`[${lint.lint_kind()}] ${lint.message()}`
);
totalLints++;
}
}
process.exit(totalLints > 0 ? 1 : 0);
WorkerLinter 在 Web Worker(Node 里是 worker_threads)中运行 WASM,不阻塞主线程。对 CI 场景,几百个 Markdown 文件的全量检查通常在秒级完成。
4.5 硬核部分:用 Rust 写一条自定义规则
Harper 的规则体系对贡献者是开放的,官方文档专门有「Author a Rule」章节。假设我们要写一条规则:禁止在技术文档里使用 "simply"(写文档的人都懂——你觉得 simple 的东西读者未必觉得)。
// harper-core/src/linting/no_simply.rs
use crate::linting::{Lint, LintKind, Linter, Suggestion};
use crate::{Document, TokenStringExt};
pub struct NoSimply;
impl Linter for NoSimply {
fn lint(&mut self, document: &Document) -> Vec<Lint> {
let mut lints = Vec::new();
for token in document.tokens() {
// 拿到 token 对应的原文字符片段
let chars = document.get_span_content(&token.span);
let word: String = chars.iter().collect();
if word.eq_ignore_ascii_case("simply") {
lints.push(Lint {
span: token.span,
lint_kind: LintKind::Style,
message: "Avoid 'simply' in technical writing; \
it assumes the reader's context."
.to_string(),
// 建议直接删除该词
suggestions: vec![Suggestion::Remove],
priority: 63,
});
}
}
lints
}
fn description(&self) -> &str {
"Flags the word 'simply', which often alienates readers."
}
}
对于更复杂的模式(比如「情态动词 + of」这种 "should of" 错误),可以不实现裸的 Linter,而是实现 PatternLinter——声明式地描述一个 Token 模式,框架负责匹配,你只负责生成 Lint。这类似正则之于手写状态机的关系,绝大多数规则用 PatternLinter 都能三五十行搞定。
这套贡献路径的门槛控制得相当好:新规则 = 新文件 + 一个 struct + 一个 trait impl + 注册。Harper 仓库里相当比例的规则来自社区贡献者的第一个 Rust PR——一个语法检查器项目顺便成了 Rust 新手村,这是我见过的最健康的开源增长模型之一。
五、性能优化:为什么它能快两个数量级
把 Harper 和 LanguageTool 的性能差距拆开看,快不是因为「Rust 比 Java 快」这么简单,而是一整套架构决策的叠加:
1. 编译期资源内嵌。 词典、affix 规则、Brill 转换规则全部在编译期打进二进制。启动即就绪,没有 JVM 预热,没有词典加载 IO。这也是为什么 harper-ls 冷启动几乎无感,而 LanguageTool server 启动要好几秒。
2. 增量检查。 编辑器场景下,用户每次击键只改动文档的一小块。harper-ls 利用 LSP 的增量同步,只对受影响的句子重新跑 Lint 管线,而不是全文重算。对一个几千行的 Markdown 文件,单次击键触发的重新检查依然在毫秒内完成。
3. Token 一次生成、多规则复用。 所有 Linter 共享同一份 Document 的 Token 流和词性标注结果。规则数量从 20 条涨到 100 条,tokenization 成本不变,只有规则匹配本身线性增长——而单条规则的匹配就是一次线性扫描加若干哈希查询,纳秒级。
4. 无 GC、无运行时。 Rust 的所有权模型意味着没有 GC 暂停。对交互式工具,尾延迟(P99)比平均延迟更重要——用户感知的是「偶尔卡一下」,而 Harper 从机制上不存在这个「偶尔」。
5. WASM 端的体积控制。 harper.js 的 WASM 二进制控制在个位数 MB(含完整词典),这决定了浏览器插件的冷启动体验。对比之下,任何「在浏览器里跑个小模型」的方案,光权重下载就得几十上百 MB。
这里有个值得所有工具作者记住的观点:性能不是优化出来的,是架构选择的副产品。 Harper 快,根本原因是它在第一天就拒绝了「模型推理」和「网络往返」这两个延迟大户,选了规则引擎这条在 2020 年代看起来「不性感」的路。
六、横向对比:Harper vs Grammarly vs LanguageTool
| 维度 | Harper | Grammarly | LanguageTool |
|---|---|---|---|
| 部署形态 | 本地二进制 / WASM | 云端 SaaS | 云端 + 可自托管(Java) |
| 隐私 | 零上传,无网络路径 | 全文上传 | 自托管可控,云端上传 |
| 延迟 | 毫秒级 | 网络往返,百 ms 起 | 本地部署秒级 |
| 内存 | 几十 MB | (云端)本地插件较轻 | 自托管约 2GB |
| 深层语义改写 | 不支持 | 强 | 中等 |
| 多语言 | 仅英文(多方言) | 多语言 | 30+ 语言 |
| 编辑器集成 | LSP 全家桶 | 专有插件 | 部分编辑器 |
| 代码注释检查 | 原生支持 | 无 | 弱 |
| 开源 | Apache-2.0 | 闭源 | 部分开源 |
| CI 可集成性 | 强(确定性输出) | 无 | 可以但重 |
结论很清晰:如果你要的是「深度改写润色」,Grammarly 依然无可替代;如果你要检查非英文文本,LanguageTool 是唯一选项。但对于程序员最高频的场景——英文文档、注释、commit message 的拼写和基础语法把关——Harper 在隐私、速度、集成体验三个维度上全面胜出,而这三个维度恰恰是程序员最在乎的。
七、总结与展望:本地优先软件的又一块拼图
Harper 值得关注,不只因为它是个好用的工具,更因为它代表了一个正在成型的趋势:Local-first 软件的复兴。
过去十年,SaaS 化把一切工具搬上云端,代价是隐私、延迟和离线可用性。而最近两年,Rust + WASM 这对组合正在把一类又一类工具拉回本地:语法检查(Harper)、密码管理(Vaultwarden)、类型检查(Pyrefly)……它们的共同配方是:用 Rust 榨干单机性能,用 WASM 打通浏览器,用开源换取信任。
对 Harper 本身,我有三个判断:
AI Agent 生态会成为它的第二增长曲线。 Agent 生成的英文内容(文档、PR 描述、commit message)需要一道确定性的、可编程的质检关卡,云端方案在 Agent 的高频调用下既贵又慢,Harper 这种毫秒级本地工具是天然适配的。它的确定性输出对 Agent 尤其友好——同样的文本永远得到同样的反馈,Agent 可以放心地把它当成收敛条件。
多语言支持是最大的想象空间,也是最大的工程挑战。 规则引擎的路线意味着每种语言都要重建词典、词性标注和规则集。Automattic 有动力做这件事(WordPress 是全球化产品),但工程量是实打实的。
它会倒逼云端竞品交出隐私答卷。 当「零上传的语法检查」从不可能变成
brew install harper一行命令,用户对「为什么我的邮件草稿需要上传到你的服务器」这个问题的容忍度会迅速下降。
最后给个直接的行动建议:如果你维护任何包含英文文档的仓库,今天就可以把 harper-ls 加进你的编辑器,再花十分钟把 harper.js 挂进 CI。成本几乎为零,而你的 README 从此少一半拼写错误——这大概是 2026 年性价比最高的工程投资之一。
工具的终局不是更强大的云,而是更强大的本地。Harper 是这句话的又一个证据。