Module 2: Automated Code Review on PRs

Handling Large PRs with Chunking

Handling Large PRs with Chunking

Overview

Your bot works perfectly on typical PRs (5-20 files). But sooner or later the large PR arrives: 50, 80, 200 files. What does the bot do now?

Three bad options:

  1. Pass everything to the model → saturated context window, degraded quality, exploding costs
  2. Truncate arbitrarily → partial review with no transparency, the important issues are left out
  3. Skip silently → the team gets no feedback, exactly when it needs it most

This capsule covers the right path: smart chunking. You'll learn to split the PR into chunks that do fit, prioritize which chunks to review, aggregate results from multiple chunks into a single coherent review, and communicate transparently when a PR exceeds the limits.

By the end, your bot will handle PRs of any size with consistent quality — or explicitly say "this PR is too large for automatic review" when appropriate.


When to Chunk

Define clear thresholds before implementing:

SMALL PR (default review in 1 pass):
- ≤20 relevant files
- ≤2,000 lines of diff
- Estimated cost: $0.01-0.03

MEDIUM PR (chunked review):
- 21-50 relevant files
- 2,001-8,000 lines of diff
- Estimated cost: $0.05-0.15
- Strategy: chunking by file or by feature

LARGE PR (partial review + warning):
- 51-100 relevant files
- 8,001-20,000 lines of diff
- Estimated cost: $0.20-0.50
- Strategy: chunking + aggressive prioritization, mark as
  "partial review" in the summary

PR TOO LARGE (skip with a message):
- >100 relevant files
- >20,000 lines of diff
- Strategy: post a comment explaining, suggest splitting the PR

The thresholds are adjustable based on the model and budget. With Haiku, you can be more permissive; with Sonnet/Opus, more conservative.


Strategy 1: Chunking by File

The simplest: review one file per call to the model.

"""Review per file."""
import json
from anthropic import Anthropic

client = Anthropic()

def review_single_file(file_data: dict, conventions: str) -> dict:
    """Review of a single file. Returns {summary, comments}."""
    patch = file_data.get("patch", "")
    if not patch:
        return {"summary": "", "comments": []}
    
    prompt = f"""Review this file from the PR.

CONVENTIONS:
{conventions}

FILE: {file_data['filename']}
DIFF:

{patch}


Return JSON with:
{{
  "summary": "1 sentence with the file's verdict",
  "comments": [
    {{"line": N, "severity": "critical|warning|suggestion", "body": "..."}}
  ]
}}
"""
    response = client.messages.create(
        model="claude-haiku-4-5",
        max_tokens=2000,
        messages=[{"role": "user", "content": prompt}],
    )
    
    text = response.content[0].text.strip()
    # Clean up code fences
    if text.startswith("```"):
        text = "\n".join(text.split("\n")[1:-1])
    
    try:
        return json.loads(text)
    except json.JSONDecodeError:
        return {"summary": "Error parsing review", "comments": []}

# Review all the relevant files
all_results = []
for f in relevant_files:
    result = review_single_file(f, conventions)
    # Add the path to each comment
    for c in result["comments"]:
        c["path"] = f["filename"]
    all_results.append({
        "filename": f["filename"],
        "summary": result["summary"],
        "comments": result["comments"],
    })

Advantages:

  • Each call is small → roomy context window, high quality
  • Parallelizable (with care for rate limits)
  • Predictable costs (linear with files)

Disadvantages:

  • The model doesn't see how the files relate to each other
  • Cross-file issues (e.g. a change in one file breaks an assumption in another) are lost
  • More API calls = more latency (unless you parallelize)

Strategy 2: Chunking by Feature

For medium PRs where the files are related, group by feature:

def group_by_feature(files: list) -> dict:
    """Group files by the feature inferred from the path."""
    groups = {}
    for f in files:
        # Heuristic: first significant dir
        parts = f["filename"].split("/")
        if len(parts) > 2:
            feature = f"{parts[0]}/{parts[1]}"  # e.g. "src/payments"
        else:
            feature = parts[0]
        groups.setdefault(feature, []).append(f)
    return groups

groups = group_by_feature(relevant_files)

for feature, files_in_feature in groups.items():
    print(f"Reviewing feature: {feature} ({len(files_in_feature)} files)")
    review = review_feature(files_in_feature, conventions, feature)
    all_results.append(review)

The review_feature function: combines the patches of the group's files and passes them together. The model sees the feature's complete context.

When to use: PRs that touch several distinct areas. Each area is reviewed separately, each one with its related files together.


Strategy 3: Prioritization + Top N

For large PRs, you can't review all the files. Prioritize the most important ones:

def prioritize_files(files: list, max_files: int = 30) -> list:
    """Return the top N most relevant files."""
    
    def priority_score(f):
        score = 0
        
        # More changes = more relevant
        score += f["changes"] * 1
        
        # Critical files weigh more
        critical_paths = ["payments", "auth", "security", "database"]
        if any(p in f["filename"] for p in critical_paths):
            score += 100
        
        # New files weigh more (more surface for new bugs)
        if f["status"] == "added":
            score += 50
        
        # Tests weigh less (it's a debatable decision)
        if "test" in f["filename"]:
            score *= 0.5
        
        return score
    
    sorted_files = sorted(files, key=priority_score, reverse=True)
    return sorted_files[:max_files]

prioritized = prioritize_files(relevant_files, max_files=30)

Common heuristics for priority_score:

  • Amount of changes (modified lines)
  • Critical path (auth, payments, security, etc.)
  • Status (added > modified > renamed)
  • File type (source code > tests > config)
  • File size (small changes in large files are risky)

Aggregating Multi-Chunk Results

After reviewing all the chunks, you need a single review published on the PR (not several).

def aggregate_reviews(per_chunk_results: list) -> dict:
    """Combine chunk reviews into one."""
    
    # Flatten all the comments
    all_comments = []
    for r in per_chunk_results:
        all_comments.extend(r["comments"])
    
    # Count by severity
    by_severity = {"critical": 0, "warning": 0, "suggestion": 0}
    for c in all_comments:
        by_severity[c.get("severity", "suggestion")] += 1
    
    # Aggregated summary: combine summaries by feature
    feature_summaries = "\n".join(
        f"- **{r['filename']}**: {r['summary']}"
        for r in per_chunk_results
        if r.get("summary")
    )
    
    overall = f"""# 🤖 Code Review (Claude Code)

This PR was reviewed in **{len(per_chunk_results)} chunks** due to its size.

## Summary by area

{feature_summaries}

## Total severity

- 🚨 Critical: {by_severity['critical']}
- ⚠️ Warning: {by_severity['warning']}
- 💡 Suggestion: {by_severity['suggestion']}

Details inline in each file.
"""
    
    return {
        "summary": overall,
        "comments": all_comments,
    }

aggregated = aggregate_reviews(all_results)

Now aggregated has the review API's structure and can be published as a single review.


Edge Case: PR Too Large

When the PR exceeds the thresholds (>100 files, >20K lines), reviewing it automatically doesn't add value proportional to the cost. Better: communicate transparently.

def post_too_large_message(pr_number, repo, token, file_count, line_count):
    """Post a comment to the PR explaining why it isn't reviewed."""
    body = f"""# 🤖 Code Review

This PR has **{file_count} files** and **{line_count} lines** modified — it exceeds the limits for automatic review.

## Suggestions

1. **Consider splitting the PR** into smaller changes. Large PRs are hard to review (human and automated).
2. If the PR is necessarily large (e.g. a massive migration), request a manual review from a human on the team.
3. If you want automated partial review of critical files, add the `review-critical-only` label.

## To enable partial review

Add the `review-critical-only` label and the files in critical paths (auth, payments, etc.) will be reviewed automatically.
"""
    
    url = f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments"
    headers = {"Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json"}
    requests.post(url, headers=headers, json={"body": body})

Result: transparency. The team knows why it didn't get an automatic review and has options (split the PR, manual review, a label for partial review).


Integrated Implementation

Combining everything in the main script:

"""Main script: review with smart chunking."""
import os
import requests

# 1. Get the PR's files
files = get_pr_files()  # function from capsule 02
relevant_files = filter_relevant(files)

file_count = len(relevant_files)
total_changes = sum(f["changes"] for f in relevant_files)

# 2. Decide the strategy based on size
if file_count == 0:
    print("No relevant files.")
    sys.exit(0)

elif file_count <= 20 and total_changes <= 2000:
    # Small PR: review in 1 pass
    print(f"Small PR ({file_count} files). Single review.")
    review = review_combined(relevant_files, conventions)

elif file_count <= 50 and total_changes <= 8000:
    # Medium PR: chunking by feature
    print(f"Medium PR ({file_count} files). Chunking by feature.")
    groups = group_by_feature(relevant_files)
    per_chunk_results = [review_feature(files_g, conventions, name) 
                         for name, files_g in groups.items()]
    review = aggregate_reviews(per_chunk_results)

elif file_count <= 100 and total_changes <= 20000:
    # Large PR: prioritization + top 30
    print(f"Large PR ({file_count} files). Prioritization + top 30.")
    prioritized = prioritize_files(relevant_files, max_files=30)
    per_chunk_results = [review_single_file(f, conventions) for f in prioritized]
    review = aggregate_reviews(per_chunk_results)
    
    # Mark as partial review
    review["summary"] = "**⚠️ Partial review** — top 30 files only.\n\n" + review["summary"]

else:
    # PR too large
    print(f"PR too large ({file_count} files, {total_changes} lines).")
    post_too_large_message(pr_number, repo, github_token, file_count, total_changes)
    sys.exit(0)

# 3. Validate and publish the review
valid_comments = validate_all(review["comments"], files)
publish_review(pr_number, repo, github_token, commit_sha, review["summary"], valid_comments)

Result: a script that scales gracefully from small to giant PRs, with appropriate feedback in each case.


Additional Optimizations

Parallelization with care

If you review files in parallel, respect Anthropic's rate limits:

import concurrent.futures
from anthropic import Anthropic

client = Anthropic()

def review_file_safe(file_data, conventions):
    """Wrapper with retry."""
    for attempt in range(3):
        try:
            return review_single_file(file_data, conventions)
        except APIError as e:
            if e.status_code == 429:  # rate limit
                time.sleep(2 ** attempt)  # exponential backoff
            else:
                raise
    return {"summary": "Failed after retries", "comments": []}

# Parallelize with care: max 5 concurrent
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
    results = list(executor.map(
        lambda f: review_file_safe(f, conventions),
        prioritized,
    ))

Benefit: a review of 30 files goes from ~5 minutes to ~1 minute. Risk: if you exceed the rate limit, everything fails.

Caching results

If the PR didn't change on some lines but there was a push, you can cache previous reviews by commit SHA + file. It's not trivial but it saves calls on re-runs.


Common Pitfalls

Error 1: Always passing the complete PR

Symptom: On large PRs, review quality visibly degrades. The bot gives generic comments or contradicts itself.

Why it happens: Saturated context window.

How to fix it: Implement thresholds and chunking. The 70%-of-the-context-window rule applies here.

Error 2: Chunking without aggregation

Symptom: The bot posts N separate reviews (one per chunk) on the same PR.

Why it happens: The script publishes each chunk immediately instead of accumulating and aggregating.

How to fix it: Accumulate the results and publish a single review at the end with aggregate_reviews.

Error 3: Opaque prioritization

Symptom: The bot reviewed 30 files. Which 30 of the 80? The team doesn't know.

Why it happens: The summary doesn't communicate what was reviewed vs what wasn't.

How to fix it: In the partial review's summary, list explicitly: "30 priority files were reviewed. Not reviewed: [list of the remaining 50 with a brief reason]".

Error 4: Not respecting rate limits when parallelizing

Symptom: Reviews fail with 429 when the PR is large.

Why it happens: ThreadPoolExecutor with max_workers=20 launches 20 simultaneous calls. Anthropic rate-limits.

How to fix it: max_workers=5 is typically safe. With exponential backoff in case of 429.

Error 5: Skipping silently

Symptom: The bot doesn't comment on large PRs. The team doesn't know if it's a bug or intentional.

Why it happens: The script does sys.exit(0) without commenting.

How to fix it: Always post a comment explaining why it wasn't reviewed. Transparency over silence.


Diagnosis

Question 1: Do you have clear thresholds for "small/medium/large/too large PR"?

If not, you'll make inconsistent decisions. Define thresholds based on your model and budget.

Question 2: Does your chunking publish a single review or several separate ones?

Several separate ones is noise. Aggregating and publishing one is the practice.

Question 3: When you prioritize, do you communicate which files were omitted?

Without communicating it, the team doesn't know how complete the review is.

Question 4: Do you parallelize reviews respecting rate limits?

If you parallelize without a limit, you'll run into 429s. max_workers=5 + backoff is reasonable.

Question 5: When a PR is too large, do you post an explanatory message or skip silently?

Silence = bad UX. An explanatory message + suggestions = professional.


Exercises

Exercise 1: Implement thresholds (Easy)

Define the 4 thresholds (small, medium, large, too large) in your script with values adjusted to your usage. Document them in CLAUDE.md or the workflow's README.

Exercise 2: Chunking by file with aggregation (Medium)

Implement:

  1. A review_single_file function that reviews one file
  2. A loop over the prioritized files
  3. An aggregate_reviews function that combines results
  4. Publishing a single review at the end
See checklist
  • Each file is reviewed in a separate API call
  • Each comment has the correct path
  • The aggregated summary lists results by file/feature
  • Only one review is published (not N)

Exercise 3: Prioritization with a score function (Hard)

Implement priority_score that considers:

  1. Amount of changes
  2. Critical path (based on a configurable list)
  3. File status (added > modified)
  4. Absolute file size

Verify with a test PR that the critical files end up at the top.


Summary

  • Clear thresholds avoid ad-hoc decisions on PRs of varying size
  • 4 strategies based on size: single review, chunking by feature, prioritization + top N, explanatory message
  • Aggregating results into a single review avoids noise on the PR
  • Transparent prioritization communicates what was reviewed and what was omitted
  • Parallelization with care speeds up large reviews without hitting rate limits
  • An explicit message on PRs that are too large maintains transparency

Next capsule: Module 3 — GitLab CI/CD and Headless SDK. Your bot works in GitHub Actions, handles small and large PRs, operates with the team's conventions. The next natural question: what happens if your team (or your client) uses GitLab? The headless SDK is the answer — you write the logic once, it runs on any platform.


Additional Resources

  1. Anthropic Rate Limits — Limits per tier
  2. Python concurrent.futures — Safe parallelization
  3. GitHub PR Files endpoint — Up to 3000 files per PR
  4. Effective batch processing with LLMs — Anthropic batch API for advanced cases
  5. Code review at Google: large CL — How Google handles large PRs (conceptual reference)
  6. The 200-line rule — An argument about the ideal size of PRs