先说结论:AI 编程助手不是均匀提速的工具。同一个项目里,有的任务它能省掉大半时间,有的任务交给它之后你需要花更多时间读 diff、改 bug、写测试。
差别不在「模型够不够聪明」,而在任务本身的形状。用两个维度就能把它分清楚。
两个维度分出四类任务#
第一个维度是可验证性:改完能不能用机器判断对错。有单测、有类型检查、有明确输入输出的,可验证性高。
第二个维度是上下文局部性:需要的知识是不是都在眼前这几个文件里。跨五个模块、依赖运行时状态、牵扯历史约定的,局部性低。
| 上下文局部 | 上下文分散 | |
|---|---|---|
| 可验证 | 最该交出去:补测试、写样板、改命名 | 可以交,但要先给足上下文和接口约束 |
| 难验证 | 谨慎:正则、SQL、配置、边界条件 | 别交:架构调整、并发模型、安全边界 |
左下那一格最容易被误判。正则和 SQL 看起来是「小任务」,但错误往往藏在边界条件里,而边界条件恰恰是你没说出口的那部分需求。
可以放心交的四类活#
补齐测试。给它一个已有函数和现有测试风格,让它写边界用例。它常常能想到你懒得写的空值、超长、异常路径。前提是测试要真的能跑,而不是只长得像测试。
样板与胶水代码。DTO 转换、序列化、命令行参数解析、日志埋点、分页模板。这类代码的模式稳定、验收标准明确,写错了也容易发现。
批量机械改写。函数改名、参数顺序调整、把旧 API 换到新 API、统一错误处理。一百处重复改动交出去,比人肉搜索替换可靠,前提是每一步都能编译通过。
解释陌生代码。读一份没注释的旧模块,让它先讲一遍调用链,你再对照验证。这个用法几乎不产生风险,因为产出是理解,不是代码。
判断标准很实际:如果它能在一分钟内被机器证伪,就交;如果需要你读三遍 diff 才能判断对不对,那读 diff 的时间已经超过自己写。
别整段交出去的五件事#
跨模块的架构调整。它能看到你贴进去的文件,看不到没贴的那些。改 A 模块时顺手破坏了 B 模块的隐式约定,是最常见的翻车方式。
并发与事务边界。锁的粒度、事务的提交时机、重试的幂等性,这些靠读代码很难验证,靠跑测试也很难覆盖。让它写并发代码,通常得到一份「看起来对」的实现。
性能敏感路径。它倾向于写清晰的实现,而清晰和快经常冲突。热点路径上的循环、内存分配、N 加一查询,还是自己看。
安全边界。鉴权、加密、输入校验、权限判断。这类代码出错不报错,只是悄悄放行,而且很难在测试里暴露。
依赖升级。它知道旧版本 API 长什么样,但对你的锁文件、私有源、构建缓存一无所知。升级这事让它查资料可以,让它直接改多半会留下一个跑不起来的仓库。
验收流程决定净收益#
同样是「生成加修改」,有人觉得提效,有人觉得被拖累,差别通常在验收环节。我的做法是三条硬规则:
- 每次改动不超过五个文件。超出就拆任务,否则 diff 变大之后你根本不会认真读。
- 先有测试再合入。没有测试覆盖的改动,宁可自己写,也不要让「看起来对」的代码进主干。
- review 只看三件事:错误处理路径、边界条件、有没有偷偷改到无关文件。
第三条最关键。AI 改代码时最容易多做的两件事,是顺手重构无关函数,以及把异常吞掉换成默认值。这两种改动在 diff 里都很小,在线上都很致命。
还有一个容易被忽略的成本:审查的注意力。连续看五份 AI 生成的 diff 之后,第三份开始你就只会扫一眼。所以任务要小、要短、一次只做一件事——这不是为了模型,是为了你自己还能保持审查质量。
"""accept_check.py —— AI 改动合入前的验收闸门(纯标准库,需 git / ruff / pytest)"""
import shutil, subprocess, sys
def run(cmd, timeout=300):
if not shutil.which(cmd[0]):
return None, f"未安装 {cmd[0]},跳过"
p = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
return p.returncode, (p.stdout + p.stderr).strip()
def main():
_, diff = run(["git", "diff", "--stat", "HEAD"])
changed = [ln for ln in (diff or "").splitlines() if "|" in ln]
print(f"改动文件 {len(changed)} 个") # 超过 5 个就该重新拆任务
if len(changed) > 5:
print("改动面过大,建议先人工读一遍 diff")
for step in (["ruff", "check", "."], ["pytest", "-q", "--maxfail=3"]):
code, out = run(step)
if code is None:
print(out)
continue
print(f"{'通过' if code == 0 else '失败'}:{' '.join(step)}")
if code != 0:
print(out[:1500]) # 静态检查或测试不过,直接拒绝合入
sys.exit(1)
if __name__ == "__main__":
main()
这个脚本的价值不在自动化本身,而在于它把「验收」变成了固定动作。有固定动作,才有稳定的净收益;靠自觉的验收,忙起来第一个被跳过。
| 任务类型 | 可交程度 | 验收方式 |
|---|---|---|
| 补测试、写样板 | 高 | 跑测试,看覆盖率变化 |
| 批量重命名、API 迁移 | 高 | 编译通过 + 抽查三处 |
| 正则、SQL、时间处理 | 中 | 边界用例 + 手工构造反例 |
| 单模块功能开发 | 中 | 先写测试再让它实现 |
| 并发、事务、鉴权 | 低 | 不交,或只让它写测试 |
| 架构调整、依赖升级 | 低 | 只让它做调研,不动手 |
结论#
- 用可验证性和上下文局部性判断,而不是用「难不难」判断。
- 测试、样板、机械改写、代码解释是四类高收益任务。
- 并发、安全、性能、架构、依赖升级不要整段交出去。
- 每次改动控制在五个文件内,diff 越大越没人认真读。
- 把静态检查和测试做成合入前的固定闸门,别依赖自觉。
工具在进步,验收标准不能因此放松。你能稳定拿到的收益,取决于你能多快判断一份 diff 对不对——这个能力不会随着模型变强而自动获得。
完