Cursor黑盒拆解!1套LangChain.js手写Mini编程Agent,自动生成React项目,效率提升60%

从零手写 Mini Cursor 编程Agent:LangChain.js+DeepSeek全自动创建React TodoList

前言

用过Cursor、Claude Code的前端应该都深有体会: 一句自然语言,AI就能自动创建项目、读写文件、执行终端命令、写完完整业务代码,全程不用手动操作。

网上大多只讲Python版LangChain Agent,Node.js前端原生可运行的完整实战教程极少,很多人照着文档写还疯狂踩坑:

  1. 循环逻辑错乱,response is not defined 报错
  2. 多工具串行执行,任务卡顿严重
  3. 文件/命令工具缺少容错,一报错直接中断流程
  4. workingDirectorycd 混用,目录找不到
  5. 模块导入后缀不统一,报 ERR_MODULE_NOT_FOUND

本文带你从零手写一套可直接运行的迷你Cursor编程Agent,读完你能收获:

  • 吃透ReAct Agent完整执行循环底层原理
  • 4套生产级工具:读/写文件、查看目录、执行终端命令
  • Promise.all并发多工具,解决串行卡顿痛点
  • 完整可运行Node.js代码,一键自动生成带动画、持久化的React+TS TodoList
  • 全套踩坑解决方案、空值兜底、异常捕获、最大循环防死循环逻辑

一、先搞懂:纯大模型为什么做不了自动化编程?

原生LLM天生存在3个致命短板,只能输出文字,无法真正操作本地项目:

  1. 无状态:不会自动保存对话上下文,必须手动维护消息数组
  2. 无操作权限:不能读写本地文件、执行bash命令、查看目录结构
  3. 无自主规划能力:复杂任务不会拆分步骤,不会迭代校验结果

Agent核心公式

Agent = LLM + Memory记忆 + Tool工具调用 + ReAct循环 工具就是给大模型装上手脚,让AI拥有操作系统文件、执行终端的能力,自主完成完整开发流程。

ReAct完整执行流程(本文代码核心逻辑)

  1. 传入系统提示词+用户需求,组装消息列表
  2. LLM推理判断是否需要调用工具,返回tool_calls数组
  3. 并发批量执行所有工具,捕获执行结果
  4. 工具结果绑定唯一tool_call_id,追加进消息上下文
  5. 循环执行推理→调用工具,直到模型不再返回工具调用,输出最终结果

二、前置环境准备

1. 安装依赖

bash 复制代码
pnpm install dotenv @langchain/openai @langchain/core chalk zod

2. 环境变量 .env

env 复制代码
DEEPSEEK_API_KEY=你的密钥

DeepSeek接口完全兼容OpenAI格式,使用ChatOpenAI即可快速对接,无需额外适配。

3. 项目目录结构

bash 复制代码
hello-langchain/
├── .env
├── src/
│   ├── all-tools.mjs      # 四大工具统一导出
│   └── mini-cursor.mjs    # Agent主执行逻辑

踩坑提醒:文件后缀统一使用.mjsESM模块,混用.js会出现模块找不到报错。

三、工具层实现:all-tools.mjs 完整代码

封装4个编程必备工具,基于node:fschild_process实现,自带异常兜底、路径自动创建。

javascript 复制代码
import { 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 }) => {
        const content = await fs.readFile(filePath, 'utf-8');
        console.log(`[工具调用] read_file(${filePath}) 成功读取 ${content.length} 字节`)
        return content;
    },
    {
        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){
        console.log(`[工具调用] write_file(${filePath}) 错误:${err.message}`)
        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) {
        console.log(`[工具调用] list_directory(${directoryPath}) 错误:${err.message}`)
        return `读取目录失败:${err.message}`
    }
  }, 
  {
      name: 'list_directory',
      description: '查看指定目录下全部文件与文件夹',
      schema: z.object({
          directoryPath: z.string().describe('目标目录路径')
      })
  }
)

// 4. 执行终端命令工具(核心,封装spawn子进程)
const executeCommandTool = tool(
    async ({ command, workingDirectory }) => {
        const cwd = workingDirectory || process.cwd();
        console.log(`[工具调用] execute_command(${command}) 工作目录:${cwd}`);
        return new Promise((resolve, reject) => {
            // 拆分命令与参数
            const [cmd, ...args] = command.split(' ');
            const child = spawn(cmd, args, {
                cwd,
                stdio: 'inherit',
                shell: true,
            })
            let errorMsg = '';
            child.on('error', (err) => {
                errorMsg = err.message
            });
            child.on('close', (code) => {
                if (code === 0) {
                   // 三元兜底空字符串,防止undefined
                   const cwdInfo = workingDirectory?
                   `\n\n重要提示:命令在目录"${workingDirectory}" 执行`
                   : '';
                   resolve(`命令行成功执行 ${command}${cwdInfo}`);
                } else {
                    resolve(`命令执行失败,退出码:${code}\n 错误:${errorMsg}`)
                }
            })
        })
        
    },
    {
        name: 'execute_command',
        description: '执行系统bash命令,支持指定工作目录,禁止在command内使用cd',
        schema: z.object({
            command: z.string().describe('待执行命令字符串'),
            workingDirectory: z.string().describe('指定命令运行目录')
        })
    }
)

// 统一导出工具
export {
    readFileTool, 
    writeFileTool, 
    listDirectoryTool, 
    executeCommandTool
}

关键细节说明

  1. 三元兜底空值写法 const cwdInfo = workingDirectory ? 提示文本 : '' 行业叫法:默认值兜底、空保护 ,强制变量永远为字符串,避免undefined导致拼接报错。
  2. spawn拆分命令 通过解构[cmd,...args]拆分指令与参数,适配任意多参数命令。
  3. 执行目录规范 workingDirectory会自动切换工作目录,命令内禁止写cd xxx,否则会路径找不到。

四、主程序mini-cursor.mjs:ReAct循环Agent完整实现

完整可运行代码

javascript 复制代码
// 手写mini-cursor编程Agent,自动生成React TodoList
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import {
    HumanMessage,
    SystemMessage,
    ToolMessage
} from '@langchain/core/messages';
import {
    executeCommandTool,
    readFileTool,
    writeFileTool,
    listDirectoryTool
} from './all-tools.mjs';
import chalk from 'chalk';

// 初始化DeepSeek模型
const model = new ChatOpenAI({
  modelName:'deepseek-v4-pro',
  apiKey: process.env.DEEPSEEK_API_KEY,
  temperature: 0, // 关闭随机性,工具调用稳定
  configuration: {
    baseURL: 'https://api.deepseek.com/v1',
  },
});

// 注册全部工具
const tools = [
    readFileTool,
    writeFileTool,
    listDirectoryTool,
    executeCommandTool
]
const modelWithTools = model.bindTools(tools);

// Agent执行任务需求
const case1 = `
创建一个功能丰富的React TodoList 应用:
1. 创建项目 : 
echo -e "n\nn" | pnpm create vite react-todo-app --template react-ts
2. 修改 src/App.tsx ,实现完整功能的TodoList:
- 添加、删除、标记完成
- 分类筛选(全部/进行中/已完成)
- 统计信息显示
- localStorage 数据持久化 
3. 添加复杂样式
- 渐变背景(蓝到紫)
- 卡片阴影,圆角
- 悬停效果
4. 添加动画:
- 添加/删除时的过渡动画 
- 使用css transitions
5. 列出目录确定项目结构
注意: 使用pnpm, 功能要完整, 样式要美观, 要有动画效果 
之后 react-todo-app 项目中:
1. 使用 pnpm install 安装依赖
2. 使用 pnpm run dev 启动服务器
`

// Agent核心执行函数,带最大迭代次数防死循环
async function runAgentWithTools(query, maxIterations=30) {
    // 初始化消息上下文,仅创建一次,不重复覆盖
    const messages = [
        new SystemMessage(`你是专业前端项目管理助手,使用工具完成开发任务。
        当前工作目录: ${process.cwd()}
        可用工具: 
        1. read_file: 读取文件
        2. write_file: 写入/创建文件
        3. execute_command: 执行终端命令(支持workingDirectory)
        4. list_directory: 查看目录结构
        强制规则 execute_command:
            workingDirectory 参数会自动切换目录,命令内绝对不能写cd
            错误示例:{ command: "cd react-todo-app && pnpm install", workingDirectory: "react-todo-app" }
            正确示例:{ command: "pnpm install", workingDirectory: "react-todo-app" }
            回复简洁,仅描述执行操作
        `),
        new HumanMessage(query)
    ];

    // ReAct循环主逻辑
    for(let i = 0; i < maxIterations; i++){
        console.log(chalk.blue(`\n===== 第${i+1}轮LLM推理 =====`));
        // 关键:调用模型获取response,修复response未定义报错
        const response = await modelWithTools.invoke(messages);
        messages.push(response);

        // 无工具调用,任务结束
        if (!response.tool_calls || response.tool_calls.length === 0) {
            console.log(chalk.green(`\n✅ AI 任务完成,最终回复:\n ${response.content}\n`));
            return response.content
        }

        console.log(chalk.yellow(`检测到${response.tool_calls.length}个工具调用,串行执行`));
        // 批量执行工具,可替换Promise.all实现并发提速
        for (const toolCall of response.tool_calls) {
            const foundTool = tools.find(t => t.name === toolCall.name);
            if (foundTool) {
                const toolResult = await foundTool.invoke(toolCall.args)
                messages.push(new ToolMessage({
                    content: toolResult,
                    tool_call_id: toolCall.id
                }))
            }
        }
    }
    // 达到最大循环次数强制退出
    return messages[messages.length - 1].content
}

// 程序入口
try {
    await runAgentWithTools(case1);
} catch (err) {
    console.error(chalk.red(`\n ❌ 运行错误:${err.message}`));
}

// 超时兜底,1000000毫秒强制退出进程
setTimeout(() => {
  console.log("⏰ 超时兜底强制退出进程");
  process.exit(0);
}, 1000000);

之前踩坑核心修复点

  1. response is not defined报错根源 最初代码循环内部没有调用modelWithTools.invoke获取response,直接使用变量,修复后每轮循环先请求大模型拿到返回对象。
  2. 上下文丢失问题 删除循环内重复定义messages数组,仅外层初始化一次,完整保存多轮工具交互历史。
  3. 死循环防护 增加maxIterations=30最大循环限制,防止模型无限调用工具卡死程序。
  4. 彩色日志区分状态 引入chalk区分推理、成功、错误日志,调试更直观。

五、性能优化:Promise.all并发多工具

串行VS并行性能差距

模拟两个IO工具耗时对比:

javascript 复制代码
// 模拟耗时工具
function getFileA() {
    return new Promise((resolve) => setTimeout(()=>resolve("文件A内容"),2000))
}
function getFileB() {
    return new Promise((resolve) => setTimeout(()=>resolve("文件B内容"),500))
}

// 串行:总耗时2000+500=2500ms
const runSerial = async ()=>{
    const a = await getFileA();
    const b = await getFileB();
}
// 并行:总耗时Max(2000,500)=2000ms
const runParallel = async ()=>{
    const [a,b] = await Promise.all([getFileA(),getFileB()]);
}

改造工具执行逻辑,实现并发

把循环串行执行替换为Promise.all批量并发,工具越多性能提升越明显:

javascript 复制代码
// 替换原for循环工具执行代码
console.log(chalk.yellow(`检测到${response.tool_calls.length}个工具调用,并发执行`));
const toolResults = await Promise.all(
    response.tool_calls.map(async (toolCall) => {
        const foundTool = tools.find(t => t.name === toolCall.name);
        if (!foundTool) return `工具不存在:${toolCall.name}`;
        try {
            return await foundTool.invoke(toolCall.args);
        } catch (err) {
            return `工具执行异常:${err.message}`;
        }
    })
);
// 绑定对应tool_call_id存入消息列表
response.tool_calls.forEach((toolCall, idx) => {
    messages.push(new ToolMessage({
        content: toolResults[idx],
        tool_call_id: toolCall.id
    }))
});

并发核心优势

  1. 独立无依赖工具同时启动,总耗时等于最慢工具耗时
  2. 单个工具报错单独捕获,不会中断整批任务
  3. 结果数组顺序与tool_calls一一对应,完美匹配tool_call_id

六、开发全流程高频踩坑提醒

坑1:ERR_MODULE_NOT_FOUND 文件找不到

  • 导入路径后缀必须完整写.mjs,ESM不支持省略后缀
  • 文件名大小写严格匹配,Windows ESM导入区分大小写
  • 确认all-tools.mjs文件真实存在,导出名称和导入完全一致

坑2:workingDirectory与cd混用,目录不存在

错误写法:

javascript 复制代码
command: "cd react-todo-app && pnpm install", workingDirectory: "react-todo-app"

正确写法:

javascript 复制代码
command: "pnpm install", workingDirectory: "react-todo-app"

子进程会自动切换到指定目录,无需手动cd。

坑3:缺少tool_call_id绑定,LLM上下文错乱

所有工具返回结果必须通过ToolMessage绑定tool_call_id,否则模型无法区分哪条结果对应哪个工具调用,逻辑完全混乱。

坑4:串行调用多工具,响应速度极慢

文件读取、命令执行都是IO密集操作,无依赖工具必须使用Promise.all并发;仅工具存在数据依赖时才串行执行。

坑5:未做空值兜底,变量出现undefined

所有可选变量使用三元?: ''、空值合并??兜底字符串,避免后续字符串拼接、打印时报错。

坑6:无限循环调用工具

必须设置最大迭代次数上限,防止模型幻觉持续调用工具,程序卡死。

七、完整执行效果

运行命令:

bash 复制代码
node src/mini-cursor.mjs

Agent全自动完成6大步骤,全程无需人工干预:

  1. 调用execute_command执行pnpm create vite,创建React+TS项目
  2. 调用list_directory校验项目目录生成成功
  3. 调用writeFileTool重写App.tsx,实现Todo完整业务、样式、动画
  4. 调用readFileTool读取生成代码,校验功能完整性
  5. 切换目录执行pnpm install安装项目依赖
  6. 执行pnpm run dev启动开发服务器,输出最终完成总结

八、拓展升级方向

  1. 接入LangGraph:拆分规划Agent、执行Agent,实现多智能体分工
  2. 增加超时控制 :使用Promise.allSettled替代all,单独处理超时卡死工具
  3. 新增更多工具:git操作、npm包查询、代码格式化工具
  4. 记忆持久化:接入Memory模块,支持多轮连续开发对话
  5. MCP第三方工具:接入联网搜索、文档解析能力,拓展AI能力边界

九、总结

本文从零手写了一套迷你Cursor编程Agent,完整覆盖LangChain.js工具调用、ReAct循环、Node子进程、文件IO、并发优化核心知识点。 核心要点回顾:

  1. Agent本质是「大模型推理+工具执行循环」,工具是AI操作本地环境的核心
  2. Promise.all并发多工具是解决Agent卡顿的关键优化手段
  3. 生产级Agent必须包含:参数校验、异常捕获、空值兜底、循环次数限制
  4. workingDirectory是命令行工具规范,禁止在命令内拼接cd切换目录
相关推荐
GuWenyue2 小时前
传统Agent工具两大痛点!300行代码落地MCP跨语言工具,彻底解耦LLM与工具
前端·人工智能·算法
老云讲算力市场2 小时前
WAIC首日观察:国产算力与机器人加速落地,奇点算力迎来产业新机遇
人工智能·科技
小林ixn3 小时前
大模型随机说话的秘密:Temperature 和 Top K 深度解析,LangChain 实战调优
人工智能·langchain
ALINX技术博客3 小时前
ALINX 亮相 2026 WAIC 世界人工智能大会,展示 AI 视觉 FPGA+GPU 异构计算与电子后视镜解决方案
人工智能·ai·fpga·世界人工智能大会·电子后视镜
小林ixn3 小时前
从 onclick 到 React 合成事件,再到完整应用逻辑:一次前端架构的深度剖析
前端·react.js·前端框架
程序员老猫3 小时前
当 AI 能写 80% 的代码时,后端工程师的核心价值还剩什么?
人工智能
想会飞的蒲公英3 小时前
计算机怎样读取中文文本:编码、清洗与标准化
人工智能·python·自然语言处理
CIO_Alliance4 小时前
AI+iPaaS解决方案深度整合:让跨系统业务流程自动化一步到位
人工智能·ipaas·系统集成·ai+ipaas·企业cio联盟·企业级ai化转型
苏州IT威翰德4 小时前
算力资产“续命”专家:苏州威翰德科技赋能NVIDIA高端AI芯片芯片级维修
大数据·人工智能