编程 Harper 深度拆解:Automattic 用 Rust 造了个「本地 Grammarly」,毫秒级响应、零上传,凭什么单日暴涨 880 Star

2026-07-28 03:45:18 +0800 CST views 7

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 建议设成 hintinformation,语法建议不该和编译错误抢视觉优先级;
  • 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

维度HarperGrammarlyLanguageTool
部署形态本地二进制 / 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 本身,我有三个判断:

  1. AI Agent 生态会成为它的第二增长曲线。 Agent 生成的英文内容(文档、PR 描述、commit message)需要一道确定性的、可编程的质检关卡,云端方案在 Agent 的高频调用下既贵又慢,Harper 这种毫秒级本地工具是天然适配的。它的确定性输出对 Agent 尤其友好——同样的文本永远得到同样的反馈,Agent 可以放心地把它当成收敛条件。

  2. 多语言支持是最大的想象空间,也是最大的工程挑战。 规则引擎的路线意味着每种语言都要重建词典、词性标注和规则集。Automattic 有动力做这件事(WordPress 是全球化产品),但工程量是实打实的。

  3. 它会倒逼云端竞品交出隐私答卷。 当「零上传的语法检查」从不可能变成 brew install harper 一行命令,用户对「为什么我的邮件草稿需要上传到你的服务器」这个问题的容忍度会迅速下降。

最后给个直接的行动建议:如果你维护任何包含英文文档的仓库,今天就可以把 harper-ls 加进你的编辑器,再花十分钟把 harper.js 挂进 CI。成本几乎为零,而你的 README 从此少一半拼写错误——这大概是 2026 年性价比最高的工程投资之一。

工具的终局不是更强大的云,而是更强大的本地。Harper 是这句话的又一个证据。

推荐文章

Go配置镜像源代理
2024-11-19 09:10:35 +0800 CST
CSS Grid 和 Flexbox 的主要区别
2024-11-18 23:09:50 +0800 CST
Go语言中的mysql数据库操作指南
2024-11-19 03:00:22 +0800 CST
120个实用CSS技巧汇总合集
2025-06-23 13:19:55 +0800 CST
deepcopy一个Go语言的深拷贝工具库
2024-11-18 18:17:40 +0800 CST
程序员茄子在线接单