Improving AI Code Review Bot Comment Quality Through Vector Embeddings

greptile · Tech · 2024 · source Agentic: yes · promotional bias: low · source-backed · extracted with prompt 0.5.6 (schema v2)

Generated from extraction.json — do not edit by hand. Every claim cites verbatim source evidence; stated means the quote directly asserts the claim, implied means it supports it indirectly.

Problem

Domain: AI code-review comment quality and noise reduction

Greptile's AI code-review bot produced too many technically true but low-value comments, causing developers to ignore the bot. The team needed to reduce noisy nit comments while preserving critical findings, define an objective quality metric, and adapt quality thresholds to each team's subjective standards without tying the product to one fine-tuned model.

Constraints:

Architecture

Greptile built a feedback-driven semantic filter around its code-review bot. The bot still generates review comments, but developer behavior supplies the quality signal: whether comments are addressed in subsequent commits, plus upvotes and downvotes. Greptile embeds past comments per team and stores them in a vector database labeled by whether they were addressed/upvoted or downvoted. For each new generated comment, the system embeds the comment, compares it with that team's historical comment vectors, blocks it when it is similar to at least three unique downvoted comments, passes it when similar to at least three upvoted comments, and otherwise lets it through. This learns each team's bar for 'nittiness' without changing the base LLM.

Diagram confidence: reconstructed (reconstructed from prose).

flowchart LR
  Pull_request["Pull request"] --> AI_code_review_bot["AI code review bot"]
  AI_code_review_bot --> Candidate_comment_generator["Candidate comment generator"]
  Candidate_comment_generator --> Comment_embedding_generator["Comment embedding generator"]
  Comment_embedding_generator --> Similarity_filter["Similarity filter"]
  Similarity_filter --> AI_code_review_bot
  AI_code_review_bot --> Posted_comments["Posted comments"]
  Posted_comments --> Developer["Developer"]
  Developer --> Address_rate_evaluator["Address-rate evaluator"]
  Pull_request --> Address_rate_evaluator
  Developer --> Reaction_signal_collector["Reaction signal collector"]
  Address_rate_evaluator --> Team_feedback_store["Team feedback store"]
  Reaction_signal_collector --> Team_feedback_store
  Team_feedback_store --> Comment_embedding_generator
  Comment_embedding_generator --> Team_vector_database["Team vector database"]
  Team_vector_database --> Similarity_filter
Drag to pan · scroll or use +/− to zoom · reconstructed from the source's described component topology

Components

Component Role Interacts with
Pull request Code change under review with diffs and subsequent commits. AI code review bot, Address-rate evaluator
AI code review bot Greptile product that reviews pull requests with context from the wider codebase and produces candidate comments. Pull request, Candidate comment generator, Similarity filter, Posted comments
Candidate comment generator LLM-powered review step that surfaces bugs, anti-patterns, repeated code, and other possible review comments. AI code review bot, Comment embedding generator
Posted comments Comments that survive filtering and appear on the pull request for developers. AI code review bot, Developer
Developer PR author or reviewer whose actions define whether comments are valuable. Posted comments, Reaction signal collector, Address-rate evaluator
Address-rate evaluator Quality metric that scans subsequent diffs to see whether a generated comment was addressed before merge. Pull request, Developer, Team feedback store
Reaction signal collector Collects upvote/downvote comment feedback where available. Developer, Team feedback store
Team feedback store Team-partitioned history of addressed, upvoted, and downvoted comments. Address-rate evaluator, Reaction signal collector, Comment embedding generator, Team vector database
Comment embedding generator Converts past and candidate comments into vector embeddings. Candidate comment generator, Team feedback store, Team vector database, Similarity filter
Team vector database Vector database partitioned by team, storing semantic representations of historical accepted and rejected comments. Comment embedding generator, Similarity filter
Similarity filter Blocks or passes new comments based on cosine similarity to at least three unique downvoted or upvoted historical comments. Comment embedding generator, Team vector database, AI code review bot

Data flow

A pull request arrives, the AI code-review tool generates candidate comments using codebase context, and Greptile evaluates historical quality signals by scanning later diffs for addressed comments and collecting reactions. Past comments are embedded and stored per team in a vector database. A new candidate comment is embedded, compared against the team's downvoted and upvoted clusters by cosine similarity, and filtered according to the three-similar-comments rule before comments are posted to the pull request.

3 supporting quotes
“We could check which comments the author actually addressed in the code by scanning the diffs of the subsequent commits.”article, stated
“we started generating embeddings vectors of past comments on a per-team level that were addressed/upvoted or downvoted by developers and storing them in a vector database.”article, stated
“When Greptile generated a comment, we generated its vector embedding and ran it through a simple filter:”article, stated

Why this architecture

The final architecture uses observed developer behavior rather than trying to encode comment quality in a prompt or a universal judge. Because nits are subjective by team, Greptile stores feedback per team and filters by semantic similarity to prior team-specific reactions. This keeps the system model-agnostic, avoids another LLM inference call in the workflow, and lets the filter learn recurring nit clusters organically.

3 supporting quotes
“Nits are subjective - definitions and standards vary from team to team”article, stated
“The bot would somehow need to infer where the bar for “nittiness” was for the team, and then filter comments accordingly.”article, stated
“We considered fine-tuning, but the cost, speed, and lack of portability (Greptile would no longer be model-agnostic) ruled it out.”article, stated

Models used: LLM-based code review generator · embedding model for comments · LLM-as-judge severity scorer (failed experiment)

Scale

Alternatives considered (roads not taken)

Use GitHub thumbs up/down as the main quality signal

Rejected because: The team chose addressed-in-code behavior instead because it produced a more objective metric tied to developer action.

"There were two ideas here:

  1. GitHub lets developers react to comments with 👍/👎. We could use this as the quality indicator."
“We picked the latter, which also gave us our performance metric - percentage of generated comments that the author actually addresses.”article, stated

Prompt engineer the base LLM to write fewer nits

Rejected because: Prompting reduced critical comments along with nits.

“Sadly, even with all kinds of prompting tricks, we simply could not get the LLM to produce fewer nits without also producing fewer critical comments.”article, stated

Few-shot examples of good and bad comments

Rejected because: The model learned superficial characteristics and made the bot worse.

“This also did not work. If anything, this made the bot even worse because it didn’t actually find a useful pattern across the available samples”article, stated
“instead inferred superficial characteristics.”article, stated

LLM-as-a-judge severity score

Rejected because: The judge was nearly random and added another inference call, making the workflow slow.

“The LLMs judgment of its own output was nearly random. This also made the bot extremely slow because there was now a whole new inference call”article, stated

Fine-tune the model

Rejected because: Fine-tuning was too costly, slow, and incompatible with Greptile's model-agnostic posture.

“We considered fine-tuning, but the cost, speed, and lack of portability (Greptile would no longer be model-agnostic) ruled it out.”article, stated

Patterns

agent-memory (memory)

Greptile stores per-team semantic histories of accepted and rejected comments so future filtering reflects that team's standards.

“generating embeddings vectors of past comments on a per-team level that were addressed/upvoted or downvoted by developers and storing them in a vector database.”article, stated
“Nits are subjective - definitions and standards vary from team to team”article, stated

confidence-thresholding (quality)

A new comment is blocked or passed only when cosine similarity exceeds a threshold against at least three unique historical comments of one class.

“If the comment had a cosine similarity exceeding some threshold with at least 3 unique downvoted comments, it would get blocked.”article, stated
“The same situation but with three upvoted comments, it would pass.”article, stated

data-quality-loop (quality)

Developer reactions and addressed-comment behavior continuously add examples to the team-specific filtering memory.

“We could check which comments the author actually addressed in the code by scanning the diffs of the subsequent commits.”article, stated
“Users downvote nit comments, and when enough comments of the same type are downvoted, the bot can filter out any new comments of that type.”article, stated

eval-driven-development (quality)

The team defined address rate as the north-star metric and iterated through failed and successful approaches against that metric.

“We picked the latter, which also gave us our performance metric - percentage of generated comments that the author actually addresses.”article, stated
“Within two weeks of rolling out this feature, existing users saw address rate (percentage of Greptile’s comments that devs address before merging) go from 19% to 55+%.”article, stated

llm-as-judge (quality)

Greptile tried an LLM judge for severity scoring, but the pattern failed in this subjective comment-quality setting.

“we figured we would simply add a filtering step where the LLM could rate the severity of a comment+diff pair on a 1-10 scale”article, stated
“Sadly, this also failed. The LLMs judgment of its own output was nearly random.”article, stated

Tradeoffs

Quality metric

Chose address rate from subsequent diffs over GitHub thumbs up/down alone.

Rationale: Address rate ties comment value to whether developers actually changed code before merging.

“We could check which comments the author actually addressed in the code by scanning the diffs of the subsequent commits.”article, stated
“We picked the latter, which also gave us our performance metric - percentage of generated comments that the author actually addresses.”article, stated

Post-generation semantic filter

Chose embedding-based comment filtering over changing the LLM with prompt engineering or fine-tuning.

Rationale: The filter reduced noise while preserving model-agnosticism and avoided prompt/fine-tune failure modes.

“The idea was to filter out comments that were very similar to some minimum number of downvoted comments.”article, stated
“We considered fine-tuning, but the cost, speed, and lack of portability (Greptile would no longer be model-agnostic) ruled it out.”article, stated

Conservative pass-through logic

Chose pass comments when neither or both similarity conditions are met over blocking uncertain comments.

Rationale: The rule blocks only comments with clear similarity to repeated downvoted clusters, avoiding aggressive suppression of potentially useful findings.

“* The same situation but with three upvoted comments, it would pass. * If neither or both, it would pass.”article, stated
“The same situation but with three upvoted comments, it would pass.”article, stated

Team-level personalization

Chose per-team vector stores over one global nit filter.

Rationale: Nittiness is subjective, so filtering standards need to adapt to each team's expectations.

“Nits are subjective - definitions and standards vary from team to team”article, stated
“generating embeddings vectors of past comments on a per-team level”article, stated

Accept imperfect but material improvement

Chose ship the embedding filter at 55+% address rate over waiting for a perfect comment-quality solution.

Rationale: The approach nearly tripled the address rate and was the most impactful noise-reduction technique despite remaining imperfect.

“Within two weeks of rolling out this feature, existing users saw address rate (percentage of Greptile’s comments that devs address before merging) go from 19% to 55+%.”article, stated
“While this is far from perfect, this has been far and away the most impactful technique in reducing the noise produced by the LLM.”article, stated

Gotchas (what bit them)

Prompting reduced critical comments along with nits.

Root cause: The model could not cleanly distinguish low-value comments from important comments through prompt instructions alone.

Resolution: Move comment suppression out of the prompt and into a feedback-driven semantic filter.

“even with all kinds of prompting tricks, we simply could not get the LLM to produce fewer nits without also producing fewer critical comments.”article, stated
“Since we couldn’t get the LLM to stop producing nit comments, we figured we would simply add a filtering step”article, stated

Few-shot examples made the bot worse.

Root cause: The LLM inferred superficial properties from examples rather than the deeper standard for useful comments.

Resolution: Use behavior-derived embeddings instead of relying on in-context examples.

“This also did not work. If anything, this made the bot even worse because it didn’t actually find a useful pattern across the available samples”article, stated
“instead inferred superficial characteristics.”article, stated

LLM-as-judge was a bad fit for subjective comment severity.

Root cause: Severity judgments over comment-plus-diff pairs were subjective and context-dependent.

Resolution: Measure actual addressed comments and semantic similarity to team feedback instead of asking another LLM to score severity.

“The LLMs judgment of its own output was nearly random.”article, stated
“This also made the bot extremely slow because there was now a whole new inference call in the workflow.”article, stated

Quality was team-specific, not universal.

Root cause: Definitions of nits vary across teams and cannot be captured by a single global standard.

Resolution: Partition the vector feedback memory by team.

“Nits are subjective - definitions and standards vary from team to team”article, stated
“generating embeddings vectors of past comments on a per-team level”article, stated

Fine-tuning would have reduced model portability.

Root cause: A fine-tuned solution would bind Greptile to a model family while adding cost and slower iteration.

Resolution: Use an external embedding/vector filter that remains model-agnostic.

“We considered fine-tuning, but the cost, speed, and lack of portability (Greptile would no longer be model-agnostic) ruled it out.”article, stated

Evaluation

Greptile evaluated code-review comment quality by tracking whether developers addressed comments in later commits before merge. This address rate replaced more subjective reactions as the primary performance metric, and the team used it to compare prompt engineering, few-shot examples, LLM-as-judge filtering, fine-tuning considerations, and the final embedding-based filter.

Metrics: address rate: percentage of generated comments developers address before merging · initial good comments: approximately 19% · initial incorrect comments: 2% · initial nit comments: 79% · LLM-as-judge severity threshold: below 7 filtered in failed experiment · embedding filter: at least 3 unique similar downvoted comments to block · address rate after rollout: 55+% within two weeks

3 supporting quotes
“We picked the latter, which also gave us our performance metric - percentage of generated comments that the author actually addresses.”article, stated
“We analyzed existing Greptile comments and found that ~19% were good, 2% were flat-out incorrect, and 79% were nits”article, stated
“Within two weeks of rolling out this feature, existing users saw address rate (percentage of Greptile’s comments that devs address before merging) go from 19% to 55+%.”article, stated

Outcomes

This is a strong production lesson in letting user behavior supervise an LLM filter. It is especially useful for pattern synthesis because it shows a failed LLM-as-judge path and a successful semantic feedback memory path on the same problem.

3 supporting quotes
“Remarkably - this works! It turns out that most nits can be placed into a small number of clusters.”article, stated
“Within two weeks of rolling out this feature, existing users saw address rate (percentage of Greptile’s comments that devs address before merging) go from 19% to 55+%.”article, stated
“This is an ongoing problem for us, and I will likely write a part II when we are fortunate enough to see another inflection in address rate!”article, stated