How to Build a Code Review Agent with AI Tools
Build an AI-powered code review agent that performs static analysis, style checking, security scanning, and generates review comments — with full GitHub PR integration.
Code review is one of the most time-consuming parts of software development. Reviewers need to check for bugs, style violations, security vulnerabilities, and architectural issues — all while keeping up with a steady stream of pull requests. An AI code review agent can handle the repetitive parts of this process, flagging issues automatically and letting human reviewers focus on design decisions and business logic.
This tutorial walks you through building a complete code review agent that uses AgentNode tools for static analysis, style checking, security scanning, and review comment generation. By the end, you will have an agent that integrates directly with GitHub PRs and posts actionable feedback.
Architecture Overview
Our code review agent has four major components:
- Diff Analyzer — Parses PR diffs and identifies changed files, functions, and lines
- Tool Pipeline — Runs static analysis, style checking, and security scanning tools from AgentNode
- Comment Generator — Uses an LLM-based tool to turn analysis results into human-readable review comments
- GitHub Integration — Posts inline comments and a summary review on the PR
Each component uses one or more agent tools from the AgentNode registry. You can browse code analysis agent tools to see what is available beyond the ones we use here.
Prerequisites
- Python 3.10+
- AgentNode SDK installed (
pip install agentnode-sdk) - GitHub personal access token with
repoandpull_requestscopes - Basic familiarity with GitHub's REST API
Step 1: Set Up the Project
mkdir code-review-agent && cd code-review-agent
python -m venv venv
source venv/bin/activate
pip install agentnode-sdk pygithub
Create the project structure:
code-review-agent/
├── agent/
│ ├── __init__.py
│ ├── diff_parser.py
│ ├── analyzer.py
│ ├── commenter.py
│ └── github_client.py
├── config.py
├── main.py
└── requirements.txt
Step 2: Parse the PR Diff
The first step is fetching and parsing the pull request diff. We need to know which files changed, what lines were added or modified, and the context around each change:
# agent/diff_parser.py
from dataclasses import dataclass
from github import Github
@dataclass
class FileChange:
filename: str
language: str
patch: str
added_lines: list[tuple[int, str]]
status: str # added, modified, removed
def parse_pr_diff(github_token: str, repo_name: str, pr_number: int) -> list[FileChange]:
g = Github(github_token)
repo = g.get_repo(repo_name)
pr = repo.get_pull(pr_number)
changes = []
for file in pr.get_files():
if file.patch is None:
continue
added_lines = []
current_line = 0
for line in file.patch.split("\n"):
if line.startswith("@@"):
parts = line.split("+")[1].split(",")[0]
current_line = int(parts) - 1
elif line.startswith("+") and not line.startswith("+++"):
current_line += 1
added_lines.append((current_line, line[1:]))
elif not line.startswith("-"):
current_line += 1
language = detect_language(file.filename)
changes.append(FileChange(
filename=file.filename,
language=language,
patch=file.patch,
added_lines=added_lines,
status=file.status
))
return changes
def detect_language(filename: str) -> str:
extensions = {
".py": "python", ".js": "javascript", ".ts": "typescript",
".go": "go", ".rs": "rust", ".java": "java", ".rb": "ruby"
}
for ext, lang in extensions.items():
if filename.endswith(ext):
return lang
return "unknown"
Step 3: Build the Analysis Pipeline
Now comes the core of our agent — the analysis pipeline that runs multiple AgentNode tools against each changed file:
# agent/analyzer.py
from dataclasses import dataclass
from agentnode_sdk import AgentNode
import asyncio
@dataclass
class AnalysisResult:
filename: str
issues: list[dict]
severity: str
tool_name: str
class CodeAnalyzer:
def __init__(self):
self.client = AgentNode()
self.static_analyzer = self.load_tool("code-static-analyzer")
self.style_checker = self.load_tool("code-style-checker")
self.security_scanner = self.load_tool("code-security-scanner")
async def analyze_file(self, file_change) -> list[AnalysisResult]:
tasks = [
self._run_static_analysis(file_change),
self._run_style_check(file_change),
self._run_security_scan(file_change),
]
results = await asyncio.gather(*tasks, return_exceptions=True)
return [r for r in results if isinstance(r, AnalysisResult)]
async def _run_static_analysis(self, file_change) -> AnalysisResult:
result = await self.static_analyzer.arun({
"code": file_change.patch,
"language": file_change.language,
"checks": ["unused_variables", "unreachable_code",
"type_errors", "null_reference", "resource_leaks"]
})
return AnalysisResult(
filename=file_change.filename,
issues=result.output["issues"],
severity=result.output["max_severity"],
tool_name="static-analysis"
)
async def _run_style_check(self, file_change) -> AnalysisResult:
result = await self.style_checker.arun({
"code": file_change.patch,
"language": file_change.language,
"ruleset": "recommended",
"fix_suggestions": True
})
return AnalysisResult(
filename=file_change.filename,
issues=result.output["violations"],
severity=result.output["max_severity"],
tool_name="style-check"
)
async def _run_security_scan(self, file_change) -> AnalysisResult:
result = await self.security_scanner.arun({
"code": file_change.patch,
"language": file_change.language,
"scan_type": "sast",
"severity_threshold": "low"
})
return AnalysisResult(
filename=file_change.filename,
issues=result.output["vulnerabilities"],
severity=result.output["max_severity"],
tool_name="security-scan"
)
async def analyze_all(self, file_changes: list) -> list[AnalysisResult]:
all_results = []
tasks = [self.analyze_file(fc) for fc in file_changes]
results_nested = await asyncio.gather(*tasks)
for results in results_nested:
all_results.extend(results)
return all_results
Step 4: Generate Review Comments
Raw analysis results need to be transformed into helpful, actionable review comments. We use an LLM-based tool for this:
# agent/commenter.py
from agentnode_sdk import AgentNode
class ReviewCommenter:
def __init__(self):
client = AgentNode()
self.comment_generator = load_tool("review-comment-generator")
async def generate_comments(self, analysis_results, file_changes):
comments = []
for result in analysis_results:
if not result.issues:
continue
file_change = next(
(fc for fc in file_changes if fc.filename == result.filename), None
)
if not file_change:
continue
for issue in result.issues:
generated = await self.comment_generator.arun({
"issue": issue,
"code_context": file_change.patch,
"language": file_change.language,
"tone": "constructive",
"include_fix_suggestion": True
})
comments.append({
"filename": result.filename,
"line": issue.get("line", 1),
"body": generated.output["comment"],
"severity": issue.get("severity", "info"),
"tool": result.tool_name
})
return comments
async def generate_summary(self, analysis_results, comments):
total_issues = sum(len(r.issues) for r in analysis_results)
critical = sum(1 for c in comments if c["severity"] == "critical")
warnings = sum(1 for c in comments if c["severity"] == "warning")
summary = await self.comment_generator.arun({
"task": "pr_summary",
"total_issues": total_issues,
"critical_count": critical,
"warning_count": warnings,
"files_analyzed": len(set(r.filename for r in analysis_results)),
"tools_used": list(set(r.tool_name for r in analysis_results))
})
return summary.output["summary"]
Step 5: Post to GitHub
The final component posts the review comments as inline comments on the PR and submits an overall review:
# agent/github_client.py
from github import Github
class GitHubReviewer:
def __init__(self, token: str):
self.g = Github(token)
def post_review(self, repo_name, pr_number, comments, summary):
repo = self.g.get_repo(repo_name)
pr = repo.get_pull(pr_number)
commit = pr.get_commits().reversed[0]
review_comments = []
for comment in comments:
review_comments.append({
"path": comment["filename"],
"line": comment["line"],
"body": comment["body"]
})
has_critical = any(c["severity"] == "critical" for c in comments)
event = "REQUEST_CHANGES" if has_critical else "COMMENT"
pr.create_review(
commit=commit, body=summary,
event=event, comments=review_comments
)
Step 6: Wire It All Together
# main.py
import asyncio, os
from agent.diff_parser import parse_pr_diff
from agent.analyzer import CodeAnalyzer
from agent.commenter import ReviewCommenter
from agent.github_client import GitHubReviewer
async def review_pr(repo_name: str, pr_number: int):
token = os.environ["GITHUB_TOKEN"]
print(f"Parsing PR #{pr_number} diff...")
file_changes = parse_pr_diff(token, repo_name, pr_number)
print(f"Found {len(file_changes)} changed files")
print("Running analysis pipeline...")
analyzer = CodeAnalyzer()
results = await analyzer.analyze_all(file_changes)
total_issues = sum(len(r.issues) for r in results)
print(f"Found {total_issues} issues across all tools")
print("Generating review comments...")
commenter = ReviewCommenter()
comments = await commenter.generate_comments(results, file_changes)
summary = await commenter.generate_summary(results, comments)
print("Posting review to GitHub...")
reviewer = GitHubReviewer(token)
reviewer.post_review(repo_name, pr_number, comments, summary)
print(f"Review posted with {len(comments)} inline comments")
if __name__ == "__main__":
import sys
repo = sys.argv[1]
pr_num = int(sys.argv[2])
asyncio.run(review_pr(repo, pr_num))
Selecting the Right Tools
The tools used in this tutorial are examples. AgentNode's registry has dozens of code analysis tools with different specializations. Here is how to choose:
- Language-specific analyzers — Tools like
python-ast-analyzerortypescript-type-checkerprovide deeper analysis than generic tools - Security scanners — Choose scanners that match your threat model
- Style checkers — Match the ruleset to your team's coding standards
- Complexity analyzers — Tools that measure cyclomatic complexity help catch hard-to-maintain code
For more on building agents that select their own tools dynamically, see build autonomous AI agents. To build custom analysis tools for your team, the build agent skills with Builder tutorial covers the process end to end.
Extending the Agent
Add CI/CD Integration
Run the agent automatically on every PR by adding it to your GitHub Actions workflow:
# .github/workflows/ai-review.yml
name: AI Code Review
on:
pull_request:
types: [opened, synchronize]
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install -r requirements.txt
- run: python main.py ${{ github.repository }} ${{ github.event.pull_request.number }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AGENTNODE_API_KEY: ${{ secrets.AGENTNODE_API_KEY }}
Add Custom Rules
You can extend the analysis pipeline with custom rules specific to your codebase:
class CustomRuleChecker:
def check(self, file_change):
issues = []
for line_num, line in file_change.added_lines:
if "TODO" in line and "@" not in line:
issues.append({
"line": line_num, "severity": "info",
"message": "TODO comment without assignee",
"suggestion": "Add an assignee: TODO(@username): ..."
})
if any(p in line.lower() for p in ["api_key=", "password=", "secret="]):
issues.append({
"line": line_num, "severity": "critical",
"message": "Potential hardcoded secret detected",
"suggestion": "Use environment variables or a secrets manager"
})
return issues
Add Review Memory
Track which issues have been flagged before to avoid repetitive comments on subsequent pushes:
import json
from pathlib import Path
class ReviewMemory:
def __init__(self, storage_path=".review_memory"):
self.path = Path(storage_path)
self.path.mkdir(exist_ok=True)
def has_been_flagged(self, pr_number, issue_hash):
memory_file = self.path / f"pr_{pr_number}.json"
if not memory_file.exists():
return False
history = json.loads(memory_file.read_text())
return issue_hash in history.get("flagged_issues", [])
def record_flag(self, pr_number, issue_hash):
memory_file = self.path / f"pr_{pr_number}.json"
history = {"flagged_issues": []}
if memory_file.exists():
history = json.loads(memory_file.read_text())
history["flagged_issues"].append(issue_hash)
memory_file.write_text(json.dumps(history))
Performance Considerations
For large PRs with many changed files, keep these optimizations in mind:
- Concurrent analysis — Our pipeline already runs tools concurrently per file, but you can also parallelize across files with
asyncio.Semaphoreto control concurrency - File filtering — Skip generated files, lock files, and vendor directories
- Caching — Cache analysis results for unchanged files between PR updates
- Severity filtering — In large PRs, only post comments for warnings and above to avoid noise
For full SDK documentation including advanced configuration for rate limiting and caching, see the AgentNode SDK documentation.
Frequently Asked Questions
Can AI agents review code?
Yes. AI agents can perform many aspects of code review including static analysis, style checking, security vulnerability scanning, and generating human-readable review comments. They are most effective at catching issues that follow patterns — unused variables, potential null references, style violations, and known vulnerability patterns. Human reviewers remain essential for architectural decisions, business logic validation, and design feedback.
What tools does a code review agent need?
A comprehensive code review agent typically needs four categories of tools: a static analyzer for catching bugs and code quality issues, a style checker for enforcing coding standards, a security scanner for identifying vulnerabilities, and a comment generator for turning raw findings into actionable review comments. AgentNode's registry provides verified tools in each category, searchable by language and specialization.
How to integrate AI code review with GitHub?
Use the GitHub API (via the PyGitHub library or direct REST calls) to fetch PR diffs, then run your analysis pipeline against the changed files. Post results as inline review comments using GitHub's pull request review API. For automation, add the agent to a GitHub Actions workflow that triggers on pull_request events, so every PR gets automatic AI analysis before human review.
LLM Runtime: Let the Model Handle It
If your agent uses OpenAI or Anthropic tool calling, AgentNodeRuntime handles tool registration, system prompt injection, and the tool loop automatically. The LLM discovers, installs, and runs AgentNode capabilities on its own — no hardcoded tool calls needed.
from openai import OpenAI
from agentnode_sdk import AgentNodeRuntime
runtime = AgentNodeRuntime()
result = runtime.run(
provider="openai",
client=OpenAI(),
model="gpt-4o",
messages=[{"role": "user", "content": "your task here"}],
)
print(result.content)
The Runtime registers 5 meta-tools (agentnode_capabilities, agentnode_search, agentnode_install, agentnode_run, agentnode_acquire) that let the LLM search the registry, install packages, and execute tools autonomously. Works with Anthropic too — just change provider="anthropic" and pass an Anthropic client.
See the LLM Runtime documentation for the full API reference, trust levels, and manual tool calling.