Cursor的核心是「AI代理+工具调用」,即让AI能够理解用户需求,自主选择工具(如读写文件、执行命令)完成复杂任务。本次基于LangChain工具链,结合提供的代码,拆解手写Cursor的核心逻辑、重点知识与实现步骤,全程结合代码实例解析,帮助快速掌握核心原理。
一、前置知识准备(代码中核心依赖与概念)
手写Cursor需依赖「LangChain核心库」「Node.js原生模块」「AI模型」三大模块,代码中已明确引入,先理解各依赖的作用:
1. 核心依赖解析
- @langchain/core/tools:LangChain的工具核心模块,提供
tool函数,用于定义AI可调用的工具(这是Cursor实现工具调用的基础)。 - node:fs/promises、node:path:Node.js原生文件系统模块(异步版)和路径处理模块,用于实现文件读写、目录操作(对应代码中的文件工具、目录工具)。
- node:child_process:Node.js原生子进程模块,提供
spawn方法,用于执行系统命令(对应代码中的执行命令工具)。 - zod:数据校验库,用于定义工具的入参规则(确保AI调用工具时传入正确格式的参数,避免异常)。
- @langchain/openai:LangChain对接OpenAI(或兼容OpenAI API的模型,如代码中的qwen-plus)的模块,用于创建AI模型实例,实现AI思考与工具调用的绑定。
- chalk:可选依赖,用于终端彩色输出,提升调试体验(代码中用于标记AI思考、错误信息等)。
2. 核心概念:AI Agent与Tool调用
Cursor的本质是「AI Agent(智能代理)」,核心逻辑是: 用户输入需求 → AI分析需求 → 自主选择工具 → 执行工具获取结果 → 结合结果继续思考(迭代)→ 完成需求并反馈。 代码中通过「工具定义→模型绑定工具→循环迭代思考」三步实现这一逻辑,也是本次手写的核心流程。
二、手写Cursor核心步骤(结合代码逐点解析)
代码已完整实现一个简易Cursor(可完成React TodoList项目的创建、开发、启动),拆解为4个核心步骤,每个步骤结合代码重点解析。
步骤1:定义AI可调用的工具(核心:tool函数的使用)
AI无法直接操作本地文件、执行命令,需先定义「工具」,并告知AI工具的功能、入参格式——这是Cursor能完成实际任务的基础。代码中定义了4个核心工具,均使用LangChain的tool函数创建,格式统一且固定。
工具定义的固定格式(以readFileTool为例)
const readFileTool = tool(
// 工具执行逻辑(核心:接收入参,执行具体操作,返回结果/错误)
async ({ filePath }) => {
try{
// 实际操作:读取文件内容(fs.readFile异步方法)
const content = await fs.readFile(filePath, 'utf-8');
// 终端打印调试信息(方便排查工具调用情况)
console.log(`[工具调用] read_file("${filePath}") 成功读取 ${content.length} 字节`)
// 返回结果给AI(AI会根据结果继续思考)
return `文件内容: \n${content}`
}catch(error){
// 异常处理:捕获错误,返回错误信息
console.log(`工具调用 read_file("${filePath}") 失败: ${error.message}`);
return `错误:${error.message}`;
}
},
// 工具描述(关键:告知AI「这个工具能做什么」「需要传入什么参数」)
{
name: 'read_file', // 工具唯一名称(AI调用时通过该名称定位工具)
description: '读取指定文件的内容', // 工具功能描述(AI据此判断是否使用该工具)
schema: z.object({ // 入参校验(zod定义,确保AI传入正确格式的参数)
filePath: z.string().describe('文件路径'), // 入参名称+类型+描述
})
}
)
4个核心工具解析(代码重点)
-
readFileTool(读取文件) :核心是
fs.readFile,异步读取指定路径的文件内容,返回文件内容或错误信息;注意先捕获异常,避免工具调用崩溃。 -
writeFileTool(写入文件) :核心是
fs.writeFile,新增逻辑:先通过path.dirname(filePath)获取文件所在目录,再用fs.mkdir(dir, { recursive: true })自动创建不存在的目录(避免路径错误),最后写入内容。 -
executeCommandTool(执行命令) :最复杂的工具,核心是
child_process.spawn(创建子进程执行系统命令):- 入参支持
workingDirectory(可选),用于指定命令执行目录,避免使用cd命令切换目录(代码中特别强调的规则,避免AI调用时出错)。 - 通过
command.split(' ')拆分命令与参数(如pnpm install拆分为['pnpm', 'install']),适配spawn方法的参数格式。 - 监听子进程的
error和close事件,处理命令执行成功/失败的逻辑,成功则返回提示,失败则抛出错误。
- 入参支持
-
listDirectoryTool(列出目录) :核心是
fs.readdir,读取指定目录下的所有文件/子目录,用map格式化输出(便于AI读取结果)。
工具定义的关键注意点
- 工具名称(name)必须唯一,AI调用时通过名称匹配工具,不可重复。
- description必须清晰简洁,AI会根据用户需求和描述,判断是否需要调用该工具(描述模糊会导致AI误判)。
- schema必须严格定义入参,避免AI传入错误格式的参数(如filePath必须是字符串),降低工具调用失败概率。
步骤2:初始化AI模型,并绑定工具
定义好工具后,需要创建AI模型实例,并将工具绑定到模型上——这样AI才能“知道”自己有哪些工具可以调用,这是实现工具调用的关键一步。
代码解析(核心部分)
// 1. 初始化AI模型(兼容OpenAI API的模型,此处用qwen-plus)
const model = new ChatOpenAI({
modelName: "qwen-plus", // 模型名称(选择更强大的模型,提升思考能力)
apiKey: process.env.OPENAI_API_KEY, // 从环境变量获取API密钥(安全规范)
temperature: 0, // 随机性为0,确保AI输出稳定、可预测(适合工具调用场景)
configuration: {
baseURL: process.env.OPENAI_API_BASE, // 可选:指定API基础路径(如对接本地模型)
}
});
// 2. 整理所有工具,绑定到模型上
const tools = [readFileTool, writeFileTool, executeCommandTool, listDirectoryTool];
const modelWithTools = model.bindTools(tools); // 关键:将工具绑定到模型,AI可调用
关键知识点
temperature: 0:工具调用场景中,随机性越低越好,避免AI乱调用工具或输出无关内容;如果是生成类场景,可适当提高。- API密钥配置:通过
dotenv/config引入环境变量,避免将密钥硬编码(代码开头的import 'dotenv/config'),符合开发规范。 bindTools(tools):LangChain的核心方法,将工具列表绑定到模型,底层会自动处理AI工具调用的逻辑(无需手动实现AI与工具的通信)。
步骤3:实现AI Agent的循环思考逻辑(核心:迭代调用工具)
Cursor的智能性体现在「循环迭代」:AI并非一次调用工具就能完成任务,可能需要多次调用工具(如先创建项目,再修改文件,再启动服务),因此需要编写循环逻辑,让AI根据工具执行结果,持续思考下一步操作。
代码解析(runAgentWithTools函数,核心中的核心)
async function runAgentWithTools(query,maxIterations = 30) {
// 1. 初始化消息列表(存储系统提示、用户需求、AI回复、工具执行结果)
const messages = [
new SystemMessage(`
你是一个项目管理助手,使用工具完成任务。
当前工作目录是:${process.cwd()}
工具:1. read_file 2. write_file 3. execute_command 4. list_directory
重要规则 - execute_command:(重点告知AI工具使用规范)
- workingDirectory 参数会自动切换到指定目录,不要在command中使用 cd命令
回复要简洁,只说做了什么
`),
new HumanMessage(query), // 用户输入的需求(如创建React TodoList)
];
// 2. 循环迭代(最多迭代30次,避免无限循环)
for (let i = 0; i < maxIterations; i++) {
console.log(chalk.bgBlue('⏳等待AI思考...'));
// 3. AI思考:根据消息列表,判断是否需要调用工具
const response = await modelWithTools.invoke(messages);
messages.push(response); // 将AI回复加入消息列表
// 4. 判断:如果AI不调用工具,说明任务完成,返回最终回复
if(!response.tool_calls || response.tool_calls.length === 0){
console.log(`\n AI 最终回复: ${response.content}`);
return response.content;
}
// 5. 如果AI调用工具,执行工具并获取结果,加入消息列表
for (const toolCall of response.tool_calls) {
const foundTool = tools.find(tool => tool.name === toolCall.name); // 匹配工具
if(foundTool){
const toolResult = await foundTool.invoke(toolCall.args); // 执行工具
// 将工具执行结果加入消息列表,供AI下次思考参考
messages.push(new ToolMessage({
content: toolResult,
tool_call_id: toolCall.id,
}));
}
}
}
// 迭代上限,返回最后一次AI回复
return messages[messages.length - 1].content;
}
循环思考逻辑拆解(关键步骤)
-
消息列表(messages) :核心载体,存储所有交互信息(系统提示、用户需求、AI回复、工具结果),AI每次思考都会基于整个消息列表,确保逻辑连贯。
-
系统提示(SystemMessage) :关键配置,用于告知AI「身份」「可用工具」「使用规则」,避免AI乱调用工具(如代码中强调executeCommandTool的使用规范,避免cd命令错误)。
-
AI思考(modelWithTools.invoke(messages)) :调用绑定工具的模型,AI会分析消息列表,判断是否需要调用工具:
- 如果不需要调用工具(response.tool_calls为空),说明任务完成,返回最终结果。
- 如果需要调用工具,会返回tool_calls数组(包含工具名称、入参、唯一ID)。
-
工具执行与结果回传:根据AI返回的tool_calls,匹配对应的工具,执行工具并获取结果,用ToolMessage将结果加入消息列表——这样AI下次思考时,就能基于工具执行结果,规划下一步操作(如执行完pnpm install后,再执行pnpm run dev)。
-
迭代上限(maxIterations) :避免AI陷入无限循环(如工具调用失败后,AI反复调用同一工具),设置合理的迭代次数(代码中默认30次)。
步骤4:测试Cursor功能(传入需求,执行Agent)
定义好Agent后,传入具体需求(如创建React TodoList),调用runAgentWithTools函数,即可实现Cursor的完整功能——AI会自主调用工具,完成需求。
代码解析(测试用例)
// 定义用户需求(创建功能丰富的React TodoList)
const case1 = `
创建一个功能丰富的 React TodoList 应用:
1. 创建项目:echo -e "n\nn" | pnpm create vite react-todo-app --template react-ts
2. 修改 src/App.tsx,实现完整功能的 TodoList:(添加、删除、编辑等)
3. 添加复杂样式和动画
4. 列出目录确认,安装依赖并启动服务器
`;
// 执行Agent,测试功能
try{
await runAgentWithTools(case1);
}catch(error){
console.log(chalk.red(`[错误] 运行代理时出错: ${error.message}`));
}
测试逻辑:用户传入case1需求后,AI会逐步执行以下操作(自主调用工具):
- 调用executeCommandTool,执行
echo -e "n\nn" | pnpm create vite react-todo-app --template react-ts,创建React项目。 - 调用executeCommandTool,指定workingDirectory为react-todo-app,执行
pnpm install安装依赖。 - 调用writeFileTool,修改src/App.tsx,实现TodoList功能、样式和动画。
- 调用listDirectoryTool,列出目录,确认项目结构正确。
- 调用executeCommandTool,执行
pnpm run dev,启动服务器。 - 所有操作完成后,AI返回最终结果,任务结束。
三、核心难点与注意事项(避坑重点)
1. 工具调用的异常处理
每个工具都必须添加try-catch捕获异常(如文件路径错误、命令执行失败),否则工具调用崩溃会导致整个Agent停止运行(代码中每个工具都有完善的异常处理,可直接参考)。
2. AI工具调用规范的约束
系统提示(SystemMessage)中必须明确工具的使用规则,尤其是复杂工具(如executeCommandTool),避免AI出现低级错误(如代码中强调“不要在command中使用cd命令”,否则会导致目录找不到)。
3. 消息列表的维护
每次AI回复、工具执行结果,都必须加入messages列表——AI的思考是基于整个消息列表的,如果遗漏,会导致AI逻辑断裂(如忘记工具执行结果,重复调用同一工具)。
4. 模型选择与配置
工具调用场景需要选择「逻辑能力强」的模型(如代码中的qwen-plus),避免使用轻量模型(如qwen-code-turbo),否则AI可能无法正确分析需求、选择工具;temperature设为0,确保输出稳定。
四、总结(手写Cursor的核心流程)
结合本次代码,手写Cursor的核心流程可总结为: 定义工具(tool函数)→ 初始化模型并绑定工具 → 编写Agent循环思考逻辑 → 传入需求测试 其中,「工具定义的规范性」「Agent的循环迭代逻辑」是核心难点,也是Cursor能否正常工作的关键。
本次代码已实现一个可直接运行的简易Cursor,可基于此扩展更多工具(如网络请求、数据库操作),让AI能完成更复杂的任务(如项目部署、数据处理),本质还是围绕「AI思考+工具调用」的核心逻辑。