
起点是一句没人回答的质疑
V2EX t/1239395:"单看每百万 Token 多少钱没意义,应该看同一件事最后到底花多少钱、还有花多长时间。"我自己埋了 bug 搭了套 harness 跑了一遍,单价差 40 倍,一次审查的总账差 545 倍。
为什么要自己搭 harness
代码审查这个赛道现在的公开数据分两类。一类是工具方自测,precision、F1、token 降幅,都是项目自己跑自己报。另一类是转述,把自测数字换个说法再发一遍。
两类都缺同样的东西:金额和秒数。按百万 token 报价的表和"审一次 PR 花多少钱"之间隔着一整个 usage,没人把它算出来。
要算这笔账,就得让模型成为唯一变量,其它所有能影响输出的东西都得钉死。下面按变量控制、数据、坑三段写。
变量控制清单
| 维度 | 固定值 | 为什么必须钉死 |
|---|---|---|
| 被测代码 | 同一份 64 行 Python 文件 | 换个文件难度就变了,分数没法横比 |
| prompt | system 与 user 逐字节相同 | 一句措辞差异就能改变误报率 |
| payload | stream=false、max_tokens=4096 |
4096 与该 CLI 的 bl text chat 默认值对齐 |
| 端点 | 直连 OpenAI 兼容端点 | 走 CLI 通道会撞上后面第一个坑 |
| 计时 | 客户端墙钟,发请求到收完响应 | 含网络往返,这才是用户体感的耗时 |
| 取样 | 每组只跑一次 | 这是缺陷不是设计,后面会披露两次跑分的差异 |
max_tokens=4096 这条得说明白:思考模型的 reasoning_tokens 不计入这个上限。qwen3.8-max 设了 4096,实际返回 11,064 个 completion tokens,其中 9,770 个是思考。这是平台行为,不是我的 harness 写错了。
被测代码:4 个真缺陷 + 2 个诱饵
| 编号 | 埋的是什么 | 判定 |
|---|---|---|
| B1 | 查用户时不判空,数据库查不到就崩 | 真缺陷 |
| B2 | 用户输入直接拼进 SQL | 真缺陷 |
| B3 | 计数器 += 1 没加锁,文件里定义了锁但从未使用 |
真缺陷 |
| B4 | open() 没用 with,异常路径下句柄泄漏 |
真缺陷 |
| D1 | @lru_cache 装饰纯函数 |
诱饵,正确用法,报它算误报 |
| D2 | except Exception: raise |
诱饵,裸 raise 恰恰保留原始堆栈,报它算误报 |
诱饵是必须的。只统计"抓到几个 bug"会把话痨模型排到前面,它报得越多命中越多,但开发者要为每一条误报付出复核成本。
计分口径:得分 = 真阳 + 0.5 × 部分命中 − 误报,满分 4。部分命中指修复代码给对了但诊断说错了病因,qwen-plus 的 B4 就是这种,它写了 with open(...),诊断栏填的却是路径遍历。行号准确率单独记,不混进得分。模型额外报出的真问题也不计分,只作为定性观察。
结果
| 模型 | 思考 token | 真阳 | 漏报 | 误报 | 得分 | 耗时 | 输入 tok | 输出 tok | 单次 ¥ |
|---|---|---|---|---|---|---|---|---|---|
| qwen-turbo | 不思考 | 2/4(B1,B2) | 2(B3,B4) | 4 | -2.0 | 7.4 秒 | 468 | 1,005 | 0.0007 |
| qwen3-coder-plus | 不思考 | 3/4(B1,B2,B4) | 1(B3) | 1 | 2.0 | 10.1 秒 | 464 | 712 | 阶梯计费,未公开 |
| qwen-plus | 不思考 | 2/4(B2,B3)+ B4 半分 | 1(B1) | 1 | 1.5 | 39.3 秒 | 464 | 1,766 | 0.0039 |
| qwen3.7-plus | 1,182 | 4/4 全中 | 0 | 0 | 4.0 | 23.4 秒 | 502 | 2,052 | 未公开 |
| qwen3.6-flash | 4,258 | 4/4 全中 | 0 | 1 | 3.0 | 42.6 秒 | 502 | 5,075 | 未公开 |
| qwen3.8-flash | 9,517 | 4/4 全中 | 0 | 0 | 4.0 | 241.0 秒 | 540 | 12,909 | 0.0353 |
| qwen3.8-flash + 显式 thinking | 11,611 | 4/4 全中 | 0 | 0 | 4.0 | 227.0 秒 | 540 | 14,954 | 0.0408 |
| qwen3.8-max | 9,770 | 4/4 全中 | 0 | 0 | 4.0 | 293.1 秒 | 540 | 11,064 | 0.4048 |
8 组,每组一次。token 全是 API 原样返回的 usage,金额是真实 usage 乘免登录查到的单价。
最后两行的开关对照值得单独看:qwen3.8-flash 默认就在思考,9,517 个 reasoning tokens。显式加 --enable-thinking 之后思考涨到 11,611、输出涨到 14,954、花费从 ¥0.0353 涨到 ¥0.0408,多了 16%,得分一分没变。在这个任务上,显式开思考只买到一张更贵的账单。
三条规律
第一条,得分的分水岭不是价格,是会不会思考。三个不思考的模型得分 -2.0、2.0、1.5,全部有漏报;五个会思考的得分 3.0、4.0、4.0、4.0、4.0,零漏报。
第二条,思考量过了一个点就不再换分。qwen3.7-plus 用 1,182 个思考 token 拿满分,qwen3.8-max 用 9,770 个也是满分。中间那 8,588 个 token 换来的是 12.5 倍耗时(23.4 秒到 293.1 秒)和 5.4 倍输出 token(2,052 到 11,064)。
第三条,行号准确率随档位单调上升。
| 模型 | 行号正确率(对 / 报出条数) |
|---|---|
| qwen-turbo | 0 / 9 |
| qwen-plus | 0 / 4 |
| qwen3-coder-plus | 1 / 5 |
| qwen3.6-flash | 2 / 5 |
| qwen3.7-plus | 6 / 6(±1) |
| qwen3.8-flash | 全对 |
| qwen3.8-max | 全对 |
turbo 把 SQL 注入报在第 9 行,实际第 18 行;空指针报在第 10 行,实际第 13 行。描述对,位置错。工程含义是便宜档的输出只能当线索清单,不能当定位结果接进跳转逻辑。
还有一条必须披露的:同一模型跑两次,得分会变。首轮走 CLI 通道时 qwen-turbo 是 -2.5(漏了 B1),本轮 -2.0(抓到了 B1);qwen-plus 首轮 2.0(B4 算全中),本轮 1.5(B4 只给对修复)。这是"每组只跑一次"的直接后果,所以下面所有结论都带"这一次实测"的限定,不外推成某个模型就是或就不是这个水平。

成本对账
单价来自 bl model list 免登录实跑,单位元/百万 tokens:
| 模型 | 输入 | 输出 | 缓存命中输入 |
|---|---|---|---|
| qwen-turbo | 0.3 | 0.6 | 0.06 |
| qwen-plus | 0.8 | 2 | --- |
| qwen3.8-flash | 0.8 | 2.7 | 0.1 |
| qwen3.8-max | 12 | 36 | 1.5 |
把真实 usage 乘上去:
| 模型 | 审一次 | 1 元可审 | 审 100 次 | 每天 10 次 × 22 工作日 |
|---|---|---|---|---|
| qwen-turbo | ¥0.0007 | ≈1,345 次 | ¥0.07 | ¥0.16 |
| qwen-plus | ¥0.0039 | ≈256 次 | ¥0.39 | ¥0.86 |
| qwen3.8-flash | ¥0.0353 | ≈28 次 | ¥3.53 | ¥7.76 |
| qwen3.8-flash + 显式 thinking | ¥0.0408 | ≈24 次 | ¥4.08 | ¥8.98 |
| qwen3.8-max | ¥0.4048 | ≈2 次 | ¥40.48 | ¥89.05 |
输入单价差 40 倍(0.3 对 12),单次总账差 545 倍(0.000743 对 0.404784)。差额不在单价上,在思考烧掉的 9,770 个输出 token 上。开头那句质疑被这组数字原样证实了。
三个模型的单价查不到:qwen3-coder 系列官方文档写明采取阶梯计费,qwen3.7-plus 和 qwen3.6-flash 在 bl model list 里没返回价格。查不到就不编,改用不等式。即使 qwen3.7-plus 与旗舰完全同价(12/36),这一次审查也只花 (502×12 + 2052×36)/1e6 = ¥0.0799,是旗舰 ¥0.4048 的 1/5.1;而它的实际单价不可能高于旗舰。这个下界是硬的,比任何估算都有用。
harness 里的 5 个坑
这几个坑都会让你的成本数据静默失真,不报错,最麻烦。
坑一,undici 的 300 秒响应头上限。本机用 bl text chat 非流式调 qwen3.8-max 会撞 Node undici 的 300s 响应头上限,实测 293.1 秒已经贴着这条线。表里的耗时改用直连端点测得。如果你要用该 CLI 跑旗舰档,加 --stream;但 --stream 下 --output json 不返回 usage,成本得另算。这是"旗舰档慢"的硬证据,不是我的网络问题。
坑二,--messages-file 要的是 messages 数组。写成 {"messages":[...]} 会报 i is not iterable,报错信息完全指不到根因。正确的形状长这样:
json
[
{"role": "system", "content": "你是代码审查器"},
{"role": "user", "content": "审查下面这份代码......"}
]
坑三,--page-size 100 静默返空。bl model list --page 1 --page-size 20 正常,--page-size 100 返回 items: [],不报错。批量抓单价的脚本要是用了大 page-size,会得到一张空表,然后以为这些模型没价格。同理 --output 只接受 text 和 json,写错值静默回落 text。
坑四,reasoning_tokens 不受 max_tokens 约束。工程含义是你没法靠 max_tokens 给思考模型封顶成本,得靠 --thinking-budget,或者直接换档。
坑五,同一份代码在不同模型下 prompt token 数不同。表里是 464、468、502、540,因为分词器不同。这也是"按百万 token 单价直接横比"不严谨的另一个原因,必须用真实 usage 算,不能用理论 token 数。
换成生产工具:ocr 的配置面
自己搭 harness 适合做实验,日常审 PR 得用现成工具。alibaba/open-code-review(命令名 ocr,Apache-2.0)v1.12.1 在 9 月 14 日发布,今天 GitHub Trending 日榜第 2,26k stars。
它内置 28 个 provider,其中一个叫 dashscope,端点是 https://dashscope.aliyuncs.com/compatible-mode/v1,预置模型列表前 5 个全是 qwen。也就是说接百炼不用写自定义 provider。
bash
npm install -g @alibaba-group/open-code-review
前置依赖:Node.js 与 Git ≥ 2.41。
API Key 在控制台的密钥管理页创建,新账号会自动带上各模型的免费额度。额度口径按官方文档:每个模型独立、通常 100 万 Token、90 天有效期、仅华北2(北京)地域、过期不补发、用尽不会自动切换到别的模型。
非交互式配置三行,CI 里也是这三行:
bash
ocr config set provider dashscope
ocr config set model qwen3.7-plus
ocr config set providers.dashscope.api_key sk-你的key
ocr llm test
配置字段名容易写错:Base URL 的字段是 url 不是 base_url,key 是 api_key,当前生效模型是单数的 model,复数的 models 只是 TUI 选择器的候选列表。内置 provider 挂在 providers.<name>.* 下,自定义的挂在 custom_providers.<name>.* 下,两个 map 不通用。配置文件在 ~/.opencodereview/config.json。
两个容易踩的点。其一,只 export DASHSCOPE_API_KEY 不够,resolver 要求 (url, token, model) 三元组齐全,环境变量只补 token 那一份。其二,别拿 Anthropic 兼容端点配 thinking 模式,issue #1064 报过 400:The tool_choice parameter does not support being set to required or object in thinking mode。该 issue 已关闭但没有维护者说明,所以我只说实测路径:走内置的 dashscope,OpenAI 兼容协议,没这事。
审查命令面:
bash
ocr review # 工作区模式,审 staged/unstaged/untracked
ocr review --from main --to feature-branch # 分支区间,merge-base 模式
ocr review --commit abc123 # 单个提交
ocr review --format json --output result.json # 结果落文件,方便接门禁
ocr scan --path internal/agent # 全文件扫描,不依赖 git 历史
模型有个硬要求:必须原生支持 tool calling。ocr 完全靠工具调用驱动审查,只在文本里"叙述"工具调用的模型(比如 deepseek-r1)无论怎么调 prompt 都跑不通。官方 FAQ 点名 qwen3 可用,我这边核到 bl model list --model qwen3.7-plus --output json 的 features 里确实含 function-calling。

场景到档位的映射
| 场景 | 档位 | 单次成本 | 依据 |
|---|---|---|---|
| pre-commit 粗筛 | qwen-turbo | ¥0.0007 | 7.4 秒返回,能抓注入这类明显问题;4 条误报,当提醒不当结论 |
| 日常 PR 审查 | qwen3.7-plus 这一档 | 单价未公开,输出 2,052 tok,上界 ¥0.0799 | 23.4 秒满分,本次实测的性价比拐点 |
| 安全相关、合并门禁 | qwen3.8-max | ¥0.4048 | 会报出埋点之外的真问题,行号可直接跳转 |
| 结论存疑时交叉复核 | 两个便宜档各跑一遍 | 两次便宜档的钱 | 这次 plus 漏 B1、coder-plus 漏 B3,漏报不重叠 |
最后一行是观察不是结论,7 组样本撑不起"交叉互补"这个普适判断,需要在你自己的代码上复验。而且误报会叠加,复核成本要一起算进去。
什么时候别接进流水线
先算时间账。V2EX 上有人实测 1,400 个 PR 跑了十多个小时,几行改动的小 PR 也要几分钟。这不是秒级工具,挂在同步阻塞的 pre-commit 上会被人骂。
也别指望它替代静态扫描。规则引擎报的是确定命中的模式,它报的是读懂代码之后觉得不对的地方,覆盖面不重合。
旗舰档不要挂在每次提交上。一次 ¥0.4048、293.1 秒,两个维度都撑不住高频调用。
复算脚本
单价和 token 都能自己核:
bash
bl model list --model qwen-plus --output json # 单价,免登录
bl quota list --model qwen-turbo # 限流,API Key 鉴权即可
token 在每次调用返回的 usage.prompt_tokens 与 usage.completion_tokens 里。算钱用 Decimal,别用 float:
python
from decimal import Decimal, ROUND_HALF_UP
def cost(in_tok, out_tok, price_in, price_out):
"""price 单位:元/百万 tokens"""
c = (Decimal(in_tok) * Decimal(price_in) + Decimal(out_tok) * Decimal(price_out)) / Decimal(1000000)
return c.quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP)
print(cost(502, 2052, "12", "36")) # 即使按旗舰单价算,qwen3.7-plus 这一次也只花 0.0799
Python 的 round() 是银行家舍入,算钱会得到反直觉的结果,所以这里显式指定 ROUND_HALF_UP。
小结
这次 8 组数据能确定的是三件事:满分出现在 23.4 秒那一档而不是 293.1 秒那一档;决定漏报与否的是会不会思考,不是单价;单价差 40 倍可以对应总账差 545 倍,因为思考 token 计入输出计费。
不能确定的是外推。每组只跑一次,同一模型两次跑分已经观察到差异;样本是一份 64 行的 Python,复杂仓库上拐点会不会右移,这次没测。下一步是把 ocr review --format json 接进 CI 当合并门禁,用真实 PR 复算一遍时间与金钱两个维度。
适用边界也说清楚:已经在用 Copilot 或 CodeRabbit 订阅、流水线跑得也顺的,这套方案解决的不是同一类问题。它面向的是需要自主指定模型底座、并且要按档位核算单次成本与耗时的场景。
实验材料(64 行被测代码、答案卷、计分脚本、8 组原始输出含 usage 与墙钟耗时)都在 source/lab/ 下,金额一律由真实 usage 乘免登录实跑单价得出,未返回单价的模型标注"未公开",不做估算。