如果你用过 Cursor 或者 GitHub Copilot,一定会惊叹于它们的能力:你只需要说一句“帮我建个 React 项目并启动”,AI 就会自动敲命令行、写代码、甚至帮你把服务器跑起来。
这种“自主编程 Agent”听起来很黑科技,但其实核心逻辑并不复杂。今天,我们通过一份不到 100 行的核心代码(基于 LangChain + DeepSeek),来手写一个属于我们自己的 mini-cursor,看看 AI 究竟是如何从“只会聊天”变成“能自动干活的程序员”的。
第一步:准备任务“剧本”与“工具箱”
要让 AI 干活,首先得给它布置任务,并给它提供干活的工具。
1. 布置任务(case1)
在代码中,case1 变量就是一份自然语言书写的需求文档:
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. 添加动画 ...
5. 列出目录确定
...
`
你看,AI 并不需要你写极其严格的代码指令,你只需要像给人类实习生布置任务一样,把步骤1234写清楚就行。
2. 装配工具(Tools) AI 本身无法操作你的电脑。我们需要导入并绑定四个基础的 I/O 工具:
const tools = [ readFileTool, writeFileTool, listDirectoryTool, executeCommandTool ];
const modelWithTools = model.bindTools(tools);
bindTools 这个动作,就像是把一把螺丝刀、一把锤子递给 AI。一旦绑定,大模型在思考时,就不局限于输出文本,而是可以选择“调用某个工具”。
第二步:给 AI 戴上“紧箍咒”(System Prompt)
这是整个代码中最容易被忽略,却极其重要的部分。在进入循环前,我们给 AI 塞入了一段长长的 SystemMessage:
new SystemMessage(`你是一个项目管理助手,使用工具完成任务。
...
重要规则 - execute_command:
- workingDirectory 参数会自动切换到指定目录
- 当使用 workingDirectory 时,绝对不要在 command 中使用 cd
- 错误示例: { command: "cd react-todo-app && pnpm install", workingDirectory: "react-todo-app" }
...
`)
为什么要写这么啰嗦的规则?
因为在实际执行命令时,如果我们的工具底层已经把工作目录(cwd)切换到了 react-todo-app,AI 如果自作聪明在命令行里再写一次 cd react-todo-app,就会导致路径重复,直接报错找不到文件。
通过提供“错误示例”和“正确示例”,能给大模型极大的约束,大幅减少它“乱执行”的概率。
第三步:Agent 的心脏 —— ReAct 循环
准备工作做完,进入真正的自动化环节。runAgentWithTools 函数里的那个 for 循环,就是这个 Agent 的心脏,它实现了 ReAct(思考+行动) 机制。
1. 记忆体(messages) Agent 需要记住自己做过什么。所以一开始初始化一个数组,包含系统指令和用户的需求:
const messages = [
new SystemMessage("..."),
new HumanMessage(query)
];
2. 核心循环运转
在每次循环中,程序会把包含完整历史的 messages 发给大模型:
const response = await modelWithTools.invoke(messages);
messages.push(response);
模型此时会返回它“思考的结果”。如果任务还没完成,它会返回一个 tool_calls 数组,告诉程序:“我要用 execute_command 工具,执行 pnpm install,目录是 react-todo-app。”
3. 执行工具并反馈 程序看到这个请求后,立马找到对应的工具去本地真实执行:
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);
// 将结果塞回给 AI
messages.push(new ToolMessage({
content: toolResult,
tool_call_id: toolCall.id
}))
}
}
这里的 toolResult 是工具返回的字符串(比如“文件写入成功”或“命令报错信息”)。程序把它包装成 ToolMessage 塞回 messages,AI 在下一轮循环时,就能看到这个结果。如果成功了,它继续做下一步;如果失败了,它能看到报错并自我纠正。
第四步:最后的“保险丝”
如果你仔细看代码的最底部,有一行看似奇怪的代码:
setTimeout(() => {
console.log("⏰ 超时兜底强制退出进程");
process.exit(0);
}, 1000000);
为什么要写一个 100 万毫秒(约 16 分钟)的强制退出机制?
因为在这个 case1 任务的最后一步是:pnpm run dev 启动服务器。
只要服务器一直开着,Node.js 的子进程就不会退出,这会导致 Agent 的循环一直挂起,永远等不到命令结束的信号。这就好比人一直在那站着等天亮,程序直接死锁。
这个 setTimeout 就是最后的保险丝,确保无论 AI 和子进程卡成了什么样,时间一到,程序强行拔电源退出,避免你的终端被彻底卡死。
总结:Mini-cursor 的极简骨架
拆解下来你会发现,这个“手写 mini-cursor”的骨架无比清晰:
- 定义工具:把读写文件、执行命令封装成 AI 能调用的 API。
- 制定规则:用 System Prompt 严令禁止容易报错的路径操作(如乱用
cd)。 - ReAct 循环:让 AI 自己思考、提需求、程序去执行、结果反馈给 AI,循环往复,直到任务结束。
当然,真实的 Cursor 在工具容错、文件搜索、代码 diff 匹配上做了极其复杂的工程化处理。但通过这段代码,我们已经完全触摸到了“编程 Agent”的核心灵魂——让大模型在拥有记忆的前提下,通过不断调用工具并接受环境反馈,去逼近最终目标。