别再迷信黑盒了!手把手带你实现一个能自动创建React项目的AI编程助手
引言:AI编程助手真的会“思考”吗?
当你对Cursor说“帮我创建一个TodoList应用”,它唰唰唰地创建文件、安装依赖、启动服务——这背后到底是魔法还是代码?
答案是:一套精心设计的Agent循环 + 几个核心工具函数。
今天,我们不玩虚的,直接手写一个mini-cursor。用不到300行代码,复刻AI编程助手的核心能力。读完这篇,你将彻底看透:
- AI如何“假装思考”实则调用工具
- LangChain如何成为AI的“神经系统”
- Node.js如何让AI“动手干活”
- 一个完整的Agent项目应该如何组织
一、架构全景:AI的“大脑”与“手脚”如何配合
text
┌──────────────────────────────────────────────────────┐
│ mini-cursor 核心架构 │
│ │
│ ┌────────────────────────────────────────────┐ │
│ │ Agent 主循环 (mini-cursor.mjs) │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ ReAct Loop: │ │ │
│ │ │ Think → Act → Observe │ │ │
│ │ │ (最多30次迭代) │ │ │
│ │ └─────────────────────────────────┘ │ │
│ └────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────┐ │
│ │ 工具层 (all-tools.mjs) │ │
│ │ ┌──────┐ ┌──────┐ ┌──────┐ ┌────────┐ │ │
│ │ │读文件 │ │写文件 │ │列目录 │ │执行命令│ │ │
│ │ └──────┘ └──────┘ └──────┘ └────────┘ │ │
│ └────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────┐ │
│ │ Node.js 子进程 (node-exec.mjs) │ │
│ │ 隔离执行 pnpm / vite 等耗时命令 │ │
│ └────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
一句话概括:大模型负责“想”,工具负责“做”,子进程负责“跑”。
二、工具层深入解析:AI的“四肢”是怎样炼成的
所有工具都通过LangChain的tool()函数创建。这个函数做了三件事:
- 接收一个异步执行函数
- 接收工具元数据(名称、描述、参数Schema)
- 返回一个可被AI调用的标准工具对象
🔧 读文件工具 —— AI的“眼睛”
javascript
const readFileTool = tool(
async({ filePath }) => {
const content = await fs.readFile(filePath, 'utf-8');
console.log(`[工具调用] 读取 ${filePath},共 ${content.length} 字符`);
return content;
},
{
name: 'read_file',
description: '读取文件内容,用于查看代码、配置文件等',
schema: z.object({
filePath: z.string().describe('文件路径,支持相对或绝对路径')
})
}
);
设计要点:
z.object()定义了参数结构,AI调用时必须传入符合格式的参数- 异步读取,不阻塞主流程
- 返回值会作为
ToolMessage注入对话上下文
✍️ 写文件工具 —— AI的“手”
javascript
const writeFileTool = tool(
async({ filePath, content }) => {
try {
const dir = path.dirname(filePath);
await fs.mkdir(dir, { recursive: true }); // 自动创建父目录
await fs.writeFile(filePath, content, 'utf-8');
return `✅ 成功写入 ${filePath}`;
} catch(err) {
return `❌ 写入失败:${err.message}`;
}
},
{
name: 'write_file',
description: '写入文件内容,不存在则创建,父目录不存在则自动创建',
schema: z.object({
filePath: z.string().describe('目标文件路径'),
content: z.string().describe('要写入的内容')
})
}
);
工程智慧:
recursive: true让AI无需关心目录是否存在- 错误被捕获并返回友好信息,AI可以根据错误调整策略
📂 列目录工具 —— AI的“导航”
javascript
const listDirectoryTool = tool(
async ({ directoryPath }) => {
const files = await fs.readdir(directoryPath);
return files.map(f => `📄 ${f}`).join('\n');
},
{
name: 'list_directory',
description: '列出目录内容,帮助AI了解项目结构',
schema: z.object({
directoryPath: z.string().describe('目录路径')
})
}
);
⚡ 执行命令工具 —— AI的“超能力”
这是最复杂的工具,也是AI真正“干活”的关键:
javascript
const executeCommandTool = tool(
async ({ command, workingDirectory }) => {
const cwd = workingDirectory || process.cwd();
return new Promise((resolve) => {
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) {
resolve(`✅ 命令执行成功: ${command}`);
} else {
resolve(`❌ 命令失败 (退出码: ${code}): ${errorMsg}`);
}
});
});
},
{
name: 'execute_command',
description: '执行系统命令,支持指定工作目录',
schema: z.object({
command: z.string().describe('要执行的命令'),
workingDirectory: z.string().describe('工作目录')
})
}
);
关键设计决策:
- 使用
spawn而非exec:流式输出,适合长时间运行的命令 stdio: 'inherit':用户能看到pnpm install的实时进度- 返回Promise,完美融入async/await体系
三、Agent主循环:AI的“大脑”如何做决策
3.1 模型绑定工具
javascript
const model = new ChatOpenAI({
modelName: 'deepseek-v4-pro',
temperature: 0,
});
const modelWithTools = model.bindTools([
readFileTool,
writeFileTool,
listDirectoryTool,
executeCommandTool
]);
bindTools()是LangChain的核心魔法:
- 自动将工具签名转换成OpenAI的
function calling格式 - 让模型知道“我有这些能力”
- 返回的响应中会包含
tool_calls字段
3.2 System Prompt —— 给AI立规矩
javascript
const messages = [
new SystemMessage(`
你是项目管理助手,可用工具:
1. read_file: 读取文件
2. write_file: 写入文件
3. execute_command: 执行命令(支持 workingDirectory 参数)
4. list_directory: 列出目录
⚠️ 重要规则:
- 使用 execute_command 时,指定 workingDirectory 后,
不要在 command 里再用 cd 切换目录
- 正确: { command: "pnpm install", workingDirectory: "react-todo-app" }
- 错误: { command: "cd react-todo-app && pnpm install", workingDirectory: "react-todo-app" }
`),
new HumanMessage(query)
];
Prompt Engineering的艺术:
- 明确告诉AI有哪些工具可用
- 给出正确和错误的使用示例
- 避免AI在命令中拼接
cd导致路径错误
3.3 ReAct循环 —— Agent的灵魂
javascript
async function runAgentWithTools(query, maxIterations = 30) {
const messages = [systemMessage, humanMessage];
for (let i = 0; i < maxIterations; i++) {
console.log(chalk.bgGreen(`🧠 第 ${i} 次思考...`));
// 1. THINK: AI推理
const response = await modelWithTools.invoke(messages);
messages.push(response);
// 2. 没有工具调用 → 任务完成
if (!response.tool_calls?.length) {
console.log(chalk.green(`✅ ${response.content}`));
return response.content;
}
// 3. ACT: 执行工具
for (const toolCall of response.tool_calls) {
const tool = tools.find(t => t.name === toolCall.name);
if (tool) {
// 4. OBSERVE: 记录结果
const result = await tool.invoke(toolCall.args);
messages.push(new ToolMessage({
content: result,
tool_call_id: toolCall.id
}));
}
}
// 回到步骤1,继续循环
}
}
ReAct循环的本质:
text
用户提问 → 🤔 Think (AI决定用什么工具)
→ ⚡ Act (执行工具)
→ 👀 Observe (把结果加入上下文)
→ 🤔 Think (根据结果决定下一步)
→ ⚡ Act ...
→ ✅ 完成任务,输出最终答案
这个过程模拟了人类解决问题的思维链:看到问题→思考方案→动手操作→观察结果→调整方案→继续操作→完成。
四、任务执行实战:从提问到完成的全流程
当我们给Agent下达这个任务:
javascript
const task = `
创建React TodoList应用:
1. 用Vite创建项目
2. 实现完整TodoList功能
3. 添加美观样式和动画
4. 安装依赖并启动服务
`;
Agent的决策轨迹是这样的:
text
🧠 第 0 次思考...
🤔 "我需要先创建项目,使用 create-vite 命令"
⚡ execute_command({
command: "pnpm create vite react-todo-app --template react-ts",
workingDirectory: process.cwd()
})
👀 "✅ 项目创建成功"
🧠 第 1 次思考...
🤔 "现在需要写入完整的App.tsx代码"
⚡ write_file({
filePath: "react-todo-app/src/App.tsx",
content: "完整的TodoList组件代码..."
})
👀 "✅ 成功写入 2847 字节"
🧠 第 2 次思考...
🤔 "需要安装依赖"
⚡ execute_command({
command: "pnpm install",
workingDirectory: "react-todo-app"
})
👀 "✅ 依赖安装完成,新增 342 个包"
🧠 第 3 次思考...
🤔 "启动开发服务器"
⚡ execute_command({
command: "pnpm run dev",
workingDirectory: "react-todo-app"
})
👀 "✅ 服务启动在 http://localhost:5173"
🧠 第 4 次思考...
🤔 "所有任务完成,可以汇报了"
💬 "🎉 TodoList应用已创建!访问 http://localhost:5173 查看"
五、子进程隔离:为什么需要独立执行
Node.js主进程是单线程的,如果在主进程里执行pnpm install,整个Agent会被阻塞。解决方案:
javascript
// node-exec.mjs
const [cmd, ...args] = command.split(' ');
const child = spawn(cmd, args, {
cwd,
stdio: 'inherit', // 实时显示输出
shell: true, // 使用Shell执行
});
child.on('close', (code) => {
if (code === 0) {
console.log('✅ 子进程执行成功');
process.exit(0);
} else {
console.log(`❌ 子进程失败,退出码:${code}`);
process.exit(code);
}
});
为什么这样设计:
- 隔离性:子进程崩溃不影响主Agent
- 实时性:
stdio: 'inherit'让用户看到命令输出 - 可监控:通过
close事件感知命令执行结果
六、完整的执行日志展示
bash
$ node mini-cursor.mjs
🧠 第 0 次思考...
[工具调用] execute_command(pnpm create vite react-todo-app --template react-ts)
工作目录:/Users/xxx/projects
✔ 项目创建成功
🧠 第 1 次思考...
[工具调用] write_file(react-todo-app/src/App.tsx)
✅ 成功写入 2847 字节
🧠 第 2 次思考...
[工具调用] execute_command(pnpm install)
工作目录:react-todo-app
✔ 依赖安装完成 (342 packages)
🧠 第 3 次思考...
[工具调用] execute_command(pnpm run dev)
工作目录:react-todo-app
➜ Local: http://localhost:5173/
🧠 第 4 次思考...
✅ AI最终回复:
🎉 TodoList 应用已创建成功!
- 项目目录:react-todo-app
- 访问地址:http://localhost:5173
- 功能:添加/删除/标记完成/分类筛选/数据持久化
- 样式:渐变背景 + 卡片设计 + 过渡动画
七、核心设计模式总结
| 设计模式 | 实现方式 | 解决的问题 |
|---|---|---|
| 工具抽象 | tool() + Zod Schema | 让AI能调用任意函数 |
| 消息历史 | 维护messages数组 | 让AI有"记忆"能力 |
| ReAct循环 | Think→Act→Observe | 让AI能自主完成任务 |
| 子进程隔离 | child_process.spawn | 避免耗时命令阻塞主线程 |
| 错误回传 | ToolMessage携带错误信息 | 让AI能根据错误调整策略 |
八、扩展与优化方向
🚀 可以立即添加的新工具
search_code:在项目中搜索代码片段git_commit:自动提交代码run_tests:执行测试用例browser_open:打开浏览器预览
🧠 可以优化的决策机制
- 并行执行:多个独立工具调用可以
Promise.all并发 - 中断机制:用户可随时打断无限循环
- 记忆模块:使用向量数据库存储项目上下文
- 安全确认:危险操作前询问用户
写在最后:AI编程的本质
通过手写这个mini-cursor,我们发现:
AI编程助手并不神秘,它就是一个精心设计的工具调用系统。
大模型负责"思考"下一步该做什么,而真正的"执行"都落在我们写的工具函数上。这就是为什么:
- Cursor能创建项目 → 因为它调用了
execute_command - Cursor能修改代码 → 因为它调用了
write_file - Cursor能理解代码 → 因为它调用了
read_file
掌握了这个核心原理,你就能:
- 定制自己的AI编程助手
- 给现有工具添加新的能力
- 理解Claude Code、Copilot等产品的底层逻辑
现在,拿起键盘,去创造属于你自己的AI助手吧!🚀
💬 互动话题:如果你来设计一个AI编程助手,你最想给它添加什么"超能力"?欢迎在评论区分享你的想法!