前言
我这两年最上头的自动化尝试,就是把 AI 塞进了 CI/CD 流水线,专门处理代码审查和安全扫描。最开始是被逼出来的:团队三个人轮流当 Review 负责人,PR 堆积比冬天的雪还厚,安全扫描工具每周生成上百条告警,其中一半连安全专家看了都摇头。那个流程表面上有质量门禁、有定时扫描,实际上是研发团队最不想碰的一块烫手山芋。
直到我陆续把两套 AI 能力接进去——一套做代码审查,一套做安全扫描研判——事情才开始变得像话。这篇文章我不谈概念,就讲这一路怎么选型、怎么设计 Pipeline、怎么设置质量门槛、怎么控制成本,以及最关键的:AI 到底有没有把研发从噩梦里解救出来,还是只是换了一种更贵的方式继续做噩梦。所有数据都来自我们内部的模拟项目,不带广告,纯经验分享。
1. 为什么说代码审查和安全扫描曾是研发的“噩梦”:人肉 Review 的排队与安全告警的“狼来了”
在谈 AI 之前,得先把原来的痛苦说清楚。这不是背景铺垫,而是理解后面所有设计决策的关键。只有知道痛在哪,才能明白为什么我用的是这种而不是那种接入方式。
1.1 代码审查不是不想审,是真的排不上队
代码审查最大的痛点从来不是“不会审”或者“审得不够仔细”,而是人手不够。每次打开 Pull Request 列表,总有一批改了两三天的分支还在等待 Review。有人会说你们不设 Deadline 吗?设了,但 Deadline 的代价是 reviewer 在自己的功能需求和审查他人代码之间反复横跳,最后只能压缩审查时间,草草看一遍 diff 就跑。
现实中的 Review 还有一个很隐蔽的问题:context switch 成本。一个负责用户中心模块的开发者,被拉去审查支付网关的改动,他首先要花很长时间理解业务背景。这个过程让人本能的抗拒,于是审查请求一拖再拖。我观察我们团队的 PR 平均等待时间,最夸张的时候到了两个工作日。这直接导致项目节奏出现奇怪的“冲刺式合入”:平时没人审,积压到周五下午大家一起盯着屏幕快速点 Approve。
更麻烦的是人的情绪因素。面对面沟通尚且会因为面子而少提意见,代码审查里更容易这样——reviewer 潜意识里不想在一次 Review 里同时提出 20 条负面评价,哪怕这些问题确实存在。于是大部分 PR 的评论集中在格式、命名这类不痛不痒的细节上,真正的结构性风险和逻辑漏洞反而被放过去了。代码审查制度在最忙的时候,变成了一个纯粹的形式主义流程。
1.2 安全扫描:告警越多,越没人信
另一个噩梦是安全扫描。传统的静态分析工具(SAST)和依赖扫描工具在流水线里跑了很久,产出的是几十甚至上百条告警。研发人员打开报告的第一反应早就不是“认真看”,而是“又来了,这工具今天想让我修什么”。
我并不是想否定这些工具的价值,它们对已知漏洞模式的匹配其实很准。问题在于它们不理解业务上下文。举个例子,项目里有一处把用户输入拼进 SQL 的代码,SAST 立刻标红“SQL 注入”,但实际这段代码前一行用了参数化查询的封装函数,根本不存在注入风险。工具不管,它只会告诉你这里有字符串拼接。研发自己确认后认为是误报,但这个过程每天要重复好几次,渐渐的就没人信这些告警了。
这是典型的“狼来了”效应。一个报告系统如果 200 条告警里只有 3 条真问题,那效果跟没有系统是一样的。真正恶劣的是,那 3 条真问题反而因为太吵而被漏掉。我记得我们有次线上事故,最后定位出来是 SSRF,事后翻报告,发现这条告警在两周前就已经被工具标出来了,但当时正好赶上扫描器改版后大批量误报,没人去翻那几百条告警。
1.3 这里面的深层矛盾:速度、质量与安全很难同时满足
如果只说“工具不行所以团队很痛苦”,这还不够完整。更深层的原因在于 DevOps 的节奏和安全性措施天然存在冲突。研发团队的目标是快速交付,安全团队的目标是别出事。CI/CD 就像一个十字路口,一个方向催着你绿灯通行,另一个方向要求你每条路都仔细检查。传统工具只会死板地亮红灯,而不会区分什么是“必须停下的红灯”和“只是看起来像红灯但其实不用理会的警示灯”。
AI 真正有机会改变的不是去生成更多告警,而是当那个能区分“真红灯”和“假红灯”的过滤网。它可以把安全扫描从“告警轰炸”变成“精准提醒”,把代码审查从“人肉排队”变成“AI 初筛 + 人工终审”。明白了这个深层次矛盾,后面的所有方案设计都会围绕一个目标:让 AI 帮助研发把注意力集中到真正重要的事情上。
2. AI 进场的正确姿势:从“人肉 Review”到“人机联合审查”
2.1 模型选型:别拿大炮打蚊子,也别用苍蝇拍打大象
首先要回答的问题是用什么模型。市面上可选的有商用大模型、开源代码模型、甚至自己微调的小参数模型。我们一开始走了弯路,以为最贵的大模型效果最好,直接把长上下文模型接到流水线里跑每条 PR,结果两周后账单出来差点没绷住。一条普通 PR 的 diff 可能只有几百行,但加上必要的上下文(函数定义、相关调用、项目规范),每次请求消耗好几万 token。如果是 1000 条 PR 一个月,成本非常可观。
经过几轮试验,我摸索出一套“分级选模型”的用法:
| 任务类型 | 模型级别 | 原因 |
|---|---|---|
| 变更描述摘要、PR 分类 | 轻量开源模型 | 只做提取和归类,不需要深度推理 |
| 常规代码审查、风格与规范检查 | 中型代码专用模型 | 对 diff 级代码理解好,成本可控 |
| 深度逻辑漏洞、安全漏洞研判 | 长上下文大模型 | 需要跨函数、跨文件的推理,复杂度高 |
很多人容易忽略的一点:代码审查并不需要每一条都让对方看“整棵项目树”,大部分 PR 的 diff 只有几百行,模型只需要看到这个 diff 以及相关函数的一部分上下文就足够了。只有在涉及跨模块交互、数据流走向的时候,才值得动用更大的模型。我们把管线做成两段式:轻量模型先做分类,看看这个 PR 是新增功能、重构、还是修复;如果分类结果是重构或修复,再调用大模型做深度分析。这样算下来,真正跑到大模型的 PR 只有 20% 左右,成本直接砍掉一半多。
2.2 审查管线怎么搭:从 diff 解析到带定位的评论生成
技术实现上,核心是四个步骤:提取 diff、收集上下文、生成审查意见、格式化评论。
提取 diff 没什么好说的,就是调代码平台的 API 把 PR 的改动部分拉下来。收集上下文这步最花心思:我们需要把 diff 里涉及到的函数定义、函数调用链、相关数据结构定义都从仓库里捞出来,一并塞进 Prompt。这步做好了,模型才能理解“这个改动会不会影响调用方的行为”。如果只给一段孤零零的 diff,模型确实只能做表面检查。
我试验过的最小可用 Prompt 长这样,大家可以直接抄作业:
你是一位经验丰富的代码审查者。请审查以下 Pull Request 的改动,重点关注: 1. 逻辑正确性问题 2. 潜在的边界条件 3. 安全漏洞(注入、越权、路径遍历等) 4. 与现有代码风格的一致性 改动内容如下: {diff_context} 请按以下格式输出: - 严重程度(critical/warning/suggestion) - 涉及文件与行号 - 问题描述 - 修改建议模型输出后,我们会做一个格式化处理,把“严重程度”映射成流水线后续的判断依据,把定位行号转成评论位置,最后通过 API 把评论贴到对应代码行的评论区。看起来挺简单,但实际跑起来有个细节很容易踩坑:模型输出的“行号”经常对不上原始 diff 的行号,尤其是模型在回答里自己“脑补”了几行修改建议后,它输出的行号已经偏移了。我们需要用模糊匹配来定位,比如用改动的代码片段去 diff 原文件查找位置,而不是完全信任模型给的数字。
2.3 实测效果:PR 覆盖率与“值得读”的评论
跑了一个月后,我们统计了模拟项目 A 的数据:总共 342 条 PR,AI 在 98% 的 PR 上都产出了至少一条评论。然后我们让团队开发者给这些评论打标签,分为“有价值的问题”“可接受的建议”“完全无关或错误的评论”三类。结果大概是这样:
| 评论类型 | 占比 | 说明 |
|---|---|---|
| 有价值的问题 | 41% | 逻辑问题、并发问题、资源泄漏等 |
| 可接受的建议 | 36% | 命名、抽象、结构优化 |
| 无关或错误评论 | 23% | 幻觉、未理解业务导致的错误判断 |
41% 听起来不算高,但注意这是 AI 生成的评论,在没有任何人工干预的情况下,这个准确率已经足够当第一道筛子。真正让开发者感到舒服的是,Review 等待时间从最长两个工作日降到了三小时以内。因为 AI 先审完一轮,经验丰富的老开发者只需要看 AI 已经过滤掉的范围内,再定睛看那两三条关键问题。团队里最明显的转变是,以前大家合入代码之前心里老悬着,现在知道有一条自动的“第一道防线”在兜底,大部分低级错误在提交时就闪了红灯。
3. 安全扫描的智能化转型:从“告警轰炸”到“可执行的优先级”
3.1 传统 SAST 工具的先天局限:它只看得见“语法”,看不见“意图”
安全扫描这块,我们原本用的是业界主流的 SAST 工具加依赖扫描。它们的工作机制本质上基于规则,官方维护一批漏洞 Pattern,然后用模式匹配去扫代码。这种机制决定了它只能做“有没有发生某种模式”的判断,做不了“这种模式在当前业务里是不是真的有问题”的判断。
举一个印象很深的例子。我们一个内部管理后台,有一段根据用户传入的文件名读取文件内容的代码。SAST 立刻报了一个“任意文件读取”。工具只看代码,不知道这个接口部署在内网且前方还有一层白名单校验。研发同事查到告警后,要花时间去看调用链,发现上层确实有校验,于是标记为误报。每个告警都走一遍这种确认流程,谁受得了?但你要是不确认,告警就永远挂在那里,后面再有大扫的时候,报告越来越长,真正的高危项就埋在一堆误报里。
跳过误报的问题,还有一个更实际的麻烦:告警缺少优先级。SAST 会给一个严重等级,比如 Critical 或 High,但这个等级是基于漏洞类型本身的通用评级,完全没有考虑代码是否真正可达、是否有实际攻击路径。研发团队面对 70 条 High,不知道先修哪个。这时候再去谈“合规要求所有 High 必须修完”就变得特别滑稽——逼着大家把精力花在并不紧急的问题上。
3.2 AI 怎么研判:让模型看 diff 和历史代码,把“告警”变成“结论”
我们的做法是把 AI 作为 SAST 告警的“二审法官”。具体流程是:流水线跑完传统扫描后,把所有告警连同对应的代码片段、函数调用栈、依赖信息打包发给 AI,让 AI 对每一条告警做研判,输出三个东西:是否疑似真实漏洞、影响面评估、修复建议。
这里最关键的是,我们给模型的 Prompt 里特别强调了几件事:
你是安全研判助手。以下是一条来自 SAST 工具的告警,请判断该告警在当前项目中是否可能真实可利用。 需要结合以下信息: 1. 被标记的代码片段 2. 该代码的调用栈信息 3. 数据流是否经过用户可控输入 4. 是否已有防御缓解措施(如白名单、输入校验、参数化查询) 输出格式: - 结论(confirmed/suspected/false_positive) - 利用条件 - 建议处置方式这个 Prompt 之所以有效,是因为我们把 SAST 的原始告警和项目实际上下文捆绑在一起,强制模型去比对。比如“任意文件读取”那条,模型看到调用链前有路径白名单检查,就会倾向于判定为 false_positive,并解释理由。如果它发现一个接口虽然标记为低危,但数据流直接来自前端请求体,并且没有任何校验,模型会给出 suspected,提醒人工复核。
效果让团队里的人都很意外。我们对过去三个月的历史告警做了一次回测,选取了六个月前攒下的 1260 条告警,让 AI 重新研判了一遍。金标准是当时人工逐个确认后的结果(虽然那个过程非常累)。AI 对“是否存在真实风险”这个二分类问题,准确率达到了 87%,召回率 92%。最实用的是,AI 把原本 1260 条告警压缩成了 37 条“建议人工优先复核”的项,团队终于可以坐下来认真看这 37 条,而不是在 1260 条里大海捞针。
3.3 一个典型研判案例:被 AI 拉下神坛的“高危 SSRF”
选一个印象最深的案例来讲。某次依赖升级后,扫描器报了一处 SSRF,标记为高危。搁以前,这又是一个要拉上全组讨论半小时的“灵异事件”。但这次 AI 的研判意见是这样的:代码确实会把用户输入的 URL 传给后端请求发起函数,看起来像 SSRF;但继续往下追踪数据流,发现这个 URL 被强制传给一个内部代理服务,该代理专门处理域名白名单,不在白名单里的域名直接拒绝解析。代理层是内网基础设施,不存在绕过风险,所以 AI 给出的结论是 false_positive,置信度 93%,建议只需在代码层加一条注释,避免未来重构时破坏代理约束。
安全负责人看了之后觉得很合理。这个判断过程其实难度不大,任何一个有经验的安全工程师都能推理出来,但问题是谁有动力每天读一千条告警然后逐条推数据流?AI 的价值不是替代专家判断,而是把专家从流水线上解放出来,只处理真正需要判断智慧的少量问题。这才是安全扫描这事从“噩梦”变成“日常工作”的本质转变:告警数量从无序轰炸变成了精准投递。
4. 把 AI 塞进 Pipeline 的落地细节:接入点、阈值、缓存与成本控制
4.1 接入点怎么选:不是每个环节都要跑一遍 AI
AI 接入 CI/CD 有一个常见误区:觉得 AI 应该出现在所有阶段,越早越好。实际上不是这样。我们在实践里把接入点分成四类,各有分工:
| 接入点 | 触发时机 | 处理任务 | 是否建议 |
|---|---|---|---|
| Pre-commit 钩子 | 开发者本地提交时 | 只跑轻量模型,检查语法、调试残留、密钥泄露 | 建议开启 |
| PR 检查 | 创建/更新 PR 时 | 完整代码审查、安全告警研判 | 必开 |
| 合并前门禁 | 点击合并按钮前 | 复核 critical 级问题是否处理 | 建议开启 |
| 夜间定时任务 | 每天凌晨 | 全仓库扫描、依赖漏洞复查、存量告警重判 | 必开 |
我们几乎没有在“每次代码推送”这个级别跑完整 AI 审查。原因很简单:成本。开发本地每一次 commit 都跑一遍完整审查,不仅模型调用费用受不了,延迟也会让人烦躁。本地钩子只做最便宜的检查,比如用一个小模型检查是否把密钥提交进仓库,或者代码里有没有遗留调试语句。真正的深度分析留给 PR 阶段,因为 PR 是研发流程里“正式评审”的自然节点,大家本来就预期这个地方会慢一点。
PR 阶段我们再细分成两条并行流水线:一条是常规代码审查线,负责逻辑和规范问题;另一条是安全扫描线,等传统 SAST 扫描完成后,把告警攒一批,统一交给 AI 研判。这里有一个时间差的问题:SAST 扫描通常需要几分钟,AI 研判又要几十秒,加起来可能超过五分钟。如果整个 PR 必须等完整流程跑完才能看到结果,开发者会觉得太慢。我们干脆把结果分成两批返回:第一批是代码审查评论,马上出现在 PR 评论区;第二批是安全研判简报,扫描结束后补充。这样体验上就舒服很多。
4.2 质量门槛怎么设:哪些问题阻止合并,哪些只当提示
这是整个流程里争议最大的地方。两条路,一条是把门槛设得很高(所有 critical 必须清零才能合并),另一条是什么都不设(AI 只提建议,不阻断任何操作)。实践下来,我强烈建议走第三条路:按“置信度”和“严重程度”双维度来控制。
我们的设置大概是这样的:
| AI 输出严重级别 | AI 置信度 | 流水线行为 |
|---|---|---|
| critical | 高 | 强制阻止合并(必须人工确认并修复) |
| critical | 中 | 标记为“需人工复核”,给 reviewer 一个专门的待办 |
| warning | 任意 | 在 PR 评论区提示,不阻止合并 |
| suggestion | 任意 | 只出现在评论里,不在任何门禁上体现 |
这个设计的核心原则是:门禁不是为了惩罚团队,而是为了把最少数量的关键问题顶到大家面前。如果我们把所有 warning 都设成阻止合并,那团队迟早会发现“即使有个 warning 也能通过”,最后 warning 就没有任何威慑力了。把阻断的权限只留给“critical + 高置信度”,才能保证每次阻断都真的有意义。
实际跑了一个季度后,拦下合并的 PR 大约是 8%,这里面绝大多数是真的问题,比如数据越权、空指针解引用、事务边界错误。开发者对拦截意见最大的已经没有了——因为理由清晰且合理,不是那种“工具说你不行你就是不行”的玄学。
4.3 Token 消耗与成本控制:穷有穷的跑法
成本是很多团队考虑“上不上 AI 审查”的第一道坎。我们算一笔账:一个 300 行 diff 的中等 PR,加上调用链上下文,每次完整审查大约消耗 1.5 万到 2 万 token。轻量模型预处理把其中大约 60% 的 PR 挡在了深度审查之前,真正跑大模型的只有 40%。一个月按 1500 条 PR 算,总 token 消耗大约在 3 亿上下。
这个数字听起来吓人,但按当前主流 API 的百万 token 价格算,一个月成本大致是几百到几千元级别,已经远低于一个工程师半天的人工成本。如果你还是觉得贵,有几种降本手段:
# 1. 增量扫描:对未变更的文件不做重复审查 # 2. Cache:以 diff 哈希作为缓存 key,同一份改动不重复调用模型 # 3. 削峰:把非关键分支的 PR 审查延迟到夜间队列统批处理 # 4. 本地模型:用专有显卡部署中型开源模型跑 summary 和格式检查,大模型只处理 critical 级特别是缓存这条,团队中 40% 的 PR 是反复修改的——开发者 push 一次新 commit,AI 就重新评论一次。我们在提交前后对比 diff 哈希,只有哈希变了才重新请求模型;如果只是新增一行注释,就不需要动用模型重新理解。这个优化让我们成本下降了大约一半。
4.4 失败模式与降级方案:AI 抽风了,流水线不能跟着崩
模型调用不像传统工具那么稳定,偶尔会超时、限流甚至返回空内容。如果这些异常直接导致流水线失败,开发者的所有 PR 都会被卡住,那真是好心办坏事。我们的做法是给每一个 AI 环节都设定了优雅降级策略。
具体是这样:代码审查环节如果模型调用失败,流水线不阻断,只标记“AI 审查暂不可用”,并保留人工 Review 通道。安全研判环节如果模型失败,则使用传统 SAST 告警原始结果,全部标为“待人工确认”,不让自动化流程把安全问题直接吞掉。另外我们在模型调用外加了一层超时控制,单次请求超过 45 秒就取消,并把该 PR 的审查任务排队到下一轮重新尝试。这样即便模型服务提供商那边出现断断续续的故障,PR 主流程依然跑得通,只是 AI 建议会短暂缺席。
5. 实测之后的冷水:AI 的幻觉、边界与人工兜底方案
5.1 AI 也会一本正经地胡说八道:幻觉式评论是最难防的
把 AI 夸完之后,我必须冷静下来聊聊问题。最烦人的是幻觉评论。模型在审查时,有时会脑补出代码里根本不存在的情况。比如我们遇到过 AI 声称某处存在“整数溢出风险”,但那个位置的变量是字符串长度,根本不参与数值计算。还有 AI 对某个函数参数来源的分析明显错误,硬说某变量来自用户输入,实际上它是一个环境变量。
这类幻觉评论最难处理的地方在于,它们看起来非常专业——有行号、有建议、有危害说明——如果不仔细核实,开发者真的会被带偏。我们做了两个应对:第一,给所有 AI 评论加上一个“AI generated”标签,并注明需要人工复核;第二,在 Prompt 里明确要求模型“如果无法确认数据来源,请明确回答不知道,而不是推测”。这个小改动让幻觉比例下降了不少,但还是没法完全消除。
更麻烦的还有一种“过度修改”倾向。模型有时候会建议把一段本身正确的代码改写成更复杂的抽象结构,理由是“提高可扩展性”。这种建议虽然不算错,但在代码审查里很致命——它会让开发者陷入不必要的重构,反而引入回归风险。我们最终的决定是:凡是模型没有明确指出“会导致 bug 或安全问题”的建议,一律降级为 suggestion,放在附注里,不给它上任何重量级展示位。
5.2 大仓库的上下文难题:上下文窗口还有天花板
AI 再怎么强,上下文窗口也有限。遇到一些大型单体仓库,一个函数往往依赖整个模块的状态,而且这些状态定义可能分散在几十个文件里。模型只能看到我们塞给它的那一点上下文,看不到的地方它就只能瞎猜。
我们试过一个方案:先让轻量模型从仓库里抽取与本次改动相关的符号依赖图(symbol-level dependency graph),再基于这个图确定要加载哪些文件。理论上很美好,实际跑起来发现还是不够用。跨模块的函数调用链一旦超过五层,模型就处理得比较吃力,反而更容易出现幻觉。现在我们的策略是:如果 AI 对某条评论的判断置信度低于 60%,就干脆只输出一个提示“建议人工重点审查此处”,同时不提供具体的修改建议。这样避免了 AI 在信息不完备时强行给出确定性结论的尴尬局面。
5.3 数据安全与私有化:代码不出内网,有几种解法
在接 AI 之前,不少人会担心一个绕不开的问题:代码会不会被发送到外部 API?这确实是个真问题,不同行业、不同项目管制要求都不一样。我们最初也只敢拿不敏感的项目试水,后面才逐步放开。
根据我接触下来的经验,按可接受程度从低到高,通常有几种做法。第一种是直接使用商用 API,允许代码片段发到云端模型厂商;这种方案成本低、效果最好,但要求代码本身不涉及敏感信息。第二种是用私有化网关类方案——现在很多模型提供商都出了代理模式,也就是数据只跟企业内部部署的中转服务交互,企业可以保留审计日志和脱敏能力。第三种当然是最彻底的:内网自托管开源模型,完全不出网。代价也很明显,小参数模型在复杂代码审查上的表现不如云端大模型,尤其是安全漏洞研判这类需要强推理的任务,差距还是挺明显的。
我们最终采用的是混合策略:常规代码审查走内网小模型,判断不了的高危问题走云端大模型但只发送脱敏后的代码骨架,把函数名、字符串常量全部替换成符号。上线之后效果还能接受,虽然稍微损失了一点准确率,但整个团队睡觉踏实多了。
5.4 人和 AI 的分工:什么必须人看,什么可以交给 AI
流水线跑顺之后,最后一步是把团队的分工方式重新划清楚。这里我想做得直白一点,直接说我们小组的现行标准:
| 事务 | 处理人 | 为什么 |
|---|---|---|
| 格式、命名、调试残留 | AI 全权处理 | 规则明确,成本最低 |
| 常规业务逻辑的局部检查 | AI 初筛 + 开发者复核 | AI 能发现遗漏,但需要人来判断业务语义 |
| 跨模块架构设计、接口兼容性 | 必须人看,AI 只提供参考意见 | 上下文和隐性知识要求太高 |
| 安全问题(尤其是涉及数据流、权限) | AI 研判 + 安全负责人终审 | AI 做过滤和排序,人做最终裁定 |
这个分工的核心是承认一个事实:AI 适合做“高频率、低复杂度”的初筛,把人的注意力集中在“低频、高复杂度”的终审上。如果不划分清楚,最容易出现的局面是团队过于信任 AI 评论,最后出现“AI 说没问题就合入,AI 说有问题才看一眼”的盲从状态。我们在每周例会上专门留了一个环节,让开发者挑出本周 AI 最蠢的评论和大家“共赏”。这不仅是缓解气氛,也是在不断校准团队对 AI 的信任度——知道它什么时候能信,什么时候必须保持怀疑。
跑了几个月后,我们内部有一个共识已经形成:AI 确实把代码审查和安全扫描从“研发的噩梦”变成了“流程中的普通一环”。这个过程没有神奇魔法,靠的是一遍遍调门槛、改 Prompt、试缓存策略,还有最朴素的一条——永远保留人的判断权。如果你准备在自己的流水线里做同样的事,我的建议是只拿一个月时间做试点,用真实 PR 跑一遍,让开发者直接感受评论质量,比任何 PPT 和方案文档都管用。我自己的经验是,第一周没几个人说好,第三周开始有人主动给 AI 提需求,第五周就有人说“没这条流水线我不知道怎么干活了”。这个转变,比任何自动化评估数据都更能说明问题。