给 Agent 装上手脚:手写 mini-Cursor,从建项目到跑起来

写在前面

上回书说到,咱们把 Agent 的 while 循环转起来了------但那个 Agent 只有一个 read_file 工具,相当于一个只能看、不能动手的实习生。你让它建项目,它能给你讲一节课,就是一行文件都不落地。

今天补上最后一块拼图:给它装上 write_filelist_directoryexecute_command,凑齐工具四件套,手写一个 mini 版 Cursor / Claude Code。

任务就一个,也是 Trae、Cursor 用户天天干的事:

用 vite 创建一个 react 的 todolist 项目,并且把它运行起来。

工具四件套:Cursor 的手和脚

拆开任何一个编程 Agent,工具盘大差不差就这四样:

工具 干啥 对应人类动作
read_file 读文件内容 眯眼看代码
write_file 写入文件 敲键盘
list_directory 列目录 ls -la 扫一眼项目结构
execute_command 执行命令行 上终端敲命令

前三个都是 node:fs/promises 的活,一个 await fs.readFile 就完事。真正的硬骨头是最后一个------让 Agent 会跑命令。而它,牵出了 Node.js 的一个底层架构问题。

Node 单线程的坎:child_process 登场

Node 主进程是单线程 的,跑着咱们的 Agent 循环。但你让它去执行 pnpm create vite------一个要跑几十秒、疯狂输出日志的 bash 命令------单线程直接被堵死,Agent 整个卡住。

而且别忘了:Windows 上的 git bash,内部本身就藏着一个 mini Linux 系统。ls -la 这些命令,得在 Node 进程之外另起炉灶。

解法:node:child_process 模块的 spawn,把命令甩给一个独立的子进程去跑:

js

arduino 复制代码
import { spawn } from 'node:child_process';

const command = 'ls -la';
const [cmd, ...args] = command.split(' ');  // 第一项是命令,rest 全是参数

const child = spawn(cmd, args, {
  cwd: process.cwd(),  // 工作目录
  stdio: 'inherit',    // 子进程继承父进程的输入输出
  shell: true,         // 走 shell 解释
});

主进程和子进程怎么通信?靠 IPC(Inner Process Communication,进程间通信)。子进程干完活,通过事件把结果告诉主进程:

配置/事件 作用 不写会怎样
stdio: 'inherit' 子进程日志直接打到你控制台 输出黑洞,什么都看不到
shell: true 走 shell 解释命令 pnpm 这种命令找不到
child.on('close', code) 监听退出码,0 是成功 不知道命令成没成
child.on('error', err) 捕获启动失败 报错信息直接丢

spawn 返回的是个 Promise------code === 0 时 resolve,否则把错误信息包在结果里回传。这套结构,就是 execute_command 工具的全部骨架。

最阴的坑:cd 和 workingDirectory 打架

execute_command 有个 workingDirectory 参数,用来指定命令在哪个目录跑。听起来很美好,坑就坑在这。

LLM 拿到这个参数后,经常自作聪明地写出这种组合:

写法 结果
command: "cd react-todo-app && pnpm install"workingDirectory: "react-todo-app" ❌ 目录不存在------已经在 react-todo-app 里了,再 cd 一次直接报错
command: "pnpm install"workingDirectory: "react-todo-app" ✅ 正确姿势,直接执行

这坑怎么治?把错误示例和正确示例直接写进 SystemMessage,用自然语言告诉 LLM"别这么干":

js

bash 复制代码
new SystemMessage(`
  重要规则 - execute_command:
  - workingDirectory 参数会自动切换到指定目录
  - 当使用 workingDirectory 时,绝对不要在 command 中使用 cd
  - 错误示例: { command: "cd react-todo-app && pnpm install", workingDirectory: "react-todo-app" }
  - 正确示例: { command: "pnpm install", workingDirectory: "react-todo-app" }
`)

这是 Agent 开发的通用套路:LLM 犯过的错,用 system prompt 圈起来警示后人。相当于给实习生贴了张"此处有坑"的便利贴。

write_file 的细节:递归建目录

写文件工具有个容易忽略的点------写 /a/b/c/App.tsx 这种深层路径时,abc 目录可能压根不存在。

js

csharp 复制代码
const dir = path.dirname(filePath);
await fs.mkdir(dir, { recursive: true });  // 递归创建,存在就跳过
await fs.writeFile(filePath, content, 'utf-8');

path.dirname 抠出路径里的目录部分,mkdirrecursive: true 一路递归建下去。已存在的目录不会报错,稳。

ReAct 循环 + 两道刹车

工具齐了,主循环跟上。这回不用 while(true) 了,换成 for 循环加上限

js

ini 复制代码
async function runAgentWithTools(query, maxIterations = 30) {
  for (let i = 0; i < maxIterations; i++) {
    console.log(chalk.bgGreen(`正在等待第${i}次 AI 思考...`));
    const response = await modelWithTools.invoke(messages);
    messages.push(response);

    // 没有工具调用 → LLM 觉得干完了,直接给最终回复
    if (!response.tool_calls?.length) {
      return response.content;
    }

    // 有工具调用 → 逐个执行,结果带 id 回喂
    for (const toolCall of response.tool_calls) {
      const foundTool = tools.find(t => t.name === toolCall.name);
      if (foundTool) {
        const result = await foundTool.invoke(toolCall.args);
        messages.push(new ToolMessage({
          content: result,
          tool_call_id: toolCall.id,
        }));
      }
    }
  }
}

为什么这里不用 Promise.all 并行?因为建项目这活有严格顺序依赖------先 create 项目,再写文件,再 install,最后 run dev。并行跑反而乱套。

循环外面还垫了一层保险:

js

arduino 复制代码
setTimeout(() => {
  console.log("⏰ 超时兜底强制退出进程");
  process.exit(0);
}, 1000000);

两道刹车,各管一头:

刹车 防什么
maxIterations = 30 LLM 无限调工具,循环永远不退
setTimeout 兜底 万一有命令挂住不返回(比如 dev server 常驻进程),整个 Node 进程赖着不死

pnpm run dev 起的 dev server 是个常驻进程,它永远不会"执行完毕"------没有 setTimeout 兜底,你的命令行窗口会一直挂着回不来。

跑起来的效果:chalk 绿底日志一行行刷,"检测到工具调用"→"执行 write_file"→"execute_command 成功"→ 最后 AI 输出总结。一个项目就这么被它自己建好、跑起来了。

5 个踩坑提醒

1. stdio 不设 'inherit',日志全丢。 子进程的输出默认不走你的控制台,Agent 装了半天气,你一行日志都看不到,还以为它死了。

2. command 和 workingDirectory 混用 cd。 上面说的那个坑,百分百踩。错误/正确示例必须写进 system prompt。

3. 工具执行结果当错误抛。throw------工具挂了要把错误信息当结果回喂给 LLM,让它自己看到报错、自己决定下一步。throw 出去整个 Agent 就崩了。

4. spawn 不加 shell: true Windows 下命令直接找不到,报 ENOENT,你还以为是路径问题。

5. 循环没有退出上限。 LLM 偶尔会陷入"调工具→看结果→再调工具"的鬼打墙,30 次上限是成本底线。

写在最后

到这儿,一个能干活的编程 Agent 齐活了:LLM 出脑子,四件套工具出手脚,ReAct 循环转起来,双刹车保平安。Trae 和 Cursor 剥掉外壳,内核就是这么一套东西,只是工具更多、prompt 调得更狠。

相关推荐
Hive_MOM27 分钟前
制造企业库存精益化管理数字化:从“库存压资金、找货靠人翻“到“库存水位实时可见、资金周转提速“
前端·制造
光影少年35 分钟前
react navite调试方案:Flipper、远程调试
前端·javascript·react native·react.js·前端框架
计算机魔术师44 分钟前
智谱开源 GLM-5.3 模型权重,主打智能体编程与网络防御
前端
百变梦仔1 小时前
多个 Skill 同时出现时,Codex 前端任务我会先定优先级
前端
IT_陈寒1 小时前
SpringBoot自动配置失效?这个隐式依赖坑了我三天
前端·人工智能·后端
码视野1 小时前
基于 Spring Boot + Vue3 的【城市雨污水管网液位淤积溯源与立交桥下穿隧洞防汛排涝智控中台】设计与实现(含PRD/三端高保真源码/大屏)
java·前端·人工智能·spring boot·后端
雪芽蓝域zzs1 小时前
第五节:Vue‑Router4 路由配置,布局嵌套路由
前端·javascript·vue.js
雪芽蓝域zzs1 小时前
第十二节:完整用户管理 CRUD 页面(表格分页、新增 / 编辑弹窗、删除、Mock 接口)
前端·javascript·vue.js
深念Y1 小时前
NativeScript 移动端开发踩坑记录
前端·ui·vue·安卓·移动端·native·原生