复刻Cursor编程Agent:用TaoToken统一Key跑通4套文件命令工具+Promise.all并发生成React项目

发布时间:2026/10/9 23:52:19
复刻Cursor编程Agent:用TaoToken统一Key跑通4套文件命令工具+Promise.all并发生成React项目
1. 从裸模型到编程 Agent为什么单靠对话接口搭不出 React 项目很多人第一次用 Cursor 或者 Claude Code 的时候都会愣一下明明只是说了句「帮我建个 React TodoList」它就能自己创建目录、写package.json、跑npm install最后还把开发服务器起起来。而你自己调大模型接口得到的永远只有一段文字连个文件都碰不到。这个差距的根源在于裸大模型是无状态的文本生成器它没有手脚。它能告诉你「你应该运行 npm create vite」但它没法真的去运行。它能在回答里贴出一段App.jsx代码但它没法把这段代码写进你的磁盘。这就是为什么我们需要 Agent 这个概念——给模型装上工具让它从「只会说」变成「能动手」。编程 Agent 的核心公式其实很朴素Agent LLM 大脑 工具集 ReAct 循环。LLM 负责推理和规划工具集负责真正操作文件系统和终端ReAct 循环负责把「推理—调用工具—观察结果—再推理」串起来直到任务完成。Cursor 之所以好用本质上就是把这套循环打磨得足够稳工具定义得足够细错误处理得足够全。这篇文章要复刻的就是这套东西。我会用 TaoToken 作为统一的模型接入通道把 4 套文件与命令工具封装好再用Promise.all做并发调度最后跑一次完整的「一句话生成 Vite React TodoList」验证。整个过程你可以直接复制代码跟做不需要任何特殊网络环境只需要一个能调通的 API Key。适合谁看已经会写 JavaScript、用过 Node但对 Agent 内部机制还停留在「知道概念、没亲手搭过」阶段的开发者。如果你之前只调过chat.completions接口那这篇正好补上从「对话」到「操作」的那一层。先说清楚一个容易混淆的点Agent 不是让模型直接执行代码而是模型输出结构化的工具调用请求tool_calls由我们的 Node 程序去解析、执行、把结果回填给模型。模型始终在「提议」真正动手的是我们写的工具函数。理解这一点后面所有的安全边界和错误处理才有落脚点。2. TaoToken 统一 Key 接入一个 Base URL 打通多模型通道在动手写工具之前得先把模型通道解决掉。自己搭 Agent 最烦的一件事就是今天想用这个模型明天想换那个模型每换一次就要改一遍 SDK、改一遍鉴权、改一遍 baseURL。如果工具层和模型层耦合在一起换模型等于重写半个项目。TaoToken 在这里扮演的角色就是统一接入层。它提供 OpenAI 兼容的接口协议你只需要记住一个 Base URL 和一把 Key就能在多个模型之间切换而 Agent 的工具代码完全不用动。这对我们这种「工具层写一次、模型随便换」的架构来说非常关键。接入信息如下建议直接记下来官网入口https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI 地址写进代码的 baseURLhttps://taotoken.net/api模型对话体验页https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewriteAPI Key 管理页https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite注意一个细节https://taotoken.net/api这个地址后面不要再手动拼/v1也不要加 UTM 参数。很多 OpenAI 兼容 SDK 会自动在 baseURL 后面补/chat/completions如果你自己又加了一层路径就会变成/api/v1/v1/chat/completions直接 404。这个坑我在第一次接的时候踩过报错信息是404 page not found排查了半天才发现是路径重复。拿到 Key 之后先别急着写 Agent用最简单的方式验证通道是否通。可以先用模型对话页发一条消息确认账号状态正常再回到代码里。这样能把「Key 问题」和「代码问题」分开排障效率高很多。关于模型选择我的建议是Agent 场景优先选工具调用能力稳定的模型。因为 Agent 的核心是模型输出结构化的tool_calls如果模型对 function calling 支持不好会出现「该调工具的时候不调」「参数格式乱写」「幻觉出不存在的工具名」这些问题。工具调用稳不稳比模型聪不聪明更重要。还有一点要提醒TaoToken 是接入通道不是编辑器也不是 Agent 框架本身。它解决的是「模型怎么调」的问题而「工具怎么定义、循环怎么写」仍然是我们自己代码的事。把这两层分清楚架构才不会乱。3. 可复制配置4 套工具定义 Agent 主循环完整代码这一节是全文的核心代码量比较大但每一段都可以直接复制运行。整体结构分三个文件package.json管依赖all-tools.js放 4 套工具agent.js放主循环。先建目录mkdir cursor-agent-clone cd cursor-agent-clone npm init -y npm install langchain/openai langchain/core dotenv zod依赖说明langchain/openai提供 OpenAI 兼容的 Chat 模型封装langchain/core提供tool工厂和消息类型zod做参数校验dotenv读环境变量。这里没有用重型框架是因为我想让你看清 Agent 循环的每一行而不是被框架的黑盒遮住。接着配置环境变量。新建.env文件TAOTOKEN_API_KEY你从 api-keys 页面拿到的Key TAOTOKEN_BASE_URLhttps://taotoken.net/api TAOTOKEN_MODEL你的模型ID注意TAOTOKEN_BASE_URL就是纯https://taotoken.net/api不带斜杠结尾不带/v1。模型 ID 填你在模型列表里看到的那个不同模型 ID 不一样别照抄别人的。然后是 4 套工具。新建all-tools.jsimport { tool } from langchain/core/tools; import fs from node:fs/promises; import path from node:path; import { spawn } from node:child_process; import { z } from zod; // 工具1读取文件 const readFileTool tool( async ({ filePath }) { try { const content await fs.readFile(filePath, utf-8); console.log([工具] read_file(${filePath}) 读取 ${content.length} 字符); return content; } catch (err) { return 读取失败${err.message}; } }, { name: read_file, description: 读取本地文件内容用于查看代码、配置。支持相对和绝对路径。, schema: z.object({ filePath: z.string().describe(待读取的文件路径), }), } ); // 工具2写入文件自动递归建目录 const writeFileTool tool( async ({ filePath, content }) { try { const dir path.dirname(filePath); await fs.mkdir(dir, { recursive: true }); await fs.writeFile(filePath, content, utf-8); console.log([工具] write_file(${filePath}) 写入 ${content.length} 字符); return 写入成功${filePath}; } catch (err) { return 写入失败${err.message}; } }, { name: write_file, description: 向指定路径写入文本内容目录不存在会自动递归创建。, schema: z.object({ filePath: z.string().describe(文件保存路径), content: z.string().describe(文件完整内容), }), } ); // 工具3遍历目录 const listDirectoryTool tool( async ({ directoryPath }) { try { const files await fs.readdir(directoryPath); console.log([工具] list_directory(${directoryPath}) 共 ${files.length} 项); return 目录内容\n${files.join(\n)}; } catch (err) { return 读取目录失败${err.message}; } }, { name: list_directory, description: 列出指定目录下的文件和文件夹用于分析项目结构。, schema: z.object({ directoryPath: z.string().describe(目标目录路径), }), } ); // 工具4执行终端命令 const executeCommandTool tool( async ({ command, workingDirectory }) { const cwd workingDirectory || process.cwd(); console.log([工具] execute_command(${command}) cwd${cwd}); return new Promise((resolve) { const child spawn(command, { cwd, shell: true, stdio: inherit, }); let errMsg ; child.on(error, (err) (errMsg err.message)); child.on(close, (code) { if (code 0) { resolve(命令执行成功${command}); } else { resolve(命令失败退出码 ${code}错误${errMsg}); } }); }); }, { name: execute_command, description: 执行 npm、vite、ls 等终端命令可指定工作目录。, schema: z.object({ command: z.string().describe(完整命令字符串), workingDirectory: z.string().describe(命令执行目录可选), }), } ); export { readFileTool, writeFileTool, listDirectoryTool, executeCommandTool };这 4 套工具覆盖了编程 Agent 最核心的动作看读文件、列目录、写写文件、跑执行命令。write_file里的fs.mkdir(dir, { recursive: true })是关键没有它往不存在的目录写文件会直接抛ENOENT这是新手最常见的报错之一。然后是主循环agent.jsimport dotenv/config; import { ChatOpenAI } from langchain/openai; import { HumanMessage, SystemMessage, ToolMessage, } from langchain/core/messages; import { readFileTool, writeFileTool, listDirectoryTool, executeCommandTool, } from ./all-tools.js; const model new ChatOpenAI({ modelName: process.env.TAOTOKEN_MODEL, apiKey: process.env.TAOTOKEN_API_KEY, temperature: 0, configuration: { baseURL: process.env.TAOTOKEN_BASE_URL, }, }); const tools [ readFileTool, writeFileTool, listDirectoryTool, executeCommandTool, ]; const modelWithTools model.bindTools(tools); const messages [ new SystemMessage( 你是专业前端编程 Agent拥有 4 个本地操作工具。 任务根据用户需求全自动搭建 Vite React TodoList 项目。 执行规范 1. 先 list_directory 查看当前目录 2. execute_command 执行 npm create vite 创建项目 3. 进入项目目录安装依赖 4. 读取并重写 src/App.jsx实现增删改查 5. 执行 npm run dev 启动 6. 完成后输出项目结构与说明 支持一次并行调用多个无依赖工具。 ), new HumanMessage(帮我创建一个 vitereact 的 TodoList 项目实现增删改查并启动), ]; let response await modelWithTools.invoke(messages); messages.push(response); let round 0; const MAX_ROUND 10; while (response.tool_calls response.tool_calls.length 0) { if (round MAX_ROUND) { console.log(达到最大循环轮次强制结束); break; } console.log(\n 第 ${round} 轮${response.tool_calls.length} 个工具调用 ); const toolResults await Promise.all( response.tool_calls.map(async (toolCall) { const target tools.find((t) t.name toolCall.name); if (!target) { return 错误不存在工具 ${toolCall.name}; } console.log([执行] ${toolCall.name} ${JSON.stringify(toolCall.args)}); try { return await target.invoke(toolCall.args); } catch (err) { return 工具异常${err.message}; } }) ); response.tool_calls.forEach((toolCall, i) { messages.push( new ToolMessage({ content: toolResults[i], tool_call_id: toolCall.id, }) ); }); response await modelWithTools.invoke(messages); messages.push(response); } console.log(\n 最终输出 ); console.log(response.content);这段代码里有几个必须理解的点。第一Promise.all把同一轮里所有tool_calls并发执行结果数组的顺序和tool_calls严格一一对应所以后面用forEach按索引绑定tool_call_id是安全的。第二每个工具调用都包了try/catch单个工具失败不会让整个循环崩掉而是把错误文本回填给模型让模型自己决定下一步。第三MAX_ROUND是防死循环的保险丝没有它模型偶尔会陷入「反复列目录」的怪圈。4. 验证请求一句话生成 React 项目并观察并发执行配置写完了现在跑一次真实验证。在项目根目录执行node agent.js你会看到终端开始滚动日志。第一轮通常只有 1 个工具调用因为模型要先list_directory看当前环境。接着它会调用execute_command执行npm create vitelatest todo-react -- --template react。这里有个细节npm create vite在非交互模式下需要把模板参数用--传进去否则会卡在交互式提问上。如果模型没写对你会在日志里看到命令挂起这时候可以在 System Prompt 里补一句「创建 vite 项目时使用 -- --template react 非交互参数」。项目创建完成后模型会进入下一轮通常是并发执行「进入目录安装依赖」和「读取 App.jsx」。注意cd这种命令在spawn里是无效的因为每个命令都是独立子进程cd只影响它自己那个进程。正确做法是通过workingDirectory参数指定 cwd而不是在命令里写cd todo-react npm install。我在工具描述里特意强调了workingDirectory就是为了引导模型用对方式。依赖装完后模型会读取src/App.jsx然后用write_file重写整个文件把 TodoList 的增删改查逻辑写进去。这一步是并发收益最明显的地方如果模型一次要写App.jsx、App.css、main.jsx三个文件串行写要等三次 IOPromise.all并发写只等最慢的那次。最后模型执行npm run dev启动开发服务器。因为用了stdio: inheritVite 的启动日志会直接打到你的终端上你能看到Local: http://localhost:5173/这样的输出。到这一步整个「一句话生成 React 项目」的链路就跑通了。实测下来从发指令到 Vite 起来整个流程大概十几秒到几十秒取决于依赖安装速度。并发带来的提升在工具调用密集的轮次里最明显尤其是多个文件读写同时发生的时候。你可以把Promise.all临时改成for循环串行执行对比一下日志里每轮的耗时差距一眼就能看出来。验证成功的标志有三个终端出现 Vite 的启动横幅、todo-react/src/App.jsx里是你期望的 TodoList 代码、浏览器打开localhost:5173能看到可交互的列表。三个都满足说明工具层、并发层、循环层全部工作正常。5. 常见报错排查401、local proxy failed 与 tool_call_id 错乱Agent 跑不起来的时候报错往往集中在几个固定位置。这一节按真实报错逐个拆。报错一401 Unauthorized。这个最直接Key 不对或者没读到。先确认.env里的TAOTOKEN_API_KEY没有多余空格和引号再确认dotenv/config是在文件最顶部 import 的——如果它排在ChatOpenAI后面环境变量还没加载apiKey就是undefined。还有一种情况是 Key 复制时带了换行肉眼看不出来建议重新从 api-keys 页面复制一次。报错二local proxy failed 或连接超时。这类错误通常出现在 baseURL 写错的时候。再强调一遍baseURL必须是https://taotoken.net/api不要加/v1不要加结尾斜杠。如果你在代码里看到请求地址变成了https://taotoken.net/api/v1/chat/completions那就是 SDK 自动补了/v1而你的 baseURL 又带了/v1路径重复。改回纯/api即可。报错三reading choices of undefined。这个报错的意思是SDK 期望响应里有choices字段但实际拿到的响应结构不对。常见原因有两个一是模型 ID 填错了请求打到了一个不存在的模型上返回的是错误对象而不是标准 completion二是 baseURL 指向了一个非 OpenAI 兼容的端点。解决办法是先用模型对话页确认模型 ID 正确再检查 baseURL。报错四OAuth 相关报错。如果你在日志里看到 OAuth、token refresh 之类的字样说明请求被路由到了需要 OAuth 鉴权的通道而不是 API Key 通道。检查你的 baseURL 是不是误填了某个控制台地址。API 调用只认https://taotoken.net/api加 Key不涉及 OAuth 流程。报错五ToolMessage 报 tool_call_id 不匹配。这个错误不会直接抛异常但表现为模型「看不见工具结果」反复调用同一个工具。根因是ToolMessage的tool_call_id和AIMessage里的tool_calls[].id对不上。用Promise.all的时候结果数组顺序和tool_calls顺序一致所以按索引绑定是安全的但如果你在 map 里做了过滤或者排序顺序就乱了。排查方法是在 pushToolMessage前打印toolCall.id和toolResults[i]的前 50 个字符确认一一对应。报错六命令执行成功但文件没生成。这通常是workingDirectory没传对。比如模型执行npm create vite时 cwd 是项目根目录但执行npm install时 cwd 还是根目录而不是todo-react依赖就装错地方了。在工具日志里把cwd打出来一眼就能定位。报错七无限循环。模型反复列目录、反复读同一个文件任务永远不结束。这是MAX_ROUND存在的意义。如果频繁触发上限说明 System Prompt 里的执行规范不够明确模型不知道「什么时候算完成」。补一句「完成 npm run dev 后立即输出总结并停止调用工具」通常能解决。排障的通用思路是先看日志里最后一个成功的工具调用是什么再看模型下一轮想调什么。Agent 的问题几乎都能通过这两步定位到具体环节。6. 继续往下走把统一 Key 的 Agent 用到真实项目里跑通这个 Demo 之后你会发现 Agent 的骨架其实就这么大工具定义、并发调度、ReAct 循环、消息绑定。剩下的都是在这四个骨架上加东西。比如加一个 Git 工具让 Agent 能提交代码加一个 RAG 检索让它读项目文档理解业务规范或者用 LangGraph 把「规划」「编码」「执行」拆成多个 Agent 协作。但无论怎么扩展模型接入层保持统一是最省心的选择。工具代码写一次模型随便换Base URL 和 Key 始终是那一套。这样你在做实验的时候可以把精力放在工具设计和循环优化上而不是反复折腾鉴权。如果你还没拿到 Key可以从 API Keys 页面开始https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。接入过程中遇到路径或参数问题文档里有完整的请求示例https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。想先感受一下模型对话效果可以直接在 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 里试。如果你打算长期跑编码类 Agent 任务Coding Plan 会更合适https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。最后留一个我踩过的坑Promise.all适合无依赖的并发工具但「先创建目录再写文件」这种有先后关系的操作必须串行。判断标准很简单——后一个工具的入参是否依赖前一个工具的输出。依赖就串行不依赖就并发。这个判断做对了Agent 的稳定性和速度都会上一个台阶。