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

相关推荐
kyriewen9 小时前
Anthropic 估值逼近万亿美元,Claude Sonnet 5 + Claude Science 一天两连发
前端·ai编程·claude
冬奇Lab9 小时前
Workflow 系列(04):Multi-Agent 协调——编排器边界、并发控制与上下文隔离
人工智能·工作流引擎
冬奇Lab9 小时前
每日一个开源项目(第147篇):HyperGraphRAG - 用超图表示 N 元关系,RAG 的第三代范式
人工智能·开源·graphql
甲维斯10 小时前
Github + 阿里云oss实现类似codex的自动更新!
人工智能
小徐_233310 小时前
Wot UI 2.2.0 发布:Button 新增 subtle,VideoPreview 预览体验继续增强
前端·微信小程序·uni-app
阿里云大数据AI技术11 小时前
光轮智能 × 阿里云:共建 Physical AI 云上数据、评测与持续学习基础设施
人工智能·机器学习
机器之心11 小时前
实锤了:Claude Code偷查用户,时区、中国AI实验室全是关键词
人工智能·openai
网易云信12 小时前
Cursor点燃个人开发者,企业级AI为何频频受挫?Agent工厂从提效工具到AI员工的跃迁
人工智能·开源