Introduction to Building a Code Review Agent with llama.cpp
Code review is one of the most time-consuming yet critical activities in software development. With the rise of local large language models (LLMs), developers can now build autonomous code review agents that run entirely on their own hardware—no API keys, no data leaving the machine, and no per-token costs. llama.cpp is the leading C++ inference engine for running quantized LLMs efficiently on consumer hardware, making it the perfect foundation for a code review agent.
In this tutorial, you'll learn how to build a complete code review agent using llama.cpp and its Python bindings. The agent will analyze source files, identify potential bugs, suggest improvements, and produce structured review reports—all running locally.
What Is a Code Review Agent?
A code review agent is an automated system that examines source code and provides feedback similar to what a human reviewer would offer. It typically performs:
- Static analysis of syntax and structure
- Detection of common anti-patterns and bugs
- Security vulnerability identification
- Style and readability suggestions
- Performance optimization recommendations
By leveraging an LLM through llama.cpp, the agent can understand code semantics beyond what traditional linters provide, offering contextual suggestions that consider the intent and logic of the code.
Why llama.cpp?
There are several compelling reasons to choose llama.cpp for this project:
- Privacy: All inference happens locally. Proprietary code never leaves your machine.
- Cost: No recurring API fees. Once you have the model, inference is free.
- Performance: Highly optimized C++ backend with support for CPU, CUDA, Metal, and Vulkan acceleration.
- Flexibility: Supports GGUF models from multiple families including Llama, Mistral, Qwen, DeepSeek, and more.
- Control: Full control over inference parameters, context windows, and system prompts.
Setting Up Your Environment
Before building the agent, you need to install llama.cpp and download a suitable model. We'll use the Python bindings (llama-cpp-python) for easier integration.
Installing llama-cpp-python
For a CPU-only installation, use pip directly:
pip install llama-cpp-python
If you have an NVIDIA GPU and want CUDA acceleration, install with the appropriate build flags:
CMAKE_ARGS="-DGGML_CUDA=on" pip install llama-cpp-python --upgrade --force-reinstall --no-cache-dir
For Apple Silicon (M1/M2/M3/M4), Metal acceleration is enabled by default on recent versions, but you can be explicit:
CMAKE_ARGS="-DGGML_METAL=on" pip install llama-cpp-python --upgrade --force-reinstall --no-cache-dir
Verify the installation works:
python -c "from llama_cpp import Llama; print('llama-cpp-python installed successfully')"
Downloading a Model
For code review tasks, you want a model that excels at code understanding. Good options include:
- Qwen2.5-Coder-7B-Instruct — Excellent code understanding in a manageable size
- DeepSeek-Coder-V2-Lite-Instruct — Strong multi-language code analysis
- Llama-3.1-8B-Instruct — General purpose with solid coding capabilities
Download a quantized GGUF model from Hugging Face. For this tutorial, we'll use Qwen2.5-Coder-7B in Q4_K_M quantization, which offers a good balance of quality and performance:
# Using huggingface-cli
pip install huggingface-hub
huggingface-cli download Qwen/Qwen2.5-Coder-7B-Instruct-GGUF qwen2.5-coder-7b-instruct-q4_k_m.gguf --local-dir ./models
Alternatively, you can download directly with Python:
from huggingface_hub import hf_hub_download
model_path = hf_hub_download(
repo_id="Qwen/Qwen2.5-Coder-7B-Instruct-GGUF",
filename="qwen2.5-coder-7b-instruct-q4_k_m.gguf",
local_dir="./models"
)
print(f"Model downloaded to: {model_path}")
Building the Code Review Agent
Now let's build the actual agent. We'll structure it as a Python class that handles model loading, prompt construction, file reading, and review generation.
Core Architecture
The agent will consist of several components:
- ModelManager: Handles loading and configuring the LLM
- CodeReader: Reads and preprocesses source files
- ReviewEngine: Constructs prompts and generates reviews
- ReportFormatter: Formats the output into readable reports
Implementing the Agent
Create a file called code_review_agent.py and add the following implementation:
import os
import json
from pathlib import Path
from typing import Optional
from llama_cpp import Llama
class CodeReviewAgent:
"""A local code review agent powered by llama.cpp."""
SYSTEM_PROMPT = """You are an expert code reviewer with deep knowledge of software engineering best practices, security, and performance optimization.
Your task is to review the provided code and provide a thorough, structured analysis.
Always format your response as valid JSON with the following structure:
{
"summary": "Brief overall assessment of the code",
"issues": [
{
"severity": "critical|high|medium|low|info",
"type": "bug|security|performance|style|maintainability",
"line": "approximate line number or range, or null",
"description": "detailed description of the issue",
"suggestion": "specific fix or improvement recommendation"
}
],
"positive_aspects": ["list of things done well"],
"overall_score": "number from 1 to 10"
}
Be precise and actionable. If there are no issues, return an empty issues array. Only report real issues—do not invent problems."""
def __init__(
self,
model_path: str,
n_ctx: int = 8192,
n_gpu_layers: int = -1,
n_threads: Optional[int] = None,
verbose: bool = False,
):
"""Initialize the code review agent with a llama.cpp model.
Args:
model_path: Path to the GGUF model file.
n_ctx: Context window size in tokens.
n_gpu_layers: Number of layers to offload to GPU (-1 for all).
n_threads: Number of CPU threads (None for auto).
verbose: Whether to print llama.cpp internal logs.
"""
self.model_path = model_path
self.n_ctx = n_ctx
self.llm = Llama(
model_path=model_path,
n_ctx=n_ctx,
n_gpu_layers=n_gpu_layers,
n_threads=n_threads,
verbose=verbose,
)
def _build_review_prompt(self, file_path: str, code: str) -> str:
"""Build the user prompt for reviewing a specific file."""
file_ext = Path(file_path).suffix or "unknown"
return f"""Please review the following code file.
File: {file_path}
Language: {file_ext}
{file_ext}
{code}
Analyze this code for bugs, security issues, performance problems, style issues, and maintainability concerns. Return your analysis as JSON."""
def _extract_json(self, text: str) -> dict:
"""Attempt to extract a JSON object from the model's response."""
# Try to find JSON between curly braces
start = text.find("{")
end = text.rfind("}")
if start != -1 and end != -1 and end > start:
json_str = text[start : end + 1]
try:
return json.loads(json_str)
except json.JSONDecodeError:
pass
return {"raw_response": text}
def review_code(
self,
code: str,
file_path: str = "snippet.py",
max_tokens: int = 2048,
temperature: float = 0.1,
) -> dict:
"""Review a code string and return structured feedback.
Args:
code: The source code to review.
file_path: Virtual file path for context.
max_tokens: Maximum tokens to generate.
temperature: Sampling temperature (lower = more focused).
Returns:
Dictionary containing the review results.
"""
user_prompt = self._build_review_prompt(file_path, code)
response = self.llm.create_chat_completion(
messages=[
{"role": "system", "content": self.SYSTEM_PROMPT},
{"role": "user", "content": user_prompt},
],
max_tokens=max_tokens,
temperature=temperature,
top_p=0.9,
)
raw_output = response["choices"][0]["message"]["content"]
review = self._extract_json(raw_output)
review["model"] = os.path.basename(self.model_path)
review["file"] = file_path
return review
def review_file(self, file_path: str, max_tokens: int = 2048) -> dict:
"""Read a file from disk and review it.
Args:
file_path: Path to the source file.
max_tokens: Maximum tokens to generate.
Returns:
Dictionary containing the review results.
"""
path = Path(file_path)
if not path.exists():
raise FileNotFoundError(f"File not found: {file_path}")
code = path.read_text(encoding="utf-8")
return self.review_code(code, file_path=str(path), max_tokens=max_tokens)
def review_directory(
self,
directory: str,
extensions: tuple = (".py", ".js", ".ts", ".java", ".cpp", ".c", ".go", ".rs"),
max_tokens: int = 2048,
) -> list:
"""Review all source files in a directory.
Args:
directory: Path to the directory to scan.
extensions: Tuple of file extensions to include.
max_tokens: Maximum tokens per review.
Returns:
List of review dictionaries.
"""
reviews = []
dir_path = Path(directory)
for file_path in sorted(dir_path.rglob("*")):
if file_path.is_file() and file_path.suffix in extensions:
print(f"Reviewing: {file_path}")
try:
review = self.review_file(str(file_path), max_tokens=max_tokens)
reviews.append(review)
except Exception as e:
reviews.append({
"file": str(file_path),
"error": str(e),
})
return reviews
Using the Agent
Now let's create a script that demonstrates how to use the agent to review code. Create a file called run_review.py:
from code_review_agent import CodeReviewAgent
import json
# Initialize the agent
agent = CodeReviewAgent(
model_path="./models/qwen2.5-coder-7b-instruct-q4_k_m.gguf",
n_ctx=8192,
n_gpu_layers=-1, # Use GPU for all layers if available
verbose=False,
)
# Example: Review a code snippet with intentional issues
sample_code = '''import os
def read_user_file(filename):
"""Read a file provided by the user."""
path = "/data/" + filename
with open(path, "r") as f:
return f.read()
def process_items(items):
results = []
for i in range(len(items)):
results.append(items[i] * 2)
return results
def get_config_value(key, config={}):
return config.get(key, None)
class DataProcessor:
def __init__(self):
self.data = []
def add(self, item):
self.data.append(item)
def process(self):
total = 0
for item in self.data:
total += item
return total / len(self.data)
'''
# Review the code
review = agent.review_code(sample_code, file_path="example.py")
# Print the structured review
print(json.dumps(review, indent=2))
When you run this script, the agent will analyze the code and return a structured JSON review. The sample code above contains several intentional issues that the agent should identify:
- Path traversal vulnerability in
read_user_file - Unnecessary use of
range(len())inprocess_items - Mutable default argument in
get_config_value - Potential division by zero in
DataProcessor.process
Reviewing Files and Directories
To review actual files on disk, use the review_file and review_directory methods:
from code_review_agent import CodeReviewAgent
import json
agent = CodeReviewAgent(
model_path="./models/qwen2.5-coder-7b-instruct-q4_k_m.gguf",
n_ctx=8192,
n_gpu_layers=-1,
)
# Review a single file
review = agent.review_file("src/main.py")
print(json.dumps(review, indent=2))
# Review an entire project directory
reviews = agent.review_directory("src/", extensions=(".py",))
# Save all reviews to a file
with open("review_report.json", "w") as f:
json.dump(reviews, f, indent=2)
print(f"Reviewed {len(reviews)} files. Report saved to review_report.json")
Generating Human-Readable Reports
Structured JSON is great for programmatic use, but you'll often want a readable report. Let's add a report formatter to our agent. Create report_formatter.py:
import json
from datetime import datetime
from pathlib import Path
SEVERITY_ICONS = {
"critical": "[CRITICAL]",
"high": "[HIGH]",
"medium": "[MEDIUM]",
"low": "[LOW]",
"info": "[INFO]",
}
def format_review_markdown(review: dict) -> str:
"""Convert a single review dictionary to a markdown report."""
lines = []
lines.append(f"# Code Review Report")
lines.append(f"")
lines.append(f"**File:** `{review.get('file', 'unknown')}`")
lines.append(f"**Model:** {review.get('model', 'unknown')}")
lines.append(f"**Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
lines.append(f"")
if "error" in review:
lines.append(f"**Error:** {review['error']}")
return "\n".join(lines)
# Summary
summary = review.get("summary", "No summary provided.")
lines.append(f"## Summary")
lines.append(f"")
lines.append(f"{summary}")
lines.append(f"")
# Overall score
score = review.get("overall_score", "N/A")
lines.append(f"**Overall Score:** {score}/10")
lines.append(f"")
# Issues
issues = review.get("issues", [])
if issues:
lines.append(f"## Issues Found ({len(issues)})")
lines.append(f"")
for i, issue in enumerate(issues, 1):
severity = issue.get("severity", "info")
icon = SEVERITY_ICONS.get(severity, "[INFO]")
issue_type = issue.get("type", "general")
line_num = issue.get("line", "N/A")
description = issue.get("description", "No description.")
suggestion = issue.get("suggestion", "No suggestion provided.")
lines.append(f"### {i}. {icon} {issue_type.capitalize()} Issue")
lines.append(f"")
lines.append(f"**Line:** {line_num}")
lines.append(f"")
lines.append(f"**Description:** {description}")
lines.append(f"")
lines.append(f"**Suggestion:** {suggestion}")
lines.append(f"")
else:
lines.append(f"## Issues Found")
lines.append(f"")
lines.append(f"No issues found. The code looks clean!")
lines.append(f"")
# Positive aspects
positives = review.get("positive_aspects", [])
if positives:
lines.append(f"## Positive Aspects")
lines.append(f"")
for positive in positives:
lines.append(f"- {positive}")
lines.append(f"")
return "\n".join(lines)
def format_reviews_markdown(reviews: list) -> str:
"""Convert multiple reviews into a combined markdown report."""
sections = []
sections.append("# Code Review Summary Report")
sections.append("")
sections.append(f"**Files Reviewed:** {len(reviews)}")
sections.append(f"**Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
sections.append("")
sections.append("---")
sections.append("")
for review in reviews:
sections.append(format_review_markdown(review))
sections.append("")
sections.append("---")
sections.append("")
return "\n".join(sections)
def save_report(reviews: list, output_path: str = "code_review_report.md"):
"""Save reviews as a markdown report file."""
report = format_reviews_markdown(reviews)
Path(output_path).write_text(report, encoding="utf-8")
print(f"Report saved to {output_path}")
# Example usage
if __name__ == "__main__":
# Load saved reviews and generate a report
with open("review_report.json", "r") as f:
reviews = json.load(f)
save_report(reviews, "code_review_report.md")
Integrating with Git Diffs
A practical code review agent should focus on changes rather than reviewing entire codebases every time. Here's how to integrate with Git to review only modified files:
import subprocess
from pathlib import Path
from code_review_agent import CodeReviewAgent
from report_formatter import save_report
def get_git_diff_files(branch: str = "main") -> list:
"""Get list of files changed compared to the given branch."""
result = subprocess.run(
["git", "diff", "--name-only", branch],
capture_output=True,
text=True,
)
return [f for f in result.stdout.strip().split("\n") if f]
def get_git_diff_content(file_path: str, branch: str = "main") -> str:
"""Get the diff content for a specific file."""
result = subprocess.run(
["git", "diff", branch, "--", file_path],
capture_output=True,
text=True,
)
return result.stdout
def review_git_changes(model_path: str, branch: str = "main"):
"""Review all changed files compared to a branch."""
agent = CodeReviewAgent(
model_path=model_path,
n_ctx=8192,
n_gpu_layers=-1,
)
changed_files = get_git_diff_files(branch)
print(f"Found {len(changed_files)} changed files compared to {branch}")
reviews = []
for file_path in changed_files:
if not Path(file_path).exists():
print(f" Skipping deleted file: {file_path}")
continue
print(f" Reviewing: {file_path}")
diff_content = get_git_diff_content(file_path, branch)
# Review the current version of the file
review = agent.review_file(file_path)
review["diff"] = diff_content
reviews.append(review)
return reviews
if __name__ == "__main__":
reviews = review_git_changes(
model_path="./models/qwen2.5-coder-7b-instruct-q4_k_m.gguf",
branch="main",
)
save_report(reviews, "git_review_report.md")
Best Practices
To get the most out of your code review agent, follow these best practices:
Choose the Right Model Size
Model selection directly impacts review quality and performance. For code review tasks:
- 7B models (Q4_K_M quantization, ~4.5GB RAM): Good for quick reviews on machines with 8GB+ RAM. Suitable for individual developers.
- 14B models (Q4_K_M quantization, ~9GB RAM): Better reasoning and nuance. Requires 16GB+ RAM. Good balance for most teams.
- 32B+ models: Highest quality reviews but require significant hardware (32GB+ RAM or a capable GPU).
Start with a 7B model and scale up if you find the reviews lack depth or miss subtle issues.
Optimize Inference Parameters
The temperature and sampling parameters significantly affect review quality. For code review:
- Use a low temperature (0.1–0.3) to reduce randomness and keep the model focused on actual issues.
- Set
top_pto 0.9 to slightly limit the token pool while maintaining natural output. - Use a large context window (8192+ tokens) to ensure the model can see entire files.
- Set
max_tokenshigh enough (2048+) to allow complete responses, especially for files with many issues.
Handle Large Files Gracefully
Large source files may exceed the model's context window. Implement chunking to handle this:
def review_large_file(self, file_path: str, chunk_size: int = 200) -> list:
"""Review a large file by splitting it into chunks."""
path = Path(file_path)
lines = path.read_text(encoding="utf-8").splitlines()
reviews = []
for i in range(0, len(lines), chunk_size):
chunk = "\n".join(lines[i : i + chunk_size])
start_line = i + 1
end_line = min(i + chunk_size, len(lines))
review = self.review_code(
chunk,
file_path=f"{file_path} (lines {start_line}-{end_line})",
)
reviews.append(review)
return reviews
Use Structured Output Consistently
Always instruct the model to return structured JSON. This makes it easy to:
- Programmatically filter issues by severity
- Integrate with CI/CD pipelines to fail builds on critical issues
- Track code quality metrics over time
- Generate dashboards and trend reports
If the model occasionally fails to produce valid JSON, implement a retry mechanism or fall back to regex-based extraction as shown in the _extract_json method.
Cache and Batch Reviews
Running inference is computationally expensive. To avoid redundant work:
- Cache review results keyed by file content hash
- Only review files that have changed since the last review
- Batch multiple small files into a single prompt when possible
- Run reviews asynchronously if processing multiple files
Here's a simple caching implementation:
import hashlib
import json
from pathlib import Path
def get_file_hash(file_path: str) -> str:
"""Generate a hash of file contents for cache keys."""
content = Path(file_path).read_bytes()
return hashlib.sha256(content).hexdigest()
def get_cached_review(file_path: str, cache_dir: str = ".review_cache") -> dict:
"""Retrieve a cached review if the file hasn't changed."""
file_hash = get_file_hash(file_path)
cache_file = Path(cache_dir) / f"{file_hash}.json"
if cache_file.exists():
return json.loads(cache_file.read_text())
return None
def save_cached_review(file_path: str, review: dict, cache_dir: str = ".review_cache"):
"""Save a review to the cache."""
file_hash = get_file_hash(file_path)
cache_file = Path(cache_dir) / f"{file_hash}.json"
Path(cache_dir).mkdir(exist_ok=True)
cache_file.write_text(json.dumps(review, indent=2))
Validate Model Suggestions
LLMs can produce plausible but incorrect suggestions. Always treat the agent's output as advisory:
- Have human reviewers confirm critical findings before acting on them
- Use the agent as a first pass, not a replacement for human review
- Cross-reference security findings with dedicated tools like Bandit or Semgrep
- Track false positive rates and adjust your system prompt accordingly
Conclusion
Building a code review agent with llama.cpp gives you a powerful, private, and cost-effective tool for improving code quality. By running entirely locally, the agent can review proprietary code without any data privacy concerns. The structured JSON output makes it easy to integrate into existing development workflows, from pre-commit hooks to CI/CD pipelines. Start with a 7B model like Qwen2.5-Coder, experiment with the inference parameters to match your quality requirements, and scale up to larger models as your hardware allows. Remember that the agent is a complement to human review, not a replacement—use it to catch low-hanging fruit and surface potential issues early, letting human reviewers focus on architecture, business logic, and the nuanced decisions that require human judgment. With the foundation built in this tutorial, you can extend the agent with features like multi-language support, diff-aware reviews, IDE integration, and automated fix suggestions to create a comprehensive local code quality pipeline.