mini-cursor 揭秘:从 Tool 定义到 Agent 循环的完整实现

mini-cursor 揭秘:从 Tool 定义到 Agent 循环的完整实现

深入剖析一个迷你 Cursor 风格 AI Agent 的底层原理------4 个工具 + 1 个循环 = 一个能自动写项目的 AI。


一、mini-cursor 是什么?

这是一个极简版的 Cursor AI Agent。你给它一段需求描述,它就能自动完成以下所有事情:

  1. 用 Vite 脚手架创建 React 项目
  2. 编写完整的业务代码(增删改查、分类筛选、数据持久化)
  3. 添加样式和动画
  4. 安装依赖,启动开发服务器

整个过程不需要你碰一下键盘。而这背后支撑它的,只有两个核心文件:

文件 职责 行数
all_tools.mjs 定义 AI 可以使用的"手和脚" 132 行
main.mjs 驱动 Agent 循环的"大脑" 113 行

200 多行代码,实现了一个能自动干活的 AI Agent。下面我们逐一拆解。


二、all_tools.mjs:赋予 AI 四种"超能力"

这个文件定义了 4 个 Tool。每个 Tool 的结构在上篇文章中已经详细分析过(处理函数 + name + description + schema),这里我们聚焦于每个工具的设计巧思

2.1 read_file ------ 最基础的眼

js 复制代码
const readFileTool = tool(
    async ({ filePath }) => {
        const content = await fs.readFile(filePath, 'utf-8');
        return `文件内容: \n${content}`;
    },
    {
        name: 'read_file',
        description: '读取指定路径的文件内容',
        schema: z.object({
            filePath: z.string().describe('文件路径')
        })
    }
)

最朴素的工具,但注意返回值被包了一层 文件内容: \n 前缀------这是给模型看的"格式化标签",帮助它区分"这是文件内容"和"这是系统消息"。

2.2 write_file ------ 能写,还能自动建目录

js 复制代码
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}`;
    },
    { ... }
)

这里的巧思是 fs.mkdir(dir, { recursive: true })。模型在写文件时,可能写出这样的路径:src/components/TodoList.tsx。如果 src/components/ 目录不存在,普通 writeFile 会直接报错。加上自动递归创建目录后,AI 可以放心地写任意深度的文件路径,不用先手动建目录。

这是一个工程化意识的体现------不要让 AI 做它容易出错的事(比如先 ls 再 mkdir 再 write),而是在工具层面就把容错做好。

2.3 execute_command ------ 最复杂、最危险、也最强大的工具

js 复制代码
const executeCommandTool = tool(
    async ({ command, workingDirectory }) => {
        const cwd = workingDirectory || process.cwd();
        return new Promise((resolve) => {
            const [cmd, ...args] = command.split(' ');
            const child = spawn(cmd, args, {
                cwd,
                stdio: 'inherit',  // 实时输出到终端
                shell: true
            });
            child.on('close', (code) => {
                if (code === 0) {
                    resolve(`命令行执行成功: ${command}`);
                } else {
                    process.exit(code || 1); // 失败直接退出整个进程
                }
            });
        });
    },
    {
        name: 'execute_command',
        description: '执行系统命令,支持指定工作目录,实时显示输出',
        schema: z.object({
            command: z.string().describe('要执行的命令'),
            workingDirectory: z.string().optional().describe('指定工作目录')
        })
    }
)

这个工具有几个设计要点值得展开:

a) spawn + stdio: 'inherit' ------ 实时输出

不用 exec(它会缓冲全部输出然后一次性返回),而是用 spawn + stdio: 'inherit'。这意味着 pnpm install 的进度条、pnpm run dev 的启动日志,都会实时显示在终端。用户体验拉满。

b) workingDirectory 参数 ------ 教模型不要用 cd

注意 SystemMessage 里反复强调的规则:

css 复制代码
错误示例: { command: "cd react-todo-app && pnpm install", workingDirectory: "react-todo-app" }
正确示例: { command: "pnpm install", workingDirectory: "react-todo-app" }

这是实际使用中踩出来的坑------模型天然喜欢用 cd && command 的方式切换目录,但如果 workingDirectory 已经指定了目录,再 cd 就会路径叠加导致失败。解决方式不是改代码逻辑,而是在 System Prompt 里反复教育模型

c) 失败时 process.exit(code || 1) ------ 直接退出

这是一个激进但合理的选择。命令行执行失败通常意味着不可恢复的错误(比如 pnpm create vite 失败了,后面所有操作都无意义),与其让 AI 在一个残破的状态下继续"修修补补",不如直接终止,让用户检查环境后重来。

2.4 list_directory ------ AI 的"ls 命令"

js 复制代码
const listDirectoryTool = tool(
    async ({ directoryPath }) => {
        const files = await fs.readdir(directoryPath);
        return `目录内容:\n ${files.map(f => `- ${f}`).join('\n')}`;
    },
    {
        name: 'list_directory',
        description: '列出指定目录下所有文件和文件夹',
        schema: z.object({ directoryPath: z.string().describe('目录路径') })
    }
)

最轻量的工具,但它的存在很重要------它是 AI 验证自己工作成果 的唯一手段。AI 写完文件后调用 list_directory,就能确认"我真的创建成功了"。

2.5 四个工具的协作全景

arduino 复制代码
                    ┌─────────────┐
                    │  list_dir   │  ← "眼睛":看看有什么
                    └─────────────┘
                    ┌─────────────┐
                    │  read_file  │  ← "眼睛":看看里面是什么
                    └─────────────┘
                    ┌─────────────┐
                    │ write_file  │  ← "手":创造/修改文件
                    └─────────────┘
                    ┌─────────────┐
                    │exec_command │  ← "手":执行命令(npm、git...)
                    └─────────────┘

眼睛(读)+ 手(写+执行)= 一个完整的开发者。 这就是 mini-cursor 的全部能力集合。


三、main.mjs:Agent 循环的深度解剖

如果说 all_tools.mjs 是给 AI 装上了"手脚",那 main.mjs 就是中枢神经系统------它决定了 AI 怎么思考、何时动手、如何调整。

3.1 初始化:装配阶段

js 复制代码
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 }
});

const tools = [readFileTool, writeFileTool, executeCommandTool, listDirectoryTool];
const modelWithTools = model.bindTools(tools);

temperature: 0 这个设置很关键。在 Agent 场景下,我们不希望模型"发挥创意"------工具调用的参数必须精准,执行顺序必须合理。temperature 为 0 最大程度保证了行为的一致性和可预测性。

3.2 核心函数 runAgentWithTools ------ 步步拆解

js 复制代码
async function runAgentWithTools(query, maxIterations = 30) {

maxIterations = 30 是一个熔断机制。没有它,如果模型陷入"调用工具 → 不满意 → 再调用 → 再不满意"的死循环,程序永远不会停。30 次迭代是一个安全上限------既能完成复杂任务(创建项目+写代码+安装依赖+启动,大约 10-15 轮),又不会无限消耗 token。

第一层:构造初始对话
js 复制代码
const message = [
    new SystemMessage(`
       你是一个项目管理助手,使用工具完成任务。
       当前工作目录: ${process.cwd()}

       工具:
       1. read_file: 读取文件
       2. write_file: 写入文件
       3. execute_command: 执行命令 (支持workingDirectory 参数)
       4. list_directory: 列出目录

       重要规则 - execute_command:
       - workingDirectory 参数会自动切换到指定目录
       - 当使用workingDirectory 参数时,不要在command中使用cd 命令
       - 错误示例: { command: "cd react-todo-app && pnpm install", workingDirectory: "react-todo-app" }
       - 正确示例: { command: "pnpm install", workingDirectory: "react-todo-app" }
       
       回复要简洁,只说做了什么
    `),
    new HumanMessage(query),
];

这里的 System Prompt 设计值得细品:

  1. 当前工作目录: ${process.cwd()}------给模型提供上下文锚点,让它知道自己在哪
  2. 工具清单 + 编号------简洁列出能力,不啰嗦
  3. execute_command 的专项规则------把踩过的坑写进 prompt,用正反示例教育模型
  4. "回复要简洁"------控制输出长度,节省 token
第二层:主循环------Agent 的心脏
js 复制代码
for (let i = 0; i < maxIterations; i++) {
    console.log(chalk.bgGreen('⏳正在等待AI思考...'));
    const response = await modelWithTools.invoke(message);
    message.push(response);

    // 分支1:没有 tool_calls → 任务完成
    if (!response.tool_calls || response.tool_calls.length === 0) {
        console.log(`\n AI 最终回复: \n ${response.content}\n`);
        return response.content;
    }

    // 分支2:有 tool_calls → 执行工具
    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);
            message.push(new ToolMessage({
                content: toolResult,
                tool_call_id: toolCall.id
            }));
        }
    }
}

这里和上一篇 tool-file-read.mjs 中的 while 循环有微妙但重要的区别:

特性 tool-file-read.mjs main.mjs
循环方式 while(response.tool_calls) for(let i=0; i<maxIterations; i++)
安全上限 无(理论上可能死循环) 30 次强制熔断
工具执行 Promise.all 并行 for...of 串行
找不到工具 返回错误信息给模型 静默跳过
为什么用 for 而不是 while

生产级 Agent 必须有熔断。一个复杂任务可能让模型反复调整(写了代码 → 不满意 → 改了再写 → 又发现新问题 → 继续改),如果没有上限,极端情况下可能跑上百轮,消耗大量 token 和时间。

为什么 for...of 串行而不是 Promise.all 并行?

对于文件操作和命令执行 ,顺序很重要------你不能在 mkdir 完成之前就开始 writeFile。串行执行保证了操作的因果顺序。这是一个务实的选择:牺牲一点性能,换取正确性保证。

为什么找不到工具时"静默跳过"?

注意这段代码:

js 复制代码
const foundTool = tools.find(t => t.name === toolCall.name);
if (foundTool) {
    // 执行...
}
// 没找到?什么都不做,跳过

对比 tool-file-read.mjs 中返回错误信息的做法,这里的设计哲学不同:mini-cursor 更偏向"自动驾驶",它不希望因为一个工具调用出错就打乱整个流程。如果模型幻觉了一个不存在的工具名,跳过它让模型在下一轮自己调整,比显式报错更稳健。

3.3 整个循环的完整心智模型

用一次真实的任务执行来还原这个过程:

makefile 复制代码
📝 任务: "创建一个 React TodoList 应用"

━━━ 第 1 轮 ━━━
模型思考: 我需要先创建项目
tool_call → execute_command("pnpm create vite react-todo-app --template react-ts")
工具执行: ✅ Vite 项目创建成功
message 状态: [SystemMessage, HumanMessage, AIMessage(tool_call), ToolMessage(结果)]

━━━ 第 2 轮 ━━━
模型思考: 项目创建好了,现在修改 App.tsx
tool_call → read_file("react-todo-app/src/App.tsx")
工具执行: ✅ 读取到默认模板代码
message 状态: [...之前的消息, AIMessage(tool_call), ToolMessage(结果)]

━━━ 第 3 轮 ━━━
模型思考: 我看到了模板代码,现在重写它,实现增删改查+筛选+持久化
tool_call → write_file("react-todo-app/src/App.tsx", "完整TodoList代码...")
工具执行: ✅ 文件写入成功
message 状态: [...之前的消息, AIMessage(tool_call), ToolMessage(结果)]

━━━ 第 4 轮 ━━━
模型思考: 代码写完了,需要添加样式
tool_call → write_file("react-todo-app/src/App.css", "渐变背景+动画样式...")
工具执行: ✅ 样式写入成功

━━━ 第 5 轮 ━━━
模型思考: 确认文件都在,然后安装依赖
tool_call → list_directory("react-todo-app/src")
工具执行: ✅ App.tsx, App.css, main.tsx...
tool_call → execute_command("pnpm install", workingDirectory="react-todo-app")
工具执行: ✅ 依赖安装完成

━━━ 第 6 轮 ━━━
模型思考: 一切就绪,启动开发服务器
tool_call → execute_command("pnpm run dev", workingDirectory="react-todo-app")
工具执行: ✅ Vite 开发服务器启动在 localhost:5173

━━━ 第 7 轮 ━━━
模型思考: 任务完成,不需要再调用工具了
response.content → "已创建功能丰富的 TodoList 应用,包含增删改查、筛选..."
response.tool_calls → undefined  ← 循环终止

每一轮,模型都在观察 → 规划 → 行动 → 观察结果 → 调整计划。这就是 Agent 的"思考-行动循环"(ReAct Loop)。

3.4 测试用例:一个能"自我实现"的任务

js 复制代码
const casel = `
创建一个功能丰富的 React TodoList 应用:

1. 创建项目:pnpm create vite react-todo-app --template react-ts
2. 修改 src/App.tsx,实现完整功能的 TodoList:
   - 添加、删除、编辑、标记完成
   - 分类筛选(全部/进行中/已完成)
   - 统计信息显示
   - localStorage 数据持久化
3. 添加复杂样式:渐变背景、卡片阴影、圆角、悬停效果
4. 添加动画:添加/删除时的过渡动画
5. 确认并启动

注意:使用 pnpm,功能要完整,样式要美观,要有动画效果
`;

await runAgentWithTools(casel);

这个 casel 字符串就是整个任务的唯一输入 。它不是代码,不是配置文件,只是一段自然语言描述。然后 runAgentWithTools 会驱动 AI 一步步完成所有操作。

注意任务描述的写法也很有讲究:

  • 步骤编号(1, 2, 3...)------帮助模型理解执行顺序
  • 具体细节(渐变背景蓝到紫、CSS transitions)------减少模型自由发挥的空间,确保输出符合预期
  • "注意"部分------强调关键约束(用 pnpm 而不是 npm,功能要完整)

四、两种架构的对观:Simple Agent vs Mini-Cursor

把上篇的 tool-file-read.mjs 和本篇的 main.mjs 放在一起对比,能看到 Agent 设计从"玩具"到"工具"的进化路径:

维度 tool-file-read.mjs main.mjs (mini-cursor)
工具数量 1 个 4 个
循环控制 while 无上限 for + maxIterations 熔断
并行策略 Promise.all 并行 for...of 串行(保证因果顺序)
错误处理 捕获并返回给模型 静默跳过 + 关键失败直接 exit
用户体验 纯文本输出 chalk 彩色输出 + 实时终端
提示词设计 简单一句话 带正反示例的规则教育
使用场景 读一个文件 创建一个完整项目

进化路线的核心趋势是:从"让模型能用工具"到"让模型用对工具、用好工具"


五、mini-cursor 的架构精髓:三个关键词

5.1 "熔断"

maxIterations = 30 不是随便写的数字。太低了复杂任务完不成,太高了可能浪费资源。它体现了 Agent 设计的核心权衡:自主性 vs 安全性

5.2 "教育"

System Prompt 里的正反示例(错误示例: cd + workingDirectory)说明了一个重要经验:工具设计的问题,很多时候要在 Prompt 层面解决,而不是代码层面。工具的参数 schema 只管类型,管不了"参数的语义正确性"------后者只能靠 Prompt 教育模型。

5.3 "闭环"

erlang 复制代码
观察(读文件/列目录)→ 规划(模型推理)→ 执行(写文件/跑命令)→ 验证(再读/再列)→ ...

这四步构成了一个完整的反馈闭环。AI 不再是"一把梭写完不管对错",而是像真正的开发者一样------写完代码,跑一下,看看报错,修改,再跑,直到成功。


六、总结

200 多行代码,4 个工具,1 个循环,就能让 AI 从"聊天机器人"变成"能独立完成项目的开发者"。

核心公式依然不变:

复制代码
定义 Tool(all_tools.mjs) → 绑定 Tool(bindTools) → 循环调用(主循环)

但 mini-cursor 在工程化上往前多走了几步:熔断机制、正反示例教育模型、串行保证因果顺序、实时终端反馈------这些细节让它从一个能跑的 Demo,变成了一个真正能用的工具

如果你要写自己的 Agent,mini-cursor 的架构是一个很好的起点------足够简单能看懂,又足够完整能干活。

相关推荐
weelinking1 小时前
【产品】00_产品经理用Claude实现产品系列介绍
数据库·人工智能·sql·数据挖掘·github·产品经理
Agent产品评测局1 小时前
制造业模具管理AI系统,主流产品能力对比详解:2026年智能制造选型深度洞察
人工智能·ai·chatgpt·制造
canonical_entropy2 小时前
从 Spec-Driven Development 到 Attractor-Guided Engineering
前端·aigc·ai编程
研☆香2 小时前
聊聊前端页面的三种长度单位
前端
研华科技Advantech2 小时前
如何用一套实训设备,打通工业AI预测性维护技术全流程?
人工智能
Lab_AI2 小时前
AI for Science: MaXFlow AI Agent+ 报告体验双升级,让AI智能体更高效易用!
人工智能·ai for science·ai agent·ai智能体
给钱,谢谢!2 小时前
React + PixiJS 实现果园成长页:从状态机到浇水动画
前端·react.js·前端框架
李坤2 小时前
让 Codex 和 Claude 互相 Review:告别手动复制
人工智能·openai·claude
南屹川3 小时前
【API设计】GraphQL实战:从REST到GraphQL的演进
人工智能