Use Cases & Solutions10 min read

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.

By agentnode

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:

  1. Diff Analyzer — Parses PR diffs and identifies changed files, functions, and lines
  2. Tool Pipeline — Runs static analysis, style checking, and security scanning tools from AgentNode
  3. Comment Generator — Uses an LLM-based tool to turn analysis results into human-readable review comments
  4. 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 repo and pull_request scopes
  • 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-analyzer or typescript-type-checker provide 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.Semaphore to 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.

Build an AI Code Review Agent with Agent Tools (2026) — AgentNode Blog | AgentNode