你好,我是 Ryan,airCloset 的 CTO。
免责声明:本文中的 "cortex" 是 airCloset 内部自建 AI 平台的代号,与 Snowflake Cortex 或 Palo Alto Networks Cortex 等现有商业服务无关。
在第一部分(引言)中,我概述了顶层架构——AI 在 cortex 平台上同时驱动 PR 审查和事件响应。在第二部分(Product Graph)中,我深入讲解了 cpg——一个将代码、文档、数据库 schema 和基础设施融合为统一业务感知索引的知识图谱。
本文聚焦于自动化 PR 审查流水线——AI 审查 PR,另一个 AI 应用修复,一旦策略门禁通过,系统自动合并。AI 辅助开发常见的两大批评——"审查者成为瓶颈"和"AI 代码降低质量标准"——在这里并不适用。下文将详细解释原因。
| # | 主题 | 核心场景 | 文章 |
|---|---|---|---|
| 1 | 系列引言:cortex 安全笼头 | PR 无人值守自动合并 / 事件在有人注意到之前已被修复 | ai-harness-intro |
| 2 | Product Graph(cpg) | 代码 / 文档 / 数据库 / 基础设施统一为一张图 | cortex-product-graph |
| 3 | 自动 PR 审查 | webhook → AI 审查 → 自动修复 → squash 合并 | 本文 ← 当前所在 |
| 4 | 自愈 + 可观测性 + 自动添加护栏 | 告警 → AI 调查 → 修复 PR + 新的 lint/类型门禁 → 自动重新部署 + 同类写入自动拒绝 | 即将发布 |
| 5 | 将安全笼头从 cortex 扩展到面向用户服务 | 非工程师实际贡献 + 将 cortex 的安全笼头扩展到整个产品组织 | 即将发布 |
| 6 | 系列总结 | 底层哲学(放弃了什么、保留了什么、为什么这样设计)+ 失败教训与回顾 | 即将发布 |
769 个 PR 已合并。
中位合并时间:31 分钟。
每个 PR 的人工审查参与度:接近零。
这是 cortex 上一个典型 30 天(4 月 21 日 — 5 月 21 日)的表现。
这 769 个 PR 每一个都经过 AI 审查者作为第一道关卡,平均每个 PR 经历 10.8 轮审查-修复循环(最多 56 轮)。五分之一在 10 分钟内合并,约一半在 30 分钟内完成。现在人类做的是查看审查结果,调整审查提示词和审查规则本身——这是人在环上(human-on-the-loop),而非人在环中(human-in-the-loop)。人类操作在策略层,而非执行层。
| 过去 30 天 | |
|---|---|
| 合并 PR 数 | 769 |
| AI 审查覆盖率 | 100% |
| 平均审查迭代次数 / PR | 10.8 |
| 最大审查迭代次数 | 56 |
| 每 PR 人工审查 | ~0% |
| 中位合并时间 | 31 分钟 |
| 10 分钟内合并 | 20% |
| 30 分钟内合并 | 49% |
这是现在 cortex 上一个月的常态。
常见的论调——"AI 加速了编写,但审查仍然是瓶颈"和"AI 写的代码降低了质量"——cortex 通过一条让这两种失败模式都无法立足的流水线来消解。让我逐一拆解。
随着 AI 写代码越来越快,审查输出的人所承受的负载按比例增长。Anthropic 的内部博客(Anthropic 团队如何使用 Claude Code)报告了同样的模式——瓶颈已从编写转移到审查,高级工程师的工作从写代码转向整合和审查 AI 输出。
cortex 也曾面临同样的困境。当我们全速运行 Claude Code 时,编写速度提升了一个数量级甚至更多。与此同时,人类可用于阅读和批准 PR 的时间只能线性增长。如果审查者(也就是我)休一天假,整个组织就陷入停滞——典型的单点故障。
第一部分和第二部一直在问同一个核心问题:"这把安全笼头要推到多远?"cortex 选择了全力以赴:AI 写代码,AI 审查代码。人类紧握在手里的是"调整提示词和规则本身"——不是在每个独立的 PR 中做决策,而是从上方俯瞰系统并做出调整。
要让这套机制生效,必须满足三个条件:
一个通用的 AI 审查者只能看到 PR 的 diff。仅靠 diff 无法反映业务含义、上游/下游依赖关系和之前的事件历史。cortex 向 AI 审查者喂入第二部分介绍的 Product Graph(cpg)——一个将代码、文档、数据库 schema 和基础设施融合到统一结构中的知识图谱,每个节点都携带业务角色和上下游依赖关系——从而使它能够追踪到 PR 甚至没有触及的代码的影响。它能发现:
仅靠 diff 的 AI 审查永远无法达到这一层次。
如果审查标准每天变来变去,团队会感到困惑,AI 也无法知道"正确"长什么样。我们通过传递一份明确的审查指南文档作为每次审查的强制引用来源来强制执行这一点(我们开源了一个快照,见下文)。
把每个误报都当作 Critical 来处理会破坏工作流。我们通过层级化的严重性体系(Critical / Major / Minor / Nit)加上严格的不降级规则来控制这一点。
所以:第二部分的 cpg 解决了"AI 能看到什么上下文"的问题,审查指南作为导向器(Guides,执行前控制)解决了"AI 应该做什么"的问题,严重性分层加不降级规则作为传感器(Sensors,执行后控制)解决了"AI 不能做什么"的问题。这清晰地映射到 Martin Fowler 的 Guides / Sensors 分类体系(在第一部分中介绍过)。
再多一个上游层:在上述三个条件生效之前,一个每文件500行的 lint 规则确保每个 PR 中的每个文件都足够小,可以装进单个 AI 会话。仅此一项就能防止 AI 审查崩溃——而且与人类审查者不同,AI 不会分心。当然,在 AI 审查者前面还有很多其他 lint 规则,但全貌属于第四部分(自愈 + 可观测性 + 自动添加护栏)。
实现方案是在每位开发者的机器上运行一个脚本。GitHub webhook 发送到内部自建的 Event Relay 服务,持久化到 Firestore,每台开发机以 SSE 客户端身份订阅。重新连接时,Last-Event-ID 会重放任何遗漏的事件——零事件丢失,仅需一次 webhook 注册。审查者模式的机器保持常开,所以任何传入的审查请求会立即触发。作者模式在 PR 提交者自己的机器上后台运行,不影响正常开发工作。
当前的架构并非最初的设计。
smee.io 和 Cloudflare Tunnel 都遇到了连接断开和事件丢失的问题,这确实造成了一些实际遗漏。切换到内部 Event Relay 后,事件丢失降至零(Firestore 持久化 + Last-Event-ID 重放),而且这个 relay 变成了一个可以复用的通用基础设施层。
用于自愈(详见第四部分)的 webhook 接入实际上也走同一个 Event Relay。GitHub、Grafana 和其他 webhook 源通过同一个 relay 整合,每台机器的 SSE 客户端只订阅它关心的事件。拥有一个通用的 webhook 中继是那种能以意想不到的方式持续产生回报的基础设施——值得早期投入。
当审查者的机器收到事件后,脚本启动 claude -p 并按顺序遍历 9 个维度(Graph / Architecture / Security / Test / Doc / Impact / Observability / AI-Antipattern / Recurrence),然后读取 AI 在末尾输出的裁定标记,通过 gh pr review 发布 APPROVE 或 REQUEST_CHANGES。
几个要点:
--mode reviewer 启动就成为审查者进程;以 --mode author 启动就成为 PR 作者响应进程。被指定为审查者的机器运行审查者模式;提交 PR 的机器运行作者模式。Event Relay 多播事件,每台机器以分布式方式响应。origin/main 合并到一个全新的工作树中。多个 PR 可以并行处理,文件状态不会相互污染。claude -p 会话在保持上下文共享的同时遍历 9 个维度,这还能发现跨维度的矛盾。仅有指南只适用于数万行代码规模的项目。在 cortex 的规模上(超过 100 万行代码),第二部分的知识图谱(cpg)是硬性前提。没有 cpg,仅移植指南无法复现同样的审查质量——AI 审查者根本无法足够快速地导航代码库来进行影响推理。
我们最初尝试将 9 个维度拆给并行的子代理。出现了三个问题:cpg / 指南 / PR diff 被注入了 9 次(token 成本暴增),跨维度的发现无法相互引用(一个根源于 [Graph] 违规的 [Test] 问题在隔离环境中被遗漏了),而且将 9 个输出汇总为单个裁定还需要额外的机制。
单个串行会话解决了所有三个问题:只需加载一次 cpg/指南,前面的发现会保留在上下文中供后续维度使用(跨维度一致性自然得到保证),最后的一个裁定标记就是完整的汇总步骤。
我们还在启动时把 CLAUDE.md 切换为审查专用版本。默认的 CLAUDE.md 塞满了开发阶段的上下文(Product Graph 操作、生产数据安全、MCP 排序)——对审查者来说是噪音。审查专用版本聚焦于严重性、不降级规则和裁定标记规范,让 AI 的关注点集中在审查任务上。
削减多余的上下文同时提升了判断精度和降低了 token 成本。
我们在实际使用中应用的几个过滤器和开关:
每条自动审查评论的结构都是标签 + 严重性 + 具体示例。
| 标签 | 维度 | 主要检查目标 |
|---|---|---|
[Graph] | Product Graph 完整性 | @graph-* JSDoc、节点依赖、文档一致性 |
[Doc] | 文档一致性 | 代码变更后应跟随的文档更新、文档位置 |
[Impact] | 影响分析 | 遗漏的上游/下游修复、via: 字段不一致 |
[Security] | 安全 | 认证、输入验证、密钥 |
[Architecture] | 可组合架构 | app/package 边界、依赖方向 |
[Test] | 测试质量 | 覆盖率、匹配器、命名 |
[Observability] | 可观测性 | 结构化日志、不截断规则 |
[AI-Antipattern] | AI 生成代码陷阱 | 幻觉 API、过度使用回退、死代码 |
[Recurrence] | 复发预防 | Bug 修复分类(lint / 横向推广 / 新指南) |
| 严重性 | 判定标准 | 操作 |
|---|---|---|
| Critical | 安全、数据损坏、生产风险、文档不一致、缺少 @graph-*、质量标准降低 | REQUEST_CHANGES |
| Major | 规范违反、可组合架构违反、缺少测试 | REQUEST_CHANGES |
| Minor | 命名、可维护性、轻度重构 | REQUEST_CHANGES(必须解决) |
| Nit | 风格偏好、轻微不一致 | APPROVE(仅评论) |
最重要的规则是"不降级":
这明确写在 severity.md 中,AI 在发出 REQUEST_CHANGES 时会字面引用它。
真实的审查评论比抽象描述更有说服力。这里是一个典型案例。这是一个功能 PR(feat(meet): dual-write embeddings to new 'embedding' column (v2)),于 2026-05-19 合并,经历了 1.5 小时内的 6 轮审查-修复迭代。
首次审查(07:35:25):
双写方案、幂等迁移脚本和
Promise.all并行化看起来都没问题。请在合并前解决以下 3 个 Critical 和 2 个 Minor 发现。
Critical
[Graph] 缺少
@graph-business标签(x3)
graph-integrity.md要求每个在 app 层下的顶层声明都要有@graph-business(严重性:Critical)。下面的 3 个新声明都缺少这个标签(详细评论在独立的线程中)。
现有代码也缺少该标签,但"沿用现有模式"不是有效的降级理由(severity.md)。
generateEmbeddingV2(v2 embedding 生成函数)EMBEDDING_MODEL_V2(模型名称配置)EMBEDDING_LOCATION_V2(区域配置)
[Graph]
embedMeetContent的@graph-connects未反映generateEmbeddingV2
embedMeetContent的 JSDoc 中有@graph-connects generateEmbedding [calls] Generate embedding,但没有为新增的generateEmbeddingV2调用添加对应的@graph-connects行。
图谱将缺少通往generateEmbeddingV2的边。* @graph-connects generateEmbedding [calls] Generate embedding + * @graph-connects generateEmbeddingV2 [calls] v2 embedding generation (dual-write) * @graph-connects insertMeetChunks [calls] Insert chunks into BQ
[Doc] 对应的 BigQuery schema 文档未更新
相关文档中"BigQuery schema"部分缺少新的
embedding列。
graph-integrity.md和severity.md都将文档不一致定义为 Critical。| `created_at` | TIMESTAMP | Created at | +| `embedding` | FLOAT64[] | Embedding vector (v2: gemini-embedding-2) |
Minor
[Test]
textEmbeddingV2值未被断言
objectContaining允许额外字段,所以即使 v2 值从未被设置,测试仍然通过。textEmbedding: [0.1, 0.2, 0.3], + textEmbeddingV2: [0.1, 0.2, 0.3],[Test] 没有"v2 返回 null"的独立场景
generateEmbeddingV2: mockGenerateEmbedding复用了 v1 的 mock,所以"v2 返回 null 而 v1 成功"的情况没有得到独立验证。
<!-- VERDICT:REQUEST_CHANGES -->
关键在于细节的精确性:
graph-integrity.md / severity.md)被明确引用。<!-- VERDICT:REQUEST_CHANGES --> 是一个机器可读的裁定标记——推动 PR 进入 REQUEST_CHANGES 状态的触发器。此后,PR 作者(通常是另一台 AI,运行在作者机器上)推送修复,审查者重新审查。下一次审查确认所有 3 个 Critical 确实被解决,提出下一个 Major / Critical,依此类推。1.5 小时内 6 轮迭代,最终 APPROVE,自动合并。
在时间线上是这样的:
如果由人类审查者来做,这将变成"Critical x3 → 等到明天修复 → 后天重新审查"——每个 PR 要 2 到 3 天。cortex 在 90 分钟内完成。
人工审查和自动审查之间的差别不仅仅是速度。单个 AI 会话按顺序遍历所有 9 个维度,每次都引用指南,这使得它更难遗漏人类因注意力分散而放过的"深层"发现——文档一致性、复发预防判断、弱的匹配器。前后对比:
这就是审查瓶颈在这里从未形成的原因。
我一直在提及的审查指南并非静态文档。在生产环境中运行这套系统暴露出了一些重复模式——AI 在特定类别的问题上判断失误。每次出现这种情况,我们不是在单个 PR 上加评论;而是重写指南,让 AI 下次能正确行事——这才是人类真正操作的元层面。
以下是我们实际遇到的一些失败案例,以及我们如何通过修改规则(而非修改 PR)来堵住每个漏洞。
早期阶段,AI 在标记违规之后会立即添加"然而,由于现有代码也有同样的违规,我将此降级为 Nit"并自我降级。结果是:新增代码上的违规不断降为 Nit,系统持续发出 Approve。
我们通过在 severity.md 中添加不降级规则堵住了这个漏洞:
"沿用现有模式"不是有效的降级理由:如果现有代码违反了指南,遵循该模式的新代码仍然会以相同的严重性被标记。"考虑在下次重构时处理"之类的延期理由不被接受。
仅此一条还不够。随着时间的推移,其他借口模式也浮现了出来——"会在单独的 PR 中处理"、"会在下一次会话中处理"、"超出范围"、"逐步来"——所以我们也把这些加入了禁止降级类别。我们还明确禁止了通过在代码中留下 TODO/FIXME 来延期。核心理念是:预先堵死每一个典型的借口路径。
最初,每次审查末尾的最终裁定是 APPROVE / REQUEST_CHANGES / COMMENT(批准 / 请求修改 / 仅评论)。当 AI 选择了 COMMENT——例如当只有 Minor 级别的问题时——脚本不采取任何行动,PR 永远处于审查待定状态,最终需要有人手动接管。典型的反模式,而且这种情况反复出现。
我们将裁定合并为 2 个选项。任何 Minor 级别及以上都是 REQUEST_CHANGES;缺失裁定标记时默认为 REQUEST_CHANGES(安全侧);只有 Nit 级别或没有发现(且 CI 通过)才产生 APPROVE。原则是:"如果判断模棱两可,以安全为默认方向,导向阻塞侧(REQUEST_CHANGES)。"完全贯彻这一设计彻底消除了 PR 卡死的类别。
最初,每条指南(graph-integrity.md、testing.md 等)只是一个带圆点的检查清单。像"测试名称是否有描述性?"或"mocks 是否最小化?"这样的条目都被列出,但没有每条的严重性。结果是,同样的违规可能在一个 PR 中被判为 Major,在另一个 PR 中被判为 Nit,完全取决于会话。
我们将每条指南的检查清单都转换成了 severity / scope / criterion 表:
| Severity | Scope | Criterion |
|---|---|---|
| Critical | 所有 PR | 缺少 @graph-business |
| Major | 仅 app 层 | 缺少测试 |
| Minor | 仅共享包 | 超过 3 个函数参数 |
| Nit | 所有 PR | 命名不一致 |
scope 列是一个机器可判定的过滤器,用于确定某条检查适用于哪些路径——这样 AI 审查者就不会在超出范围的 PR 上触发不相关的项目。仅仅是将它放入表格——判断的可复现性就显著提升了。
运行一段时间后,我们注意到 AI 生成的代码有自己的一类反模式——调用不存在的 API(幻觉 API——比如 user.findOrCreate() 看起来合理但实际上未定义)、吞掉错误并返回回退值(例如在上游 API 失败时静默返回空数组)、留下未使用的函数(重构新增了函数但没有删除旧的,留下死代码)、将修改范围扩大到要求之外(你让它改一个函数,它格式化了整个文件)、添加不必要的向后兼容代码(为内部函数创建一个已废弃的别名)——而 security.md / testing.md 无法捕获这些。存在一类"只有 AI 会犯的错误"。
我们为此添加了一个专门的 ai-antipattern.md。现在审查在 [AI-Antipattern] 标签下明确捕捉这些问题。审查 AI 输出需要围绕 AI 特有的陷阱来设计——仅仅把人类审查的经验移植到 AI 上是走不到这一步的。
最后一个也是最重要的模式。当 AI 编写修复 PR 时,偶尔它不是修复指南违规,而是编写一个放松指南的 PR。例如:
而 AI 构建了一个形式上自洽的理由:"现有代码已经违反了这条规则,所以我们调整标准以匹配实现吧。"如果不加约束,AI 会逐步把质量标准向下拉。
我们通过在 severity.md 中将"质量标准放松"列为 Critical 来堵住这个漏洞:
一个放松质量标准的 PR——指南文档、lint 规则、覆盖率阈值——不能由 AI 审查者批准。它被以
REQUEST_CHANGES退回。需要人类审查者的批准。"现有代码已经违反了这个标准"不是放松的有效理由。
这是一个明确的边界——我们刻意不让 AI 拥有自动批准的权限。标准本身是否改变,是人类的决定。这是"AI 审查 AI"架构的元级安全阀。
共同的主线是:"当 AI 出错时,不要覆盖单个 PR——重写指南,让修复向前传播。"
只要这个循环在转动,指南就是一个吸收 AI 在生产中产生的失败模式的活文档。不要试图一开始就写出完美的指南。捕捉 AI 出错的时刻,然后为那个时刻编写规则。这才是"即使人类不在循环中,质量也不会下降"背后的实际机制。
还有一条线索。目前,"AI 出错了,该重写指南了"这个触发器主要还是人类的判断,但这部分维护工作也在逐渐变得可自动化。自愈(下次的第四部分)——AI 调查生产事故、打开修复 PR、通过自动审查、自动重新部署——要求每个修复 PR 在 [Recurrence] 视角下编写 {添加 lint / 添加指南 / 横向推广} 三者之一。所以 AI 正在越来越多地参与维护自己的审查标准,人类仍然在采纳环节保持在循环中。我将在第四部分回到这个话题。
一旦 REQUEST_CHANGES 落地,运行在 PR 作者机器上的同一个脚本,但处于作者模式,接收事件并开始工作。
[REQUEST_CHANGES 检测到]
| 通过 Event Relay 推送 SSE
[作者模式在 PR 作者机器上启动]
| 将 origin/main 合并到工作树
| (锁定文件预先解析,剩余冲突由 AI 处理)
| 读取 auto-review 评论作为上下文
| 在工作树内运行 claude -p
| 提交 + 推送修改
| 新的 SHA 通过 Event Relay 传回审查者机器 → 重新审查
这里有两个关键设计选择。
TODO/FIXME 或将工作拆分到单独的 PR 来逃避,下一次审查会拒绝它。一旦 auto-review 返回 APPROVE 且 CI 全部通过,auto-merge 脚本运行并 squash-merge 该 PR。
[Auto review APPROVE + CI 通过]
|
auto-merge 脚本
| squash merge 到 main
|
[main 已更新]
|
Turborepo 构建(仅受影响的包)
|
Pulumi up(多个堆栈并行)
|- API 服务
|- pipeline 服务
|- MCP 服务器
`- 基础设施
|
[部署完成]
|
cpg 索引重建(仅重新生成已变更节点的 embedding——见第二部分)
pulumi up <stack1> <stack2> ... 并行运行,因此同时部署 9 个堆栈大约在 8-12 分钟内完成。端到端,从合并到生产平均需要 10-15 分钟。
这与自愈 PR 形成了良好的叠加效应。事故告警 → 自愈识别根因 → 创建修复 PR → 自动审查通过 → 自动合并 → 自动部署作为一个闭环运行,无需人类参与(详见第四部分)。
进一步拆解一下标题中的数字。
在 30 天内的 769 个 PR 中,每个 PR 平均有 10.8 轮审查迭代,最大 56 轮。平均值超过 10 意味着第一次审查几乎总是会至少发现一个问题。
前面展示的 embedding 模型迁移 PR 需要 6 轮迭代才能合并,这代表了平均 PR 的水平。人类审查者需要数天的事情,cortex 在几分钟内解决。
第一次审查中最常见的发现:
@graph-business——cpg 依赖的前提条件(来自第二部分)。新增声明时的经典发现。docs/ 部分未更新。objectContaining 削弱了值断言,通过 toBe 进行单属性检查。event 字段或必需键偏离了结构化日志规范。这些是人类审查者在实践中经常遗漏的类别,尤其是文档一致性和复发预防检查。AI 审查者在每个 PR 上机械地应用它们。
它不为零。每个月有几次会出现"这是 Nit 而不是 Major"类型的误判。修复路径就是前面描述的那条——不是在单个 PR 上加评论,而是修改指南,修正所有后续审查的判断。
在过去的六个月里,cortex 上工程师的角色从"写作者"和"审查者"转变为"操作者"——运行系统的人类,而非参与每个具体决策的人。
留在人类手中的是:"到底要构建什么(产品 / 需求)"、"这个方向真的对吗(架构判断)"、"添加哪条指南及放在哪里",以及"查看审查结果并相应调整提示词和指南"。高抽象层次的工作——不是单个决策,而是从上方观察整个系统并进行引导。可以说,从人在循环中(human-in-the-loop)转变为人监督循环(human-on-the-loop)。
被广泛报道的现象——"AI 降低了质量"、"审查者成了瓶颈"——发生在仅在写作者一侧扩展护栏,而将审查者一侧留给人类的情况下。如果写作速度提升了而审查没有,瓶颈自然会出现。问题自然会被遗漏。
cortex 则相反。我们在全面扩展写作者侧之前,先扩展了审查者侧的护栏。Anthropic 观察到瓶颈从写作转向审查的判断完全正确——而这正是为什么"把关审查者的角色也交给 AI"是 cortex 选择的答案。
"AI 编写代码,AI 审查代码。"这是 cortex 自动审查流水线的核心。质量下降和审查瓶颈是你将护栏扩展到什么程度的函数——它们不是 AI 辅助开发的固有属性。
第四部分预告:自愈 + 可观测性 + 自动添加护栏——一条流水线,生产告警(通过 OTel/Loki/Mimir/Tempo/Faro 观测)触发 AI 调查,AI 编写修复 PR 并添加新的 lint/类型护栏,然后自动审查、自动合并、自动重新部署。修复和复发预防护栏同时落地,因此同一类事故在结构上无法再次发生。如果自动审查在 PR 时保护质量,第四部分在生产时保护质量,同时增长质量门本身。
上面的标题数字包含了自愈 PR(AI 调查、修复和自动部署的生产告警)。对于某些类型的事故,修复在任何人反应过来之前就已经合并了——这就是 cortex 今天的状态。下次再见。
——
一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。