手写AI编程助手:从零构建基于Agent与Tool的自动化系统 1. 项目概述为什么我们要手写一个“最小版本”的Cursor最近在AI编程工具圈里Cursor这个名字可以说是如雷贯耳。它凭借深度集成大语言模型LLM的能力将代码补全、解释、重构甚至整个功能的生成都提升到了一个新的自动化水平。很多开发者第一次用上Cursor都会有那种“哇原来编程可以这样”的震撼感。但震撼过后作为一个喜欢刨根问底的技术人我脑子里冒出的第一个念头是这玩意儿到底是怎么工作的它的核心魔法是什么能不能我们自己动手搞出一个最精简的版本来理解其背后的机制这就是“手写Cursor最小版本”这个项目的由来。它不是一个要替代Cursor的商业产品而是一个纯粹的技术探索和教学项目。我们的目标是像拆解一台精密的钟表一样把Cursor这类AI编程助手中最核心的“Agent智能体”与“Tool工具”交互机制剥离出来用最少的代码、最清晰的逻辑实现一个可以理解自然语言指令、调用代码工具、并完成简单编程任务的微型系统。这个项目特别适合以下几类朋友一是对AI应用开发特别是Agent架构感兴趣但被LangChain等框架的复杂性劝退的入门者二是已经用过Cursor、GitHub Copilot想深入理解其原理的中高级开发者三是任何希望将大语言模型能力以结构化、可控制的方式集成到自己产品中的工程师。通过这个项目你将不再把Cursor看作一个黑盒魔法而是能清晰地看到其内部齿轮如何咬合转动。我们会使用Node.js环境因为它对异步操作和快速原型开发非常友好并且会涉及LangChain的核心思想但我们会刻意避免直接使用其重型框架而是从零构建确保每一步你都了然于胸。2. 核心架构设计Agent与Tool的共生关系要理解我们的小型Cursor首先得吃透两个核心概念Agent智能体和Tool工具。你可以把Agent想象成一位经验丰富但“手无寸铁”的软件架构师。他拥有强大的思维能力由大语言模型提供能理解你的需求“帮我创建一个Express服务器”也能规划步骤“先初始化项目再安装依赖然后创建入口文件”但他自己不会敲键盘写代码。这时候Tool就是他的双手。每一位“架构师”身边都围绕着一群专业的“工具人”Tools比如“文件系统工具人”负责读写文件“NPM工具人”负责执行包管理命令“代码执行工具人”能运行一段脚本。2.1 Agent的核心职责思考、规划与调度在我们的最小系统中Agent是整个大脑。它的工作流是一个经典的“感知-思考-行动”循环感知接收用户的自然语言指令例如“在./src目录下创建一个名为app.js的文件内容是一个简单的HTTP服务器。”思考大语言模型LLM分析这条指令。它会将模糊的需求分解成具体的、可执行的操作序列。这个思考过程的关键输出是一个结构化决策下一步该调用哪个Tool调用时应该传入什么参数行动根据思考结果调用对应的Tool并传入精确的参数。观察获取Tool执行后的结果成功或失败包括输出信息。循环基于观察到的结果再次进行“思考”决定下一步行动直到任务被判定为完成或无法继续。这个循环的难点在于如何让LLM的“思考”结果能稳定地、结构化地驱动我们的程序。我们不能指望LLM每次都会输出“请调用createFileTool参数为{path: ‘./src/app.js’ content: ‘...’}”这样完美的JSON。因此我们需要设计一个“交互协议”。2.2 Tool的设计哲学单一职责与标准化接口Tool的设计必须遵循“单一职责原则”。一个Tool只做一件事并且把它做好。在我们的最小版本里我们可能只需要三个核心Tool文件操作Tool创建、读取、写入、删除文件。Shell命令执行Tool执行如npm init -ynode -v这样的系统命令。代码片段解释Tool对某段代码进行总结、解释或提出修改建议这是Cursor的亮点功能之一。每个Tool都必须提供标准化的描述和接口。描述是给AgentLLM看的需要清晰说明这个Tool是干什么的、接受什么参数。例如文件写入Tool的描述可能是“writeFile将内容写入指定路径的文件。参数path字符串文件路径content字符串文件内容。” 接口是给我们的程序调用的就是一个普通的JavaScript函数。2.3 为什么不用现成的LangChain你可能会问LangChain不就是专门干这个的吗为什么还要手写没错LangChain提供了一整套强大的Agent和Tool抽象。但正因其强大和全面它也带来了较高的学习成本和抽象层次。对于学习原理而言它就像直接给了你一辆组装好的汽车而你却想看看发动机和变速箱是怎么连接的。我们的“手写”过程就是从制造螺丝和齿轮开始理解整个传动系统。这能让你在未来即使使用LangChain、LangGraph或AutoGen这类框架时也能清楚地知道底层在发生什么从而能更灵活地调试和定制。3. 手把手实现从零搭建最小化Agent系统理论说得再多不如一行代码。让我们开始动手搭建。请确保你已安装Node.js建议版本18以上和npm。3.1 项目初始化与核心依赖安装首先创建一个新目录并初始化项目mkdir mini-cursor-agent cd mini-cursor-agent npm init -y接着安装我们最核心的依赖用于与大语言模型API通信的库。这里我们选择OpenAI的官方Node.js SDK因为它最通用。你当然也可以替换成其他兼容OpenAI API格式的模型服务如DeepSeek、Ollama本地模型等。npm install openai同时我们还需要dotenv来管理API密钥等敏感信息npm install dotenv在项目根目录创建.env文件填入你的OpenAI API密钥OPENAI_API_KEYsk-your-actual-api-key-here3.2 构建基础Tool类与具体工具实现我们先定义一个基础的Tool类所有具体工具都继承自它。这个类主要定义了工具的描述和统一的调用方法。src/core/Tool.jsclass Tool { constructor(name, description, parameters) { this.name name; this.description description; // 给LLM看的描述 this.parameters parameters; // 参数定义例如 [{name: ‘path’ type: ‘string’}] } // 实际执行工具功能的函数由子类实现 async _call(argumentsObject) { throw new Error(‘_call() must be implemented by subclass’); } // 提供给Agent调用的安全接口 async call(argumentsObject) { try { const result await this._call(argumentsObject); return { success: true output: result }; } catch (error) { return { success: false output: Error: ${error.message} }; } } // 生成给LLM的工具描述片段 toLLMDescriptor() { return { name: this.name, description: this.description, parameters: this.parameters, }; } } module.exports Tool;现在让我们实现第一个具体的工具文件写入工具。src/tools/WriteFileTool.jsconst Tool require(‘../core/Tool’); const fs require(‘fs’).promises; const path require(‘path’); class WriteFileTool extends Tool { constructor() { super( ‘write_file’, ‘Write content to a file at the specified path. Creates directories if needed.’, [ { name: ‘path’ description: ‘The file path’ type: ‘string’ required: true }, { name: ‘content’ description: ‘The content to write’ type: ‘string’ required: true }, ] ); } async _call({ path: filePath content }) { // 确保目录存在 const dir path.dirname(filePath); await fs.mkdir(dir { recursive: true }); // 写入文件 await fs.writeFile(filePath content ‘utf-8’); return File written successfully to ${filePath}; } } module.exports WriteFileTool;按照同样的模式我们可以快速实现一个执行Shell命令的工具。这里我们需要特别注意安全性避免执行任意危险命令。在我们的最小版本中我们可以做一个简单的允许命令列表或者仅用于项目相关的安全命令如npm git等。src/tools/ShellCommandTool.js(简化安全版)const Tool require(‘../core/Tool’); const { exec } require(‘child_process’); const { promisify } require(‘util’); const execAsync promisify(exec); class ShellCommandTool extends Tool { constructor(allowedCommands [‘npm’ ‘node’ ‘ls’ ‘pwd’ ‘mkdir’ ‘echo’]) { super( ‘shell_command’, ‘Execute a safe shell command. Currently allowed prefixes: ‘ allowedCommands.join(‘ ‘), [ { name: ‘command’ description: ‘The shell command to execute’ type: ‘string’ required: true }, ] ); this.allowedCommands allowedCommands; } async _call({ command }) { // 基础的安全检查命令是否以允许的前缀开头 const isAllowed this.allowedCommands.some(cmd command.trim().startsWith(cmd)); if (!isAllowed) { throw new Error(Command not allowed. Allowed prefixes: ${this.allowedCommands.join(‘ ‘)}); } const { stdout stderr } await execAsync(command { cwd: process.cwd() }); if (stderr) { // 有些命令如npm install会输出信息到stderr但不一定是错误 console.warn(‘Shell command stderr:’ stderr); } return stdout || ‘Command executed (no stdout)’; } } module.exports ShellCommandTool;3.3 实现Agent大脑与LLM的思维链交互这是最核心的部分。我们需要设计一个Agent类它持有可用的工具列表并能与LLM对话将LLM的文本输出解析成工具调用指令。src/core/Agent.js(核心骨架)const OpenAI require(‘openai’); require(‘dotenv’).config(); class Agent { constructor(tools []) { this.openai new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); this.tools tools; this.conversationHistory []; // 保存对话历史用于保持上下文 } // 关键方法让Agent根据当前对话历史和用户新消息决定下一步行动 async think(userMessage) { this.conversationHistory.push({ role: ‘user’ content: userMessage }); // 1. 准备给LLM的系统提示词System Prompt这是指导LLM行为的关键 const systemPrompt You are a helpful programming assistant. You have access to the following tools: ${this.tools.map(t - ${t.name}: ${t.description}).join(‘\n’)} To use a tool, you MUST respond in the following JSON format: { “thought”: “Your reasoning about what to do next”, “action”: { “name”: “tool_name”, “arguments”: { “arg1”: “value1”, “arg2”: “value2” } } } If the task is complete or no tool is needed, respond with: { “thought”: “Final summary or answer”, “action”: null } Current working directory: ${process.cwd()} ; // 2. 构建消息列表 const messages [ { role: ‘system’ content: systemPrompt }, …this.conversationHistory, ]; // 3. 调用LLM const completion await this.openai.chat.completions.create({ model: ‘gpt-4o-mini’ // 或 ‘gpt-3.5-turbo’ 根据成本和性能选择 messages, temperature: 0.1, // 低温度让输出更确定、更遵循格式 response_format: { type: “json_object” }, // 强制要求返回JSON这是关键 }); const llmResponse completion.choices[0].message.content; this.conversationHistory.push({ role: ‘assistant’ content: llmResponse }); // 4. 解析LLM的响应 let actionDecision; try { actionDecision JSON.parse(llmResponse); } catch (error) { console.error(‘Failed to parse LLM response as JSON:’ llmResponse); // 如果解析失败可以尝试让LLM重试或返回一个错误结果 return { thought: ‘I received an invalid response format.’, action: null, result: null, }; } return actionDecision; // 返回 { thought action } } // 执行Agent决策出的行动 async act(actionDecision) { if (!actionDecision.action) { // 没有行动直接返回思考结果作为最终回复 return { result: actionDecision.thought }; } const { name: toolName arguments: toolArgs } actionDecision.action; const tool this.tools.find(t t.name toolName); if (!tool) { return { result: Error: Tool ‘${toolName}’ not found. }; } // 调用工具 const toolResult await tool.call(toolArgs); // 将工具执行结果也加入历史供下一轮思考参考 this.conversationHistory.push({ role: ‘tool’ content: JSON.stringify({ tool: toolName result: toolResult }) }); return { result: toolResult }; } // 主循环接收用户输入思考行动直到任务完成 async run(userInput) { console.log(User: ${userInput}); let shouldContinue true; let finalAnswer null; while (shouldContinue) { const decision await this.think(userInput); console.log(Agent Thought: ${decision.thought}); if (decision.action) { console.log(Agent Action: ${decision.action.name}, decision.action.arguments); const actionResult await this.act(decision); console.log(Tool Result:, actionResult.result); // 如果工具执行成功将本轮的用户输入置空或一个固定提示让Agent基于工具结果进行下一轮思考。 // 如果失败或任务完成则跳出循环。 if (actionResult.result.success false) { finalAnswer Task failed: ${actionResult.result.output}; shouldContinue false; } else { // 继续循环下一轮“思考”的输入是上一步的工具结果摘要 userInput The previous action ‘${decision.action.name}’ completed with result: ${actionResult.result.output}. What should I do next based on the original goal?; } } else { // 没有下一步行动任务完成 finalAnswer decision.thought; shouldContinue false; } } return finalAnswer; } } module.exports Agent;这段代码是核心中的核心。有几个关键点系统提示词System Prompt它定义了Agent的角色、可用的工具以及强制性的JSON输出格式。这个格式是我们与LLM约定的“协议”是让非结构化的文本对话变成结构化程序指令的桥梁。response_format: { type: “json_object” }这是OpenAI API的一个强大功能它极大地提高了LLM返回规整JSON的概率是我们项目能跑通的关键。对话历史管理我们将用户消息、Assistant的思考、Tool的执行结果都存入conversationHistory。这样LLM在每一轮思考时都能拥有完整的上下文知道之前做了什么、结果如何从而做出连贯的决策。主循环run方法它实现了经典的Agent循环用户输入 - 思考(LLM) - 执行(Tool) - 观察结果 - 再次思考…直到LLM认为任务完成返回action: null。3.4 组装并运行创建你的第一个AI编程助手现在让我们把零件组装起来看看它能否真正工作。src/index.jsconst Agent require(‘./core/Agent’); const WriteFileTool require(‘./tools/WriteFileTool’); const ShellCommandTool require(‘./tools/ShellCommandTool’); async function main() { // 1. 初始化工具 const tools [ new WriteFileTool(), new ShellCommandTool(), // 未来可以轻松添加更多工具如 ReadFileTool GitTool等 ]; // 2. 创建Agent注入工具 const myAssistant new Agent(tools); // 3. 发布第一个任务 const task “Initialize a new Node.js project in the current directory and create a simple ‘hello.js’ file that prints ‘Hello from Mini-Cursor!’.”; console.log(‘Starting task:’ task); const finalResult await myAssistant.run(task); console.log(‘\n Task Finished ’); console.log(‘Final Result:’ finalResult); } main().catch(console.error);运行这个程序node src/index.js如果一切配置正确你将看到类似以下的输出流Starting task: Initialize a new Node.js project... User: Initialize a new Node.js project... Agent Thought: I need to first run ‘npm init -y’ to create a package.json, then create the hello.js file. Agent Action: shell_command { command: ‘npm init -y’ } Tool Result: { success: true output: ‘… package.json created …’ } User: The previous action ‘shell_command’ completed with result: … What should I do next based on the original goal? Agent Thought: Now I need to create the hello.js file with the specified content. Agent Action: write_file { path: ‘./hello.js’ content: “console.log(‘Hello from Mini-Cursor!’);” } Tool Result: { success: true output: ‘File written successfully to ./hello.js’ } User: The previous action ‘write_file’ completed with result: … What should I do next? Agent Thought: Both steps are complete. The task is finished. Task Finished Final Result: Both steps are complete. The task is finished.检查你的目录会发现新生成了package.json和hello.js文件。运行node hello.js你会看到打印出的问候语。恭喜你已经成功创建了一个具备基础“思考-行动”能力的AI编程助手雏形。4. 深入优化与实战技巧上面的代码跑通了核心流程但距离一个健壮的、可用的系统还有距离。下面分享几个关键的优化点和实战中踩过的坑。4.1 提升LLM决策的稳定性与准确性LLM的输出具有随机性即使我们要求返回JSON它有时也会“胡言乱语”或格式错误。除了使用response_format参数我们还可以在代码层面增加“重试”和“后处理”逻辑。结构化参数验证在Agent.act()方法中调用工具前严格验证参数是否存在、类型是否正确。可以集成像zod这样的验证库。思维链Chain-of-Thought鼓励在系统提示词中明确要求LLM先进行推理“thought”字段再决定行动。这能显著提高决策质量。我们的提示词已经包含了这一点。错误处理与重试当LLM返回的JSON无法解析或指定的工具不存在时不要直接崩溃。可以将错误信息反馈给LLM让它重新思考。这需要在run循环中增加一个错误处理分支将错误信息作为新一轮的用户输入。4.2 工具设计的进阶考量工具结果的处理工具返回的结果可能很长如npm install的输出。直接塞回给LLM可能会浪费tokens并干扰其思考。一个好的做法是让工具返回一个摘要。例如ShellCommandTool可以在成功时返回“Command ‘npm init’ executed successfully.”失败时返回简洁的错误信息。工具的组合与规划复杂的任务如“搭建一个Express服务器”需要多个工具按顺序调用。目前我们的Agent能通过循环自动处理。但对于更复杂的、有分支条件的任务可能需要引入更高级的规划能力这就可以借鉴LangGraph中“状态机”的概念。安全性加固我们的ShellCommandTool的白名单机制非常初级。在生产环境中需要更严格的沙箱机制比如在Docker容器内执行命令或使用更精细的权限控制。4.3 扩展你的工具库Cursor的强大在于丰富的工具集。你可以轻松地为你的迷你Agent添加新工具代码解释工具接受一个文件路径读取文件内容然后调用LLM的API使用另一个专门的提示词来总结或解释代码。代码重构工具接受代码片段和重构指令如“提取函数”调用LLM生成新代码然后通过WriteFileTool写回。Git操作工具封装git addgit commitgit status等命令。网络搜索工具集成Serper API或类似服务让Agent能获取最新信息来解决问题。添加新工具的过程完全标准化继承Tool基类实现_call方法定义好描述和参数即可。然后将其加入到传递给Agent的工具列表中。这就是模块化的魅力。4.4 性能与成本控制上下文长度管理conversationHistory会不断增长导致每次调用API的tokens消耗增加成本上升并且可能超过模型的最大上下文长度。需要实现一个“滑动窗口”或“摘要”机制只保留最近N轮对话或对历史进行总结压缩。模型选择对于简单的代码生成和工具调用gpt-4o-mini或gpt-3.5-turbo通常足够且成本更低。对于复杂的逻辑规划可以考虑使用gpt-4o。异步与流式处理如果工具调用比较耗时如下载依赖可以考虑让多个工具并行执行如果它们之间没有依赖关系。对于长时间运行的任务可以向用户提供流式进度反馈。5. 常见问题与排查实录在开发和测试这个最小系统的过程中我遇到了不少典型问题这里记录下排查思路和解决方案。问题1LLM不返回JSON或者返回的JSON格式错误。现象JSON.parse抛出异常程序中断。排查首先检查系统提示词是否清晰强调了JSON格式。然后打印出LLM返回的原始内容llmResponse看看它到底说了什么。有时候LLM会在JSON前后加上“json”这样的markdown标记。解决在代码中增加预处理尝试去除llmResponse中的markdown代码块标记。使用更严格的提示词例如“你必须且只能返回一个有效的JSON对象不要有任何其他文字。”启用API的response_format: { type: “json_object” }参数我们已采用这是最有效的解决方案。实现重试逻辑捕获解析异常后将错误信息连同原始对话历史再次发送给LLM要求它纠正。问题2Agent陷入死循环或者重复执行同一个操作。现象控制台不断打印相似的思考和行动任务无法完成。排查观察conversationHistory。问题通常出在工具执行结果的反馈上。如果工具结果过于冗长或模糊LLM可能无法正确判断任务状态。也可能是系统提示词中关于“任务完成”的条件描述不清。解决优化工具反馈确保工具返回清晰、简短、确定性的结果。例如“File created successfully”比一长串文件内容更好。增强提示词在系统提示词中明确给出任务完成的例子。例如“当你认为用户请求的所有步骤都已正确执行完毕时将action设置为null并在thought中总结完成情况。”设置循环上限在Agent.run方法中设置一个最大循环次数比如10次超过后强制终止避免无限消耗API费用。问题3Shell命令执行失败但错误信息不清晰。现象ShellCommandTool返回success: false但output只是简单的“Error: Command failed”。排查child_process.exec的错误对象通常包含stderr信息。解决修改ShellCommandTool._call中的错误处理将stderr信息包含在返回的错误结果中方便Agent和开发者诊断。try { const { stdout stderr } await execAsync(command { cwd: process.cwd() }); // … 处理成功情况 } catch (error) { // 将stderr和error.message都返回 return { success: false output: Command failed: ${error.message}. Stderr: ${error.stderr || ‘None’}, }; }问题4如何处理用户模糊或复杂的指令现象用户说“优化这个文件”Agent可能不知所措因为它不知道“优化”具体指什么也不知道是哪个文件。解决这是当前AI助手的通用挑战。在我们的架构下有两种应对策略让Agent学会追问在系统提示词中赋予Agent在信息不足时主动询问用户的能力。例如可以设计一个特殊的ask_user工具当LLM认为需要澄清时就调用这个工具将问题输出给用户并等待用户下一轮输入。设计更精准的工具将“优化”拆解成多个具体工具如format_code_tool格式化、refactor_code_tool重构、add_comments_tool加注释。然后由LLM根据上下文决定调用哪一个或哪几个。通过这个从零手写最小版本Cursor Agent的项目我们不仅实现了一个能跑通的自动化编程助手原型更重要的是我们彻底拆解了AI Agent的核心运作机制如何让大语言模型从“聊天者”转变为“执行者”。这个过程中对工具抽象、提示词工程、交互协议和安全性的思考是任何深入AI应用开发的开发者都必须掌握的基石。你可以以此为基础添加更强大的工具如数据库操作、API调用、集成更复杂的规划逻辑如LangGraph、甚至为其开发一个前端聊天界面逐步构建出属于你自己的、高度定制化的AI生产力工具。