Chuyển đến nội dung chính

AI code review nobody mutes: the problem is precision, not capability

The failure mode is always the same. Someone wires an AI reviewer into CI on a Monday. It posts thirty comments on the first pull request: twenty-seven are variable naming, “consider extracting this into a helper”, and a suggestion to add a test that already exists two files over. Three are real, and one of those is a genuine data-loss bug in a retry path.

Nobody finds the three. By Wednesday people scroll past the bot’s review to reach the human one. By the following Monday it is a non-blocking check with notifications muted — a dead integration that still burns tokens on every push.

The bot was not bad at finding bugs. It found the bug. It was bad at not saying things, and in code review that is the entire job. A reviewer’s budget is not compute, it is your teammates’ willingness to keep reading, and that belief updates fast. If the first ten comments a developer reads contain one useful one, they have learned a rule — skim it, it is usually nothing — and that rule is applied to comment eleven, which is the data-loss bug. You did not fail to find it. You failed to get it read.

Which gives you an uncomfortable but load-bearing asymmetry: a missed bug costs you less than a wrong comment. A missed bug leaves you exactly where you were before the bot existed, with humans reviewing. A wrong comment costs a reader’s attention and some of their trust, permanently, across every future comment. So the design problem is precision, not recall, and almost every knob worth turning is a knob that makes the bot say less.

Anything a deterministic tool can decide must never reach the model

The largest single source of noise is a model re-deriving opinions a linter already holds, worse and less consistently. Push everything decidable down to a tool that is silent, fast, and fires in the editor before the PR exists.

QuestionWho should answer it
Is this formatted correctly?dart format
Unused import, dead code, missing await?dart analyze
Does this await leave a BuildContext unsafe?the use_build_context_synchronously lint
Is this an unawaited future?the unawaited_futures lint
Does the naming match the repo?an analyzer rule or a custom_lint plugin
Does the retry path reuse its idempotency key after a cold restart?the model

If you can write it as a lint rule, write it as a lint rule. It never hallucinates and it never consumes review attention, because it never reaches the PR. The model’s job is only the last row: defects that need two files and a sequence of events held in your head at once.

Generic best practices are the noise generator

A model with no repo context falls back on the average of all code it has seen. That is how you get “consider adding error handling” on a function whose entire purpose is to propagate the error to a caller that handles it.

Fix this with a conventions file the reviewer reads on every run. The important part is not the rules — it is that every rule carries an explicit counterexample. The carve-out is what buys precision.

## Error handling
Repository and data-source methods return `Result<T, AppError>`
(lib/core/result.dart). Throwing out of a repository method is a bug.
NOT A FINDING: `throw` inside a private helper the repository already
wraps, or anything under test/.

## Migrations
A change to a table under db/schema/ requires a paired file in
db/migrations/ in the same PR.
NOT A FINDING: a change to a view, or an index-only change.

Derive these from evidence, not taste. The best source is your own review history: pull the last few hundred human review comments off merged PRs, cluster them, keep what recurs across different reviewers. A rule a human has actually made three times is worth automating. A rule nobody has ever made is one you invented, and it will generate noise forever.

Scope: the diff, plus exactly what the diff touches

Handing the model whole files is the second-biggest noise source, because it will review the parts nobody changed. Reviewers hate that more than they hate being wrong — the comment is often correct and still infuriating, because the author did not write that line and will not be fixing it here.

Assemble context deliberately: the changed hunks as a unified diff; for each symbol the diff modifies, its current definition and its call sites one hop out; the test file covering the changed file if one exists; the conventions file. Then tell the model that nothing else in the repository exists, and that unchanged code appears only as context and is never a legitimate target. That one instruction removes a large class of comments in a single line. And review the diff since the last reviewed SHA, not the whole PR again — otherwise every force-push reposts the same three comments and the thread becomes unreadable on its own.

A hard cap, a deleted tier, and one falsifiable claim per finding

Severity without a cap is decoration. The cap is what forces the model to rank, and ranking is the behaviour you actually want.

TierWhat it meansWhat happens
S1Data loss, corruption, auth bypass, a credential in the diff, a crash on a reachable pathInline comment
S2Contract break: exported API, serialized format, or a schema change without its migrationInline comment
S3Violates a rule written in the conventions fileInline only if the cap has room
S4Everything else — naming, readability, “consider extracting”, perf hunches, test opinionsDeleted

S4 is deleted, not collapsed into a details block. A collapsed section still trains people to open it, so it still costs attention, and it is exactly where the noise lives. If it is not worth an inline comment, it is not worth existing.

Make the cap a small fixed number — five is a reasonable start — and do not scale it with diff size. The cap is on the reader’s attention, and their attention does not grow because your PR is bigger. If more than five S1/S2 findings survive, post five and say how many were suppressed: a PR with eight genuine correctness findings needs splitting, not a longer review.

Then the second half of the problem. “This could cause a race condition” is unfalsifiable, and unfalsifiable comments are the worst kind — the reader cannot dismiss one without doing work, so they either do the work and resent it or dismiss it and feel vaguely guilty. Force structure instead; make the model emit an object, not prose:

{
  "file": "lib/sync/queue_flusher.dart",
  "line": 142,
  "severity": "S1",
  "claim": "Retried flushes generate a new idempotency key after the queue is re-hydrated from disk.",
  "trigger": "The app is killed mid-flush and restarts before the server's 2xx arrives.",
  "consequence": "The server treats the retry as a new request and creates a duplicate order.",
  "evidence": ["lib/sync/queue_flusher.dart:131-149", "lib/sync/queue_store.dart:88"],
  "falsifiable_by": "If QueueStore.hydrate() preserves item.idempotencyKey, this finding is wrong.",
  "confidence": 0.7
}

falsifiable_by is the field that does the work: it names one thing to check. The reviewer opens one file, looks at one method, and either fixes a real bug or closes the comment in ten seconds with no residue. Findings that cannot produce that line are, in practice, the vague ones — dropping them at the schema level is a free precision win. Render the comment leading with the trigger, not the abstraction:

S1 — duplicate order on restart-during-flush. If the app is killed mid-flush and restarts before the 2xx arrives, hydrate() rebuilds the item without its original idempotency key, so the retry posts as a new request. Wrong if: QueueStore.hydrate() already restores item.idempotencyKey.

Propose, then refute

The proposer prompt is mostly a list of prohibitions, which is the right shape for a precision-first tool.

You are reviewing ONE pull request diff.

You are given:
  CONVENTIONS - the written rules of this repository.
  DIFF        - unified diff of the changed hunks only.
  CONTEXT     - for each changed symbol: its current definition, its
                callers one hop out, and its test file.
Nothing else in the repository exists as far as you are concerned.

Report a finding ONLY if all four hold:
  a. The defect is introduced or made reachable BY THIS DIFF.
  b. You can name a concrete trigger: an input, a sequence, a state.
  c. You can name the consequence in words an on-call engineer would use.
  d. Someone can prove you wrong by checking ONE thing that you name.

Never report:
  - anything `dart analyze` or `dart format` decides
  - style, naming, file layout, or "consider extracting"
  - missing tests, unless this diff changes a behaviour an existing test
    asserts and that test was not updated
  - performance, unless you can point at a loop bound or an allocation
    that scales with user-controlled input
  - anything on a line shown to you only as CONTEXT

Severity: S1 correctness/security, S2 contract break, S3 violates a rule
written in CONVENTIONS. Do not emit S4.

Output a JSON array, at most 8 objects, matching the schema below.
An empty array is a correct and common answer.

That last line earns its place. Without explicit permission to find nothing, models treat an empty result as failure to do the task and manufacture something to fill the silence.

Then the highest-leverage single change in the whole design, at the cost of one extra call per finding: a second pass whose only job is to destroy the findings. Give it the finding and the same context, but not the proposer’s reasoning and not its confidence score, so it cannot anchor on the justification.

You are the refuter. Below is a finding another reviewer proposed and the
exact context that reviewer was given. Your job is to KILL it.

Work through these in order:
1. Is the trigger reachable? Look for the guard that prevents it.
2. Is the consequence real, or handled downstream in the context shown?
3. Is the claim about a line this diff did not change?
4. Does CONVENTIONS explicitly permit this pattern?
5. Is the finding true but unverifiable from the context given - would
   checking it require reading code that is not here?

Return exactly: {"verdict": "KILL" | "SURVIVES", "reason": "..."}

Default to KILL. "I cannot verify this either way" is a KILL.
You are not scored on how many findings you let through.

Two details matter more than the wording. The default must be KILL — an unverifiable finding dies, because posting it makes a human do the verification you could not. And the refuter must be told it is not scored on throughput; without that, models drift toward letting things pass to seem agreeable. Case 5 is the one that surprises people: findings that are probably true but not checkable from the given context are the most expensive comments you can post, because they generate a long thread that ends in “actually that’s fine, see the other service”.

Wiring it into CI without handing out your keys

Four stages, each a separate step so you can inspect the intermediate JSON when a bad comment escapes.

name: ai-review
on: pull_request

permissions:
  contents: read
  pull-requests: write

jobs:
  review:
    runs-on: ubuntu-latest
    env:
      MODEL_API_KEY: ${{ secrets.MODEL_API_KEY }}
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Collect diff and one-hop context
        run: |
          tools/review/collect.sh \
            "${{ github.event.pull_request.base.sha }}" \
            "${{ github.event.pull_request.head.sha }}" > /tmp/context.json
      - run: tools/review/propose.sh /tmp/context.json > /tmp/proposed.json
      - run: |
          tools/review/refute.sh /tmp/context.json /tmp/proposed.json \
            > /tmp/survivors.json
      - run: tools/review/post.sh /tmp/survivors.json --max 5

Post one review, not N comments. The reviews endpoint accepts a comments array in a single request, so the author gets one notification instead of five. Send "event": "COMMENT".

Dedupe across pushes. End each comment body with an invisible marker — <!-- rv:sha256(file + normalized_claim) --> — read existing comments before posting, and skip anything already there. Hash the path plus normalized claim text rather than the line number, because rebases break line-based deduping.

Never check out fork code with a privileged token. Workflows triggered by pull_request from a fork get a read-only GITHUB_TOKEN and no secrets, which is the protection you want. The tempting workaround is pull_request_target, which runs with the base repo’s secrets — combining that with a checkout of the PR head hands your API keys to anyone who opens a pull request. For fork coverage, run the model in the restricted job and post from a separate workflow_run job that never executes fork code.

Action rate is the only number that tunes anything

Comment count, findings per PR and “issues detected” are vanity. The metric that matters is the fraction of posted comments that cause a change:

Action rate = (comments whose anchored lines changed before merge, plus comments a human replied to in agreement) ÷ comments posted.

Instrumenting it is mechanical. Each comment already carries its finding ID in the dedupe marker. A job walks recently merged PRs, pulls review comments through the REST API, and for each one diffs the anchored file between the comment’s commit and the merge commit to see whether the flagged range moved. Thread resolution state is available through GitHub’s GraphQL API, but treat it as a weak signal — people resolve threads to clear the UI, not to signal agreement.

Then tune against it. Action rate falling: raise the bar — drop S3 entirely, lower the cap, raise the confidence floor. Action rate high but the bot nearly silent: you are over-filtering, so lower the confidence floor one notch and watch what comes back. Volume rising while action rate holds: the conventions file is earning its keep.

The aggregate is less useful than the breakdown. Group action rate by the convention rule that produced the finding, sort ascending, and delete the worst rule. A small number of rules usually produce most of the noise, and removing one is a bigger precision win than any prompt edit. My own threshold, and this is opinion rather than measurement: if fewer than half your posted comments produce a change, you are spending trust faster than you earn it, and the fix is to post less rather than to explain better.

”The bot approved it” must never be a thing anyone can say

Silence from the reviewer has two possible causes: there was no defect, or the defect was outside what the reviewer could see. Those look identical from outside, and the whole design above makes the second one common — you scoped it to the diff, capped it at five, and told a refuter to kill anything it could not verify. Low recall is the price you agreed to pay.

Do not let it emit APPROVE. The reviews API takes APPROVE, REQUEST_CHANGES or COMMENT. Only ever send COMMENT. An approval is a social claim — that someone competent looked and will be associated with the outcome — and the tool cannot back it. Do not let it satisfy a review requirement: keep required reviewers and CODEOWNERS exactly as they were, because if the bot’s check can turn a PR green, someone will eventually ship on it. And do not let it block. The only category with a real argument for blocking is a credential committed in the diff, and that is a deterministic secret scanner’s job, not a model’s. So in practice the bot blocks on nothing.

What it should be is a very good colleague who reads the diff first, says three things a quarter that save you a weekend, and is otherwise quiet. That is a much smaller product than the one people usually build, and it is the only version still installed six months later.

FAQ

Won’t a cap of five make it miss real bugs? Yes, sometimes. That is the deliberate trade: a missed finding leaves you where you were before the bot existed, while a wrong finding permanently degrades how the next one is read. You are not replacing human review, so the floor is not zero — the humans are still there.

Do I need a frontier model for this? The proposer benefits most from a strong model, since spotting a cross-file sequencing bug is the hard part. The refuter is a much narrower task — read a claim, check it against provided context, default to no — and is a reasonable place for a smaller model. Measure action rate before and after downgrading either one instead of guessing.

Where do conventions come from if we’ve never written them down? From your merged PRs. Export the human review comments from the last few months, cluster them by theme, and keep what recurs across different reviewers. Write each as a rule plus at least one explicit “NOT A FINDING” counterexample, then delete any rule whose findings show a poor action rate after a few weeks.

What about very large PRs or monorepos? Scope context by the CODEOWNERS path owning the changed files, and keep the cap per PR rather than per package. If more than five genuine S1/S2 findings survive refutation, the honest output is five comments plus a note that the PR is too large to review properly.


The severity ladder, the five-comment cap and the fifty-percent action-rate threshold are my opinions, not measured results — starting points to tune against your own numbers. The GitHub Actions behaviour described (fork token restrictions, the reviews endpoint’s event values, batching comments into one review) is documented, but CI platforms change; verify workflow permissions and API shapes against the current GitHub docs before shipping this.


Originally published on FlutterCook. Read the latest version there — that copy is the one kept up to date.

Nhận xét

Bài đăng phổ biến từ blog này

Thiết kế giao diện với DotNetBar (Phần 1)

Đây là phiên bản DotNetBar hỗ trợ C# và Visual Basic https://www.dropbox.com/s/wx80jpvgnlrmtux/DotNetBar.rar  , phiên bản này hỗ trợ giao diện Metro cực kỳ “dễ thương” Các bạn load về và cài đặt, khi cài đặt xong sẽ có source code mẫu của tất cả các control. Để sử dụng được các control của DotNetBar các bạn nhớ add item vào controls box. Thiết kế giao diện với DotNetBar, giao diện sẽ rất đẹp. Link các video hướng dẫn chi tiết cách sử dụng và coding: http://www.devcomponents.com/dotnetbar/movies.aspx Hiện tại DotNetBar có rất nhiều công cụ cực mạnh, trong đó có 3 công cụ dưới đây: DotNetBar for Windows Forms Requires with Visual Studio 2003, 2005, 2008, 2010 or 2012.   DotNetBar for WPF Requires with Visual Studio 2010 or 2012 and Windows Presentation Foundation.   DotNetBar for Silverlight Requires with Visual Studio 2010 or 2012 and Silverlight. Dưới đây là một số hình ảnh về các control trong DotnetBar.   Metro User Interface  controls with Metro Tiles, toolba...

5 concepts every Flutter dev should know

  Phụ lục: State management architecture Testing IDE Shortcuts Platform channel Maintaining a project Tôi đã làm việc với Flagship trong một thời gian dài, và đây là những điều mà tôi phát hiện ra là điều cần phải có đối với bất kỳ nhà phát triển Flagship nào, về tổng thể nó sẽ khiến bạn trở thành một nhà phát triển Flagship giỏi trong thời gian dài. 1. State management architecture Đây là một trong những chủ đề quan trọng nhất trong cộng đồng thiết bị rung, nó khá quan trọng nếu bạn muốn duy trì một dự án rung kích thước trung bình hoặc lớn. Nó sẽ giúp tạo một dự án suôn sẻ và thêm các tính năng mới một cách hoàn hảo.  2. Testing Đây là một chủ đề duy nhất mà tôi không hiểu tại sao nó lại quan trọng trước đó trong sự nghiệp của tôi, nhưng khi tôi tiến lên trong sự nghiệp của mình và có kinh nghiệm với nhiều dự án và vấn đề xảy ra trong môi trường sản xuất. Tôi đã nhận ra một cách khó khăn, tại sao điều này lại quan trọng như vậy. Nếu bạn vẫn muốn có thêm lý do để cân nhắc thử...

Announcing Flutter 2

  Phụ lục: Flutter on the web Flutter 2 on desktops, foldables, and embedded devices The growing Flutter ecosystem Dart: The secret sauce behind Flutter Flutter 2: Available now Hôm nay, chúng tôi sẽ công bố Flutter 2: một bản nâng cấp lớn cho Flutter cho phép các nhà phát triển tạo các ứng dụng đẹp, nhanh chóng và di động cho bất kỳ nền tảng nào. Với Flutter 2, bạn có thể sử dụng cùng một cơ sở mã để gửi các ứng dụng gốc cho năm hệ điều hành: IOS, Android, Windows, macOS và Linux; cũng như trải nghiệm web nhắm mục tiêu các trình duyệt như Chrome, Firefox, Safari hoặc Edge. Flutter thậm chí có thể được nhúng vào ô tô, TV và thiết bị gia dụng thông minh, mang đến trải nghiệm di động và lan tỏa nhất cho thế giới điện toán xung quanh. Mục tiêu của chúng tôi là thay đổi cơ bản cách các nhà phát triển nghĩ về việc xây dựng ứng dụng, bắt đầu không phải với nền tảng bạn đang nhắm mục tiêu mà là với trải nghiệm bạn muốn tạo. Flutter cho phép bạn tạo ra những trải nghiệm tuyệt đẹp trong đó ...