AI EngineeringZero to ProductionHome·About·Contact
Project 3 · Design Chapter

Coding & Code-Review Agent

The fastest-growing enterprise use case: an agent that reviews pull requests for real bugs and style issues, explains CI failures, and proposes fixes as commits — grounded in your repo's conventions. It reads code, reasons about it, and comments like a senior engineer, with a human always merging.

🎯 Advanced🔥 fastest-growing🧑‍💻 engineering teamsclosest cousin of the DevOps agent

What this project teaches you to design

  • A repo-aware review agent that cites specific files and lines.
  • The propose-don't-push safety model (PR comments & suggested commits, human merges).
  • Tools for reading a diff, searching the repo, running tests, and posting review comments.
  • Code-review evals that reward recall without drowning humans in false positives.

The brief advanced

"Catch bugs and convention violations before a human reviewer spends time — and before they reach production." Human review is a bottleneck and inconsistent. An agent that does a thorough first pass on every PR — flagging real issues with file/line references and suggested fixes — makes human review faster and catches more.

1 · Discovery — where does review time go? advanced

Where time goesAgent leverage
Reading a diff to understand what changed⭐⭐⭐ high — summarize the change
Spotting bugs, edge cases, security issues⭐⭐⭐ high — the core value
Checking style/convention adherence⭐⭐ medium — but linters do the mechanical part
Diagnosing why CI failed⭐⭐⭐ high — read the log, pinpoint the cause
Architectural / product judgment⭐ low — humans decide; agent informs
Problem statement"Every PR waits on a human to read the diff, hunt for bugs, and check conventions before merge. If an agent did a rigorous first pass — real bugs with file:line and a suggested fix, plus a plain-English change summary — reviewers would move faster and fewer bugs would slip through. The agent proposes; a human always merges."

2 · Architecture advanced

PR openedwebhook RAG: conventions+ past reviews Review agentread · reason · verify get_diff read_file / grep run_tests verify findingsdrop false positives PR comments (file:line) suggested commit
🗺️ How to read this diagram

This is the whole pipeline the project builds, drawn left to right: a pull request comes in, an agent reads and reasons about the code, it double-checks each problem it thinks it found, and only survivors become review comments. Follow the blue arrows — they are the flow of one PR through the system.

  • Far left — PR opened (webhook): the trigger. When a developer opens a pull request, GitHub/GitLab pings the agent to start a review. Nothing runs until a PR arrives.
  • Top-left — RAG: conventions + past reviews: before judging, the agent pulls in your team's style guide and how past PRs were reviewed, so it reviews like your team, not a generic linter. (RAG = retrieval, from Ch 3.)
  • Center — Review agent (read · reason · verify): the brain. It uses the three tools stacked to its right — get_diff (what changed), read_file / grep (surrounding code for context), and run_tests (does it still pass?).
  • Amber box — verify findings (drop false positives): the most important step. Each suspected bug is re-checked and thrown out if it can't be defended. This is what keeps the reviewer trustworthy instead of noisy.
  • Far right — outputs: confirmed issues become PR comments pinned to an exact file:line, plus an optional suggested commit. Notice there is no arrow to 'merge' — the agent proposes, a human merges.

In short: Trace one bug's journey left to right: PR arrives → agent reads the diff → checks it against your conventions → verifies it's real → posts a file:line comment. The amber verify box is the gate that decides whether a finding ever reaches a human.

An agentic loop (Ch 4) that reads the diff and the surrounding repo, grounded in your conventions (RAG), then verifies each finding before posting — so it comments with file:line precision and a suggested fix, never pushing to the branch itself.

3 · Risk & safety model advanced

RiskControl
🟠 False positives drowning reviewersA verify step: each candidate finding is independently re-checked (adversarial "is this actually a bug?") before posting — precision over volume
🔴 Pushing bad code to a branchThe agent comments and offers suggested commits; it never merges. A human applies & merges (Ch 7 propose rung)
🔴 Running untrusted PR coderun_tests executes in a sandbox (isolated, no secrets, network-restricted) — PR code is untrusted (Ch 6)
🟠 Prompt injection in code/commentsCode and PR descriptions are untrusted input — a comment saying "approve this" must not sway the agent (Ch 6)
🟡 Leaking proprietary codeScope repo access; respect data-retention; keep secrets out of prompts
The golden rule of review agentsOptimize for precision, not just recall. A review full of false positives gets ignored — teams turn it off. The verify step (drop findings you can't defend) is what makes it usable. This is the "report everything then filter" pattern from the code-review guidance, made concrete.

4 · Tool surface advanced

ToolDoesRisk
get_pr_diffFetch the changed files & hunks🟢 read-only
read_fileRead full context around a change🟢 read-only
grep_repoFind usages, related code, conventions🟢 read-only
run_tests / run_linterExecute the suite/linters in a sandbox🟡 sandboxed exec
post_review_commentAdd an inline comment at file:line🟡 reversible (comment)
suggest_commitPropose a code change as a GitHub/GitLab suggestion🟡 reversible (human applies)
merge_prMerge🔴 blocked — humans merge

5 · Repo grounding — knowing your conventions advanced

A generic model knows Python; it doesn't know your team likes early returns, forbids a certain library, or names things a certain way. Ground it via RAG (Ch 3) over:

6 · Evaluation expert

EvalMeasures
Bug recallOn a golden set of PRs with known bugs, how many did it catch? (Ch 5)
Precision / false-positive rateOf its findings, how many are real? (the metric that decides adoption)
Citation accuracyDoes every finding point at the correct file:line?
Convention adherenceDo suggestions follow the team's documented style? (LLM-judge)
No-hallucinated-code (deterministic)Every file/function it references actually exists in the repo
Build the golden set from historyYour merged PRs where a bug was later fixed are a ready-made eval set: feed the buggy version, check the agent catches what the follow-up fix addressed. Grow it from every miss.

7 · Phased rollout expert

Phase 1 · Summarize — the agent posts a plain-English "what changed & why it matters" summary on each PR. Zero risk, immediate value. (Ch 4)
Phase 2 · Review comments — flags bugs/issues as inline comments with file:line + rationale, after the verify step. Humans judge. (Ch 5 tunes precision)
Phase 3 · Suggested fixes — proposes commits a human can apply in one click; explains CI failures with fixes. (Ch 6)
Never — merge, approve, or push directly to protected branches. Humans own the merge button.

Skills & course map expert

SkillLearn it in
Agentic loop reading diffs/files with toolsCh 4
Repo/convention grounding (RAG)Ch 3
Verify-before-report (adversarial check)Ch 5
Sandboxed test execution, propose-via-PR, secretsCh 6 + Ch 8 safety model
Structured findings (file, line, severity, fix)Python P4 (Pydantic)
The propose-don't-act disciplineCh 7 FDE

Build-along plan expert

  1. Start read-only (Ch 4): tools to fetch a diff and read files from a local repo; produce a change summary.
  2. Add findings as structured output (Pydantic): [{file, line, severity, issue, suggested_fix, confidence}].
  3. Add the verify step (Ch 5 idea): re-check each finding; drop the ones it can't defend — this is what makes it usable.
  4. Ground in conventions (Ch 3): RAG over the style guide + past review comments.
  5. Wire to a real repo: post comments via the GitHub/GitLab API; run tests in a sandbox; measure precision/recall on a golden set of historical PRs.
Reuses the most from Project 1If you did the DevOps capstone, you already have the tool-registry + safety-gate + propose-via-PR machinery. This project mostly swaps the tool set. Want dedicated build labs + code? Ask and I'll produce them.
🛠️ Hands-on build — everything below is on this pageThe rest of this page is the complete, self-contained build: set up from an empty folder, paste in every file, run it (with a mock, so no API key is needed), and pass the tests. Follow it top to bottom — no other page required.

Learning objectives

  • Produce structured findings (file, line, severity, fix, confidence).
  • Build the two-pass review→verify pipeline (the precision lever).
  • Measure recall on known bugs and keep precision high.

What you'll build expert

A reviewer that reads a PR diff, proposes candidate findings, then independently re-checks each one and drops the indefensible — reporting only confirmed issues with file:line and a fix. It never merges; a human does.

Finished code includedAll in llm-course-starter/coding-agent/. The rationale is in the design chapter.

Step 1 · The findings schema expert

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Step 1
Setup to run this snippet
from pydantic import BaseModel
from pydantic import Field
from typing import Literal
agent/schemas.pyclass Finding(BaseModel):
    file: str
    line: int
    severity: Literal["low", "medium", "high"]
    issue: str
    suggested_fix: str
    confidence: float = Field(ge=0, le=1)

class Review(BaseModel):
    summary: str
    findings: list[Finding] = Field(default_factory=list)

class Verdict(BaseModel):        # the verifier's judgement
    is_real: bool
    reason: str
▶ How this works

Before writing any logic, we declare the exact shape of the data we want back from the model. These are Pydantic models — think of each as a form with typed blanks. If the model returns something that doesn't fit the form, Pydantic rejects it, so bad data never flows downstream. This is the project's contract.

  1. class Finding(BaseModel) is one reported problem. Each field is a labelled blank with a type: file: str, line: int, an issue description, and a suggested_fix.
  2. severity: Literal["low", "medium", "high"] means severity must be one of exactly those three words — nothing else is accepted. That's how we stop the model inventing junk categories.
  3. confidence: float = Field(ge=0, le=1) is a number the model must keep between 0 and 1 (ge = greater-or-equal, le = less-or-equal) — a 0-to-1 sureness score.
  4. class Review bundles a plain-English summary with a findings list; default_factory=list just means "start empty if there are none". class Verdict is the verifier's yes/no answer later: is_real plus a reason.

What the output means: Nothing prints — this file only defines the shapes. Their payoff shows up when .parse(...) forces the model's reply into these exact forms.

Try this: Imagine the model returns severity: "urgent". Because it isn't in the Literal list, Pydantic raises a ValidationError — the schema catching bad data for you, before it can cause a bug.

Step 2 · The mock diff (with real bugs) expert

Step 2

repo/sample_diff.py is a plain-text diff containing three planted bugs (missing amount check, a TypeError, a missing None-check) plus KNOWN_ISSUES the eval scores against. In production this comes from git diff / the PR API.

repo/sample_diff.pyDIFF = '''...
+    gateway.charge(user.card, amount)   # BUG: no positive-amount check
+    log.info("charged " + amount)       # BUG: TypeError, amount is a float
...'''
KNOWN_ISSUES = ["amount", "TypeError", "None"]
▶ How this works

To build and test a review agent you need something for it to review. Rather than wire up a live repo, this file hard-codes a small diff (the text showing what a PR changed) with three bugs deliberately planted in it — so we always know the right answer to check the agent against.

  1. DIFF is a multi-line string (the '''...''' triple quotes let it span lines). Lines starting with + are newly added code — exactly what a real git diff shows.
  2. The # BUG: comments mark the three planted problems: charging with no positive-amount check, a TypeError from adding a float to a string, and (further down) using a value that might be None.
  3. KNOWN_ISSUES = ["amount", "TypeError", "None"] is the answer key: three short keywords, one per planted bug. Step 6's eval later checks the agent's findings mention each of these.

What the output means: This file just holds data — the diff to review and the list of bugs it contains. In production this text would come from git diff or the PR API instead.

Try this: Add a fourth planted bug to DIFF and a matching keyword to KNOWN_ISSUES. Now the eval expects the agent to catch four — a quick way to make the test harder.

Step 3 · The review pass expert

Step 3

Uses the client / objects set up earlier in this lesson, and needs ANTHROPIC_API_KEY set. Run the earlier blocks first.

agent/engine.pyREVIEW_SYSTEM = ("You are a senior code reviewer... Report everything you "
  "suspect — a separate verification step will filter.")

def review_pr(diff):
    return client.messages.parse(model=MODEL, max_tokens=1500,
        system=REVIEW_SYSTEM, output_format=Review,
        messages=[{"role":"user","content":f"Diff:\n{diff}"}]).parsed_output
▶ How this works

This is the first of two model calls: the review pass. Its job is to cast a wide net — surface every problem it suspects. We deliberately tell it to over-report, because a second, stricter pass (Step 4) will throw out the false alarms. Recall first, precision later.

  1. REVIEW_SYSTEM is the system prompt — the reviewer's standing instructions. Notice it literally says "Report everything you suspect — a separate verification step will filter", setting up the two-pass design.
  2. def review_pr(diff): takes the diff text and returns findings. It calls client.messages.parse(...) — the .parse method (not plain .create) forces the reply into a shape.
  3. output_format=Review is the key line: it tells the model "answer as a Review object" (the schema from Step 1), so you get a summary and a typed list of findings, not free-form prose.
  4. .parsed_output at the end hands back that validated Review object directly — ready to loop over, no manual parsing.

What the output means: A Review object: a one-line change summary plus a list of Findings (file, line, severity, issue, fix) — likely including some over-eager guesses that the next step will prune.

Try this: Read the two-sentence REVIEW_SYSTEM aloud. That single instruction — "report everything, something else will filter" — is the whole philosophy of the review half of the pipeline.

Tell it to over-report hereThe review pass is told to surface everything — recall first. Filtering happens next. This is the exact "report everything, filter downstream" guidance from the code-review section of the course, split into two model calls.

Step 4 · The verify pass (the precision lever) expert

Step 4

Uses the client / objects set up earlier in this lesson, and needs ANTHROPIC_API_KEY set. Run the earlier blocks first.

agent/engine.pyVERIFY_SYSTEM = ("You are a skeptical reviewer... decide if it is a genuine, "
  "defensible bug. Default to is_real=false if unsure. Precision, not volume.")

def verify(diff, finding):
    v = client.messages.parse(model=MODEL, max_tokens=400, system=VERIFY_SYSTEM,
        output_format=Verdict, messages=[{"role":"user","content":
        f"Diff:\n{diff}\n\nClaimed issue: {finding.issue}\n\nReal bug?"}])
    return v.parsed_output.is_real

def review_and_verify(diff):
    review = review_pr(diff)
    confirmed = [f for f in review.findings if verify(diff, f)]   # the filter
    return review.summary, confirmed
terminalpython agent/engine.py       # needs key
SUMMARY: Refactors charge() and refund()...
3 confirmed finding(s):
  payments.py:4 [high] charge() no longer checks amount > 0...
  payments.py:5 [medium] string concatenation with a float raises TypeError...
  payments.py:9 [high] refund() dereferences a possibly-None txn...
▶ How this works

This is the second model call — the verify pass — and it's the heart of the project. A brand-new, skeptical reviewer looks at each suspected bug on its own and answers one question: is this a real, defensible bug? Only survivors are kept. This is the precision lever that stops the tool from becoming noise.

  1. VERIFY_SYSTEM is a deliberately harsh prompt: "Default to is_real=false if unsure. Precision, not volume." When in doubt, it drops the finding — the opposite bias from the review pass.
  2. def verify(diff, finding): asks the model about one finding at a time. output_format=Verdict forces a clean yes/no answer, and .parsed_output.is_real pulls out just the True/False.
  3. def review_and_verify(diff): wires the two passes together. First review = review_pr(diff) gets all the candidates.
  4. confirmed = [f for f in review.findings if verify(diff, f)] is the filter (the # the filter comment): it keeps a finding only if verify(...) returns True. This is a list comprehension — a compact for-loop that builds a new, shorter list.

What the output means: The SUMMARY line, then 3 confirmed finding(s) — each printed as file:line [severity] description. The false positives from Step 3 have been silently dropped; only defensible bugs remain.

Try this: Count the calls: one review call, then one verify call per finding. That's the cost of precision. If you find the reviewer too noisy, the fix is almost always to make VERIFY_SYSTEM stricter — not to touch the review pass.

Why two passes beat oneA single "find bugs and be careful" prompt either misses bugs (too cautious) or floods you with false positives (too eager). Splitting into over-report → skeptically-verify gets you both recall and precision. It costs an extra call per finding — worth it, because a noisy reviewer gets disabled.

5 · Tests (no key) expert

Step 5
terminalpython -m pytest tests/ -v
test_finding_schema_validates PASSED
test_finding_rejects_bad_severity PASSED
test_diff_contains_the_known_bugs PASSED
3 passed
▶ How this works

These tests check the parts that don't need the model at all — the schema and the fixture — so they run instantly with no API key. Fast, free tests like these guard the foundations every other step stands on.

  1. python -m pytest tests/ -v runs the test suite. -v (verbose) lists each test by name with PASSED or FAILED.
  2. test_finding_schema_validates builds a well-formed Finding and confirms Pydantic accepts it — the happy path works.
  3. test_finding_rejects_bad_severity feeds a severity outside the Literal list and confirms Pydantic rejects it — proving the guard from Step 1 actually bites.
  4. test_diff_contains_the_known_bugs checks the fixture from Step 2 really holds the planted bugs, so the eval has something real to catch.

What the output means: 3 passed — all three green. Because none of these call the API, they pass the same way on any machine, key or no key.

Try this: Temporarily change a severity in schemas.py to allow "urgent" and re-run — test_finding_rejects_bad_severity will fail, showing you the test is genuinely enforcing the rule.

✅ Test cases
TestProves
schema validatesfindings are well-formed
rejects bad severitythe Literal guard works (no junk severities)
fixture has known bugsthe eval has real bugs to catch

6 · Evals (needs key) expert

Step 6
terminalpython evals.py
confirmed findings: 3
  [HIT] known issue: amount
  [HIT] known issue: TypeError
  [HIT] known issue: None
recall on known bugs: 100%
✅ evals passed

Recall is measured against KNOWN_ISSUES; the eval fails if it misses most bugs and warns if it over-reports (a sign the verify step isn't filtering).

▶ How this works

The eval answers the question that decides whether this agent is worth shipping: does it actually catch the bugs? It runs the full review→verify pipeline on the planted diff and scores the confirmed findings against the KNOWN_ISSUES answer key from Step 2. This one needs an API key because it calls the model.

  1. python evals.py runs the whole pipeline once and then grades it.
  2. confirmed findings: 3 is how many bugs survived the verify filter. Each [HIT] known issue: ... line means a planted bug (amount / TypeError / None) was matched by a confirmed finding.
  3. recall on known bugs: 100% means it found all three planted bugs. Recall = of the bugs that exist, what fraction did we catch — the headline quality number.
  4. The note explains the two failure modes the eval watches for: it fails if it misses most bugs (recall too low) and warns if it reports far more than three (a sign the verify step has stopped filtering).

What the output means: A per-bug hit list, a recall percentage, and ✅ evals passed. Together with Step 5's schema tests, this is your evidence the pipeline both works and stays precise.

Try this: After adding a fourth bug in Step 2's 'Try this', re-run — recall is now out of four. This is exactly how you'd grow a real golden set: every bug the agent misses becomes a new case in the eval.

Grow the eval from git historyReal golden set: past PRs where a bug was later fixed. Feed the buggy version, check the agent catches what the fix addressed. Each miss becomes a new case.

Troubleshooting expert

⚠️ Common issues
SymptomFix
Too many findings (noisy)Strengthen VERIFY_SYSTEM ("default to is_real=false"); the verify step is your filter
Misses obvious bugsMake the review prompt over-report; raise effort; ensure the diff is fully in the prompt
Wrong line numbersInclude line context in the diff; ask the model to cite the hunk header
Verify drops real bugsVerifier too strict — soften slightly, or pass more surrounding code as context
ValidationError on severityThe model returned a severity outside the Literal — that's the schema doing its job; the SDK retries

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Read-only tools and a diff summaryBeginner

Context: A code-review agent should start with zero blast radius: read-only tools while you build the reasoning, write tools gated behind human merge only later. A reviewer also can't judge a change it can't see.

Your task: Implement read-only tools (get_pr_diff, read_file) as offline fakes and a summarize_diff that reports files and line churn.

Requirements:

  • get_pr_diff and read_file as offline fakes (real API stubbed)
  • summarize_diff reports files touched and lines added/removed
  • It parses a unified diff's +++/+/- markers
  • No tool in this milestone can modify the repo
  • Demonstrate the summary on a sample diff

💡 Hint: Walk the diff lines: +++ names a file, a leading +/- counts an added/removed line.

Show solution

Start read-only — zero blast radius while you build the reasoning. Offline fakes stand in for the API:

DIFF = '''+++ b/app.py
+def add(a, b): return a - b
-def add(a, b): return a + b
+++ b/util.py
+import os'''

def get_pr_diff(pr_id): return DIFF          # real: GitHub/GitLab API
def read_file(path):    return "..."         # real: repo checkout

def summarize_diff(diff):
    files, added, removed = set(), 0, 0
    for line in diff.splitlines():
        if line.startswith("+++"):   files.add(line.split("b/")[-1])
        elif line.startswith("+"):   added += 1
        elif line.startswith("-"):   removed += 1
    return {"files": sorted(files), "added": added, "removed": removed}

print(summarize_diff(get_pr_diff(1)))
# {'files': ['app.py', 'util.py'], 'added': 3, 'removed': 1}

Read-only tools let the agent gather context safely. Write tools (posting comments) come later, gated behind human merge — never let the first version touch the repo.

Exercise 2 · A structured Finding schemaIntermediate

Context: Freeform review text isn't actionable. A typed schema is the contract that lets you pin each comment to file:line and filter by confidence downstream.

Your task: Define Pydantic Finding and Review schemas and a labelled messages.parse call that fills them.

Requirements:

  • Finding has file, line, severity, issue, suggested_fix, and a 0–1 confidence
  • Severity is constrained (e.g. a Literal of low/medium/high)
  • Review holds a summary and a list of findings
  • A hand-built review validates the schema offline
  • The model-fill step is a labelled messages.parse with output_format

💡 Hint: Every finding must carry a line to anchor to and a confidence to threshold on — that's what makes the posting step trivial later.

Show solution

Structured findings are what let you pin comments to file:line and filter by confidence later:

from pydantic import BaseModel, Field
from typing import Literal

class Finding(BaseModel):
    file: str
    line: int
    severity: Literal["low", "medium", "high"]
    issue: str
    suggested_fix: str
    confidence: float = Field(ge=0.0, le=1.0)

class Review(BaseModel):
    summary: str
    findings: list[Finding]

# offline: a hand-built review validates the schema
r = Review(summary="1 bug", findings=[Finding(file="app.py", line=2,
    severity="high", issue="add() subtracts", suggested_fix="return a + b",
    confidence=0.95)])
print(r.findings[0].file, r.findings[0].severity)   # app.py high

# --- needs API key: model fills the schema ---
# r = client.messages.parse(model="claude-opus-4-8", output_format=Review,
#     system="Review this diff. Emit findings with file:line and a fix.",
#     messages=[{"role":"user","content":diff}]).parsed_output

The typed schema is the contract: every finding must have a line to anchor to and a confidence to threshold on, so the posting step downstream is trivial.

Exercise 3 · Two-pass review -> verify to kill false positivesAdvanced

Context: A single review pass over-reports. A second, skeptical verify pass drops indefensible findings — trading a little recall for a lot of precision, because a bot that cries wolf gets muted.

Your task: Add a verify pass that re-checks each finding and keeps only the defensible ones, modelled offline.

Requirements:

  • Each finding is re-checked independently and dropped unless defensible
  • A verdict carries whether it's real and a reason
  • The offline heuristic keeps high-confidence findings with a concrete fix
  • Weak / no-fix findings are dropped
  • Demonstrate that a noisy review shrinks to the defensible findings

💡 Hint: The real version is a second messages.parse asking 'is this a real bug, and why?'; offline, gate on confidence plus a non-empty fix.

Show solution

The verify pass is a second, skeptical look that drops indefensible findings — the precision lever:

class Verdict(BaseModel):
    is_real: bool
    reason: str

def verify(finding):
    # --- real: a second messages.parse asking "is this a real bug? why?" ---
    # offline heuristic: high-confidence + concrete fix survives
    real = finding.confidence >= 0.7 and bool(finding.suggested_fix.strip())
    return Verdict(is_real=real,
                   reason="defensible" if real else "low-confidence/no fix")

def two_pass(review):
    kept = [f for f in review.findings if verify(f).is_real]
    return Review(summary=review.summary, findings=kept)

noisy = Review(summary="", findings=[
    Finding(file="a", line=1, severity="high", issue="bug",
            suggested_fix="fix it", confidence=0.9),
    Finding(file="b", line=2, severity="low", issue="maybe?",
            suggested_fix="", confidence=0.4)])
print(len(two_pass(noisy).findings))   # 1 -> the weak finding is dropped

Reviewers who cry wolf get muted. The verify pass keeps only findings the agent can defend, so the comments developers see are worth reading — precision matters more than recall for a bot.

Exercise 4 · Ground reviews in team conventions via RAGExpert

Context: A generic reviewer flags style the team allows and misses the house rules. Retrieving the team's own conventions lets a finding cite the actual rule, which is far more likely to be actioned than a generic warning.

Your task: Retrieve relevant convention snippets for the changed files and inject them so findings cite the actual rule.

Requirements:

  • A small store of team conventions (logging, errors, SQL, etc.)
  • Retrieval returns the conventions relevant to signals present in the diff
  • Results are capped at top-k
  • A finding can cite the rule name it violates
  • Demonstrate retrieval on a diff that trips two rules

💡 Hint: Match diff content against a per-rule signal (e.g. an f-string into SQL, a bare print() so the reviewer speaks the team's language.

Show solution

Grounding in retrieved conventions turns opinion into "this violates rule X":

CONVENTIONS = {
    "logging": "Use structured logging via log.info(event=..., **kw); no print().",
    "errors":  "Raise typed exceptions; never bare except:.",
    "sql":     "Parameterize queries; never f-string user input into SQL.",
}

def retrieve_conventions(diff_text, k=2):
    hits = []
    for name, rule in CONVENTIONS.items():
        signal = {"logging":"print(", "errors":"except:", "sql":"f\""}[name]
        if signal in diff_text:
            hits.append((name, rule))
    return hits[:k]

diff = 'query = f"select * from t where id={uid}"\nprint("done")'
for name, rule in retrieve_conventions(diff):
    print(f"[{name}] {rule}")
# [sql] Parameterize queries; ...
# [logging] Use structured logging ...

A finding that cites the team's own rule ("violates conventions/sql: parameterize queries") is far more likely to be actioned than a generic "possible SQL injection". Retrieval makes the reviewer speak the team's language.

Exercise 5 · Measure precision/recall on a golden PR setProfessional

Context: Before wiring to a live repo, a bot reviewer must earn trust with numbers — and precision first, because a reviewer that's right half the time gets muted while one that's right 90% gets read.

Your task: Score findings against a golden set of known bugs, report precision and recall, and gate rollout on a precision bar.

Requirements:

  • A golden set of known real bugs keyed by (file, line)
  • Compute precision and recall from true/false positives
  • Report both plus the raw TP / FP counts
  • Gate rollout on a precision bar (e.g. 0.8), failing below it
  • Optimize precision before recall in the framing

💡 Hint: Precision is TP over everything you flagged; recall is TP over the known bugs — a change that lifts recall by tanking precision must fail the gate.

Show solution

A bot reviewer must earn trust with numbers — high precision first, so developers don't learn to ignore it:

GOLD = {("app.py", 2), ("db.py", 40)}          # known real bugs (file,line)

def score(found, gold):
    found = set(found)
    tp = len(found & gold)
    precision = tp / (len(found) or 1)
    recall    = tp / (len(gold) or 1)
    return {"precision": round(precision,2), "recall": round(recall,2),
            "tp": tp, "fp": len(found - gold)}

found = [("app.py", 2), ("app.py", 9)]         # one real, one false positive
m = score(found, GOLD)
print(m)                                       # precision 0.5, recall 0.5
PRECISION_BAR = 0.8
raise SystemExit(0 if m["precision"] >= PRECISION_BAR else 1)   # gate rollout

Optimize precision before recall: a reviewer that's right 90% of the time gets read; one that's right 50% gets muted. Track both each run so a prompt change that boosts recall by tanking precision is caught.

Exercise 6 · Wire to a real repo and post file:line comments safelyIndustry scenario

Context: The agent advises; humans decide. On real PRs it posts file:line comments (or suggests commits) but never crosses the line of merging — that button belongs to a person.

Your task: Wire the write tools (post_review_comment, optional suggest_commit) with a hard rule that the agent never merges.

Requirements:

  • post_review_comment supports a dry-run before real posting
  • The real posting path is labelled (needs API key + repo token)
  • A merge attempt raises a safety error — always
  • The agent may suggest a commit but never merges
  • Demonstrate posting the verified findings and the blocked merge

💡 Hint: Default posting to dry-run so you can eyeball comments first; make merge raise unconditionally as the line the agent never crosses.

Show solution

Writes are gated and merge is off-limits — the agent advises, humans decide:

class SafetyError(PermissionError): pass

def post_review_comment(pr_id, file, line, body, dry_run=True):
    if dry_run:
        return f"[dry-run] {file}:{line} -> {body[:40]}"
    # --- needs API key + repo token: GitHub/GitLab review comment ---
    # gh.pulls.create_review_comment(pr=pr_id, path=file, line=line, body=body)
    return f"posted {file}:{line}"

def merge(*a, **kw):
    raise SafetyError("agent must never merge; humans merge only")

for f in two_pass(noisy).findings:
    print(post_review_comment(1, f.file, f.line,
          f"{f.issue}\n```suggestion\n{f.suggested_fix}\n```"))
try: merge(pr=1)
except SafetyError as e: print("blocked:", e)

Start every PR in dry_run to eyeball the comments, then flip it on for real posting. The merge guard is the line the agent never crosses — it can suggest a commit, but a human owns the merge button.

✓ Checkpoint — done when…

  • The review pass produces structured findings with file:line.
  • The verify pass drops indefensible findings.
  • Schema tests pass with no key.
  • The eval catches the known bugs without a flood of false positives.
📋 Master rubric — grade your review agent
DimensionMeets the barAbove the bar (staff-level)
Recall on real bugsRecall is measured against a known-bug set; the agent flags the seeded defects with file:line references.Recall holds across bug classes (logic, security, resource leaks), not just the easy ones, and misses are triaged rather than ignored.
False-positive rate / precisionThe two-pass review→verify pipeline drops indefensible findings; precision is measured, and noise is low enough that a human trusts the output.Precision is tuned so reviewers act on nearly every comment; the verify pass is independent (fresh context), not the same call rationalising itself.
Safety of any auto-actionThe agent proposes — PR comments and suggested commits — and never merges; a human is always the gate to main.Suggested edits are minimal-diff and reversible; nothing touches CI secrets or runs untrusted code from the diff during review.
Grounding to the diffFindings reference actual files and lines in the PR, not hallucinated code; comments quote the offending lines.The agent distinguishes changed-line issues from pre-existing debt and scopes comments to the diff so it doesn't relitigate the whole repo.
Eval & regression gatePrecision and recall are computed by an eval harness, not by demo vibes; the numbers gate changes to the reviewer.A regression suite fails CI if precision or recall drops, so a prompt tweak can't silently make the reviewer noisier.
Cost & latency per PRPer-PR token/dollar cost and wall-clock are tracked and stay within a per-PR budget.Cost scales with diff size, not repo size (retrieval is scoped); large PRs degrade gracefully instead of timing out or blowing budget.

Score each row 0 (missing) / 1 (meets) / 2 (above). A passing review agent is 9+/12 with safety-of-auto-action at 2 — any path that can merge or mutate main without a human is an automatic fail, and a build whose false-positive rate trains humans to ignore it has failed its one job regardless of recall.

Knowledge check check yourself

✓ Knowledge check

Why does the review agent optimize for precision (a verify step that drops findings it can't defend) rather than maximizing recall?

Show answer
A review full of false positives gets ignored and teams turn it off. Re-checking each candidate finding and dropping anything indefensible keeps the reviewer trustworthy — precision over volume is what makes the tool usable in practice.
✓ Knowledge check

Why is merge_pr blocked and why must run_tests run in a sandbox?

Show answer
The agent proposes (comments, suggested commits) and a human always merges — the propose-don't-push safety model. PR code is untrusted, so running its tests in an isolated sandbox (no secrets, network-restricted) prevents untrusted code from doing harm during review.
© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in