基于 RAG 构建团队专属 AI 代码审查助手:从向量数据库到 CI 集成

4 阅读14分钟

标签:前端工程化、人工智能、代码审查、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 也改到了它,那关键词命中往往比语义近似更可靠。

所以更合理的做法是混合检索,至少包含三部分:

  1. 向量检索:找语义相近的历史经验
  2. 关键词检索:找精确命中的 API / 文件 / 术语
  3. 时序加权:最近 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,每条评论至少包含:

  • line
  • severity
  • message
  • reference
  • suggestion

下面给一个核心示例。

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 里一般会做这些事:

  1. 读取当前 PR diff
  2. 提取关键词和变更文件路径
  3. 调用 HybridRetriever 检索历史资料
  4. 调用 ReviewAgent 生成 review JSON
  5. 调 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 进入工程体系后最值得期待的地方。