在 AI 编程助手爆发的时代,理解 Agent 的底层原理比使用工具更重要。本项目实现了一个从零构建的 VSCode Agent 插件,包含:
- ✅ 大模型意图拆解(Plan-and-Execute)
- ✅ 动态工具调用(Function Calling)
- ✅ 安全的文件操作与授权
- ✅ 流式内容展示(SSE + Markdown)
- ✅ 终端命令执行
- ✅ Diff 预览与确认机制
架构设计
数据流向:
- 用户输入 → React UI → Extension → Server
- 意图拆解 → Server 调用 OpenAI(带工具定义)
- 工具执行 → Server 下发 Tool Call → Extension 执行 → 结果回传
- 循环迭代 → Server 再次请求 OpenAI → 直到任务完成
- 流式展示 → Server 通过 SSE 推送内容 → React 实时渲染
1. 工具调用
Function Calling 是 OpenAI 提供的一种机制,允许模型在需要时调用外部函数来获取信息或执行操作。它不是让模型直接执行代码,而是让模型生成结构化的函数调用请求,由开发者执行后返回结果。
定义可用工具列表(Tool Definitions)
// src/tools/definitions.ts
export const TOOLS = [
{
type: "function",
function: {
name: "read_file",
description: "读取指定路径的文件内容。用于获取现有文件的内容进行分析或修改。",
parameters: {
type: "object",
properties: {
path: {
type: "string",
description: "文件的绝对路径。建议使用工作区内的相对路径或绝对路径。"
}
},
required: ["path"]
}
}
},
{
type: "function",
function: {
name: "write_file",
description: "写入内容到文件。会覆盖原文件内容。修改前建议先读取原文件进行对比。",
parameters: {
type: "object",
properties: {
path: {
type: "string",
description: "文件的绝对路径。"
},
content: {
type: "string",
description: "要写入的文件内容。确保内容格式正确。"
}
},
required: ["path", "content"]
}
}
},
{
type: "function",
function: {
name: "run_command",
description: "在终端执行 Shell 命令。用于运行构建、测试、安装依赖等操作。",
parameters: {
type: "object",
properties: {
command: {
type: "string",
description: "要执行的 Shell 命令。确保命令安全且可执行。"
}
},
required: ["command"]
}
}
},
{
type: "function",
function: {
name: "search_web",
description: "搜索网页获取信息。当需要最新信息或不确定答案时使用。",
parameters: {
type: "object",
properties: {
query: {
type: "string",
description: "搜索关键词"
}
},
required: ["query"]
}
}
}
] as const;
工具注入OpenAI
// 🔥 核心:调用 OpenAI 并传入工具定义
const stream = await openai.chat.completions.create({
model: 'gpt-4o-2024-08-06',
messages: messages as any,
tools: TOOLS, // 传入工具定义
tool_choice: 'auto', // 让模型自动决定是否调用
stream: true,
});
获取OpenAI返回的工具调用列表
let toolCalls: any[] = [];
// 处理流式响应
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta;
// 🔧 处理 Tool Calls(关键!)
if (delta?.tool_calls) {
for (const tc of delta.tool_calls) {
const index = tc.index || 0;
if (!toolCalls[index]) {
toolCalls[index] = {
id: tc.id || `call_${Date.now()}_${index}`,
function: { name: '', arguments: '' }
};
}
// 累积参数(流式返回可能分多次)
if (tc.id) toolCalls[index].id = tc.id;
if (tc.function?.name) toolCalls[index].function.name = tc.function.name;
if (tc.function?.arguments) {
toolCalls[index].function.arguments += tc.function.arguments;
}
}
}
}
解析工具调用配置信息
const parsedToolCalls = toolCalls.map((tc: any) => {
try {
return {
id: tc.id,
name: tc.function.name,
arguments: JSON.parse(tc.function.arguments || '{}')
};
} catch (e) {
return {
id: tc.id,
name: tc.function.name,
arguments: {}
};
}
});
通知 Extension 执行工具
res.write(`data: ${JSON.stringify({
type: 'tool_calls',
data: parsedToolCalls
})}\n\n`);
Extension 端执行工具
// 🔥 核心:执行工具
async execute(toolName: string, args: any): Promise<string> {
switch (toolName) {
case 'read_file':
return await this.readFile(args.path);
case 'write_file':
return await this.writeFile(args.path, args.content);
case 'run_command':
return await this.runCommand(args.command);
case 'search_web':
return await this.searchWeb(args.query);
default:
return `❌ 未知工具: ${toolName}`;
}
}
工具调用流程图
2. 文件系统操作与授权机制
步骤 1: 路径规范化和安全检查
vscode.Uri.file(path)获取虚拟路径vscode.workspace.getWorkspaceFolder(uri)检查是否在工作区
const normalizedPath = this.normalizePath(filePath);
const uri = vscode.Uri.file(normalizedPath);
// 安全检查:是否在工作区内
const workspaceFolder = vscode.workspace.getWorkspaceFolder(uri);
if (!workspaceFolder) {
return `❌ 文件不在工作区内,拒绝修改: ${normalizedPath}`;
}
// 安全检查:文件类型
const fileExt = path.extname(normalizedPath).toLowerCase();
const dangerousExts = ['.exe', '.dll', '.so', '.dylib', '.bin'];
if (dangerousExts.includes(fileExt)) {
return `❌ 拒绝修改二进制文件: ${normalizedPath}`;
}
步骤 2: 检查文件是否存在并读取原内容
vscode.workspace.fs.stat(uri)检查文件/目录是否存在和获取元数据vscode.workspace.fs.readFile(uri)读取文件内容
const stat = await vscode.workspace.fs.stat(uri);
// 返回: { type: FileType, size: number, mtime: number, ... }
// 如果文件不存在,抛出错误:FileNotFoundError
const fileStat = await vscode.workspace.fs.stat(uri);
const fileExists = true;
// 检查是否是文件(不是文件夹)
if (fileStat.type !== vscode.FileType.File) {
return `❌ ${normalizedPath} 是文件夹,不是文件`;
}
// 检查文件大小(防止读取超大文件)
const maxSize = 10 * 1024 * 1024; // 10MB
if (fileStat.size > maxSize) {
return `❌ 文件过大 (${(fileStat.size / 1024 / 1024).toFixed(2)}MB),拒绝修改`;
}
// 读取原内容
const data = await vscode.workspace.fs.readFile(uri);
originalContent = Buffer.from(data).toString('utf-8');
步骤 3: 内容变更检测
if (fileExists && originalContent === newContent) {
return `⏭️ 文件内容未变化,无需修改: ${normalizedPath}`;
}
步骤 4: 写权限检查与授权
vscode.workspace.fs.writeFile()写入文件
if (fileExists) {
// 尝试写入测试(实际权限检查)
try {
// 创建临时文件测试写入权限
const testUri = vscode.Uri.file(path.join(path.dirname(normalizedPath), `.tmp_${Date.now()}`));
await vscode.workspace.fs.writeFile(testUri, Buffer.from('test', 'utf-8'));
await vscode.workspace.fs.delete(testUri);
} catch (error) {
// 没有写入权限,请求用户授权
const granted = await this.requestWritePermission(normalizedPath);
if (!granted) {
return `⛔ 用户拒绝了写权限`;
}
}
}
步骤 6: 执行写入(含备份)
//1 创建备份(可选)
let backupPath: string | undefined;
if (fileExists) {
backupPath = await this.createBackup(normalizedPath, originalContent);
}
// 2 实际写入
try {
await vscode.workspace.fs.writeFile(uri, Buffer.from(newContent, 'utf-8'));
} catch (writeError) {
// 写入失败,恢复备份
if (backupPath) {
await this.restoreBackup(normalizedPath, backupPath);
}
throw writeError;
}
// 3 清理备份(成功后删除)
if (backupPath) {
try {
await vscode.workspace.fs.delete(vscode.Uri.file(backupPath));
} catch (e) {
// 备份删除失败不影响主流程
}
}
授权流程
3. VSCode 授权机制
VSCode 的权限模型是:
- 声明权限(package.json 的 capabilities)
- 用户运行时授权(通过 Dialog 或 Workspace Trust)
方式一:Workspace Trust(工作区信任)
// package.json
{
"capabilities": {
"workspace": {
"read": true,
"write": true,
"trusted": true // 需要用户信任工作区
}
}
}
用户第一次打开文件夹时,VSCode 会询问是否信任:
// 检查工作区是否被信任
const isTrusted = vscode.workspace.isTrusted;
if (!isTrusted) {
const result = await vscode.window.showInformationMessage(
'需要信任工作区才能访问文件',
'信任工作区',
'取消'
);
if (result === '信任工作区') {
await vscode.commands.executeCommand('vscode.trustWorkspace');
}
}
方式二:用户选择授权(最常用)
// ✅ 正确方式:让用户主动选择文件
const result = await vscode.window.showOpenDialog({
defaultUri: vscode.Uri.file(path),
canSelectFiles: true,
canSelectFolders: false,
openLabel: '选择并授权文件'
});
if (result && result.length > 0) {
// 用户选择了文件,URI 自动获得访问权限
const selectedUri = result[0];
const content = await vscode.workspace.fs.readFile(selectedUri);
return content;
}
为什么选择后就获得权限?
- VSCode 在
showOpenDialog时,会通过系统文件选择器获取权限 - 返回的 URI 被标记为 "已授权",后续操作可以正常访问
方式三:全局文件系统 API(最稳健)
// 使用 Node.js fs 模块(需要额外权限)
import * as fs from 'fs';
// 但这样会绕过 VSCode 的权限系统!
// ❌ 不推荐:在 VSCode 插件中使用 Node.js fs
const content = fs.readFileSync(path, 'utf-8');
4. 大模型意图拆解与多步规划
复杂的用户请求(如 "重构整个项目的 API 调用")需要拆解为多个步骤:
- 读取所有 API 文件
- 分析调用模式
- 生成新的代码
- 逐个文件替换
System Prompt 设计
const SYSTEM_PROMPT = `你是一个专业的代码助手 Agent。
## 核心能力
你可以使用以下工具来完成任务:
1. **read_file** - 读取文件内容
2. **write_file** - 写入内容到文件(会覆盖)
3. **run_command** - 执行 Shell 命令
4. **search_web** - 搜索网页获取信息
## 工作流程
1. **理解意图**:分析用户想要什么
2. **拆解步骤**:将复杂任务分解为可执行的小步骤
3. **执行计划**:按步骤调用工具,每一步都验证结果
4. **迭代优化**:如果结果不理想,调整计划继续执行
## 规则
- 每次调用工具后,等待工具结果再继续
- 如果结果已满足用户需求,立即返回最终答案
- 最多调用 10 次工具
- 修改文件前,先读取原内容确认
- 对于不确定的操作,先用 search_web 查询
## 示例
用户:"重构 config.json 中的 API 地址"
你的思考:
1. 读取 config.json → 了解当前配置
2. 分析需要修改哪些 URL
3. 生成新的配置内容
4. 写入文件(显示 Diff 预览)
5. 确认修改完成
开始工作前,先思考并制定计划。`;
多步规划实现(Plan-and-Execute)
// 用户提示词前追加一条 role = 'system' 的message
export class TaskPlanner {
static async planTask(userPrompt: string, openai: OpenAI): Promise<PlanStep[]> {
const response = await openai.chat.completions.create({
model: 'gpt-4o-2024-08-06',
messages: [
{
role: 'system',
content: `你将用户的请求拆解为可执行的步骤列表。
输出格式为 JSON 数组:[{ "step": 1, "action": "描述", "tool": "工具名", "params": {} }]
工具名:read_file, write_file, run_command, search_web`
},
{ role: 'user', content: userPrompt }
],
response_format: { type: 'json_object' }
});
const result = JSON.parse(response.choices[0].message.content || '{"steps": []}');
return result.steps || [];
}
}
// 在 AgentController 中使用
private async executePlan(userPrompt: string): Promise<void> {
// 1️⃣ 生成计划
const steps = await TaskPlanner.planTask(userPrompt, this.openai);
// 2️⃣ 展示计划给用户
this.webviewManager.sendMessage({
type: 'plan',
data: { steps }
});
// 3️⃣ 逐步执行
for (const step of steps) {
this.webviewManager.sendMessage({
type: 'stepStart',
data: { step }
});
try {
// 执行工具
const result = await this.executor.execute(step.tool, step.params);
this.webviewManager.sendMessage({
type: 'stepComplete',
data: { step, result }
});
// 检查是否需要继续
const shouldContinue = await this.checkContinuation(result);
if (!shouldContinue) break;
} catch (error) {
this.webviewManager.sendMessage({
type: 'stepError',
data: { step, error: String(error) }
});
// 询问用户是否继续
const continue = await this.askUserContinue();
if (!continue) break;
}
}
// 4️⃣ 总结
await this.generateSummary();
}
5. 变更内容预览(Diff Preview)
Diff 数据生成与传输
// src/agent/ToolExecutor.ts
private async showDiffPreview(path: string, original: string, modified: string): Promise<boolean> {
const stepId = `diff_${Date.now()}`;
// 发送 Diff 数据到 Webview
this.webviewManager.sendMessage({
type: 'showDiff',
data: { path, original, modified, stepId }
});
// 等待用户确认
return new Promise((resolve) => {
const handler = (msg: any) => {
if (msg.type === 'diffConfirmed' && msg.payload.stepId === stepId) {
resolve(msg.payload.confirmed);
this.webviewManager.offMessage('diffConfirmed', handler);
}
};
this.webviewManager.onMessage('diffConfirmed', handler);
// 超时
setTimeout(() => {
resolve(false);
}, 60000);
});
}
Diff 预览效果
6. 终端执行与命令管理
终端创建与命令执行
private async runCommand(command: string): Promise<string> {
// 1️⃣ 安全检查
const dangerousCommands = [
'rm -rf', 'sudo', 'dd if', 'mkfs',
'chmod 777', 'chown', 'kill -9'
];
if (dangerousCommands.some(cmd => command.includes(cmd))) {
return `❌ 检测到危险命令,已阻止执行: ${command}`;
}
// 2️⃣ 用户确认
const confirm = await vscode.window.showWarningMessage(
`Agent 要执行命令: ${command}`,
{ modal: true },
'✅ 允许执行',
'❌ 拒绝'
);
if (confirm !== '✅ 允许执行') {
return `⛔ 用户拒绝了命令执行`;
}
// 3️⃣ 创建终端
const terminal = vscode.window.createTerminal({
name: 'Agent Command',
cwd: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath,
iconPath: new vscode.ThemeIcon('terminal')
});
// 4️⃣ 显示终端
terminal.show(true);
// 5️⃣ 执行命令
terminal.sendText(command);
// 6️⃣ 捕获输出(需要 Shell Integration)
let output = '';
const outputDisposable = vscode.window.onDidWriteTerminalData((event) => {
if (event.terminal === terminal) {
output += event.data;
}
});
// 7️⃣ 等待命令完成(简化版)
await new Promise(resolve => setTimeout(resolve, 3000));
outputDisposable.dispose();
return `✅ 命令已执行: ${command}\n\n📋 输出(部分):\n${output.slice(-500)}`;
}
7. 授权UI
方式一、全局弹框模式
方式二、内联交互模式
操作流如下所示: