DEV Community

Riley Zhu
Riley Zhu

Posted on

How to Feed a Pull Request to an LLM: Four Diff-Packing Strategies, Compared

How a pull request diff is packed into an LLM prompt affects review cost, coverage, and reliability more than the model choice does. A comparison of four packing strategies — single-shot, per-file chunking, retrieval-based selection, and map-reduce summarization — shows that no single approach dominates across all PR shapes. The right strategy depends on PR size, token budget, and how much a missed bug costs the team.

The Packing Problem

An AI review bot has to fit an unbounded diff into a bounded context window. A PR touching forty files can easily exceed the token budget of a cheap model. The naive answer — paste the whole diff and hope — produces either quiet truncation or a large bill. Packing is the layer that decides which parts of the diff the model actually sees, and it deserves the same design attention as the prompt itself.

The four strategies below represent the common patterns found in open-source review bots and agent frameworks. Each one trades off token cost, context completeness, and latency differently.

Strategy A: Single-Shot

The entire diff goes into one prompt, and the model reviews everything in a single pass. This is the simplest strategy and the one most tutorials show.

def pack_single_shot(diff: str) -> str:
    return (
        'Review this pull request diff. '
        'Report bugs, style issues, and security problems.\n\n'
        f'```
{% endraw %}
diff\n{diff}\n
{% raw %}
```'
    )
Enter fullscreen mode Exit fullscreen mode

Single-shot preserves cross-file context, which matters when a bug only appears because two files changed together. Its weakness is the hard ceiling: once the diff outgrows the context window, the strategy either fails or drops the tail.

Strategy B: Per-File Chunking

The diff is split at file boundaries, each file is sent as its own review call, and the results are merged. This is the strategy most CI bots adopt after their first truncation incident.

def split_by_file(diff: str):
    files = []
    current = []
    current_path = None
    for line in diff.splitlines(keepends=True):
        if line.startswith('diff --git '):
            if current:
                files.append((current_path, ''.join(current)))
            current = [line]
            current_path = line.split(' b/')[-1].strip()
        else:
            current.append(line)
    if current:
        files.append((current_path, ''.join(current)))
    return files

def pack_per_file(files):
    return [
        f'Review this file for bugs and security issues.\n### {path}\n```
{% endraw %}
diff\n{patch}\n
{% raw %}
```'
        for path, patch in files
    ]
Enter fullscreen mode Exit fullscreen mode

Per-file chunking removes the size ceiling, because each call only needs to fit one file. The cost is lost cross-file context: a rename that breaks an import, or a change in one file that invalidates an assumption in another, will not be caught.

Strategy C: Retrieval-Based Selection

Instead of reviewing everything, the bot embeds the PR description and the first lines of each file, then picks the top-K files most related to the change. This strategy is for monorepos where a PR touches dozens of files but only a few are semantically relevant.

def select_files(pr_description: str, files, top_k=8):
    # Pseudocode: swap in your preferred embedding API.
    desc_vec = embed(pr_description)
    scored = []
    for path, patch in files:
        score = cosine(desc_vec, embed(path + '\n' + patch[:2000]))
        scored.append((score, path, patch))
    scored.sort(reverse=True)
    return scored[:top_k]
Enter fullscreen mode Exit fullscreen mode

Retrieval keeps token cost flat regardless of PR size, which makes it attractive for very large diffs. The risk is that the embedding model misses a file that matters for reasons the description does not mention.

Strategy D: Map-Reduce Summarization

Each file is summarized in a first pass, and a second pass reviews the combined summaries. This is the pattern used by document-QA pipelines and long-context agents.

def map_reduce(files, summarize_fn, review_fn):
    summaries = [summarize_fn(path, patch) for path, patch in files]
    return review_fn('\n\n'.join(summaries))
Enter fullscreen mode Exit fullscreen mode

Map-reduce handles arbitrarily large PRs and produces a coherent final review. It pays a double latency cost, and it can lose the line-level detail that makes code review useful: a summary of a buggy function often omits the exact line where the bug lives.

The Comparison Harness

To compare the four strategies on equal footing, the team built a small harness that runs each strategy against a seeded-bug corpus and reports four metrics: token cost, review coverage, latency, and bug catch rate.

import json, time

def benchmark(strategies, pr_diff, pr_description, seeded_bugs, review_fn):
    results = []
    for name, pack_fn in strategies.items():
        start = time.time()
        packed = pack_fn(pr_diff, pr_description)
        output = review_fn(packed)
        elapsed = time.time() - start
        results.append({
            'strategy': name,
            'latency_s': round(elapsed, 2),
            'tokens': estimate_tokens(packed),
            'caught': sum(1 for b in seeded_bugs if b in output),
        })
    return results
Enter fullscreen mode Exit fullscreen mode

The harness expects a review_fn that wraps the model call, so it works with any provider. The seeded-bug corpus is a set of known defect strings injected into a fixture repository, which makes the catch rate measurable instead of vibes-based.

The Decision Table

The comparison does not produce a single winner; it produces a decision table. The table below is the one the team now uses when wiring a new repository into the bot.

Condition Recommended Strategy
PR touches 5 files or fewer A: single-shot
PR touches more than 15 files, budget is tight B: per-file chunking
Monorepo, PR touches 30+ files, few are relevant C: retrieval selection
Very large PR, cross-file summary needed D: map-reduce
Token budget is the hard constraint B with a per-file token cap

The pattern behind the table is simple: small diffs get the simplest strategy, and each escalation trades away context for bounded cost. Teams that pick one strategy for every PR end up either overpaying or missing bugs.

Where the Free Tier Fits

The benchmark ran on the open-source MonkeyCode project's free server option, using its free model access and a free allowance of 10 million tokens. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The free tier mattered because a four-strategy comparison multiplies token usage by four, and running that on a paid API would have turned the experiment into a budget decision instead of a technical one.

Limitations

The harness uses a character-based token estimate, which is accurate enough for comparison but not for billing. The seeded bugs are synthetic, so the catch rate is a lower bound for real-world usefulness, not a promise. The results also depend on the model and the repository, which means the decision table is a starting point rather than a law.

Who Should Skip This

Teams with consistently small PRs should just use single-shot and spend their time elsewhere. Teams that need whole-repository reasoning, where the answer depends on code outside the diff, should use a retrieval-augmented agent instead of any packing strategy. Teams with strict data policies should check whether sending diffs to a hosted model is allowed at all before optimizing the packing layer.

The Takeaway

Diff packing is a tunable parameter, not a fixed implementation detail, and the four strategies here form a spectrum from cheapest to most context-aware. The harness in this article runs against any git repository, and the free tier is enough to try all four strategies on a real PR. The numbers will differ from the table above, and that difference is exactly the information the team needs to make the right call.

Top comments (0)