手写 mini-cursor 完整版:从 Promise 子进程到 ReAct 循环,30 轮迭代完成 TodoList 全流程

手写 mini-cursor 完整版:从 Promise 子进程到 ReAct 循环,30 轮迭代完成 TodoList 全流程

上一版我们搭好了 mini-cursor 的骨架,但 executeCommandTool 还是空的。这一版一切就绪------executeCommandTool 用 Promise 封装 spawn 完整实现,workingDirectory 参数告别"cd 进目录再 cd 出来"的混乱,30 轮 ReAct 循环配合超时兜底,最终让 AI 自主完成一个带渐变背景、动画效果、localStorage 持久化的 React TodoList 应用全流程。


前言

如果你还在纠结"AI 编程 Agent 到底是怎么工作的",展开这个问题------它需要:

  1. 创建一个 Vite + React 项目
  2. 写入完整的 TodoList 组件代码(增删改查、分类筛选、localStorage)
  3. 添加渐变背景、卡片阴影、过渡动画
  4. 安装依赖并启动开发服务器

这已经不是"一句话生成代码"了,这是一个多步骤工程任务 。而 Claude Code、Cursor Agent 能做到的,我们现在用 120 行 Node.js 代码也能做到------一个完整的 ReAct 循环 + 四个工具函数

本文是 mini-cursor 系列的完整版,核心升级:

  • executeCommandTool 完整实现,Promise 包装 spawn 子进程
  • workingDirectory 参数替代错误的 cd 模式
  • 30 轮 ReAct 循环,支持复杂多步骤任务
  • setTimeout 超时兜底,防止进程僵死
  • 完整的 React TodoList 项目实战案例

一、executeCommandTool 完整实现:Promise 包装 spawn

1.1 为什么需要 Promise 包装?

spawn 启动的子进程是事件驱动 的,不是异步函数。Agent 的 tool() 函数要求返回一个值(或 Promise),所以需要用 Promise 包装 spawn 的 close 事件:

javascript 复制代码
const executeCommandTool = tool(
    async ({ command, workingDirectory }) => {
        const cwd = workingDirectory || process.cwd();
        
        return new Promise((resolve, reject) => {
            // 切分命令为 cmd 和 args
            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) {
                    console.log(`[工具调用] execute_command(${command}) 成功执行`);
                    
                    // 重要提示:告知 AI 命令在哪个目录执行
                    const cwdInfo = workingDirectory
                        ? `\n\n重要提示:命令在目录 "${workingDirectory}" 执行`
                        : '';
                    
                    resolve(`命令行成功执行 ${command}${cwdInfo}`);
                } else {
                    console.log(`[工具调用] execute_command(${command}) 退出码:${code}`);
                    resolve(`命令执行失败,退出码:${code}\n错误:${errorMsg}`);
                }
            });
        });
    },
    {
        name: 'execute_command',
        description: '执行系统命令,支持指定工作目录,实时显示输出',
        schema: z.object({
            command: z.string().describe('要执行的命令'),
            workingDirectory: z.string().describe('工作目录(推荐指定)')
        })
    }
);

1.2 Promise 包装的工作流

php 复制代码
Agent 调用 execute_command
    ↓
执行 spawn(cmd, args, { cwd, stdio: 'inherit', shell: true })
    ↓
子进程独立运行(不阻塞主进程)
    ↓
监听 close 事件:子进程结束
    ↓
resolve(结果) → Promise 完成 → 返回给 Agent
    ↓
Agent 继续下一步

1.3 参数名变化:directoryPath → workingDirectory

对比上一版的关键变化:

参数 旧版 新版 原因
参数名 directoryPath workingDirectory 语义更清晰,表明这是"工作目录"而非"目录路径"
描述 工作目录(推荐指定) 工作目录(推荐指定) 不变
行为 自动切换 自动切换 不变

二、workingDirectory 的正确用法:告别 cd 混乱

2.1 错误模式:手动 cd

很多初学 Agent 的人在写 Prompt 时,会这样写命令:

php 复制代码
execute_command({
  command: "cd react-todo-app && pnpm install",
  workingDirectory: "react-todo-app"
})

问题在哪?

bash 复制代码
workingDirectory 已经将工作目录切到了 react-todo-app,
再执行 cd react-todo-app 会出错!

流程:
  父进程在 /home/user/projects
  ↓
  spawn 设置 cwd = /home/user/projects/react-todo-app
  ↓
  子进程执行:cd react-todo-app && pnpm install
  ↓
  cd react-todo-app 找不到目录(已经在 react-todo-app 里了)
  → 命令失败

2.2 正确模式:利用 workingDirectory

bash 复制代码
execute_command({
  command: "pnpm install",
  workingDirectory: "react-todo-app"
})

流程:
  父进程在 /home/user/projects
  ↓
  spawn 设置 cwd = /home/user/projects/react-todo-app
  ↓
  子进程执行:pnpm install
  ↓
  ✅ 直接在 react-todo-app 目录下执行

2.3 SystemMessage 中的规则约束

为了让 LLM 理解这个规则,SystemMessage 中明确写了:

bash 复制代码
重要规则 - 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,直接执行命令即可

这就是 Prompt Engineering + Harness Engineering 的结合------在 Prompt 中注入规则约束,引导 LLM 正确使用工具。


三、完整 ReAct 循环:30 轮迭代 + 超时兜底

3.1 主循环代码

javascript 复制代码
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: 列出目录
        ...`),
        new HumanMessage(query)
    ];

    // ReAct 循环
    for (let i = 0; i < maxIterations; i++) {
        const response = await modelWithTools.invoke(messages);
        messages.push(response);

        // 没有 tool_calls → 任务完成
        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({
                    content: toolResult,
                    tool_call_id: toolCall.id
                }));
            }
        }
    }

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

3.2 循环迭代可视化

php 复制代码
第 1 轮:AI 收到任务 → 思考 → 需要创建项目
  → execute_command({ command: "pnpm create vite ...", workingDirectory: "." })
  → 项目创建成功

第 2 轮:AI 思考 → 需要查看生成的目录结构
  → list_directory({ directoryPath: "react-todo-app/src" })
  → 看到 src/App.tsx 等文件

第 3 轮:AI 思考 → 需要写入 TodoList 组件代码
  → write_file({ filePath: "react-todo-app/src/App.tsx", content: "..." })
  → 写入成功

第 4 轮:AI 思考 → 需要安装依赖
  → execute_command({ command: "pnpm install", workingDirectory: "react-todo-app" })
  → 安装成功

第 5 轮:AI 思考 → 需要启动项目
  → execute_command({ command: "pnpm run dev", workingDirectory: "react-todo-app" })
  → 项目运行在 http://localhost:5173

✅ 任务完成!

3.3 超时兜底机制

javascript 复制代码
try {
    await runAgentWithTools(case1);
} catch (err) {
    console.error(`\n❌ 错误:${err.message}`);
}

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

为什么需要超时兜底?

场景 问题 兜底方案
Agent 陷入死循环 无限轮询,不退出 maxIterations = 30 终止
子进程挂起 npm install 卡住不动 setTimeout 强制退出主进程
API 调用超时 LLM 不响应 兜底 process.exit(0)

setTimeout 的 1000000ms(约 16 分钟):这是一个非常宽松的超时限制,给复杂项目留足时间,但防止进程无限运行。


四、实战:React TodoList 完整项目

4.1 任务描述

javascript 复制代码
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 启动服务器
`

4.2 任务拆解

less 复制代码
任务分解(AI 的 ReAct 推理过程):

Round 1: 创建项目
  Reason: 需要先创建 Vite + React 项目
  Act: execute_command({ command: "echo -e \"n\nn\" | pnpm create vite react-todo-app --template react-ts", workingDirectory: "." })
  Observe: 项目创建成功

Round 2: 查看目录结构
  Reason: 需要知道生成的文件结构,以便修改正确的文件
  Act: list_directory({ directoryPath: "react-todo-app/src" })
  Observe: 看到 App.tsx、main.tsx、index.css 等文件

Round 3: 读取当前代码
  Reason: 需要了解当前 App.tsx 的内容,以便修改
  Act: read_file({ filePath: "react-todo-app/src/App.tsx" })
  Observe: 看到默认模板代码

Round 4: 写入 TodoList 完整代码
  Reason: 需要替换为完整的 TodoList 组件
  Act: write_file({ filePath: "react-todo-app/src/App.tsx", content: "..." })
  Observe: 写入成功

Round 5: 写入样式文件
  Reason: 需要添加渐变背景、卡片阴影、动画
  Act: write_file({ filePath: "react-todo-app/src/App.css", content: "..." })
  Observe: 写入成功

Round 6: 安装依赖
  Reason: 需要安装项目依赖
  Act: execute_command({ command: "pnpm install", workingDirectory: "react-todo-app" })
  Observe: 依赖安装完成

Round 7: 启动项目
  Reason: 需要启动开发服务器
  Act: execute_command({ command: "pnpm run dev", workingDirectory: "react-todo-app" })
  Observe: 项目运行在 http://localhost:5173

✅ 任务完成!

4.3 完整的 TodoList 代码(App.tsx)

当 AI 写入 App.tsx 时,它应该生成类似这样的代码:

tsx 复制代码
import { useState, useEffect } from 'react';
import './App.css';

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

type Filter = 'all' | 'active' | 'completed';

function App() {
  const [todos, setTodos] = useState<Todo[]>(() => {
    const saved = localStorage.getItem('todos');
    return saved ? JSON.parse(saved) : [];
  });
  const [input, setInput] = useState('');
  const [filter, setFilter] = useState<Filter>('all');

  // 持久化到 localStorage
  useEffect(() => {
    localStorage.setItem('todos', JSON.stringify(todos));
  }, [todos]);

  const addTodo = () => {
    if (!input.trim()) return;
    setTodos([...todos, { id: Date.now(), text: input, completed: false }]);
    setInput('');
  };

  const toggleTodo = (id: number) => {
    setTodos(todos.map(t => t.id === id ? { ...t, completed: !t.completed } : t));
  };

  const deleteTodo = (id: number) => {
    setTodos(todos.filter(t => t.id !== id));
  };

  const filteredTodos = todos.filter(t => {
    if (filter === 'active') return !t.completed;
    if (filter === 'completed') return t.completed;
    return true;
  });

  const stats = {
    total: todos.length,
    active: todos.filter(t => !t.completed).length,
    completed: todos.filter(t => t.completed).length,
  };

  return (
    <div className="app">
      <h1>📋 TodoList</h1>
      <div className="input-area">
        <input
          value={input}
          onChange={e => setInput(e.target.value)}
          onKeyDown={e => e.key === 'Enter' && addTodo()}
          placeholder="添加新任务..."
        />
        <button onClick={addTodo}>添加</button>
      </div>
      <div className="filters">
        {(['all', 'active', 'completed'] as Filter[]).map(f => (
          <button
            key={f}
            className={filter === f ? 'active' : ''}
            onClick={() => setFilter(f)}
          >
            {f === 'all' ? '全部' : f === 'active' ? '进行中' : '已完成'}
          </button>
        ))}
      </div>
      <ul className="todo-list">
        {filteredTodos.map(todo => (
          <li key={todo.id} className={`todo-item ${todo.completed ? 'done' : ''}`}>
            <input type="checkbox" checked={todo.completed} onChange={() => toggleTodo(todo.id)} />
            <span className="text">{todo.text}</span>
            <button className="delete" onClick={() => deleteTodo(todo.id)}>✕</button>
          </li>
        ))}
      </ul>
      <div className="stats">
        <span>总计: {stats.total}</span>
        <span>进行中: {stats.active}</span>
        <span>已完成: {stats.completed}</span>
      </div>
    </div>
  );
}

export default App;

对应的 App.css

css 复制代码
.app {
  max-width: 600px;
  margin: 0 auto;
  padding: 2rem;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  min-height: 100vh;
  color: white;
}

h1 { text-align: center; font-size: 2.5rem; }

.input-area {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1.5rem;
}

.input-area input {
  flex: 1;
  padding: 0.75rem;
  border: none;
  border-radius: 8px;
  font-size: 1rem;
}

.input-area button {
  padding: 0.75rem 1.5rem;
  background: #4CAF50;
  color: white;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  transition: transform 0.2s;
}

.input-area button:hover { transform: scale(1.05); }

.filters { display: flex; gap: 0.5rem; margin-bottom: 1rem; }

.filters button {
  padding: 0.5rem 1rem;
  border: 1px solid rgba(255,255,255,0.3);
  background: transparent;
  color: white;
  border-radius: 20px;
  cursor: pointer;
  transition: all 0.3s;
}

.filters button.active { background: white; color: #667eea; }

.todo-list { list-style: none; padding: 0; }

.todo-item {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.75rem;
  background: rgba(255,255,255,0.15);
  border-radius: 8px;
  margin-bottom: 0.5rem;
  transition: all 0.3s;
  animation: slideIn 0.3s ease;
}

.todo-item:hover { transform: translateX(5px); background: rgba(255,255,255,0.25); }

.todo-item.done .text { text-decoration: line-through; opacity: 0.6; }

.todo-item .text { flex: 1; }

.todo-item .delete {
  background: none;
  border: none;
  color: #ff6b6b;
  cursor: pointer;
  font-size: 1.2rem;
  transition: transform 0.2s;
}

.todo-item .delete:hover { transform: scale(1.2); }

.stats {
  display: flex;
  justify-content: space-around;
  margin-top: 1.5rem;
  padding: 1rem;
  background: rgba(255,255,255,0.1);
  border-radius: 8px;
}

@keyframes slideIn {
  from { opacity: 0; transform: translateY(-10px); }
  to { opacity: 1; transform: translateY(0); }
}

五、完整架构图

scss 复制代码
┌──────────────────────────────────────────────────────────────┐
│                    mini-cursor 完整版架构                      │
├──────────────────────────────────────────────────────────────┤
│  mini-cursor.mjs                                              │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  runAgentWithTools(query, maxIterations = 30)        │    │
│  │  ┌────────────────────────────────────────────────┐  │    │
│  │  │  SystemMessage(规则约束)                       │  │    │
│  │  │  + HumanMessage(用户任务)                      │  │    │
│  │  │  ↓                                              │  │    │
│  │  │  ReAct 循环 (for i < maxIterations)             │  │    │
│  │  │  ├── modelWithTools.invoke → 推理               │  │    │
│  │  │  ├── 检测 tool_calls                            │  │    │
│  │  │  ├── 遍历执行工具 → ToolMessage 加入历史         │  │    │
│  │  │  └── 无 tool_calls → 返回结果 ✅                │  │    │
│  │  └────────────────────────────────────────────────┘  │    │
│  │  setTimeout(1000000ms) → 超时兜底                    │    │
│  └──────────────────────────────────────────────────────┘    │
│                    ↓ bindTools                                │
│  all-tools.mjs                                                │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  read_file      → fs.readFile                        │    │
│  │  write_file     → fs.mkdir + fs.writeFile            │    │
│  │  list_directory → fs.readdir                         │    │
│  │  execute_command → spawn + Promise 包装              │    │
│  │                    ├── cwd → workingDirectory       │    │
│  │                    ├── stdio: 'inherit'             │    │
│  │                    └── shell: true                  │    │
│  └──────────────────────────────────────────────────────┘    │
├──────────────────────────────────────────────────────────────┤
│  模型: deepseek-v4-pro(temperature: 0)                      │
│  框架: LangChain(ChatOpenAI + bindTools)                    │
└──────────────────────────────────────────────────────────────┘

知识树

arduino 复制代码
手写 mini-cursor 完整版
├── executeCommandTool(Promise 包装 spawn)
│   ├── command.split(' ') → [cmd, ...args]
│   ├── cwd = workingDirectory || process.cwd()
│   ├── stdio: 'inherit'(实时输出)
│   ├── shell: true(管道/重定向)
│   └── Promise(resolve on close)
├── workingDirectory 正确用法
│   ├── ❌ 错误:command 中写 cd
│   └── ✅ 正确:workingDirectory 自动切换目录
├── ReAct 循环(30 轮迭代)
│   ├── invoke → tool_calls 检测
│   ├── tools.find → tool.invoke
│   ├── ToolMessage 加入历史
│   └── setTimeout 超时兜底
├── 实战:React TodoList 全流程
│   ├── pnpm create vite → 创建项目
│   ├── write_file → 写入 App.tsx + App.css
│   ├── pnpm install → 安装依赖
│   └── pnpm run dev → 启动服务
└── 审查纠正
    ├── fs.readdir 返回值陷阱
    ├── AIMessage 导入说明
    ├── chalk 注释原因
    └── echo -e "n\nn" 技巧

结语

从"骨架版"到"完整版",mini-cursor 的变化体现了 Agent 工程化的演进路径:

  1. 工具要完整executeCommandTool 从空壳到完整实现,Agent 才能真正干活
  2. 参数要合理workingDirectory 替代 cd 模式,减少 Agent 犯错
  3. 循环要有兜底maxIterations + setTimeout,防止 Agent 无限执行
  4. Prompt 要有规则:SystemMessage 中注入约束,引导 LLM 正确使用工具

现在你的 mini-cursor 已经可以完成"创建项目 → 写入代码 → 安装依赖 → 启动服务"的完整流程了。虽然不如 Claude Code 那样强大,但核心原理完全一致------LLM 负责推理,工具负责执行,循环负责推进

100 行代码,一个能自己干活的编程 Agent。这就是 ReAct 循环 + 工具集的力量。


参考与拓展阅读:

  • Node.js 官方文档:child_process.spawn
  • LangChain 官方文档:bindTools 和 Messages
  • 《ReAct 框架深度解析》------ Reason + Act + Observe 循环
  • 《手写 mini-cursor 骨架版》------ 从零搭建三层架构
  • 《从零手写 AI Agent》------ LangChain + Tool 入门

如果本文帮你理解了编程 Agent 从骨架到完整的演进过程,欢迎点赞 + 收藏。动手跑一下你的 mini-cursor,看看它能不能完成 TodoList 项目,欢迎在评论区分享你的执行结果 👇

#AIAgent #Node.js #child_process #编程Agent #LangChain #掘金技术社区

相关推荐
飞猪~2 小时前
Langchain python版本 LLM 重要函数invoke,ainvoke,stream,astream,batch,abatch
python·langchain·batch
大流星5 小时前
LangChainJS之Runnable(三)
javascript·langchain
学渣超5 小时前
微服务日志智能诊断系统(八) 全链路追踪——request_id驱动的跨服务诊断
java·后端·agent
前端Baymax5 小时前
Agent工具调用中无限重试死循环的根因与修复
ai·agent·rag
yeffky5 小时前
Vibe Coding一个能办事的 AI Agent:Spring Boot + DeepSeek + LangGraph4j 六节点 ReAct 项目实战
人工智能·后端·agent
不好听6135 小时前
从零构建一个 AI Coding Agent:不会写代码?让 AI 帮你写
agent
小Z加点糖5 小时前
开源 FlowScript:把 Skill 变成可执行、可检查、可回放的工作流
agent
学渣超5 小时前
微服务日志智能诊断系统(九) 让 LLM 成为日志分析专家
java·后端·agent
想要成为糕糕手5 小时前
从 0 到 1 打造 Coding Agent——mini cursor(终篇):装上命令行手脚,造一个会"干活"的 AI 程序员
langchain·agent·ai编程