1. 问题背景:为什么需要自动化代码审查

很多小团队并没有配备专职的代码审查人员,往往是开发者互相 review。由于排期紧张,review 很容易变成「点开看一眼、点个 Approve」,一些低级问题就流进了主干分支。我们希望有一个能 7×24 小时值守的机器人:每当有人创建或更新 Pull Request,它自动拉取 diff,站在工程规范、安全性、可读性角度给出结构化意见,并直接评论到 PR 页面。

OpenAI Codex 特别适合这类任务:它既理解代码,也理解自然语言,还能按指定 JSON 格式输出结果,方便程序消费。相比把审查规则硬编码成几十条正则表达式,用 Codex 构建审查逻辑不仅实现简单,而且对「未定义边界」的问题也有不错的泛化能力。

本文使用 Python + FastAPI 搭建一个真实可运行的 GitHub App,通过 Webhook 接收 PR 事件,调用 OpenAI Codex 的 Responses API 生成审查意见,再把意见写回 GitHub。文章不会涉及账号购买、充值或支付相关话题,所有流程都可以在本地或自建服务器上完成。

2. 整体架构与数据流

整个系统的数据流如下所示:

GitHub Pull Request

Webhook

FastAPI 接收事件

拉取 PR Diff

Codex 生成审查意见

解析 JSON 结果

GitHub API 评论

作者收到通知

我们只需要关注四个核心模块:事件接收、diff 获取、Codex 调用、评论回写。为了降低复杂度,本文先实现「机器人评论」,不实现复杂的行级定位;把意见以评论形式挂到 PR 下,维护成本最低。

3. 环境准备与项目骨架

先创建项目目录并安装依赖。建议使用 Python 3.11 及以上版本,因为我们需要 str | None 这样的类型写法,代码可读性更好。

mkdir codex-reviewer
cd codex-reviewer
python -m venv .venv
source .venv/bin/activate
pip install fastapi uvicorn openai httpx pydantic-settings

项目文件结构如下,保持小而清晰:

codex-reviewer/
├── app/
│   ├── __init__.py
│   ├── config.py
│   ├── github.py
│   ├── codex.py
│   └── main.py
├── .env
└── requirements.txt

.env 中存放敏感信息,不要提交到仓库:

OPENAI_API_KEY=sk-xxx
GITHUB_TOKEN=github_pat_xxx
WEBHOOK_SECRET=your_random_secret

4. 实现 GitHub Webhook 接收器

FastAPI 应用的主入口负责两件事:验证 Webhook 签名、把任务放到后台队列。虽然示例里直接同步处理,但真实环境建议用任务队列,避免 Codex 调用超时导致 GitHub 重试风暴。

from fastapi import FastAPI, Request, Response, BackgroundTasks
import hmac
import hashlib

from app.config import settings
from app.github import review_pull_request

app = FastAPI(title="codex-reviewer")


def verify_signature(payload: bytes, signature: str) -> bool:
    digest = hmac.new(
        settings.WEBHOOK_SECRET.encode(),
        payload,
        hashlib.sha256,
    ).hexdigest()
    expected = f"sha256={digest}"
    return hmac.compare_digest(expected, signature)


@app.post("/webhook")
async def webhook(request: Request, background: BackgroundTasks):
    signature = request.headers.get("X-Hub-Signature-256", "")
    payload = await request.body()

    if not verify_signature(payload, signature):
        return Response(status_code=401)

    event = request.headers.get("X-GitHub-Event")
    if event not in ("pull_request", "pull_request_target"):
        return {"ok": True}

    data = await request.json()
    action = data.get("action")
    if action not in ("opened", "synchronize", "reopened"):
        return {"ok": True}

    background.add_task(
        review_pull_request,
        data["repository"]["full_name"],
        data["pull_request"]["number"],
    )
    return {"ok": True}

这里用 pull_requestpull_request_target 两种事件都接收,前者是 fork 仓库触发时的安全默认选项,后者可用于需要访问仓库 secrets 的场景。对个人实验项目来说,二选一即可。

5. 调用 Codex 生成审查意见

这是整个机器人的核心。我们从 GitHub 拉取 PR 的 diff,再把它随系统提示一起交给 Codex,要求模型只输出可解析的 JSON。

import json
from openai import OpenAI

from app.config import settings

client = OpenAI(api_key=settings.OPENAI_API_KEY)

SYSTEM_PROMPT = """你是一名资深代码审查工程师。
请审查提供的 git diff,重点关注:
1. 潜在的 bug 与空值处理;
2. 复杂度失控的函数;
3. 缺失的错误处理;
4. 安全隐患,如注入、密钥泄漏、敏感信息打印;
5. 与命名规范和可读性相关但实际会影响维护的问题。

不要评价与功能无关的格式偏好,不要修改 diff 之外的内容。
必须以 JSON 数组输出,每个元素包含:
- file: 文件路径
- severity: "error" | "warning" | "info"
- message: 审查意见
- suggestion: 建议的修改方向

没有问题时输出空数组。不要输出 JSON 以外的内容。"""


def review_diff(diff: str) -> list[dict]:
    if not diff.strip():
        return []

    response = client.responses.create(
        model="codex-mini-latest",
        instructions=SYSTEM_PROMPT,
        input=[
            {"role": "user", "content": diff},
        ],
        text={"format": {"type": "json_object"}},
        max_output_tokens=4096,
    )

    raw = response.output_text
    data = json.loads(raw)

    if isinstance(data, dict):
        reviews = data.get("reviews", [])
    elif isinstance(data, list):
        reviews = data
    else:
        reviews = []

    return reviews

这里使用结构化输出参数 text.format = json_object,能显著降低模型输出非 JSON 的概率。但即便如此,仍然要在解析处加上兜底逻辑,避免一次解析失败就中断整个任务。

6. 把审查结果写回 GitHub

拿到审查结果后,按 diff 是否为空、评论是否为空分别处理,避免给作者刷屏。评论内容使用 Markdown 表格展示,方便阅读。

import httpx

from app.codex import review_diff
from app.config import settings

GITHUB_API = "https://api.github.com"


def get_pull_request(repo: str, number: int) -> dict:
    headers = {
        "Authorization": f"Bearer {settings.GITHUB_TOKEN}",
        "Accept": "application/vnd.github+json",
    }
    resp = httpx.get(
        f"{GITHUB_API}/repos/{repo}/pulls/{number}",
        headers=headers,
        timeout=30,
    )
    resp.raise_for_status()
    return resp.json()


def get_diff(repo: str, number: int) -> str:
    headers = {
        "Authorization": f"Bearer {settings.GITHUB_TOKEN}",
        "Accept": "application/vnd.github.diff",
    }
    resp = httpx.get(
        f"{GITHUB_API}/repos/{repo}/pulls/{number}",
        headers=headers,
        timeout=60,
    )
    resp.raise_for_status()
    return resp.text


def build_comment(reviews: list[dict]) -> str:
    if not reviews:
        return "✅ Codex 已完成审查,未发现需要处理的问题。"

    lines = [
        "🤖 Codex 自动审查意见如下:",
        "",
        "| 文件 | 级别 | 意见 |",
        "| --- | --- | --- |",
    ]
    for item in reviews:
        severity = item.get("severity", "info")
        icon = {"error": "🔴", "warning": "🟡", "info": "🔵"}.get(severity, "⚪")
        lines.append(
            f"| {item.get('file', '-')} | {icon} {severity} | {item.get('message', '')} |"
        )
    return "\n".join(lines)


def review_pull_request(repo: str, number: int):
    diff = get_diff(repo, number)
    reviews = review_diff(diff)
    body = build_comment(reviews)

    headers = {
        "Authorization": f"Bearer {settings.GITHUB_TOKEN}",
        "Accept": "application/vnd.github+json",
    }
    resp = httpx.post(
        f"{GITHUB_API}/repos/{repo}/issues/{number}/comments",
        headers=headers,
        json={"body": body},
        timeout=30,
    )
    resp.raise_for_status()
    return resp.json()

issues/{number}/comments 这个端点对 PR 同样有效,因为 Pull Request 在 GitHub 内部也以 issue 形式存在,用它发评论无需引入 PR 专属的 review 状态流转。

7. 健壮性设计:重试、超时与 diff 截断

真实项目里,diff 可能非常大。一个大型 PR 经常有数万行变更,直接塞进模型会超出上下文窗口,也浪费 token。所以在调用 Codex 之前,应该先做一次粗粒度过滤。下面这段代码会跳过生成物、锁定文件、二进制文件和纯空行变更。

SKIP_PATTERNS = (
    "package-lock.json",
    "yarn.lock",
    "pnpm-lock.yaml",
    "poetry.lock",
    "pnpm-lock.yaml",
    "*.min.js",
    "*.map",
)

MAX_DIFF_CHARS = 60_000


def filter_diff(diff: str) -> str:
    lines = diff.splitlines()
    kept: list[str] = []
    for line in lines:
        if line.startswith("diff --git"):
            continue
        if any(line.endswith(p.removeprefix("*")) or p in line for p in SKIP_PATTERNS):
            continue
        kept.append(line)

    text = "\n".join(kept)
    if len(text) > MAX_DIFF_CHARS:
        text = text[:MAX_DIFF_CHARS] + "\n... diff 过长,已截断 ..."
    return text

这段过滤逻辑比较朴素,但足以拦掉最常见的噪音。若要做得更精细,可以在拿到完整 diff 后按文件切分,再分别调用模型,最后合并结果。

此外,GitHub Webhook 期待接收方快速返回 2xx。如果 Codex 调用在请求线程里阻塞 30 秒,GitHub 会判定交付失败并重复推送。因此使用 BackgroundTasks(生产环境换成 Celery、Dramatiq 或 RQ)非常重要。对 Codex 调用本身,也应该加上重试与指数退避,处理偶发的 429 或 5xx 错误。

8. 部署安全建议

在公开仓库上运行机器人时,以下问题需要特别注意:

  • 使用 GitHub App 而不是个人访问令牌,App 可以精确到「只读 pull_request、写 issue 评论」等最小权限。
  • Webhook Secret 要足够随机,签名验证失败直接返回 401。
  • 不要把 OPENAI_API_KEY 打印到日志里,也不要出现在异常堆栈中。
  • 对 Codex 输出做兜底解析,防止模型返回包裹在 Markdown 代码块里的 JSON。你可以在解析前先把文本中的 json 和 包裹去掉。

9. 实测效果与局限

把机器人部署到一个内部服务仓库后,它很快就能稳定指出几类典型问题:except Exception 后吞掉异常、未处理 None 返回值、拼接 SQL 字符串、把密钥直接写进代码。对刚接触 Python 的开发者来说,这些提醒比静态检查工具更「会说话」,因为它能同时给出具体建议。

但 Codex 审查也有明显局限:它可能对 difff 缺少上下文、对跨文件语义产生误判;有时会提出风格层面的建议,需要靠 system prompt 约束;超大 PR 的处理仍需要分片策略。把 Codex 当作「辅助第一道关卡」,而不是替代人工 review,是更务实的定位。

10. 小结

本文从零搭建了一个基于 OpenAI Codex 的自动化代码审查机器人,覆盖了 Webhook 接收、diff 拉取、模型调用、结果解析和评论回写五个环节。核心工作只有约一百多行 Python,却能给团队省下大量重复性 review 时间。

后续可以继续扩展:把评论升级为行级 review comments、加入对 issue 中自然语言需求的解析、按仓库自定义审查规则。AI 编程工具的落地,往往不需要立刻替代整个流程,从「检查 diff 并提出意见」这样一个小切口开始,反而更容易产生真实价值。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐