200 行代码复刻 Cursor 核心:手把手写一个 Coding Agent 🔧

手写 Mini-Cursor:200 行代码打造你的专属编程 Agent 🚀

前言

Cursor、Claude Code、Trae......这些 AI 编程工具背后的核心技术都一样------Coding Agent

你有没有好奇过:一个 AI 是怎么做到"自动创建项目、写代码、运行起来"的?它背后到底是什么机制?

这篇文章带你用 200 行代码手写一个 Mini-Cursor------它会自动用 Vite 创建 React 项目、编写完整的 TodoList 代码、安装依赖、启动开发服务器。全程不需要你写一行 React 代码!

📌 读完你会收获:

  • 完整 Coding Agent 的架构设计
  • 4 大核心工具(读/写/列目录/执行命令)的实现细节
  • Node.js 子进程 spawn 在 Agent 中的实战应用
  • Promise.all 并发优化的真实案例
  • 一个真正能跑起来的编程助手

一、先看看它能做什么

假设你对 Mini-Cursor 说:

"用 Vite 创建一个 React TodoList 项目,要有完整功能、美观样式、动画效果,然后把它跑起来。"

它会自己做完下面这些事:

arduino 复制代码
Agent 的 Planning(由 LLM 自动推理):
┌─────────────────────────────────────────┐
│ 步骤1:调用 execute_command             │
│   → pnpm create vite react-todo-app     │
│                                         │
│ 步骤2:调用 write_file                  │
│   → 写入完整 TodoList 组件代码           │
│                                         │
│ 步骤3:调用 execute_command             │
│   → pnpm install(安装依赖)            │
│                                         │
│ 步骤4:调用 execute_command             │
│   → pnpm run dev(启动开发服务器)       │
│                                         │
│ 步骤5:调用 list_directory              │
│   → 确认项目文件结构正确                │
└─────────────────────────────────────────┘

全程自动,你只需要看着它干活。


二、架构设计:一个 Coding Agent 需要哪些"器官"?

2.1 核心公式

markdown 复制代码
Coding Agent = LLM(大脑)
             + fs 工具(读写文件的手)
             + cli 工具(执行命令的手)
             + ReAct 循环(决策→执行→观察→决策)

2.2 工具矩阵

一个能编程的 Agent 至少需要这 4 个工具:

arduino 复制代码
┌──────────────────┬────────────────────────────────┐
│      工具         │           能力                  │
├──────────────────┼────────────────────────────────┤
│  read_file       │  读取文件,理解已有代码           │
│  write_file      │  创建/覆写文件,生成代码          │
│  list_directory  │  列出目录,确认项目结构           │
│  execute_command │  执行 cli 命令,真正"动手"       │
└──────────────────┴────────────────────────────────┘

🎯 这四个工具正好对应一个人类程序员的基本操作:看代码、写代码、看目录、跑命令。Agent 就是让 LLM 掌握了这四个"动作"。


三、工具实现:给 LLM 装上"手脚"

3.1 文件读写工具

这三个工具比较直观,我们用 Node.js 的 fs/promises 模块:

js 复制代码
// all-tools.mjs(节选)
import { tool } from '@langchain/core/tools';
import fs from 'node:fs/promises';
import path from 'node:path';
import { z } from 'zod';

// 📖 读文件
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('要读取的文件路径')
        })
    }
);

// ✍️ 写文件
const writeFileTool = tool(
    async ({ filePath, content }) => {
        // 🔑 自动创建目录(递归),不用担心路径不存在
        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}`;
    },
    {
        name: 'write_file',
        description: '向指定路径写入文件内容,自动创建目录',
        schema: z.object({
            filePath: z.string().describe('文件路径'),
            content: z.string().describe('要写入的文件内容')
        })
    }
);

🧠 知识点:path.dirname + fs.mkdir({ recursive: true })

写文件时,如果目录不存在会报错。用 path.dirname(filePath) 提取目录路径,再用 fs.mkdir(dir, { recursive: true }) 递归创建,无论嵌套多深都能自动建好。这是 Agent 工具"鲁棒性"的关键------LLM 可能随便给一个路径,工具要能兜底。

3.2 列出目录工具

js 复制代码
// 📂 列出目录
const listDirectoryTool = tool(
    async ({ directoryPath }) => {
        const files = await fs.readdir(directoryPath);
        console.log(`[工具调用] list_directory(${directoryPath}) 成功列出 ${files.length} 项`);
        return `目录内容:\n ${files.map(file => file).join('\n')}`;
    },
    {
        name: 'list_directory',
        description: '列出指定目录下的所有文件和文件夹',
        schema: z.object({
            directoryPath: z.string().describe('目录路径')
        })
    }
);

3.3 执行命令工具------Agent 的"终极武器"

这是整个 Coding Agent 最有技术含量的工具。它需要解决一个问题:

Node.js 主进程(Agent)如何安全地执行外部命令并获取结果?

答案:子进程(child_process)

scss 复制代码
┌─────────────────┐     spawn()      ┌─────────────────┐
│   Node 主进程    │ ──────────────→  │    子进程        │
│   (Agent)       │                  │  (执行 cli 命令)  │
│                 │ ←── IPC 通信 ─── │                 │
│  单线程,不能    │                  │  隔离执行,       │
│  被阻塞          │                  │  不影响主进程      │
└─────────────────┘                  └─────────────────┘

为什么要用子进程?

Node.js 是单线程的。如果直接在主进程跑 npm create vite(一个可能耗时几十秒的命令),整个 Agent 就卡住了。 子进程把命令执行"分离"出去,主进程可以继续做其他事,等子进程完成后通过 IPC(进程间通信)通知主进程。

js 复制代码
// 🔧 执行命令工具(带实时输出)
import { spawn } from 'node:child_process';

const executeCommandTool = tool(
    async ({ command, workingDirectory }) => {
        const cwd = workingDirectory || process.cwd();
        console.log(`[工具调用] execute_command(${command}) 工作目录:${cwd}`);

        return new Promise((resolve, reject) => {
            // 🔑 关键:拆分命令和参数
            // "pnpm create vite react-todo-app --template react-ts"
            // → cmd = "pnpm", args = ["create", "vite", "react-todo-app", "--template", "react-ts"]
            const [cmd, ...args] = command.split(' ');

            const child = spawn(cmd, args, {
                cwd,                    // 指定工作目录
                stdio: 'inherit',       // 继承父进程的输入输出(实时看到命令输出)
                shell: true,            // 支持 shell 语法
            });

            let errorMsg = '';

            child.on('error', (err) => {
                errorMsg = err.message;
            });

            child.on('close', (code) => {
                if (code === 0) {
                    // ✅ 退出码 0 = 成功
                    resolve(`命令执行成功:${command}`);
                } else {
                    // ❌ 非 0 退出码 = 失败
                    resolve(`命令执行失败,退出码:${code}\n错误:${errorMsg}`);
                }
            });
        });
    },
    {
        name: 'execute_command',
        description: '执行系统命令,支持指定工作目录,实时显示输出',
        schema: z.object({
            command: z.string().describe('要执行的命令'),
            workingDirectory: z.string().describe('工作目录(推荐指定)')
        })
    }
);

🧠 知识点:spawn 的四个关键参数

参数 作用 不设置的后果
cwd 指定子进程的工作目录 命令在当前目录执行,可能找不到项目文件夹
stdio: 'inherit' 子进程的输出直接显示在控制台 看不到 npm install 的进度,Agent 像"黑盒"
shell: true 启用 shell 模式解析命令 无法使用管道、重定向等 shell 语法
Promise 包装 把事件驱动的子进程变成 async/await 时序混乱,不知道命令什么时候执行完

🧠 知识点:退出码(Exit Code)

  • 0 = 命令成功执行
  • 0 = 出错了(如 1 一般错误,127 命令未找到)
  • Agent 不需要"猜"命令是否成功------看退出码就行

3.4 一个重要的细节:workingDirectory 的正确用法

在 System Prompt 里我们特别强调了一个容易踩坑的点:

js 复制代码
// ❌ 错误用法:workingDirectory 已经切换到 react-todo-app 了,命令里又 cd 一次
{
    command: "cd react-todo-app && pnpm install",
    workingDirectory: "react-todo-app"
}
// 实际执行路径:react-todo-app/react-todo-app/ ------ 不存在!

// ✅ 正确用法:workingDirectory 负责切换目录,命令只写要做的事
{
    command: "pnpm install",
    workingDirectory: "react-todo-app"
}
// 实际执行路径:react-todo-app/ ------ 完美!

🎯 这个细节如果不写明在 System Prompt 里,LLM 很容易犯"双重 cd"的错误------因为人类也会这样写。这也是 System Prompt 工程的一部分。


四、ReAct 循环:Agent 的大脑运转机制

有了工具,下一步是让 LLM 知道什么时候用哪个工具、用完之后怎么继续。这就是 ReAct 循环。

scss 复制代码
         ┌──────────────────────────────────────┐
         │                                      │
         ▼                                      │
   ┌───────────┐   有 tool_calls    ┌─────────┐  │
   │  LLM 推理  │ ───────────────→  │ 执行工具  │  │
   │ (Reason)  │                   │  (Act)   │  │
   └───────────┘                   └─────────┘  │
         ▲                              │       │
         │                              ▼       │
         │   无 tool_calls         ┌─────────┐  │
         └── 直接输出结果 ←────────│ 观察结果  │──┘
                                   │(Observe) │
                                   └─────────┘

4.1 完整代码实现

js 复制代码
// mini-cursor.mjs(核心部分)
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 参数会自动切换到指定目录
            - 绝对不要在 command 中使用 cd
        `),
        new HumanMessage(query)
    ];

    // 🔁 ReAct 循环
    for (let i = 0; i < maxIterations; i++) {
        console.log(`正在等待第 ${i} 次 AI 思考...`);

        // Reason:LLM 推理,决定下一步
        const response = await modelWithTools.invoke(messages);
        messages.push(response);

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

        // Act:执行 LLM 请求的所有工具
        for (const toolCall of response.tool_calls) {
            const foundTool = tools.find(t => t.name === toolCall.name);
            if (foundTool) {
                // Observe:工具执行结果
                const toolResult = await foundTool.invoke(toolCall.args);

                // 把结果以 ToolMessage 格式加入对话
                messages.push(new ToolMessage({
                    content: toolResult,
                    tool_call_id: toolCall.id  // ← id 关联!
                }));
            }
        }
    }

    return messages[messages.length - 1].content;
}

4.2 为什么 ToolMessage 必须带 tool_call_id

想象这样一个场景:LLM 一次请求调用了 3 个工具,返回了 3 个结果。如果没有 tool_call_id,LLM 就不知道"结果A 对应 工具1"还是"结果A 对应 工具3"。

ini 复制代码
              tool_call_id = "call_abc"
read_file ─────────────────────────────→ ToolMessage("文件内容...", id="call_abc")
              tool_call_id = "call_def"
write_file ────────────────────────────→ ToolMessage("写入成功", id="call_def")
              tool_call_id = "call_ghi"
exec_cmd ──────────────────────────────→ ToolMessage("命令成功", id="call_ghi")

🎯 tool_call_id 就是工具调用和结果之间的"快递单号"------LLM 靠它来把结果和请求对上号。


五、并发执行:让 Agent 更快

5.1 来自实际项目的教训

tool.mjs 中的这段代码:

js 复制代码
// ⚡ 并发执行所有工具调用
const toolResults = await Promise.all(
    response.tool_calls.map(async (toolCall) => {
        const tool = tools.find(t => t.name === toolCall.name);
        if (!tool) {
            return `错误:工具 ${toolCall.name} 不存在`;
        }
        try {
            const result = await tool.invoke(toolCall.args);
            return result;
        } catch (error) {
            return `错误:${error.message}`;
        }
    })
);

为什么用 Promise.all 而不是 for...of + await

假设 LLM 一次返回了 3 个工具调用(读 A 文件、读 B 文件、列出 C 目录),它们之间互不依赖:

css 复制代码
串行(for + await):                并行(Promise.all):
┌─────────┐                          ┌─────────┐
│ 读 A 文件 │  500ms                  │ 读 A 文件 │
└─────────┘                          │ 读 B 文件 │  ← 同时起飞!
┌─────────┐                          │ 列 C 目录 │
│ 读 B 文件 │  500ms                  └─────────┘
└─────────┘                          总耗时 ≈ 500ms(取最慢的)
┌─────────┐                               vs
│ 列 C 目录 │  300ms
└─────────┘
总耗时 = 1300ms(累加)

5.2 Promise 的核心概念回顾

项目中的 1.html 用了一个极简 demo 来解释 Promise:

js 复制代码
function getWeather() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve({ temp: 38, conditions: 'Sunny with Clouds' })
        }, 2000)  // 2秒后返回
    })
}

function getTweets() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(['I like cake', 'BBQ is good too!'])
        }, 500)   // 0.5秒后返回
    })
}

// ❌ 串行:2s + 0.5s = 2.5s
const weather = await getWeather();
const tweets = await getTweets();

// ✅ 并行:max(2s, 0.5s) = 2s
const [weather, tweets] = await Promise.all([getWeather(), getTweets()]);

Promise 三态转换(不可逆):

scss 复制代码
         ┌── resolve() → fulfilled(成功)
Pending ─┤
         └── reject()  → rejected(失败)

一旦从 Pending 变成 fulfilled/rejected,状态就锁定了,不能再变。

🎯 Agent 并发执行最佳实践 :当 LLM 返回的多个 tool_calls 之间没有依赖关系时(读两个文件、查两个 API、列两个目录),用 Promise.all 并行执行,性能提升显著。当有依赖时(先创建文件再读它),串行执行。


六、完整任务拆解:看 Mini-Cursor 一次完整的执行

6.1 任务描述

我们给 Mini-Cursor 的任务是:

markdown 复制代码
创建一个功能丰富的 React TodoList 应用:
1. 创建项目:pnpm create vite react-todo-app --template react-ts
2. 修改 src/App.tsx,实现完整功能:
   - 添加、删除、标记完成
   - 分类筛选(全部/进行中/已完成)
   - 统计信息显示
   - localStorage 数据持久化
3. 添加复杂样式:渐变背景、卡片阴影、圆角、悬停效果
4. 添加动画:CSS transitions
5. 列出目录确认

之后 react-todo-app 项目中:
1. pnpm install 安装依赖
2. pnpm run dev 启动服务器

6.2 LLM 的实际执行过程

css 复制代码
🔄 第 0 次 AI 思考...
   LLM:我需要先创建项目
   → tool_calls: [{ name: "execute_command", args: { command: "pnpm create vite react-todo-app --template react-ts" } }]
   → 执行:项目创建成功 ✅

🔄 第 1 次 AI 思考...
   LLM:项目创建好了,现在写入 TodoList 代码
   → tool_calls: [{ name: "write_file", args: { filePath: "react-todo-app/src/App.tsx", content: "..." } }]
   → 执行:文件写入成功 ✅

🔄 第 2 次 AI 思考...
   LLM:还需要写 CSS
   → tool_calls: [{ name: "write_file", args: { filePath: "react-todo-app/src/App.css", content: "..." } }]
   → 执行:CSS 写入成功 ✅

🔄 第 3 次 AI 思考...
   LLM:列出目录确认文件都在
   → tool_calls: [{ name: "list_directory", args: { directoryPath: "react-todo-app/src" } }]
   → 执行:确认文件结构 ✅

🔄 第 4 次 AI 思考...
   LLM:安装依赖
   → tool_calls: [{ name: "execute_command", args: { command: "pnpm install", workingDirectory: "react-todo-app" } }]
   → 执行:依赖安装完成 ✅

🔄 第 5 次 AI 思考...
   LLM:启动开发服务器
   → tool_calls: [{ name: "execute_command", args: { command: "pnpm run dev", workingDirectory: "react-todo-app" } }]
   → 执行:开发服务器启动 ✅

🔄 第 6 次 AI 思考...
   LLM:所有任务完成!
   → tool_calls: []  ← 没有工具调用了
   → 输出:"TodoList 项目已创建完成并启动..." 🎉

七、最终效果:Agent 生成的 TodoList

Mini-Cursor 生成的 App.tsx 包含了:

  • 完整的状态管理useState + useEffect + useCallback
  • localStorage 持久化:刷新不丢数据
  • 三态筛选:全部 / 进行中 / 已完成
  • 统计面板:总计、进行中、已完成数量
  • 动画效果:添加/删除时的 CSS 过渡动画
  • TypeScript:完整的类型定义

这说明 LLM 生成的代码质量已经相当高了------不仅能跑,而且代码结构清晰、类型完备。


八、体系化知识总结

8.1 四个层次的能力

rust 复制代码
┌──────────────────────────────────────────────────────┐
│ 第一层:LLM 基础调用                                  │
│   model.invoke("问题") → 回答                         │
│   只能聊天,不能干活                                   │
├──────────────────────────────────────────────────────┤
│ 第二层:工具定义 + bindTools                          │
│   tool(async fn + zod schema + description)          │
│   model.bindTools(tools)                             │
│   LLM 知道"我能用什么",但只会一轮                     │
├──────────────────────────────────────────────────────┤
│ 第三层:ReAct 循环                                    │
│   invoke → tool_calls → execute → ToolMessage →      │
│   invoke → ... → 无 tool_calls → 返回结果             │
│   LLM 能在多轮中持续决策直到任务完成                    │
├──────────────────────────────────────────────────────┤
│ 第四层:完整 Coding Agent                             │
│   LLM + 4 个工具 + ReAct + 子进程 + 并发优化           │
│   能独立完成:创建项目 → 写代码 → 安装依赖 → 启动服务   │
└──────────────────────────────────────────────────────┘

8.2 知识点速查表

概念 一句话解释 在代码中的位置
tool() LangChain 的工具定义函数,分处理函数 + 说明书两部分 all-tools.mjs
description 写给 LLM 看的工具使用说明,决定 LLM 何时调用 每个 tool 的第二个参数
schema (Zod) 参数校验规则,LLM 必须按要求提供参数 z.object({...})
bindTools() 把工具注册给 LLM,让 LLM "知道"自己能干什么 model.bindTools(tools)
tool_calls LLM 返回的工具调用请求列表 response.tool_calls
tool_call_id 工具调用的唯一 ID,关联请求和结果 ToolMessage({tool_call_id})
spawn 创建子进程执行命令,不阻塞主进程 execute_command
stdio: 'inherit' 子进程输出直接显示在控制台 spawn 的第三个参数
退出码 0=成功,非 0=失败 child.on('close', code)
Promise.all 并行执行多个异步任务 工具并发执行
ReAct 循环 Reason → Act → Observe 的不断循环 runAgentWithTools 的 for 循环
maxIterations 防止 Agent 死循环的安全阀 for (let i = 0; i < maxIterations; i++)
temperature: 0 LLM 输出更稳定、更可预测 new ChatOpenAI({ temperature: 0 })

8.3 Agent 开发的黄金法则

  1. 工具描述就是 LLM 的"使用说明书"------写得越具体,LLM 用工具越准
  2. System Prompt 是 Agent 的行为准则------规则、边界、坑点都要写清楚
  3. 始终有 maxIterations 兜底------防止 Agent 陷入死循环
  4. 子进程 ≠ 主进程 ------命令行任务一定要用 spawn 隔离执行
  5. 能并行就并行 ------多个不相关的工具调用用 Promise.all
  6. tool_call_id 是"快递单号"------别漏了,否则 LLM 不知道结果对应哪个请求

九、延伸思考:从 Mini-Cursor 到真正的 Cursor

我们手写的 Mini-Cursor 只有 200 行代码,但它已经具备了 Cursor/Claude Code 的核心骨架。真正的 Coding Agent 在此基础上加了什么?

sql 复制代码
Mini-Cursor(200行)        真正的 Coding Agent
─────────────────────      ─────────────────────
4 个工具                     几十个工具(git、LSP、浏览器...)
单 Agent                     多 Agent 协作
无记忆                       Memory 模块(长期记忆)
只有对话历史                  向量检索(RAG)、上下文窗口管理
无权限控制                   沙箱隔离、权限分级
ReAct 基础循环               复杂工作流编排(plan → execute → verify)

🎯 但核心思想完全一样:LLM + Tool + 循环 = Agent。理解了 Mini-Cursor,你就理解了所有 Coding Agent 的本质。


如果这篇文章帮你搞懂了 Coding Agent 的实现原理,欢迎点赞、收藏、关注!把 mini-cursor.mjs 跑起来,你会更直观地感受到 Agent 的魔力 👏


相关推荐
qq_454245031 小时前
状态的演进:从混沌单体到边界清晰的模块化格局
架构
跨境数据猎手1 小时前
多平台电商比价系统从零搭建合规
开发语言·爬虫·架构
用户1040747270231 小时前
《架构特别篇三:DEVICES 层》
架构
捷配链1 小时前
新能源四层板叠层逻辑-最优架构适配电控高低压混合场景
架构·制造·pcb
AustinXu1 小时前
进化搜索:Harness Engineering 之后,Agent 怎么自己进化自己
架构·claude·敏捷开发
Token炼金师2 小时前
工程基础设施层:MLOps闭环断裂让模型上线即裸奔
人工智能·深度学习·llm
柒和远方2 小时前
Worker Readiness:为什么 `/health` 不能证明后台任务能跑
前端·后端·架构
万亿少女的梦1682 小时前
基于深度学习的动物图像分类系统设计与实现
pytorch·深度学习·计算机视觉·图像分类·convnext