十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

手写AI Agent核心:从零构建轻量级Cursor执行引擎

手写AI Agent核心:从零构建轻量级Cursor执行引擎 1. 项目概述为什么我们要“手搓”一个Cursor的最小版本最近在AI Agent的开发圈子里一个话题讨论得挺热我们真的需要LangChain、LangGraph这些重型框架吗还是说有时候自己动手从零开始构建一个核心功能反而能让我们对Agent和Tool的运作机制理解得更透彻这个项目——“手写Cursor最小版本”——就是基于这个想法的一次实践。这里的“Cursor”并非指那个流行的AI代码编辑器而是在AI Agent语境下一个能够理解用户意图、自主调用工具Tool并执行任务的核心“执行光标”。你可以把它想象成一个简化版的、专属于你自己的AI助手大脑。它接收你的自然语言指令比如“帮我查一下北京的天气”然后它需要解析出意图查询天气找到对应的工具天气查询API调用它并把结果组织成你能理解的话返回给你。市面上成熟的框架比如LangChain确实提供了开箱即用的Agent、大量预置Tool和复杂的编排逻辑。但对于学习者和希望深度定制的开发者来说它们有时显得过于“黑盒”和臃肿。通过手写一个最小版本我们能剥离所有非核心的装饰聚焦于几个最本质的问题Agent如何思考Tool如何被定义和调用状态如何流转这个过程不仅能加深理解更能让你获得一种“一切尽在掌握”的构建能力。无论你是想入门Agent开发还是希望为自己的小项目嵌入一个轻量级AI大脑这个“手搓”之旅都会很有价值。2. 核心架构设计一个最小可行Agent需要什么要构建一个可用的Cursor Agent我们不需要一开始就追求大而全。相反我们应该采用“最小可行产品”MVP的思路只实现最核心的链路。经过拆解一个最基础的Agent系统至少包含以下四个核心模块它们共同构成了一个完整的“感知-思考-行动”循环。2.1 大脑LLM驱动的工作流引擎Agent的核心是一个工作流引擎它负责驱动整个任务执行的循环。这个引擎的核心逻辑是一个while循环它不断重复“思考-行动-观察”的步骤直到任务完成或达到停止条件。class CursorAgent { constructor(llm, tools) { this.llm llm; // 大语言模型实例 this.tools tools; // 工具集 this.memory []; // 对话历史记忆 } async run(userInput) { let maxSteps 10; // 防止无限循环 let step 0; let finalAnswer null; // 将用户输入加入记忆 this.memory.push({ role: user, content: userInput }); while (step maxSteps !finalAnswer) { step; // 1. 思考根据当前记忆让LLM决定下一步做什么 const llmResponse await this._think(); // 2. 解析LLM的响应判断是调用工具还是直接回答 const action this._parseLlmResponse(llmResponse); if (action.type tool_call) { // 3. 行动执行工具调用 const toolResult await this._act(action); // 4. 观察将工具执行结果加入记忆供下一轮思考使用 this.memory.push({ role: tool, content: Tool ${action.toolName} returned: ${toolResult} }); } else if (action.type final_answer) { // 任务完成给出最终答案 finalAnswer action.answer; } } return finalAnswer || 任务未能在限定步骤内完成。; } async _think() { // 构建包含系统指令、记忆和当前任务的提示词发送给LLM // 具体实现见下文 } _parseLlmResponse(response) { // 解析LLM返回的文本提取出是调用工具还是最终回答 // 具体实现见下文 } async _act(action) { // 找到对应的工具并执行 const tool this.tools.find(t t.name action.toolName); if (!tool) { return Error: Tool ${action.toolName} not found.; } return await tool.execute(action.arguments); } }这个run方法就是Agent的主循环。它清晰地展示了ReActReasoning and Acting模式的核心Agent在思考_think后决定行动_act然后观察结果并进入下一轮思考。maxSteps是一个重要的安全阀防止Agent陷入死循环。注意在实际项目中这个循环可能需要处理更复杂的状态比如并行工具调用、子任务分解等。但对于最小版本串行的“思考-行动”循环已经足够清晰和强大。2.2 工具可插拔的功能模块Tool是Agent延伸的手脚。一个Tool本质上是一个具有明确输入输出规范的函数。在我们的设计中每个Tool需要三个基本属性name: 工具的唯一标识符LLM通过这个名字来调用它。description: 工具功能的自然语言描述这是LLM理解何时该使用此工具的关键。execute: 具体的执行函数。// 定义一个简单的计算器工具 const calculatorTool { name: calculator, description: Useful for performing basic arithmetic calculations. Input should be a mathematical expression like 2 2 or sqrt(16)., async execute(args) { try { // 注意这里使用eval有安全风险仅用于演示。生产环境应用用安全的数学表达式解析库如math.js const result eval(args.expression); return The result of ${args.expression} is ${result}.; } catch (error) { return Calculation error: ${error.message}; } } }; // 定义一个模拟的网络搜索工具 const webSearchTool { name: search_web, description: Useful for searching the web for current information. Input should be a search query string., async execute(args) { // 这里模拟一个网络请求 await new Promise(resolve setTimeout(resolve, 500)); // 模拟延迟 return Here are the search results for ${args.query}: [Simulated result 1, Simulated result 2].; } };工具设计的核心原则是“描述清晰”和“功能单一”。description字段必须足够详细让LLM能准确判断在什么场景下使用它。例如“进行数学计算”就比“计算器”要好。功能单一则意味着一个工具只做一件事这有利于LLM理解和组合使用。2.3 记忆对话历史与上下文管理记忆Memory是Agent拥有“连续性”的关键。没有记忆Agent就是健忘的每一轮对话都是独立的。在我们的最小实现中我们采用最简单的对话历史记忆即一个数组按顺序存储用户输入、AI思考、工具调用和工具结果。// 在Agent的构造函数中初始化 this.memory []; // 在运行循环中我们会不断往memory里push内容 // 用户输入: { role: user, content: ... } // AI思考/决策: { role: assistant, content: ... } (来自_think) // 工具结果: { role: tool, content: ... }当构建每次思考的提示词Prompt时我们会将最近的若干条记忆例如最后10条包含进去作为上下文提供给LLM。这被称为“上下文窗口”管理。对于更复杂的场景你可能需要实现摘要记忆将长历史总结成一段话、向量记忆根据语义搜索相关历史等但对话历史记忆是基础且必需的。2.4 提示工程让LLM学会“思考”和“调用”这是连接LLM大脑和我们自定义逻辑的桥梁。我们需要精心设计提示词Prompt来引导LLM按照我们设定的格式进行输出。一个典型的提示词包含以下几个部分系统指令System Instruction定义Agent的角色、能力和输出格式要求。这是最重要的部分。工具描述Tool Descriptions以结构化文本列出所有可用工具的名称和描述。对话历史Conversation History提供之前的交互记录赋予Agent上下文。当前请求Current Request用户的最新输入。输出格式指令Output Format明确告诉LLM应该如何回应。async _think() { const systemInstruction You are a helpful AI assistant that can use tools to solve problems. 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 EXACT JSON format: { thought: Your reasoning about what to do next, action: { type: tool_call, toolName: name_of_the_tool, arguments: { arg1: value1, arg2: value2 } } } If you have the final answer for the user, respond with: { thought: Your final reasoning, action: { type: final_answer, answer: The final answer to the user } } You must always output valid JSON.; // 构建对话历史上下文只取最近N条以避免超出LLM令牌限制 const recentMemory this.memory.slice(-6); // 示例取最近6条消息 const memoryContext recentMemory.map(m ${m.role}: ${m.content}).join(\n); const prompt ${systemInstruction}\n\n## Conversation History:\n${memoryContext}\n\n## Current User Request:\n${this.memory[this.memory.length-1].content}\n\nYour response:; // 调用LLM API这里以调用OpenAI格式的API为例 const response await this.llm.generate(prompt); return response; }这个提示词做了几件关键事它明确了Agent的身份列出了可用的“技能”工具并强制规定了输出的JSON格式。这种结构化输出JSON Mode对于后续的解析至关重要。LLM的“思考”过程会放在thought字段这不仅是给我们看的有时也能帮助调试Agent的决策逻辑。3. 分步实现与核心代码解析有了清晰的设计我们就可以开始动手编码了。我们将使用Node.js环境因为它有丰富的生态和异步处理能力非常适合构建这类IO密集型的Agent应用。3.1 环境搭建与基础依赖首先确保你安装了Node.js建议版本18或以上。然后初始化项目并安装核心依赖。我们不需要LangChain但需要一个能与LLM API通信的库比如openai如果你用OpenAI的模型或者通用的HTTP客户端如axios。mkdir mini-cursor-agent cd mini-cursor-agent npm init -y npm install axios # 用于调用LLM API为了模拟LLM我们也可以先创建一个简单的Mock LLM类这样可以在不连接真实API的情况下测试核心逻辑。这对于快速迭代和单元测试非常有用。// mockLLM.js class MockLLM { constructor() { // 一个简单的规则如果用户输入包含“计算”就调用计算器包含“搜索”就调用搜索否则直接回答。 this.rules { 计算: { type: tool_call, toolName: calculator, arguments: { expression: 22 } // 简化处理实际应从输入中提取 }, 搜索: { type: tool_call, toolName: search_web, arguments: { query: default query } } }; } async generate(prompt) { // 模拟LLM的思考延迟 await new Promise(resolve setTimeout(resolve, 100)); // 这是一个极其简化的“推理”。真实场景下这里会调用GPT等模型的API。 // 我们假设prompt的最后一行是用户输入。 const lines prompt.split(\n); const lastUserLine lines.find(line line.includes(User Request:)); const userInput lastUserLine ? lastUserLine.replace(User Request:, ).trim() : ; let action; for (const [key, value] of Object.entries(this.rules)) { if (userInput.includes(key)) { action value; break; } } if (action) { return JSON.stringify({ thought: 用户想进行${Object.keys(this.rules).find(k userInput.includes(k))}操作我需要调用相应的工具。, action: action }); } else { return JSON.stringify({ thought: 这个问题不需要使用工具我可以直接回答。, action: { type: final_answer, answer: 这是一个模拟回答。你说了“${userInput}” } }); } } } module.exports MockLLM;这个Mock类虽然简单但能让我们立刻跑通整个Agent循环验证架构是否可行这是快速原型开发的关键一步。3.2 核心Agent类的完整实现现在我们将之前设计的各个模块组合起来形成一个完整的CursorAgent类。// cursorAgent.js const MockLLM require(./mockLLM); // 或替换为真实的LLM客户端 class CursorAgent { constructor(llm, tools, options {}) { this.llm llm || new MockLLM(); this.tools tools || []; this.memory []; this.maxSteps options.maxSteps || 10; // 工具名称到工具对象的映射方便快速查找 this.toolMap {}; this.tools.forEach(tool { this.toolMap[tool.name] tool; }); } // 重置记忆开始一个新的会话 reset() { this.memory []; } // 主运行方法 async run(userInput) { this.memory.push({ role: user, content: userInput }); let step 0; let finalAnswer null; console.log(开始处理: ${userInput}); while (step this.maxSteps finalAnswer null) { step; console.log(\n--- 第 ${step} 步 ---); // 1. 思考 const llmResponse await this._think(); console.log(LLM原始响应: ${llmResponse}); // 2. 解析 let action; try { const parsed JSON.parse(llmResponse); if (parsed.thought) { console.log(Agent思考: ${parsed.thought}); } action parsed.action; } catch (error) { console.error(解析LLM响应失败响应不是有效的JSON:, llmResponse); // 如果解析失败尝试将其视为最终答案 action { type: final_answer, answer: llmResponse }; } // 3. 判断行动类型并执行 if (action.type tool_call) { const toolName action.toolName; const toolArgs action.arguments || {}; console.log(决定调用工具: ${toolName}参数:, toolArgs); // 检查工具是否存在 if (!this.toolMap[toolName]) { const errorMsg 工具“${toolName}”不存在。; console.error(errorMsg); this.memory.push({ role: system, content: errorMsg }); continue; // 继续下一轮循环让LLM根据错误信息重新决策 } // 执行工具 let toolResult; try { toolResult await this.toolMap[toolName].execute(toolArgs); console.log(工具执行结果: ${toolResult}); } catch (toolError) { toolResult 工具执行出错: ${toolError.message}; console.error(toolResult); } // 4. 观察将结果存入记忆 this.memory.push({ role: tool, content: 调用工具 ${toolName} 完成结果: ${toolResult} }); } else if (action.type final_answer) { finalAnswer action.answer; console.log(得出最终答案: ${finalAnswer}); this.memory.push({ role: assistant, content: finalAnswer }); } else { console.error(未知的action类型: ${action.type}); // 存入错误信息让LLM在下一轮知晓 this.memory.push({ role: system, content: 内部错误: 接收到未知的action类型“${action.type}”。 }); } } if (finalAnswer null) { finalAnswer 任务在 ${this.maxSteps} 步内未完成。可能陷入了循环或需要更多步骤。; console.warn(finalAnswer); } return finalAnswer; } // 构建提示词并调用LLM async _think() { // 构建系统指令和工具描述 const toolDescriptions this.tools.map(t - ${t.name}: ${t.description}).join(\n); const systemInstruction 你是一个智能助手可以通过调用工具来解决问题。你可以使用的工具如下 ${toolDescriptions} 你必须严格按照以下JSON格式回应 { thought: 你的推理过程, action: { type: tool_call 或 final_answer, // 如果 type 是 tool_call则需要以下字段 toolName: 工具名称, arguments: { /* 工具参数对象 */ } // 如果 type 是 final_answer则需要 answer: 给用户的最终答案 } } 请确保你的回应是且仅是一个合法的JSON对象。; // 构建对话历史上下文避免过长 const recentMemory this.memory.slice(-8); // 限制上下文长度 const memoryContext recentMemory.map(m ${m.role}: ${m.content}).join(\n); // 获取最新的用户输入通常是最后一条 const lastUserMessage this.memory.filter(m m.role user).pop(); const currentRequest lastUserMessage ? lastUserMessage.content : ; const prompt ${systemInstruction}\n\n## 对话历史:\n${memoryContext}\n\n## 当前用户请求:\n${currentRequest}\n\n你的回应:; // 调用LLM return await this.llm.generate(prompt); } } module.exports CursorAgent;这个实现包含了健壮的错误处理JSON解析失败、工具不存在、工具执行出错并添加了详细的控制台日志方便我们跟踪Agent的每一步决策。maxSteps参数防止了无限循环这是一个在实际开发中必须考虑的安全措施。3.3 集成真实LLM以OpenAI API为例当核心逻辑测试通过后我们就可以替换掉Mock LLM接入真实的AI模型。这里以OpenAI的GPT-3.5/4为例。首先安装OpenAI官方库并设置你的API密钥建议通过环境变量OPENAI_API_KEY管理。npm install openai然后创建一个真实的LLM包装类// openaiLLM.js const OpenAI require(openai); class OpenAILLM { constructor(apiKey, model gpt-3.5-turbo) { this.client new OpenAI({ apiKey }); this.model model; } async generate(prompt) { try { const completion await this.client.chat.completions.create({ model: this.model, messages: [ { role: system, content: 你是一个严格遵守输出格式的AI助手。 }, { role: user, content: prompt } ], temperature: 0.1, // 低温度使输出更稳定、更倾向于遵循指令 response_format: { type: json_object } // 关键要求API返回JSON对象 }); const content completion.choices[0]?.message?.content; if (!content) { throw new Error(OpenAI API返回内容为空); } return content; } catch (error) { console.error(调用OpenAI API失败:, error); // 返回一个兜底的错误JSON避免整个流程中断 return JSON.stringify({ thought: 调用语言模型时发生错误。, action: { type: final_answer, answer: 抱歉处理您的请求时遇到了问题请稍后再试。 } }); } } } module.exports OpenAILLM;这里有两个关键点response_format: { type: json_object }这是OpenAI API较新版本提供的功能能显著提高模型输出合规JSON的概率。对于其他厂商的API可能需要通过提示词更严格地约束。temperature: 0.1较低的“温度”参数使得模型的输出更确定、更可预测这对于需要稳定解析JSON的Agent场景非常重要。现在你可以在初始化Agent时使用这个真实的LLM类const OpenAILLM require(./openaiLLM); const CursorAgent require(./cursorAgent); const calculatorTool require(./tools/calculator); const webSearchTool require(./tools/webSearch); const llm new OpenAILLM(process.env.OPENAI_API_KEY, gpt-3.5-turbo); const tools [calculatorTool, webSearchTool]; const agent new CursorAgent(llm, tools); (async () { const answer await agent.run(请问3的4次方是多少); console.log(\n最终回复:, answer); })();4. 实战演练从简单计算到多轮对话让我们用几个具体的例子来看看这个手写的迷你Cursor Agent是如何工作的。4.1 场景一单次工具调用计算器用户输入“计算一下 (15 27) * 3 的结果。”第一轮循环_think(): LLM收到包含系统指令、工具列表计算器、搜索和用户输入的提示词。它推理后决定调用计算器。LLM响应JSON:{ thought: 用户需要一个算术表达式的结果。我有一个计算器工具可以处理这个。, action: { type: tool_call, toolName: calculator, arguments: { expression: (15 27) * 3 } } }_parseLlmResponse(): 解析出要调用calculator工具参数为{“expression”: “(1527)*3”}。_act(): 找到calculator工具并执行execute({“expression”: “(1527)*3”})。工具内部使用eval演示用或安全计算库得出结果126。记忆更新新增一条{role: ‘tool’, content: ‘调用工具 calculator 完成结果: The result of (15 27) * 3 is 126.’}。第二轮循环_think(): 这次提示词中包含了上一轮的工具调用结果。LLM看到结果后认为已经得到答案无需再调用工具。LLM响应JSON:{ thought: 计算器已经给出了结果126。我可以将此作为最终答案返回给用户。, action: { type: final_answer, answer: (15 27) * 3 的计算结果是 126。 } }解析出final_answer循环结束返回最终答案。控制台输出会清晰显示这两步的思考、行动和结果。4.2 场景二多轮对话与上下文记忆对话流用户“今天北京天气怎么样”用户“那上海呢”这个场景考验的是Agent的记忆能力。处理第一问假设我们有一个get_weather工具。LLM会调用它参数{“city”: “北京”}然后将天气结果存入记忆。处理第二问“那上海呢”这是典型的指代。在第二轮_think()时提示词中包含了之前的对话历史用户问北京天气工具返回北京天气。LLM结合上下文能正确推理出“上海”指的是城市并调用get_weather({“city”: “上海”})。这就是记忆模块的价值体现。4.3 场景三复杂任务分解雏形我们的最小版本目前是串行思维一次只做一个动作。但通过巧妙的提示词设计可以引导LLM进行简单的任务分解。例如用户问“北京和上海的平均气温差是多少”一个更强大的Agent可能会先分解为“查北京气温”和“查上海气温”然后调用计算器计算差值。在我们的框架下LLM可能会这样工作第一轮思考后决定先查北京气温调用get_weather({“city”: “北京”})结果中包含气温15°C。第二轮记忆中有北京气温现在需要上海气温调用get_weather({“city”: “上海”})得到20°C。第三轮记忆中有两地气温调用calculator({“expression”: “20 - 15”})得到5。第四轮给出最终答案。这展示了如何通过多轮简单的工具调用串联起来完成一个相对复杂的任务。要实现更复杂的并行或条件逻辑就需要引入更高级的编排机制这通常是LangGraph等框架解决的问题但我们的最小版本已经具备了实现基础链式任务的能力。5. 避坑指南与进阶思考在亲手实现和调试这个迷你Agent的过程中我踩过不少坑也总结出一些让Agent更稳定、更聪明的经验。5.1 常见问题与调试技巧LLM不按格式输出JSON现象JSON.parse报错Agent流程中断。解决强化提示词在系统指令中明确强调“EXACT JSON format”、“必须”、“只输出JSON”。使用JSON Mode如果API支持如OpenAI务必设置response_format: { type: ‘json_object’ }这是最有效的方法。后处理清洗在解析前用正则表达式尝试从响应文本中提取第一个完整的JSON对象块。例如const jsonMatch response.match(/{[\s\S]*?}/);。设置低Temperature如0.1或0.2减少随机性。工具调用参数错误现象LLM决定调用工具但生成的参数对象格式不对或者缺少必要参数。解决在工具描述中明确参数格式例如描述写成“Input should be a JSON object with ‘city’ field (string) representing the city name.”提供示例在提示词中给出一两个工具调用的完整JSON示例。在_act方法中增加参数验证在执行工具前检查参数是否存在、类型是否正确如果不对将明确的错误信息反馈给记忆让LLM在下一次思考时修正。Agent陷入死循环或无效循环现象Agent反复调用同一个工具或者在不该调用工具时调用始终无法给出最终答案。解决设置最大步数就像我们代码里的maxSteps这是最后的安全网。优化工具描述确保final_answer的使用场景在提示词中被清晰定义。例如“当你拥有足够信息可以直接、完整地回答用户问题时请给出最终答案。”在记忆中注入系统提示如果发现Agent在兜圈子可以在记忆里加入一条{role: ‘system’, content: ‘你似乎陷入了循环请重新评估是否需要继续调用工具还是可以直接回答。’}手动引导它。上下文长度爆炸现象对话轮次多了以后提示词变得非常长导致API调用成本增加、速度变慢甚至可能超出模型的上下文窗口限制。解决限制记忆长度像我们代码中slice(-8)做的那样只保留最近N条消息。实现记忆摘要更高级的做法是当历史对话较长时调用LLM本身对之前的对话进行总结然后用一段摘要替换掉详细的历史记录。这能极大地节省令牌token。5.2 性能优化与扩展方向这个最小版本是起点你可以根据需求对它进行扩展并行工具调用目前的循环是串行的。可以修改_think和_act让LLM能一次性输出一个包含多个工具调用的计划列表action类型改为parallel_tool_calls然后使用Promise.all并行执行最后统一观察结果。这能显著提升处理效率。工具动态注册与管理目前的工具列表是在Agent初始化时固定的。可以实现一个registerTool方法允许在运行时动态添加或移除工具使Agent能力更灵活。更复杂的记忆系统引入向量数据库如Chroma、Pinecone存储长期记忆实现基于语义的相关记忆检索而不仅仅是最近的几条。这对于需要大量背景知识的对话至关重要。验证与安全在生产环境中直接执行来自LLM的代码如eval或发起网络请求是极度危险的。必须对工具调用进行严格的沙箱隔离、参数白名单验证和权限控制。流式输出与用户体验当前Agent是“思考-执行-再思考”的阻塞模式用户需要等待全部完成。可以改为流式Streaming输出先将LLM的“思考”过程流式展示给用户再展示工具调用状态和最终结果体验会更像ChatGPT。5.3 与LangChain等框架的对比思考最后回到我们开头的问题有了这个手写版本还需要LangChain吗手写版本的优点极致轻量与透明零外部依赖除LLM SDK代码完全可控调试方便。学习价值高彻底理解Agent核心机制不被框架抽象所迷惑。高度定制可以针对特定业务场景做极其精细的优化没有框架的通用性包袱。LangChain等框架的优点开箱即用提供了大量预构建的工具Tools、链Chains、Agent模板和记忆实现。生态丰富集成了无数第三方API、数据库和工具连接能力强大。最佳实践内置框架本身沉淀了很多处理边缘情况、优化提示词、管理复杂工作流如LangGraph的经验。社区支持遇到问题容易找到解决方案和讨论。我的建议是从手写开始用框架进阶。对于学习、原型验证或极其简单的场景手写一个最小版本完全足够且收益巨大。它能给你带来无与伦比的掌控感和深刻理解。当你需要快速构建一个功能复杂、集成度高的生产级应用时再转向LangChain这类成熟框架利用其生态和稳定性来提升开发效率。此时因为你已经“手搓”过核心你对框架的运作原理将一目了然能更高效地使用和定制它。
返回列表