cursor、cline很🔥,AI浪潮下作为前端如何构建自己的vscode编程agent

1. Agent 定义

编程 Agent 指能自主理解开发者意图、执行编码任务并反馈结果的智能体。其核心特征:

  • 环境感知:实时读取编辑器内容/项目结构
  • 任务分解:将复杂需求拆解为可执行步骤
  • 工具调用:通过 API 调用编译/调试等能力
  • 持续学习:基于用户反馈优化决策逻辑

传统 Agent vs 大模型 Agent:

  • 传统:基于硬编码规则(如代码片段模板)
  • 大模型:基于自然语言理解动态生成策略

2. Agent典型结构

typescript 复制代码
// 典型架构示意 
interface CodingAgent { 
llm: LanguageModel; // 大模型核心 
tools: AgentTool[]; // 功能工具集 
memory: VectorDB; // 上下文记忆 
execute(prompt: string): Promise<CodeResult>; }

协同工作模式

  1. 大模型负责:

    • 自然语言理解(需求解析)
    • 代码生成
    • 错误原因推测
  2. 外部工具负责:

    • 代码静态分析
    • 自动化测试执行
    • 依赖关系管理

3. Function Calling 基本结构

核心价值:搭建自然语言与 API 的桥梁

typescript 复制代码
const TOOL_SCHEMA = {
  name: "generateComponent",
  description: "创建 React 组件文件",
  parameters: {
    type: "object",
    properties: {
      name: { type: "string" },
      props: { 
        type: "array",
        items: { type: "string" } 
      },
      useTS: { type: "boolean" }
    }
  }
};

实现流程:

  1. 大模型判断需要调用工具的场景
  2. 返回结构化调用参数
  3. 执行具体函数并返回结果

4. 实现 Agent Tools

工具开发示例:terminal执行及文件的增删改查

typescript 复制代码
import { tool } from "@langchain/core/tools";
import os from "os";
import * as path from "path";
import * as vscode from "vscode";
import { z } from "zod";
import { TerminalManager } from "../../terminal/TerminalManager";
const cwd =
  vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ??
  path.join(os.homedir(), "Desktop");
const terminalManager = new TerminalManager();
export const shell = tool(
  (input) => {
    return new Promise(async (resolve, reject) => {
      const terminalInfo = await terminalManager.getOrCreateTerminal(cwd);
      terminalInfo.terminal.show(); // weird visual bug when creating new terminals (even manually) where there's an empty space at the top.
      const process = terminalManager.runCommand(terminalInfo, input.content);
      let result = "";
      process.on("line", (line) => {
        console.log("line", line);
        result += line + "\n";
      });
     
      process.once("completed", () => {
        resolve(
          `Command executed.${
            result.length > 0
              ? `\nOutput:\n${result.slice(-500)}`
              : ""
          }`
        );
      });
      await process;
      console.log("process final");
    });
  },
  {
    name: "execute_command",
    description: `Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: ${cwd.toPosix()}`,
    schema: z.object({
      content: z
        .string()
        .describe(
          "The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions"
        ),
    }),
  }
);

export const createFile = tool(
  (input) => {
    return new Promise(async (resolve, reject) => {
      try {
        const fileUri = vscode.Uri.joinPath(getRootPath(), input.path);

        // 将内容转换为 Uint8Array 格式
        const data = new TextEncoder().encode(input.content);
        // 写入文件到文件系统
        await vscode.workspace.fs.writeFile(fileUri, data);
        // 写入后打开文件
        const document = await vscode.workspace.openTextDocument(fileUri);
        await vscode.window.showTextDocument(document);
        resolve(`File  created successfully at path: ${input.path}`);
      } catch (error) {
        reject(`createFile Error: ${error}`);
      }
    });
  },
  {
    name: "write_to_file",
    description:
      "Request to write content to a file at the specified path. If the file exists, it will be overwritten with the provided content. If the file doesn't exist, it will be created. This tool will automatically create any directories needed to write the file",
    schema: z.object({
      content: z
        .string()
        .describe(
          " The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified"
        ),
      path: z
        .string()
        .describe(
          `The path of the file to write to (relative to the current working directory ${cwd.toPosix()})`
        ),
    }),
  }
);

export const getFileContent = tool(
  (input) => {
    return new Promise(async (resolve, reject) => {
      try {
        const filePath = vscode.Uri.joinPath(getRootPath(), input.path);
        console.log(`Reading file path: ${filePath}`);
        try {
          const data = await vscode.workspace.fs.readFile(filePath);
          resolve(new TextDecoder().decode(data));
        } catch (error) {
          console.error(`Error reading file: ${error}`);
          reject(`Error reading file: ${error}`);
        }
      } catch (error) {
        reject(`getFileContent Error: ${error}`);
      }
    });
  },
  {
    name: "read_file",
    description:
      "Request to read the contents of a file at the specified path. Use this when you need to examine the contents of an existing file you do not know the contents of, for example to analyze code, review text files, or extract information from configuration files. Automatically extracts raw text from PDF and DOCX files. May not be suitable for other types of binary files, as it returns the raw content as a string.",
    schema: z.object({
      path: z
        .string()
        .describe(
          `The path of the file to read (relative to the current working directory ${cwd.toPosix()})`
        ),
    }),
  }
);

export const deleteFile = tool(
  (input) => {
    return new Promise(async (resolve, reject) => {
      const filePath = vscode.Uri.joinPath(getRootPath(), input.path);
      try {
        await vscode.workspace.fs.delete(filePath);
        console.log(`File  deleted successfully from path: ${filePath}`);
        resolve(`File  deleted successfully from path: ${filePath}`);
      } catch (error) {
        console.error(`Error deleting file: ${error}`);
        reject(`Failed to delete file: ${error}`);
      }
    });
  },
  {
    name: "delete_file",
    description: `A tool to delete a file.(relative to the current working directory ${cwd.toPosix()})`,
    schema: z.object({
      path: z.string().describe("The path to delete the file."),
    }),
  }
);

export const listFiles = tool(
  (input) => {
    return new Promise(async (resolve, reject) => {
      const dirPath = vscode.Uri.joinPath(getRootPath(), input.path);
      try {
        const files = await vscode.workspace.fs.readDirectory(dirPath);
        resolve(JSON.stringify(files.map(([name, _]) => name)));
      } catch (error) {
        reject(`listFiles Error: ${error}`);
      }
    });
  },
  {
    name: "list_files",
    description:
      "Request to list files and directories within the specified directory. If recursive is true, it will list all files and directories recursively. If recursive is false or not provided, it will only list the top-level contents. Do not use this tool to confirm the existence of files you may have created, as the user will let you know if the files were created successfully or not.",
    schema: z.object({
      path: z
        .string()
        .describe(
          `The path of the directory to list contents for (relative to the current working directory ${cwd.toPosix()})`
        ),
    }),
  }
);

export const updatedFile = tool(
  (input) => {
    // fs更新文件内容
    return new Promise(async (resolve, reject) => {
      try {
        const fileUri = vscode.Uri.joinPath(getRootPath(), input.path);

        // 将内容转换为 Uint8Array 格式
        const data = new TextEncoder().encode(input.content);
        // 写入文件到文件系统
        await vscode.workspace.fs.writeFile(fileUri, data);
        const document = await vscode.workspace.openTextDocument(fileUri);
        await vscode.window.showTextDocument(document);
        resolve(`File updated successfully at path: ${input.path}`);
      } catch (error) {
        reject(`updatedFile Error: ${error}`);
      }
    });
  },
  {
    name: "replace_in_file",
    description: `The path of the file to modify (relative to the current working directory ${cwd.toPosix()})`,
    schema: z.object({
      path: z.string().describe("The path to update the file."),
      content: z.string().describe("The new content for the file."),
    }),
  }
);

function getRootPath() {
  const workspaceFolders = vscode.workspace.workspaceFolders;
  if (!workspaceFolders || workspaceFolders.length === 0) {
    throw new Error("未找到工作区,请打开一个文件夹作为工作区");
  }

  // 拼接完整的文件路径
  return workspaceFolders[0].uri;
}

5. 实现基于ReAct结构Agent

typescript 复制代码
import { BaseLanguageModelInput } from "@langchain/core/language_models/base";
import {
  AIMessage,
  BaseMessage,
  HumanMessage,
  SystemMessage,
} from "@langchain/core/messages";
import { Runnable } from "@langchain/core/runnables";
import {
  Annotation,
  BinaryOperatorAggregate,
  CompiledStateGraph,
  END,
  Messages,
  MessagesAnnotation,
  START,
  StateDefinition,
  StateGraph,
  StateType,
  UpdateType,
} from "@langchain/langgraph";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import { ChatOllamaCallOptions } from "@langchain/ollama";
import { ChatOpenAI } from "@langchain/openai";
import * as vscode from "vscode";
import { RAY_CONFIG } from "../consts";
import {
  createFile,
  deleteFile,
  getFileContent,
  listFiles,
  shell,
  updatedFile,
} from "./tools/tools";
const agentTools = [
  createFile,
  shell,
  getFileContent,
  deleteFile,
  listFiles,
  updatedFile,
];
const GraphAnnotation = Annotation.Root({
  ...MessagesAnnotation.spec,
});
export class Ray {
  private llm: Runnable<
    BaseLanguageModelInput,
    AIMessage,
    ChatOllamaCallOptions
  >;
  private webView: vscode.Webview | null;
  private messages: BaseMessage[] = [
    new SystemMessage(
      "You are Ray, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices."
    ),
  ];
  private workflow: StateGraph<
    {
      messages: BinaryOperatorAggregate<BaseMessage[], Messages>;
    },
    StateType<{
      messages: BinaryOperatorAggregate<BaseMessage[], Messages>;
    }>,
    UpdateType<{
      messages: BinaryOperatorAggregate<BaseMessage[], Messages>;
    }>,
    "__start__" | "agent" | "tools",
    {
      messages: BinaryOperatorAggregate<BaseMessage[], Messages>;
    },
    {
      messages: BinaryOperatorAggregate<BaseMessage[], Messages>;
    },
    StateDefinition
  >;
  private app: CompiledStateGraph<
    StateType<{
      messages: BinaryOperatorAggregate<BaseMessage[], Messages>;
    }>,
    UpdateType<{
      messages: BinaryOperatorAggregate<BaseMessage[], Messages>;
    }>,
    "__start__" | "agent" | "tools",
    {
      messages: BinaryOperatorAggregate<BaseMessage[], Messages>;
    },
    {
      messages: BinaryOperatorAggregate<BaseMessage[], Messages>;
    },
    StateDefinition
  >;
  constructor() {
    if (!RAY_CONFIG.get("AgentServerKey")) {
      vscode.window.showInformationMessage("请填写Agent相关设置,并重启vscode");
      vscode.commands.executeCommand(
        "workbench.action.openSettings",
        "@ext:koujialong.ray-coder"
      );
    }
    this.llm = this.initLLM().bindTools(agentTools);
    this.workflow = this.initWorkFlow();
    this.app = this.workflow.compile();
    this.webView = null;
  }

  private callModel = async (state: typeof GraphAnnotation.State) => {
    const fields = await this.getCurrentWorkspaceData();
    let { messages } = state;
    const response = await this.llm.invoke([...messages, ...fields]);

    // We return a list, because this will get added to the existing list
    return { messages: [response] };
  };

  private shouldContinue = ({ messages }: typeof GraphAnnotation.State) => {
    // console.log("shouldContinue", messages);
    const lastMessage = messages[messages.length - 1] as AIMessage;

    // // // If the LLM makes a tool call, then we route to the "tools" node
    if (lastMessage.tool_calls?.length) {
      return "tools";
    }
    // Otherwise we can just end
    return END;
  };

  private initLLM() {
    const llm = new ChatOpenAI({
      apiKey: RAY_CONFIG.get("AgentServerKey"),
      temperature: 0,
      modelName: RAY_CONFIG.get("AgentModel"), // 可以根据需要选择不同的模型
      // streaming: true,
      configuration: {
        baseURL: RAY_CONFIG.get("AgentServer"),
      },
    });
    return llm;
  }

  private getCurrentWorkspaceData = async (): Promise<SystemMessage[]> => {
    // 获取当前工作区文件夹路径
    const workspaceFolders = vscode.workspace.workspaceFolders;
    if (!workspaceFolders || workspaceFolders.length === 0) {
      console.error("未找到工作区,请打开一个文件夹作为工作区");
      return [];
    }

    // 拼接完整的文件路径
    const dirPath = vscode.Uri.joinPath(workspaceFolders[0].uri);
    try {
      const files = await this.readDirectory(dirPath);
      console.log("files==>", files);
      // 将文件数据添加到状态中,以便后续工具使用
      return [new SystemMessage(`project files:\n${JSON.stringify(files)}`)];
    } catch (error) {
      console.error("获取工作区数据失败:", error);
      return [];
    }
  };

  private async readDirectory(dirPath: vscode.Uri): Promise<any[]> {
    const entries = await vscode.workspace.fs.readDirectory(dirPath);
    const result: any[] = [];

    for (const [name, type] of entries) {
      const uri = vscode.Uri.joinPath(dirPath, name);
      if (type === vscode.FileType.Directory) {
        result.push({ name, type: "directory" });
      } else if (type === vscode.FileType.File) {
        // 如果是文件,添加文件信息
        result.push({ name, type: "file" });
      }
    }

    return result;
  }

  private initWorkFlow() {
    return new StateGraph(GraphAnnotation)
      .addNode("agent", this.callModel)
      .addNode("tools", new ToolNode(agentTools))
      .addEdge(START, "agent") // __start__ is a special name for the entrypoint
      .addEdge("tools", "agent")
      .addConditionalEdges("agent", this.shouldContinue);
  }

  public setWebView(webView: vscode.Webview) {
    this.webView = webView;
  }

  public async ask(info: string) {
    for await (const chunk of await this.app.stream(
      {
        messages: [...this.messages, new HumanMessage(info)],
      },
      {
        streamMode: "values",
      }
    )) {
      console.log("messages", chunk["messages"]);
      this.messages = chunk["messages"];
      this.webView?.postMessage({
        type: "agentResponse",
        value: chunk["messages"],
      });
    }
  }

  clear() {
    this.app = this.workflow.compile();
    this.messages = this.messages.slice(0, 1);
    this.webView?.postMessage({
      type: "agentResponse",
      value: [],
    });
  }
}

总体技术栈为:langchain、langgraph、function calling

最终效果

如何体验

结语

从demo实现可以看出coding agent的无限可能,我时常在想这属实是我革我自己命了😭

相关推荐
取个名字真难呐1 分钟前
GAN随手笔记
人工智能·笔记·生成对抗网络
Carlos_sam2 分钟前
OpenLayers:如何使用渐变色
前端·javascript
就叫飞六吧2 分钟前
git克隆项目报错:error: unable to create file vue...... Filename too long
前端·vue.js·git
光影少年3 分钟前
vue3为什么要用引入Composition api
前端·vue.js
YJlio5 分钟前
AI 的出现是否能替代 IT 从业者?
人工智能
不爱学英文的码字机器9 分钟前
边缘计算的崛起:从云端到设备端的IT新纪元
人工智能·边缘计算
羊思茗52012 分钟前
探索HTML5 Canvas:创造动态与交互性网页内容的强大工具
前端·html·html5
究极无敌暴龙战神X16 分钟前
哈希表 - 两个数组的交集(集合、数组) - JS
前端·javascript·散列表
前端御书房20 分钟前
基于 Trae 的超轻量级前端架构设计与性能优化实践
前端·性能优化