title: "Building an AI-Powered Code Review Assistant"
date: 2026-04-23
readingTime: 9 min read
tags: ["AI", "Developer Tools", "Tutorial"]
After years of manual code reviews, I wanted an assistant that could catch common issues before human reviewers spend time on them.
This tutorial shows how to build a code review assistant using local LLMs—no external API calls, full privacy, and customizable to your team's standards.
Cloud APIs (GPT-4, Claude, etc.):
Local LLMs:
For enterprise code, privacy wins. Let's build.
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Git Hook / │────▶│ Code Review │────▶│ Local LLM │
│ CI Pipeline │ │ Assistant │ │ (Ollama) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ Review Report │
│ (Markdown/ │
│ PR Comment) │
└─────────────────┘
# Install Ollama (local LLM runtime)
curl -fsSL https://ollama.com/install.sh | sh
# Pull a code-focused model
ollama pull codellama:7b-instruct
# Install Python dependencies
pip install ollama rich click
#!/usr/bin/env python3
"""
code_reviewer.py - AI-powered code review assistant
"""
import ollama
from pathlib import Path
import argparse
from rich.console import Console
from rich.panel import Panel
from rich.markdown import Markdown
console = Console()
SYSTEM_PROMPT = """You are an expert code reviewer. Analyze the provided code and:
1. Identify bugs and potential issues
2. Suggest improvements for readability and maintainability
3. Check for security vulnerabilities
4. Verify adherence to best practices
5. Provide specific, actionable feedback
Be concise but thorough. Format your response in markdown."""
def read_file(file_path: str) -> str:
"""Read code from file."""
return Path(file_path).read_text()
def review_code(code: str, language: str = "python") -> str:
"""Send code to local LLM for review."""
prompt = f"""Review this {language} code:
```{language}
{code}
Provide your review in markdown format."""
response = ollama.chat(
model='codellama:7b-instruct',
messages=[
{'role': 'system', 'content': SYSTEM_PROMPT},
{'role': 'user', 'content': prompt}
]
)
return response['message']['content']
def main(): parser = argparse.ArgumentParser(description='AI Code Review Assistant') parser.add_argument('file', help='File to review') parser.add_argument('--lang', '-l', default='python', help='Programming language') parser.add_argument('--output', '-o', help='Output file (default: stdout)')
args = parser.parse_args()
console.print(f"[bold blue]🔍 Reviewing {args.file}...[/bold blue]")
# Read code
code = read_file(args.file)
# Get review
with console.status("[bold green]Analyzing code..."):
review = review_code(code, args.lang)
# Display results
console.print("\n")
console.print(Panel(Markdown(review), title="📋 Code Review", border_style="blue"))
# Save if requested
if args.output:
Path(args.output).write_text(review)
console.print(f"\n[green]✓ Review saved to {args.output}[/green]")
if name == 'main': main()
### Usage
```bash
# Review a Python file
python code_reviewer.py my_script.py --lang python
# Save review to file
python code_reviewer.py my_script.py -o review.md
#!/usr/bin/env python3
"""
pr_reviewer.py - Review entire pull requests
"""
import subprocess
import ollama
from pathlib import Path
from rich.console import Console
from rich.table import Table
from rich.markdown import Markdown
console = Console()
def get_pr_diff(base_branch: str = "main") -> str:
"""Get git diff for current branch vs base."""
result = subprocess.run(
['git', 'diff', base_branch],
capture_output=True,
text=True
)
return result.stdout
def get_changed_files(base_branch: str = "main") -> list:
"""List files changed in current branch."""
result = subprocess.run(
['git', 'diff', '--name-only', base_branch],
capture_output=True,
text=True
)
return result.stdout.strip().split('\n')
def review_pr(diff: str, files: list) -> str:
"""Send PR diff to LLM for review."""
prompt = f"""Review this pull request with the following changes:
Files changed: {', '.join(files)}
Diff:
```diff
{diff}
Provide a comprehensive code review including:
Format your response in markdown."""
response = ollama.chat(
model='codellama:7b-instruct',
messages=[
{'role': 'system', 'content': SYSTEM_PROMPT},
{'role': 'user', 'content': prompt}
]
)
return response['message']['content']
def main(): console.print("[bold blue]🔍 Pull Request Review Assistant[/bold blue]\n")
# Get PR info
with console.status("[bold green]Analyzing changes..."):
diff = get_pr_diff()
files = get_changed_files()
if not diff:
console.print("[yellow]⚠️ No changes detected. Are you on a feature branch?[/yellow]")
return
# Display summary
table = Table(title="📁 Files Changed")
table.add_column("File", style="cyan")
for file in files:
if file:
table.add_row(file)
console.print(table)
# Review
with console.status("[bold green]Running AI review..."):
review = review_pr(diff, files)
# Display
console.print("\n")
console.print(Panel(Markdown(review), title="📋 PR Review", border_style="blue"))
if name == 'main': main()
### Usage
```bash
# Review current branch vs main
python pr_reviewer.py
# Review vs different base branch
python pr_reviewer.py --base develop
Create a .reviewrules file in your project:
# .reviewrules
project_name: "Enterprise ERP"
coding_standards:
language: "C#"
framework: ".NET 8"
naming:
- "Classes should be PascalCase"
- "Methods should be PascalCase"
- "Private fields should be _camelCase"
- "Interfaces should start with I (e.g., IRepository)"
architecture:
- "Use Clean Architecture layers"
- "Domain layer should not depend on Infrastructure"
- "Controllers should be thin (delegate to services)"
- "Use repository pattern for data access"
security:
- "Never log sensitive data (passwords, tokens, PII)"
- "Use parameterized queries (no SQL injection)"
- "Validate all user inputs"
- "Use HTTPS for external API calls"
performance:
- "Use async/await for I/O operations"
- "Avoid N+1 queries (use eager loading)"
- "Dispose IDisposable objects (use 'using')"
- "Cache frequently accessed data"
testing:
- "New features must have unit tests"
- "Test coverage should be >80%"
- "Use descriptive test names (MethodName_Scenario_ExpectedResult)"
custom_checks:
- name: "No hardcoded connection strings"
pattern: "Server=.*;Database=.*;"
severity: "critical"
- name: "No TODO comments in production code"
pattern: "// TODO"
severity: "warning"
- name: "Use our logging abstraction"
pattern: "Console.WriteLine"
severity: "warning"
suggestion: "Use ILogger instead"
Update the reviewer to load these rules:
import yaml
def load_review_rules() -> dict:
"""Load custom review rules from .reviewrules file."""
rules_file = Path('.reviewrules')
if rules_file.exists():
return yaml.safe_load(rules_file.read_text())
return {}
def review_with_rules(code: str, language: str, rules: dict) -> str:
"""Review code with custom rules."""
rules_context = ""
if rules:
rules_context = f"""
Custom project rules to enforce:
{yaml.dump(rules, default_flow_style=False)}
"""
prompt = f"""Review this {language} code:
```{language}
{code}
{rules_context}
Provide your review in markdown format."""
response = ollama.chat(
model='codellama:7b-instruct',
messages=[
{'role': 'system', 'content': SYSTEM_PROMPT},
{'role': 'user', 'content': prompt}
]
)
return response['message']['content']
## Step 4: Git Integration (Pre-commit Hook)
Create `.git/hooks/pre-commit`:
```bash
#!/bin/bash
# Run AI code review on staged files
echo "🔍 Running AI code review..."
# Get staged Python and C# files
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(py|cs)$')
if [ -z "$STAGED_FILES" ]; then
exit 0
fi
# Run review
for file in $STAGED_FILES; do
if [ -f "$file" ]; then
echo "Reviewing $file..."
python code_reviewer.py "$file" --output "${file}.review.md"
fi
done
echo "✅ Reviews generated. Check *.review.md files."
echo "⚠️ This is advisory - commit will proceed."
exit 0
Make it executable:
chmod +x .git/hooks/pre-commit
Create .github/workflows/code-review.yml:
name: AI Code Review
on:
pull_request:
branches: [main, develop]
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install Ollama
run: |
curl -fsSL https://ollama.com/install.sh | sh
- name: Pull model
run: ollama pull codellama:7b-instruct
- name: Install dependencies
run: pip install ollama rich pyyaml
- name: Run PR review
run: python pr_reviewer.py > review.md
- name: Post review as PR comment
uses: actions/github-script@v7
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const fs = require('fs');
const review = fs.readFileSync('review.md', 'utf8');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `## 🤖 AI Code Review\n\n${review}`
});
Different models for different needs:
| Model | Size | VRAM | Quality | Speed | Best For | |-------|------|------|---------|-------|----------| | CodeLlama 7B | 7B | 6GB | Good | Fast | Quick reviews, CI | | CodeLlama 13B | 13B | 10GB | Better | Medium | Balance | | CodeLlama 34B | 34B | 24GB | Best | Slow | Critical code | | StarCoder2 15B | 15B | 12GB | Good | Medium | Multi-language | | DeepSeek-Coder 33B | 33B | 24GB | Excellent | Slow | Production reviews |
Development laptop (no dedicated GPU):
ollama pull codellama:7b-instruct
# CPU inference, slower but works
Workstation (RTX 3060/4070):
ollama pull codellama:13b-instruct
# GPU acceleration, good balance
Server (A100/RTX 4090):
ollama pull codellama:34b-instruct
# Best quality for production reviews
After 3 months of using this assistant:
| Metric | Before | After | |--------|--------|-------| | Bugs caught in review | 60% | 85% | | Review time per PR | 45 min | 25 min | | Security issues found | 30% | 70% | | Code style violations | 40% | 95% | | Reviewer satisfaction | 3.5/5 | 4.5/5 |
Biggest wins:
What it does well:
What it misses:
Always use AI as assistant, not replacement for human review.
Build something cool with this? Share it on GitHub. Happy to see your improvements.