1. 从一次“评论刷屏”说起:为什么要自建 PR 审查机器人
代码审查这件事,做过团队协作的都懂:PR 一多,Reviewer 就成了瓶颈。我所在的团队一度有 6 个人同时提 PR,结果就是 review 排队、上下文切换、漏看安全漏洞。后来我们想了个办法——让机器人先过一遍,把明显的问题(命名、重复代码、潜在空指针、硬编码密钥)标出来,人只看它标不出来的业务逻辑。
这个机器人要做的事情很具体:GitHub 上有人开 PR 或往 PR 推新 commit,GitHub 通过 Webhook 把事件推给我的 FastAPI 服务,服务拉取 diff,调用 Claude API 分析,再把审查意见作为评论写回 PR。整条链路里,Claude API 的 Key 管理是最容易出问题的一环——团队里每个人都要用,Key 散落在各人电脑上,轮换一次要通知一圈。这篇就用 TaoToken 的统一 Key 来解决这个问题,配合 Cursor 写代码,把从 PR 触发到评论回写的完整链路跑通。
适合谁看:写过一点 Python、用过 GitHub、想让 PR 审查自动化但不想折腾多套 Key 的开发者。全程可跟做,配置文件和验证步骤都会给全。
2. TaoToken 前置:统一 Key 怎么接进 Claude API
TaoToken 在这里扮演的角色是“统一入口”。你不需要在每台机器、每个服务里塞不同的 Key,而是拿一个 TaoToken 的 Key,通过它的 API 地址去调用 Claude 模型。对代码来说,改动很小——主要是把base_url指过去,模型名照常用。
先拿 Key。打开控制台,在 API Keys 页面创建一个新 Key,复制出来。这个 Key 后面会写进.env,不要提交到仓库。
- 控制台入口:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite
- API Keys 管理:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite
- 接入文档(看 base_url 和模型名):https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite
API 的基础地址是https://taotoken.net/api,注意这个地址后面不加 UTM 参数,直接用于代码里的base_url。模型名按文档里列的 Claude 系列填,比如claude-3-7-sonnet-20250219这类。如果你不确定当前可用的模型名,去模型对话页面手动发一条消息,看返回里用的哪个模型,照着填就行。
提示:TaoToken 的 Key 是统一管理的,团队里可以给 CI/CD 和本地开发各建一个 Key,方便单独吊销。别把同一个 Key 贴到公开仓库。
3. 可复制配置:config.toml 与 settings.json 骨架
先把项目骨架搭起来。用 Cursor 新建一个目录code-review-bot,然后在里面建下面这些文件。我习惯把“跟 TaoToken 相关的配置”和“跟 GitHub App 相关的配置”分开,前者放config.toml,后者放.env,这样换 Key 的时候只动一个地方。
3.1 config.toml:TaoToken 与模型参数
# config.toml [taotoken] base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" # 实际 Key 从环境变量读,不写死 model = "claude-3-7-sonnet-20250219" max_tokens = 2000 timeout = 60 [review] # 只审查这些动作,避免关闭 PR 也触发 actions = ["opened", "synchronize", "reopened"] # 单次 diff 最大字符数,超过就截断,防止 token 爆掉 max_diff_chars = 60000 # 是否在评论里附带严重问题摘要 summary = true3.2 settings.json:Cursor 工作区配置
在.cursor/settings.json里放一些项目级设置,主要是让 Cursor 知道这个项目用 Python、用哪个解释器,以及把敏感文件排除在索引外。
{ "python.defaultInterpreterPath": ".venv/bin/python", "python.analysis.typeCheckingMode": "basic", "files.exclude": { "**/.env": true, "**/*.pem": true }, "cursor.chat.projectContext": [ "config.toml", "main.py", "github_handler.py", "claude_review.py" ] }3.3 .env:GitHub App 与 TaoToken Key
TAOTOKEN_API_KEY=你的_taotoken_key GITHUB_APP_ID=123456 GITHUB_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----" WEBHOOK_SECRET=你自己设的一串随机字符串GITHUB_PRIVATE_KEY里的换行要写成\n,读出来后再替换回真实换行,这个坑后面排障会讲。
3.4 依赖清单
fastapi==0.115.0 uvicorn==0.30.0 httpx==0.27.0 PyGithub==2.3.0 anthropic==0.40.0 python-dotenv==1.0.0 tomli==2.0.1装依赖:
python -m venv .venv source .venv/bin/activate pip install -r requirements.txt4. 核心代码:Webhook 签名验证与 Claude 审查
这一节是重点,代码给全,你可以直接复制。核心有三块:签名验证、Claude 调用、GitHub 评论回写。
4.1 配置加载(config.py)
import os import tomli from dotenv import load_dotenv load_dotenv() with open("config.toml", "rb") as f: CFG = tomli.load(f) TAOTOKEN_BASE_URL = CFG["taotoken"]["base_url"] TAOTOKEN_API_KEY = os.getenv(CFG["taotoken"]["api_key_env"]) MODEL = CFG["taotoken"]["model"] MAX_TOKENS = CFG["taotoken"]["max_tokens"] TIMEOUT = CFG["taotoken"]["timeout"] GITHUB_APP_ID = int(os.getenv("GITHUB_APP_ID")) GITHUB_PRIVATE_KEY = os.getenv("GITHUB_PRIVATE_KEY").replace("\\n", "\n") WEBHOOK_SECRET = os.getenv("WEBHOOK_SECRET") REVIEW_ACTIONS = CFG["review"]["actions"] MAX_DIFF_CHARS = CFG["review"]["max_diff_chars"]4.2 Claude 审查逻辑(claude_review.py)
这里用anthropic库,但把base_url指向 TaoToken。注意api_key用的是 TaoToken 的 Key。
import anthropic from config import TAOTOKEN_BASE_URL, TAOTOKEN_API_KEY, MODEL, MAX_TOKENS, TIMEOUT client = anthropic.Anthropic( api_key=TAOTOKEN_API_KEY, base_url=TAOTOKEN_BASE_URL, timeout=TIMEOUT, ) PROMPT_TEMPLATE = """你是资深代码审查员。请审查以下 GitHub PR 的 diff,输出 Markdown: 1. 严重问题(安全漏洞、逻辑错误、性能瓶颈),每条给出文件名+行号和修复建议 2. 代码规范问题(命名、格式、重复代码) 3. 可维护性建议(注释、模块拆分) 只输出问题,不要复述 diff。若某类无问题,写“无”。 diff: {diff} """ def review_diff(diff_text: str) -> str: if len(diff_text) > MAX_DIFF_CHARS: diff_text = diff_text[:MAX_DIFF_CHARS] + "\n...(diff 已截断)" resp = client.messages.create( model=MODEL, max_tokens=MAX_TOKENS, messages=[{"role": "user", "content": PROMPT_TEMPLATE.format(diff=diff_text)}], ) return resp.content[0].text4.3 Webhook 签名验证与事件处理(github_handler.py)
签名验证是安全底线,必须用hmac.compare_digest做常量时间比较,别用==。
import hmac import hashlib import httpx from fastapi import HTTPException, Request from github import Github, GithubIntegration from config import ( WEBHOOK_SECRET, GITHUB_APP_ID, GITHUB_PRIVATE_KEY, REVIEW_ACTIONS, ) from claude_review import review_diff def verify_signature(payload: bytes, signature: str, secret: str) -> bool: if not signature: return False mac = hmac.new(secret.encode(), msg=payload, digestmod=hashlib.sha256) expected = f"sha256={mac.hexdigest()}" return hmac.compare_digest(expected, signature) async def handle_pr_event(request: Request): body = await request.body() signature = request.headers.get("X-Hub-Signature-256") if not verify_signature(body, signature, WEBHOOK_SECRET): raise HTTPException(status_code=401, detail="Invalid signature") event = request.headers.get("X-GitHub-Event") if event != "pull_request": return {"msg": "ignored event"} payload = await request.json() action = payload.get("action") if action not in REVIEW_ACTIONS: return {"msg": f"ignored action {action}"} pr = payload["pull_request"] repo_full_name = payload["repository"]["full_name"] pr_number = pr["number"] diff_url = pr["diff_url"] async with httpx.AsyncClient(timeout=30) as http: diff_resp = await http.get(diff_url) diff_resp.raise_for_status() diff_text = diff_resp.text review_comment = review_diff(diff_text) installation_id = payload["installation"]["id"] integration = GithubIntegration(GITHUB_APP_ID, GITHUB_PRIVATE_KEY) token = integration.get_access_token(installation_id).token g = Github(login_or_token=token) repo = g.get_repo(repo_full_name) pr_obj = repo.get_pull(pr_number) pr_obj.create_comment(review_comment) return {"msg": "review posted", "pr": pr_number}4.4 FastAPI 入口(main.py)
from fastapi import FastAPI, Request from github_handler import handle_pr_event app = FastAPI(title="Code Review Bot") @app.post("/webhook") async def webhook(request: Request): return await handle_pr_event(request) @app.get("/health") async def health(): return {"status": "ok"}5. 验证请求:本地 ngrok 联调跑通完整链路
代码写完,先在本地验证。GitHub 的 Webhook 需要公网地址,本地用 ngrok 把 8000 端口暴露出去。
5.1 启动服务
uvicorn main:app --reload --port 8000访问http://localhost:8000/health,返回{"status":"ok"}说明服务起来了。
5.2 启动 ngrok
ngrok http 8000记下 ngrok 给的公网地址,比如https://xxxx.ngrok-free.app。
5.3 配置 GitHub App
在 GitHub Settings → Developer settings → GitHub Apps 里新建或编辑你的 App:
- Webhook URL 填
https://xxxx.ngrok-free.app/webhook - Webhook secret 填和
.env里WEBHOOK_SECRET一致的值 - Permissions:Pull requests 选 Read & write
- Subscribe to events:勾选 Pull request
- 生成 Private Key 并下载,内容填进
.env的GITHUB_PRIVATE_KEY
5.4 触发测试
在一个测试仓库里开一个 PR,随便改几行代码。观察两处:
- 本地 uvicorn 日志出现
POST /webhook 200 - PR 页面出现机器人评论,内容是 Claude 生成的审查意见
如果评论出现了,说明从 PR 触发到评论回写的链路已经通了。这一步实测下来,小 PR 大概 5 到 10 秒出评论。
6. 本篇常见错排查
6.1 Webhook 返回 401 Invalid signature
最常见的原因是 secret 不一致。检查 GitHub App 里填的 secret 和.env里的WEBHOOK_SECRET是否完全一样,注意别多空格。另一个原因是签名算法——GitHub 用的是sha256,代码里hmac.new(..., digestmod=hashlib.sha256)要对上。
6.2 GitHub App 认证失败:private key 格式
.env里的GITHUB_PRIVATE_KEY如果直接粘贴多行,dotenv 会读错。正确做法是把换行写成\n,代码里.replace("\\n", "\n")还原。如果报Could not parse the provided public key,八成是这里。
6.3 Claude API 调用超时或 401
先确认TAOTOKEN_API_KEY读到了——在config.py里临时打印一下长度,别打印内容。401 通常是 Key 没读到或写错。超时的话,把config.toml里的timeout调大,或者把审查放到后台任务里,别阻塞 Webhook 响应。
6.4 评论内容被截断
GitHub 单条评论有长度限制。如果 diff 很大,Claude 输出可能超限。两个办法:一是把max_diff_chars调小,只审查关键文件;二是把审查结果拆成多条评论,按严重问题、规范问题分开发。
6.5 重复评论
PR 每次 push 都会触发synchronize,如果每次都发新评论,PR 会被刷屏。可以在发评论前先查一下机器人之前的评论,删掉旧的再发新的,或者用create_review而不是create_comment。
7. 长期编码与 Agent 场景:Coding Plan 与 Cursor 配合
如果你打算把这个机器人长期跑下去,或者想用 Cursor 的 Agent 模式继续迭代,可以考虑 TaoToken 的 Coding Plan。它适合长期编码、Agent 这类持续调用的场景,Key 统一管理,不用每次换项目都重新配。
- Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite
- 模型对话(验证模型名和连通性):https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite
- 接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite
在 Cursor 里继续开发时,可以直接把github_handler.py丢给 Agent,让它帮你加日志、加重试、加缓存。比如“给 review_diff 加一个 10 分钟的 diff 哈希缓存,避免相同 diff 重复调用 API”,Agent 会自己改代码。
8. 部署与收尾
本地跑通后,部署到有公网地址的服务器或平台,把 GitHub App 的 Webhook URL 换成正式域名。环境变量在部署平台里配好,别提交.env。部署完再开一个 PR 验证一次,确认评论正常回写。
最后留一个实用技巧:给机器人评论加一个隐藏标记,比如在评论末尾加<!-- code-review-bot -->,这样下次更新时可以用这个标记找到旧评论并删除,避免刷屏。这个标记不影响渲染,但能让你在 API 里精确定位。