标签:前端工程化、人工智能、代码审查、RAG、GitHub Actions
这两年,很多团队都试过用 AI 做代码审查。
最常见的做法,是把 PR 的 diff 丢给大模型,然后让它输出一堆 review 建议。刚开始看起来很惊艳:它会提醒空值风险、命名问题、抽象层级、性能隐患,甚至还会一本正经地给出“最佳实践”。
但真把这种方案接进团队流程后,大家通常很快会遇到三个问题:
第一,它不懂你们团队自己的规范。
比如团队明明规定异步数据流必须走 React Query,它却建议你手写 useEffect + fetch;
明明你们内部有统一请求层,它却让你直接在组件里发请求。
第二,它不了解你们踩过的历史坑。
比如某个 API 在 Safari 下会触发内存泄漏,某个埋点 SDK 曾经引起过首屏阻塞,某个封装方式已经在事故复盘里被明确禁止。
这些信息,公共模型并不知道。
第三,它会用“通用正确”去误伤“团队最优” 。
很多优秀的业务抽象、兼容性兜底、历史包袱处理,在一个不知道上下文的模型看来,可能都像“多余设计”或者“代码味道”。
所以,通用 AI 做代码审查,最大的问题从来不是“模型会不会看代码”,而是:
它没有团队记忆。
而代码审查这种事情,偏偏极度依赖团队记忆。
一个成熟团队的 review,从来不只是看这一行代码写得对不对,更是在判断:
- 它是否符合团队长期约定
- 是否重复踩过历史事故
- 是否违背过去已经做出的架构决策
- 是否和现有代码体系保持一致
这就是为什么,真正可用的 AI 审查系统,不能只靠“大模型 + diff”,而必须引入一层团队知识增强。
最实用的一条路,就是:RAG(检索增强生成) 。
也就是:
- 先从团队历史资料中检索相关上下文
- 再让模型基于这些上下文生成审查意见
- 并且尽量带上引用来源,而不是空口给建议
这样,AI 才不是一个“会说漂亮话的外部顾问”,而更像一个“读过你们团队全部历史 review 和事故文档的老同事”。
这篇文章,我就从工程落地角度,完整讲一套方案:
如何基于 RAG,构建一个团队专属的 AI 代码审查助手,并把它接进 GitHub Actions。
一、问题定义:为什么通用 AI 审查很快会遇到上限
先把问题说透。
很多人做 AI 审查时,脑子里的默认模型是:
“PR diff + 大模型 = 自动 review”
这个公式当然不是完全错,但它只适合做最浅层的通用检查。
一旦你想让它真正进入团队流程,它就会立刻暴露上限。
1. 它不懂团队规范
通用模型的“最佳实践”,本质上是互联网平均经验。
而一个团队真正执行的,往往是局部最优规则。
比如:
- 状态获取统一走 React Query,而不是组件内直接缓存
- 埋点必须经过中间层,不能直接调第三方 SDK
- 表格页统一使用某套基础组件,不允许自造轮子
- 某些移动端页面禁用复杂动画,因为线上机型兼容性差
这些规则很多都不写在公共文档里,只存在于:
- 历史 CR
- 团队 wiki
- ADR 文档
- 事故复盘
- 老同事口口相传的经验里
如果没有把这些东西接入模型,AI 的建议再聪明,也只能停留在“通用但不贴身”。
2. 它不了解历史 bug
真正有价值的 review,很多时候不是发现“语法问题”,而是识别“已知高危模式”。
比如:
- 这个 API 之前在 Safari 上有泄漏
- 这个异步取消逻辑以前引发过竞态
- 这个组件的卸载顺序曾经踩过坑
- 这个库的某个版本在低端机上有性能问题
这些都不是模型凭空推理就能稳定得出的。
它必须建立在团队历史经验可检索的前提下。
3. 它没有“引用意识”
人在做高质量 review 时,一个常见动作是:
- “这个问题之前在 XX PR 里讨论过”
- “按我们 ADR 第 14 条,这里应该走统一封装”
- “去年某次事故复盘里提过,这种写法有风险”
也就是说,成熟 review 的说服力不只是来自“这条建议本身”,更来自“它背后有依据”。
而通用大模型最大的问题之一就是:它太会说了,但不一定知道自己为什么这么说。
这在团队协作里是很危险的,因为没人喜欢被一个“像很懂、但拿不出证据”的 AI 指挥。
所以问题的本质是什么
不是模型不够强。
而是缺少一层团队知识检索与引用机制。
这也正是 RAG 发挥作用的地方。
二、系统架构:让 AI 先检索团队记忆,再生成 review
这套系统如果用一句话概括,就是:
开发者提交 PR → GitHub Actions 触发 → 检索团队知识 → 生成带引用的审查意见 → 回写到 PR
你可以把它拆成四层。
第一层:触发层
触发源通常就是 GitHub 的 pull_request 事件。
只要有人新开 PR,或者推送新 commit,审查流程就自动跑起来。
这里拿到的核心输入有两个:
- 当前 PR 的 diff
- PR 涉及的文件路径、变更位置、上下文代码
这部分信息决定了“要审什么”。
第二层:数据层
这是整套系统最重要的底座。
它存储的不是通用代码知识,而是团队自己的审查语料与工程记忆,通常可以包括:
- 历史 PR review comments
- ADR(架构决策记录)
- bug 复盘文档
- 团队编码规范
- 关键模块的设计说明
- 曾经封禁或限制的实现方式
这些数据经过清洗、切分、向量化之后,进入向量数据库,比如 Pinecone、Weaviate、Qdrant 都可以。
第三层:检索与推理层
当一个 PR 到来时,系统会做两件事:
第一,基于当前 diff 构造查询,去向量库里找“最相关的历史经验”。
第二,把检索结果和当前代码上下文一起喂给大模型,让它生成结构化 review。
这时候模型不是在“空想”,而是在“带资料答题”。
第四层:应用层
模型输出的 review 结果,最终要回写到 PR 里。
为了更接近真实团队使用,建议把评论分成三级:
- Block:高风险,建议阻塞合并
- Suggestion:建议修改,但不一定阻塞
- Note:补充说明或可选优化
这样开发者不会被一堆同级评论淹没,而能快速判断优先级。
这四层连起来,整套链路就完整了:
PR 触发 → 检索团队记忆 → AI 结合上下文生成意见 → 按优先级评论到 PR
这时候,AI 审查助手才第一次具备“像你们团队的人”的可能。
三、技术实现:从数据管道到 GitHub Actions 全流程打通
下面进入实战部分。
3.1 数据管道:先把团队历史 review 变成可检索知识
想让 AI 审查真的有团队味,第一步不是写 Prompt,而是把历史数据整理好。
最有价值的一批数据,通常来自历史 PR review comments。
因为它们天然就是团队真实发生过的“代码审查判断”。
先写一个 GitHistoryExtractor,把 review comments 抓下来,并做一轮基础清洗。
const fetch = require('node-fetch');
class GitHistoryExtractor {
constructor({ githubToken, owner, repo }) {
this.githubToken = githubToken;
this.owner = owner;
this.repo = repo;
this.baseUrl = `https://api.github.com/repos/${owner}/${repo}`;
}
async fetchReviewComments(page = 1) {
const res = await fetch(
`${this.baseUrl}/pulls/comments?per_page=100&page=${page}`,
{
headers: {
Authorization: `Bearer ${this.githubToken}`,
Accept: 'application/vnd.github+json'
}
}
);
if (!res.ok) {
throw new Error(`GitHub API error: ${res.status}`);
}
return res.json();
}
classifyComment(body) {
const text = body.toLowerCase();
if (
text.includes('performance') ||
text.includes('re-render') ||
text.includes('slow')
) {
return 'performance';
}
if (
text.includes('security') ||
text.includes('token') ||
text.includes('xss')
) {
return 'security';
}
if (
text.includes('convention') ||
text.includes('统一') ||
text.includes('规范')
) {
return 'convention';
}
return 'general';
}
async extractValuableComments(maxPages = 5) {
const allComments = [];
for (let page = 1; page <= maxPages; page++) {
const comments = await this.fetchReviewComments(page);
if (!comments.length) break;
for (const item of comments) {
const body = item.body || '';
if (body.length < 50) continue;
allComments.push({
id: item.id,
path: item.path,
body,
diffHunk: item.diff_hunk,
category: this.classifyComment(body),
createdAt: item.created_at,
prUrl: item.pull_request_url
});
}
}
return allComments;
}
}
module.exports = { GitHistoryExtractor };
这个阶段的重点不是“抓全”,而是“抓有价值的”。
像“LGTM”“小改一下”“格式有点乱”这种评论,通常没必要进知识库。真正值得保留的是那些:
- 解释了为什么这么改
- 涉及团队规范
- 提到了历史事故
- 有明确工程判断依据
换句话说,向量库不是垃圾桶。
你喂进去的语料质量,直接决定后面 AI 审查的上限。
为什么要分类
把历史评论分成 performance/security/convention/general 这样的类别,非常有用。
因为在后面的检索阶段,你可以根据当前 PR 特征做更精准的召回,比如:
- 涉及请求和用户输入时,提高 security 类评论权重
- 涉及大列表渲染时,提高 performance 类评论权重
- 涉及公共组件改动时,提高 convention 类评论权重
这会让检索结果更像“有判断的经验调用”,而不是纯粹的语义相似匹配。
3.2 向量化与存储:为什么建议用 CodeBERT 而不是通用 Embedding
很多人做 RAG 时,习惯直接拿通用文本 embedding 模型把所有内容塞进向量库。
这在普通知识问答里没问题,但在代码审查场景里,效果往往不够理想。
因为代码 review 的语义很特殊,它同时混合了:
- 自然语言说明
- 文件路径语义
- API 名、函数名、组件名
- 代码片段上下文
如果只用通用 embedding,它对“代码结构特征”的感知通常比较弱。
更稳的做法,是优先考虑代码语义更强的模型,比如 CodeBERT 这一类编码器。
下面给一个简化示例,展示怎么把 review comments 向量化并存到 Pinecone。
const { Pinecone } = require('@pinecone-database/pinecone');
class ReviewVectorStore {
constructor({ pineconeApiKey, indexName, embedder }) {
this.pinecone = new Pinecone({ apiKey: pineconeApiKey });
this.index = this.pinecone.index(indexName);
this.embedder = embedder;
}
buildDocument(comment) {
return `
[Category]
${comment.category}
[File]
${comment.path}
[Diff]
${comment.diffHunk || ''}
[Review Comment]
${comment.body}
[Created At]
${comment.createdAt}
`.trim();
}
async upsertComments(comments) {
const vectors = [];
for (const comment of comments) {
const text = this.buildDocument(comment);
const embedding = await this.embedder.embed(text);
vectors.push({
id: String(comment.id),
values: embedding,
metadata: {
path: comment.path,
category: comment.category,
body: comment.body,
createdAt: comment.createdAt,
prUrl: comment.prUrl
}
});
}
if (vectors.length > 0) {
await this.index.upsert(vectors);
}
return vectors.length;
}
}
module.exports = { ReviewVectorStore };
这里的关键点有两个。
第一,别只存评论正文
文件路径、diff 片段、时间、类别,这些都很重要。
因为真正相关的 review,不只是“话像不像”,还包括:
- 是不是改的同类型文件
- 是不是涉及相似 API
- 是不是最近半年刚反复提过的问题
第二,时间必须进 metadata
后面做时序加权时,你会发现这一步非常值钱。
因为两年前的经验不一定无效,但三天前刚刚被 review 过的问题,通常更值得优先参考。
3.3 检索策略:混合检索比纯向量检索更适合代码审查
很多 RAG Demo 一上来就只讲向量检索,但在代码审查场景里,纯向量检索通常不够稳。
原因很简单:代码 review 既有语义相似,也有大量必须精确命中的关键词。
比如:
- 文件名
- hook 名
- API 名
- 组件名
- 路径前缀
- 某个内部约定函数
如果某条历史评论明确提到了 useRequestWithRetry,而当前 PR 也改到了它,那关键词命中往往比语义近似更可靠。
所以更合理的做法是混合检索,至少包含三部分:
- 向量检索:找语义相近的历史经验
- 关键词检索:找精确命中的 API / 文件 / 术语
- 时序加权:最近 3 个月的记录权重更高
下面给一个简化的 HybridRetriever。
class HybridRetriever {
constructor({ vectorStore, keywordSearcher }) {
this.vectorStore = vectorStore;
this.keywordSearcher = keywordSearcher;
}
computeTimeWeight(createdAt) {
const now = Date.now();
const created = new Date(createdAt).getTime();
const days = (now - created) / (1000 * 60 * 60 * 24);
// 90 天半衰期
return Math.exp(-days / 90);
}
async retrieve({ queryText, keywords = [], topK = 8 }) {
const vectorResults = await this.vectorStore.search(queryText, topK * 2);
const keywordResults = await this.keywordSearcher.search(keywords, topK * 2);
const merged = new Map();
for (const item of [...vectorResults, ...keywordResults]) {
const existing = merged.get(item.id) || {
...item,
score: 0
};
const baseScore = item.score || 0;
const timeWeight = this.computeTimeWeight(item.metadata.createdAt);
const finalScore = baseScore * 0.7 + timeWeight * 0.3;
existing.score = Math.max(existing.score, finalScore);
merged.set(item.id, existing);
}
return Array.from(merged.values())
.sort((a, b) => b.score - a.score)
.slice(0, topK);
}
}
module.exports = { HybridRetriever };
这段代码虽然简化了很多细节,但思路是对的:
- 只做语义检索,容易漏掉关键术语
- 只做关键词检索,又会丢掉抽象层面的经验
- 再加上时间衰减,才能让系统更贴近真实团队节奏
代码审查最怕的,不是“找不到任何东西”,而是“找回来一堆过时经验”。
所以时序权重非常重要。
3.4 审查 Agent:要求模型必须带引用、输出 JSON
检索拿到结果之后,接下来才轮到模型生成 review。
这一层的目标有两个:
第一,让模型结合检索上下文输出真正贴近团队的意见。
第二,让它必须给引用,而不是只会说大道理。
输出格式建议固定为 JSON,每条评论至少包含:
lineseveritymessagereferencesuggestion
下面给一个核心示例。
const OpenAI = require('openai');
class ReviewAgent {
constructor(apiKey) {
this.client = new OpenAI({ apiKey });
}
buildPrompt({ diff, filePath, retrievedContexts }) {
return `
你是团队专属 AI 代码审查助手,请基于当前 PR diff 和历史审查资料,输出结构化 review 结果。
要求:
1. 必须优先依据提供的历史资料进行判断
2. 每条建议必须引用资料编号,例如 [1]
3. 输出 JSON 数组,不要输出 markdown
4. 每项格式为:
{
"line": 12,
"severity": "block|suggestion|note",
"message": "说明问题",
"reference": "[1]",
"suggestion": "给出可执行建议"
}
5. 如果没有足够依据,不要硬提建议
文件路径:
${filePath}
当前 diff:
${diff}
历史资料:
${retrievedContexts
.map((item, index) => {
return `[${index + 1}]
Path: ${item.metadata.path}
Category: ${item.metadata.category}
CreatedAt: ${item.metadata.createdAt}
Comment: ${item.metadata.body}`;
})
.join('\n\n')}
`;
}
async reviewCode({ diff, filePath, retrievedContexts }) {
const prompt = this.buildPrompt({ diff, filePath, retrievedContexts });
const response = await this.client.responses.create({
model: 'gpt-4.1',
temperature: 0.2,
input: prompt
});
const text = response.output_text?.trim() || '[]';
try {
return JSON.parse(text);
} catch (error) {
throw new Error(`Review JSON 解析失败: ${text}`);
}
}
}
module.exports = { ReviewAgent };
这里有三个非常关键的限制。
1. 强制引用
没有引用的建议,可信度会明显下降。
而且一旦开发者发现它“总在空口胡说”,很快就不会再信它。
2. 不够确定就别硬说
很多 AI 系统的问题,不是能力不足,而是太想表现。
审查系统宁可少说,也不要误报一堆。
3. 输出 JSON,不要自然语言长文
因为最终是要回写到 PR 的,结构化输出后续更好处理。
同时也方便你做优先级统计、误报分析、人工反馈闭环。
3.5 GitHub Actions 集成:让它真正进入团队流程
前面的分析、检索、生成,如果只停留在本地脚本,其实价值有限。
真正的分水岭,是把它接进 CI,让它在 PR 打开时自动执行。
下面给一个可直接改造的 workflow.yml 示例。
name: ai-code-review
on:
pull_request:
types: [opened, synchronize]
jobs:
review:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm install
- name: Run AI review
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
PINECONE_API_KEY: ${{ secrets.PINECONE_API_KEY }}
PINECONE_INDEX: ${{ secrets.PINECONE_INDEX }}
run: node scripts/run-review.js
这个 workflow 触发后,你的 run-review.js 里一般会做这些事:
- 读取当前 PR diff
- 提取关键词和变更文件路径
- 调用
HybridRetriever检索历史资料 - 调用
ReviewAgent生成 review JSON - 调 GitHub API 把评论发回 PR
为了让结果更易读,建议在评论中加上 emoji 区分优先级,比如:
🛑 Block⚠️ Suggestionℹ️ Note
这样开发者一眼就知道哪些是高优先级。
四、效果数据:为什么这类系统最适合做“团队经验放大器”
如果只看表面,AI 审查助手像是在节省代码 review 时间。
但它真正大的价值,其实不是“少看几行代码”,而是把团队经验规模化复制。
一个比较常见的落地收益,大概会体现在两类指标上。
1. 审查耗时下降
比如原来一个中型 PR,从提审到拿到有效 review,平均要 2.5 天。
因为 reviewer 也忙,很多规范问题、历史坑提醒、低价值重复建议,会拖慢整个链路。
接入 AI 审查之后,如果它能先过滤掉一批重复问题,很多团队会把平均审查周期压到 1.2 天左右。
这里节省的,不只是 reviewer 的时间,更是开发者等待反馈的时间。
2. 规范类 bug 明显减少
另一个很常见的收益,是那些“本来不该再犯”的规范性问题开始下降。
比如接入前,一个月可能有 12 个因为团队规范没执行到位而产生的问题;
接入后,这类问题可能降到 2 个左右。
尤其对新人特别友好。
因为新人最大的问题,从来不是不会写代码,而是不知道:
- 哪些坑这个团队已经踩过
- 哪些抽象这里明令不推荐
- 哪些模块看似能改,其实牵一发动全身
而 AI 审查助手一旦做好,本质上就是在帮新人实时读取团队记忆。
五、局限与优化:别把它当神谕,要把它当“可持续训练的团队系统”
这类系统非常有价值,但也必须讲边界。
局限 1:冷启动问题
如果一个团队历史 review 很少、ADR 也没积累、bug 复盘不成体系,那 RAG 的底座天然就弱。
这种情况下,最现实的做法是先用“内部资料 + 开源规范库”双轨制:
- 内部知识作为高优先级来源
- 外部规范作为 fallback
等团队数据慢慢积累起来,再逐步提升内部知识的权重。
局限 2:大文件和长 diff 容易超 Token
大模型再强,也有上下文限制。
如果一个 PR 改了十几个文件、几千行 diff,直接全喂进去肯定不现实。
比较稳的策略是:
- 按文件拆分审查
- 大文件按函数级或组件级分块
- 优先审查高风险变更区域,而不是平均铺开
代码审查本来就不该追求“什么都看一遍”,而该优先看“最值得看”的地方。
局限 3:误报率仍然存在
哪怕你做了 RAG、做了引用、做了混合检索,误报也不会彻底消失。
真实工程里,15% 左右的误报率其实很常见。
关键不在于追求 0 误报,而在于建立反馈闭环。
比如:
- 开发者可以标记“有用 / 无用”
- reviewer 可以对 AI 评论做二次判定
- 被判定为误报的样本反向进入训练数据
- 经常误报的规则降低权重或直接下线
也就是说,这套系统不是“一次建好永远正确”,而是一个会不断吸收团队反馈的工程系统。
六、结语
很多团队在做 AI 落地时,第一反应都是“让它更聪明”。
但代码审查这件事,真正的关键从来不是“聪不聪明”,而是“有没有上下文”。
没有团队记忆的 AI,只能给出通用建议;
而带着团队历史 review、架构决策和事故经验的 AI,才有机会成为真正可用的审查助手。
所以这套方案最核心的价值,不是自动帮你挑几个命名问题,也不是替你写几句漂亮评论。
它真正做的是一件更重要的事:
把原本只存在于老同事脑子里的经验,变成团队可检索、可引用、可传承的系统能力。
当它真正跑起来以后,AI 不再只是一个会聊天的模型。
它会慢慢变成团队的记忆体。
而这,才是 AI 进入工程体系后最值得期待的地方。