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:
- Pass everything to the model → saturated context window, degraded quality, exploding costs
- Truncate arbitrarily → partial review with no transparency, the important issues are left out
- 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:
- A
review_single_filefunction that reviews one file - A loop over the prioritized files
- An
aggregate_reviewsfunction that combines results - 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:
- Amount of changes
- Critical path (based on a configurable list)
- File status (
added>modified) - 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
- Anthropic Rate Limits — Limits per tier
- Python concurrent.futures — Safe parallelization
- GitHub PR Files endpoint — Up to 3000 files per PR
- Effective batch processing with LLMs — Anthropic batch API for advanced cases
- Code review at Google: large CL — How Google handles large PRs (conceptual reference)
- The 200-line rule — An argument about the ideal size of PRs