为什么我放弃了用 LLM 给 AI 代码打分,改为用编译器打分

6 阅读8分钟

§1 / 需求从哪来的

用 ai-spec 跑了大概两个月之后,我遇到了一个具体的问题:

我不知道这次 run 到底比上次好还是差。

同样的需求让 AI 跑两次,出来的文件大概率不一样。你说有改善,凭什么?你说有退步,什么地方变了?完全没有客观依据。

Cursor 和 Claude Code 给你 diff,你 accept 或 reject——这解决了「看到结果」的问题,但没有解决「这次质量怎么样」的问题。

跑了 30 次之后,我对这套 AI coding 工作流的认知其实就四个字:感觉还行

感觉这东西不准。


§2 / 我的第一反应:用 LLM 当裁判

我当时的第一反应和大多数人一样——让一个更强的模型来评分。

思路很直接:跑完一次 codegen,再调一次 LLM,把生成的代码喂给它,让它输出一个质量分数。

听起来合理。但我认真试了之后,发现三个绕不过去的问题。

问题一:循环论证。

让模型 A 生成,让模型 B 评分,然后根据 B 的反馈改 A——你在用模型的「自信度」代替真实的「质量」。

具体表现:模型对自己输出的评分永远偏高,因为它不知道自己不知道什么。你问 GPT-4「这段代码好不好」,它的回答里隐含的前提是「我生成的代码我得认」。这不是评测,这是一群模型互相点头。

问题二:成本。

我每天跑 ai-spec create 大概 5-10 次。如果每次额外调一次 LLM 评分,光评分的人民币可能比代码生成本身还贵。

更关键的是:LLM 评分每次的输出不稳定。同样的代码,两次调用可能给出 8.2 和 7.6——这个 0.6 的差距,到底是代码质量变了,还是模型的随机性?根本无法区分。

问题三:评分标准不一致。

LLM 的评分取决于 prompt 里怎么描述「好代码」。你写「请从可读性角度评分」,它给你一个分数;换成「请从安全性角度评分」,又是另一个分数。你无法保证每次评分用的是同一个标准——这让跨 run 比较完全没有意义。

§3 / 换个思路:用确定性方法

我后来意识到,评分这件事其实不需要 LLM。

LLM 的核心能力是理解语义和生成文字。但「这段代码能不能编译」这件事,编译器自己就能回答,比任何模型都准。

这个认识让整个设计进入了另一个方向:不测「好不好」,测「通不通」。

于是有了 Harness Score。

Harness Score 是一个 0-10 的分数,四个维度加权:

维度权重计算方式
Spec Compliance30%Spec 里的验收条件 vs 实际输出文件的 diff
DSL Coverage25%DSL(json) 里的 models/endpoints 有多少被实现了
Compile/Lint20%tsc --noEmit 的退出码
3-pass Review25%LLM 评审(通过 / 需修改 / 不通过)

除了 Review 那 25% 是 LLM,其他三个维度全是机械计算。

没有任何额外的 LLM 调用。不存在「调一个更强的模型来评分」这种循环论证。


§4 / 四个维度各自怎么工作的

Spec Compliance(30%):验收条件说了算

ai-spec pipeline 的第一个输出是一份 Spec,里面有一节叫「验收条件」。

比如:「登录接口要返回 refreshToken,错误时返回 401,body 里要有 error_code 字段」——这些是写进 spec 的条件。

跑完之后,拿这些验收条件和实际输出的文件做 diff。写了,分数高;没写,分数低。

这个维度的意义:防止 Spec 写得清楚但实现跑偏。 AI 有时候会把 spec 里明确写的内容漏掉,compliance 维度能把这个问题放大。

DSL Coverage(25%):契约说了什么,代码实现了多少

DSL(json) 是 ai-spec pipeline 的第二个输出——它把需求翻译成机器可读的结构化契约,包含 models、endpoints、behaviors。

跑完之后,扫实际生成的文件路径,看哪些 models 有对应的 model 文件,哪些 endpoints 有对应的 api 文件。

这个维度的意义:防止「接口文档写了但代码没实现」。 Coverage 低说明 DSL 里定义了但实际没写,通常是 AI 在某个文件上卡住之后跳过了某些端点。

Compile/Lint(20%):编译器说了算

tsc --noEmit 直接跑。退出码 0 = 10 分,非 0 = 0 分。

没有模糊空间:代码要么能编译,要么不能。能编译不代表逻辑对,但编译不过一定有问题。

20% 的权重不算高,但它是一个硬门槛。如果 compile 是 0,即使其他三个维度都是满分,总分也只有 8.0——这个门槛足够让人不能忽视。

3-pass Review(25%):LLM 评审,但加了约束

这是四个维度里唯一一个依赖 LLM 的环节。

Review 分成三轮:

  • 第一轮:架构层——代码结构是否符合项目规范
  • 第二轮:实现层——函数逻辑、边界情况、异常处理
  • 第三轮:影响层——复杂度、引入的依赖风险、是否破坏现有功能

三轮的结果映射:通过 = 10分需修改 = 5分不通过 = 0分,加权均分。

§5 / 为什么这个分数能跨 run 比较

分数本身没有意义,比较才有意义。

Harness 的设计关键在这里:每次 run 的分数绑定了 prompt hash。

Harness Score  8.4 / 10
  compliance 8.6  ·  coverage 9.2  ·  compile 10  ·  review 7.1
  prompt_hash: a7f2d3e1
  run_id: 20260408-143022

prompt hash 是根据这次需求的文字生成的。只要需求文字一样,hash 就一样,分数就可以横比。

同一个 prompt 跑了三次:

run 1: 7.2  (compliance 7.0 / coverage 6.8 / compile 10 / review 5.8)
run 2: 7.8  (compliance 8.2 / coverage 7.5 / compile 10 / review 6.4)
run 3: 8.4  (compliance 8.6 / coverage 9.2 / compile 10 / review 7.1)

趋势在往上走。这比「感觉还不错」有说服力多了。

prompt hash 绑定的另一个价值:你能定位是什么让分数变了。

如果从 Claude 换成 DeepSeek 之后分数跌了,你就知道这个 provider 对这类任务的适配度不够,可以切回去或者调整 prompt。如果 compliance 维度一直低,你就知道是 spec 的验收条件写得不够细。

每一个维度都在告诉你一个具体的方向。


§6 / 温度计,不是裁判

Harness Score 的定位是温度计,不是裁判

温度计不判断对错。它只需要读数准确。它告诉你「这次是 8.4,上次是 7.6」——你根据这个信号决定要不要多 review 一次,而不是让它替你做决定。

裁判和温度计的区别:

裁判温度计
判断「好不好」
读数「几分」主观客观
成本高(每次调用 LLM)低(编译器跑一下)
跨 run 可比❌(标准不一致)✅(同一把尺子)

你仍然要 review。但 review 的切入点从「全看一遍」变成「看分数低的地方」。

§7 / 这个分数能做什么、不能做什么

它能量化的:

  • 这次 run 和 spec 的吻合度
  • 跨 run 的质量趋势
  • 不同 provider 对同一类任务的适配度差异
  • 代码能不能编译

它不能量化的:

  • Runtime 正确性(业务逻辑 bug Harness 看不出来的)
  • 代码可读性和可维护性(这部分还是靠人审)
  • 架构是否合理(Harness 只看你写没写,不看你写得好不好)

Harness 的价值是把「我这次跑完觉得还行」变成一个可记录的量化数字。


§8 / Fix-history:Harness 的另一个维度

光有分数不够。分数告诉你「哪里坏了」,但没有解决「下次怎么不坏」。

这个问题我用一个叫 Fix-history 的机制解决。

每次 ai-spec 的 import 验证器发现一个错误的 import——比如 AI 生成了一个从一个不存在的文件导入的语句——会自动修复,然后把这个错误记进一个 ledger:

// .ai-spec-fix-history.json
{
  "pattern": "import { Task } from '@/apis/task/type'",
  "file_not_exist": "src/apis/task/type.ts",
  "fix": "created: src/apis/task/type.ts",
  "count": 3,
  "last_seen": "2026-04-08"
}

下次跑 codegen 之前,这个 ledger 会被加载,重复出现的 pattern 会被注入到 prompt 里:

❌ Do NOT: import { Task } from '@/apis/task/type'
   Reason: file did not exist (seen 3x)
   Previously fixed by creating: src/apis/task/type.ts

这个过程零 AI 成本——不是调一次 LLM 来学习,是字符串匹配加文本注入。

跑得越久,AI 犯同样错误的几率就越低。这就是 ai-spec 的「体温记忆」。


§9 / 现在的状态

Harness 已经集成在 ai-spec 的 pipeline 里,不需要额外配置。跑完任何 ai-spec create 命令都会自动输出分数。

$ ai-spec create "给订单模块加退款功能"
  ✔ Spec generated     .ai-spec/specs/order-refund.md
  ✔ DSL valid         Models: 3  Endpoints: 78 files written
  ✔ 3 errors fixed in 1 cycle3-pass review passed

  Harness Score  8.1 / 10
    compliance 8.4  ·  coverage 9.0  ·  compile 10  ·  review 5.8
  9 files written  ·  restore: ai-spec restore 20260410-094521-c3d8

GitHub: github.com/hzhongzhong… npm: npm install -g ai-spec-dev 站点: ai-spec.dev