Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

AI System Design

Design an AI Code Review Bot

Learn AI system design by building an automated PR review bot like GitHub Copilot for PRs, CodeRabbit or Graphite step by step.

The numbers to beatconfidence scoreper findinghigh-severitysurfaces immediatelylow-confidencesuppressed or batched

The whole design, in writing

Every step of the build above, written out: the problem each piece solves, the option that was taken and the ones that were not, the numbers, and how it fails in production.

The big idea

What does a code review bot actually need to get right?

The obvious version is "send the diff to an LLM, post whatever it says as PR comments." Try that at real scale and you get a bot developers mute within a week: it repeats itself on every push, comments confidently on things it doesn't have context for, buries the one real bug under nitpicks, and occasionally quotes a leaked API key straight back into a public comment thread.

Developeropens a PR
New in this step: Developer.

Every one of those failures has a specific fix: run cheap, deterministic checks before any LLM call, scope review to the diff plus just enough context, retrieve repo-wide context the diff alone can't show, gate on confidence before posting, track what's already been said, and redact secrets. We'll build all of it.

What the new pieces do

Developerclient
Pushes a diff and expects useful, precise review comments — not 40 nitpicks and not silence on the one real bug.

Step 1 · Cheap checks first

Don't send everything to an LLM

A PR touches 30 files. Some issues — an unused import, a hardcoded credential pattern, a missing semicolon — are trivially, deterministically detectable. Should everything, including these, go through an LLM call?

Webhook / APIPR opened/updatedStatic Analyzerslinters, SAST
New in this step: Webhook / API, Static Analyzers.

An unused variable and a hardcoded secret pattern are both deterministically detectable. Route them through an LLM anyway?

  1. Using a slow, costly, probabilistic tool for something a linter catches perfectly and instantly is a strictly worse trade on speed, cost, AND reliability — "simpler architecture" isn't worth being worse at the job.

  2. Static analysis tools are precise and instantaneous for their class of problems (unused code, known-bad patterns, style). Running them first means the LLM never has to (and never should try to) rediscover what a linter already knows with certainty — its budget goes entirely toward the semantic reasoning only it can do.

  3. An LLM asked to also catch every stylistic/lint-level issue is doing redundant, error-prone work on problems that have a perfectly reliable, instant, free existing solution — a wasted opportunity and a reliability regression on the "easy" findings.

Run static analyzers and linters first — deterministic, instant, and precise for their class of issues. Only what genuinely requires understanding intent (does this logic actually do what the PR claims, is this the right approach) needs to reach the LLM at all.

Why this piece earns its place

A linter answers a different question than review asks. It reports the state of a file; review asks what this PR introduced. So run the analyzers on the merge base as well as the head and post the difference — not the head's findings filtered by location. The second run is the part people skip, and it is what saves you when code moves: a function lifted wholesale into a new file arrives inside the diff carrying every long-standing violation it already had, and a location filter alone hands all of them to this author as new work. Everything the second run cancels out is a backlog item, not review. Two operational details follow. Use the repository's own analyzer config rather than your defaults, or the bot spends its first week arguing with a style the team already settled. And distinguish no findings from did not run: an analyzer that dies on a half-finished refactor exits with an empty result, which looks exactly like a clean file, and a whole PR silently loses its cheapest coverage with nothing in any dashboard to say so. Pin analyzer versions per repo too — a toolchain upgrade that rewrites the finding set overnight arrives as a flood of comments on PRs that changed nothing.

What the new pieces do

Webhook / APIgateway
Receives the PR webhook, kicks off review, and is the single point that decides what actually gets posted back.
Static Analyzersservice
Fast, deterministic, precise checks — unused variables, style, known-bad security patterns — that never need an LLM call.

Step 2 · Diff-scoped, not file-scoped

Review the change, with just enough context

A file is 800 lines; a PR changes 12 of them. Should the LLM see the whole file every time, or just the diff?

Webhook / APIPR opened/updatedDiff + Context Assemblerscoped to the change
New in this step: Diff + Context Assembler.

Scope review to the diff plus surrounding context — the changed lines, a reasonable window of unchanged code around them for coherence, but not the entire file (let alone the entire repo). This keeps every review focused on what actually changed (comments on 770 untouched lines waste tokens and, worse, invite the bot to comment on pre-existing code that wasn't part of this PR at all), and keeps the token/cost budget proportional to the size of the change, not the size of the file.

Why this piece earns its place

The window is the part that gets built wrong. A fixed number of lines either side of a hunk cuts the enclosing function in half, and a model handed half a function invents the other half — expand to syntactic boundaries instead, with the same parser you already want for chunking, so the unit is a whole function or class body. Include the removed lines, not only the new side of the diff: a review that sees just the new state cannot notice that a bounds check disappeared, and a deletion is often the most valuable thing a diff has to show. Two hunks inside the same function should be assembled once, together, or the bot reviews the same code twice and contradicts itself. The second reason this step matters is downstream. The hunk is the addressing scheme for everything after it — a comment has to be posted against a file, a side and a line the code host will accept, and the feedback loop attributes a reaction back to that same location. Assemble context loosely here and every one of those coordinates is approximate.

What the new pieces do

Diff + Context Assemblerservice
Builds exactly what the LLM needs: the changed lines, enough surrounding code to make sense of them, and nothing from the rest of the file that wasn't touched.

Step 3 · Context the diff alone can't show

What about the function this diff calls?

The diff modifies a function's call site, passing an argument in a new order. The LLM can't judge whether that's correct without knowing the CALLED function's actual signature — which isn't in the diff at all, because that function wasn't touched.

Diff + Context Assembler+ repo-aware retrievalRepo Embedding Indexsymbols, conventions
New in this step: Repo Embedding Index.

The diff calls a function whose definition isn't in the diff. How does the LLM get that context?

  1. This is RAG applied to a codebase: rather than shipping the whole repo to the LLM (far too large, mostly irrelevant), retrieve exactly the symbols and context the diff touches — the called function's signature, a relevant type definition, the project's style guide — and include only that.

  2. Most real repos are far larger than any practical context window, and even where they'd technically fit, the vast majority of that content is irrelevant to this specific diff — an expensive, unfocused way to get a small amount of needed information.

  3. Ignoring cross-file context means the bot can never catch a whole class of real bugs — call-site mismatches, broken invariants across a call boundary — which are exactly the kind of subtle issue a good human reviewer would catch by checking the callee.

Maintain a Repo Embedding Index — an indexed, retrievable representation of the codebase's symbols, definitions, and conventions. When the diff references something outside itself (a called function, an imported type, a style guideline), the Context Assembler retrieves just that, giving the LLM the specific cross-file context it needs without shipping the entire repository.

Why this piece earns its place

Index at definition granularity — a function or type with its signature, its docstring and its path as one unit — rather than fixed windows of characters. A retrieved fragment of a function body is worse than retrieving nothing, because it looks like context and the model reasons from it with the same confidence. Then handle the miss, which is the failure nothing downstream will show you. Retrieval always returns its nearest candidates and never says it did not find the symbol you asked for, so a call site gets judged against a plausible wrong signature and the finding reads exactly as sure as one that is right. Return unresolved references explicitly, name them in the prompt, and let Step 4's gate treat a review built on a miss as a reason to hold the finding back. The other thing to say out loud is that this box is the one piece of standing state the design owns. Indexing a codebase is not a webhook-latency operation, so a repo's first PR arrives before its index does, and the index costs money on days nobody opens a PR at all. Decide up front that a missing or half-built index degrades to diff-only review rather than making a developer wait on it.

What the new pieces do

Repo Embedding Indexdata
A retrievable index of the whole codebase, so the LLM can pull in a called function's definition or a project convention it wasn't directly shown.

Step 4 · The semantic layer

What only an LLM can actually judge

With static checks done and context assembled, the LLM reviews for logic correctness, missed edge cases, naming, and whether the change actually does what the PR description claims. Should every single thing it flags get posted as a comment?

Webhook / APIDiff + Context AssemblerReview LLM
New in this step: Review LLM. · swipe to pan the diagram

The LLM flags a dozen potential issues, from "this could theoretically be null" to a clear logic bug. Post all of them?

  1. A dozen comments per PR, many of them low-confidence nitpicks, is exactly how developers learn to ignore (or mute) a review bot entirely — volume without precision destroys the trust the bot depends on to be useful at all.

  2. Not every LLM-generated observation is worth interrupting a developer for. Attaching a confidence/severity score to each finding and gating what actually gets posted keeps the bot's signal-to-noise ratio high, which is the difference between a tool developers trust and one they mute.

  3. Artificially capping at one comment would suppress genuine, distinct issues just as readily as it suppresses noise — the goal is filtering by QUALITY of the finding, not an arbitrary count.

Have the LLM attach a confidence and severity score to each finding, and gate posting on that score — clear logic bugs and security issues surface immediately; speculative or stylistic nitpicks are suppressed or batched into a single low-priority summary rather than posted as individual interruptions.

Why this piece earns its place

A model's self-reported confidence is not a probability. Ask for one and the numbers cluster in a narrow band and rank poorly across different prompts, so treat it as an ordering, set the cut against a labelled sample per category, and keep the threshold in per-repo config rather than in the prompt — a number you can move without re-testing how the model behaves. Cheaper corroboration beats asking the model twice. Require every finding to resolve, in two separate checks: the code it quotes has to appear somewhere in what was actually sent — the hunk, the window around it, or a retrieved definition — and the line it comments on has to be one this PR changed. Conflating those two throws away exactly the cross-file catches Step 3 exists to produce, because a finding about a callee's signature is evidence and the comment still lands on the call site. A finding quoting code nobody sent resolves to neither, and dropping it before scoring is cheaper than any threshold. Then log the suppressed findings anyway, with their scores and their fate. Without that shadow record the bar can only ever ratchet upward, because you have no evidence about what you are not posting — and the day someone asks whether the bot would have caught a bug that shipped, the answer is in that log or nowhere.

  • confidence scoreper finding
  • high-severitysurfaces immediately
  • low-confidencesuppressed or batched

What the new pieces do

Review LLMservice
Reasons about logic, naming, and missed edge cases — the layer static analyzers structurally can't do, since it requires understanding intent.

Step 5 · Never repeat yourself

A re-push shouldn't repost old comments

A developer pushes a fix, then pushes again to address review feedback, then again for a typo. Each push re-triggers review. Should each run repost everything it finds, including issues already flagged (and possibly already fixed) on an earlier push?

DeveloperWebhook / APIDiff + Context AssemblerComment Ledger
New in this step: Comment Ledger. · swipe to pan the diagram

A PR gets pushed 5 times. Should each push's review repost all previously-flagged issues again?

  1. Reposting identical comments on every push is exactly the comment-spam pattern that erodes trust — "nothing missed" doesn't require repeating what was already clearly said once.

  2. Stopping review after the first push means genuinely NEW issues introduced by later pushes (a fix that itself introduces a bug) go completely uncaught — the bot needs to keep reviewing, just not keep repeating itself.

  3. A ledger of "what has already been said, about what specific piece of code" lets each new review run diff against its own prior findings: already-flagged-and-unresolved issues stay silent (they were already said once), resolved issues can even be acknowledged, and only genuinely new findings from the latest push get posted.

Maintain a Comment Ledger keyed by (finding, diff-hunk) that records exactly what's already been posted. Every new review run checks against it: a previously-flagged, still-unresolved issue isn't reposted; a resolved one can be quietly closed; only genuinely new findings from the latest push generate a new comment.

Why this piece earns its place

Everything here rests on the key, and the obvious keys are all wrong. Comment text changes between runs because the model rewords itself. Line numbers move when anything above them is edited. Commit SHAs disappear on a force-push, and a rebase — routine on a long-lived PR — rewrites all of them at once, so a ledger keyed on SHA forgets everything it knew and reposts the lot in one go. What survives is a fingerprint: category, file path, and a hash of the normalized flagged code with whitespace and formatting stripped, so the same issue on code that merely moved is still the same issue. Then let the write make the decision. Two pushes seconds apart start two review runs that both read an empty ledger and both post, which surfaces as an occasional duplicate nobody can reproduce on demand. A unique constraint on the key, claimed before the comment is sent, makes that race harmless. It buys cancellation too: a run that finds a newer push already recorded can stop rather than finish work no one will read.

What the new pieces do

Comment Ledgerdata
Tracks exactly which finding, on which diff-hunk, has already been posted — so a re-push only surfaces genuinely new issues.

Step 6 · Handle secrets carefully

Don't leak what you're flagging

A diff accidentally includes a real API key. The bot should flag it — but sending that raw secret up to an external LLM provider as part of "look what I found," and then quoting it back verbatim in a public PR comment, would make the leak WORSE, not better.

DeveloperWebhook / APIStatic AnalyzersDiff + Context AssemblerRepo Embedding IndexReview LLMComment Ledger
The system as it stands at this step. · swipe to pan the diagram

Detect likely secrets with a pattern-based scanner (part of the deterministic Step 1 layer) before anything reaches the LLM, and redact the actual value in both the LLM request and the posted comment — flag "a credential-shaped string was found on line 42" without ever repeating the credential itself anywhere in the pipeline, including in logs.

Why this piece earns its place

Redaction belongs where the scanner already is. The Static Analyzers are the first thing to touch a diff — before any context is assembled, before a byte of it leaves the building — so that is where the matched value gets swapped for a stable placeholder, once, in the text every later stage reads. Scrub the outbound comment instead and the value has already been through the prompt, the retry, the request trace and the crash dump, and now you are hunting it in four places at once. Two things follow that people learn the hard way. A detected secret is already leaked: it is in the git history, and deleting the line in the next commit does not unpublish it. The comment's real job is therefore rotate this credential, not remove this line, and saying the second one leaves the developer believing they have fixed something they have not. And detectors fire on test fixtures, base64 blobs and deliberately fake example keys, so each repo needs an allowlist — which costs more here than ordinary noise, because every redacted region is code the model then reviews blind. Over-redact a file and the bot is reasoning about a hole in it, with no drop in how sure it sounds.

Step 7 · Learn from developer reactions

Fewer repeat false positives over time

Developers thumbs-down a category of comment repeatedly — say, a particular style suggestion this team has explicitly decided against. Should the bot keep making the same suggestion forever?

DeveloperWebhook / APIDiff + Context AssemblerRepo Embedding IndexReview LLMComment LedgerDev Feedback
New in this step: Dev Feedback. · swipe to pan the diagram

Feed 👍/👎 reactions and explicit dismissals back into per-repo confidence thresholds — a category of finding that's consistently downvoted on a given repo gets its posting bar raised (or is suppressed outright) for that repo specifically, without affecting other repos where the same finding might be genuinely valued. This is a lightweight feedback loop, not a full retraining pipeline: threshold and prompt adjustments, scoped per repository.

Why this piece earns its place

The signal is sparse and biased, and treating it as clean data is how these loops go wrong. Silence is the normal outcome and it means nothing — the comment may have been right and quietly acted on. The stronger signal is free and already in the repository: did the flagged lines change before the PR merged? It is noisier than it sounds — lines move for reasons that have nothing to do with the comment — but it arrives for every comment rather than for the handful that earn a reaction, so pool it across a whole category and never read it as a verdict on one finding. Second, this loop can starve itself. Raise the bar on a category and you stop posting it, so you stop collecting evidence about it, and it can never earn its way back. Keep Step 4's suppressed-findings log as the offline record, or keep posting a thin sample below the bar, so there is something to re-evaluate against. Third, bound what feedback is allowed to move. Per-repo thresholds exist so one team's taste stays local — but a high-severity security category must not be tunable down to silence by a few irritated clicks on a Friday. Feedback adjusts the nitpick bar; it does not get a vote on a credential.

What the new pieces do

Dev Feedbackevents
Reactions on posted comments feed back into per-repo confidence thresholds, quietly reducing repeat false positives over time.

Step 8 · The sharp edges

The 5,000-line PR

Most PRs are small. Occasionally one isn't — a large refactor or a vendored dependency update touches thousands of lines. Reviewing all of it with full LLM depth would be slow and expensive, and most of a huge mechanical diff is genuinely low-risk.

DeveloperWebhook / APIStatic AnalyzersDiff + Context AssemblerRepo Embedding IndexReview LLMComment LedgerDev Feedback
The system as it stands at this step. · swipe to pan the diagram

Apply a budget and prioritization strategy for large PRs: chunk the diff, prioritize files by risk signal (source code over generated/vendored files, files touching auth/payments/security-sensitive paths over config), and apply full LLM-depth review to the highest-priority chunks first within a fixed budget — falling back to static-analysis-only coverage for the long tail rather than either reviewing everything at full depth (too slow/expensive) or skipping huge PRs entirely (too risky).

Why this piece earns its place

Enforce the budget where the work is spawned, not as a wall-clock timeout on the whole job. A review killed at the deadline has usually already posted part of itself, and a half-posted review is worse than a late one: the ledger now records findings from a pass that never finished, so the rest is never said and nothing ever retries it. Decide the chunk list up front, post at the end, and let the ledger act as the resume point when a run dies. Then say what you did not review. A bot that silently covers a fraction of a large refactor reads, to the author, as full coverage and a clean bill of health — one line naming which paths got full-depth review and which got static-only is the difference between a tool you can trust and one that lies by omission. Last, isolate it: a single huge PR at full depth will occupy a repo's workers and delay the small PRs queued behind it. A per-repo concurrency cap keeps that from being the thing people actually notice, because the median stays fine while everyone else waits.

You did it

You just designed an AI code review bot.

DeveloperWebhook / APIStatic AnalyzersDiff + Context AssemblerRepo Embedding IndexReview LLMComment LedgerDev Feedback
The finished design, end to end. · swipe to pan the diagram

Everything you assembled, in order

  • Cheap, deterministic static analyzers run first — the LLM is reserved for what genuinely requires understanding intent.
  • Review is scoped to the diff plus just enough surrounding context, not the whole file or the whole repo.
  • A Repo Embedding Index (RAG over the codebase) supplies cross-file context — a called function's signature, a project convention — the diff alone can't show.
  • Every finding carries a confidence/severity score; only what clears the bar gets posted, keeping signal-to-noise high enough that developers don't mute the bot.
  • A Comment Ledger tracks what's already been said per diff-hunk, so repeated pushes to a PR surface only genuinely new findings.
  • Likely secrets are detected and redacted before anything reaches the LLM or a posted comment — the review itself must never become a new leak.
  • Developer 👍/👎 feedback tunes per-repo confidence thresholds, so repeat false positives quietly decrease over a team's first few weeks.
  • Large PRs get budgeted, risk-prioritized review rather than either unbounded cost or being skipped entirely.

Deep cut · 14:32

Right nine times in ten. Still shut out by Thursday.

The interactive build above lays out a code review bot: cheap checks first, a diff-scoped review, retrieval for the rest of the repo, a confidence and severity bar, and a ledger of what it already said. The film zooms into the one failure that sinks most of them: a bot that says too much. Greta, a clockwork harbour pilot, boards each ship leaving port and pins notes for the crew. On Monday she finds one real problem. By Thursday there are forty notes a push, the same thirty-nine again every time, the real one buried in the middle, and a photo of the strongroom key with its serial number readable by anyone on the quay. The crew pull the gangplank up in her face.

  • See the wreck: reading everything is expensive and saying everything is noise. Each note was fine alone. Together they bury the one that mattered, and the bot that was right nine times in ten catches nothing once nobody reads it.
  • Take it into the interview: run a linter and secret redaction before any model sees the code, review the diff plus its surrounding block rather than the whole repo, retrieve the rest on demand, only post a comment when confidence and severity clear a bar, and keep a ledger tied to the code itself so the same finding is never said twice. Then name the bill: each part costs time, money or a missed catch, and the confidence threshold trades noise against silence.

Where an interviewer pokes next

Getting the boxes right is the easy half. These are the questions that separate a candidate who drew the diagram from one who has run the thing. Answer each one out loud before you open it.

  1. Why bother with static analyzers at all if the LLM could plausibly catch the same issues?

    An LLM CAN often catch the same class of issue (an unused variable, an obvious SQL injection pattern), but "can, probabilistically, most of the time" is a strictly worse guarantee than "does, deterministically, every time" for issues that have an exactly correct answer — and it costs real latency and money to get that weaker guarantee. Running static analysis first isn't about capability, it's about using the right tool for issues that don't need judgment, so the LLM's limited, expensive attention goes entirely toward the class of problem — semantic, intent-level reasoning — that only it can actually do.

  2. How would the Comment Ledger handle a developer manually editing or deleting a bot comment?

    The ledger should treat a human dismissal (explicit "resolve" or deletion of a bot comment) as a strong signal distinct from the comment simply not being reposted — it's recorded as "a human actively disagreed with or resolved this," which both prevents it from ever resurfacing for that exact diff-hunk again AND feeds into the Step 7 feedback loop as a stronger downvote signal than a passive 👎 reaction would be. The key design point is that the ledger isn't just "have I said this" — it's also tracking the OUTCOME of having said it, which is what makes the feedback loop meaningful rather than just a repost-suppression mechanism.

  3. What happens when the Repo Embedding Index goes stale relative to the actual codebase (e.g. after a large unrelated merge)?

    A stale index risks retrieving an outdated version of a function signature or convention, which could lead the LLM to flag a call site as mismatched when it's actually correct against the CURRENT code — a false positive caused by index lag rather than a real issue. The practical fix is re-indexing incrementally on every merge to the base branch (not the full repo, just what changed), keeping index staleness bounded to roughly "since the last merge," and, for extra safety, having the Context Assembler fetch the live, current version of any specific file it's about to rely on heavily rather than trusting the index's cached content for load-bearing context.

  4. Should the confidence threshold for posting be the same for a security-related finding as for a style nitpick?

    No — this is exactly why severity and confidence are tracked as separate dimensions rather than one combined score. A HIGH-severity finding (a likely security vulnerability, a probable data-loss bug) is worth surfacing even at moderate confidence, because the cost of a false negative (missing a real vulnerability) is high and the cost of a false positive (a developer spends 30 seconds ruling it out) is low. A LOW-severity finding (a naming nitpick) should require much HIGHER confidence before posting, because the cost of being wrong (an unwelcome, low-value interruption) isn't offset by much upside even when right. The posting threshold should be a function of severity, not a single flat bar applied to everything.

  5. Could this same architecture extend to reviewing infrastructure-as-code (Terraform, Kubernetes manifests) instead of application code?

    Yes, and the architecture barely changes — the diff-scoping, static-analysis-first (policy-as-code tools like tfsec or OPA play the same role linters do), repo-context-retrieval, and confidence-gated commenting pattern all transfer directly. What changes is domain-specific: the Repo Embedding Index would need to understand infra-specific relationships (a resource reference, a module dependency) instead of function call sites, and severity scoring would weight differently (a public S3 bucket or an overly permissive IAM policy is high-severity in a way that has a different flavor than an application logic bug, but the SAME gating principle — surface high-severity findings at lower confidence thresholds — still applies).

Check yourself — the answers, and why

Nine steps in, these are the calls you should be able to make cold. Pick one, then read why.

  1. Deterministic static analyzers run before the LLM mainly because…

    Use the exactly-correct, near-free tool for what it can catch; reserve the expensive probabilistic model for judgment calls.

  2. Review is scoped to the diff plus surrounding context, not the whole file, because…

    Commenting on untouched pre-existing code is both wasted cost and an irrelevant, trust-eroding surprise for the developer.

  3. A Repo Embedding Index is used to…

    This is RAG applied to a codebase — retrieve exactly what's relevant, not everything.

  4. A Comment Ledger exists to…

    Without tracking what's already been said, comment volume scales with push count instead of actual issue count.

  5. A likely secret found in a diff should be…

    The review itself must never become a new place a leaked credential is exposed.

How you’d open this design in an interview

Before any boxes: agree what it must do, pin the qualities that shape everything, then build — naming each trade-off as you make it. The walkthrough above is that exact order.

What it must do

Agree on these before drawing a single box.

  • Static first: run deterministic linters/SAST on a PR before any LLM call.
  • Semantic review: reason about logic, naming and missed edge cases on the diff plus context.
  • Repo context: retrieve a called function’s signature or a project convention the diff doesn’t show.
  • Comment with precision: post only findings that clear a confidence/severity bar.
  • Never twice: a re-push surfaces only genuinely new findings, and secrets are redacted.

The qualities that shape everything

Each one names the mechanism that buys it.

Trivial issues caught instantly and for free
Deterministic static analyzers/linters run first, so the LLM never rediscovers what a linter already knows with certainty.
Cost proportional to the change, comments relevant
Scope review to the diff plus a window of surrounding context — not the whole file — so nothing comments on untouched pre-existing code.
Judge cross-file correctness
A repo embedding index (RAG over the codebase) retrieves just the referenced signatures, types and conventions the diff itself doesn’t contain.
High signal-to-noise so developers don’t mute it
The LLM scores each finding by confidence and severity, and only what clears the bar is posted; nitpicks are suppressed or batched.
Comment volume tracks issues, not push count
A comment ledger keyed by (finding, diff-hunk) records what’s been posted, so a re-push surfaces only genuinely new findings.
The review never becomes a new leak
Pattern-detect likely secrets in the deterministic layer and redact the value before it reaches the LLM, a comment, or logs.

The trade-offs you say out loud

Senior signal isn’t the boxes — it’s naming what you gave up and why it was the right price.

Deterministic static checks first over a single unified LLM pass

Using a slow, costly, probabilistic model for what a linter catches perfectly and instantly is worse on speed, cost and reliability. Run static analysis first so the LLM’s budget goes entirely to the semantic reasoning only it can do.

Retrieving from a repo index (RAG) over shipping the entire repository as context

Most repos exceed any practical context window and are mostly irrelevant to one diff. Retrieving just the referenced signatures, types and conventions gives the LLM the cross-file context it needs without dumping everything.

Confidence/severity gating over posting everything the LLM flags

A dozen low-confidence nitpicks per PR is exactly how developers learn to mute a review bot — volume without precision destroys trust. Scoring findings and surfacing only what clears the bar keeps signal-to-noise high.

A comment ledger per diff-hunk over reposting every finding on each push

Reposting identical comments on every push is the spam pattern that erodes trust, and stopping after the first push misses new bugs. A ledger keyed by (finding, hunk) surfaces only genuinely new findings, so volume tracks issues not push count.

Redacting secrets before the LLM over quoting the flagged credential back

Sending a raw secret to an external LLM and echoing it into a public comment thread makes the leak worse. Detect and redact at detection time, upstream of every consumer including logs — flag “a credential-shaped string on line 42”, never the value.

The answer, out loud

What a strong answer to “Design an AI Code Review Bot” sounds like, first question to last trade-off. It is about 7 minutes of talking; the whiteboard and the interviewer fill the rest of the 45. Read it aloud once, then close the page and give it yourself.

  1. 0–3 min

    Name what the bot is competing for

    I'd start by saying what I'm optimizing for, because it isn't catch rate. This bot is asking for a developer's attention, and it gets very few asks before a team decides it isn't worth reading — and once they've decided that, a better model doesn't win them back. So I'd design around the right to speak rather than around coverage: every box I draw exists to make a posted comment worth the interruption. I'd rather miss something than say something worthless.

  2. 3–7 min

    Cheap and certain before slow and probabilistic

    The skeleton is a webhook, a deterministic layer, a context assembler, the model, and a gateway that decides what gets posted. The first thing that runs is static analysis — no model call at all. I'd reach for that over a single unified LLM pass because for an unused import or a known-bad security pattern a linter is exactly correct and effectively free, and "probably, most of the time" is a worse guarantee at higher cost. That layer also becomes where anything I need certainty about goes — the secret scanner I'll come to is a pattern match, not a judgment call.

    Built in step 1: Don't send everything to an LLM
  3. 7–12 min

    Review the change, not the file

    Then I'd scope what the model sees: the changed lines plus enough surrounding code to make them coherent, not the whole file and certainly not the repo. I'd bake that in rather than add it later as an optimization, because of the long run — repositories only get bigger, and I don't want a bot whose cost and noise grow with the codebase instead of with what the team did this week. Scoped to the diff, a one-line fix costs what a one-line fix should cost in a ten-year-old repo and in a brand-new one.

    Built in step 2: Review the change, with just enough context
  4. 12–18 min

    Retrieve what the diff cannot show

    The diff on its own isn't enough for the bugs I most want caught. If it reorders arguments at a call site, judging that needs the called function's signature — which isn't in the diff, because that function wasn't touched. I wouldn't ship the whole repository: most of it is irrelevant here and it doesn't fit anyway. I'd keep a repo embedding index and retrieve only what this diff references — the signature, a type definition, a project convention. It's retrieve-then-generate applied to a codebase, and it's what makes cross-file bugs reachable at all.

    Built in step 3: What about the function this diff calls?
  5. 18–24 min

    Score every finding, then gate on the score

    Now the model reviews for logic, missed edge cases, naming, and whether the change does what the PR claims. What matters is that not everything it flags gets posted: each finding carries a confidence and a severity, and posting is gated on both. A clear logic bug or a security issue surfaces immediately; speculative or stylistic notes are suppressed, or batched into one low-priority summary. And I'd have the model emit each finding and its score in one structured response rather than asking afterwards how sure it was — a separate scoring pass mostly tells you the model still agrees with itself.

    Built in step 4: What only an LLM can actually judge
  6. 24–29 min

    Say it once

    A long-lived PR gets pushed repeatedly and each push re-triggers review. If every run reposts what it finds, five comments across six pushes is thirty comments, and that's exactly the spam pattern that ends the bot. So I'd keep a comment ledger keyed by finding and diff-hunk. Each run diffs against what's already been said: a still-unresolved finding stays quiet because it was already said once, a resolved one can be closed, and only genuinely new findings from the latest push get posted. Volume then tracks issues rather than pushes.

    Built in step 5: A re-push shouldn't repost old comments
  7. 29–34 min

    Do not make the leak worse

    One case I'd raise unprompted, because it's where the bot's own output is the attack surface: a diff containing a real credential. Everywhere else I'm asking whether a finding is correct; here the question is where that string travels, and the answer is an external model, a public thread and our logs. So the scan sits in the deterministic layer — the only one that runs before any of that — and what leaves it is a placeholder. I'd also exempt this finding from the rules I just gave: it's never batched and never waits.

    Built in step 6: Don't leak what you're flagging
  8. 34–38 min

    Adapt in a way you can explain

    Two things make it durable. The first is that the loop is scoped per repository on purpose: a category one team has explicitly decided against is another team's house rule, and a single global bar averages them into being wrong in both places. So reactions and dismissals move that repo's threshold and nothing else's. The second is that it moves thresholds and prompts, not weights — on a system whose whole problem is trust, I want every adaptation to be something I can read, explain, and revert in an afternoon.

    Built in step 7: Fewer repeat false positives over time
  9. 38–42 min

    The PR that does not fit

    Then the PR that doesn't fit. The first thing I'd say to the author of a five-thousand-line PR is to split it, but the design can't lean on that, because it'll land anyway. So I'd treat it as a budgeting problem, not a bigger version of the normal case: this PR gets a fixed amount of expensive review, and the question is how it's spent. Size is the wrong input — a vendored dependency bump and a hand-written refactor can be the same number of lines and nothing like the same risk.

    Built in step 8: The 5,000-line PR
  10. 42–45 min

    Close on the trade-off

    To close: static checks first, diff-scoped context, retrieval for the rest, gated comments, a ledger so nothing is said twice, redaction, and a feedback loop. The trade-off through all of it is precision over recall — I'd knowingly miss some real issues to keep the ones I post worth reading, because the failure that kills this is being muted, not being incomplete. With more time I'd settle whether a finding should ever block a merge, since the moment one can fail a build every threshold here moves, and I'd go at the index, built off the base branch, which the code under review isn't on.

What this teaches

Learn AI system design by building an automated PR review bot like GitHub Copilot for PRs, CodeRabbit or Graphite step by step. An interactive guide covering why cheap deterministic linters run before any LLM call, diff-scoped review with surrounding context, retrieving repo-wide context the diff alone can't show, confidence-gated commenting so the bot doesn't spam nitpicks, an idempotency ledger so re-pushes don't repost old comments, secret redaction, and a developer feedback loop that tunes false positives down over time.

Key takeaways

  • Cheap, deterministic static analyzers run first — the LLM is reserved for what genuinely requires understanding intent.
  • Review is scoped to the diff plus just enough surrounding context, not the whole file or the whole repo.
  • A Repo Embedding Index (RAG over the codebase) supplies cross-file context — a called function's signature, a project convention — the diff alone can't show.
  • Every finding carries a confidence/severity score; only what clears the bar gets posted, keeping signal-to-noise high enough that developers don't mute the bot.
  • A Comment Ledger tracks what's already been said per diff-hunk, so repeated pushes to a PR surface only genuinely new findings.
  • Likely secrets are detected and redacted before anything reaches the LLM or a posted comment — the review itself must never become a new leak.
  • Developer 👍/👎 feedback tunes per-repo confidence thresholds, so repeat false positives quietly decrease over a team's first few weeks.
  • Large PRs get budgeted, risk-prioritized review rather than either unbounded cost or being skipped entirely.

Concepts covered

  • What does a code review bot actually need to get right?
  • Don't send everything to an LLM
  • Review the change, with just enough context
  • What about the function this diff calls?
  • What only an LLM can actually judge
  • A re-push shouldn't repost old comments
  • Don't leak what you're flagging
  • Fewer repeat false positives over time
  • The 5,000-line PR
RUN IT YOURSELF

The review comments that come back attached to code they were never about

Six bot comments, then the author pushes: lines are inserted above them, two flagged lines are fixed where they stand, a dead function is deleted, and a helper is copy-pasted at the bottom. This file re-anchors all six twice — once by line number, once by a fingerprint of the flagged code — and prints the number that separates them: how many end up posted on unrelated code with nothing to show it. Change SHIFT, hit Run, and watch one column climb while the other does not move.

HOW TO READ THE CODE — 6 IDEAS
  1. The ledger key is the whole design (step 5): line numbers move when anything above them is edited, so 4 of 6 comments re-attach to unrelated code at SHIFT=3 — and each one still renders like a good comment.
  2. Walk SHIFT from 0 to 8 and the line anchor goes 1 2 3 4 5 5 5 5 5: it is wrong in proportion to how much the push inserted, which is why it survives into production — it is nearly right on a small push and never announces the transition.
  3. The fingerprint is wrong once, at every shift. Movement cannot break it — the key is the line with whitespace and formatting stripped — so its one miss is not caused by the push being bigger.
  4. That one miss is the honest cost of keying on content (step 6): comment #5 flagged if amount > 0: in refund(), the author fixed it there and left the copy-pasted twin in charge(), so exactly one line still hashes to it and the anchor calls that a hit. Comments #3 and #5 are the same key; one is right and one is not.
  5. Its other two misses are loud, and the line anchor gets one too: gone (rewritten, nothing hashes to it), ambiguous (still copy-pasted, so it declines instead of guessing), and past EOF (the deletion shrank the file past comment #6). A miss the bot can say out loud costs a retry; a silent one costs the developer’s trust.
  6. The flagged credential is fingerprinted but never printed, in the demo or in this output — redaction happens at detection, upstream of every consumer (step 6).
CPython · WebAssembly
built to comment only when it's right, and never twice on the same push — make the calls, kill the ledger, run the gauntlet.
Finished this one? 0 / 65 AI System Designs done
NextDesign a Conversational AI Closest AI system design to this one

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More AI System Designs