手写 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`)
}

运行示例

相关推荐
SelectDB8 小时前
易车 × Apache Doris:构建湖仓一体新架构,加速 AI 业务融合实践
大数据·agent·mcp
UIUV8 小时前
Splitter学习笔记(含RAG相关流程与代码实践)
后端·langchain·llm
数据智能老司机9 小时前
亲测!Openclaw高级Skills分享,内含最全Skills教程
agent
雮尘11 小时前
让 AI Agent 高效并行开发的命令-git worktree
人工智能·git·agent
哔哩哔哩技术12 小时前
游戏数据分析Agent的全栈架构演进
人工智能·agent
前端付豪13 小时前
Nest 项目小实践之图书增删改查
前端·node.js·nestjs
zone773913 小时前
005:RAG 入门-LangChain读取表格数据
后端·python·agent
gustt14 小时前
使用 LangChain 构建 AI 代理:自动化创建 React TodoList 应用
人工智能·llm·agent
DevYK17 小时前
🦐 PP-Claw(皮皮虾):用 Go 复刻一个轻量级 AI Agent 全栈解决方案
agent