Skip to content

实现 mini cursor:大模型自动调 Tool 执行命令

上节只加了读文件的 Tool,如果把写文件、执行命令、列目录都加上,不就能做 Cursor 的事了?

这节实现:大模型根据 prompt 生成项目 → 自动读写文件 → 安装依赖 → 跑起来,全程自己调 Tool。

Node.js 执行命令

用内置模块 child_processspawn

js
import { spawn } from 'node:child_process';

const child = spawn('pnpm', ['install'], {
  cwd: './my-project',  // 工作目录
  stdio: 'inherit',     // 子进程输出直接显示在控制台
  shell: true,
});

封装四个 Tool

读文件、写文件、执行命令、列目录,全部放在 all-tools.mjs

js
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';

// 读文件
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('文件路径'),
    }),
  }
);

// 写文件(自动创建目录)
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('要写入的文件内容'),
    }),
  }
);

// 执行命令
const executeCommandTool = tool(
  async ({ command, workingDirectory }) => {
    const cwd = workingDirectory || process.cwd();
    return new Promise((resolve, reject) => {
      const child = spawn(command, [], {
        cwd,
        stdio: 'inherit',
        shell: true,
      });
      child.on('close', (code) => {
        if (code === 0) resolve(`命令执行成功: ${command}`);
        else reject(`命令执行失败,退出码: ${code}`);
      });
    });
  },
  {
    name: 'execute_command',
    description: '执行终端命令',
    schema: z.object({
      command: z.string().describe('要执行的命令'),
      workingDirectory: z.string().optional().describe('工作目录'),
    }),
  }
);

// 列目录
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('目录路径'),
    }),
  }
);

Agent 调用循环

把四个 Tool 绑定到模型,循环执行直到没有新的 tool_calls:

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

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);

    // 没有 tool_calls 就结束,输出最终回复
    if (!response.tool_calls || response.tool_calls.length === 0) {
      return response.content;
    }

    // 执行所有工具调用
    for (const toolCall of response.tool_calls) {
      const tool = tools.find(t => t.name === toolCall.name);
      const result = await tool.invoke(toolCall.args);
      messages.push(new ToolMessage({
        content: result,
        tool_call_id: toolCall.id,
      }));
    }
  }
}

跑起来

js
const task = `
请创建一个 React + Vite 的 TodoList 项目到 react-todo-app 目录:
1. 用 pnpm create vite 创建项目
2. 写入完整的 TodoList 组件(增删改查、动画、渐变背景)
3. pnpm install 安装依赖
4. pnpm run dev 启动服务
`;

await runAgentWithTools(task);

大模型会自动调用 list_directory、write_file、read_file、execute_command,把项目创建好、跑起来。

注意点

  • workingDirectory 和 cd 不要同时用spawncwd 已经切换了目录,命令里再 cd 会找不到路径
  • maxIterations 设上限 — 防止死循环,30 次够用了
  • temperature: 0 — 让模型严格按指令执行