你让 Agent 审一个未提交的改动。它读了十几个文件,过了一会儿给出一个看起来很完整的报告。
这时你顺手又改了两行代码。
报告还能用吗?
很多代码评审工具默认把答案当成一张静态截图:开始时拿到 diff,结束时输出 finding。可工作区不是截图。评审进行中,文件会变、分支会切、未跟踪文件会出现,甚至 HEAD 也可能移动。报告如果没有绑定它实际看过的目标,最后那句"第 8 行有问题"可能已经对应不到任何代码。
Blade Code v0.10.7 新增的 Session-native 只读 Code Review,把这个问题放到了运行时边界里处理。它的重点不是多一个 /review 命令,而是:评审先固定目标,目标变化就明确标记过期,无法证明位置对应时直接失败。
评审开始前,先给工作区做一张"指纹卡片"
packages/cli/src/services/GitReviewTargetService.ts 没有把目标简单写成一个路径。它先解析目标类型:
text
/review uncommitted 当前 HEAD 到工作区的 staged、unstaged、untracked
/review base main merge-base 到当前 HEAD 的变化
/review commit <sha> 某个 commit 引入的变化
然后把这些东西连续写进 SHA-256:
ts
hash.update(`kind\0${request.kind}\0head\0${headSha}\0`);
hash.update(diff);
hash.update(`\0merge-base\0${mergeBase}`); // base review
hash.update(`\0commit\0${commitSha}`); // commit review
未跟踪文件也不能漏掉。实现会把相对路径和文件内容写入同一个 digest,并为它们记录从第 1 行开始的 changed range。最后返回的 target 信息包含 headSha、文件数量和 digest,而不是只有一句"审当前目录"。
这张指纹卡片解决的是一个很具体的追责问题:当报告说"这个 finding 来自这次提交"时,系统至少知道它对应哪一个 commit 身份、哪一份 diff 和哪一批文件。
为什么一定要记录精确 changed line
只保存文件名还不够。一个文件里可能有 500 行,Agent 报告第 8 行的问题,必须证明第 8 行确实属于这次评审目标。
changedLinesFromDiff() 逐段解析 unified diff 的 hunk,维护旧行号和新行号,把新增、删除的行合并成连续范围。模型返回 finding 时,宿主会检查:
- 路径属于 target;
- 行号没有越界;
- 行范围和真实 changed lines 有重叠;
- priority、confidence 和结构化字段符合协议。
只要输出无法解析、引用了未改动的代码,整个 review 会 fail closed。这个选择有点严格,但它避免了一种很难排查的假精确:报告看上去指向 src/auth.ts:L8,实际上 Agent 读的是另一版文件。
Finding 不是一段自由文本,而是一个有边界的对象:
ts
{
title: "[P1] 命令式标题",
body: "触发场景、影响和修复方向",
priority: 1,
confidenceScore: 0.99,
codeLocation: {
path: "src/auth.ts",
lineStart: 8,
lineEnd: 8
}
}
这对读者的实际收益是,拿到报告后可以直接回到改动行处理,而不是重新猜 Agent 到底在说哪个版本。
评审跑着跑着,目标变了怎么办
最常见的现场是这样的:
docs/reference/native-code-review.md 明确规定:评审运行期间 target 发生变化,结果标记为 stale。系统不把旧报告涂成绿色,也不自动把模型重新跑一遍来"补救"。
这是一条值得迁移到其他工具的判断规则:
评审结果要么绑定一个可复核的输入,要么公开承认输入已经失效。
很多工具只关注模型给出的结论,却没有把"结论对应哪一版代码"当成一等信息。实际协作中,后者往往更重要。
只读不是一句系统提示词
如果评审 Agent 可以写工作区,它就可能一边审、一边修,最后你分不清报告是针对原始改动,还是针对它自己改过的版本。
Blade 的内置 reviewer 只允许 Read、Glob、Grep 和分类后的只读 Bash。GitReviewTargetService 还会限制目标最多 500 个文件、diff 与未跟踪内容合计最多 8 MiB。reviewer 进入 workspace-read-only sandbox,网络关闭,HOME、Blade storage、provider credentials 不可读,Git 使用隔离配置。
这里有两个不同的边界:
text
工具白名单 防止 reviewer 主动修改或外传
target digest 防止 reviewer 的结论脱离它实际看到的输入
只做第一层,Agent 可能安全地审了一份已经过期的代码;只做第二层,Agent 仍可能在审查过程中改变代码。两层需要一起存在。
为什么 review 生命周期要写进 Session
Blade 没有把评审状态放在 Web 页面组件里。父 Session JSONL 会记录:
text
review_started
→ review_completed
→ rendered assistant report
packages/cli/src/context/reviews.ts 会按 reviewId 投影这些事件。只有出现 review_started 且还没有完成事件的记录,才是 pending review。进程在 reviewer 完成前退出,下一次 owner 写入 interrupted;如果已经写入 review_completed,但渲染消息前崩溃,fresh Session 可以直接恢复报告,不需要重放模型。
这和"页面刷新后重新发起一次请求"差别很大。前者恢复的是已经落盘的事实,后者可能把一次只读评审变成两次不同输入的模型调用。
Fork 不复制 live review lifecycle,rewind 会移除 checkpoint 后的 review 事件和报告。它们不是 UI 细节,而是 Session 历史语义的一部分。
这次发布证明了什么,没证明什么
公开 v0.10.7 changelog 写明了三类 target、stale、abort、interrupted、crash projection、fork/rewind、finding contract 和只读 sandbox 的回归覆盖,也记录了 production Web、ACP、TUI 的真实 GPT 验证。
这些是 release notes 的交付声明,不是我在这篇文章里重新跑出的 production 结果。本文能直接核对的是公开源码和文档中的目标解析、digest、changed line、finding 校验以及 Session 投影逻辑。没有当前凭证时,不把 release note 写成自己的端到端复测。
如果你要评估自己的 AI 评审器,先问这五个问题:
- 评审输入有没有 commit identity、文件内容和 changed line 的摘要?
- 未跟踪文件是否被纳入目标,而不是默认忽略?
- 评审中目标变化后,结果是 stale、重跑,还是悄悄继续?
- finding 的路径和行号是否经过宿主校验?
- 进程重启后,恢复的是完成事件,还是重新调用模型?
回答不清楚之前,先别把"AI 代码评审"当成一份可以直接合并的意见。它首先是一份针对特定输入、在特定时间点生成的证据。
源码依据 :Blade Code v0.10.7(公开提交 1044d7e565fa55ba8b60ae6749d918c37dd59966),docs/reference/native-code-review.md、packages/cli/src/services/GitReviewTargetService.ts、packages/cli/src/context/reviews.ts。