AI 编程代理高速产出代码但悄然破坏架构,作者分享用 ARCHITECTURE.md 与 AGENTS.md 将人类标准转化为机器可执行的检查规则。
AI 编码代理速度很快。它们写出来的代码能编译、乍一看也没问题,但会慢慢侵蚀你的架构。一个组件直接导入了数据库层。一个函数膨胀到 200 行,包含三层嵌套的三元表达式。一个没人要的依赖出现了。一个路由文件里塞进了 formatDate helper,然后是 slugify,再然后是 retryWithBackoff。测试跑遍了每一行,但从不检查结果。
这些都不是什么新鲜事。赶工的初级开发者也会做同样的事。新的是量。如果一个代理每天开十个 PR,光靠人工仔细审查根本 catch 不过来。
答案不是停用 AI,而是把你的标准写成机器能够执行的形式,这样无论是谁写的代码、什么东西写的代码,规则都能生效。以下是我通常为项目搭建的工具栈。
工具只能执行已经存在的规则。在添加任何工具之前,我会先决定代码应该长什么样,定义规则,然后写入两个文件:
ARCHITECTURE.md 涵盖各层、各层之间哪些可以导入、状态放在哪里、以及数据流如何流转。
AGENTS.md 在当下是必不可少的,通常自动生成然后由我编辑。它涵盖 AI 代理的约定:命名规范、推荐使用的模式、需要避免的模式、以及禁止使用的工具(例如:"我们用 Biome,不要加 ESLint")。
代理会读取这些文件。新员工也会。而下面的每一个工具都会关联回其中一个文件里的某个章节,所以当检查失败时,消息会解释原因,而不只是说"什么"。
我最近发现的一个东西:dependency-cruiser 可以检查哪些文件导入了哪些。你描述层与层之间允许的依赖方向,当有东西越界时它就会让构建失败:
这是 AI 最容易犯的错误,因为它总是挑最近的导入路径写。一个永远给出相同答案的规则,在 review 之前就能拦住它。
dependency-cruiser 告诉你哪些文件在互相通信。它没法告诉你文件里面的代码在做什么。这正是 Semgrep 的用武之地。
Semgrep 是一个理解代码的模式匹配器。你写一条规则说"形状像 X 的代码,在匹配 Y 的文件中,就是违规"。Semgrep 把每个文件解析成语法树,报告每一次匹配。它不运行你的代码,也不试图理解你的整个应用。它只检查形态。
一条规则是一个简短的 YAML 条目:
rules:
- id: api-routes-no-helpers
languages: [python]
severity: ERROR
message: >
routes.py 只用于路由处理器。
把 helper 移到它们自己的模块(见 ARCHITECTURE.md → API)。
paths:
include: [packages/api/src/api/routes.py]
patterns:
- pattern: |
def $FUNC(...):
...
- pattern-not-inside: |
@$ROUTER.$METHOD(...)
def $F(...):
...
pattern 是要匹配的代码。$FUNC 匹配任意名称,... 匹配任意内容。
pattern-not-inside 是例外。有路由装饰器的函数是允许的。
paths 把规则限定在特定文件。
message 是开发者(或 AI 代理)看到规则失败时显示的内容。写成一条指令。
运行 semgrep --config .semgrep/rules/ 可以对整个仓库检查那个文件夹里的每条规则。我们按领域(app、API、mq)各放一个 YAML 文件,这样规则容易找到,每条规则都配一个小测试 fixture,包含应该匹配和不应该匹配的代码,这样我们就知道规则确实捕获了我们想要的东西。
读一下他们的文档(或者问你的代理),看看能做到什么程度,发挥创意。
这是我们最需要的规则。有些文件只应该做一件事,而 AI 代理喜欢往里面塞"就一个小 helper"。几个月后,你的路由文件就变成了一半工具函数。
所以我们让它变得不可能。这些文件每个只能包含一种东西:
Helper 放在自己的模块里,在那里可以命名、测试和复用。规则消息告诉代理具体怎么做,所以下一次它通常会自己修好。
其他值得执行的模式
一旦你用形态的思路思考,很多 review 评论都可以变成规则:
tx: DbClient = db(这会隐藏事务 bug)Semgrep 支持很多语言,所以可以覆盖任何你想要的东西。
经验法则:如果一条 review 评论可以写成代码模式,就把它做成 Semgrep 规则。然后它在每个 PR 上都会以相同的方式被检查。
你可能已经有违规了。别让这阻止你。在 CI 中,Semgrep 可以对比 baseline:
架构规则保持代码整洁。它们不告诉你代码是否安全。AI 代理和人类一样会写安全 bug,只是更快:用字符串拼接 SQL、一个端点忘了检查调用者身份、把 token 粘贴到配置文件里、用户输入被渲染成 HTML。
安全工具分为三组,每组能看到其他组看不到的东西:
SAST 和 SCA 在每个 PR 上运行,只需要秒或分钟。DAST 需要一个已部署的应用,所以它针对 preview 环境或 staging 运行。以下大多数工具覆盖了不止一组:
我是 OSS 支持者,所以我通常用 Semgrep 和 Trivy。我工作过的公司用最适合自己的。没有评判,选哪个都行。
好的 SAST 工具会跟踪数据流:它们从请求处理器开始追踪用户输入,如果输入未经转义就到达 SQL、shell 或 HTML,就 flag 它。问一个代理"加个搜索",你可能会得到:
@router.get("/search")
def search(q: str, db: Session = Depends(get_db)):
return db.execute(text(f"SELECT * FROM products WHERE name LIKE '%{q}%'"))
能跑,但这是 SQL 注入。Semgrep Code 用你已经跑着的引擎捕获它(加 --config p/owasp-top-ten)。CodeQL 对公开仓库免费,并在 PR 中显示结果。Snyk Code 和 Checkmarx 在各自平台覆盖同样的领域。只在高置信度规则上让构建失败。嘈杂的检查会被关掉。
SCA:你的依赖
代理自由地添加包,有时是过时的,有时是编出来的和被抢注的("slopsquatting")。Trivy 是免费基线,覆盖漏洞包、secret、容器镜像和 IaC:
trivy fs . --scanners vuln,secret,misconfig --severity HIGH,CRITICAL --exit-code 1
Trivy 只匹配版本,所以无论你是否使用了有漏洞的部分,它都会 flag 有漏洞的包。Semgrep Supply Chain 检查可达性:你的代码真的调用了那个有漏洞的函数吗?Snyk 会开 fix PR 并在新 CVE 击中你已经发版的代码时 alert。Checkmarx One 也能检测恶意包,比如 typosquat。
要求对 package.json 和 lockfiles 加 CODEOWNERS approval,这样每个新依赖都是人工决策。
DAST:运行中的应用
DAST 向已部署的应用发送真实请求,所以它能捕获代码扫描做不到的东西:代理留下的没有 auth 检查的端点、通过改 ID 读到另一个用户数据、缺失的安全响应头、暴露的 debug 页面。针对 preview 部署或 staging 运行:
docker run -t ghcr.io/zaproxy/zaproxy:stable zap-api-scan.py \
-t https://preview.example.com/openapi.json -f openapi
ZAP 是免费的。Nuclei 检查已知暴露。StackHawk、Snyk API & Web 和 Checkmarx DAST 是付费选项。给扫描器凭证和 API spec,否则它只会扫你的登录页面然后就没了。
DAST,我认为是锦上添花,不是必须有。Semgrep 的人也是这么想的。偶尔手动测试或渗透测试就足够了。
这是我从 Bob 大叔那里学到的。在 AI 出现之前我从来没想过。
CRAP 代表 Change Risk Anti-Patterns(变更风险反模式),是一种用于识别风险高、复杂、测试差的代码的指标。CRAP Index 衡量特定函数或方法的维护风险。
经验法则:分数高于 30 表示高风险、"CRAPpy" 的方法,需要关注或重构。
我找到了两个可以集成到流水线中的工具,能帮助控制 AI 生成的代码:
crap4ts 从复杂度和覆盖率计算 CRAP 分数。复杂代码配合弱测试得分很高。设一个上限,超过它的函数就让构建失败。
ArchUnitTS 让你把架构规则写成单元测试,如果你想把它们放在测试套件旁边的话。
有些规则没法写成模式:"这个名字和代码做的事匹配吗?"或者"这个抽象是否过早?"对于这些,我们可以用 CodeRabbit(或者很多其他 AI 代码审查工具)配合严格的配置文件,比如 .coderabbit.yaml:
重要的是 CodeRabbit 是最后一层,不是第一层。AI review 不够一致,所以任何能通过一个永远给出相同答案的工具来检查的东西,都应该用那种工具。CodeRabbit 处理需要判断的情况。
我们搭建的这些已经覆盖了大部分。以下还有一些工具可以帮助你更严格地控制仓库:
Knip 找到未使用的文件、导出和依赖。AI 会留下很多死代码。
Mutation testing(Bob 大叔也在关注,看起来他认为这是 CI/CD 检查的必要组成部分)(Stryker)故意改变你的代码并检查测试是否会失败。这是捕获"跑了代码但从不检查结果"的测试的最佳方式。
PR 大小限制:超过几百行的 PR 让构建失败或 warn。小的 PR 才能真正被 review。不过这个我不会做。
Pre-commit hooks:比如 Lefthook,或者最常见于 JS 项目的 husky。你可以配置它们运行任何东西,包括 Biome、Semgrep 和类型检查,在本地,所以你在 push 之前就能得到反馈。
每一层都遵循同样的思路:把每条规则移到能执行它的最可靠的工具上。
AI 想写多少代码就写多少。只要它通过和所有人一样的检查就行。这就是保持控制的方法。