手写 Cursor 最小版本:基于 LangChain 打造自己的 AI 编程 Agent
近期 AI Agent 爆火,从千问点奶茶、OpenClaw 养虾,到我们常用的 Cursor 编程助手,本质上都是“大模型 + 工具 + 记忆”的组合体。Cursor 之所以能帮我们快速生成、修改代码,核心就是让大模型拥有了“读写文件”“执行命令”的能力——而这,我们用 LangChain 就能手写一个最小版本。
今天就带大家从零搭建一个简化版 Cursor(编程 Agent),既能理解 AI Agent 的核心原理,也能直接上手实操,用代码实现“让 AI 自动创建 React TodoList 项目”,全程干货,建议收藏实操!
一、先搞懂:AI Agent 与 Cursor 的核心逻辑
在动手写代码前,我们先理清一个关键问题:Cursor 这类编程 Agent,到底是怎么工作的?
其实答案很简单:AI Agent = LLM(大模型) + Tool(工具) + Memory(记忆) + RAG(知识库) 。对于 Cursor 来说,核心是前两者:
- LLM:负责思考、规划、生成代码(比如判断需要创建什么文件、写什么逻辑);
- Tool:给大模型“赋能”,让它能执行读写文件、运行终端命令等操作(毕竟大模型本身只能“说”,不能“做”);
- Memory 和 RAG:Cursor 简化版可暂不实现,核心先搞定“LLM + Tool”的联动。
我们今天的目标,就是用 LangChain 框架,封装“读写文件”“执行命令”“列出目录”4 个核心工具,绑定大模型,实现一个能自动完成“创建 React 项目 + 写功能 + 启服务”的最小版 Cursor。
二、环境准备:基础依赖安装
首先准备好开发环境,需要 Node.js(建议 16+),然后初始化项目并安装依赖:
# 初始化项目
mkdir handwritten-cursor && cd handwritten-cursor
npm init -y
# 重命名入口文件为 mjs(支持 ES 模块)
mv index.js index.mjs
# 安装核心依赖
pnpm add @langchain/core @langchain/openai zod fs-extra node:fs/promises node:path node:child_process chalk dotenv
依赖说明:
- @langchain/core + @langchain/openai:LangChain 核心框架,用于绑定大模型和工具;
- zod:用于校验工具的输入参数(比如文件路径必须是字符串);
- fs/promises、path、child_process:Node.js 原生模块,实现文件操作和命令执行;
- chalk:终端彩色输出,提升体验;
- dotenv:加载环境变量(存储大模型 API 密钥)。
创建 .env 文件,配置大模型参数(这里用 OpenAI 兼容模型,也可以替换为千问、DeepSeek 等):
MODEL_NAME=qwen-coder-turbo
OPENAI_API_KEY=你的API密钥
OPENAI_BASE_URL=你的模型基础地址
三、核心开发:封装 4 个必备工具(Tool)
Cursor 的核心能力,就是让大模型能操作文件和执行命令。我们用 LangChain 的 tool 函数,封装 4 个工具,分别对应“读取文件、写入文件、执行命令、列出目录”——这也是编程 Agent 最基础、最核心的工具集。
创建 all_tools.mjs 文件,编写工具代码:
// all_tools.mjs
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 }) => {
try {
const content = await fs.readFile(filePath, 'utf-8');
console.log(`[工具调用] read_file("${filePath}") 成功读取 ${content.length} 字节`);
return `文件内容:\n${content}`;
} catch (error) {
console.log(`工具调用 read_file("${filePath}") 失败: ${error.message}`);
return `错误:${error.message}`;
}
},
{
name: 'read_file',
description: '读取指定路径的文件内容,适用于查看已有的代码文件',
schema: z.object({
filePath: z.string().describe('文件的绝对路径或相对路径,例如 src/App.tsx')
})
}
);
// 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 (error) {
console.log(`[工具调用] write_file("${filePath}") 失败: ${error.message}`);
return `写入文件失败:${error.message}`;
}
},
{
name: 'write_file',
description: '向指定路径写入文件内容,自动创建不存在的目录,适用于生成代码文件',
schema: z.object({
filePath: z.string().describe('文件的绝对路径或相对路径,例如 src/App.tsx'),
content: z.string().describe('要写入的文件内容,比如 React 组件代码')
})
}
);
// 3. 执行命令工具:执行系统命令,支持指定工作目录
const executeCommanTool = 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', (error) => {
errorMsg = error.message;
});
child.on('close', (code) => {
if (code === 0) {
console.log(`[工具调用] execute_command("${command}") 命令执行成功`);
// 提示用户后续命令可直接使用指定工作目录,无需 cd
const cwdInfo = workingDirectory
? `\n\n重要提示:命令在目录"${workingDirectory}"中执行成功。\n如果需要继续在该目录执行命令,请使用 workingDirectory 参数,不要使用 cd 命令`
: '';
resolve(`命令执行成功: ${command} ${cwdInfo}`);
} else {
if (errorMsg) console.error(`错误:${errorMsg}`);
process.exit(code || 1);
}
});
});
},
{
name: 'execute_command',
description: '执行系统终端命令,支持指定工作目录,实时显示命令输出,适用于创建项目、安装依赖、启动服务',
schema: z.object({
command: z.string().describe('要执行的终端命令,例如 pnpm install、pnpm run dev'),
workingDirectory: z.string().optional().describe('指定命令执行的工作目录,默认当前目录,无需再用 cd 切换')
})
}
);
// 4. 列出目录工具:查看指定目录下的所有文件和文件夹
const listDirectoryTool = tool(
async ({ directoryPath }) => {
try {
const files = await fs.readdir(directoryPath);
console.log(`[工具调用] list_directory("${directoryPath}") 成功列出 ${files.length} 个文件`);
return `目录内容:\n ${files.map(f => `- ${f}`).join('\n')}`;
} catch (error) {
console.log(`[工具调用] list_directory("${directoryPath}") 失败: ${error.message}`);
return `列出目录失败:${error.message}`;
}
},
{
name: 'list_directory',
description: '列出指定目录下的所有文件和文件夹,适用于确认项目结构、查看文件是否生成',
schema: z.object({
directoryPath: z.string().describe('目录的绝对路径或相对路径,例如 react-todo-app')
})
}
);
export { readFileTool, writeFileTool, executeCommanTool, listDirectoryTool };
工具封装要点:
- 每个工具都用 tool 函数包裹,第一个参数是工具执行逻辑,第二个参数是工具配置(名称、描述、参数校验);
- 描述要清晰:大模型会根据描述判断“什么时候该用哪个工具”,比如“生成代码”就用 write_file,“安装依赖”就用 execute_command;
- 参数校验:用 zod 校验输入,避免大模型传入无效参数(比如文件路径不是字符串);
- 错误处理:每个工具都有异常捕获,确保命令执行失败、文件读取失败时,能给大模型返回明确的错误信息,方便它调整策略。
四、关键步骤:绑定大模型与工具,实现 Agent 逻辑
工具封装完成后,我们需要将工具绑定到大模型,再编写 Agent 的核心循环逻辑——让大模型能“思考→调用工具→根据工具结果调整→继续执行”,直到完成任务。
创建 index.mjs 文件,编写 Agent 核心代码:
// index.mjs
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import { HumanMessage, SystemMessage, ToolMessage } from '@langchain/core/messages';
import { readFileTool, writeFileTool, executeCommanTool, listDirectoryTool } from './all_tools.mjs';
import chalk from 'chalk'; // 彩色输出,提升终端体验
// 1. 初始化大模型(兼容 OpenAI 格式,可替换为千问、DeepSeek 等)
const model = new ChatOpenAI({
modelName: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
temperature: 0, // 温度为 0,保证输出稳定,避免随机生成
configuration: {
baseURL: process.env.OPENAI_BASE_URL
}
});
// 2. 绑定工具到大模型,让大模型知道可以调用哪些工具
const tools = [readFileTool, writeFileTool, executeCommanTool, listDirectoryTool];
const modelWithTools = model.bindTools(tools);
// 3. Agent 核心循环:思考→调用工具→迭代,直到完成任务
async function runAgentWithTools(query, maxIterations = 30) {
// 初始化消息列表:系统提示 + 用户任务
const messages = [
new SystemMessage(`
你是一个编程助手(简化版 Cursor),专注于通过工具完成前端项目开发任务。
当前工作目录: ${process.cwd()}
你可以使用以下工具:
1. read_file: 读取文件内容,用于查看已生成的代码
2. write_file: 写入文件内容,用于生成/修改代码文件,自动创建目录
3. execute_command: 执行终端命令,支持指定工作目录(无需 cd 切换)
4. list_directory: 列出目录内容,用于确认项目结构
重要规则(必看):
- 使用 execute_command 时,若指定 workingDirectory,不要在 command 中使用 cd 命令
- 错误示例: { command: "cd react-todo-app && pnpm install", workingDirectory: "react-todo-app" }
- 正确示例: { command: "pnpm install", workingDirectory: "react-todo-app" }
工作要求:
1. 先规划任务步骤,再逐步调用工具执行;
2. 工具执行失败后,根据错误信息调整策略(比如文件路径错误就修正路径);
3. 回复简洁,只说明当前做了什么,无需多余解释。
`),
new HumanMessage(query), // 用户的任务指令
];
// 循环迭代(最多 30 次,避免无限循环)
for (let i = 0; i < maxIterations; i++) {
console.log(chalk.bgGreen('⏳正在等待 AI 思考...'));
const response = await modelWithTools.invoke(messages);
messages.push(response); // 记录大模型的响应
// 如果大模型不调用工具,说明任务完成,返回最终结果
if (!response.tool_calls || response.tool_calls.length === 0) {
console.log(`\n ${chalk.green('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 // 关联工具调用 ID,让大模型知道这是哪个工具的结果
}));
}
}
}
// 迭代次数上限,返回最后一次响应
return messages[messages.length - 1].content;
}
// 4. 定义测试任务:让 AI 自动创建 React TodoList 项目(完整功能)
const case1 = `
创建一个功能丰富的 React TodoList 应用,全程使用 pnpm,步骤如下:
1. 创建项目:echo -e "n\nn" | pnpm create vite react-todo-app --template react-ts
2. 修改 src/App.tsx,实现完整功能:
- 核心功能:添加、删除、编辑、标记待办完成
- 辅助功能:分类筛选(全部/进行中/已完成)、统计待办数量、localStorage 数据持久化
3. 添加美观样式:
- 渐变背景(从蓝色到紫色)
- 卡片阴影、圆角,按钮悬停效果
4. 添加过渡动画:
- 待办添加/删除时的过渡效果,使用 CSS transitions 实现
5. 列出项目目录,确认文件生成正常
6. 进入 react-todo-app 目录,执行 pnpm install 安装依赖,再执行 pnpm run dev 启动服务器
`;
// 执行 Agent,开始任务
try {
await runAgentWithTools(case1);
} catch (error) {
console.error(`\n ${chalk.red('任务执行失败:')} ${error.message}\n`);
};
Agent 核心逻辑解析:
- 消息列表:存储系统提示、用户任务、大模型响应、工具执行结果,大模型会根据整个消息上下文思考下一步操作;
- 循环迭代:大模型每次思考后,要么调用工具,要么返回最终结果;工具执行后,结果会回传给大模型,形成“思考→执行→反馈→再思考”的闭环;
- 系统提示:关键中的关键!必须明确告诉大模型“能做什么、不能做什么、工具怎么用”,否则大模型可能会调用错误的工具(比如用 cd 命令切换目录)。
五、实操测试:运行 Agent,让 AI 自动写项目
所有代码编写完成后,执行以下命令,启动 Agent:
node index.mjs
此时会看到终端输出以下流程(AI 自动执行,无需手动操作):
- AI 思考后,调用 execute_command 工具,执行 pnpm create vite 创建 React 项目;
- 项目创建完成后,调用 write_file 工具,修改 src/App.tsx,写入 TodoList 完整代码(包括功能、样式、动画);
- 调用 list_directory 工具,确认项目目录结构正确;
- 调用 execute_command 工具,进入项目目录,执行 pnpm install 安装依赖;
- 最后调用 execute_command 工具,执行 pnpm run dev 启动服务器。
等待执行完成后,打开浏览器访问 http://localhost:5173,就能看到 AI 自动生成的 React TodoList 应用——有渐变背景、卡片阴影、过渡动画,功能完整,数据持久化也能正常使用。
六、核心总结:手写 Cursor 的关键的启示
通过这个最小版本的 Cursor,我们能清晰地看到 AI Agent 的核心本质:大模型负责“思考和规划”,工具负责“执行和反馈”,两者结合,就能实现“自动完成复杂任务” 。
对比真正的 Cursor,我们的简化版缺少了 Memory(记忆上下文)和 RAG(知识库),但核心的“LLM + Tool”联动已经实现——只要再扩展这两个模块,就能让它记住之前的代码修改记录、读取项目文档,变得和真正的 Cursor 一样强大。
最后再提炼几个关键要点,帮你快速上手 AI Agent 开发:
- 工具封装要“清晰”:名称、描述、参数校验必须明确,否则大模型会用错工具;
- 系统提示要“细致”:把工具的使用规则、任务要求说清楚,减少大模型的无效尝试;
- 循环逻辑要“稳健”:设置迭代上限,处理工具执行失败的情况,避免无限循环。
现在,你已经掌握了 AI 编程 Agent 的核心开发思路,不妨试着扩展一下:添加 Memory 模块,让 AI 记住你之前的修改需求;或者添加 RAG 模块,让它能读取项目文档,生成更贴合项目规范的代码。
如果在实操中遇到问题,欢迎在评论区留言交流,一起解锁 AI Agent 的更多玩法
附:常见问题排查
- 大模型不调用工具:检查系统提示是否清晰,工具的 description 是否准确,确保大模型知道“什么时候该用什么工具”;
- 命令执行失败:检查 workingDirectory 是否正确,避免在 command 中使用 cd 命令;
- 文件写入失败:检查文件路径是否正确,确保 Node.js 有对应目录的读写权限;
- API 调用失败:检查 .env 文件中的 API 密钥和基础地址是否正确,确保模型支持 LangChain 调用。