手写 Mini Cursor:基于 Node.js 与 LangChain 的开发实战

引言:从 Prompt Engineering 到 Agentic Engineering 的范式转移

如果你一直关注 AI 领域的动态,你一定会发现近期 Agent 产品正在经历一次爆火。

  • 国内大厂相继推出了诸如"千问点奶茶"、豆包、元宝等产品,这标志着互联网计算正在向 AI Agent 推理迈进,这是一个更复杂、更智能、更划时代的产品形态。
  • 在编程领域,OpenClaw 提出了"一人公司"的概念,通过虚拟数字人和多 Agent 协作(如编程、PPT、算账、市场等 Agent),能够自动拆解和计划任务。
  • 我们熟知的 Cursor 就是一个强大的编程 Agent,此外还有 Manus 以及它的开源版本。
  • 甚至在数据分析领域,也有像 seedance 这样的 Agent 用于分析抖音视频数据。

这背后揭示了一个深刻的行业趋势:我们正在从单纯的 LLM Prompt Engineering(提示词工程,如 DeepSeek 的使用),全面走向 Agentic Engineering(智能体工程,需要全栈开发能力)

今天,我们将从零开始,基于 Node.js 和 LangChain 框架,手写一个拥有"手眼"能力的 Mini版 Cursor!

🤖 到底什么是 AI Agent?

很多人觉得 AI Agent 高不可攀,但如果我们剥开它神秘的外衣,它的底层逻辑其实非常清晰。

大模型(LLM)本身就具备思考(Thinking)和规划(Planning)的能力,但它被困在"文本框"里。AI Agent 其实就是给大模型拓展了 Tool(工具)和 Memory(记忆)。

  • 当你赋予它 Tool 时,它就有了"手",可以自动做事情(比如读写文件、执行终端命令)。
  • 当你赋予它 Memory 时,它就有了记忆,能够记住上周你们聊过的消息,记住你想让它记住的东西。
  • 当你赋予它 RAG(检索增强生成)能力时,它就能查询公司内部的私密文档来获取上下文知识,为你解答特定问题。

总结成一个万能公式:

AI Agent = LLM + Memory + Tool + RAG + Prompt Template

在本文中,我们将聚焦于最核心的 LLM + Tool 联动,使用 LangChain 作为我们的 AI Agent 框架来实现全栈开发。

🛠️ 第一步:打好底层地基 ------ 掌握 Node.js 子进程通信

既然我们要让 AI 帮我们写代码、建项目,AI 就必须能够执行终端命令(比如 npm install)。在此之前,我们需要了解 Node.js 是如何执行系统命令的。

在 Node.js 中,我们可以使用内置的 node:child_process 模块的 spawn 方法来创建一个子进程。

  • 进程:操作系统分配资源的最小单位,是一个正在运行的程序实例。
  • 线程:CPU 调度的最小单位,是进程里的执行路径;一个进程可以包含多个线程。

Node.js 是多进程架构的。我们的主进程(比如运行 node node-exec.mjs 的进程)需要启动一个子进程来执行命令,这样命令本身就不会阻塞主进程。

以下是封装执行命令工具的核心逻辑:

JavaScript 复制代码
import { spawn } from 'node:child_process'; // 高级,创建一个子进程

// 作为windows用户,这条命令我们是不可以直接在终端执行的,
// 所以我们需要打开 git bash 来实现这条命令,所以我们后面启动任务时,也是在 git bash 运行
const command = 'ls -la'; 

const [cmd, ...args] = command.split(' '); // 解析命令
const cwd = process.cwd(); // 获取当前工作目录

// 并发启动子进程
const child = spawn(cmd, args, {
    cwd,
    stdio: 'inherit', // 继承父进程的输入输出 stdin stdout,实现在控制台实时打印
    shell: true       // 开启 shell 模式,才能解析命令中的管道、重定向等
});

let errorMsg = '';
// 进程间的通信,基于监听事件
child.on('error', (error) => {
    errorMsg = error.message;
});

child.on('close', (code) => {
    if (code === 0) {
        console.log('命令执行成功,子进程退出');
        process.exit(0);
    } else {
        if (errorMsg) console.error(`错误: ${errorMsg}`);
        process.exit(code || 1);
    }
});

掌握了这个底层的 API,我们的 AI 就拥有了操控操作系统的"遥控器"。

🧰 第二步:封装 LangChain Tools(赋予大模型双手)

在拥有了执行系统命令的能力后,我们需要将其以及其他能力(如读写文件)包装成大模型能够理解的格式。我们将使用 @langchain/core/tools 中的 tool 方法,并结合 zod 进行数据校验。

在本项目中,我们使用 qwen-coder-turbo 作为大模型。LangChain 提供了极大的便利,我们可以通过 pnpm i @langchain/openai 来安装适配常见模型的包。

我们在 all_tools.mjs 中定义了以下四个核心工具:

1. read_file (读取文件)

利用 node:fs/promises 异步读取指定路径的文件内容。

JavaScript 复制代码
const readFileTool = tool(
    async ({ filePath }) => {
        try {
            const content = await fs.readFile(filePath, 'utf-8');
            return `文件内容: \n${content}`;
        } catch (error) {
            return `读取文件失败: ${error.message}`;
        }
    },
    {
        name: 'read_file',
        description: '读取指定路径的文件内容',
        schema: z.object({ filePath: z.string().describe('文件路径') })
    }
);

2. write_file (写入文件)

在写入前,通过 path.dirname 获取目录,并使用 fs.mkdir 配合 recursive: true 选项递归自动创建缺失的目录,防止报错。

JavaScript 复制代码
// 代码省略部分细节,核心逻辑:
const dir = path.dirname(filePath);
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(filePath, content, 'utf-8');

3. list_directory (列出目录)

使用 fs.readdir 获取目录下的所有文件和文件夹,帮助 AI 了解项目结构。

4. execute_command (执行系统命令)

这是最关键的工具!结合我们第一步学到的 spawn 知识,允许 AI 指定 workingDirectory(默认回退到 process.cwd()),并在成功后给予 AI 明确的提示信息,防止 AI 在命令中滥用 cd 导致路径迷失。

🧠 第三步:构建智能体的思考循环 (Agent Loop)

有了工具,怎么让大模型自动调用呢?这就涉及到了 Agent 最核心的工作流设计。

首先,我们要用 LangChain 提供的方法,将工具绑定到模型上:

JavaScript 复制代码
// model 不再孤单,有了工具的陪伴,llm 就可以干活了
const modelWithTools = model.bindTools(tools);

接下来,我们要编写一个循环,让 Agent 不断地 思考 -> 调用工具 -> 获取结果 -> 再次思考。这个循环正是 Agent 更加智能化的核心,它允许 LLM 不断迭代,直到任务完成。

main.mjs 中,我们设计了一个最多执行 30 轮 (maxIterations = 30) 的核心函数 runAgentWithTools

  1. 初始化上下文 :注入 System Message(规定它是一个项目管理助手,告知当前工作路径和可用工具,特别是强调使用 workingDirectory 参数时不要使用 cd 命令)和用户的 Human Message。
  2. 触发思考 :调用 await modelWithTools.invoke(messages) 等待 AI 做出决策。
  3. 判断状态 :如果返回的结果中没有 tool_calls(工具调用队列为空),说明 AI 认为任务已经完成,直接返回内容并跳出循环。
  4. 执行工具 :如果存在 tool_calls,我们就遍历它,去本地 tools 数组中找到对应的工具并执行 foundTool.invoke(toolCall.args)
  5. 回传结果 :将工具执行的结果封装成 ToolMessage 再次推入 messages 数组,形成多轮对话上下文,开启下一轮迭代。

通过这种方式,AI 就可以自主拆解复杂的开发任务了!

💥 第四步:见证奇迹的时刻 ------ 让 AI 写一个 React TodoList

现在,我们的 Mini Cursor 已经组装完毕。让我们给它一个极具挑战性的任务(Prompt):

创建一个功能丰富的 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 install 和 pnpm run dev。

当你运行这个脚本时(我们使用了 chalk 库让控制台输出带有颜色的 ⏳正在等待AI思考... 提示),你会看到令人震撼的自动化过程:

  1. AI 决定调用 execute_command 执行 Vite 脚手架命令。
  2. AI 等待命令成功的反馈后,调用 write_filesrc/App.tsx 写入带有 React 完整逻辑和精美 CSS 的代码。
  3. AI 再次调用 execute_command 安装依赖并启动项目。

全程无需人工干预!这就是 Agentic Engineering 带来的生产力革命。

总结

通过这几百行代码,我们不仅深入理解了 node:child_process 的底层机制,还熟练运用了 LangChain 的 toolbindTools 特性,最终手写出了一个能够自主执行系统命令、编写文件的 Mini Cursor 工具。这不再是简单的"问答机器人",而是一个真正能落地的生产力智能体。下面我会贴上我的源码和运行示例,关于langchain的包安装和node初始化项目的命令我就不再赘述了,不懂的可以自己查阅。

源码

  1. all_tools.mjs
js 复制代码
// langchain tool 工具
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';

// 读取文件工具
const readFileTool = tool(
    async ({ filePath }) => {
        try {
            const content = await fs.readFile(filePath, 'utf-8');
            console.log(`[工具调用] read_file("${filePath}") 成功读取 ${content.length} 个字符`);
            return `文件内容: \n${content}`;
        } catch (error) {
            console.error(`[工具调用] read_file("${filePath}") 读取文件失败: ${error.message}`);
            return `读取文件失败: ${error.message}`;
        }
    },
    {
        name: 'read_file',
        description: '读取指定路径的文件内容',
        schema: z.object({
            filePath: z.string().describe('文件路径')
        })
    }
)

// 写入文件工具
const writeFileTool = tool(
    async ({ filePath, content }) => {
        try {
            // 如果 filePath 是 /a/b/c.txt 则返回 /a/b 目录
            const dir = path.dirname(filePath);
            // make directory 创建目录,recursive: true 递归创建
            await fs.mkdir(dir, { recursive: true });
            await fs.writeFile(filePath, content, 'utf-8');
            console.log(`[工具调用] write_file("${filePath}") 成功写入 ${content.length} 个字符`);
            return `文件写入成功: ${filePath}`;
        } catch (error) {
            console.error(`[工具调用] write_file("${filePath}") 写入文件失败: ${error.message}`);
            return `写入文件失败: ${error.message}`;
        }
    },
    {
        name: 'write_file',
        description: '向指定路径写入文件内容,自动创建目录',
        schema: z.object({
            filePath: z.string().describe('文件路径'),
            content: z.string().describe('要写入的文件内容')
        })
    }
)

// 执行命令工具
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;
                reject(errorMsg);
            })
            child.on('close', (code) => {
                if (code === 0) {
                    console.log(`[工具调用] execute_command("${command}") 执行成功`);
                    const cwdInfo = workingDirectory ? `
                        \n\n重要提示: 命令在目录"${workingDirectory}"中执行成功。
                        如果需要在这个目录下继续执行命令,请使用 workingDirectory
                        "${workingDirectory}" 参数,不要使用 cd 命令
                    ` : '';
                    resolve(`命令执行成功: ${command} ${cwdInfo}`);
                } else {
                    if (errorMsg) {
                        console.error(`错误: ${errorMsg}`);
                    }
                    console.error(`[工具调用] execute_command("${command}") 执行失败,退出码: ${code}`);
                    reject(`命令执行失败: ${command},退出码: ${code}`);
                }
            })
        })
    },
    {
        name: "execute_command",
        description: '执行系统命令,支持指定工作目录,实时显示输出',
        schema: z.object({
            command: z.string().describe('要执行的命令'),
            workingDirectory: z.string().optional().describe('指定工作目录,默认当前工作目录')
        })
    }
)

// 列出目录工具
const listDirectoryTool = tool(
    async({ directoryPath }) => {
        try {
            const files = await fs.readdir(directoryPath);
            console.log(`[工具调用] list_directory("${directoryPath}") 成功列出 ${files.length} 个文件`);
            return `目录内容: \n ${files.map(f => `- ${f}`).join('\n')}`;
        } catch(err) {
            console.error(`[工具调用] list_directory("${directoryPath}") 列出目录失败: ${err.message}`);
            return `列出目录失败: ${err.message}`;
        }
    },
    {
        name: "list_directory",
        description: '列出指定目录下的所有文件和文件夹',
        schema: z.object({
            directoryPath: z.string().describe('目录路径')
        })
    }
)

export {
    readFileTool,
    writeFileTool,
    executeCommandTool,
    listDirectoryTool
}
  1. main.mjs
js 复制代码
import {
    readFileTool,
    writeFileTool,
    executeCommandTool,
    listDirectoryTool
} from './all_tools.mjs';
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import {
    HumanMessage,
    SystemMessage,
    ToolMessage
} from '@langchain/core/messages';
import chalk from 'chalk'; // 引入 chalk 库,用于控制台输出颜色

const model = new ChatOpenAI({
    modelName: process.env.OPENAI_API_MODEL_NAME,
    apiKey: process.env.OPENAI_API_KEY,
    temperature: 0,
    configuration: {
        baseURL: process.env.OPENAI_API_BASE_URL
    }
});

const tools = [
    readFileTool,
    writeFileTool,
    executeCommandTool,
    listDirectoryTool
]

// modelWithTools 绑定了工具,llm 就可以调用工具了
const modelWithTools = model.bindTools(tools);

// web 4.0 AI earn money
// 最大迭代次数(最多循环多少轮)
// 最多给你 30 次调用工具的机会完成任务。
async function runAgentWithTools(query, maxIterations = 30) {
    // 检测任务完成情况
    // 不再调用tools说明任务完成,还在调用 tools 说明 llm 还在思考,每一轮都会将工具返回的内容加入上下文
    const messages = [
        new SystemMessage(`
            你是一个项目管理助手,使用工具完成任务。
            当前工作目录: ${process.cwd()}

            工具:
            1. read_file: 读取文件
            2. write_file: 写入文件
            3. execute_command: 执行命令 (支持workingDirectory 参数)
            4. list_directory: 列出目录

            重要规则 -execute_command:
            - workingDirectory 参数会自动切换为指定目录
            - 当使用 workingDirectory 参数时,不要在 command 中使用 cd 命令
            - 错误示例: { command: "cd react-todo-app && pnpm install", workingDirectory: "react-todo-app" }
            这是错误的!因为 workingDirectory 已经在 react-todo-app 目录了,再 cd react-todo-app 会找不到目录
            - 正确示例: { command: "pnpm install", workingDirectory: "react-todo-app" }
            这样就对了!workingDirectory 已经切换到 react-todo-app,直接执行命令即可

            回复要简洁,只说做了什么
        `),
        new HumanMessage(query),
    ];
    // 循环是 agent 的核心,让 llm 思考,规划,调整,不断迭代,直到任务完成。更加智能化
    for (let i = 0; i < maxIterations; i++) {

        console.log(chalk.bgGreen('⏳正在等待AI思考...'));

        const response = await modelWithTools.invoke(messages);

        // 把 AI 回复加入上下文
        messages.push(response);

        // 如果没有 tool 调用,说明任务完成
        if (!response.tool_calls || response.tool_calls.length === 0) {
            console.log(`\n AI 最终回复:\n ${response.content}\n`);
            return response.content;
        }

        // 执行工具
        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({
                        tool_call_id: toolCall.id,
                        content: toolResult
                    })
                );
            }
        }
    }
    return messages[messages.length - 1].content;
}

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 启动服务器
`
try {
    await runAgentWithTools(case1);
} catch (err) {
    console.error(`\n错误: ${err.message}\n`)
}

运行示例

相关推荐
奔跑中的小象16 分钟前
统信UOS + 天数AI卡部署SGLang服务手册
人工智能·uos·sglang·天数智芯
DevSecOps选型指南19 分钟前
中国版Mythos,为何是悬镜安全灵脉CodeAI?
人工智能·安全
Shockang30 分钟前
LangGraph 状态机实战
人工智能
刹那芳华199242 分钟前
循环神经网络的从零开始实现(RNN)
人工智能·rnn·深度学习
阿童木写作1 小时前
跨境图片翻译工具多合一,批量图片视频字幕翻译加智能抠图
人工智能·python·音视频·语音识别
科技之门1 小时前
AI3D从建模到贴图、绑骨和动画的完整流程怎么做?V2Fun完整工作流指南
人工智能·3d·贴图
宇宙第一小趴菜1 小时前
二、机器学习的应用领域和发展史
人工智能·机器学习
前端开发江鸟1 小时前
我写过 MCP Server,却一直以为 MCP 只有 Tool
人工智能
天国梦1 小时前
自习室智能化升级避坑指南:天学网AI智习室方案实测与选型建议
大数据·人工智能
阿里云云原生2 小时前
可用性从 99.9% 跃升至 99.995%:畅捷通如何用 AI 重塑运维底座?
运维·网络·人工智能