打造自己的代码审查助手

你是否曾经想要拥有完全在本地机器上运行的 AI 代码审查器?在本教程中,我们将使用 ClientAI 和 Ollama 构建该工具。

我们的助手将分析 Python 代码结构、识别潜在问题并提出改进建议 — 同时保证你的代码私密和安全。

让我们开始吧!

1、项目概述

我们的代码分析助手将能够:

  • 分析代码结构和复杂性
  • 识别样式问题和潜在问题
  • 生成文档建议
  • 提供可操作的改进建议

所有这些都将在你的机器上本地运行,让你能够使用 AI 辅助代码审查,同时保持代码的完全隐私。

2、设置环境

首先,为你的项目创建一个新目录:

mkdir local_task_planner
cd local_task_planner

安装支持 Ollama 的 ClientAI:

mkdir local_task_planner
cd local_task_planner

确保你的系统上安装了 Ollama。你可以从 Ollama 的网站获取它。

现在让我们创建将代码写入其中的文件:

touch code_analyzer.py

并从我们的核心导入开始:

import ast
import json
import logging
import re
from dataclasses import dataclass
from typing import List

from clientai import ClientAI
from clientai.agent import (
  Agent,
  ToolConfig,
  act,
  observe,
  run,
  synthesize,
  think,
)
from clientai.ollama import OllamaManager, OllamaServerConfig

这些组件中的每一个都发挥着至关重要的作用:

  • ast:通过将 Python 代码解析为树结构来帮助我们理解它
  • ClientAI:提供我们的 AI 框架
  • 用于数据处理和模式匹配的各种实用程序模块

3、构建分析结果

在分析代码时,我们需要一种干净的方法来组织我们的发现。以下是我们将如何构造结果:

@dataclass
class CodeAnalysisResult:
  """Results from code analysis."""
  complexity: int
  functions: List[str]
  classes: List[str]
  imports: List[str]
  issues: List[str]

将其视为代码分析的成绩单:

  • 复杂度分数表示代码的复杂程度
  • 函数和类的列表有助于我们理解代码结构
  • imports 显示外部依赖关系
  • issues 跟踪我们发现的任何问题

4、构建核心分析引擎

现在来看看实际的核心 — 让我们构建代码分析引擎:

def analyze_python_code_original(code: str) -> CodeAnalysisResult:
    """Analyze Python code structure and complexity."""
    try:
        tree = ast.parse(code)
        functions = []
        classes = []
        imports = []
        complexity = 0
        for node in ast.walk(tree):
            if isinstance(node, ast.FunctionDef):
                functions.append(node.name)
                complexity += sum(
                    1
                    for _ in ast.walk(node)
                    if isinstance(_, (ast.If, ast.For, ast.While))
                )
            elif isinstance(node, ast.ClassDef):
                classes.append(node.name)
            elif isinstance(node, (ast.Import, ast.ImportFrom)):
                for name in node.names:
                    imports.append(name.name)

        return CodeAnalysisResult(
            complexity=complexity,
            functions=functions,
            classes=classes,
            imports=imports,
            issues=[],
        )
    except Exception as e:
        return CodeAnalysisResult(
            complexity=0,
            functions=[],
            classes=[],
            imports=[],
            issues=[str(e)]
        )

此函数就像我们的代码侦探。它:

  • 将代码解析为树结构
  • 遍历树以查找函数、类和导入
  • 通过计算控制结构来计算复杂度
  • 返回综合分析结果

5、实施样式检查

好的代码不仅仅是正常工作——它应该是可读且可维护的。这是我们的样式检查器:

def check_style_issues_original(code: str) -> List[str]:
    """Check for Python code style issues."""
    issues = []
    for i, line in enumerate(code.split("\n"), 1):
        if len(line.strip()) > 88:
            issues.append(f"Line {i} exceeds 88 characters")
    function_pattern = r"def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\("
    for match in re.finditer(function_pattern, code):
        name = match.group(1)
        if not name.islower():
            issues.append(f"Function '{name}' should use snake_case")
    return issues

我们的样式检查器重点关注两个关键方面:

  • 行长度 — 确保代码保持可读性
  • 函数命名约定 —强制执行 Python 首选的蛇形命名风格

6、文档助手

文档对于可维护的代码至关重要。以下是我们的文档生成器:

def generate_docstring(code: str) -> str:
    """Generate docstring for Python code."""
    try:
        tree = ast.parse(code)
        for node in ast.walk(tree):
            if isinstance(node, (ast.FunctionDef, ast.ClassDef)):
                args = []
                if isinstance(node, ast.FunctionDef):
                    args = [a.arg for a in node.args.args]
                return f"""
                Suggested docstring for {node.name}:
                Args:
                {chr(4).join(f"{arg}: Description of {arg}" for arg in args)}
                Returns:
                    Description of return value
                Examples:
                    ```python
                    # Example usage of {node.name}
                    ```
                """
        return "No functions or classes found to document."
    except Exception as e:
        return f"Error generating docstring: {str(e)}"

此帮助程序:

  • 识别函数和类
  • 提取参数信息
  • 生成文档模板
  • 包含示例的占位符

7、使我们的工具适用于 AI

为了准备将我们的工具与 AI 系统集成,我们需要将它们包装成 JSON 友好的格式:

def analyze_python_code(code: str) -> str:
    """Wrap analyze_python_code_original to return JSON string."""
    if not code:
        return json.dumps({"error": "No code provided"})
    result = analyze_python_code_original(code)
    return json.dumps({
        "complexity": result.complexity,
        "functions": result.functions,
        "classes": result.classes,
        "imports": result.imports,
        "issues": result.issues,
    })

def check_style_issues(code: str) -> str:
    """Wrap check_style_issues_original to return JSON string."""
    if not code:
        return json.dumps({"error": "No code provided"})
    issues = check_style_issues_original(code)
    return json.dumps({"issues": issues})

这些包装器添加了输入验证、JSON 序列化和错误处理,使我们的助手更能防错。

8、使用 ClientAI 注册我们的工具

首先,我们需要将我们的工具提供给 AI 系统。以下是我们注册它们的方式:

def create_review_tools() -> List[ToolConfig]:
    """Create the tool configurations for code review."""
    return [
        ToolConfig(
            tool=analyze_python_code,
            name="code_analyzer",
            description=(
                "Analyze Python code structure and complexity. "
                "Expects a 'code' parameter with the Python code as a string."
            ),
            scopes=["observe"],
        ),
        ToolConfig(
            tool=check_style_issues,
            name="style_checker",
            description=(
                "Check Python code style issues. "
                "Expects a 'code' parameter with the Python code as a string."
            ),
            scopes=["observe"],
        ),
        ToolConfig(
            tool=generate_docstring,
            name="docstring_generator",
            description=(
                "Generate docstring suggestions for Python code. "
                "Expects a 'code' parameter with the Python code as a string."
            ),
            scopes=["act"],
        ),
    ]

让我们分解一下这里发生的事情:

每个工具都包装在一个 ToolConfig 对象中,该对象告诉 ClientAI:

  • tool:工具。要调用的实际函数
  • name :名称。工具的唯一标识符
  • description :描述。工具的功能以及它需要哪些参数
  • scopes :范围。何时可以使用该工具(“观察”用于分析,“行动”用于生成)

我们将工具分为两类:

  • “观察”工具(code_analyzerstyle_checker)收集信息
  • “行动”工具(docstring_generator)生成新内容

9、构建 AI 助手类

现在让我们创建 AI 助手。我们将设计它分步工作,模仿人类代码审查员的思维方式:

class CodeReviewAssistant(Agent):
    """An agent that performs comprehensive Python code review."""

    @observe(
        name="analyze_structure",
        description="Analyze code structure and style",
        stream=True,
    )
    def analyze_structure(self, code: str) -> str:
        """Analyze the code structure, complexity, and style issues."""
        self.context.state["code_to_analyze"] = code
        return """
        Please analyze this Python code structure and style:

        The code to analyze has been provided in the context as 'code_to_analyze'.
        Use the code_analyzer and style_checker tools to evaluate:
        1. Code complexity and structure metrics
        2. Style compliance issues
        3. Function and class organization
        4. Import usage patterns
        """

第一个方法至关重要:

  • @observe装饰器将此标记为观察步骤
  • stream=True 启用实时输出
  • 我们将代码存储在上下文中,以便在后续步骤中访问它
  • 返回字符串是引导 AI 使用我们工具的提示

接下来,我们添加改进建议步骤:

    @think(
        name="suggest_improvements",
        description="Suggest code improvements based on analysis",
        stream=True,
    )
    def suggest_improvements(self, analysis_result: str) -> str:
        """Generate improvement suggestions based on the analysis results."""
        current_code = self.context.state.get("current_code", "")
        return f"""
        Based on the code analysis of:

        ```python
        {current_code}
        ```

        And the analysis results:
        {analysis_result}

        Please suggest specific improvements for:
        1. Reducing complexity where identified
        2. Fixing style issues
        3. Improving code organization
        4. Optimizing import usage
        5. Enhancing readability
        6. Enhancing explicitness
        """

这个方法使用 @think 表示这是一个推理步骤,然后将分析结果作为输入,从上下文中检索原始代码,最后创建一个结构化的改进建议提示。

10、命令行界面

现在让我们创建一个用户友好的界面。我们将其分解为几个部分:

def main():
    # 1. Set up logging
    logger = logging.getLogger(__name__)

    # 2. Configure Ollama server
    config = OllamaServerConfig(
        host="127.0.0.1",  # Local machine
        port=11434,        # Default Ollama port
        gpu_layers=35,     # Adjust based on your GPU
        cpu_threads=8,     # Adjust based on your CPU
    )

第一部分设置错误日志记录,使用合理的默认值配置 Ollama 服务器并允许自定义GPU 和 CPU 使用率的优化。

接下来,我们创建 AI 客户端和助手:

    # Use context manager for Ollama server
    with OllamaManager(config) as manager:
        # Initialize ClientAI with Ollama
        client = ClientAI(
            "ollama", 
            host=f"http://{config.host}:{config.port}"
        )

        # Create code review assistant with tools
        assistant = CodeReviewAssistant(
            client=client,
            default_model="llama3",
            tools=create_review_tools(),
            tool_confidence=0.8,  # How confident the AI should be before using tools
            max_tools_per_step=2, # Maximum tools to use per step
        )

关于此设置的关键点:

  • 上下文管理器 (with) 确保正确的服务器清理
  • 我们连接到本地 Ollama 实例
  • 助手配置:我们的自定义工具、工具使用置信度阈值和每步工具数量限制,以防止过度使用

最后,我们创建交互式循环:

        print("Code Review Assistant (Local AI)")
        print("Enter Python code to review, or 'quit' to exit.")
        print("End input with '###' on a new line.")

        while True:
            try:
                print("\n" + "=" * 50 + "\n")
                print("Enter code:")
                
                # Collect code input
                code_lines = []
                while True:
                    line = input()
                    if line == "###":
                        break
                    code_lines.append(line)

                code = "\n".join(code_lines)
                if code.lower() == "quit":
                    break

                # Process the code
                result = assistant.run(code, stream=True)

                # Handle both streaming and non-streaming results
                if isinstance(result, str):
                    print(result)
                else:
                    for chunk in result:
                        print(chunk, end="", flush=True)
                print("\n")

            except Exception as e:
                logger.error(f"Unexpected error: {e}")
                print("\nAn unexpected error occurred. Please try again.")

此接口:

  • 收集多行代码输入,直到看到 ###
  • 处理流式和非流式输出
  • 提供干净的错误处理
  • 允许使用 quit轻松退出

让我们将其制作成我们可以运行的脚本:

if __name__ == "__main__":
    main()

11、使用助手

让我们看看助手如何处理真实代码。让我们运行它:

python code_analyzer.py

这是一个需要查找问题的示例:

def calculate_total(values,tax_rate):
    Total = 0
    for Val in values:
        if Val > 0:
            if tax_rate > 0:
                Total += Val + (Val * tax_rate)
            else:
                Total += Val
    return Total

助手将分析多个方面:

  • 结构问题(嵌套 if 语句增加复杂性、缺少类型提示、没有输入验证)。
  • 风格问题(变量不一致命名、逗号后缺少空格、缺少文档字符串)。

12、扩展思路

以下是增强助手的一些详细方法:

  • 其他分析工具
  • 增强样式检查
  • 文档改进
  • 自动修复功能

可以通过创建新工具函数、将其包装在适当的 JSON 格式中、将其添加到 create_review_tools() 函数,然后更新助手的提示以使用新工具来添加这些功能。


原文链接:Building a Code Analysis Assistant with Ollama: A Step-by-Step Guide to Local LLMs

汇智网翻译整理,转载请标明出处