0Pricing
AI Agents · 课时

自动化 PR 审查评论

读取 PR 差异,并通过 API 发布行内审查评论。

自动化 PR 审查评论 是 CoddyKit 上的免费 AI Agents 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 AI Agents 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 AI Agents 课程共包含 4 节课。

什么是自动 PR 审查

自动 PR 审查代理会读取已更改的文件、分析差异,并发布针对性的审查评论——就像人工审查者一样,但速度更快且结果更一致。应用场景包括:安全扫描、样式规范检查、检测常见错误、检查测试覆盖率,或总结 PR 的作用。

from github import Github
import os

g = Github(token=os.environ['GITHUB_TOKEN'])
repo = g.get_repo('myorg/myrepo')

# Get a specific pull request by number
pr = repo.get_pull(42)
print(f'PR #{pr.number}: {pr.title}')
print(f'Author: {pr.user.login}')
print(f'Base branch: {pr.base.ref} <- Head: {pr.head.ref}')
print(f'State: {pr.state}')
print(f'Changed files: {pr.changed_files}')
print(f'Additions: +{pr.additions}')
print(f'Deletions: -{pr.deletions}')

列出未关闭的拉取请求

使用 repo.get_pulls() 列出 PR。您可以按状态(open、closed)、基准分支或排序方式进行筛选。对于审查机器人,通常需要处理尚未审查的 open PR。

from github import Github
import os

g = Github(token=os.environ['GITHUB_TOKEN'])
repo = g.get_repo('myorg/myrepo')

# All open PRs targeting main
open_prs = repo.get_pulls(
    state='open',
    base='main',
    sort='created',
    direction='desc'
)

for pr in open_prs:
    # Check if agent has already reviewed it
    reviews = list(pr.get_reviews())
    agent_reviewed = any(
        r.user.login == 'my-agent-bot[bot]'
        for r in reviews
    )
    if not agent_reviewed:
        print(f'Needs review: PR #{pr.number} - {pr.title}')

获取已更改的文件

pr.get_files() 返回一个 File 对象列表,每个对象表示 PR 中更改的一个文件。主要属性包括:filename、status(added/modified/removed)、additions、deletions 和 patch(统一差异文本)。

from github import Github
import os

g = Github(token=os.environ['GITHUB_TOKEN'])
repo = g.get_repo('myorg/myrepo')
pr = repo.get_pull(42)

changed_files = list(pr.get_files())
print(f'Changed files: {len(changed_files)}')

for file in changed_files:
    print(f'\n{file.status.upper()}: {file.filename}')
    print(f'  +{file.additions} -{file.deletions}')

    # The patch is the unified diff
    if file.patch:
        print(f'  Patch preview: {file.patch[:200]}')

# Filter to only Python files
python_files = [
    f for f in changed_files
    if f.filename.endswith('.py') and f.status != 'removed'
]

读取文件补丁(差异)

file.patch 是一个统一差异字符串。以 + 开头的行表示新增内容,以 - 开头的行表示删除内容。@@ 标题显示差异块从哪些行号开始。解析这些内容即可准确找出新代码中的问题位置。

def parse_patch_hunks(patch):
    if not patch:
        return []

    hunks = []
    current_hunk = None
    new_line_num = 0

    for line in patch.split('\n'):
        if line.startswith('@@'):
            # Parse: @@ -old_start,old_count +new_start,new_count @@
            import re
            match = re.search(r'\+([0-9]+)', line)
            if match:
                new_line_num = int(match.group(1))
            current_hunk = {'header': line, 'lines': [], 'start_line': new_line_num}
            hunks.append(current_hunk)
        elif current_hunk is not None:
            if line.startswith('+'):
                current_hunk['lines'].append({'type': 'add', 'content': line[1:], 'line': new_line_num})
                new_line_num += 1
            elif line.startswith('-'):
                current_hunk['lines'].append({'type': 'remove', 'content': line[1:], 'line': None})
            else:
                new_line_num += 1

    return hunks

# --- demo ---
patch = (
    '@@ -10,3 +10,4 @@\n'
    ' def add(a, b):\n'
    '-    return a - b\n'
    '+    return a + b\n'
    '+    # fixed subtraction bug\n'
)
for hunk in parse_patch_hunks(patch):
    print(hunk['header'])
    for line in hunk['lines']:
        print(' ', line)

获取 PR 提交

pr.get_commits() 返回 PR 中的提交。每个提交都包含提交消息、作者和已更改文件列表。分析提交消息有助于审查机器人总结 PR 的作用,并检查提交规范是否良好。

from github import Github
import os

g = Github(token=os.environ['GITHUB_TOKEN'])
repo = g.get_repo('myorg/myrepo')
pr = repo.get_pull(42)

commits = list(pr.get_commits())
print(f'Commits in PR: {len(commits)}')

for commit in commits:
    print(f'\nCommit: {commit.sha[:8]}')
    print(f'Author: {commit.commit.author.name}')
    print(f'Message: {commit.commit.message[:80]}')
    print(f'Changes: +{commit.stats.additions} -{commit.stats.deletions}')

# Check commit message quality
poor_messages = [
    c for c in commits
    if len(c.commit.message.split()[0] if c.commit.message else '') < 3
    or c.commit.message.lower().startswith(('wip', 'fix', 'test'))
]
if poor_messages:
    print(f'{len(poor_messages)} commits have poor message quality')

发布 PR 审查

使用 pr.create_review() 提交正式审查。event 参数控制审查类型:'APPROVE'、'REQUEST_CHANGES' 或 'COMMENT'。一次审查可以包含总体说明,以及针对特定行的行内评论列表。

from github import Github
import os

g = Github(token=os.environ['GITHUB_TOKEN'])
repo = g.get_repo('myorg/myrepo')
pr = repo.get_pull(42)

# Submit a review with a general comment (no inline comments)
pr.create_review(
    body=(
        '**Automated Code Review**\n\n'
        'I analyzed this PR and found the following:\n'
        '- No security issues detected\n'
        '- Style conforms to project guidelines\n'
        '- Test coverage looks adequate\n\n'
        '_This review was generated automatically._'
    ),
    event='COMMENT'  # 'APPROVE', 'REQUEST_CHANGES', or 'COMMENT'
)

发布行内审查评论

行内评论会直接显示在差异中的特定行上。每条评论都指定 path(文件路径)、line(新文件中的行号)和 body。请将评论列表传入 pr.create_review(comments=[...])。

from github import Github
import os

g = Github(token=os.environ['GITHUB_TOKEN'])
repo = g.get_repo('myorg/myrepo')
pr = repo.get_pull(42)

# Build inline comments from analysis results
inline_comments = [
    {
        'path': 'src/auth.py',
        'line': 47,
        'body': (
            ':warning: **Security Issue:** Using `md5` for password hashing.\n'
            'Use `bcrypt` or `argon2` instead.\n'
            'Example: `import bcrypt; bcrypt.hashpw(password.encode(), bcrypt.gensalt())`'
        )
    },
    {
        'path': 'src/utils.py',
        'line': 23,
        'body': (
            ':bulb: Consider using `pathlib.Path` instead of `os.path.join` '
            'for better cross-platform compatibility.'
        )
    }
]

pr.create_review(
    body='Security and style review complete. See inline comments.',
    event='REQUEST_CHANGES',
    comments=inline_comments
)
print(f'Review submitted with {len(inline_comments)} inline comments')

LLM 驱动的代码分析

将文件差异连同安全/质量审查提示词提供给 LLM。要求它返回结构化 JSON,列出问题的文件路径、行号、严重程度和描述。使用这些结果构建行内评论列表。

import anthropic
import json
import os

client = anthropic.Anthropic(api_key=os.environ['ANTHROPIC_API_KEY'])

def analyze_file_with_llm(filename, patch):
    prompt = (
        f'Review this Python code diff for issues.\n'
        f'File: {filename}\n\n'
        f'Return JSON array: [{{"line": N, "severity": "error|warning|info", '
        f'"message": "..."}}]\n'
        f'Focus on: security vulnerabilities, bugs, and anti-patterns.\n'
        f'Diff:\n{patch[:3000]}'
    )

    response = client.messages.create(
        model='claude-opus-4-5',
        max_tokens=1000,
        messages=[{'role': 'user', 'content': prompt}]
    )

    try:
        issues = json.loads(response.content[0].text)
        return [
            {'path': filename, 'line': i.get('line', 1),
             'body': f'[{i.get("severity", "info").upper()}] {i.get("message", "")}'}
            for i in issues if isinstance(i, dict)
        ]
    except json.JSONDecodeError:
        return []

完整的 PR 审查代理流程

将所有部分连接成完整的 PR 审查流程:获取文件,筛选出可审查的文件,使用 LLM 分析每个文件,收集评论,并通过一次接口调用提交审查。为控制 LLM 成本,请限制为前 10 个已更改的文件。

def run_pr_review(g, repo_name, pr_number):
    repo = g.get_repo(repo_name)
    pr = repo.get_pull(pr_number)

    print(f'Reviewing PR #{pr_number}: {pr.title}')

    # Get changed files (limit to 10)
    files = [
        f for f in list(pr.get_files())[:10]
        if f.filename.endswith(('.py', '.js', '.ts'))
        and f.patch
    ]

    all_comments = []
    for file in files:
        print(f'  Analyzing: {file.filename}')
        comments = analyze_file_with_llm(file.filename, file.patch)
        all_comments.extend(comments)

    # Determine review event based on findings
    critical = [c for c in all_comments if '[ERROR]' in c['body']]
    event = 'REQUEST_CHANGES' if critical else 'COMMENT'

    summary = (
        f'Analyzed {len(files)} files. '
        f'Found {len(all_comments)} issues '
        f'({len(critical)} critical).'
    )

    pr.create_review(body=summary, event=event, comments=all_comments[:25])
    print(f'Review submitted: {event} with {len(all_comments)} comments')

检查现有的代理审查

发布审查前,请检查您的代理机器人是否已经在当前状态下审查过此 PR。通过在现有审查列表中查找机器人的用户名来避免重复审查。

def should_review_pr(pr, bot_username):
    reviews = list(pr.get_reviews())

    # Check if bot already reviewed this PR
    bot_reviews = [
        r for r in reviews
        if r.user and r.user.login == bot_username
    ]

    if not bot_reviews:
        return True  # No previous review, proceed

    # Check if new commits were pushed since last review
    last_review_time = max(r.submitted_at for r in bot_reviews)
    commits = list(pr.get_commits())
    latest_commit_time = commits[-1].commit.author.date if commits else None

    if latest_commit_time and latest_commit_time > last_review_time:
        print(f'New commits since last review, re-reviewing...')
        return True

    print(f'PR #{pr.number} already reviewed by {bot_username}')
    return False

# --- demo: minimal stand-ins for PyGithub-style PR/review/commit objects ---
import datetime
from types import SimpleNamespace

class _FakeCommit:
    def __init__(self, date):
        self.commit = SimpleNamespace(author=SimpleNamespace(date=date))

class _FakeReview:
    def __init__(self, login, submitted_at):
        self.user = SimpleNamespace(login=login)
        self.submitted_at = submitted_at

class _FakePR:
    def __init__(self, number, reviews, commits):
        self.number = number
        self._reviews = reviews
        self._commits = commits
    def get_reviews(self):
        return self._reviews
    def get_commits(self):
        return self._commits

pr_never_reviewed = _FakePR(101, [], [_FakeCommit(datetime.datetime(2026, 8, 1))])
print('PR #101 needs review:', should_review_pr(pr_never_reviewed, 'coddy-bot'))

review_time = datetime.datetime(2026, 8, 1, 10, 0)
pr_up_to_date = _FakePR(102, [_FakeReview('coddy-bot', review_time)],
                        [_FakeCommit(review_time - datetime.timedelta(hours=1))])
print('PR #102 needs review:', should_review_pr(pr_up_to_date, 'coddy-bot'))

PR 审查安全扫描模式

一种常见的自动化审查模式是扫描新增行中的安全反模式,例如硬编码的机密、使用已弃用的函数、SQL 字符串拼接或调用 eval()。请针对一组模式检查每个新增行,并将发现的问题标记为行内评论。

import re

SECURITY_PATTERNS = [
    (r'eval\s*\(', 'Dangerous eval() call — code injection risk'),
    (r'os\.system\s*\(', 'os.system() is unsafe; use subprocess with a list'),
    (r'pickle\.loads?\s*\(', 'pickle.load is unsafe with untrusted data'),
    (r'execute\(.*%\s*', 'Possible SQL injection — use parameterized queries'),
    (r'md5\s*\(|hashlib\.md5', 'MD5 is cryptographically broken for passwords; use bcrypt'),
]

def scan_patch_for_security(filename, patch):
    if not patch:
        return []
    comments = []
    new_line = 0
    for line in patch.split('\n'):
        if line.startswith('@@'):
            import re as _re
            m = _re.search(r'\+([0-9]+)', line)
            if m:
                new_line = int(m.group(1))
        elif line.startswith('+') and not line.startswith('+++'):
            content = line[1:]
            for pattern, message in SECURITY_PATTERNS:
                if re.search(pattern, content):
                    comments.append({'path': filename, 'line': new_line,
                                     'body': f':rotating_light: **Security:** {message}'})
            new_line += 1
        elif not line.startswith('-'):
            new_line += 1
    return comments

# --- demo ---
patch = (
    '@@ -1,2 +1,3 @@\n'
    ' def run(cmd):\n'
    '+    os.system(cmd)\n'
    '+    hashlib.md5(cmd.encode())\n'
)
for c in scan_patch_for_security('run.py', patch):
    print(c)

快速检查:审查事件

请检验您对 PR 审查选项的理解。

自动化 PR 审查回顾

现在,您的智能体已经能够以编程方式审查拉取请求:

  • repo.get_pull(number) — 获取包含元数据的 PR 对象
  • pr.get_files() — 列出已更改的文件,其中包含 filename、patch、additions
  • pr.get_commits() — 检查提交消息和统计信息
  • pr.create_review(body, event, comments) — 提交审查,可选择包含行内评论
  • 行内评论格式:{'path': filename, 'line': N, 'body': markdown}
  • 对于一般观察使用 'COMMENT',对于阻塞性问题使用 'REQUEST_CHANGES'
  • 重新审查前请检查已有的审查结果,以避免重复
  • 将文件分析限制为 10 个文件、行内评论限制为 25 条,以符合 API 限制

常见问题解答

「自动化 PR 审查评论」课时是免费的吗?

是的 — 「自动化 PR 审查评论」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 AI Agents 课程的其余内容,请升级到 CoddyKit PRO。 AI Agents 课程共包含 4 节课。

「自动化 PR 审查评论」这节课中我会学到什么?

读取 PR 差异,并通过 API 发布行内审查评论。 你通过在浏览器中直接运行的动手代码来练习 AI Agents,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 AI Agents 需要有经验吗?

无需任何先前经验。CoddyKit 上的 AI Agents 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。

「自动化 PR 审查评论」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 AI Agents 课中编写并运行代码吗?

能。每节 AI Agents 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. GitHub REST API 概览
  2. 列出与管理问题
  3. 自动化 PR 审查评论
  4. 提交历史与差异分析
← 返回 AI Agents