Appearance
24-项目二:AI 编程助手(CLI + Web)
岗位能力对标
| 招聘要求 | 本节覆盖 |
|---|---|
| 企业级开发能力 | ✅ 性能/安全/成本 |
| 工程化能力 | ✅ 监控/测试/部署 |
| 项目交付能力 | ✅ 完整项目实战 |
| 代码分析能力 | ✅ AST解析/静态分析 |
| Agent设计能力 | ✅ 多轮对话/上下文感知 |
学习目标
学完本节,你将能够:
- 理解代码分析原理:掌握AST解析、静态分析等核心技术
- 开发CLI工具:构建命令行AI编程助手
- 构建Web界面:使用React+Monaco Editor实现代码编辑器
- 集成AI Agent:实现代码解释、重构、Bug检测功能
- 实现上下文感知:支持多轮对话和代码上下文理解
- 掌握性能优化:Token优化、缓存策略、并发控制
前置知识
- 已完成前面章节的学习
- 具备基础 Agent 开发能力
- 熟悉JavaScript/Python基础语法
- 了解React前端开发
一、需求分析
1.1 功能需求
┌─────────────────────────────────────────────────────────┐
│ AI 编程助手功能架构 │
├─────────────────────────────────────────────────────────┤
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ │
│ │ 代码解释 │ │ 重构建议 │ │ Bug检测 │ │ 文档生成 │ │
│ └──────────┘ └──────────┘ └──────────┘ └─────────┘ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ │
│ │ 测试生成 │ │ 代码补全 │ │ 性能分析 │ │ 安全扫描 │ │
│ └──────────┘ └──────────┘ └──────────┘ └─────────┘ │
└─────────────────────────────────────────────────────────┘1.2 核心功能详解
1.2.1 代码解释
typescript
// 输入:复杂代码片段
async function processUserData(users: User[]): Promise<Result> {
const validUsers = users
.filter(u => u.age >= 18)
.map(u => ({ ...u, status: 'active' }));
return await batchUpdate(validUsers);
}
// AI输出:
/**
* 功能说明:
* 1. 筛选成年用户(age >= 18)
* 2. 将用户状态设置为 'active'
* 3. 批量更新到数据库
*
* 时间复杂度:O(n)
* 空间复杂度:O(n)
*/1.2.2 重构建议
typescript
// 原始代码(问题:嵌套过深,可读性差)
function getData() {
fetch('/api/data')
.then(res => {
if (res.ok) {
res.json().then(data => {
if (data.success) {
processData(data);
}
});
}
});
}
// AI重构建议:
async function getData() {
const res = await fetch('/api/data');
if (!res.ok) return;
const data = await res.json();
if (data.success) {
processData(data);
}
}1.2.3 Bug检测
typescript
// 输入代码
function calculateDiscount(price, discount) {
return price - (price * discount / 100);
}
// AI检测到的Bug:
/**
* ⚠️ 潜在问题:
* 1. 缺少参数类型检查(price/discount可能为负数)
* 2. 缺少边界检查(discount > 100会导致负价格)
* 3. 浮点数精度问题
*
* 修复建议:
*/
function calculateDiscount(price: number, discount: number): number {
if (price < 0 || discount < 0) {
throw new Error('Price and discount must be positive');
}
if (discount > 100) {
throw new Error('Discount cannot exceed 100%');
}
return Math.round((price - (price * discount / 100)) * 100) / 100;
}二、技术方案
2.1 架构设计
┌──────────────────────────────────────────────────────────────┐
│ AI 编程助手架构 │
├──────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ CLI 层 │ │ Web 层 │ │ VS Code │ │
│ │ (Node.js) │ │ (React) │ │ 插件 │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ └──────────────────┼──────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ API Gateway │ │
│ │ (统一接口/认证/限流) │ │
│ └─────────────────────────┬───────────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Core Service │ │
│ ├─────────────────────────────────────────────────────┤ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ AST解析器 │ │ 代码分析器 │ │ Agent引擎 │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ └─────────────────────────┬───────────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ LLM Provider (OpenAI/Claude) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘2.2 AST解析技术
2.2.1 JavaScript/TypeScript AST解析
typescript
// 使用 @babel/parser 解析代码
import { parse } from '@babel/parser';
import traverse from '@babel/traverse';
interface CodeAnalysis {
functions: FunctionInfo[];
classes: ClassInfo[];
imports: ImportInfo[];
complexity: number;
}
class ASTAnalyzer {
private parserOptions = {
sourceType: 'module' as const,
plugins: ['typescript', 'jsx', 'decorators-legacy'],
};
analyze(code: string): CodeAnalysis {
const ast = parse(code, this.parserOptions);
const analysis: CodeAnalysis = {
functions: [],
classes: [],
imports: [],
complexity: 0,
};
traverse(ast, {
// 提取函数信息
FunctionDeclaration(path) {
analysis.functions.push({
name: path.node.id?.name || 'anonymous',
params: path.node.params.map(p => (p as any).name),
loc: path.node.loc,
});
},
// 计算圈复杂度
'IfStatement|SwitchCase|ConditionalExpression|LogicalExpression'(path) {
analysis.complexity++;
},
});
return analysis;
}
// 提取代码结构用于LLM上下文
extractContext(code: string, line: number): string {
const ast = parse(code, this.parserOptions);
let context = '';
traverse(ast, {
FunctionDeclaration(path) {
const { start, end } = path.node.loc || {};
if (start && end && start.line <= line && end.line >= line) {
context = code.slice(
(path.node as any).start,
(path.node as any).end
);
}
},
});
return context;
}
}2.2.2 Python AST解析
python
import ast
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class FunctionInfo:
name: str
args: List[str]
line_start: int
line_end: int
docstring: Optional[str]
class PythonAnalyzer:
def analyze(self, code: str) -> dict:
tree = ast.parse(code)
analysis = {
'functions': [],
'classes': [],
'imports': [],
'complexity': 0
}
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
analysis['functions'].append(FunctionInfo(
name=node.name,
args=[arg.arg for arg in node.args.args],
line_start=node.lineno,
line_end=node.end_lineno or node.lineno,
docstring=ast.get_docstring(node)
))
elif isinstance(node, (ast.If, ast.While, ast.For)):
analysis['complexity'] += 1
return analysis
def extract_function_at_line(self, code: str, line: int) -> Optional[str]:
"""提取指定行所在的函数代码"""
tree = ast.parse(code)
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
if node.lineno <= line <= (node.end_lineno or node.lineno):
lines = code.split('\n')
return '\n'.join(lines[node.lineno-1:node.end_lineno])
return None2.3 Agent设计
2.3.1 Agent架构
typescript
interface AgentContext {
sessionId: string;
messages: Message[];
codeContext?: CodeContext;
projectContext?: ProjectContext;
}
interface CodeContext {
currentFile: string;
selectedCode: string;
cursorPosition: Position;
fileStructure: FileNode[];
}
class CodeAssistantAgent {
private llm: LLMProvider;
private astAnalyzer: ASTAnalyzer;
private contextManager: ContextManager;
constructor(config: AgentConfig) {
this.llm = new OpenAIProvider(config.apiKey);
this.astAnalyzer = new ASTAnalyzer();
this.contextManager = new ContextManager();
}
// 代码解释
async explainCode(
code: string,
context: AgentContext
): Promise<AgentResponse> {
// 1. AST分析提取关键信息
const analysis = this.astAnalyzer.analyze(code);
// 2. 构建Prompt
const prompt = this.buildExplainPrompt(code, analysis);
// 3. 调用LLM
const response = await this.llm.chat({
messages: [
...this.contextManager.getHistory(context.sessionId),
{ role: 'user', content: prompt },
],
temperature: 0.3,
});
// 4. 保存上下文
this.contextManager.addMessage(context.sessionId, {
role: 'assistant',
content: response.content,
});
return {
content: response.content,
suggestions: this.parseSuggestions(response.content),
};
}
// 重构建议
async suggestRefactoring(
code: string,
context: AgentContext
): Promise<RefactoringSuggestion[]> {
const analysis = this.astAnalyzer.analyze(code);
const prompt = `Analyze the following code and suggest refactoring improvements.
Code:
\`\`\`${context.codeContext?.currentFile?.endsWith('.ts') ? 'typescript' : 'javascript'}
${code}
\`\`\`
Code Metrics:
- Cyclomatic Complexity: ${analysis.complexity}
- Function Count: ${analysis.functions.length}
Provide refactoring suggestions in JSON format:
{
"suggestions": [
{
"type": "extract-function|rename-variable|simplify-condition",
"description": "...",
"originalCode": "...",
"refactoredCode": "...",
"benefits": ["..."]
}
]
}`;
const response = await this.llm.chat({
messages: [{ role: 'user', content: prompt }],
temperature: 0.2,
});
return this.parseRefactoringSuggestions(response.content);
}
// Bug检测
async detectBugs(
code: string,
context: AgentContext
): Promise<BugReport[]> {
const prompt = `Review the following code for potential bugs and issues.
Code:
\`\`\`
${code}
\`\`\`
Check for:
1. Null/undefined references
2. Type mismatches
3. Logic errors
4. Performance issues
5. Security vulnerabilities
Report issues in JSON format:
{
"issues": [
{
"severity": "high|medium|low",
"type": "null-reference|type-error|logic-error",
"line": 5,
"description": "...",
"fix": "..."
}
]
}`;
const response = await this.llm.chat({
messages: [{ role: 'user', content: prompt }],
temperature: 0.1,
});
return this.parseBugReports(response.content);
}
private buildExplainPrompt(code: string, analysis: CodeAnalysis): string {
return `Explain the following code in detail:
Code:
\`\`\`
${code}
\`\`\`
Analysis:
- Functions: ${analysis.functions.map(f => f.name).join(', ')}
- Complexity: ${analysis.complexity}
Please explain:
1. What this code does
2. Key functions and their purposes
3. Data flow
4. Time/space complexity
5. Potential edge cases`;
}
}2.3.2 上下文管理
typescript
class ContextManager {
private sessions: Map<string, SessionContext> = new Map();
private maxHistoryLength = 10;
private maxTokensPerSession = 4000;
createSession(sessionId: string): void {
this.sessions.set(sessionId, {
messages: [],
codeSnapshots: new Map(),
tokenCount: 0,
});
}
addMessage(sessionId: string, message: Message): void {
const session = this.sessions.get(sessionId);
if (!session) return;
// 管理历史长度
if (session.messages.length >= this.maxHistoryLength) {
session.messages.shift();
}
session.messages.push(message);
session.tokenCount += this.estimateTokens(message.content);
// Token超限处理
if (session.tokenCount > this.maxTokensPerSession) {
this.compressHistory(sessionId);
}
}
// 智能上下文压缩
private compressHistory(sessionId: string): void {
const session = this.sessions.get(sessionId);
if (!session) return;
// 保留关键消息(系统提示、代码上下文)
const criticalIndices = [0]; // 系统消息
// 压缩旧的用户-助手对话
const compressedMessages: Message[] = [];
for (let i = 1; i < session.messages.length; i++) {
if (criticalIndices.includes(i) || i > session.messages.length - 4) {
compressedMessages.push(session.messages[i]);
}
}
// 添加摘要
const summary = this.summarizeMessages(
session.messages.slice(1, -3)
);
compressedMessages.splice(1, 0, {
role: 'system',
content: `[Conversation Summary]: ${summary}`,
});
session.messages = compressedMessages;
session.tokenCount = this.calculateTotalTokens(compressedMessages);
}
private summarizeMessages(messages: Message[]): string {
// 使用LLM或启发式方法生成摘要
const topics = new Set<string>();
messages.forEach(m => {
if (m.content.includes('explain')) topics.add('code explanation');
if (m.content.includes('refactor')) topics.add('refactoring');
if (m.content.includes('bug')) topics.add('bug detection');
});
return `Discussed: ${Array.from(topics).join(', ')}`;
}
getHistory(sessionId: string): Message[] {
return this.sessions.get(sessionId)?.messages || [];
}
private estimateTokens(text: string): number {
// 粗略估计:1 token ≈ 4 characters
return Math.ceil(text.length / 4);
}
}三、CLI工具实现(Node.js)
3.1 项目结构
ai-code-assistant-cli/
├── src/
│ ├── commands/
│ │ ├── explain.ts # 代码解释命令
│ │ ├── refactor.ts # 重构建议命令
│ │ ├── detect.ts # Bug检测命令
│ │ └── chat.ts # 交互式对话
│ ├── core/
│ │ ├── analyzer.ts # AST分析器
│ │ ├── agent.ts # Agent核心
│ │ └── context.ts # 上下文管理
│ ├── utils/
│ │ ├── file.ts # 文件操作
│ │ └── prompt.ts # Prompt构建
│ └── index.ts # CLI入口
├── bin/
│ └── aica # 可执行文件
├── package.json
└── tsconfig.json3.2 核心代码实现
typescript
// src/index.ts
#!/usr/bin/env node
import { Command } from 'commander';
import { explainCommand } from './commands/explain';
import { refactorCommand } from './commands/refactor';
import { detectCommand } from './commands/detect';
import { chatCommand } from './commands/chat';
const program = new Command();
program
.name('aica')
.description('AI Code Assistant - Your intelligent programming companion')
.version('1.0.0');
// 代码解释命令
program
.command('explain')
.description('Explain code in a file or from stdin')
.argument('[file]', 'file to analyze')
.option('-l, --line <number>', 'specific line number')
.option('-f, --function <name>', 'specific function name')
.action(explainCommand);
// 重构建议命令
program
.command('refactor')
.description('Get refactoring suggestions')
.argument('<file>', 'file to refactor')
.option('-s, --strategy <type>', 'refactoring strategy', 'all')
.action(refactorCommand);
// Bug检测命令
program
.command('detect')
.description('Detect bugs and issues')
.argument('<file>', 'file to analyze')
.option('-o, --output <format>', 'output format', 'table')
.action(detectCommand);
// 交互式对话
program
.command('chat')
.description('Start interactive chat session')
.option('-p, --project <path>', 'project context path')
.action(chatCommand);
program.parse();typescript
// src/commands/explain.ts
import fs from 'fs/promises';
import chalk from 'chalk';
import { CodeAssistantAgent } from '../core/agent';
import { ASTAnalyzer } from '../core/analyzer';
import { loadConfig } from '../utils/config';
interface ExplainOptions {
line?: string;
function?: string;
}
export async function explainCommand(
file: string | undefined,
options: ExplainOptions
): Promise<void> {
try {
// 读取代码
let code: string;
if (file) {
code = await fs.readFile(file, 'utf-8');
} else {
// 从stdin读取
code = await readStdin();
}
// 提取特定范围代码
if (options.line) {
const lineNum = parseInt(options.line);
const analyzer = new ASTAnalyzer();
code = analyzer.extractContext(code, lineNum) || code;
}
// 初始化Agent
const config = await loadConfig();
const agent = new CodeAssistantAgent(config);
console.log(chalk.blue('🔍 Analyzing code...\n'));
// 调用Agent解释
const response = await agent.explainCode(code, {
sessionId: generateSessionId(),
messages: [],
codeContext: {
currentFile: file || 'stdin',
selectedCode: code,
},
});
// 输出结果
console.log(chalk.green('📖 Explanation:\n'));
console.log(response.content);
if (response.suggestions?.length > 0) {
console.log(chalk.yellow('\n💡 Suggestions:'));
response.suggestions.forEach((s, i) => {
console.log(` ${i + 1}. ${s}`);
});
}
} catch (error) {
console.error(chalk.red('Error:'), error.message);
process.exit(1);
}
}
async function readStdin(): Promise<string> {
return new Promise((resolve, reject) => {
let data = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => data += chunk);
process.stdin.on('end', () => resolve(data));
process.stdin.on('error', reject);
});
}
function generateSessionId(): string {
return `cli-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}typescript
// src/commands/detect.ts
import fs from 'fs/promises';
import chalk from 'chalk';
import Table from 'cli-table3';
import { CodeAssistantAgent } from '../core/agent';
import { loadConfig } from '../utils/config';
interface DetectOptions {
output: string;
}
export async function detectCommand(
file: string,
options: DetectOptions
): Promise<void> {
const code = await fs.readFile(file, 'utf-8');
const config = await loadConfig();
const agent = new CodeAssistantAgent(config);
console.log(chalk.blue('🔍 Scanning for bugs and issues...\n'));
const bugs = await agent.detectBugs(code, {
sessionId: generateSessionId(),
messages: [],
codeContext: { currentFile: file, selectedCode: code },
});
if (bugs.length === 0) {
console.log(chalk.green('✅ No issues found!'));
return;
}
// 按严重程度分组
const grouped = bugs.reduce((acc, bug) => {
acc[bug.severity] = acc[bug.severity] || [];
acc[bug.severity].push(bug);
return acc;
}, {} as Record<string, BugReport[]>);
// 输出结果
if (options.output === 'table') {
const table = new Table({
head: ['Severity', 'Type', 'Line', 'Description', 'Fix'],
colWidths: [10, 15, 8, 40, 30],
});
bugs.forEach(bug => {
table.push([
colorSeverity(bug.severity),
bug.type,
bug.line,
bug.description.substring(0, 37) + '...',
bug.fix?.substring(0, 27) + '...' || 'N/A',
]);
});
console.log(table.toString());
} else {
console.log(JSON.stringify(bugs, null, 2));
}
// 统计
const high = grouped['high']?.length || 0;
const medium = grouped['medium']?.length || 0;
const low = grouped['low']?.length || 0;
console.log(chalk.red(`\n⚠️ Found ${bugs.length} issues:`));
console.log(` High: ${high} | Medium: ${medium} | Low: ${low}`);
}
function colorSeverity(severity: string): string {
switch (severity) {
case 'high': return chalk.red('HIGH');
case 'medium': return chalk.yellow('MEDIUM');
case 'low': return chalk.blue('LOW');
default: return severity;
}
}typescript
// src/commands/chat.ts
import readline from 'readline';
import chalk from 'chalk';
import { CodeAssistantAgent } from '../core/agent';
import { loadConfig } from '../utils/config';
import { scanProject } from '../utils/file';
interface ChatOptions {
project?: string;
}
export async function chatCommand(options: ChatOptions): Promise<void> {
const config = await loadConfig();
const agent = new CodeAssistantAgent(config);
const sessionId = `chat-${Date.now()}`;
let projectContext: ProjectContext | undefined;
if (options.project) {
console.log(chalk.blue('📁 Scanning project...'));
projectContext = await scanProject(options.project);
console.log(chalk.green(`Found ${projectContext.files.length} files\n`));
}
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: chalk.cyan('aica> '),
});
console.log(chalk.green('🤖 AI Code Assistant Chat'));
console.log(chalk.gray('Type "/help" for commands, "/quit" to exit\n'));
rl.prompt();
rl.on('line', async (input) => {
const command = input.trim();
if (command === '/quit') {
console.log(chalk.yellow('Goodbye!'));
rl.close();
return;
}
if (command === '/help') {
showHelp();
rl.prompt();
return;
}
if (command.startsWith('/explain ')) {
const file = command.slice(9);
await handleExplain(file, agent, sessionId, projectContext);
} else if (command.startsWith('/file ')) {
const file = command.slice(6);
await handleFileContext(file, sessionId, projectContext);
} else {
// 普通对话
await handleChat(command, agent, sessionId, projectContext);
}
rl.prompt();
});
}
async function handleChat(
input: string,
agent: CodeAssistantAgent,
sessionId: string,
projectContext?: ProjectContext
): Promise<void> {
const spinner = startSpinner('Thinking...');
try {
const response = await agent.chat(input, {
sessionId,
messages: [],
projectContext,
});
stopSpinner(spinner);
console.log(chalk.green('\n🤖:'), response.content);
} catch (error) {
stopSpinner(spinner);
console.error(chalk.red('Error:'), error.message);
}
}
function showHelp(): void {
console.log(chalk.yellow('\nAvailable commands:'));
console.log(' /explain <file> - Explain a file');
console.log(' /file <file> - Load file context');
console.log(' /clear - Clear conversation');
console.log(' /help - Show this help');
console.log(' /quit - Exit\n');
}3.3 package.json
json
{
"name": "ai-code-assistant-cli",
"version": "1.0.0",
"description": "AI-powered code assistant CLI",
"main": "dist/index.js",
"bin": {
"aica": "./bin/aica"
},
"scripts": {
"build": "tsc",
"dev": "ts-node src/index.ts",
"lint": "eslint src/**/*.ts",
"test": "jest"
},
"dependencies": {
"@babel/parser": "^7.23.0",
"@babel/traverse": "^7.23.0",
"commander": "^11.0.0",
"chalk": "^4.1.2",
"cli-table3": "^0.6.3",
"openai": "^4.0.0",
"ora": "^5.4.1"
},
"devDependencies": {
"@types/babel__traverse": "^7.20.0",
"@types/node": "^20.0.0",
"typescript": "^5.0.0",
"ts-node": "^10.9.0"
}
}四、Web界面实现(React + Monaco Editor)
4.1 项目结构
ai-code-assistant-web/
├── src/
│ ├── components/
│ │ ├── Editor/
│ │ │ ├── MonacoEditor.tsx # Monaco编辑器封装
│ │ │ └── CodeHighlight.tsx # 代码高亮
│ │ ├── Chat/
│ │ │ ├── ChatPanel.tsx # 聊天面板
│ │ │ ├── MessageList.tsx # 消息列表
│ │ │ └── CodeBlock.tsx # 代码块渲染
│ │ ├── Sidebar/
│ │ │ ├── FileTree.tsx # 文件树
│ │ │ └── ProjectOverview.tsx # 项目概览
│ │ └── Toolbar/
│ │ ├── ActionButtons.tsx # 操作按钮
│ │ └── ModelSelector.tsx # 模型选择
│ ├── hooks/
│ │ ├── useAgent.ts # Agent交互Hook
│ │ ├── useCodeAnalysis.ts # 代码分析Hook
│ │ └── useWebSocket.ts # WebSocket连接
│ ├── services/
│ │ ├── agent.service.ts # Agent服务
│ │ ├── file.service.ts # 文件服务
│ │ └── websocket.service.ts # WebSocket服务
│ ├── store/
│ │ ├── editor.store.ts # 编辑器状态
│ │ ├── chat.store.ts # 聊天状态
│ │ └── project.store.ts # 项目状态
│ ├── types/
│ │ └── index.ts # 类型定义
│ ├── utils/
│ │ ├── ast.ts # AST工具
│ │ └── prompt.ts # Prompt工具
│ ├── App.tsx
│ └── main.tsx
├── public/
├── index.html
├── package.json
├── vite.config.ts
└── tailwind.config.js4.2 核心组件实现
typescript
// src/components/Editor/MonacoEditor.tsx
import React, { useRef, useEffect, useCallback } from 'react';
import Editor, { OnMount, Monaco } from '@monaco-editor/react';
import * as monaco from 'monaco-editor';
import { useEditorStore } from '../../store/editor.store';
interface MonacoEditorProps {
value: string;
language?: string;
onChange?: (value: string) => void;
onSelect?: (selection: Selection) => void;
}
interface Selection {
text: string;
startLine: number;
endLine: number;
}
export const MonacoEditor: React.FC<MonacoEditorProps> = ({
value,
language = 'typescript',
onChange,
onSelect,
}) => {
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
const monacoRef = useRef<Monaco | null>(null);
const { setEditor, addDecoration } = useEditorStore();
// 编辑器挂载
const handleEditorMount: OnMount = (editor, monaco) => {
editorRef.current = editor;
monacoRef.current = monaco;
setEditor(editor);
// 配置TypeScript
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.ES2020,
allowNonTsExtensions: true,
moduleResolution:
monaco.languages.typescript.ModuleResolutionKind.NodeJs,
module: monaco.languages.typescript.ModuleKind.CommonJS,
noEmit: true,
esModuleInterop: true,
jsx: monaco.languages.typescript.JsxEmit.React,
reactNamespace: 'React',
allowJs: true,
typeRoots: ['node_modules/@types'],
});
// 添加右键菜单
editor.addAction({
id: 'explain-code',
label: '🤖 Explain Code',
contextMenuGroupId: '9_cutcopypaste',
run: () => {
const selection = editor.getSelection();
if (selection) {
const model = editor.getModel();
if (model) {
const text = model.getValueInRange(selection);
onSelect?.({
text,
startLine: selection.startLineNumber,
endLine: selection.endLineNumber,
});
}
}
},
});
// 快捷键
editor.addCommand(
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyE,
() => {
const selection = editor.getSelection();
if (selection && onSelect) {
const model = editor.getModel();
if (model) {
const text = model.getValueInRange(selection);
onSelect({
text,
startLine: selection.startLineNumber,
endLine: selection.endLineNumber,
});
}
}
}
);
};
// 高亮代码行
const highlightLines = useCallback(
(lines: number[], type: 'error' | 'warning' | 'info') => {
if (!editorRef.current) return;
const decorations = lines.map((line) => ({
range: new monaco.Range(line, 1, line, 1),
options: {
isWholeLine: true,
className: `line-highlight-${type}`,
glyphMarginClassName: `glyph-${type}`,
},
}));
addDecoration(decorations);
},
[addDecoration]
);
return (
<div className="editor-container h-full w-full">
<Editor
height="100%"
defaultLanguage={language}
value={value}
onChange={onChange}
onMount={handleEditorMount}
options={{
minimap: { enabled: true },
fontSize: 14,
lineNumbers: 'on',
roundedSelection: false,
scrollBeyondLastLine: false,
readOnly: false,
automaticLayout: true,
glyphMargin: true,
folding: true,
renderWhitespace: 'selection',
suggest: {
showKeywords: true,
showSnippets: true,
},
}}
/>
</div>
);
};typescript
// src/components/Chat/ChatPanel.tsx
import React, { useState, useRef, useEffect } from 'react';
import { MessageList } from './MessageList';
import { useAgent } from '../../hooks/useAgent';
import { useChatStore } from '../../store/chat.store';
import { Message } from '../../types';
export const ChatPanel: React.FC = () => {
const [input, setInput] = useState('');
const messagesEndRef = useRef<HTMLDivElement>(null);
const { messages, isLoading, addMessage } = useChatStore();
const { sendMessage, explainSelection, refactorSelection, detectIssues } =
useAgent();
// 自动滚动到底部
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const handleSend = async () => {
if (!input.trim() || isLoading) return;
const userMessage: Message = {
id: Date.now().toString(),
role: 'user',
content: input,
timestamp: new Date(),
};
addMessage(userMessage);
setInput('');
try {
const response = await sendMessage(input);
addMessage({
id: (Date.now() + 1).toString(),
role: 'assistant',
content: response.content,
timestamp: new Date(),
suggestions: response.suggestions,
});
} catch (error) {
addMessage({
id: (Date.now() + 1).toString(),
role: 'error',
content: 'Failed to get response. Please try again.',
timestamp: new Date(),
});
}
};
const handleQuickAction = async (action: string, code: string) => {
let response;
switch (action) {
case 'explain':
response = await explainSelection(code);
break;
case 'refactor':
response = await refactorSelection(code);
break;
case 'detect':
response = await detectIssues(code);
break;
default:
return;
}
addMessage({
id: Date.now().toString(),
role: 'assistant',
content: response.content,
timestamp: new Date(),
codeBlocks: response.codeBlocks,
});
};
return (
<div className="chat-panel flex flex-col h-full bg-gray-50">
{/* 消息列表 */}
<div className="flex-1 overflow-y-auto p-4">
<MessageList
messages={messages}
onQuickAction={handleQuickAction}
/>
<div ref={messagesEndRef} />
</div>
{/* 快捷操作 */}
<div className="quick-actions px-4 py-2 border-t bg-white">
<div className="flex gap-2">
<button
onClick={() => handleQuickAction('explain', getSelectedCode())}
className="px-3 py-1 text-sm bg-blue-100 text-blue-700 rounded hover:bg-blue-200"
>
Explain
</button>
<button
onClick={() => handleQuickAction('refactor', getSelectedCode())}
className="px-3 py-1 text-sm bg-green-100 text-green-700 rounded hover:bg-green-200"
>
Refactor
</button>
<button
onClick={() => handleQuickAction('detect', getSelectedCode())}
className="px-3 py-1 text-sm bg-red-100 text-red-700 rounded hover:bg-red-200"
>
Detect Bugs
</button>
</div>
</div>
{/* 输入框 */}
<div className="input-area p-4 border-t bg-white">
<div className="flex gap-2">
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
}}
placeholder="Ask about your code... (Shift+Enter for new line)"
className="flex-1 p-3 border rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={3}
/>
<button
onClick={handleSend}
disabled={isLoading || !input.trim()}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? '...' : 'Send'}
</button>
</div>
</div>
</div>
);
};typescript
// src/hooks/useAgent.ts
import { useCallback } from 'react';
import { AgentService } from '../services/agent.service';
import { useEditorStore } from '../store/editor.store';
import { useChatStore } from '../store/chat.store';
const agentService = new AgentService();
export const useAgent = () => {
const { currentFile, selectedCode } = useEditorStore();
const { sessionId } = useChatStore();
const sendMessage = useCallback(
async (message: string) => {
return agentService.chat({
sessionId,
message,
codeContext: selectedCode
? {
code: selectedCode.text,
fileName: currentFile?.name,
language: currentFile?.language,
}
: undefined,
});
},
[sessionId, selectedCode, currentFile]
);
const explainSelection = useCallback(
async (code: string) => {
return agentService.explain({
sessionId,
code,
fileName: currentFile?.name,
});
},
[sessionId, currentFile]
);
const refactorSelection = useCallback(
async (code: string) => {
return agentService.refactor({
sessionId,
code,
strategy: 'all',
});
},
[sessionId]
);
const detectIssues = useCallback(
async (code: string) => {
return agentService.detect({
sessionId,
code,
});
},
[sessionId]
);
return {
sendMessage,
explainSelection,
refactorSelection,
detectIssues,
};
};typescript
// src/services/agent.service.ts
import { API_BASE_URL } from '../config';
interface ChatRequest {
sessionId: string;
message: string;
codeContext?: {
code: string;
fileName?: string;
language?: string;
};
}
interface ExplainRequest {
sessionId: string;
code: string;
fileName?: string;
}
interface RefactorRequest {
sessionId: string;
code: string;
strategy: string;
}
interface DetectRequest {
sessionId: string;
code: string;
}
export class AgentService {
private baseUrl = API_BASE_URL;
async chat(request: ChatRequest) {
const response = await fetch(`${this.baseUrl}/agent/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request),
});
if (!response.ok) {
throw new Error('Failed to send message');
}
return response.json();
}
async explain(request: ExplainRequest) {
const response = await fetch(`${this.baseUrl}/agent/explain`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request),
});
return response.json();
}
async refactor(request: RefactorRequest) {
const response = await fetch(`${this.baseUrl}/agent/refactor`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request),
});
return response.json();
}
async detect(request: DetectRequest) {
const response = await fetch(`${this.baseUrl}/agent/detect`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request),
});
return response.json();
}
}4.3 主应用组件
typescript
// src/App.tsx
import React, { useState } from 'react';
import { MonacoEditor } from './components/Editor/MonacoEditor';
import { ChatPanel } from './components/Chat/ChatPanel';
import { FileTree } from './components/Sidebar/FileTree';
import { Toolbar } from './components/Toolbar/ActionButtons';
import { useEditorStore } from './store/editor.store';
import './App.css';
function App() {
const [sidebarWidth, setSidebarWidth] = useState(250);
const [chatWidth, setChatWidth] = useState(400);
const { currentFile, setCurrentFile, selectedCode, setSelectedCode } =
useEditorStore();
const handleFileSelect = (file: FileNode) => {
setCurrentFile(file);
};
const handleCodeSelect = (selection: Selection) => {
setSelectedCode(selection);
};
return (
<div className="app h-screen flex flex-col">
{/* 顶部工具栏 */}
<Toolbar />
{/* 主内容区 */}
<div className="flex-1 flex overflow-hidden">
{/* 左侧文件树 */}
<div
className="sidebar bg-gray-900 text-white overflow-auto"
style={{ width: sidebarWidth }}
>
<FileTree onSelect={handleFileSelect} />
</div>
{/* 拖拽调整条 */}
<div
className="resize-bar w-1 bg-gray-300 cursor-col-resize hover:bg-blue-500"
onMouseDown={(e) => handleResize(e, setSidebarWidth)}
/>
{/* 中间编辑器 */}
<div className="editor flex-1">
{currentFile ? (
<MonacoEditor
value={currentFile.content}
language={currentFile.language}
onChange={(value) => {
setCurrentFile({ ...currentFile, content: value || '' });
}}
onSelect={handleCodeSelect}
/>
) : (
<div className="h-full flex items-center justify-center text-gray-400">
Select a file to start
</div>
)}
</div>
{/* 拖拽调整条 */}
<div
className="resize-bar w-1 bg-gray-300 cursor-col-resize hover:bg-blue-500"
onMouseDown={(e) => handleResize(e, setChatWidth)}
/>
{/* 右侧聊天面板 */}
<div
className="chat-panel border-l"
style={{ width: chatWidth }}
>
<ChatPanel />
</div>
</div>
</div>
);
}
function handleResize(
e: React.MouseEvent,
setWidth: (width: number) => void
) {
const startX = e.clientX;
const startWidth = (e.target as HTMLElement).parentElement?.clientWidth || 0;
const handleMouseMove = (e: MouseEvent) => {
const diff = e.clientX - startX;
setWidth(Math.max(200, Math.min(500, startWidth + diff)));
};
const handleMouseUp = () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
}
export default App;4.4 package.json
json
{
"name": "ai-code-assistant-web",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"lint": "eslint . --ext ts,tsx"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"@monaco-editor/react": "^4.6.0",
"monaco-editor": "^0.44.0",
"zustand": "^4.4.0",
"react-markdown": "^9.0.0",
"react-syntax-highlighter": "^15.5.0",
"lucide-react": "^0.292.0",
"clsx": "^2.0.0"
},
"devDependencies": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.2.0",
"typescript": "^5.0.0",
"vite": "^5.0.0",
"tailwindcss": "^3.3.0",
"autoprefixer": "^10.4.0",
"postcss": "^8.4.0",
"eslint": "^8.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0"
}
}五、VS Code 插件基础
5.1 插件结构
ai-code-assistant-vscode/
├── src/
│ ├── extension.ts # 插件入口
│ ├── commands/
│ │ ├── explain.ts # 解释命令
│ │ ├── refactor.ts # 重构命令
│ │ └── detect.ts # 检测命令
│ ├── providers/
│ │ ├── inlineCompletion.ts # 内联补全
│ │ └── codeAction.ts # 代码操作
│ ├── services/
│ │ └── agent.service.ts # Agent服务
│ └── utils/
│ └── editor.ts # 编辑器工具
├── package.json # 插件配置
└── tsconfig.json5.2 核心实现
typescript
// src/extension.ts
import * as vscode from 'vscode';
import { ExplainCommand } from './commands/explain';
import { RefactorCommand } from './commands/refactor';
import { DetectCommand } from './commands/detect';
import { InlineCompletionProvider } from './providers/inlineCompletion';
import { CodeActionProvider } from './providers/codeAction';
export function activate(context: vscode.ExtensionContext) {
console.log('AI Code Assistant is now active!');
// 注册命令
context.subscriptions.push(
vscode.commands.registerCommand(
'aica.explain',
() => ExplainCommand.run()
),
vscode.commands.registerCommand(
'aica.refactor',
() => RefactorCommand.run()
),
vscode.commands.registerCommand(
'aica.detect',
() => DetectCommand.run()
)
);
// 注册内联补全
context.subscriptions.push(
vscode.languages.registerInlineCompletionItemProvider(
{ pattern: '**' },
new InlineCompletionProvider()
)
);
// 注册代码操作
context.subscriptions.push(
vscode.languages.registerCodeActionsProvider(
{ pattern: '**' },
new CodeActionProvider(),
{
providedCodeActionKinds: [
vscode.CodeActionKind.QuickFix,
vscode.CodeActionKind.Refactor,
],
}
)
);
// 注册侧边栏
const treeDataProvider = new ChatTreeDataProvider();
vscode.window.registerTreeDataProvider('aicaChat', treeDataProvider);
}
export function deactivate() {}typescript
// src/commands/explain.ts
import * as vscode from 'vscode';
import { AgentService } from '../services/agent.service';
export class ExplainCommand {
static async run() {
const editor = vscode.window.activeTextEditor;
if (!editor) {
vscode.window.showErrorMessage('No active editor');
return;
}
const selection = editor.selection;
const selectedText = editor.document.getText(selection);
if (!selectedText) {
vscode.window.showWarningMessage('Please select some code first');
return;
}
// 显示进度
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: 'Analyzing code...',
cancellable: false,
},
async () => {
try {
const agent = new AgentService();
const explanation = await agent.explain({
code: selectedText,
language: editor.document.languageId,
});
// 显示结果在Webview面板
const panel = vscode.window.createWebviewPanel(
'aicaExplanation',
'Code Explanation',
vscode.ViewColumn.Beside,
{}
);
panel.webview.html = getExplanationHtml(explanation);
} catch (error) {
vscode.window.showErrorMessage(`Error: ${error.message}`);
}
}
);
}
}
function getExplanationHtml(explanation: string): string {
return `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: sans-serif; padding: 20px; }
pre { background: #f4f4f4; padding: 10px; border-radius: 5px; }
</style>
</head>
<body>
<h2>Code Explanation</h2>
<div>${explanation}</div>
</body>
</html>
`;
}typescript
// src/providers/inlineCompletion.ts
import * as vscode from 'vscode';
import { AgentService } from '../services/agent.service';
export class InlineCompletionProvider
implements vscode.InlineCompletionItemProvider
{
private agent = new AgentService();
private debounceTimer: NodeJS.Timeout | null = null;
async provideInlineCompletionItems(
document: vscode.TextDocument,
position: vscode.Position,
context: vscode.InlineCompletionContext,
token: vscode.CancellationToken
): Promise<vscode.InlineCompletionItem[] | null> {
// Debounce
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
return new Promise((resolve) => {
this.debounceTimer = setTimeout(async () => {
try {
const linePrefix = document
.lineAt(position)
.text.substr(0, position.character);
// 获取上下文
const contextCode = this.getContext(document, position);
const completion = await this.agent.complete({
prefix: linePrefix,
context: contextCode,
language: document.languageId,
});
if (completion) {
resolve([
new vscode.InlineCompletionItem(
completion,
new vscode.Range(position, position)
),
]);
} else {
resolve(null);
}
} catch (error) {
resolve(null);
}
}, 300);
});
}
private getContext(
document: vscode.TextDocument,
position: vscode.Position
): string {
const startLine = Math.max(0, position.line - 50);
const endLine = position.line;
const range = new vscode.Range(startLine, 0, endLine, position.character);
return document.getText(range);
}
}json
// package.json
{
"name": "ai-code-assistant",
"displayName": "AI Code Assistant",
"description": "AI-powered code assistant for VS Code",
"version": "1.0.0",
"engines": {
"vscode": "^1.80.0"
},
"categories": ["Machine Learning", "Snippets", "Other"],
"activationEvents": ["onStartupFinished"],
"main": "./out/extension.js",
"contributes": {
"commands": [
{
"command": "aica.explain",
"title": "Explain Code",
"category": "AICA"
},
{
"command": "aica.refactor",
"title": "Refactor Code",
"category": "AICA"
},
{
"command": "aica.detect",
"title": "Detect Issues",
"category": "AICA"
}
],
"menus": {
"editor/context": [
{
"command": "aica.explain",
"group": "9_cutcopypaste@5",
"when": "editorHasSelection"
},
{
"command": "aica.refactor",
"group": "9_cutcopypaste@6",
"when": "editorHasSelection"
}
]
},
"keybindings": [
{
"command": "aica.explain",
"key": "ctrl+shift+e",
"mac": "cmd+shift+e",
"when": "editorHasSelection"
}
],
"views": {
"explorer": [
{
"id": "aicaChat",
"name": "AI Assistant"
}
]
},
"configuration": {
"title": "AI Code Assistant",
"properties": {
"aica.apiKey": {
"type": "string",
"default": "",
"description": "API Key for AI service"
},
"aica.model": {
"type": "string",
"default": "gpt-4",
"enum": ["gpt-4", "gpt-3.5-turbo", "claude-3"],
"description": "AI Model to use"
}
}
}
},
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./"
},
"devDependencies": {
"@types/vscode": "^1.80.0",
"@types/node": "^20.0.0",
"typescript": "^5.0.0"
}
}六、上下文感知与多轮对话
6.1 上下文架构
┌─────────────────────────────────────────────────────────────┐
│ 上下文层次结构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Layer 4: 项目上下文 (Project Context) │
│ ├── 文件结构 │
│ ├── 依赖关系 │
│ ├── 配置文件 │
│ └── 代码规范 │
│ │
│ Layer 3: 文件上下文 (File Context) │
│ ├── 导入/导出 │
│ ├── 类/函数定义 │
│ ├── 类型定义 │
│ └── 注释文档 │
│ │
│ Layer 2: 代码上下文 (Code Context) │
│ ├── 当前函数 │
│ ├── 相关变量 │
│ ├── 调用链 │
│ └── 选中代码 │
│ │
│ Layer 1: 对话上下文 (Conversation Context) │
│ ├── 历史消息 │
│ ├── 用户偏好 │
│ └── 会话状态 │
│ │
└─────────────────────────────────────────────────────────────┘6.2 上下文收集实现
typescript
// src/core/context/ProjectContext.ts
import { glob } from 'glob';
import { readFile } from 'fs/promises';
import { parse } from '@babel/parser';
import traverse from '@babel/traverse';
interface ProjectContext {
fileStructure: FileNode[];
dependencies: DependencyGraph;
entryPoints: string[];
configFiles: ConfigFile[];
codeStats: CodeStatistics;
}
class ProjectContextCollector {
async collect(projectPath: string): Promise<ProjectContext> {
const [fileStructure, dependencies, configFiles] = await Promise.all([
this.scanFileStructure(projectPath),
this.buildDependencyGraph(projectPath),
this.readConfigFiles(projectPath),
]);
return {
fileStructure,
dependencies,
entryPoints: this.findEntryPoints(fileStructure),
configFiles,
codeStats: await this.calculateStats(projectPath),
};
}
private async scanFileStructure(projectPath: string): Promise<FileNode[]> {
const files = await glob('**/*.{js,ts,jsx,tsx}', {
cwd: projectPath,
ignore: ['node_modules/**', 'dist/**', 'build/**'],
});
const tree: FileNode[] = [];
for (const file of files.slice(0, 100)) {
// 限制文件数量
const content = await readFile(`${projectPath}/${file}`, 'utf-8');
const summary = this.summarizeFile(content);
tree.push({
path: file,
type: 'file',
summary,
size: content.length,
});
}
return tree;
}
private summarizeFile(content: string): FileSummary {
try {
const ast = parse(content, {
sourceType: 'module',
plugins: ['typescript', 'jsx'],
});
const summary: FileSummary = {
exports: [],
imports: [],
functions: [],
classes: [],
};
traverse(ast, {
ExportNamedDeclaration(path) {
const name = path.node.declaration
? (path.node.declaration as any).id?.name
: path.node.source?.value;
if (name) summary.exports.push(name);
},
ImportDeclaration(path) {
summary.imports.push({
source: path.node.source.value,
names: path.node.specifiers.map((s) => s.local.name),
});
},
FunctionDeclaration(path) {
summary.functions.push({
name: path.node.id?.name || 'anonymous',
params: path.node.params.length,
});
},
ClassDeclaration(path) {
summary.classes.push({
name: path.node.id?.name,
methods: path.node.body.body.length,
});
},
});
return summary;
} catch {
return { exports: [], imports: [], functions: [], classes: [] };
}
}
private async buildDependencyGraph(
projectPath: string
): Promise<DependencyGraph> {
// 构建模块依赖图
const graph: DependencyGraph = { nodes: [], edges: [] };
const files = await glob('**/*.{js,ts}', {
cwd: projectPath,
ignore: ['node_modules/**'],
});
for (const file of files) {
const content = await readFile(`${projectPath}/${file}`, 'utf-8');
const imports = this.extractImports(content);
graph.nodes.push({ id: file, type: 'module' });
for (const imp of imports) {
if (!imp.startsWith('.')) continue; // 只处理相对导入
const resolved = this.resolveImport(file, imp);
graph.edges.push({ from: file, to: resolved, type: 'import' });
}
}
return graph;
}
private extractImports(content: string): string[] {
const imports: string[] = [];
const importRegex = /import\s+.*?\s+from\s+['"]([^'"]+)['"];?/g;
let match;
while ((match = importRegex.exec(content)) !== null) {
imports.push(match[1]);
}
return imports;
}
// 生成项目上下文摘要
generateSummary(context: ProjectContext): string {
return `
Project Overview:
- Total Files: ${context.fileStructure.length}
- Entry Points: ${context.entryPoints.join(', ')}
- Main Technologies: ${this.detectTechnologies(context.configFiles)}
Key Files:
${context.fileStructure
.slice(0, 10)
.map((f) => ` - ${f.path}`)
.join('\n')}
Dependencies:
${context.dependencies.nodes.length} modules, ${context.dependencies.edges.length} dependencies
`.trim();
}
}6.3 多轮对话管理
typescript
// src/core/context/ConversationManager.ts
interface ConversationSession {
id: string;
messages: Message[];
codeSnapshots: Map<string, CodeSnapshot>;
userPreferences: UserPreferences;
tokenCount: number;
}
interface CodeSnapshot {
timestamp: number;
code: string;
analysis: CodeAnalysis;
version: number;
}
class ConversationManager {
private sessions: Map<string, ConversationSession> = new Map();
private maxTokens = 4000;
private maxMessages = 20;
createSession(id: string): ConversationSession {
const session: ConversationSession = {
id,
messages: [],
codeSnapshots: new Map(),
userPreferences: {
explanationDetail: 'medium',
preferredLanguage: 'typescript',
},
tokenCount: 0,
};
this.sessions.set(id, session);
return session;
}
addMessage(sessionId: string, message: Message): void {
const session = this.sessions.get(sessionId);
if (!session) throw new Error('Session not found');
// 添加消息
session.messages.push({
...message,
timestamp: Date.now(),
});
// 更新Token计数
session.tokenCount += this.estimateTokens(message.content);
// 检查是否需要压缩
if (session.tokenCount > this.maxTokens) {
this.compressSession(sessionId);
}
// 限制消息数量
if (session.messages.length > this.maxMessages) {
this.archiveOldMessages(sessionId);
}
}
// 保存代码快照(用于追踪变更)
saveCodeSnapshot(
sessionId: string,
filePath: string,
code: string,
analysis: CodeAnalysis
): void {
const session = this.sessions.get(sessionId);
if (!session) return;
const existing = session.codeSnapshots.get(filePath);
const snapshot: CodeSnapshot = {
timestamp: Date.now(),
code,
analysis,
version: existing ? existing.version + 1 : 1,
};
session.codeSnapshots.set(filePath, snapshot);
}
// 获取代码变更历史
getCodeHistory(sessionId: string, filePath: string): CodeSnapshot[] {
const session = this.sessions.get(sessionId);
if (!session) return [];
const snapshots: CodeSnapshot[] = [];
session.codeSnapshots.forEach((snapshot, path) => {
if (path === filePath) {
snapshots.push(snapshot);
}
});
return snapshots.sort((a, b) => a.timestamp - b.timestamp);
}
// 智能会话压缩
private compressSession(sessionId: string): void {
const session = this.sessions.get(sessionId);
if (!session) return;
// 保留最近的对话
const recentMessages = session.messages.slice(-5);
// 压缩早期对话
const oldMessages = session.messages.slice(0, -5);
const summary = this.summarizeMessages(oldMessages);
session.messages = [
{
role: 'system',
content: `[Previous conversation summary]: ${summary}`,
timestamp: Date.now(),
},
...recentMessages,
];
session.tokenCount = this.calculateTotalTokens(session.messages);
}
private summarizeMessages(messages: Message[]): string {
// 提取关键主题
const topics = new Set<string>();
const codeFiles = new Set<string>();
messages.forEach((msg) => {
const content = msg.content.toLowerCase();
if (content.includes('explain')) topics.add('code explanation');
if (content.includes('refactor')) topics.add('refactoring');
if (content.includes('bug') || content.includes('fix'))
topics.add('bug fixing');
if (content.includes('test')) topics.add('testing');
// 提取文件名
const fileMatches = msg.content.match(/[\w-]+\.(js|ts|jsx|tsx)/g);
fileMatches?.forEach((f) => codeFiles.add(f));
});
return `Topics: ${Array.from(topics).join(', ')}; Files: ${Array.from(
codeFiles
).join(', ')}`;
}
// 构建Prompt上下文
buildPromptContext(sessionId: string, currentCode?: string): string {
const session = this.sessions.get(sessionId);
if (!session) return '';
const parts: string[] = [];
// 添加用户偏好
parts.push(`User prefers ${session.userPreferences.explanationDetail} explanations.`);
// 添加相关历史
const relevantHistory = this.findRelevantHistory(session, currentCode);
if (relevantHistory.length > 0) {
parts.push('Previous context:');
relevantHistory.forEach((msg) => {
parts.push(`${msg.role}: ${msg.content.substring(0, 200)}...`);
});
}
return parts.join('\n');
}
private findRelevantHistory(
session: ConversationSession,
currentCode?: string
): Message[] {
if (!currentCode) return session.messages.slice(-3);
// 基于代码相似度找到相关历史
return session.messages.filter((msg) => {
// 简单相似度检查
const msgWords = new Set(msg.content.toLowerCase().split(' '));
const codeWords = new Set(currentCode.toLowerCase().split(' '));
const intersection = new Set([...msgWords].filter((x) => codeWords.has(x)));
return intersection.size > 5; // 至少有5个共同词
});
}
private estimateTokens(text: string): number {
return Math.ceil(text.length / 4);
}
private calculateTotalTokens(messages: Message[]): number {
return messages.reduce((sum, msg) => sum + this.estimateTokens(msg.content), 0);
}
}七、完整项目代码和部署
7.1 项目完整结构
ai-code-assistant/
├── packages/
│ ├── cli/ # CLI工具
│ │ ├── src/
│ │ ├── bin/
│ │ ├── package.json
│ │ └── tsconfig.json
│ ├── web/ # Web界面
│ │ ├── src/
│ │ ├── public/
│ │ ├── package.json
│ │ └── vite.config.ts
│ ├── vscode-extension/ # VS Code插件
│ │ ├── src/
│ │ ├── package.json
│ │ └── tsconfig.json
│ └── server/ # 后端服务
│ ├── src/
│ ├── Dockerfile
│ ├── package.json
│ └── docker-compose.yml
├── shared/ # 共享代码
│ ├── types/
│ ├── utils/
│ └── package.json
├── docs/
├── scripts/
├── package.json # Root package.json
├── turbo.json # Turborepo配置
└── README.md7.2 后端服务实现
typescript
// packages/server/src/index.ts
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import { AgentController } from './controllers/agent.controller';
import { authMiddleware } from './middleware/auth';
import { errorHandler } from './middleware/error';
import { logger } from './utils/logger';
const app = express();
const server = createServer(app);
const wss = new WebSocketServer({ server });
// 安全中间件
app.use(helmet());
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
}));
// 限流
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100, // 每个IP 100次请求
message: 'Too many requests, please try again later.',
});
app.use('/api/', limiter);
app.use(express.json({ limit: '10mb' }));
// 认证
app.use('/api', authMiddleware);
// 路由
const agentController = new AgentController();
app.post('/api/agent/chat', agentController.chat.bind(agentController));
app.post('/api/agent/explain', agentController.explain.bind(agentController));
app.post('/api/agent/refactor', agentController.refactor.bind(agentController));
app.post('/api/agent/detect', agentController.detect.bind(agentController));
// WebSocket处理
wss.on('connection', (ws) => {
logger.info('New WebSocket connection');
ws.on('message', async (data) => {
try {
const message = JSON.parse(data.toString());
await agentController.handleWebSocket(ws, message);
} catch (error) {
ws.send(JSON.stringify({ error: error.message }));
}
});
ws.on('close', () => {
logger.info('WebSocket connection closed');
});
});
// 健康检查
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// 错误处理
app.use(errorHandler);
const PORT = process.env.PORT || 3001;
server.listen(PORT, () => {
logger.info(`Server running on port ${PORT}`);
});typescript
// packages/server/src/controllers/agent.controller.ts
import { Request, Response } from 'express';
import WebSocket from 'ws';
import { CodeAssistantAgent } from '../core/agent';
import { ContextManager } from '../core/context';
import { logger } from '../utils/logger';
export class AgentController {
private agent: CodeAssistantAgent;
private contextManager: ContextManager;
constructor() {
this.agent = new CodeAssistantAgent({
apiKey: process.env.OPENAI_API_KEY!,
model: process.env.LLM_MODEL || 'gpt-4',
});
this.contextManager = new ContextManager();
}
async chat(req: Request, res: Response): Promise<void> {
try {
const { sessionId, message, codeContext } = req.body;
// 获取或创建会话
let context = this.contextManager.getSession(sessionId);
if (!context) {
context = this.contextManager.createSession(sessionId);
}
// 添加用户消息
this.contextManager.addMessage(sessionId, {
role: 'user',
content: message,
});
// 调用Agent
const response = await this.agent.chat(message, {
sessionId,
messages: context.messages,
codeContext,
});
// 保存助手回复
this.contextManager.addMessage(sessionId, {
role: 'assistant',
content: response.content,
});
res.json({
success: true,
content: response.content,
suggestions: response.suggestions,
});
} catch (error) {
logger.error('Chat error:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
async explain(req: Request, res: Response): Promise<void> {
try {
const { sessionId, code, fileName } = req.body;
const response = await this.agent.explainCode(code, {
sessionId,
messages: this.contextManager.getHistory(sessionId),
codeContext: {
currentFile: fileName,
selectedCode: code,
},
});
res.json({
success: true,
explanation: response.content,
keyPoints: response.keyPoints,
});
} catch (error) {
logger.error('Explain error:', error);
res.status(500).json({ error: 'Failed to explain code' });
}
}
async refactor(req: Request, res: Response): Promise<void> {
try {
const { sessionId, code, strategy } = req.body;
const suggestions = await this.agent.suggestRefactoring(code, {
sessionId,
messages: this.contextManager.getHistory(sessionId),
});
res.json({
success: true,
suggestions,
});
} catch (error) {
logger.error('Refactor error:', error);
res.status(500).json({ error: 'Failed to generate suggestions' });
}
}
async detect(req: Request, res: Response): Promise<void> {
try {
const { sessionId, code } = req.body;
const issues = await this.agent.detectBugs(code, {
sessionId,
messages: this.contextManager.getHistory(sessionId),
});
res.json({
success: true,
issues,
summary: {
high: issues.filter((i) => i.severity === 'high').length,
medium: issues.filter((i) => i.severity === 'medium').length,
low: issues.filter((i) => i.severity === 'low').length,
},
});
} catch (error) {
logger.error('Detect error:', error);
res.status(500).json({ error: 'Failed to detect issues' });
}
}
// WebSocket处理
async handleWebSocket(ws: WebSocket, message: any): Promise<void> {
const { type, sessionId, payload } = message;
switch (type) {
case 'chat':
// 流式响应
await this.handleStreamingChat(ws, sessionId, payload);
break;
case 'explain':
case 'refactor':
case 'detect':
// 非流式响应
break;
default:
ws.send(JSON.stringify({ error: 'Unknown message type' }));
}
}
private async handleStreamingChat(
ws: WebSocket,
sessionId: string,
payload: any
): Promise<void> {
try {
const stream = await this.agent.streamChat(payload.message, {
sessionId,
messages: this.contextManager.getHistory(sessionId),
codeContext: payload.codeContext,
});
for await (const chunk of stream) {
ws.send(
JSON.stringify({
type: 'chunk',
content: chunk.content,
})
);
}
ws.send(JSON.stringify({ type: 'done' }));
} catch (error) {
ws.send(JSON.stringify({ type: 'error', message: error.message }));
}
}
}7.3 Docker部署
dockerfile
# packages/server/Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
# 安装依赖
COPY package*.json ./
COPY tsconfig.json ./
RUN npm ci
# 复制源码
COPY src ./src
# 构建
RUN npm run build
# 生产镜像
FROM node:18-alpine
WORKDIR /app
# 只复制必要文件
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
ENV NODE_ENV=production
ENV PORT=3001
EXPOSE 3001
CMD ["node", "dist/index.js"]yaml
# docker-compose.yml
version: '3.8'
services:
server:
build:
context: ./packages/server
dockerfile: Dockerfile
ports:
- '3001:3001'
environment:
- NODE_ENV=production
- PORT=3001
- OPENAI_API_KEY=${OPENAI_API_KEY}
- LLM_MODEL=${LLM_MODEL:-gpt-4}
- REDIS_URL=redis://redis:6379
depends_on:
- redis
restart: unless-stopped
redis:
image: redis:7-alpine
ports:
- '6379:6379'
volumes:
- redis_data:/data
restart: unless-stopped
web:
build:
context: ./packages/web
dockerfile: Dockerfile
ports:
- '3000:80'
environment:
- VITE_API_URL=http://localhost:3001
depends_on:
- server
restart: unless-stopped
volumes:
redis_data:7.4 部署脚本
bash
#!/bin/bash
# scripts/deploy.sh
set -e
echo "🚀 Deploying AI Code Assistant..."
# 检查环境变量
if [ -z "$OPENAI_API_KEY" ]; then
echo "❌ OPENAI_API_KEY is not set"
exit 1
fi
# 构建所有包
echo "📦 Building packages..."
npm run build
# 构建Docker镜像
echo "🐳 Building Docker images..."
docker-compose build
# 启动服务
echo "▶️ Starting services..."
docker-compose up -d
# 健康检查
echo "🏥 Health check..."
sleep 5
curl -f http://localhost:3001/health || exit 1
curl -f http://localhost:3000 || exit 1
echo "✅ Deployment successful!"
echo " Web: http://localhost:3000"
echo " API: http://localhost:3001"八、面试考点
8.1 代码分析相关
Q1: 什么是AST?在代码分析中有什么作用?
AST(抽象语法树)是源代码的树状表示,它将代码结构化为节点和边。
作用:
1. 代码解析:将文本代码转换为结构化数据
2. 静态分析:检测代码模式、复杂度、潜在问题
3. 代码转换:Babel转译、代码格式化
4. 智能提示:IDE自动补全、跳转定义
示例:
代码: function add(a, b) { return a + b; }
AST:
FunctionDeclaration
├── id: Identifier (add)
├── params: [Identifier(a), Identifier(b)]
└── body: BlockStatement
└── ReturnStatement
└── BinaryExpression (+)
├── left: Identifier(a)
└── right: Identifier(b)Q2: 如何计算代码的圈复杂度?
typescript
// 圈复杂度 = 决策点数量 + 1
// 决策点:if, while, for, case, catch, &&, ||, ?:
function calculateCyclomaticComplexity(code: string): number {
const ast = parse(code);
let complexity = 1; // 基础复杂度
traverse(ast, {
// 条件语句
IfStatement() { complexity++; },
ConditionalExpression() { complexity++; },
// 循环语句
WhileStatement() { complexity++; },
ForStatement() { complexity++; },
ForInStatement() { complexity++; },
ForOfStatement() { complexity++; },
DoWhileStatement() { complexity++; },
// Switch
SwitchCase(path) {
if (path.node.test) complexity++; // 排除default
},
// 异常处理
CatchClause() { complexity++; },
// 逻辑运算符
LogicalExpression(path) {
if (path.node.operator === '&&' || path.node.operator === '||') {
complexity++;
}
},
});
return complexity;
}
// 复杂度等级:
// 1-10: 低风险
// 11-20: 中等风险
// 21-50: 高风险
// >50: 极高风险,需要重构Q3: 如何处理大规模代码库的AST分析性能问题?
typescript
// 1. 增量分析 - 只分析变更的文件
class IncrementalAnalyzer {
private cache: Map<string, ASTCacheEntry> = new Map();
async analyzeFile(filePath: string, content: string): Promise<AST> {
const hash = this.computeHash(content);
const cached = this.cache.get(filePath);
if (cached && cached.hash === hash) {
return cached.ast; // 使用缓存
}
const ast = await this.parse(content);
this.cache.set(filePath, { hash, ast, timestamp: Date.now() });
return ast;
}
}
// 2. Worker线程并行处理
import { Worker } from 'worker_threads';
class ParallelAnalyzer {
private workers: Worker[] = [];
async analyzeBatch(files: string[]): Promise<AnalysisResult[]> {
const chunks = this.chunkArray(files, this.workers.length);
const promises = chunks.map((chunk, i) =>
this.assignToWorker(this.workers[i], chunk)
);
return (await Promise.all(promises)).flat();
}
}
// 3. 流式解析 - 大文件分块处理
async function* streamParse(filePath: string): AsyncGenerator<ASTNode> {
const stream = createReadStream(filePath);
const parser = createParser();
for await (const chunk of stream) {
const nodes = parser.feed(chunk);
for (const node of nodes) {
yield node;
}
}
}
// 4. 索引和预过滤
class CodeIndex {
private index: MiniSearch;
buildIndex(files: FileInfo[]) {
this.index = new MiniSearch({
fields: ['content', 'exports', 'imports'],
storeFields: ['path', 'summary'],
});
this.index.addAll(files);
}
search(query: string): FileInfo[] {
return this.index.search(query);
}
}8.2 Agent设计相关
Q4: 如何设计一个支持多轮对话的AI Agent?
核心要素:
1. 会话管理
- 会话ID标识
- 消息历史存储
- 上下文窗口管理
2. 上下文压缩
- Token限制处理
- 历史消息摘要
- 关键信息保留
3. 状态追踪
- 用户意图识别
- 对话状态机
- 槽位填充
4. 记忆机制
- 短期记忆(当前会话)
- 长期记忆(用户偏好)
- 知识库检索
实现架构:
┌─────────────────────────────────────────┐
│ User Input │
└─────────────┬───────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ Intent Recognition │
│ - 分类用户意图 │
│ - 提取关键实体 │
└─────────────┬───────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ Context Assembly │
│ - 加载历史消息 │
│ - 检索相关知识 │
│ - 压缩超长上下文 │
└─────────────┬───────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ LLM Processing │
│ - 生成回复 │
│ - 工具调用 │
└─────────────┬───────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ Response & State Update │
│ - 返回结果 │
│ - 更新会话状态 │
│ - 保存记忆 │
└─────────────────────────────────────────┘Q5: 如何处理LLM的Token限制问题?
typescript
// 策略1: 滑动窗口
class SlidingWindowContext {
private maxTokens = 4000;
private reserveTokens = 500; // 预留空间给回复
buildMessages(history: Message[], currentInput: string): Message[] {
const systemPrompt = this.getSystemPrompt();
const availableTokens = this.maxTokens - this.reserveTokens;
let totalTokens = this.estimateTokens(systemPrompt);
const selectedMessages: Message[] = [];
// 从最新的消息开始选择
for (let i = history.length - 1; i >= 0; i--) {
const msgTokens = this.estimateTokens(history[i].content);
if (totalTokens + msgTokens > availableTokens) {
break;
}
selectedMessages.unshift(history[i]);
totalTokens += msgTokens;
}
return [
{ role: 'system', content: systemPrompt },
...selectedMessages,
{ role: 'user', content: currentInput },
];
}
}
// 策略2: 分层摘要
class HierarchicalSummarizer {
async compressMessages(messages: Message[]): Promise<Message[]> {
if (messages.length <= 10) return messages;
// 早期消息摘要
const oldMessages = messages.slice(0, -10);
const recentMessages = messages.slice(-10);
const summary = await this.summarize(oldMessages);
return [
{ role: 'system', content: `[Summary]: ${summary}` },
...recentMessages,
];
}
private async summarize(messages: Message[]): Promise<string> {
const prompt = `Summarize the following conversation, keeping key facts and user preferences:\n\n${
messages.map(m => `${m.role}: ${m.content}`).join('\n')
}`;
const response = await this.llm.complete(prompt);
return response;
}
}
// 策略3: 向量检索
class VectorContextRetriever {
private vectorStore: VectorStore;
async buildContext(query: string, allMessages: Message[]): Promise<Message[]> {
// 将消息转换为向量
const embeddings = await Promise.all(
allMessages.map(m => this.embed(m.content))
);
// 存储向量
await this.vectorStore.upsert(
allMessages.map((m, i) => ({
id: m.id,
vector: embeddings[i],
metadata: { role: m.role, content: m.content },
}))
);
// 检索相关消息
const queryEmbedding = await this.embed(query);
const relevant = await this.vectorStore.query(queryEmbedding, 5);
return relevant.map(r => ({
role: r.metadata.role,
content: r.metadata.content,
}));
}
}8.3 性能优化相关
Q6: 如何优化AI编程助手的响应速度?
typescript
// 1. 流式响应
async function* streamResponse(prompt: string): AsyncGenerator<string> {
const stream = await openai.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: prompt }],
stream: true,
});
for await (const chunk of stream) {
yield chunk.choices[0]?.delta?.content || '';
}
}
// 2. 智能缓存
class SmartCache {
private cache: LRUCache<string, CacheEntry>;
async getOrCompute(
key: string,
compute: () => Promise<string>,
ttl: number = 3600000
): Promise<string> {
// 规范化key(去除空格、变量名等)
const normalizedKey = this.normalize(key);
const cached = this.cache.get(normalizedKey);
if (cached && !this.isExpired(cached, ttl)) {
return cached.value;
}
const value = await compute();
this.cache.set(normalizedKey, { value, timestamp: Date.now() });
return value;
}
private normalize(code: string): string {
return code
.replace(/\s+/g, ' ') // 标准化空白
.replace(/'[^']*'/g, "''") // 规范化字符串
.replace(/"[^"]*"/g, '""') // 规范化字符串
.trim();
}
}
// 3. 预加载和预热
class Preloader {
async warmup(): Promise<void> {
// 预热常用Prompt模板
const templates = [
this.loadTemplate('explain'),
this.loadTemplate('refactor'),
this.loadTemplate('detect'),
];
// 建立常用代码模式索引
await this.buildPatternIndex();
// 预连接LLM
await this.pingLLM();
}
private async buildPatternIndex(): Promise<void> {
const patterns = [
{ pattern: 'for loop', description: 'Iteration pattern' },
{ pattern: 'async/await', description: 'Async pattern' },
{ pattern: 'try/catch', description: 'Error handling' },
];
for (const p of patterns) {
const embedding = await this.embed(p.pattern);
await this.index.store(embedding, p);
}
}
}
// 4. 并发控制
class ConcurrencyController {
private semaphore: Semaphore;
constructor(maxConcurrency: number = 5) {
this.semaphore = new Semaphore(maxConcurrency);
}
async execute<T>(fn: () => Promise<T>): Promise<T> {
await this.semaphore.acquire();
try {
return await fn();
} finally {
this.semaphore.release();
}
}
}
// 5. 代码预处理减少Token
function optimizeCodeForLLM(code: string): string {
return code
.replace(/\/\*[\s\S]*?\*\//g, '') // 移除多行注释
.replace(/\/\/.*$/gm, '') // 移除单行注释
.replace(/^\s*import.*$/gm, '') // 移除导入语句(可单独提供)
.replace(/\n\s*\n/g, '\n') // 移除空行
.trim();
}Q7: 如何保证代码分析的安全性?
typescript
// 1. 输入验证和过滤
class InputValidator {
validateCode(code: string): ValidationResult {
// 检查代码大小
if (code.length > 100000) {
return { valid: false, error: 'Code too large' };
}
// 检测敏感模式
const dangerousPatterns = [
/eval\s*\(/i,
/Function\s*\(/i,
/child_process/,
/fs\s*\.\s*unlink/,
];
for (const pattern of dangerousPatterns) {
if (pattern.test(code)) {
return { valid: false, error: 'Potentially dangerous code detected' };
}
}
return { valid: true };
}
}
// 2. 沙箱执行
import { VM } from 'vm2';
class SandboxExecutor {
private vm: VM;
constructor() {
this.vm = new VM({
timeout: 1000,
sandbox: {},
require: {
external: false,
builtin: [],
},
});
}
safeExecute(code: string): any {
try {
return this.vm.run(code);
} catch (error) {
return { error: error.message };
}
}
}
// 3. 输出过滤
class OutputSanitizer {
sanitize(text: string): string {
// 移除潜在的XSS
return text
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
}
// 4. 速率限制和配额
class RateLimiter {
private limits: Map<string, RateLimit> = new Map();
checkLimit(userId: string): boolean {
const now = Date.now();
const limit = this.limits.get(userId) || {
count: 0,
resetTime: now + 3600000, // 1小时
};
if (now > limit.resetTime) {
limit.count = 0;
limit.resetTime = now + 3600000;
}
if (limit.count >= 100) { // 每小时100次
return false;
}
limit.count++;
this.limits.set(userId, limit);
return true;
}
}避坑指南
Token超限问题
- 始终监控Token使用量
- 实现智能上下文压缩
- 预留足够的响应Token空间
AST解析兼容性
- 不同语言版本需要不同parser配置
- 处理语法错误时的降级策略
- 大文件解析的性能问题
上下文丢失
- 会话超时处理
- 代码变更检测
- 多文件关联分析
LLM幻觉问题
- 结合静态分析验证
- 提供代码执行环境验证
- 置信度评分机制
扩展阅读
课后练习
- 基础练习:实现一个支持Python代码分析的CLI工具
- 进阶练习:添加代码相似度检测功能,识别重复代码
- 挑战练习:实现一个支持多语言(JS/Python/Java)的统一分析平台
- 实战项目:将本项目的CLI工具集成到你的日常开发工作流中