
1. 项目概述为什么我们要亲手打造一个AI编程助手最近几个月AI编程工具的热度居高不下。无论是Trae还是Cursor它们展现出的代码理解、自动补全甚至自主规划任务的能力让很多开发者感到既兴奋又焦虑。兴奋的是生产力工具即将迎来一次范式转移焦虑的是如果只是停留在“使用”层面我们很可能沦为这些工具的操作员而无法理解其背后的运作机制更谈不上定制和优化。这正是我决定动手从零开始复刻一个AI编程Agent的核心动机。这个项目不是要做一个商业级的、功能完备的替代品而是一个教学级、可拆解、可扩展的“骨架”。通过亲手实现我们能彻底搞明白几个关键问题一个AI Agent是如何理解自然语言需求的它如何拆解复杂的编程任务又是如何调用工具比如文件系统、终端、代码解释器来执行并验证结果的理解了这些你不仅能更高效地使用现成的AI编程工具还能根据自己团队的独特工作流定制专属的智能助手。这个项目适合有一定Node.js基础并对AI应用开发感兴趣的开发者。即使你对LangChain、Agent这些概念感到陌生也没关系我们会从最基础的原理讲起用代码把每一个抽象概念具象化。最终你将获得一个能够运行在你本地、理解你简单指令比如“在src/utils目录下创建一个格式化日期的函数”并执行的小型AI编程助手。更重要的是你将掌握构建更复杂Agent系统的核心方法论。2. 核心架构设计拆解Trae/Cursor的“大脑”与“四肢”要复刻核心能力我们首先要解构目标。像Trae或Cursor这样的AI编程助手其核心可以抽象为一个基于大语言模型LLM的智能体Agent系统。这个系统通常由几个关键部分组成一个负责思考和决策的“大脑”LLM一套可供调用的“工具”Tools一个管理任务状态的“工作记忆”Memory以及一个协调任务流的“调度器”Orchestrator。2.1 大脑的选择与连接LLM API集成“大脑”是整个Agent的智慧源泉。我们不会从头训练一个大模型而是通过API调用现有的强大模型如OpenAI的GPT-4、Anthropic的Claude或者国内可便捷访问的DeepSeek、通义千问等。这里的选择至关重要它直接决定了Agent的理解能力、推理深度和代码生成质量。对于本项目考虑到易用性和性能我们选择使用OpenAI的GPT-3.5-turbo或GPT-4作为起点。你需要准备一个有效的API Key。连接方式很简单我们使用openai这个官方NPM包。import OpenAI from openai; const openai new OpenAI({ apiKey: process.env.OPENAI_API_KEY, // 永远不要将API Key硬编码在代码中 }); async function getLLMResponse(prompt) { const completion await openai.chat.completions.create({ model: gpt-3.5-turbo, messages: [{ role: user, content: prompt }], temperature: 0.1, // 对于代码生成低温度值如0.1-0.3可以使输出更确定、更可靠。 }); return completion.choices[0].message.content; }注意模型的选择是第一个需要权衡的点。GPT-4在复杂逻辑推理和长上下文理解上远超GPT-3.5但成本也高出一个数量级。对于学习和原型开发GPT-3.5-turbo是完全足够的。务必通过环境变量管理你的API Key这是开发安全的基本要求。2.2 工具的抽象与实现赋予Agent“动手”能力一个只会思考不会行动的Agent是没用的。编程Agent的核心工具通常包括文件系统工具读取、创建、修改、删除文件。终端/命令执行工具运行Shell命令例如npm install、git commit等。代码解释器在一个安全的沙箱环境中执行代码片段并获取结果用于验证逻辑。我们需要将这些能力封装成标准的“Tool”接口供LLM调用。LangChain等框架对此有优秀抽象但为了理解本质我们先自己实现一个最简单的工具调用协议。一个工具可以定义为一个包含name、description和execute函数的对象。LLM根据描述决定何时调用哪个工具。// 示例一个简单的文件读取工具 const readFileTool { name: “read_file”, description: “读取指定路径文件的内容。输入应为文件的绝对路径或相对于项目根目录的路径。”, execute: async (filePath) { try { const content await fs.promises.readFile(filePath, ‘utf-8’); return 文件 ${filePath} 的内容如下\n\\\\n${content}\n\\\; } catch (error) { return 读取文件失败${error.message}; } } }; // 示例一个命令执行工具需极其谨慎 const runCommandTool { name: “run_command”, description: “在项目根目录下执行一个安全的Shell命令如列出文件、运行测试。禁止执行危险命令如 rm -rf, curl | bash 等。输入应为命令字符串。”, execute: async (command) { // 强烈建议在这里加入命令白名单或危险命令过滤逻辑 if (isDangerousCommand(command)) { return “拒绝执行潜在危险命令。”; } const { exec } require(‘child_process’); return new Promise((resolve) { exec(command, { cwd: process.cwd() }, (error, stdout, stderr) { if (error) { resolve(命令执行出错${stderr}); } else { resolve(命令输出\n${stdout}); } }); }); } };实操心得工具的安全性设计是重中之重。尤其是run_command工具必须实施严格的沙箱或白名单机制。在原型阶段可以暂时只允许ls、find、cat、npm run test等无害命令或者直接注释掉这个工具专注于文件操作。一个失控的Shell工具可能瞬间摧毁你的项目。2.3 任务规划与执行循环ReAct模式实战有了大脑和工具如何让它们协同工作这里我们引入一个经典范式ReAct (Reasoning Acting)。在这个模式中Agent的思考过程被显式化它先进行一步推理Reason决定下一步要做什么比如调用某个工具然后执行行动Act观察结果Observation并基于结果进行下一步推理如此循环直至任务完成或达到步骤限制。这个循环是Agent自主性的核心。我们需要构建一个“主循环”函数初始化将用户需求如“添加一个登录功能”作为初始目标。推理将当前目标、已执行步骤的历史和工具描述组合成Prompt发送给LLM要求它输出下一步的“思考”和“行动”例如“我需要先查看项目结构。我将使用read_file工具查看package.json。”。解析与行动从LLM的回复中解析出要调用的工具名称和输入参数然后执行对应的工具。观察获取工具执行的结果。更新状态将“思考-行动-观察”这一步添加到历史记录中。判断终止LLM在推理后可能认为任务已完成输出最终答案。或者我们设置一个最大循环次数以防无限循环。循环如果未终止将更新后的历史再次送入步骤2。async function runAgent(initialGoal, tools, maxSteps 10) { let stepHistory 目标${initialGoal}\n; for (let step 0; step maxSteps; step) { // 构建Prompt包含工具描述和历史 const prompt buildPrompt(initialGoal, stepHistory, tools); const llmResponse await getLLMResponse(prompt); // 解析LLM响应这里需要设计一个稳定的解析格式例如使用JSON或特定分隔符 const { thought, action, actionInput } parseLlmResponse(llmResponse); stepHistory \n思考${thought}\n行动${action} 输入${actionInput}\n; if (action.toLowerCase() ‘final_answer’) { console.log(‘任务完成’); return actionInput; // 最终答案 } const tool tools.find(t t.name action); if (!tool) { stepHistory 观察错误 - 未知工具 \${action}\\n; continue; } const observation await tool.execute(actionInput); stepHistory 观察${observation}\n; } return ‘达到最大步骤限制任务未完成。’; }注意事项Prompt工程是Agent稳定性的关键。你需要精心设计给LLM的Prompt明确指示其输出格式例如“请按以下格式回复\n思考...\n行动工具名\n输入工具参数”并清晰描述每个工具的能力和限制。不稳定的输出格式是Agent开发初期最常见的调试难点。3. 核心模块深度实现与集成理解了宏观架构我们开始深入实现各个核心模块并将它们集成起来形成一个可运行的最小可行产品MVP。3.1 构建稳健的提示词Prompt工程系统Prompt是驱动LLM的“燃料”。一个编程Agent的Prompt通常包含以下几个部分系统角色设定定义Agent的身份和能力。工具描述以结构化方式列出所有可用工具的名称、描述和参数示例。行动格式指令严格要求LLM以特定格式如JSON、Markdown代码块返回“思考”和“行动”。任务历史包含之前的步骤提供上下文。当前目标用户本次提出的需求。function buildPrompt(goal, history, tools) { const toolDescriptions tools.map(t - ${t.name}: ${t.description}).join(‘\n’); return 你是一个专业的AI编程助手。你的目标是通过一系列步骤完成用户的编程任务。 你可以使用以下工具 ${toolDescriptions} 你必须严格按照以下格式回应 \\\ 思考[你下一步的推理过程] 行动[工具名称如果任务完成则写“FINAL_ANSWER”] 输入[工具的输入参数如果行动是FINAL_ANSWER则这里写最终回复] \\\ 以下是到目前为止的任务历史 ${history} 当前目标${goal} 请开始你的下一步 ; }实操心得格式越严格解析越简单。让LLM输出JSON是最理想的方式例如{“thought”: “…”, “action”: “…”, “actionInput”: “…”}。这能极大简化后续的解析逻辑提高Agent的稳定性。你可以要求LLM“只输出一个合法的JSON对象”并在Prompt中给出示例。3.2 实现带状态的会话与工作记忆简单的单次任务Agent功能有限。一个实用的编程助手需要记住之前的对话上下文理解“之前你创建了那个文件现在我想修改它”这样的指代。这就需要引入**记忆Memory**模块。我们可以实现一个简单的对话缓冲区记忆保存最近N轮的用户-Agent交互历史。class ConversationBufferMemory { constructor(maxMessages 20) { this.messages []; // 格式 { role: ‘user’|‘assistant’, content: string } this.maxMessages maxMessages; } addUserMessage(content) { this.messages.push({ role: ‘user’, content }); this._trim(); } addAssistantMessage(content) { this.messages.push({ role: ‘assistant’, content }); this._trim(); } getHistory() { // 将历史消息格式化为字符串用于构建Prompt return this.messages.map(m ${m.role}: ${m.content}).join(‘\n’); } _trim() { if (this.messages.length this.maxMessages) { // 简单策略保留最新的消息可以更复杂如基于重要性修剪 this.messages this.messages.slice(-this.maxMessages); } } }在主循环中我们不再只传递步骤历史而是将整个对话历史包含用户的多次提问和Agent的多次回复都纳入Prompt。这样LLM就能拥有连续的上下文记忆。3.3 集成代码解释器与安全沙箱代码解释器是编程Agent的“试炼场”。它允许Agent生成一段代码如一个函数逻辑立即执行并看到结果从而验证其正确性或进行调试。在服务器端直接执行未知代码是极度危险的我们必须使用沙箱。对于Node.js环境一个相对安全的选择是使用vm2或isolated-vm这类沙箱模块。它们可以创建一个隔离的JavaScript运行环境限制访问权限。const { VM } require(‘vm2’); const codeInterpreterTool { name: “execute_javascript”, description: “在一个安全的沙箱中执行JavaScript代码片段。输入应为纯代码字符串。可用于验证算法逻辑、计算表达式等。无法访问文件系统或网络。”, execute: async (codeSnippet) { try { const vm new VM({ timeout: 1000, // 超时1秒防止死循环 sandbox: {}, // 空的沙箱对象不提供任何外部API }); const result vm.run(codeSnippet); return 执行成功结果${JSON.stringify(result)}; } catch (error) { return 执行失败${error.message}; } } };警告即使使用vm2复杂的JavaScript代码仍有可能找到逃逸沙箱的方法。因此永远不要在生产环境中无条件信任并执行来自LLM的任意代码。这个工具应仅限于在受控的开发环境中用于执行简单的、无副作用的计算逻辑验证。4. 从原型到实用优化策略与高级特性完成基础MVP后我们的Agent已经可以处理一些简单指令了。但要让它更实用、更强大我们需要引入更多优化和高级特性。4.1 引入分层任务分解与规划复杂任务如“为我的React项目添加用户认证”无法一步完成。高级Agent如Trae/Cursor其核心能力之一是任务分解。我们可以让LLM在开始行动前先制定一个高层计划。实现思路是设计一个“规划器Planner”模块。当接收到复杂任务时先调用一次LLM要求它将任务分解为3-5个清晰的、有序的子任务。然后Agent再逐个执行这些子任务。async function planTasks(ultimateGoal) { const planningPrompt 请将以下复杂的编程任务分解为一个有序的子任务列表。每个子任务应该是一个具体、可执行的动作。 任务“${ultimateGoal}” 请以JSON数组格式输出例如[子任务1描述, 子任务2描述, ...] 输出 ; const planJson await getLLMResponse(planningPrompt); try { return JSON.parse(planJson); } catch (e) { // 如果解析失败退回单步执行模式 return [ultimateGoal]; } } // 在主函数中 const subTasks await planTasks(userRequest); for (const subTask of subTasks) { await runAgent(subTask, tools, memory); }4.2 实现文件系统的智能感知与操作基础的文件读写工具是“盲”的它需要明确的路径。一个更智能的Agent应该具备一定的项目结构感知能力。我们可以创建一个增强版的“项目上下文工具”。这个工具可以列出目录让Agent知道当前项目有哪些文件和文件夹。搜索文件根据关键词如“component”、“auth”查找相关文件。智能路径补全当用户说“在组件目录下创建按钮”Agent能结合记忆和当前目录结构推断出完整路径如./src/components/Button.jsx。这需要我们在记忆或状态中维护一份当前工作区的简单索引并在Prompt中动态提供相关文件列表的摘要。4.3 错误处理与自我修正循环Agent在执行中难免出错工具调用失败、生成的代码有语法错误、路径不存在等。一个鲁棒的Agent应该具备自我修正能力。实现自我修正的关键在于将错误信息清晰地反馈给LLM并引导它分析原因、调整策略。我们可以在主循环中增加一个错误处理分支const observation await tool.execute(actionInput); if (observation.startsWith(‘错误’) || observation.startsWith(‘失败’)) { // 将错误观察加入历史并添加一条系统提示要求LLM分析错误并重试 stepHistory 观察${observation}\n系统上一步操作出错了。请分析错误原因调整你的计划或行动然后重试。\n; } else { stepHistory 观察${observation}\n; }通过这种方式Agent可以学会“吃一堑长一智”。例如如果因为文件不存在而读取失败下一次它可能会先检查文件是否存在或者先创建该文件。5. 实战演练构建一个自动创建React组件的Agent让我们将以上所有模块组合起来完成一个具体的实战场景创建一个能根据描述自动生成React功能组件的Agent。目标用户输入“创建一个显示当前时间的时钟组件要有数字格式和暂停/开始按钮”Agent能自动在正确的目录下创建Clock.jsx和Clock.css文件并写入合理的初始代码。步骤分解初始化项目确保我们的Agent工具集包含read_file读package.json确定是React项目、list_directory查看src结构、write_file创建文件和execute_javascript可选用于验证简单逻辑。接收指令用户提出创建时钟组件的需求。任务规划Planner将任务分解为子任务1检查项目结构确定组件存放目录通常是src/components。子任务2设计组件接口Props和状态State。子任务3编写Clock.jsx的React组件代码。子任务4编写Clock.css的样式代码。子任务5在App.js中引入并预览可选更高级。循环执行Agent携带“检查项目结构”的子任务进入ReAct循环。思考“我需要知道项目结构特别是组件目录在哪里。我先查看根目录。”行动list_directory输入./观察看到src,package.json,node_modules等。思考“现在查看src目录内部。”行动list_directory输入./src观察看到components,App.js,index.js。确认组件目录存在。思考“组件目录已找到。现在需要设计时钟组件的逻辑。我将使用代码解释器先构思一下时间逻辑。”行动execute_javascript输入一段测试setInterval和日期格式化的代码片段。观察代码执行成功输出符合预期。思考“逻辑验证通过。现在开始编写组件文件。”行动write_file输入路径./src/components/Clock.jsx内容为生成的JSX代码。观察文件写入成功。行动write_file输入路径./src/components/Clock.css内容为基本样式。观察文件写入成功。思考“两个核心文件已创建。任务完成。”行动FINAL_ANSWER输入“已成功在./src/components/目录下创建Clock.jsx和Clock.css文件。组件包含数字时钟显示和暂停/开始按钮的基本逻辑与样式。”通过这个流程你可以清晰地看到Agent如何将模糊的自然语言指令通过思考、规划、使用工具一步步转化为具体的、可执行的文件操作。虽然这个例子相对简单但它完整地展示了AI编程Agent的核心工作流。6. 常见问题、调试技巧与性能优化在开发过程中你一定会遇到各种问题。以下是一些常见坑点及其解决方案。6.1 Agent陷入循环或执行无关动作这是最常见的问题通常由以下原因导致Prompt指令不清晰LLM没有完全理解它必须遵循严格的格式或者工具描述不够精确。解决方案强化格式指令在Prompt开头和结尾都强调格式要求。为每个工具提供更具体、无歧义的描述和使用示例。工具能力不足或结果模糊当工具返回的结果Observation过于冗长或模糊时LLM无法提取有效信息进行下一步推理。解决方案工具返回的结果应简洁、结构化。例如list_directory工具不应返回原始ls -la的输出而应返回一个清理过的文件列表字符串。缺乏终止条件LLM可能不知道何时该说“任务完成”。解决方案在Prompt中明确定义完成标准例如“当你认为已经满足了用户的初始目标时请使用FINAL_ANSWER行动”。6.2 解析LLM响应失败LLM并不总是乖乖输出你想要的格式可能会添加额外解释。解决方案采用更鲁棒的解析策略。不要只依赖字符串匹配或正则表达式。可以在Prompt中要求LLM将JSON输出在Markdown代码块中如json … 然后提取代码块内容。使用“流式”或“函数调用”特性如果API支持。OpenAI的Chat Completions API提供了function calling现称tool calls功能能强制模型以结构化格式返回工具调用请求这是最稳定、最推荐的方式。在解析失败时将错误信息连同原始响应一起反馈给LLM要求它重试并纠正格式。6.3 成本控制与响应速度频繁调用LLM API尤其是GPT-4成本会快速上升响应也慢。优化策略缓存对相同的Prompt进行缓存避免重复计算。总结历史在对话轮次增多后将漫长的历史消息总结成一段简短的摘要再放入Prompt而不是全部传递。这能有效减少Token消耗。使用更小/更快的模型对于简单的工具选择、格式校验等步骤可以尝试使用更便宜、更快的模型如GPT-3.5-turbo-instruct。设置超时和重试对API调用设置合理的超时并实现指数退避重试机制处理网络波动。6.4 安全性加固如前所述安全性是生命线。工具层面对run_command实施命令白名单。对write_file限制可写入的目录范围禁止写入系统目录、.git目录等。沙箱层面确保代码执行环境被严格隔离无网络、无文件系统访问权限。输入层面对所有来自LLM的、即将作为参数传递给工具的内容进行校验和清理防止注入攻击。7. 超越复刻探索更高级的架构与生态当你成功实现了基础版的编程Agent后可以以此为基石探索更强大的架构和集成。7.1 拥抱成熟的框架LangChain与LangGraph我们从头实现是为了理解原理。在实际项目中强烈建议使用成熟的框架如LangChain.js和LangGraph。它们提供了生产级的Agent、工具、记忆、链等抽象能节省大量底层工程时间。LangChain提供了构建LLM应用所需的所有标准化组件。你可以用几行代码就组装出一个具备工具调用能力的Agent。LangGraph专注于构建有状态的、多步骤的工作流。它用“图”的概念来描述Agent的执行流程非常适合实现我们上面提到的复杂任务规划和循环执行并且原生支持分支、循环、并行等复杂逻辑。使用LangChain后我们构建Agent的代码会变得非常声明式和简洁。7.2 集成外部知识与RAG我们的Agent目前只依赖LLM的内置知识和项目内的文件。要让它更专业可以为其集成检索增强生成RAG能力。接入文档将React官方文档、团队内部组件库文档等向量化存储。当用户询问“如何使用Context API”时Agent能先检索相关文档片段再基于这些准确信息生成回答或代码。学习代码库将整个项目的代码进行索引。当用户说“修改登录逻辑”时Agent能快速定位到所有相关的登录函数和组件。这相当于给Agent配备了一个强大的外部记忆库使其回答和操作更加精准、符合项目规范。7.3 从命令行工具到IDE插件我们目前构建的是一个命令行工具。真正的生产力提升在于将其集成到开发环境中。VS Code/WebStorm插件将Agent的能力封装成IDE插件。用户可以在编辑器内直接与Agent对话高亮一段代码后让Agent解释、重构或生成测试。这提供了最无缝的体验。代码提交助手集成到Git钩子中在提交代码前让Agent自动审查代码风格、发现潜在bug、生成提交信息。这个项目的价值最终体现在它能否无缝融入你现有的开发工作流成为一个得力的“副驾驶”而不是一个需要额外切换上下文去使用的独立工具。通过这个从0到1的复刻过程你获得的不仅仅是另一个工具的使用方法而是深度理解并参与塑造下一代人机协作编程模式的能力。当你再使用Cursor或Trae时你看到的将不再是魔法而是一个个你亲手实现过、可以拆解和讨论的精妙模块。