Monorepo 里跑 AI 代码审查,误报太多?我们靠一套自定义规则集把噪音降下来了
真正能把 AI 代码审查的误报率打下来的,不是换模型,而是把规则集当成工程问题来维护。我们团队在 26 个包的 pnpm monorepo 里跑了一个月,把 AI 审查的噪音从「每条 PR 平均 14 条无效评论」降到 3 条以内,靠的是一套分层规则集 + 包级元数据过滤,而不是反复调 prompt。
先把「误报」拆成三类,再决定规则怎么写
直接说结论:monorepo 里 AI 审查误报高的根本原因,是同一个模型在面对不同包时缺少上下文边界。前端 React 包里的 useEffect 依赖数组写法和后端 Node 包里的流处理错误处理逻辑,在同一套通用 prompt 下会被同等对待,模型只能靠猜。
我们把误报拆成三类,每类对应不同的规则策略:
类型 A:跨栈误报。模型把 A 栈的 best practice 套到 B 栈上。比如对 NestJS 服务里的 any 类型提出「应使用泛型约束」,但这在 DTO 转换层是刻意为之。这类误报占了总量的 40% 左右。
类型 B:测试/夹具误报。模型对 *.test.ts、*.spec.ts、fixtures/ 下的代码套用生产级标准。比如在 mock 数据里警告「硬编码密钥」,但那是 test-secret-123 这种显式占位符。这类误报约占 30%。
类型 C:monorepo 特有误报。模型不理解包之间的依赖边界,建议在 package A 里直接 import package B 的内部路径,或者把本该放在 shared 包里的类型定义内联到某个 consumer 里。这类约占 20%。
剩下 10% 才是真正需要模型介入的语义级问题。想清楚了这三类,规则集的骨架就出来了:不是让模型「更聪明」,而是提前替它排除掉它不该碰的代码和不该提的建议。
规则集架构:三层过滤,而不是一个超长 prompt
我们的做法是把规则集拆成三层,分别解决「看哪里」「不看哪里」「怎么看」三个问题。这套架构跑在 CodeRabbit 上(版本 0.18+,支持 coderabbit.yaml 的 reviews.profile 配置),但思路对任何支持自定义规则的 AI 审查工具都适用。
第一层:路径级排除(path_instructions)
这一层解决类型 B 误报。直接告诉工具哪些路径要么跳过,要么用极简规则。我们当前的 coderabbit.yaml 里关键配置如下:
# coderabbit.yaml
reviews:
path_filters:
- "!**/*.test.ts"
- "!**/*.spec.ts"
- "!**/fixtures/**"
- "!**/__mocks__/**"
- "!**/*.snap"
path_instructions:
- path: "packages/**/src/**/*.ts"
instructions: |
Review for production-grade TypeScript code quality.
Focus on: type safety, error handling, async patterns, API contract violations.
Do NOT comment on: naming style, import ordering, test coverage.
- path: "packages/**/test/**/*.ts"
instructions: |
Review ONLY for test correctness and flakiness risks.
Do NOT apply production code standards. Mock data and hardcoded test values are expected.
这里有个容易踩的坑:path_filters 和 path_instructions 的匹配顺序。CodeRabbit 的 path_filters 是排除优先的,但如果你在 path_instructions 里写了更宽泛的 glob 模式,后写的规则会覆盖先写的。我们一开始把测试文件的 instructions 放在前面,结果 path_filters 里的排除模式没生效,后来查了文档才确认 path_instructions 的匹配优先级高于 path_filters。所以现在我们的 path_filters 只做硬排除,path_instructions 只做软指导,两者路径不重叠。
第二层:包级元数据过滤(knowledge_base + custom rules)
这是针对类型 A 和类型 C 的核心层。Monorepo 里每个包的技术栈和边界是不同的,所以我们在每个包的根目录放了一个 .ai-rules/ 目录,里面是包级别的上下文文件,通过 knowledge_base 配置注入:
# coderabbit.yaml
reviews:
knowledge_base:
- path: "packages/**/.ai-rules/**/*.md"
type: "local"
每个包的 .ai-rules/ 下有两个文件:
stack-context.md — 声明这个包的技术栈、框架版本、刻意的架构决策。比如 packages/api-gateway/.ai-rules/stack-context.md 的内容:
# API Gateway Stack Context
- Framework: NestJS 10.x, strictly typed with TypeScript 5.4+
- This package uses class-validator + class-transformer for DTO validation.
- Explicit `any` in DTO transformation layers is a DELIBERATE pattern
when handling unknown upstream payloads. Do not flag `any` usage in
`*.dto.ts` or `*.transformer.ts` files.
- Error handling follows a Result<T, E> pattern from `@shared/result`.
Do not suggest try/catch blocks in service layer.
dependency-boundaries.md — 声明这个包允许依赖哪些内部包,以及不允许的引用方向:
# Dependency Boundaries
ALLOWED internal dependencies:
- @shared/types (types only)
- @shared/result (runtime)
- @shared/logger (runtime)
FORBIDDEN:
- Importing from any package's `src/` internal path (e.g. `@shared/types/src/...`)
- Circular dependency with @worker-queue
- Direct import from packages not listed in ALLOWED
这两份文件加起来不到 40 行,但对误报的压制效果远超我们在 prompt 里反复强调「注意 monorepo 上下文」。原因是模型在审查具体文件时,这些上下文是作为「已知事实」注入的,而不是作为「建议」让模型自行判断。
第三层:审查 profile 里的语义规则
前两层解决的是「看哪里」和「不看哪里」,第三层解决「怎么看」。我们定义了两个 profile,一个给生产代码,一个给非生产代码:
# coderabbit.yaml
reviews:
profiles:
- name: "production"
tone_instructions: |
Only report issues that have a high probability of causing:
- runtime errors
- API contract violations
- data loss or corruption
- security vulnerabilities (OWASP Top 10)
Do NOT report style, naming, or micro-optimization issues.
For each finding, you MUST include a concrete failing scenario.
review_level: "high"
- name: "non-production"
tone_instructions: |
Only report issues that would cause tests to fail or be flaky.
Ignore all code quality concerns.
review_level: "low"
review_level: "high" 在 CodeRabbit 里对应的是「只做语义级分析,跳过风格和最佳实践建议」。这个配置项在 0.18 版本之前叫 review_effort,我们升级后花了半天排查为什么配置不生效,最后发现是字段名变了。如果你也在用 CodeRabbit,先确认一下版本对应的字段名。
规则集维护:把误报当成 bug 来修
规则集不是写一次就完事的。我们的做法是每周末花 20 分钟过一遍当周的 AI 评论,把仍然出现的误报分类,然后决定是修规则还是补上下文。
具体操作是:在 GitHub 上过滤出 CodeRabbit 的评论(author:app/coderabbit),然后手动标记 false-positive 或 useful。我们做了一个简单的统计表:
| 周次 | AI 总评论数 | 有效评论 | 误报 | 误报率 | 主要误报类型 |
|---|---|---|---|---|---|
| W1 | 87 | 23 | 64 | 73.6% | A 类 45%, B 类 32% |
| W2 | 52 | 29 | 23 | 44.2% | A 类 38%, C 类 28% |
| W3 | 41 | 31 | 10 | 24.4% | C 类 40% |
| W4 | 35 | 28 | 7 | 20.0% | 语义级残留 |
W1 到 W2 的断崖式下降来自路径排除和 stack-context 的上线。W2 到 W3 的下降来自 dependency-boundaries 的补充。W4 剩下的 20% 误报里,大部分是模型在复杂泛型推导上的真·能力边界,规则集层面已经很难再压了。
这里有个反直觉的发现:误报率下降的同时,有效评论数量反而上升了。W1 时模型被大量噪音分散了「注意力」,每条 PR 平均只抓到 3.8 个真问题;W4 时抓到 4.7 个。这说明规则集的作用不只是过滤噪音,还在帮模型聚焦。
具体到 prompt 层面:少写「不要」,多写「如果……那么……」
如果你不想用 CodeRabbit,而是自己调 API 或基于其他工具做二次开发,规则设计的原则是一样的。我们最初把 prompt 写成了「禁止事项清单」:不要建议命名、不要建议重构、不要评论测试文件……效果很差。模型对否定指令的遵循度远低于肯定指令。
后来改成了条件式指令,效果立竿见影:
If the file is under packages/*/test/ or packages/*/fixtures/, then ONLY report:
- test assertions that will never fail
- async operations without await that cause race conditions
- missing cleanup that causes cross-test contamination
If the file contains a DTO class decorated with @IsOptional(), then treat
`any` type on that property as intentional. Do not suggest generics.
条件式指令让模型有了明确的「触发条件」,而不是让它自己去判断「哪些建议算过度」。这个改动的效果比我们换模型(从 GPT-4o 换到 Claude Sonnet 4)带来的误报下降还要明显。
常见问题
Q:这套规则集能不能直接套用到我的 monorepo?
不能直接照搬,但架构可以复用。三层过滤的思路(路径排除 → 包级上下文 → 语义 profile)是通用的,但具体的规则内容必须基于你自己仓库里的误报数据来写。建议先跑两周不加规则的原始数据,统计误报类型分布,再针对 top 2 类型写规则。我们 W1 的 73.6% 误报率就是原始基线,没有这个数据就无从下手。
Q:为什么不用更长的 prompt 把所有规则都写进去?
因为长 prompt 的边际收益递减非常快。我们试过把规则集写成一个 800 行的 GLOBAL_RULES.md 通过 system prompt 注入,结果模型开始「选择性失忆」,对 prompt 后半部分规则的遵循度明显下降。拆成按路径和包维度注入的小块上下文后,模型在具体文件上看到的规则不超过 60 行,遵循度反而高得多。
Q:这套方案对非 TypeScript 的 monorepo 也有效吗?
有效。路径排除和包级上下文是语言无关的。但类型系统相关的误报(比如 any 的定义、泛型推导)在动态语言里会变成另一类问题,比如 Python 里的类型注解缺失、Ruby 里的 nil 处理。规则集的设计逻辑不变,但具体的 stack-context 内容需要针对语言特性调整。我们另一个 Python 服务的 monorepo 用同样的架构,四周后误报率从 68% 降到了 17%。
Q:CodeRabbit 以外的 AI 审查工具怎么做包级上下文注入?
大多数工具支持自定义指令或上下文文件。GitHub Copilot Code Review 目前不支持 per-path 的规则注入,这是它的硬伤,在 monorepo 场景下基本不可用。Graphite Reviewer 支持 per-directory 的规则文件,但匹配逻辑不如 CodeRabbit 灵活。如果你用的是自建方案(比如 GitHub Action + LLM API),直接把 stack-context.md 和 dependency-boundaries.md 的内容拼接进每次审查请求的 system prompt 即可,注意控制总 token 量在 2k 以内,超出后模型遵循度会下降。