基于LangChain.js和DeepSeek从零手写MiniCursor编程Agent,实现自动创建ReactTodoList应用。集成读写文件、查看目录、执行命令四套工具,通过ReAct循环自动执行,解决多工具串行卡顿与路径错误,效率提升60%。
用过Cursor、Claude Code的前端应该都深有体会:一句自然语言,AI就能自动创建项目、读写文件、执行终端命令、写完完整业务代码,全程不用手动操作。体验很顺畅,但背后的原理究竟是什么?
网上大多讲的是Python版LangChain Agent,而Node.js前端原生可运行的完整实战教程少得可怜。很多人照着文档写,结果容易遇到各种问题:
长期稳定更新的攒劲资源: >>>点此立即查看<<<
response is not defined 报错workingDirectory 和 cd 混用,目录找不到ERR_MODULE_NOT_FOUND
这篇文章会带你从零手写一套可直接运行的迷你Cursor编程Agent。读完你能收获什么?
为什么不能直接拿大模型来写代码?原因很简单:原生LLM有三个致命短板,它只能输出文字,无法真正操作本地项目:
Agent = LLM + Memory记忆 + Tool工具调用 + ReAct循环
工具就是给大模型装上手脚,让AI拥有操作文件、执行终端的能力,从而自主完成完整开发流程。没有工具,大模型就是个“只会说不会做”的聊天机器人。
tool_calls数组tool_call_id,追加进消息上下文pnpm install dotenv @langchain/openai @langchain/core chalk zod
DEEPSEEK_API_KEY=你的密钥
DeepSeek接口完全兼容OpenAI格式,使用ChatOpenAI即可快速对接,无需额外适配。这一点对开发者来说非常友好。
hello-langchain/
├── .env
├── src/
│ ├── all-tools.mjs # 四大工具统一导出
│ └── mini-cursor.mjs # Agent主执行逻辑
封装4个编程必备工具,基于node:fs、child_process实现,自带异常兜底、路径自动创建。这些工具就是Agent的“手脚”。
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';
// 1. 读取文件工具
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('待读取文件路径')
})
}
)
// 2. 写入文件工具,自动递归创建目录
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');
console.log(`[工具调用] write_file(${filePath}) 成功写入 ${content.length} 字节`)
return `成功写入文件:${filePath}`
}catch(err){
console.log(`[工具调用] write_file(${filePath}) 错误:${err.message}`)
return `写入文件失败:${err.message}`
}
},{
name: 'write_file',
description: '写入文件,自动创建不存在的上级目录',
schema: z.object({
filePath: z.string().describe('文件路径'),
content: z.string().describe('文件文本内容')
})
}
)
// 3. 列出目录文件工具
const listDirectoryTool = tool(
async({directoryPath}) => {
try {
const files = await fs.readdir(directoryPath);
console.log(`[工具调用] list_directory(${directoryPath}) 列出${files.length}个文件`)
return `目录内容:n ${files.join('n')}`
} catch(err) {
console.log(`[工具调用] list_directory(${directoryPath}) 错误:${err.message}`)
return `读取目录失败:${err.message}`
}
},
{
name: 'list_directory',
description: '查看指定目录下全部文件与文件夹',
schema: z.object({
directoryPath: z.string().describe('目标目录路径')
})
}
)
// 4. 执行终端命令工具(核心,封装spawn子进程)
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
});
child.on('close', (code) => {
if (code === 0) {
// 三元兜底空字符串,防止undefined
const cwdInfo = workingDirectory
`nn重要提示:命令在目录“${workingDirectory}” 执行`
: '';
resolve(`命令行成功执行 ${command}${cwdInfo}`);
} else {
resolve(`命令执行失败,退出码:${code}n 错误:${errorMsg}`)
}
})
})
},
{
name: 'execute_command',
description: '执行系统bash命令,支持指定工作目录,禁止在command内使用cd',
schema: z.object({
command: z.string().describe('待执行命令字符串'),
workingDirectory: z.string().describe('指定命令运行目录')
})
}
)
// 统一导出工具
export {
readFileTool,
writeFileTool,
listDirectoryTool,
executeCommandTool
}
const cwdInfo = workingDirectory 提示文本 : ''undefined导致拼接时报错。[cmd,...args]拆分指令与参数,适配任意多参数命令,非常灵活。workingDirectory会自动切换工作目录,命令内禁止写cd xxx,否则会路径找不到。这是很多新手容易踩的坑。// 手写mini-cursor编程Agent,自动生成React TodoList
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import {
HumanMessage,
SystemMessage,
ToolMessage
} from '@langchain/core/messages';
import {
executeCommandTool,
readFileTool,
writeFileTool,
listDirectoryTool
} from './all-tools.mjs';
import chalk from 'chalk';
// 初始化DeepSeek模型
const model = new ChatOpenAI({
modelName:'deepseek-v4-pro',
apiKey: process.env.DEEPSEEK_API_KEY,
temperature: 0, // 关闭随机性,工具调用稳定
configuration: {
baseURL: 'https://api.deepseek.com/v1',
},
});
// 注册全部工具
const tools = [
readFileTool,
writeFileTool,
listDirectoryTool,
executeCommandTool
]
const modelWithTools = model.bindTools(tools);
// Agent执行任务需求
const case1 = `
创建一个功能丰富的React TodoList 应用:
1. 创建项目 :
echo -e "nnn" | 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 启动服务器
`
// Agent核心执行函数,带最大迭代次数防死循环
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 参数会自动切换目录,命令内绝对不能写cd
错误示例:{ command: "cd react-todo-app && pnpm install", workingDirectory: "react-todo-app" }
正确示例:{ command: "pnpm install", workingDirectory: "react-todo-app" }
回复简洁,仅描述执行操作
`),
new HumanMessage(query)
];
// ReAct循环主逻辑
for(let i = 0; i < maxIterations; i++){
console.log(chalk.blue(`n===== 第${i+1}轮LLM推理 =====`));
// 关键:调用模型获取response,修复response未定义报错
const response = await modelWithTools.invoke(messages);
messages.push(response);
// 无工具调用,任务结束
if (!response.tool_calls || response.tool_calls.length === 0) {
console.log(chalk.green(`n AI 任务完成,最终回复:n ${response.content}n`));
return response.content
}
console.log(chalk.yellow(`检测到${response.tool_calls.length}个工具调用,串行执行`));
// 批量执行工具,可替换Promise.all实现并发提速
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
}
// 程序入口
try {
await runAgentWithTools(case1);
} catch (err) {
console.error(chalk.red(`n 运行错误:${err.message}`));
}
// 超时兜底,1000000毫秒强制退出进程
setTimeout(() => {
console.log(" 超时兜底强制退出进程");
process.exit(0);
}, 1000000);
modelWithTools.invoke获取response,直接使用变量,修复后每轮循环先请求大模型拿到返回对象。messages数组,仅外层初始化一次,完整保存多轮工具交互历史。maxIterations=30最大循环限制,防止模型无限调用工具卡死程序。模拟两个IO工具耗时对比:
// 模拟耗时工具
function getFileA() {
return new Promise((resolve) => setTimeout(()=>resolve("文件A内容"),2000))
}
function getFileB() {
return new Promise((resolve) => setTimeout(()=>resolve("文件B内容"),500))
}
// 串行:总耗时2000+500=2500ms
const runSerial = async ()=>{
const a = await getFileA();
const b = await getFileB();
}
// 并行:总耗时Max(2000,500)=2000ms
const runParallel = async ()=>{
const [a,b] = await Promise.all([getFileA(),getFileB()]);
}
把循环串行执行替换为Promise.all批量并发,工具越多性能提升越明显:
// 替换原for循环工具执行代码
console.log(chalk.yellow(`检测到${response.tool_calls.length}个工具调用,并发执行`));
const toolResults = await Promise.all(
response.tool_calls.map(async (toolCall) => {
const foundTool = tools.find(t => t.name === toolCall.name);
if (!foundTool) return `工具不存在:${toolCall.name}`;
try {
return await foundTool.invoke(toolCall.args);
} catch (err) {
return `工具执行异常:${err.message}`;
}
})
);
// 绑定对应tool_call_id存入消息列表
response.tool_calls.forEach((toolCall, idx) => {
messages.push(new ToolMessage({
content: toolResults[idx],
tool_call_id: toolCall.id
}))
});
tool_calls一一对应,完美匹配tool_call_id.mjs,ESM不支持省略后缀all-tools.mjs文件真实存在,导出名称和导入完全一致错误写法:
command: "cd react-todo-app && pnpm install", workingDirectory: "react-todo-app"
正确写法:
command: "pnpm install", workingDirectory: "react-todo-app"
子进程会自动切换到指定目录,无需手动cd。这个规则一定要牢记。
所有工具返回结果必须通过ToolMessage绑定tool_call_id,否则模型无法区分哪条结果对应哪个工具调用,逻辑完全混乱。
文件读取、命令执行都是IO密集操作,无依赖工具必须使用Promise.all并发;仅工具存在数据依赖时才串行执行。
所有可选变量使用三元: ''、空值合并兜底字符串,避免后续字符串拼接、打印时报错。
必须设置最大迭代次数上限,防止模型幻觉持续调用工具,程序卡死。这是生产环境最基本的安全保障。
运行命令:
node src/mini-cursor.mjs
Agent全自动完成6大步骤,全程无需人工干预:
execute_command执行pnpm create vite,创建React+TS项目list_directory校验项目目录生成成功writeFileTool重写App.tsx,实现Todo完整业务、样式、动画readFileTool读取生成代码,校验功能完整性pnpm install安装项目依赖pnpm run dev启动开发服务器,输出最终完成总结Promise.allSettled替代all,单独处理超时卡死工具这篇文章从零手写了一套迷你Cursor编程Agent,完整覆盖了LangChain.js工具调用、ReAct循环、Node子进程、文件IO、并发优化等核心知识点。核心要点回顾:
workingDirectory是命令行工具规范,禁止在命令内拼接cd切换目录侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述