AI 代码审查 VSCode 插件实战

前言

💡 痛点:每次提交前都要请同事 Review?Code Review 排队耗时长?新来的同事不知道团队规范?

🎯 解决方案:AI 代码审查 VSCode 插件 --- 实时审查、自动修复、学习团队规范。
#mermaid-svg-au20ZioQEdrIwTqc{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-au20ZioQEdrIwTqc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-au20ZioQEdrIwTqc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-au20ZioQEdrIwTqc .error-icon{fill:#552222;}#mermaid-svg-au20ZioQEdrIwTqc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-au20ZioQEdrIwTqc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-au20ZioQEdrIwTqc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-au20ZioQEdrIwTqc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-au20ZioQEdrIwTqc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-au20ZioQEdrIwTqc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-au20ZioQEdrIwTqc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-au20ZioQEdrIwTqc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-au20ZioQEdrIwTqc .marker.cross{stroke:#333333;}#mermaid-svg-au20ZioQEdrIwTqc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-au20ZioQEdrIwTqc p{margin:0;}#mermaid-svg-au20ZioQEdrIwTqc .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-au20ZioQEdrIwTqc .cluster-label text{fill:#333;}#mermaid-svg-au20ZioQEdrIwTqc .cluster-label span{color:#333;}#mermaid-svg-au20ZioQEdrIwTqc .cluster-label span p{background-color:transparent;}#mermaid-svg-au20ZioQEdrIwTqc .label text,#mermaid-svg-au20ZioQEdrIwTqc span{fill:#333;color:#333;}#mermaid-svg-au20ZioQEdrIwTqc .node rect,#mermaid-svg-au20ZioQEdrIwTqc .node circle,#mermaid-svg-au20ZioQEdrIwTqc .node ellipse,#mermaid-svg-au20ZioQEdrIwTqc .node polygon,#mermaid-svg-au20ZioQEdrIwTqc .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-au20ZioQEdrIwTqc .rough-node .label text,#mermaid-svg-au20ZioQEdrIwTqc .node .label text,#mermaid-svg-au20ZioQEdrIwTqc .image-shape .label,#mermaid-svg-au20ZioQEdrIwTqc .icon-shape .label{text-anchor:middle;}#mermaid-svg-au20ZioQEdrIwTqc .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-au20ZioQEdrIwTqc .rough-node .label,#mermaid-svg-au20ZioQEdrIwTqc .node .label,#mermaid-svg-au20ZioQEdrIwTqc .image-shape .label,#mermaid-svg-au20ZioQEdrIwTqc .icon-shape .label{text-align:center;}#mermaid-svg-au20ZioQEdrIwTqc .node.clickable{cursor:pointer;}#mermaid-svg-au20ZioQEdrIwTqc .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-au20ZioQEdrIwTqc .arrowheadPath{fill:#333333;}#mermaid-svg-au20ZioQEdrIwTqc .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-au20ZioQEdrIwTqc .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-au20ZioQEdrIwTqc .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-au20ZioQEdrIwTqc .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-au20ZioQEdrIwTqc .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-au20ZioQEdrIwTqc .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-au20ZioQEdrIwTqc .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-au20ZioQEdrIwTqc .cluster text{fill:#333;}#mermaid-svg-au20ZioQEdrIwTqc .cluster span{color:#333;}#mermaid-svg-au20ZioQEdrIwTqc div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-au20ZioQEdrIwTqc .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-au20ZioQEdrIwTqc rect.text{fill:none;stroke-width:0;}#mermaid-svg-au20ZioQEdrIwTqc .icon-shape,#mermaid-svg-au20ZioQEdrIwTqc .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-au20ZioQEdrIwTqc .icon-shape p,#mermaid-svg-au20ZioQEdrIwTqc .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-au20ZioQEdrIwTqc .icon-shape .label rect,#mermaid-svg-au20ZioQEdrIwTqc .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-au20ZioQEdrIwTqc .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-au20ZioQEdrIwTqc .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-au20ZioQEdrIwTqc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 后端
VSCode 插件
编辑器事件
语言服务
AI 审查引擎
诊断
代码建议
自动修复
编辑器 UI
LLM API
规则引擎

已有方案 vs AI 插件:

维度 Code Review 平台 ESLint/Pylint AI VSCode 插件
审查时机 提交后 保存时 编辑时实时
语义理解 ✅ 理解业务逻辑
团队规范 依赖 Reviewer 规则配置 自动学习
修复建议 文字评论 ✅ 自动修复 智能修复
学习成本 高(配置) 零配置

一、插件架构设计

1.1 整体架构

typescript 复制代码
// ===== VSCode 插件架构(TypeScript)=====

import * as vscode from 'vscode';

/**
 * AI Code Review 插件架构
 * 
 * 分层设计:
 * 1. Extension Layer (入口层) - VSCode 生命周期管理
 * 2. Service Layer (服务层) - AI 审查/规则引擎/配置管理
 * 3. Provider Layer (提供者层) - 诊断/代码操作/Hover
 * 4. LLM Layer (模型层) - 多模型适配/缓存/限流
 */

// ===== 入口层 =====

export class AIReviewExtension {
    private context: vscode.ExtensionContext;
    private configManager: ConfigManager;
    private ruleEngine: RuleEngine;
    private aiEngine: AIReviewEngine;
    private diagnosticProvider: DiagnosticProvider;
    private codeActionProvider: CodeActionProvider;
    private hoverProvider: HoverProvider;

    constructor(context: vscode.ExtensionContext) {
        this.context = context;
        this.configManager = new ConfigManager(context);
        this.ruleEngine = new RuleEngine(this.configManager);
        this.aiEngine = new AIReviewEngine(this.configManager);
        this.diagnosticProvider = new DiagnosticProvider(
            this.ruleEngine,
            this.aiEngine
        );
        this.codeActionProvider = new CodeActionProvider(this.aiEngine);
        this.hoverProvider = new HoverProvider(this.aiEngine);
    }

    activate() {
        // 注册诊断
        this.diagnosticProvider.register(this.context);
        // 注册代码操作
        this.codeActionProvider.register(this.context);
        // 注册 Hover
        this.hoverProvider.register(this.context);
        // 注册命令
        this.registerCommands();
        // 监听配置变更
        this.watchConfigChanges();
        // 初始化状态栏
        this.initStatusBar();
    }

    private registerCommands() {
        const commands = [
            ['ai-review.reviewFile', () => this.reviewCurrentFile()],
            ['ai-review.reviewSelection', () => this.reviewSelection()],
            ['ai-review.reviewDiff', () => this.reviewGitDiff()],
            ['ai-review.explainCode', () => this.explainCode()],
            ['ai-review.generateTest', () => this.generateTests()],
            ['ai-review.optimizeCode', () => this.optimizeCode()],
            ['ai-review.fixAll', () => this.fixAllIssues()],
            ['ai-review.showHistory', () => this.showReviewHistory()],
            ['ai-review.configure', () => this.openSettings()],
            ['ai-review.toggle', () => this.toggleAutoReview()],
        ] as const;

        for (const [id, handler] of commands) {
            this.context.subscriptions.push(
                vscode.commands.registerCommand(id, handler)
            );
        }
    }

    private async reviewCurrentFile() {
        const editor = vscode.window.activeTextEditor;
        if (!editor) return;

        const document = editor.document;
        const language = document.languageId;
        const code = document.getText();

        await vscode.window.withProgress(
            { location: vscode.ProgressLocation.Notification, title: 'AI 审查中...' },
            async () => {
                const results = await this.aiEngine.reviewFile(code, language);
                this.diagnosticProvider.showResults(document, results);
            }
        );
    }

    private async reviewSelection() {
        const editor = vscode.window.activeTextEditor;
        if (!editor) return;

        const selection = editor.selection;
        const code = editor.document.getText(selection);
        const language = editor.document.languageId;

        const results = await this.aiEngine.reviewSelection(
            code, language, selection.start.line
        );
        this.diagnosticProvider.showResults(editor.document, results);
    }

    private async reviewGitDiff() { /* 见第六章 */ }
    private async explainCode() {
        const editor = vscode.window.activeTextEditor;
        if (!editor) return;
        const selection = editor.selection;
        const code = editor.document.getText(selection);
        const explanation = await this.aiEngine.explain(code);
        vscode.window.showInformationMessage(explanation);
    }
    private async generateTests() { /* 生成测试 */ }
    private async optimizeCode() { /* 优化代码 */ }
    private async fixAllIssues() { /* 批量修复 */ }
    private showReviewHistory() { /* 审查历史 */ }
    private openSettings() {
        vscode.commands.executeCommand(
            'workbench.action.openSettings', 'ai-review'
        );
    }
    private toggleAutoReview() {
        const config = this.configManager.getConfig();
        config.autoReview = !config.autoReview;
        this.configManager.saveConfig(config);
    }

    private watchConfigChanges() {
        this.context.subscriptions.push(
            vscode.workspace.onDidChangeConfiguration(e => {
                if (e.affectsConfiguration('ai-review')) {
                    this.configManager.reload();
                }
            })
        );
    }

    private initStatusBar() {
        const statusItem = vscode.window.createStatusBarItem(
            vscode.StatusBarAlignment.Right, 100
        );
        statusItem.text = '$(beaker) AI Review';
        statusItem.command = 'ai-review.toggle';
        statusItem.show();
        this.context.subscriptions.push(statusItem);
    }
}

二、AI 审查引擎

2.1 双引擎审查

typescript 复制代码
// ===== 服务层:AI 审查引擎 =====

interface ReviewIssue {
    id: string;
    type: 'ai' | 'rule';
    severity: 'error' | 'warning' | 'info' | 'hint';
    category: ReviewCategory;
    title: string;
    description: string;
    line: number;
    column: number;
    endLine?: number;
    endColumn?: number;
    fix?: CodeFix;
    relatedRules?: string[];
    confidence: number; // 0-1
}

type ReviewCategory =
    | 'security' | 'performance' | 'bug_risk'
    | 'code_style' | 'best_practice' | 'maintainability'
    | 'error_handling' | 'naming' | 'complexity'
    | 'dead_code' | 'type_safety';

interface CodeFix {
    title: string;
    edits: TextEdit[];
}

interface TextEdit {
    range: { startLine: number; startCol: number; endLine: number; endCol: number };
    newText: string;
}

class AIReviewEngine {
    private ruleEngine: RuleEngine;
    private llmClient: LLMClient;
    private cache: Map<string, ReviewIssue[]>;
    private rateLimiter: RateLimiter;
    private config: AIReviewConfig;

    constructor(configManager: ConfigManager) {
        this.config = configManager.getConfig();
        this.ruleEngine = new RuleEngine(configManager);
        this.llmClient = new LLMClient(this.config.llm);
        this.cache = new Map();
        this.rateLimiter = new RateLimiter(
            this.config.llm.maxRequestsPerMinute || 20
        );
    }

    /**
     * 双引擎审查:规则引擎(毫秒级)+ AI(秒级)
     */
    async reviewFile(
        code: string,
        language: string
    ): Promise<ReviewIssue[]> {
        const cacheKey = `${language}:${hashContent(code)}`;

        // 检查缓存
        if (this.cache.has(cacheKey)) {
            return this.cache.get(cacheKey)!;
        }

        // 1. 规则引擎(同步,毫秒级)
        const ruleIssues = this.ruleEngine.check(code, language);

        // 2. AI 引擎(异步,秒级)
        let aiIssues: ReviewIssue[] = [];
        try {
            await this.rateLimiter.wait();
            aiIssues = await this.llmClient.reviewCode(code, language);
        } catch (error) {
            console.error('AI review failed:', error);
        }

        // 3. 去重合并
        const allIssues = this.deduplicate(ruleIssues, aiIssues);

        // 缓存
        this.cache.set(cacheKey, allIssues);

        return allIssues;
    }

    async reviewSelection(
        code: string,
        language: string,
        startLine: number
    ): Promise<ReviewIssue[]> {
        await this.rateLimiter.wait();
        const aiIssues = await this.llmClient.reviewSelection(code, language);

        // 调整行号偏移
        return aiIssues.map(issue => ({
            ...issue,
            line: issue.line + startLine,
            endLine: issue.endLine ? issue.endLine + startLine : undefined,
        }));
    }

    /**
     * 代码解释
     */
    async explain(code: string): Promise<string> {
        await this.rateLimiter.wait();
        return this.llmClient.explainCode(code);
    }

    /**
     * 优化建议
     */
    async optimize(code: string, language: string): Promise<CodeFix[]> {
        await this.rateLimiter.wait();
        return this.llmClient.optimizeCode(code, language);
    }

    /**
     * 去重:规则和 AI 可能发现同一问题
     */
    private deduplicate(
        ruleIssues: ReviewIssue[],
        aiIssues: ReviewIssue[]
    ): ReviewIssue[] {
        const merged = [...ruleIssues];
        const existingLocations = new Set(
            ruleIssues.map(i => `${i.line}:${i.column}`)
        );

        for (const aiIssue of aiIssues) {
            const key = `${aiIssue.line}:${aiIssue.column}`;
            if (!existingLocations.has(key)) {
                merged.push(aiIssue);
                existingLocations.add(key);
            } else {
                // 同位置:保留置信度更高的
                const existing = merged.find(
                    i => `${i.line}:${i.column}` === key
                );
                if (existing && aiIssue.confidence > existing.confidence) {
                    Object.assign(existing, aiIssue, { type: 'ai' });
                }
            }
        }

        // 按严重程度排序
        const severityOrder = { error: 0, warning: 1, info: 2, hint: 3 };
        merged.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);

        return merged;
    }
}

function hashContent(content: string): string {
    // 简单哈希(生产环境用 crypto)
    let hash = 0;
    for (let i = 0; i < content.length; i++) {
        const char = content.charCodeAt(i);
        hash = ((hash << 5) - hash) + char;
        hash = hash & hash;
    }
    return hash.toString(36);
}

三、规则引擎

3.1 可编程规则

typescript 复制代码
// ===== 规则引擎 =====

interface Rule {
    id: string;
    name: string;
    description: string;
    category: ReviewCategory;
    severity: ReviewIssue['severity'];
    language: string | string[]; // 'all' 或具体语言
    check: (code: string, ast?: any) => ReviewIssue[];
    fix?: (code: string, issue: ReviewIssue) => string;
}

class RuleEngine {
    private rules: Map<string, Rule> = new Map();
    private enabledRules: Set<string> = new Set();

    constructor(configManager: ConfigManager) {
        this.loadBuiltinRules();
        this.loadCustomRules(configManager);
    }

    /**
     * 规则检查
     */
    check(code: string, language: string): ReviewIssue[] {
        const issues: ReviewIssue[] = [];
        const lines = code.split('\n');

        for (const rule of this.rules.values()) {
            if (!this.enabledRules.has(rule.id)) continue;

            const languages = Array.isArray(rule.language) ? rule.language : [rule.language];
            if (!languages.includes('all') && !languages.includes(language)) continue;

            try {
                const ruleIssues = rule.check(code, lines);
                issues.push(...ruleIssues);
            } catch (error) {
                console.error(`Rule ${rule.id} failed:`, error);
            }
        }

        return issues;
    }

    private loadBuiltinRules() {
        // 安全规则
        this.addRule({
            id: 'sec-hardcoded-password',
            name: '硬编码密码',
            description: '检测硬编码的密码和密钥',
            category: 'security',
            severity: 'error',
            language: 'all',
            check: (code) => {
                const issues: ReviewIssue[] = [];
                const patterns = [
                    /password\s*[:=]\s*['"][^'"]{4,}['"]/gi,
                    /secret\s*[:=]\s*['"][^'"]{4,}['"]/gi,
                    /api_key\s*[:=]\s*['"][^'"]{10,}['"]/gi,
                    /token\s*[:=]\s*['"][^'"]{10,}['"]/gi,
                ];
                const lines = code.split('\n');
                for (const pattern of patterns) {
                    for (let i = 0; i < lines.length; i++) {
                        const match = lines[i].match(pattern);
                        if (match) {
                            issues.push({
                                id: `sec-${i}`,
                                type: 'rule',
                                severity: 'error',
                                category: 'security',
                                title: '检测到硬编码的敏感信息',
                                description: `发现可能硬编码的 ${pattern.source.slice(0, 15)}...,建议使用环境变量或密钥管理服务。`,
                                line: i,
                                column: match.index || 0,
                                confidence: 0.9,
                                relatedRules: ['OWASP A02', 'CWE-798'],
                            });
                        }
                    }
                }
                return issues;
            },
        });

        // 性能规则
        this.addRule({
            id: 'perf-n-plus-one',
            name: 'N+1 查询',
            description: '检测循环中的数据库/HTTP 请求',
            category: 'performance',
            severity: 'warning',
            language: ['python', 'javascript', 'typescript'],
            check: (code) => {
                const issues: ReviewIssue[] = [];
                const lines = code.split('\n');
                let inLoop = false;
                let loopStart = 0;

                for (let i = 0; i < lines.length; i++) {
                    const line = lines[i];
                    if (/\b(for|while)\b/.test(line) && !/\/\//.test(line)) {
                        inLoop = true;
                        loopStart = i;
                    }
                    if (inLoop) {
                        if (/\b(requests\.(get|post)|fetch\(|\.query\(|\.execute\(|await\s+\w+\.(find|get))/.test(line)) {
                            issues.push({
                                id: `perf-${i}`,
                                type: 'rule',
                                severity: 'warning',
                                category: 'performance',
                                title: '可能的 N+1 查询',
                                description: `循环内(第 ${loopStart + 1} 行)发现数据请求,可能导致 N+1 查询问题。建议批量查询后内存关联。`,
                                line: i,
                                column: 0,
                                confidence: 0.7,
                                fix: {
                                    title: '建议改为批量查询',
                                    edits: [],
                                },
                            });
                        }
                    }
                }
                return issues;
            },
        });

        // 复杂度规则
        this.addRule({
            id: 'complex-high-cyclomatic',
            name: '圈复杂度过高',
            description: '函数圈复杂度超过阈值',
            category: 'complexity',
            severity: 'warning',
            language: 'all',
            check: (code) => {
                const issues: ReviewIssue[] = [];
                // 简化检测:统计 if/else/for/while/try/&&/||
                const complexityPattern = /\b(if|else\s+if|for|while|case|catch|&&|\|\||\?)\b/g;
                const lines = code.split('\n');
                let currentFunction = '';
                let funcStart = 0;
                let complexity = 0;

                for (let i = 0; i < lines.length; i++) {
                    const line = lines[i];

                    if (/\b(def|function|func|fn)\s+/.test(line)) {
                        // 新函数开始
                        if (currentFunction && complexity > 10) {
                            issues.push({
                                id: `complex-${funcStart}`,
                                type: 'rule',
                                severity: 'warning',
                                category: 'complexity',
                                title: `函数 ${currentFunction} 圈复杂度过高`,
                                description: `圈复杂度为 ${complexity},建议不超过 10。考虑拆分子函数。`,
                                line: funcStart,
                                column: 0,
                                endLine: i,
                                confidence: 0.8,
                            });
                        }
                        const match = line.match(/(?:def|function|func|fn)\s+(\w+)/);
                        currentFunction = match ? match[1] : 'anonymous';
                        funcStart = i;
                        complexity = 1;
                    }

                    const matches = line.match(complexityPattern);
                    if (matches) {
                        complexity += matches.length;
                    }
                }

                // 检查最后一个函数
                if (currentFunction && complexity > 10) {
                    issues.push({
                        id: `complex-${funcStart}`,
                        type: 'rule',
                        severity: 'warning',
                        category: 'complexity',
                        title: `函数 ${currentFunction} 圈复杂度过高`,
                        description: `圈复杂度为 ${complexity},建议不超过 10。`,
                        line: funcStart,
                        column: 0,
                        confidence: 0.8,
                    });
                }

                return issues;
            },
        });

        // 错误处理
        this.addRule({
            id: 'err-bare-except',
            name: '裸异常捕获',
            description: 'except/pass 无操作的异常处理',
            category: 'error_handling',
            severity: 'warning',
            language: ['python'],
            check: (code) => {
                const issues: ReviewIssue[] = [];
                const lines = code.split('\n');
                for (let i = 0; i < lines.length; i++) {
                    if (/^\s*except\s*:/.test(lines[i]) || /^\s*except\s+Exception\s*:/.test(lines[i])) {
                        issues.push({
                            id: `err-${i}`,
                            type: 'rule',
                            severity: 'warning',
                            category: 'error_handling',
                            title: '过于宽泛的异常捕获',
                            description: '建议捕获具体异常类型,避免隐藏真实错误。',
                            line: i,
                            column: lines[i].indexOf('except'),
                            confidence: 0.85,
                        });
                    }
                }
                return issues;
            },
        });
    }

    private loadCustomRules(configManager: ConfigManager) {
        // 加载用户自定义规则(从配置文件)
        const customRules = configManager.getCustomRules();
        for (const rule of customRules) {
            this.addRule(rule);
        }
    }

    private addRule(rule: Rule) {
        this.rules.set(rule.id, rule);
        this.enabledRules.add(rule.id);
    }

    enableRule(id: string) { this.enabledRules.add(id); }
    disableRule(id: string) { this.enabledRules.delete(id); }
}

四、LLM 客户端层

4.1 多模型适配

typescript 复制代码
// ===== LLM 客户端层 =====

interface LLMConfig {
    provider: 'openai' | 'anthropic' | 'local' | 'azure';
    apiKey?: string;
    baseUrl?: string;
    model: string;
    maxTokens: number;
    temperature: number;
    maxRequestsPerMinute: number;
    timeout: number;
}

interface LLMResponse {
    content: string;
    usage: {
        promptTokens: number;
        completionTokens: number;
        totalTokens: number;
    };
    model: string;
    latencyMs: number;
}

abstract class LLMProvider {
    abstract chat(messages: ChatMessage[], options?: ChatOptions): Promise<LLMResponse>;
    abstract name(): string;
}

interface ChatMessage {
    role: 'system' | 'user' | 'assistant';
    content: string;
}

interface ChatOptions {
    temperature?: number;
    maxTokens?: number;
    responseFormat?: 'text' | 'json';
}

class LLMClient {
    private provider: LLMProvider;
    private config: LLMConfig;
    private promptTemplates: PromptTemplates;

    constructor(config: LLMConfig) {
        this.config = config;
        this.provider = this.createProvider(config);
        this.promptTemplates = new PromptTemplates();
    }

    private createProvider(config: LLMConfig): LLMProvider {
        switch (config.provider) {
            case 'openai': return new OpenAIProvider(config);
            case 'anthropic': return new AnthropicProvider(config);
            case 'local': return new LocalProvider(config);
            case 'azure': return new AzureOpenAIProvider(config);
            default: return new OpenAIProvider(config);
        }
    }

    /**
     * 审查代码(结构化输出)
     */
    async reviewCode(code: string, language: string): Promise<ReviewIssue[]> {
        const prompt = this.promptTemplates.codeReview(code, language);
        const response = await this.provider.chat(
            [
                { role: 'system', content: SYSTEM_PROMPT_CODE_REVIEW },
                { role: 'user', content: prompt },
            ],
            { responseFormat: 'json', temperature: 0.2 }
        );

        return this.parseReviewResponse(response.content, language);
    }

    async reviewSelection(code: string, language: string): Promise<ReviewIssue[]> {
        const prompt = this.promptTemplates.selectionReview(code, language);
        const response = await this.provider.chat(
            [
                { role: 'system', content: SYSTEM_PROMPT_CODE_REVIEW },
                { role: 'user', content: prompt },
            ],
            { responseFormat: 'json', temperature: 0.2 }
        );
        return this.parseReviewResponse(response.content, language);
    }

    async explainCode(code: string): Promise<string> {
        const response = await this.provider.chat([
            { role: 'system', content: '你是代码解释专家,用简洁清晰的语言解释代码逻辑。' },
            { role: 'user', content: `请解释以下代码:\n\n${code}` },
        ]);
        return response.content;
    }

    async optimizeCode(code: string, language: string): Promise<CodeFix[]> {
        const prompt = `请优化以下 ${language} 代码,给出修改建议。

代码:
${code}

请以 JSON 数组格式返回修复建议:
[
  {
    "title": "修复标题",
    "description": "修复说明",
    "original": "原始代码片段",
    "fixed": "修复后代码片段"
  }
]`;

        const response = await this.provider.chat([
            { role: 'system', content: '你是代码优化专家。只输出 JSON。' },
            { role: 'user', content: prompt },
        ], { responseFormat: 'json' });

        // 解析并转换为 CodeFix
        try {
            const fixes = JSON.parse(response.content);
            return (Array.isArray(fixes) ? fixes : []).map((f: any) => ({
                title: f.title,
                edits: [{
                    range: { startLine: 0, startCol: 0, endLine: 0, endCol: 0 },
                    newText: f.fixed,
                }],
            }));
        } catch {
            return [];
        }
    }

    private parseReviewResponse(content: string, language: string): ReviewIssue[] {
        try {
            const data = JSON.parse(content);
            const issues: ReviewIssue[] = [];
            const items = Array.isArray(data) ? data : data.issues || [];

            for (const item of items) {
                issues.push({
                    id: `ai-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
                    type: 'ai',
                    severity: item.severity || 'warning',
                    category: item.category || 'best_practice',
                    title: item.title,
                    description: item.description,
                    line: (item.line || 1) - 1, // 0-indexed
                    column: (item.column || 1) - 1,
                    confidence: item.confidence || 0.7,
                    fix: item.fix ? {
                        title: item.fix.title || 'AI 建议',
                        edits: item.fix.edits || [],
                    } : undefined,
                });
            }

            return issues;
        } catch {
            console.error('Failed to parse AI review response');
            return [];
        }
    }
}

// ===== 提示模板 =====

const SYSTEM_PROMPT_CODE_REVIEW = `你是资深代码审查专家。审查代码时关注:
1. 安全漏洞(注入、XSS、CSRF、硬编码密钥)
2. 性能问题(N+1查询、内存泄漏、阻塞调用)
3. Bug 风险(空指针、边界条件、竞态条件)
4. 错误处理(异常吞没、资源泄漏)
5. 最佳实践(命名、复杂度、可维护性)

严格以 JSON 格式输出审查结果,不要加额外文字。`;

class PromptTemplates {
    codeReview(code: string, language: string): string {
        return `审查以下 ${language} 代码,找出所有问题。

\`\`\`${language}
${code}
\`\`\`

输出 JSON 数组:
[
  {
    "title": "问题标题",
    "severity": "error|warning|info",
    "category": "security|performance|bug_risk|code_style|best_practice|...",
    "description": "详细描述",
    "line": 1,
    "column": 1,
    "confidence": 0.8,
    "fix": {
      "title": "修复建议",
      "description": "如何修复"
    }
  }
]`;
    }

    selectionReview(code: string, language: string): string {
        return `审查以下 ${language} 代码片段:

\`\`\`${language}
${code}
\`\`\`

关注:安全问题、性能问题、Bug 风险、最佳实践。
输出 JSON 格式。`;
    }
}

五、编辑器集成

5.1 诊断与代码操作

typescript 复制代码
// ===== 提供者层 =====

class DiagnosticProvider {
    private ruleEngine: RuleEngine;
    private aiEngine: AIReviewEngine;
    private diagnosticCollection: vscode.DiagnosticCollection;
    private config: AIReviewConfig;

    constructor(ruleEngine: RuleEngine, aiEngine: AIReviewEngine) {
        this.ruleEngine = ruleEngine;
        this.aiEngine = aiEngine;
        this.diagnosticCollection =
            vscode.languages.createDiagnosticCollection('ai-review');
        this.config = vscode.workspace.getConfiguration('ai-review') as any;
    }

    register(context: vscode.ExtensionContext) {
        // 保存时审查
        context.subscriptions.push(
            vscode.workspace.onDidSaveTextDocument(doc => this.onSave(doc))
        );

        // 输入时实时审查(debounce)
        let debounceTimer: NodeJS.Timeout;
        context.subscriptions.push(
            vscode.workspace.onDidChangeTextDocument(e => {
                if (this.config.autoReview) {
                    clearTimeout(debounceTimer);
                    debounceTimer = setTimeout(() => this.onSave(e.document), 2000);
                }
            })
        );

        // 打开文件时规则引擎审查
        context.subscriptions.push(
            vscode.workspace.onDidOpenTextDocument(doc => {
                const ruleIssues = this.ruleEngine.check(doc.getText(), doc.languageId);
                this.showResults(doc, ruleIssues);
            })
        );
    }

    private async onSave(document: vscode.TextDocument) {
        // 1. 规则引擎即时
        const ruleIssues = this.ruleEngine.check(
            document.getText(), document.languageId
        );
        this.showResults(document, ruleIssues);

        // 2. AI 异步审查
        if (this.config.enableAI) {
            const aiIssues = await this.aiEngine.reviewFile(
                document.getText(), document.languageId
            );
            const allIssues = [...ruleIssues, ...aiIssues];
            this.showResults(document, allIssues);
        }
    }

    showResults(document: vscode.TextDocument, issues: ReviewIssue[]) {
        const diagnostics: vscode.Diagnostic[] = issues.map(issue => {
            const startLine = issue.line;
            const startCol = issue.column;
            const endLine = issue.endLine || startLine;
            const endCol = issue.endColumn || startCol + 20;

            const range = new vscode.Range(startLine, startCol, endLine, endCol);
            const severityMap = {
                error: vscode.DiagnosticSeverity.Error,
                warning: vscode.DiagnosticSeverity.Warning,
                info: vscode.DiagnosticSeverity.Information,
                hint: vscode.DiagnosticSeverity.Hint,
            };

            const diagnostic = new vscode.Diagnostic(
                range,
                `[${issue.type.toUpperCase()}] ${issue.title}`,
                severityMap[issue.severity]
            );
            diagnostic.source = `AI Review (${issue.category})`;
            diagnostic.message = `${issue.description}\n\n置信度: ${(issue.confidence * 100).toFixed(0)}%`;
            diagnostic.code = issue.id;

            return diagnostic;
        });

        this.diagnosticCollection.set(document.uri, diagnostics);
    }

    clear(document: vscode.TextDocument) {
        this.diagnosticCollection.delete(document.uri);
    }
}

class CodeActionProvider {
    private aiEngine: AIReviewEngine;

    constructor(aiEngine: AIReviewEngine) {
        this.aiEngine = aiEngine;
    }

    register(context: vscode.ExtensionContext) {
        context.subscriptions.push(
            vscode.languages.registerCodeActionsProvider(
                { scheme: 'file' },
                this,
                { providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] }
            )
        );
    }

    async provideCodeActions(
        document: vscode.TextDocument,
        range: vscode.Range,
        context: vscode.CodeActionContext
    ): Promise<vscode.CodeAction[]> {
        const actions: vscode.CodeAction[] = [];

        for (const diagnostic of context.diagnostics) {
            if (diagnostic.source?.startsWith('AI Review')) {
                // AI 修复建议
                const fixAction = new vscode.CodeAction(
                    `🤖 AI: ${diagnostic.message.split('\n')[0]}`,
                    vscode.CodeActionKind.QuickFix
                );
                fixAction.diagnostics = [diagnostic];
                fixAction.command = {
                    title: 'AI Fix',
                    command: 'ai-review.applyFix',
                    arguments: [document, diagnostic],
                };
                actions.push(fixAction);
            }
        }

        // 全文件 AI 审查
        const reviewAll = new vscode.CodeAction(
            '🔍 AI 审查全文件',
            vscode.CodeActionKind.SourceFixAll.append('ai-review')
        );
        reviewAll.command = {
            title: 'Review File',
            command: 'ai-review.reviewFile',
        };
        actions.push(reviewAll);

        return actions;
    }
}

class HoverProvider {
    private aiEngine: AIReviewEngine;

    constructor(aiEngine: AIReviewEngine) {
        this.aiEngine = aiEngine;
    }

    register(context: vscode.ExtensionContext) {
        context.subscriptions.push(
            vscode.languages.registerHoverProvider(
                { scheme: 'file' },
                this
            )
        );
    }

    async provideHover(
        document: vscode.TextDocument,
        position: vscode.Position,
        _token: vscode.CancellationToken
    ): Promise<vscode.Hover | undefined> {
        // 获取光标所在行的代码
        const line = document.lineAt(position.line).text;

        // 代码解释
        const explanation = await this.aiEngine.explain(line);
        return new vscode.Hover(
            new vscode.MarkdownString(`### 🤖 AI 解释\n\n${explanation}`)
        );
    }
}

六、Git Diff 审查

6.1 审查变更

typescript 复制代码
// ===== Git Diff 审查 =====

class GitDiffReviewer {
    private aiEngine: AIReviewEngine;

    constructor(aiEngine: AIReviewEngine) {
        this.aiEngine = aiEngine;
    }

    /**
     * 审查当前 Git Diff
     */
    async reviewCurrentDiff(): Promise<ReviewIssue[]> {
        const diff = await this.getGitDiff();
        if (!diff) {
            vscode.window.showInformationMessage('没有检测到变更');
            return [];
        }

        const issues = await this.aiEngine.reviewFile(diff, 'diff');
        return issues;
    }

    private async getGitDiff(): Promise<string | null> {
        try {
            const { execSync } = require('child_process');
            return execSync('git diff HEAD', { encoding: 'utf-8' });
        } catch {
            return null;
        }
    }

    /**
     * 审查 PR 变更
     */
    async reviewPullRequest(
        repoUrl: string,
        prNumber: number,
        token: string
    ): Promise<ReviewIssue[]> {
        const diff = await this.fetchPRDiff(repoUrl, prNumber, token);
        if (!diff) return [];

        return this.aiEngine.reviewFile(diff, 'diff');
    }

    private async fetchPRDiff(
        repoUrl: string,
        prNumber: number,
        token: string
    ): Promise<string | null> {
        try {
            const { execSync } = require('child_process');
            const url = `https://api.github.com/repos/${repoUrl}/pulls/${prNumber}`;
            const cmd = `curl -s -H "Authorization: token ${token}" "${url}"`;
            const result = JSON.parse(execSync(cmd, { encoding: 'utf-8' }));
            return result.diff_url ? execSync(
                `curl -s -H "Authorization: token ${token}" "${result.diff_url}"`,
                { encoding: 'utf-8' }
            ) : null;
        } catch {
            return null;
        }
    }
}

七、配置与团队规范

7.1 插件配置

typescript 复制代码
// ===== 配置管理 =====

interface AIReviewConfig {
    autoReview: boolean;
    enableAI: boolean;
    enableRules: boolean;
    llm: LLMConfig;
    languages: string[];
    severityFilters: {
        error: boolean;
        warning: boolean;
        info: boolean;
        hint: boolean;
    };
    excludePatterns: string[];
    maxFileSize: number;
    debounceMs: number;
}

class ConfigManager {
    private context: vscode.ExtensionContext;

    constructor(context: vscode.ExtensionContext) {
        this.context = context;
    }

    getConfig(): AIReviewConfig {
        const config = vscode.workspace.getConfiguration('ai-review');
        return {
            autoReview: config.get('autoReview', false),
            enableAI: config.get('enableAI', true),
            enableRules: config.get('enableRules', true),
            llm: {
                provider: config.get('llm.provider', 'openai'),
                apiKey: config.get('llm.apiKey', ''),
                baseUrl: config.get('llm.baseUrl', ''),
                model: config.get('llm.model', 'gpt-4'),
                maxTokens: config.get('llm.maxTokens', 4096),
                temperature: config.get('llm.temperature', 0.2),
                maxRequestsPerMinute: config.get('llm.maxRequestsPerMinute', 20),
                timeout: config.get('llm.timeout', 30000),
            },
            languages: config.get('languages', ['python', 'javascript', 'typescript']),
            severityFilters: {
                error: config.get('severityFilters.error', true),
                warning: config.get('severityFilters.warning', true),
                info: config.get('severityFilters.info', true),
                hint: config.get('severityFilters.hint', false),
            },
            excludePatterns: config.get('excludePatterns', [
                '**/node_modules/**',
                '**/dist/**',
                '**/.git/**',
                '**/vendor/**',
            ]),
            maxFileSize: config.get('maxFileSize', 100000),
            debounceMs: config.get('debounceMs', 2000),
        };
    }

    reload() {
        // 配置变更时触发重新加载
    }

    saveConfig(config: AIReviewConfig) {
        const vsConfig = vscode.workspace.getConfiguration('ai-review');
        for (const [key, value] of Object.entries(config)) {
            vsConfig.update(key, value, vscode.ConfigurationTarget.Global);
        }
    }

    getCustomRules(): Rule[] {
        // 从工作区 .ai-review-rules.json 加载自定义规则
        return [];
    }
}

7.2 package.json 配置

json 复制代码
{
    "name": "ai-code-review",
    "displayName": "AI Code Review",
    "description": "实时 AI 代码审查助手",
    "version": "1.0.0",
    "engines": { "vscode": "^1.80.0" },
    "activationEvents": [
        "onLanguage:python",
        "onLanguage:javascript",
        "onLanguage:typescript",
        "onCommand:ai-review.reviewFile"
    ],
    "main": "./out/extension.js",
    "contributes": {
        "commands": [
            {
                "command": "ai-review.reviewFile",
                "title": "AI Review: 审查当前文件",
                "category": "AI Review"
            },
            {
                "command": "ai-review.reviewSelection",
                "title": "AI Review: 审查选中代码",
                "category": "AI Review"
            },
            {
                "command": "ai-review.reviewDiff",
                "title": "AI Review: 审查 Git 变更",
                "category": "AI Review"
            },
            {
                "command": "ai-review.explainCode",
                "title": "AI Review: 解释代码",
                "category": "AI Review"
            },
            {
                "command": "ai-review.generateTest",
                "title": "AI Review: 生成测试",
                "category": "AI Review"
            },
            {
                "command": "ai-review.optimizeCode",
                "title": "AI Review: 优化代码",
                "category": "AI Review"
            },
            {
                "command": "ai-review.fixAll",
                "title": "AI Review: 一键修复全部",
                "category": "AI Review"
            }
        ],
        "configuration": {
            "title": "AI Code Review",
            "properties": {
                "ai-review.autoReview": {
                    "type": "boolean",
                    "default": false,
                    "description": "编辑时自动审查"
                },
                "ai-review.enableAI": {
                    "type": "boolean",
                    "default": true,
                    "description": "启用 AI 审查"
                },
                "ai-review.enableRules": {
                    "type": "boolean",
                    "default": true,
                    "description": "启用规则引擎"
                },
                "ai-review.llm.provider": {
                    "type": "string",
                    "enum": ["openai", "anthropic", "local", "azure"],
                    "default": "openai",
                    "description": "LLM 提供商"
                },
                "ai-review.llm.model": {
                    "type": "string",
                    "default": "gpt-4",
                    "description": "模型名称"
                },
                "ai-review.llm.apiKey": {
                    "type": "string",
                    "description": "API Key"
                },
                "ai-review.debounceMs": {
                    "type": "number",
                    "default": 2000,
                    "description": "实时审查防抖(毫秒)"
                }
            }
        },
        "keybindings": [
            {
                "command": "ai-review.reviewFile",
                "key": "ctrl+shift+r",
                "mac": "cmd+shift+r"
            },
            {
                "command": "ai-review.reviewSelection",
                "key": "ctrl+shift+alt+r",
                "mac": "cmd+shift+alt+r"
            },
            {
                "command": "ai-review.explainCode",
                "key": "ctrl+shift+e",
                "mac": "cmd+shift+e"
            }
        ]
    }
}

八、总结

架构全景

组件 职责
入口层 AIReviewExtension VSCode 生命周期、命令注册、状态栏
服务层 AIReviewEngine 双引擎审查、去重、缓存、限流
规则引擎 RuleEngine 可编程规则、安全/性能/复杂度检测
LLM 层 LLMClient 多模型适配、结构化输出
提供者层 DiagnosticProvider 诊断显示、保存/输入触发
提供者层 CodeActionProvider 快速修复建议
提供者层 HoverProvider 代码解释

最佳实践

实践 说明
双引擎 规则引擎毫秒级 + AI 秒级,互补增强
去重合并 规则和 AI 可能发现同一问题,保留高置信度
限流控制 避免过度调用 LLM API
缓存结果 相同代码不重复审查
团队规范 自定义规则支持,学习代码风格
零配置 开箱即用,按需深度配置

本文涵盖 AI 代码审查 VSCode 插件完整技术栈:架构设计、双引擎审查、可编程规则、多模型 LLM 适配、编辑器诊断集成、Git Diff 审查、配置管理、团队规范。

相关推荐
未来之窗软件服务1 小时前
精选之变,顺势而生(2026 年高考语文作文)
大数据·人工智能·高考·仙盟创梦ide·东方仙盟
意图共鸣1 小时前
意图共鸣科技发布《AI记忆链商业化白皮书3.0》:从存算解耦到“第二大脑”的技术演进
人工智能·科技·架构
仰望星空的代码1 小时前
科技是市场的唯一
大数据·人工智能·科技·财经·股市行情
芯盾时代1 小时前
企业建立安全防线治理失控的Agent
大数据·人工智能·安全
AI数据皮皮侠1 小时前
全国高考报名、录取数据(1977-2026)
大数据·数据库·人工智能·python·机器学习·高考
东方佑1 小时前
条件随机、自指与分形:论现实世界的递归生成逻辑
人工智能
老H科研技术1 小时前
第 04 篇:MCP中SDK 对比与选型 —— 选对工具,事半功倍
人工智能·mcp
DS随心转插件2 小时前
AI导出鸭:DeepSeek 转 Word 效果实测与案例展示
人工智能·ai·word·豆包·deepseek·ai导出鸭
宁静致远46882 小时前
从零构建 RWKV 批量推理服务器:2的幂次动态缩容、异步拷回与向量化采样
人工智能