AI 写的代码,为什么总像“差不多对”?我用 opencode 把 commit 前审查做成三道门
我用opencode做代码审查,注重agents.md的质量,聚焦多轮核心BUG输出质量,减少MR被驳回的风险
- 审查阶段不要使用可写 agent。 可写 agent 看到问题后很容易顺手修复,修复后的代码又变成新的审查对象。我要的是报告,不是未经确认的工作树变化。
AGENTS.md不是越长越好。 我见过把抽象价值观、编码风格和所有历史事故都塞进去的规则文件,最后 AI 什么都读到了一点,却没有一条真正执行到位。规则应当短、具体、可验证。- 不要只统计 AI 提了多少条意见。 我更看重高价值意见的比例、误报率、漏报案例和处理耗时。意见越多不等于审查越好。

我最近在 Go 项目里遇到过一个很典型的例子:我让 AI 把一个用 sync.Mutex 保护的缓存改成 sync.RWMutex,因为读请求远多于写请求。代码能编译,go test -race 也通过了,我正准备提交时又看了一遍 diff,才发现 AI 在 RLock() 之后没有为所有提前返回的分支补上 RUnlock()。原代码用 defer Unlock() 做了一次兜底,锁的种类变了,兜底结构却没有一起变。
这个问题不是函数体“完全写错”,而是局部正确、整体破坏。测试没有触发那个错误分支,竞态检测自然也抓不到。类似的情况我见过很多:错误被 _ 接住、context 半路丢失、goroutine 没有退出条件、异常路径没有补齐、依赖被悄悄换成团队没人维护的库。AI 生成代码的速度越快,diff 的影响半径就越容易被低估。
我现在把 commit 前的审查拆成三道门:机器门负责确定性检查,AI 自审负责挑出候选风险,人工 review 负责业务判断和最终签字。这篇文章记录我在 Go 项目里实践这套流程的方式,也会说明 opencode 适合放在哪一层、哪些事情仍然不能交给它。
蓝色 表示工具或事实,绿色 表示我的实践判断,红色 表示容易踩的坑,黄色 表示可以直接改写到自己项目里的模板。
一、先把 commit 当成一次小型发布
以前我把 git commit 当作保存进度,后来几次线上问题让我改了看法:commit 是把一组行为变化交给团队和环境的发布动作。即使还没有上线,它也已经影响了后续 review、合并、回滚和事故追踪。
我的三道门是串联关系:
git add -p
↓
机器门:格式、构建、测试、静态分析、漏洞与密钥扫描
↓
AI 自审:只读 staged diff,报告有证据的风险
↓
人工 review:不变量、业务边界、上线与回滚
↓
git commit
三道门各自负责不同的问题,不能互相替代:
| 关卡 | 我让它负责什么 | 不通过时怎么做 |
|---|---|---|
| 机器门 | 可重复、可自动判断的检查 | 修复后重新运行,不让失败静默通过 |
| AI 自审 | 从 diff 和仓库上下文里寻找候选风险 | 对照证据逐条确认,区分阻断、待验证和误报 |
| 人工 review | 业务不变量、权限边界、边界条件和回滚 | 由作者或领域负责人补充设计并重新审查 |
机器负责“确定”,AI 负责“提醒”,人负责“决定”。只要其中一层越权签字,流程就会变成形式。
二、第一道门:让机器先清掉机械问题
机器门只做我能稳定复现的检查。下面是一个 Go 项目的示例脚本;它是文章里的模板,不代表每个仓库都必须原样使用。工具版本、模块数量和测试耗时,都要按项目实际情况调整。
#!/usr/bin/env bash
# scripts/precommit.sh
set -euo pipefail
echo "==> gofmt"
test -z "$(gofmt -l .)" || {
echo "请运行 gofmt -w . 后再提交"
exit 1
}
echo "==> go vet"
go vet ./...
echo "==> go build"
go build ./...
echo "==> unit tests with race detector"
go test -race -count=1 ./...
echo "==> staticcheck"
staticcheck ./...
echo "==> govulncheck"
govulncheck ./...
echo "==> gitleaks on staged content"
gitleaks protect --staged --redact --no-banner
我会重点看下面四件事。
- 先统一格式,再开始 review。
gofmt不是审美检查,而是在减少 diff 噪音。AI 生成的代码可能出现缩进、import 顺序或局部格式不一致;这些问题应该在 reviewer 看到业务逻辑之前被清掉。 - 让
-race真正执行,而不是只看 CI 结果。go test -race只能发现被测试路径触发的竞态,所以它不是并发正确性的证明;但把它放在本地门禁里,至少能尽早抓住共享map、slice或 channel 的明显问题。-count=1用来避免测试缓存掩盖本次运行。 - 把
go vet和staticcheck当成互补工具。go vet负责 Go 工具链内置的可疑构造检查,staticcheck覆盖更多静态分析场景。它们仍然不能替代业务 review,但能把“看起来能跑、语义上可疑”的代码提前暴露出来。 - 密钥扫描只扫本次要提交的内容。 我一开始用全仓库扫描,历史 example 文件里的占位字符串反复触发告警。后来改用
gitleaks protect --staged --redact,范围更接近 commit 本身,命中后也不会把敏感内容再次打印到日志里。
机器门的目标不是证明代码绝对正确,而是把明显、重复、机器擅长的问题在 commit 前清零。
三、第二道门:让 AI 自审,但不让 AI 签字
机器门通过后,我会让 AI 只读当前的 staged diff。这里的关键不是一句“请仔细审查”,而是给它清晰的范围、固定的输出字段和明确的停止条件。
我现在使用的提示词大致如下:
你是本次 commit 的代码自审助手,只能审查,不能修改文件。
审查对象:git diff --staged
只报告能够被当前 diff 或仓库上下文直接支持的问题,不输出风格建议、泛泛的重构建议或无法定位的“考虑加测试”。
按以下五类检查,按严重程度排序,每类最多 3 条:
1. 错误处理:error 被忽略、上下文丢失、资源未释放;
2. 并发与资源:锁未释放、goroutine 无退出路径、共享状态未保护;
3. 安全与边界:输入未校验、请求无超时、响应体无大小限制、日志泄露敏感信息;
4. 接口契约:函数语义、字段兼容性、nil 行为或权限边界发生变化;
5. 范围越界:引入任务未要求的依赖、功能、重构或行为变化。
每条意见必须包含:
- 文件:行号
- 触发条件
- 证据(对应代码片段或调用关系)
- 验证方式
- 置信度:高 / 中 / 待验证
没有发现时写“未发现”。最后输出:
- 仍需人工关注的点:
- 未覆盖的检查项:
- 建议:阻断 commit / 待人工确认 / 通过 commit
我不再强迫 AI 禁用“可能”“或许”这类词,而是要求它给出置信度。不确定并不可怕,把不确定伪装成确定才危险。 没有文件、行号和证据的意见,我会直接归入噪音。
我处理审查结果时只做四种动作:
- 证据充分、触发条件明确: 修复问题,重新跑机器门,再审一次 diff。
- 信息不足但值得追查: 标记为“待验证”,补测试、日志或仓库上下文,不直接当成阻断结论。
- 对照代码后确认是误报: 记录不采纳的理由,避免下一轮重复争论。
- 涉及业务目标或权限边界: 交给领域负责人,AI 只能提供线索,不能替代决策。
如果我把这一步封装成 opencode 的自定义命令,可以放在项目的 .opencode/commands/commit-review.md:
---
description: 审查当前 staged diff,只报告有证据的风险
agent: plan
---
你是一名 Go 项目的 commit 前审查助手。本次只审查,不修改。
审查以下变更:
!`git diff --staged --stat && git diff --staged`
遵循团队的 Go 审查规则。优先报告错误处理、并发与资源、安全边界、接口契约和范围越界问题。每条意见必须包含文件:行号、触发条件、证据、验证方式和置信度。
! 加反引号是 opencode 的 shell 插值语法,会在提交提示词前把命令输出插入上下文。这类插值会在 agent 权限流程之外执行,所以我只放入自己信任的、只读的命令,不把未经审查的参数拼进去。
四、opencode 的价值:把角色、权限和规则拆开
我使用 opencode,不是因为它“更会写代码”,而是因为它能把审查过程拆成几个可配置的角色。官方文档把 agent、command、skill、rule 和权限分别管理,这正好对应我对审查流程的需要。
| 角色或机制 | 我让它做什么 | 权限原则 |
|---|---|---|
plan | 作为审查主代理,阅读 staged diff 并输出报告 | 禁止编辑; shell 默认询问 |
explore | 查找仓库内的调用链、相似实现和约束文档 | 只读仓库 |
scout | 查看外部依赖的文档、版本和已知问题 | 只读外部资料,结论附来源 |
general | 需要时做安全、性能等专项视角 | 不默认启用,避免扩大权限和噪音 |
AGENTS.md | 保存团队长期规则 | 进入 Git,短而具体 |
commands | 定义 /commit-review 这样的触发入口 | 只编排动作,不堆所有规则 |
skills | 封装 Go 工具链和输出方式 | 复用方法,按需加载 |
我会给审查主代理设置明确的权限边界。下面的配置重点是示意“编辑拒绝、读取 diff 可用”,实际的命令白名单要按自己的环境收紧:
{
"$schema": "https://opencode.ai/config.json",
"agent": {
"plan": {
"mode": "primary",
"permission": {
"edit": "deny",
"bash": {
"*": "ask",
"git diff*": "allow",
"git status*": "allow"
}
}
}
}
}
这里有一个容易误解的地方:plan 是工作模式,不等于所有命令都自动安全;真正决定能不能编辑、能不能执行 shell 的,是权限配置。我曾经在审查阶段使用可写的 build agent,结果它发现问题后直接替我改了代码,连修复本身都要重新排查。
- 让不同 agent 负责不同上下文。
我通常先让 plan 看 diff,再按需要调用 explore 或 scout:
@explore 查找 internal/order 中所有调用 OrderService.MarkPaid 的位置,
并找出与“订单支付幂等”相关的 README、AGENTS.md 和文档。
@scout 查看当前依赖的官方文档和变更记录,确认本次升级是否有兼容性变化,
不要只引用搜索摘要,给出可以复核的链接。
这样做的好处是,主审代理不需要一边猜仓库上下文、一边做风险判断。我把“找信息”和“下判断”分开后,审查意见明显更容易复核。
- 模型按任务难度分级,不要把版本写死。
我的经验是,仓库路径查找不需要最贵的模型,跨文件理解和安全专项才值得提高推理深度。可以按这个思路配置:
| 环节 | 模型策略 | 我关注的指标 |
|---|---|---|
| 仓库查找 | 轻量、响应快 | 是否找全调用点和规则文件 |
| 主审查 | 上下文和工具调用能力优先 | 是否能把风险落到证据 |
| 安全或性能专项 | 必要时使用更强推理 | 召回率是否值得额外成本 |
模型名称、可用 provider 和价格都会变化,我不把“最新版本”和固定费用写进团队规范。每次切换后,我会用 /model 确认实际模型;如果使用 variants,也会检查字段名和最终推理配置是否真的生效。我更关心每周的误报率、漏报案例和审查耗时,而不是一次调用的理论价格。
- 把规则放在项目里,而不是藏在个人记忆里。
我会把长期有效的规则放进项目根目录的 AGENTS.md,例如:
# AGENTS.md
## Commit 前审查纪律
1. 只读 staged diff,不修改代码;
2. 每条意见必须包含文件:行号、触发条件、证据和验证方式;
3. 不确定的内容标记“待验证”,不要伪装成结论;
4. 涉及业务不变量、权限和回滚的问题,必须留给人工确认。
## Go 运行时规则
1. 外部调用沿链路传递 context,不偷偷换成 context.Background();
2. 成功拿到资源后尽早安排释放,循环内的 defer 要特别检查;
3. goroutine 必须有退出路径,共享状态必须有明确的同步策略;
4. 外部输入、请求超时和响应体大小都要有边界。
规则不需要写成长篇风格手册。每条规则最好都能在几十秒内写出一个反例,否则它很可能还停留在口号层面。 CLAUDE.md 可以作为兼容入口,但我会优先使用项目级 AGENTS.md,避免两份规则同时存在时产生歧义。
- 用 skill 封装“怎么检查”。
command 解决“什么时候触发”,skill 解决“具体怎么做”。例如 .opencode/skills/go-quality/SKILL.md 可以定义:
---
name: go-quality
description: 按项目约定运行 Go 代码质量检查
license: MIT
---
## 检查顺序
1. gofmt -l .
2. go vet ./...
3. go test -race -count=1 ./...
4. staticcheck ./...
5. govulncheck ./...
6. gitleaks protect --staged --redact
## 输出要求
- 只报告非空结果;
- 记录每一步的退出状态;
- 不要用 `|| true` 吞掉失败;
- 环境缺少工具时明确写出,不要假装通过。
我会在 @go-quality 的结果出来后,再让 plan 解释高风险项,而不是让 AI 自己把工具失败改写成“检查通过”。LSP 也可以提供 gopls 的类型、引用和诊断信息,但它只是上下文来源,不是机器门的替代品。
五、第三道门:人工只回答机器和 AI 回答不了的问题
当我走到人工 review 时,不再重复检查缩进和明显错误,而是把 diff 放回业务上下文里,依次问四个问题:
- 不变量还在吗? 原有 API 行为、权限边界、并发限制、数据一致性要求是否仍然成立?
- 异常路径完整吗? 空值、重复请求、超大输入、非法字符、超时和部分失败有没有明确行为?
- 资源生命周期清楚吗? 锁、文件、HTTP 响应、数据库 rows、goroutine 和 channel 谁创建、谁释放、谁负责关闭?
- 上线和回滚能执行吗? 是否需要灰度或 feature flag?数据库迁移能否回滚?出问题时用什么指标发现?
例如,下面这段代码单看每一行都很顺:
func (s *OrderService) MarkPaid(ctx context.Context, orderID string) error {
order, err := s.repo.FindByID(ctx, orderID)
if err != nil {
return fmt.Errorf("find order: %w", err)
}
order.Status = "paid"
return s.repo.Save(ctx, order)
}
但我把它放回支付回调的上下文后,会继续追问:
- 两个回调同时到达时,是否会重复写入?
- 已经是
paid的订单再次调用时,是否应该幂等返回? - 状态更新和审计记录是否需要同一个事务?
Save失败后,调用方能否安全重试?
这些问题不一定能从函数体本身得出答案,需要产品规则、数据模型和上线方案共同回答。人工 review 的价值不是再读一遍语法,而是判断这段代码有没有破坏系统原本承诺的行为。
六、让审查结果留下来:commit message 也是质量门
AI 审查如果只存在于一次 TUI 对话里,几天后就很难追溯。我会把结论摘要放入 PR 描述,并在 commit message 里留下变更原因、不变量、测试和风险。
下面是我会交给 AI 填充、再由自己确认的 commit message 模板:
<scope>: <一句话目标>
变更原因:
- 为什么需要改
- 由什么场景触发(issue、反馈或告警)
改动范围:
- 修改了哪些包、接口、配置或依赖
- 明确哪些内容没有改
不变量:
- 哪些 API、权限、数据一致性或性能行为不能改变
测试:
- 运行了哪些命令
- 是否覆盖失败路径、边界条件和并发场景
上线风险:
- 是否需要灰度或 feature flag
- 回滚动作是什么
- 用什么指标确认上线正常
我把 AI 自审报告附在 PR 的“AI 自审”区块里,至少保留:审查对象、使用的规则、工具结果、阻断项和人工未覆盖项。能被检索到、能被下一次复盘引用的审查,才算真正进入团队流程。
这里我有一个重要调整:机器检查适合放进 pre-commit,AI 自审是否放进 hook,要看团队是否接受网络依赖、模型耗时和服务失败。对我来说,AI 自审默认是一个明确执行的命令;如果要自动阻断 commit,就必须先定义超时、重试和人工放行规则,不能用 || true 偷偷软化。
七、我踩过的坑,以及 opencode 的边界
- 审查阶段不要使用可写 agent。 可写 agent 看到问题后很容易顺手修复,修复后的代码又变成新的审查对象。我要的是报告,不是未经确认的工作树变化。
- 不要用
|| true吞掉关键检查。 无论是gitleaks、漏洞扫描还是 AI 命令,失败都应该让人看到。允许“失败但继续”之前,必须把结果写入可追溯的日志,并明确由谁决定是否放行。 - 不要把模型名称和费用当成永久事实。 provider、模型、字段名和价格都会变化;配置更新后要用实际命令确认,而不是只看配置文件里写了什么。
AGENTS.md不是越长越好。 我见过把抽象价值观、编码风格和所有历史事故都塞进去的规则文件,最后 AI 什么都读到了一点,却没有一条真正执行到位。规则应当短、具体、可验证。- 不要只统计 AI 提了多少条意见。 我更看重高价值意见的比例、误报率、漏报案例和处理耗时。意见越多不等于审查越好。
opencode 也有清楚的边界:
- 它不能替我决定订单金额、租户隔离、权限层级等业务规则;
- 它不能替代
gofmt、测试、静态分析、漏洞扫描等确定性工具; - 它看到的上下文取决于我提供的仓库、文档和调用链,没读到的事故复盘不会凭空出现在结论里;
- 它输出的“通过”不是合并授权,最终仍然要由人确认不变量、边界和回滚。
一个工具能清楚说明自己做不到什么,往往比一个声称什么都能做的工具更可靠。
给读者的启示
- 把 commit 当成一次小型发布。 先跑机器门,再做 AI 自审,最后由人结合业务上下文确认;顺序本身就是风险控制。
- 机器门先覆盖确定性问题。 Go 项目可以从
gofmt、go vet、go test -race、staticcheck、govulncheck和 staged 密钥扫描开始,再按仓库实际情况调整。 - AI 自审要少而具体。 固定检查范围,要求文件:行号、触发条件、证据、验证方式和置信度;没有证据的意见就标成待验证。
- opencode 的核心价值是流程编排。 用
plan做主审,按需用explore查仓库、用scout查外部资料,再用AGENTS.md、commands 和 skills 把规则与方法版本化。 - 不要让关键检查静默失败。 失败要可见,结果要可追溯,是否放行要由明确的人负责。
- commit message 也是一次自问。 如果我写不出不变量、测试范围和回滚方案,通常说明这次改动还没有被真正想清楚。
我现在的判断是:代码质量不是由 AI 写得多快决定的,而是由 commit 前的审查链路有多稳决定的。 opencode 这类工具真正有价值的地方,是让我可以把“谁来审、看什么、能不能改、用什么上下文、结果留在哪里”都配置清楚。AI 可以成为团队质量流程的一部分,但按下 commit 按钮的人,仍然必须知道自己在交付什么。
我参考的公开资料: