最新下载
热门教程
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
我花了两小时找了一个数据结构 bug:才把迷你 Cursor跑起来:绝望(bushi)
时间:2026-07-18 08:37:02 编辑:袖梨 来源:一聚教程网
一、今天的目标:手写一个"迷你 Cursor"
1.1 要实现什么?
老师说:

用大白话说,就是让 AI 自己完成这件事:
复制代码"给我用 Vite 创建一个 React TodoList,功能齐全、样式漂亮、能跑起来。"
要做到这件事,Agent 需要哪些能力?
| 需要的能力 | 对应的 Tool | 比喻 |
|---|---|---|
| 创建项目目录和文件 | write_file | AI 的手,能写文件 |
| 修改已有文件 | read_file + write_file | AI 的眼睛+手 |
| 查看项目结构 | list_directory | AI 的导航 |
| 安装依赖/启动项目 | exec_command | AI 的脚,能运行命令 |
老师说一个 Agent 要完成的步骤:
1.2 四个工具,一个都不能少
在 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';
readFileTool:读文件
复制代码const readFileTool = tool(
async ({filePath}) => {
const content = await fs.readFile(filePath, 'utf-8');
return content;
},
{
name: 'read_file',
description: '读取文件内容',
schema: z.object({
filePath: z.string().describe('文件路径'),
}),
}
)
writeFileTool:写文件(自动创建目录)
复制代码const writeFileTool = tool(
async ({filePath, content}) => {
const dir = path.dirname(filePath);
await fs.mkdir(dir, { recursive: true }); // 自动创建目录
await fs.writeFile(filePath, content, 'utf-8');
return `成功写入 ${filePath}`;
},
{
name: 'write_file',
description: '写入文件内容,自动创建目录',
schema: z.object({
filePath: z.string().describe('文件路径'),
content: z.string().describe('要写入的文件内容'),
}),
}
)
fs.mkdir(dir, { recursive: true }) 是"自动创建父目录"的魔法——如果 dir 不存在,它会把所有父目录都建好。
ListDirectoryTool:列出目录
复制代码const ListDirectoryTool = tool(
async ({workingDirectory}) => {
const files = await fs.readdir(workingDirectory);
return `目录内容:n ${files.join('n')}`;
},
{
name: 'list_directory',
description: '列出指定目录下的所有文件和文件夹',
schema: z.object({
workingDirectory: z.string().describe('目录路径'),
}),
}
)
execCommandTool:执行命令
复制代码const execCommandTool = tool(
async ({command, workingDirectory}) => {
const cwd = workingDirectory || process.cwd();
return new Promise((resolve, reject) => {
const [cmd, ...args] = command.split(' ');
const child = spawn(cmd, args, {
cwd,
stdio: 'inherit',
shell: true,
});
// ... 错误处理和返回
});
},
{
name: 'exec_command',
description: '执行系统命令,支持指定工作目录',
schema: z.object({
command: z.string().describe('要执行的命令'),
workingDirectory: z.string().describe('工作目录'),
}),
}
)
二、踩坑记:数据结构错误找了两小时
2.1 问题出在哪?
看 mini-cursor.mjs 中 ReAct 循环的核心代码:
复制代码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);
// ↑ 这里传的是 args 对象
messages.push(new ToolMessage({
content: toolResult,
tool_call_id: toolCall.id,
}));
}
}
toolCall.args 传进来的是一个对象: { filePath: "src/tool.mjs" }。
而在 all-tools.mjs 中,工具的 async 函数是解构参数的:
复制代码async ({filePath}) => { ... }
// 从对象中解构出 filePath
这是对的。
但我在一开始写的时候,写成了这样:
复制代码// 错误写法
async (filePath) => { ... }
// filePath 是一个对象 { filePath: "..." },不是字符串 "..."// 然后我试图用 fs.readFile(filePath)
// 但实际上 filePath = { filePath: "src/tool.mjs" }
// 读文件 → 报错 → 找两小时
async (filePath) 和 async ({filePath}) 之间差了一个花括号,但这一个花括号,让我调试了两小时。
2.2 为什么差一个括号差这么多?
复制代码// invoke 传进来的是:{ filePath: "src/tool.mjs", content: "..." }// 解构参数:直接从对象里拿出 filePath
async ({filePath}) => {
await fs.readFile(filePath, 'utf-8'); // filePath = "src/tool.mjs"
}// 普通参数:filePath 是整个对象
async (filePath) => {
await fs.readFile(filePath, 'utf-8');
// filePath = { filePath: "src/tool.mjs" }
// fs.readFile({ filePath: "..." }) → 报错
}
invoke 传递的是对象,函数必须用解构才能拿到里面的具体值。
2.3 血的教训
老师说:
LangChain 帮你做了很多事,但参数传递的格式,你必须和它保持一致。 tool() 第二个参数定义了 schema,invoke 时就会按 schema 的结构传参。
写代码的时候,参数类型比逻辑更容易出错。 逻辑错了能跑但结果不对,参数类型错了直接报错——而且报错信息往往含糊不清。
三、System Prompt 里的"潜规则":比代码更重要的提示词
mini-cursor.mjs 的 System Prompt 里有一段特别重要的内容:
复制代码new SystemMessage(`
...重要规则 - execute_command:
- workingDirectory 参数会自动切换到指定目录
- 当使用 workingDirectory 时,绝对不要在 command 中使用 cd
- 错误示例: { command: "cd react-todo-app && pnpm install", workingDirectory: "react-todo-app" }
这是错误的!因为 workingDirectory 已经在 react-todo-app 目录了,再 cd react-todo-app 会找不到目录
- 正确示例: { command: "pnpm install", workingDirectory: "react-todo-app" } 重要规则 - 文件路径:
- 项目文件在 react-todo-app/src/ 目录下
- 读写文件时,路径要带上 react-todo-app/ 前缀 重要规则 - 执行顺序:
- 启动 dev server (pnpm run dev) 必须是最后一步
- 因为它是长进程不会退出
`)
这些"潜规则"写在 System Prompt 里,比写在代码里还重要。
因为代码是固定的,但 LLM 的行为是可塑的。如果你不告诉 LLM:
- 不要在
workingDirectory里再cd——它会写出cd dir && pnpm install,然后在dir目录下再cd dir,找不到路径。 - 文件名要带前缀——它会写
src/App.tsx而不是react-todo-app/src/App.tsx。 - dev server 要最后启动——它会先启动 server,再装依赖,结果 server 跑了,依赖没装完。
System Prompt 是 Agent 的"操作手册",写得越细,AI 犯错越少。
四、完整的 ReAct 循环 + Promise.all
mini-cursor.mjs 的 ReAct 循环比上节课的版本更完善:
复制代码async function runAgentWithTools(query, maxIterations = 30) {
const messages = [
new SystemMessage(`...`),
new HumanMessage(query),
]; for (let i = 0; i < maxIterations; i++) {
const response = await modelWithTools.invoke(messages);
messages.push(response); // 没有工具调用 → 直接返回答案
if (!response.tool_calls || response.tool_calls.length === 0) {
console.log(`n 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,
}));
}
}
}
}
重复一下 ReAct 循环的上节课学到的三个动作:
| 步骤 | 代码 | 说明 |
|---|---|---|
| Reason | modelWithTools.invoke(messages) | LLM 思考下一步 |
| Act | foundTool.invoke(toolCall.args) | 执行工具 |
| Observe | new ToolMessage({ tool_call_id }) | 观察工具结果 |
循环条件: response.tool_calls 不为空。
终止条件: LLM 认为不需要再调工具,直接生成答案。
五、案例:从零创建一个 TodoList 项目
case1 的内容是:
复制代码创建以一个功能丰富的 React TodoList 应用:
1. npm create vite@latest react-todo-app -- --template react-ts
2. 修改 src/App.tsx,实现完整功能的 TodoList
- 添加、删除、标记完成
- 分类筛选(全部/进行中/已完成)
- 统计信息显示
- localStorage 数据持久化
3. 添加复杂样式(渐变背景、卡片阴影、圆角、悬停效果)
4. 添加动画(添加/删除时的过渡动画,用 CSS Transitions)
5. 列出目录确定注意:使用 pnpm,功能要完整,样式要美观,要有动画效果
Agent 的工作流程大概是这样:
| 轮次 | 思考 | 行动 | 观察 |
|---|---|---|---|
| 1 | 需要先创建项目 | exec_command: "npm create vite@latest..." | 项目创建成功 |
| 2 | 需要写代码 | write_file: "react-todo-app/src/App.tsx" | 文件写入成功 |
| 3 | 需要写样式 | write_file: "react-todo-app/src/App.css" | 写入成功 |
| 4 | 需要安装依赖 | exec_command: "pnpm install" | 安装成功 |
| 5 | 看看目录结构 | list_directory: "react-todo-app/src" | 文件都在 |
| 6 | 启动服务器 | exec_command: "pnpm run dev" | 服务器启动成功 |
每一步都在 ReAct 循环里自动完成——完全不需要人工干预。
六、总结:迷你 Cursor 的架构
| 层 | 文件 | 做了什么 |
|---|---|---|
| 工具层 | all-tools.mjs | 定义 4 个工具(读/写/列表/执行命令) |
| Agent 层 | mini-cursor.mjs | ReAct 循环 + 工具调用 |
| 子进程层 | node-exec.mjs | spawn 执行 CLI 命令 |
| System Prompt | mini-cursor 内联 | 给 LLM 的操作规则 |
"迷你 Cursor"的架构,和真正的 Cursor/Claude Code 是同一个思路。 只不过真正的产品有更完善的错误处理、更丰富的工具集、更复杂的任务编排。
写在最后
今天最大的收获有两个。第一个是终于写出了一个能自己干活的编程 Agent——从创建项目到启动服务器,全程自动化。第二个是花了两小时跟一个括号过不去——async (filePath) 和 async ({filePath}) 的区别,让我深刻理解了"参数解构"这件事。
老师说后面还要继续学,估计会讲到多智能体协作(LangGraph)和更复杂的任务编排。我已经准备好了——下次再遇到 bug,我会先检查括号。
下次面试官问你:"Agent 怎么执行多个工具调用?"
你可以淡定地说:
然后看着面试官满意的表情,心里默念:这波,又稳了。
本文所有代码示例均来自课堂学习资料,真实可运行。
相关文章
- hive复杂数据类型性能如何 07-18
- B站下载器app极速下载入口-哔哩哔哩as软件BILIBILIAS安卓最新版安装 07-18
- hive复杂数据类型如何定义 07-18
- hive复杂数据类型使用详解 07-18
- hive复杂数据类型有哪些种类 07-18
- hive加载数据都有哪些工具 07-18