一个检查 AI 生成文档的工具,它自己不用 AI
现在我收到的每一份文档,都有一部分是机器写的。这没问题。有问题的是:机器写的那部分,和已经定下来的那部分,排版一模一样——而我往往要等到动手三天之后才分得出来。
问题不在于「是不是 AI 写的」
市面上的 AI 检测只回答一个问题:这是不是模型写的?在真实工作里这个问题没有意义。答案永远是「是」,而且本来就该是——现在没有谁的文档是全手写的,也不该要求谁全手写。
我真正需要被回答的问题,窄得多,也有用得多:
这份文档里,哪些地方其实还没定,却被排版成了已经定了?
这才是那个病。参数表左边给出精确默认值,右边小字写着「需业务确认」;数据源清单第一行标着「待定」,后面十一章的公式照样算得天衣无缝。读的人记住的是默认值和公式,于是整条链路在一个不存在的输入上开工。
人很少这么写。模型天天这么写——因为它生成的各个章节互相不知道对方的存在,而且它唯一在优化的目标就是「看起来完整」。
为什么不用模型
最顺手的做法当然是把文档喂给大模型,问它哪里有问题。我没这么做,三个理由:
- 用 AI 去查 AI 的膨胀,查出来的东西自己也会膨胀。我要的是能直接拿进会议室、不用先自己复核一遍的结论。
- 这些文档是保密的。同事唯一肯把真实文档粘进去的版本,是没有后端、不发任何网络请求的版本。这个约束是产品本身,不是产品的缺陷。
- 确定性是可核验的。每一条命中都指向一个行号,没有任何东西是生成出来的。它不会幻觉——代价是它绝不聪明。这个取舍是刻意的。
所以它是正则。一个 HTML 文件,没有构建步骤,全部在浏览器里跑完。整套规则引擎你一次就能读完。
在一份 6 万字的真 PRD 上查出了什么
一份产业分析类产品的内部需求文档。2545 行,260 个标题,102 张表。十一章,每一章都填满了。看上去极其完备。
浏览器里十秒:
| 档位 | 数量 | 含义 |
|---|---|---|
| 阻断 | 4 | 不等人回答,活真的开不了工 |
| 待澄清 | 7 | 开工前需要有答案 |
| AI 痕迹 | 2 | 给读者的备注,从不计入问题数 |
四条阻断项,原样照抄输出:
这张表 38 行参数,30 行自己标着「需确认」
同一行在说两件相反的事:左边给了精确默认值,右边承认这个值没人定过。读的人只会记住左边。
「专利」这一环还是空的,下游已经有 3 处在用它算
数据源那一行写着「待定」。而消费它的公式、归一化步骤和图表,全都已经写完了。
有 2 行一边标着「数据缺失」,一边照样算出了结果
这条最要命。缺失被如实标注了,但它没有阻断计算。缺失声明和结果数字并排躺在同一行里,而被引用出去的只有那个数字。
17 个包名 / CLI / 接口地址,没有一个标注验证过
仓库名、命令行名、endpoint。它们看上去是文档里最硬的部分,实际是幻觉率最高的部分。每一个都要有人搭进去一个下午。
还有两条口径冲突。我认为这是最值得说的一类,因为人几乎不会犯:
- β 被定义了两次。参数表里它是 LDA 超参(0.01);往后四章,它是期望膨胀期的趋势斜率阈值(>0.001)。实现第五章的人读了第一章的定义,就会安安静静地做错。公式照样跑得通,输出照样像模像样。
- 同一个融资指标,第 2 章按月算,第 5 章按季度算。名字一样、单位一样、粒度不同。两个看起来可比、实际不可比的数。
同一套材料的另一份文档里,有一句写着「L3 的 5 项:」,后面列了 6 个。纯算术。没人发现,因为那个数和那个清单是分两次生成的。
最值钱的规则,不需要懂你的业务
这个工具没法判断一个公式合不合理——它从不假装能。但有一类问题是纯算术,而且恰恰是这一类最有用,因为它没得辩:
- 声明为权重的百分比加起来是 105%,不是 100%
- 「五项:」后面列了六个
- 「100% 自动」旁边写着「前提是数据源可接入」
- 一个符号被定义成两个不同的量
- 同一个指标,一处算最近 6 个月,一处算最近 12 个月
没有人需要争论这份文档写得好不好,只需要说哪个数是对的。这也是为什么输出永远是一串问题、永远不打分:分数会被辩论(「你凭什么说我 60 分」),问题不会——你要么答得上来,要么答不上来,而答不上来的那几条就是这份文档真实的进度。
我认为最重要的一节,而它不是技术
工具会标出看起来像一次性生成的段落。把这件事做成「指控」很容易。我刻意做成了相反的东西。
「这段是我的 AI 自己加的,删掉就行」,比「我写错了」容易说出口一百倍。
指出问题如果等于指控对方能力有问题,对方就只能硬撑,事情谈不下去——然后问题照样上线。把锅留给模型,人才有退路,沟通才真的发生。
所以 AI 痕迹这一档单独算,从不计入「必须回答的问题」。它的措辞也是刻意软的:「要是哪条本来就是模型自己加的,你说一声我直接划掉,不用解释。」
同样的理由,默认模式是自检,不是体检。一个只能用来挑别人毛病的工具,第一次用完就会被所有人记恨。你得能先说「我自己每份文档发出去之前都过一遍」,别人才肯碰它。
在值得尊敬的文档上量误报
合成样本只能证明一条规则会触发,证明不了它准。所以我拿两份公开的英文文档当对照组——写它们的人非常擅长写规格,因此上面的每一条命中,默认都是可疑的:
curl -sL https://raw.githubusercontent.com/kubernetes/enhancements/master/keps/NNNN-kep-template/README.md
curl -sL https://www.rfc-editor.org/rfc/rfc9110.txt
第一次跑的结果很难看,而且难看得很有价值。
| 文档 | 未验证具体物 · 修之前 | 修之后 |
|---|---|---|
| Kubernetes KEP 模板 | 37 | 11 |
| RFC 9110 | 122 | 15 |
| 6 万字真 PRD(真命中) | 19 | 17 |
从中挖出两个真缺陷:
RFC 9110 被报了「没有验收标准」。它有规范性要求——MUST、SHALL、规范性引用。它对正确性的定义比我收到过的任何 PRD 都严格,只是它不用「验收」这个词。词表现在覆盖了 RFC 2119 关键词。
「未验证具体物」在对着参考文献开火。这条规则的前提是这些名字是模型编的。可是在真的工程文档里,标识符本来就多——而且我一查发现,KEP 模板里反引号标识符总共只有 5 个,其余三十多条命中全是文档引用链接。三处改动修好了它:跳过围栏代码块、跳过 Markdown 链接与引用式定义、把全文出现 3 次以上的标识符视为真实(编出来的名字通常只提一次,真正在用的会反复出现)。
误报降了约七成,而真命中的数字一个没掉。基线数字和那两条 curl 命令现在写在仓库里,改任何规则都要复跑——命中数只许降,不许升。
有一条已知的噪音我没修:RFC 9110 上「无源数字」仍然有 82 条,因为英文词表里含 window、weight、ratio,而它们在 HTTP 里都是真术语。它属于「待澄清」不是「阻断」,而收紧它会伤到这个工具真正要服务的那类文档的召回。所以它留着,并写明。
它查不了什么
写明这一段,本身就是这个工具存在的理由。一份不肯说自己做不到什么的文档,正是它要查的东西。
- 查不了对错。它不懂你的业务。
- 口径冲突只查得到数值层面的。两章对「活跃用户」的说法不同、但都没给数——它看不见。试过,确定性规则要么零命中,要么对所有合理差异乱叫。仍是缺口。
- 查不出「写得对但做不到」。人力、预算、权限,文档里不写就看不见。
- 命中不等于有罪。每一条都只是「这里需要一个活人回答」。
试一下
粘一份文档进去看看。它在你的浏览器里跑完,不上传、不联网、断网也能用。别信这句话——源码就一个 HTML 文件,自己搜 fetch,一个都没有。
A linter for AI-generated specs that uses no AI
Every spec I receive now is partly machine-written. That is fine. What is not fine is that the machine-written parts are typeset exactly like the decided parts, and I can't tell them apart until I'm three days into building.
The problem isn't that AI wrote it
AI-detection tools answer one question: was this written by a model? In real work that question is useless. The answer is always yes, and it should be — nobody hand-writes documents any more, and nobody should have to.
The question I actually need answered is narrower and much more useful:
Which parts of this document are undecided, but typeset as if they were decided?
That's the failure mode. A parameter table gives a precise default on the left and admits in small type on the right that nobody has signed off. A data-source list says TBD on row one, and eleven chapters of formulas downstream compute perfectly anyway. The reader remembers the defaults and the formulas. The chain gets built on an input that does not exist.
Humans rarely produce this. A model produces it constantly, because it generates sections that don't know about each other, and because looking complete is the only thing it is optimising for.
Why it uses no model
The obvious build is "feed the doc to an LLM and ask what's wrong." I didn't, for three reasons:
- Using AI to audit AI inflation produces inflated audits. I wanted findings I could take into a meeting without re-verifying them first.
- The documents are confidential. The only version of this tool that colleagues will actually paste a real spec into is one with no backend and no network calls. That constraint is the product, not a limitation of it.
- Determinism is checkable. Every finding points at a line number. Nothing is generated. It never hallucinates — and it is never clever. That trade is deliberate.
So it's regex. One HTML file, no build step, runs entirely in the browser. You can read the whole rule engine in one sitting.
What it found in a real 60,000-character PRD
An internal requirements document for an industry-analysis product. 2,545 lines, 260 headings, 102 tables. Eleven chapters, every one of them filled in. It looks extremely thorough.
Ten seconds, in the browser:
| Tier | Count | Meaning |
|---|---|---|
| Blocking | 4 | work genuinely cannot start until a human answers |
| To clarify | 7 | needs an answer before kickoff |
| AI traces | 2 | a note for the reader, never counted as a problem |
The four blocking ones, verbatim from the output:
This table has 38 parameters, and 30 of them are marked "needs sign-off" by the table itself
One row says two opposite things: a precise default on the left, and an admission on the right that nobody agreed to it. Readers only remember the left.
"Patents" is still empty, and 3 places downstream already compute with it
The data source row reads TBD. The formulas, the normalisation step and the chart that consume it are all written out in full.
2 rows say "data missing" and still produce a number
This is the worst one. The gap was honestly labelled — and it did not stop the calculation. The disclaimer and the number sit on the same row, and only the number gets quoted.
17 package / CLI / endpoint names, none marked as verified
Repo names, CLI names, endpoints. They look like the hardest facts in the document and they are the highest-hallucination part of it. Every one costs somebody an afternoon.
And two definition clashes that I think are the most interesting category, because a human almost never makes them:
- β is defined twice. In the parameter table it's an LDA hyperparameter (0.01). Four chapters later it's a growth-slope threshold (>0.001). Whoever implements chapter five reads chapter one's definition and silently builds the wrong thing. The formula compiles. The output looks plausible.
- The same financing metric is monthly in chapter 2 and quarterly in chapter 5. Same name, same unit, different granularity. Two numbers that look comparable and aren't.
In a sibling document from the same set, a line reads "the five L3 items:" and then lists six. Pure arithmetic. Nobody caught it because the list and the count were generated in separate passes.
The rules that earn their keep need no domain knowledge
The tool cannot tell whether a formula is sound — it never pretends to. But one family of problems is pure arithmetic, and that family turns out to be the valuable one, because there is nothing to argue about:
- Percentages declared as weights that sum to 105%, not 100%
- "Five items:" followed by a list of six
- "100% automated" sitting next to "provided the data source can be connected"
- One symbol defined as two different quantities
- The same metric computed over 6 months in one place and 12 in another
Nobody has to debate whether the document is well written. They only have to say which number is correct. That's also why the output is a list of questions and never a score: a score invites argument ("who says I'm a 60?"), a question doesn't. You either answer it or you can't, and the ones you can't are the document's real progress.
The part I think matters most, and it isn't technical
The tool flags passages that look one-pass generated. It would be easy to make that an accusation. It is deliberately the opposite.
"That part was my AI, just delete it" is a hundred times easier to say than "I got it wrong."
If pointing at a problem means accusing a colleague of incompetence, they can only dig in, and the conversation dies — and then the problem ships anyway. Leaving the blame with the model gives people a way out, and that's when the conversation actually happens.
So the AI-trace tier is counted separately and never added to the questions that must be answered. The phrasing is softer on purpose: "If any of these points were the model's addition rather than your intent, just say so and I'll drop them — no explanation needed."
For the same reason the default mode is self-check, not review. A tool that can only be used to find fault with other people gets resented the first time it's used. You need to be able to say "I run this on everything I send" before anyone else will touch it.
Measuring the false positives, on documents that deserve respect
Synthetic fixtures prove a rule fires. They prove nothing about precision. So I ran it against two public English documents written by people who are very good at writing specifications, where every hit is by definition suspect:
curl -sL https://raw.githubusercontent.com/kubernetes/enhancements/master/keps/NNNN-kep-template/README.md
curl -sL https://www.rfc-editor.org/rfc/rfc9110.txt
The first run was bad, and in an instructive way.
| Document | Unverified specifics, before | after |
|---|---|---|
| Kubernetes KEP template | 37 | 11 |
| RFC 9110 | 122 | 15 |
| The 60k PRD (true positives) | 19 | 17 |
Two real defects came out of this:
RFC 9110 was flagged for having no acceptance criteria. It has conformance requirements — MUST, SHALL, normative references. It defines correctness more rigorously than any PRD I have ever received; it just doesn't use the word "acceptance." The vocabulary now covers RFC 2119 keywords.
"Unverified specifics" was firing on citations. The rule's premise is a model invented these names. In a real engineering document, identifiers are simply normal — and when I looked, the KEP template contains only five backticked identifiers. The other thirty-two hits were documentation links. Three changes fixed it: skip fenced code blocks, skip Markdown link and reference syntax, and treat any identifier appearing three or more times as real, because an invented name is usually mentioned once and a load-bearing one is mentioned repeatedly.
False positives dropped by roughly 70% and the true-positive counts did not move. The baseline numbers and those two curl commands are now in the repo, and any rule change has to re-run them — hits may go down, never up.
One known noise source I did not fix: on RFC 9110, "unsourced numbers" still returns 82, because the English vocabulary includes window, weight and ratio, which are real HTTP terms. It's a clarify-tier finding, not a blocking one, and tightening it costs recall on the documents this is actually for. So it stays, documented.
What it cannot do
Stating this is the whole reason the tool gets to exist. A document that refuses to say what it can't do is exactly what it was built to find.
- It can't tell right from wrong. It doesn't know your business.
- Definition clashes only at the numeric level. Two chapters disagreeing about what "active user" means, in prose, with no numbers attached — invisible to it. I tried; deterministic rules either found nothing or barked at every legitimate difference. Still a gap.
- It can't see "correct but undoable." Headcount, budget, access — not in the document, not visible.
- A hit is not a verdict. Every one means "a human needs to answer this here."
Try it
Paste a spec and see. It runs in your browser; nothing is uploaded, there are no network calls, and it works offline. Don't take that sentence's word for it — the source is one HTML file. Search it for fetch; there isn't one.