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的无限可能,我时常在想这属实是我革我自己命了😭

相关推荐
CELLGENE BIOSCIENCE1 分钟前
精准检测,洞见未来|赛唐生物应邀出席2026张江药谷产业发展闭门交流会,共话药物质量安全新篇章
大数据·人工智能
啊阿狸不会拉杆2 分钟前
《机器学习导论》第 1 章 - 引言
人工智能·python·算法·机器学习·ai·numpy·matplotlib
coldstarry3 分钟前
sheng的学习笔记-AI-adaboost(Adaptive Boosting)
人工智能·笔记·学习
KG_LLM图谱增强大模型3 分钟前
Graph-O1:基于蒙特卡洛树搜索与强化学习的文本属性图推理框架
人工智能·知识图谱
北京青翼科技3 分钟前
高速采集卡丨AD 采集丨 多通道数据采集卡丨高速数据采集系统丨青翼科技FMC 子卡
图像处理·人工智能·fpga开发·信号处理·智能硬件
qq_12498707533 分钟前
基于html的书城阅读器系统的设计与实现(源码+论文+部署+安装)
前端·vue.js·spring boot·后端·mysql·信息可视化·html
轻轻唱5 分钟前
2026专业PPT设计服务商推荐:TOP10深度评测与选择指南
大数据·人工智能·算法
众趣科技7 分钟前
前馈神经网络入门:空间计算的三维重建魔法
人工智能·神经网络·空间计算
张人玉7 分钟前
VisionPro Blob、条码识别、OCR 核心学习笔记
人工智能·机器学习·计算机视觉·vsionpro
ws2019078 分钟前
AI重塑第三空间,AUTO TECH China 2026广州汽车智能座舱展解锁产业升级新密码
人工智能·科技·汽车