一个前端开发者花三个晚上,用 500 行代码教会 AI 自主编程的故事。
你有没有想过,Cursor 是怎么工作的?你给它一句话,它就能自动创建项目、写代码、装依赖、启动服务------整个过程你只需看着。
好奇心驱使下,我决定把这个过程"拆开看看"。不调用任何现成的 Agent 框架,只用最基础的 LangChain API,从零搭建一个最小可用的 AI 编程助手。
最终效果:在终端敲一行命令,Agent 就自动完成了创建 React 项目、编写组件、安装依赖、启动开发服务器的全流程。
css
$ node src/main.js "用 pnpm 创建 todo 应用并启动"
🚀 Agent 启动
━━━ 第 1/15 轮 ━━━
🔧 模型请求调用 execute_command:
⚡ pnpm create vite react-todo-app --template react-ts (3.2s) ✅
━━━ 第 2/15 轮 ━━━
🔧 模型请求调用 write_file:
✔ react-todo-app/src/App.tsx → 3847 字符 ✅
...(几轮后)
🤖 Agent 最终回复:任务完成,服务器已启动
这篇文章记录了我从零构建这个项目的完整过程,以及踩过的每一个坑。
一、核心原理:ReAct 模式
Agent 的核心不是"让 AI 写代码",而是让 AI 在思考和行动之间循环。
1.1 不是一次性生成
普通的 ChatGPT 对话是一次性的:你问,它答。
AI Coding Agent 不同。任务通常需要多步操作------创建项目 → 写文件 → 装依赖 → 启动服务 → 检查报错 → 修 bug → ...。每步的结果都可能影响下一步的决策。
这就需要一种循环机制:ReAct(Reasoning + Acting)。
markdown
用户任务
↓
模型思考 → 决定:调用 execute_command
↓
命令执行 → 返回结果:命令成功
↓
模型再思考 → 决定:调用 write_file
↓
文件写入 → 返回结果:写入成功
↓
模型继续思考 → 任务完成,返回总结文本
1.2 用代码实现这个循环
js
// 这是整个 Agent 的核心------只用了 30 行代码
for (let i = 0; i < maxIterations; i++) {
// 1. 把到目前为止的对话历史发给模型
const response = await model.invoke(messages);
// 2. 模型返回纯文本?任务完成
if (!response.tool_calls || response.tool_calls.length === 0) {
return response.content;
}
// 3. 模型想调用工具?逐个执行
for (const toolCall of response.tool_calls) {
const tool = tools.find(t => t.name === toolCall.name);
const result = await tool.invoke(toolCall.args);
// 4. 把执行结果作为新消息加入历史
messages.push(new ToolMessage({
content: result,
tool_call_id: toolCall.id,
}));
}
// 回到第 1 步,继续循环
}
这就是 ReAct 的全部。没有魔法,就是一个 while 循环。
1.3 终止条件
循环什么时候停?
- 模型返回纯文本,没有 tool_calls → 任务完成,输出结果
- 达到最大迭代次数(如 15 轮)→ 强制结束,防止死循环
二、给 AI 装上手脚:工具系统
光有"大脑"不够,还得让它能干活。我实现了四个工具:
| 工具 | 作用 | 关键实现 |
|---|---|---|
read_file |
读取文件内容 | fs.readFile(path, 'utf8') |
write_file |
写入文件(自动建目录) | fs.mkdir(dir, {recursive}) + fs.writeFile() |
list_directory |
列出目录内容 | fs.readdir(path) |
exec_command |
执行终端命令 | spawn(cmd, args, {shell, stdio: 'inherit'}) |
每个工具都使用 LangChain 的 tool() 函数定义,Zod v4 做参数校验:
js
import { tool } from '@langchain/core/tools'
import { z } from 'zod'
export const writeFileTool = tool(
async ({ filePath, content }) => {
const dir = path.dirname(filePath);
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(filePath, content, 'utf8');
return `成功写入 ${filePath}`;
},
{
name: 'write_file',
description: '向指定路径写入文件,自动创建父目录',
schema: z.object({
filePath: z.string().describe('文件路径'),
content: z.string().describe('文件内容'),
})
}
)
LangChain 会把 Zod schema 转成 JSON Schema 发给模型,模型就知道这个工具需要什么参数了。
三、最难搞的部分:让 Agent 执行命令
write_file 和 read_file 都好说,Node.js 的 fs 模块足够。但 exec_command 是真正的坑。
3.1 为什么必须开子进程
Node.js 是一个 JS 运行环境,它本身不懂怎么执行 pnpm install。
当 Agent 决定"我要装依赖",我的代码必须临时开一个子进程来跑这条命令:
js
import { spawn } from 'node:child_process'
const childProcess = spawn('pnpm', ['install'], {
cwd: '/project/react-todo-app', // 在哪执行
shell: 'bash.exe', // 用哪个 shell 解析命令
stdio: 'inherit', // 输出直接在终端显示
});
spawn 本质上就是在代码里替你在终端敲了一行命令。
3.2 shell 到底是什么
敲到终端里的 pnpm install 只是一串字符。CPU 看不懂。
Shell(bash / cmd / PowerShell)是一个翻译官,负责把这串字符翻译成操作系统能理解的操作:找到 pnpm.exe → 启动它 → 把 install 传给它。
AI 生成的命令通常包含管道(|)、链式执行(&&),这些不是可执行文件的名字,而是 Shell 语法。所以必须有一个 Shell 进程来解析。
3.3 Windows 上的坑
我是 Windows 用户。Agent 跑的也是我的 Windows 电脑。
问题是:AI 生成的命令全是 Linux 语法(ls、cat、echo -e),但 Windows 默认 Shell(cmd.exe)不兼容这些。
解决方案:显式指定 Git Bash 作为首选的 shell。
js
shell: process.platform === 'win32'
? 'D:\\Git\\Git\\bin\\bash.exe' // Windows:用 Git Bash
: true, // Mac/Linux:默认 shell 就是 bash
Git Bash 是安装 Git for Windows 时附带的,本质上是把 Linux 的 bash 搬到了 Windows。
3.4 工具失败不崩溃
这是我踩的最大的坑。
最初版本中,命令执行失败就 reject()。结果:Agent 跑到一半崩溃,整个 Node 进程退出。
后来我改了策略:
js
// ❌ 之前
if (code !== 0) {
reject(`命令失败,退出码: ${code}`); // Agent 直接死掉
}
// ✅ 现在
if (code !== 0) {
resolve(`命令失败,退出码: ${code}`); // 把错误作为文本返回给模型
}
工具失败时,返回描述性的错误文本(而不是抛异常)。模型看到错误后可以自己决定怎么做------这就是 ReAct 的核心价值:它不是死板的脚本,它能根据结果调整策略。
四、项目架构:从意大利面条到模块化
第一版开发时,为了快速验证想法,所有代码都塞在 main.js 里。模型配置、Agent 循环、可视化日志、任务定义,全在一个 200 行的文件里。
验证通过后,我把它拆成了清晰的模块:
bash
src/
├── main.js # 入口(30 行):解析参数,组装启动
├── config.js # 配置中心(模型、超时、迭代次数)
├── agent/
│ └── ReactAgent.js # 核心类:可复用的 ReAct 循环
├── prompts/
│ └── systemPrompt.js # System prompt 模板
├── tools/ # 四个工具
│ ├── index.js # 工具注册中心(统一导出)
│ ├── readFileTool.js
│ ├── writeFileTool.js
│ ├── listDirTool.js
│ └── execCommandTool.js
└── utils/
└── logger.js # 统一 chalk 彩色日志
核心设计决策:
- ReactAgent 类与 CLI 解耦:可以用在任何项目里,不绑定命令行
- 工具是纯函数:只返回结果字符串,不做可视化(日志交给 Agent 层统一处理)
- 所有配置支持环境变量覆盖:MODEL_NAME、BASE_URL、TIMEOUT、MAX_ITERATIONS 等
五、我学到了什么
关于 Agent 设计:
- ReAct 循环本身极其简单------就是一个 for 循环 + 消息管理。框架的价值在于工程化,不是算法
- 工具的错误处理策略比工具本身更重要。不崩溃是底线
- AI 写 bash 很强,写 PowerShell 很弱。技术选型要顺着 AI 来,而不是跟着系统默认走
关于 Node.js:
spawn是 Agent 项目里最复杂的部分。理解 shell、子进程、环境变量、工作目录的关系花了我最多时间path.join()代替手写斜杠不是挑剔,是跨平台必须process.cwd()是整个项目的"锚点"。所有路径都从它开始计算
关于 Windows 开发:
- AI 工具的生态是以 Linux/Mac 为中心的。在 Windows 上开发,得学会"假装在 Linux"------WSL 或 Git Bash
- Git Bash 不是妥协,是最佳选择。它让 bash 语法在 Windows 上完美运行
六、下一步
这个项目是概念验证。未来可以扩展的方向:
- 让 Agent 支持"自我修正"(看到终端报错后自动修 bug)
- 添加上下文管理(长对话的 token 压缩)
- 支持更多模型(Claude、GPT 等)
- 增加任务模板,减少反复描述
仓库地址 :min-cursor
如果你也在学习 Agent 开发,或者对底层原理感兴趣,欢迎交流。
本文是 Min-Cursor 项目系列的第一篇。下一篇将深入探讨 Agent 中的子进程管理和 Shell 选择。