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

相关推荐
灵动的艺术2 小时前
如一 Agent 架构解读(二十四):Agent 高效率低成本思考
人工智能·agent·如一
Xxtaoaooo2 小时前
双11避坑:我用 WorkBuddy + TextIn xParse 做了个「避坑对比台」,商品详情图丢进去,参数矛盾自动现形
人工智能·1024程序员节·textin·workbuddy·ai数据层基础设施
昵称画3 小时前
交接班记录电子化:三班之间的信息不漏
开发语言·数据库·人工智能·低代码·php
Navicat中国3 小时前
将 Navicat 的 AI 助手连接到本地 LLM
人工智能·chatgpt
“AI国潮设计-小江”3 小时前
【AIGC实战】Python+SDXL批量生成“潮汕英歌舞不倒翁”IP | 角色一致性与多场景融合方案,附完整代码
开发语言·人工智能·python·prompt·aigc
点PY3 小时前
WeDetect论文精读
人工智能
沙蒿同学3 小时前
不引 Ghostscript:我用纯 Go 给 PDF 做了一场「压缩手术」
前端·后端·github
CCC:CarCrazeCurator3 小时前
稀疏奖励 vs 稠密奖励:强化学习中的反馈带宽权衡
人工智能·深度学习·机器人
骑着蜗牛撵大象3273 小时前
语音变分录、规则挡幻觉:拆解开源 AI 记账系统 Smart Ledger 的全栈设计
人工智能·ai·架构·js·数据校验
墨染天姬3 小时前
[AI]Cursor Subagent 机制详解
人工智能