您的位置:首页 > 手游攻略 > 我花了两小时找了一个数据结构 bug: 才把迷你 Cursor 跑起来 绝望bushi

我花了两小时找了一个数据结构 bug: 才把迷你 Cursor 跑起来 绝望bushi

作者:互联网  时间: 2026-07-18 08:16:55  


一、今天的目标:手写一个"迷你 Cursor"

1.1 要实现什么?

老师说:

我花了两小时找了一个数据结构 bug,才把"迷你 Cursor"跑起来,绝望(bushi)

用大白话说,就是让 AI 自己完成这件事:

 复制代码"给我用 Vite 创建一个 React TodoList,功能齐全、样式漂亮、能跑起来。"

要做到这件事,Agent 需要哪些能力?

需要的能力对应的 Tool比喻
创建项目目录和文件write_fileAI 的手,能写文件
修改已有文件read_file + write_fileAI 的眼睛+手
查看项目结构list_directoryAI 的导航
安装依赖/启动项目exec_commandAI 的脚,能运行命令

老师说一个 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() 第二个参数定义了 schemainvoke 时就会按 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 循环的上节课学到的三个动作:

步骤代码说明
ReasonmodelWithTools.invoke(messages)LLM 思考下一步
ActfoundTool.invoke(toolCall.args)执行工具
Observenew 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.mjsReAct 循环 + 工具调用
子进程层node-exec.mjsspawn 执行 CLI 命令
System Promptmini-cursor 内联给 LLM 的操作规则

"迷你 Cursor"的架构,和真正的 Cursor/Claude Code 是同一个思路。 只不过真正的产品有更完善的错误处理、更丰富的工具集、更复杂的任务编排。


写在最后

今天最大的收获有两个。第一个是终于写出了一个能自己干活的编程 Agent——从创建项目到启动服务器,全程自动化。第二个是花了两小时跟一个括号过不去——async (filePath)async ({filePath}) 的区别,让我深刻理解了"参数解构"这件事。

老师说后面还要继续学,估计会讲到多智能体协作(LangGraph)和更复杂的任务编排。我已经准备好了——下次再遇到 bug,我会先检查括号。

下次面试官问你:"Agent 怎么执行多个工具调用?"

你可以淡定地说:

然后看着面试官满意的表情,心里默念:这波,又稳了。


本文所有代码示例均来自课堂学习资料,真实可运行。

最新游戏

更多

Copyright©2010-2019. All rights reserved | 波波三国游戏官网|[email protected]

备案编号:湘ICP备2022015115号-4