Agent之Tool--手写cursor 最小版本

Agent之Tool--手写cursor 最小版本

近期Agent 爆火产品

近期比较爆火的产品大家都应该知道,像千问点奶茶,openClaw养虾等产品都火的一塌糊涂,这些产品都有一个共性,就是不仅仅只局限在之前的llm大模型只能生成的阶段了,而是不仅有脑子还长出了手和脚,可以切实地帮助我们去完成某个具体的任务,像发个邮件,点杯奶茶等等都可以直接通过这些产品去完成,而我们将具有手和脚还有记忆和可以对我们内部知识库进行查询与使用的产品统一叫做Agent,而这些也意味着我们当前ai的发展阶段正在从llm prompt engineering(DeepSeek)转向 Agentic Engineering(智能体openClaw)

Agent 是什么?

其实就是给大模型扩展了Tool和memory的功能,他本来就可以思考,规划,我们给他用来tool扩展了能力 他就可以自动做事情,用memory管理记忆,他就可以记住你想他记住的东西,还可以使用RAG查询内部知识来获取(context)

这样知道内部知识,能思考规划,有记忆,能够帮你做事情的扩展后的大模型,就是一个Agent

  • AI Agent 如何打造?
    • 直接调大模型? 获得智能,生成代码 gemini 3.1 pro
    • 你上周和它聊过的消息,它是不是记不住?(bug) Memory
    • 你让他帮你访问一个网页,做一些事情 Tool
    • 你想让他基于公司内部的私密文档做一些解答 RAG
  • AI Agent = llm + memory + tool + RAG

手写mini-cursor

今天我要手写的就是cursor里面的一个小功能:当我们在输入框输入"请使用react创建一个todoList"时,我们编写的程序可以像cursor这样的编程Agent一样完成创建,编写,导入包,运行整个的一套程序这样的一个功能

首先我们要明确:llm只能完成这些思考(thinking),规划(planing) aigc生成代码等生成式的任务

正常情况下我们知道我们可以在我们的程序中调用各种llm,但是都只是在生成阶段,也就是相比于上述整个功能,仅仅只依靠大模型我们只能完成生成代码的功能,而像创建文件夹,运行命令行,将代码插入文件当中,这些都是大模型做不了的,因为大模型相当于只有大脑,除了生成式的东西他都做不了,这是我们就需要依靠tools,给大模型各种tool,大模型只需要判断什么时候去调用哪个tool即可,相当于给大模型装上了手和脚

而用react 创建一个todoList需要哪些tools呢?

  • 有读写文件的能力:毫无疑问,要生成一个项目,肯定就要有读取文件和写入文件的功能,当然写入文件中就包括创建文件
  • tool bash 执行命令:在我们运行一个项目中肯定要在终端中运行一些命令,类似pnpm i,pnpm run dev之类的命令,并且要额外注意这个tool与前面两个tool的不同之处在于这个并不是和我们程序运行的主进程在同一个进程,而是该主进程下的一个子进程

这里再次复习一遍进程和线程吧

进程:资源分配的最小单位,实际上就是一个正在运行的一个程序实例,比如你打开浏览器、微信、音乐播放器------每一个正在运行的程序,背后都是一个独立的进程。进程就是"程序运行起来之后,占着资源在干活的那个东西"。

线程: 执行的最小单位。线程不能单独存在,必须有进程来启动和管理

关系

  1. 进程内的任意一个线程出错都会导致整个进程崩溃
  2. 线程之间共享进程中的数据
  3. 当一个进程关闭后,操作系统会回收进程所占的内存
  4. 进程之间互不影响

类比

👉 进程 = 一个厨房里正在做一顿饭

👉 线程 = 厨房里具体干活的每一个人/步骤

如果一个人做菜(单线程),流程是这样的:

洗菜 → 切菜 → 炒菜 → 装盘(一步一步来)

但如果有多个人(多线程):

  • 一个人洗菜
  • 一个人切菜
  • 一个人炒菜

这些步骤可以同时进行,效率就更高。

关键区别你可以这么记:

  • 进程:资源的拥有者(厨房)
  • 线程:真正干活的执行者(厨师)

那具体如何实现呢?

首先我们选择js,使用node去实现这样一个简单的cursor,先实现底层功能,不去编写前端界面,而其中最核心的ai功能:大模型调用,tool的编写与调用,我打算使用langchain去完成,这里我先简单介绍一下langchain吧

LangChain

LangChain 是一个 AI Agent 框架 提供了 memory,tool,rag 等功能 ,LangChain 通过 统一接口、组件化 prompt、声明式数据流、可编排序列,把原本"随意试探"的 AI 使用方式,封装成可工程化、可复用、可扩展的系统方法论。

LangChain 核心包

作用
@langchain/core 核心运行框架,包含 Runnable、Chain、Tool、Message、Prompt、Memory 等
@langchain/openai OpenAI 模型封装
@langchain/chat-models 封装各种聊天模型接口(除了 OpenAI,如 Anthropic Claude)
@langchain/agents Agent 智能体,用于让模型自动选择工具和决策流程
@langchain/prompts Prompt 模板、few-shot、模板变量管理
@langchain/vectorstores 向量数据库封装,如 Pinecone、Chroma、Weaviate
@langchain/tools 各种可执行工具封装,供 Agent 调用
@langchain/memory 对话记忆 / 状态管理
@langchain/document-loaders 文档加载器(PDF、网页、文本)
@langchain/schema 类型和结构定义(Message、OutputSchema)
@langchain/callbacks 日志、事件、进度回调管理
LLM with Tools

这里我选择

先小试牛刀编写一个读取文件的tool
js 复制代码
import { tool } from '@langchain/core/tools';
import fs from 'node:fs/promises';
import path from 'node:path';
import { z } from 'zod';
// 读取文件工具
const readFileTool = tool(
    async ({ filePath }) =>  {
        try {
        console.log(`[工具调用] read_file("${filePath}") 成功读取 ${content.length} 字节`) 
        const content = await fs.readFile(filePath, 'utf-8');
        return `文件内容:\n${content}`;
        } catch (error) {
            console.log(`工具调用 read_file("${filePath}") 失败: ${error.message}`);
            return `错误:${error.message}`;
        }
    },
    {
        name: 'read_file',
        description: '读取制定路径的文件内容',
        schema: z.object({//对此工具的数据类型进行约束,外部是object类型,参数是字符串lei
            filePath: z.string().describe('文件路径')
        })
    }
)

代码详细介绍:

  • tool这里我们使用@langchain/core/tools去进行tool工具的声明

  • fs,path然后读于读取文件,这方面,我们使用node内部自带的函数去完成

    • fs/promises

      • 作用:操作文件和目录,提供 Promise 异步接口。

      • 常用方法

        • readFile(path):读文件
        • writeFile(path, data):写文件
        • mkdir(path):创建目录
        • readdir(path):列目录
        • rm(path):删除文件/目录
      • 特点 :支持 async/await,比老回调方式简洁。

    • path

      • 作用:处理文件路径,保证跨平台兼容。

      • 常用方法

        • join():拼接路径
        • resolve():生成绝对路径
        • basename():文件名
        • dirname():目录名
        • extname():扩展名
  • zod 是一个用于 TypeScript 和 JavaScript 的模式验证库,可以用来定义数据结构(schema)、校验数据类型和格式,并自动生成类型,保证数据安全可靠,同时支持嵌套、数组和可选字段,使用简洁直观,非常适合在前后端或 API 开发中做数据验证。

  • 编写一个 Tool 的通用方法可以总结为三步:

    1. 定义执行逻辑
    • 写一个函数,处理输入并返回结果

    • 如果有异步操作,用 async

    • 可选加 try/catch 捕获异常并返回可读错误

    1. 定义元信息
    • name:工具唯一标识

    • description:功能描述

    • schema:用 zod 定义输入参数类型

    1. tool() 封装
    ini 复制代码
    const myTool = tool(执行逻辑函数, { name, description, schema });

    💡 核心思路:逻辑函数 + 输入校验 + 描述信息 → tool 封装

再来试一个比较难的tool,在终端执行命令的tool

由于这个tool需要在终端执行命令,所以需要涉及到多进程之间的关系,话不多说,上代码

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 executeCommanTool = tool(
    async ({ command, workingDirectory }) => {
        const cwd = workingDirectory || process.cwd(); // 默认当前工作目录
        console.log(`[工具调用] execute_command("${command}") 在目录 ${cwd} 执行命令`);
        return new Promise((resolve, reject) => {
            const [cmd, ...args] = command.split(' ');
            const child = spawn(cmd, args, {
                cwd,
                stdio: 'inherit',
                shell: true
            })
            let errorMsg = '';
            child.on('error', (error) => {
                errorMsg = error.message;
            })
            child.on('close', (code) => {
                if (code === 0) {
                    // 成功退出
                    console.log(`[工具调用] execute_command("${command}") 命令执行成功`);
                    const cwdInfo = workingDirectory? 
                    `
                    \n\n重要提示:命令在目录"${workingDirectory}"中执行成功。
                    如果需要在这个项目目录中继续执行命令,请使用 workingDirectory 
                    "${workingDirectory}" 参数, 不要使用cd 命令
                    `:``
                    resolve(`命令执行成功: ${command} ${cwdInfo}`);
                } else {
                    if (errorMsg) {
                        console.error(`错误:${errorMsg}`);
                    }
                    process.exit(code || 1);
                }
            })
        })
    },
    {
        name: 'execute_command',
        description: '执行系统命令,支持指定工作目录,实时显示输出',
        schema: z.object({
            command: z.string().describe('要执行的命令'),
            workingDirectory: z.string().optional().describe('指定工作目录,默认当前工作目录')
        })
    }
)

代码详细介绍:

这段代码定义了一个 LangChain ToolexecuteCommanTool,功能是:

  • 执行系统命令(支持 Linux/macOS/Windows)

  • 可指定工作目录(workingDirectory),默认使用当前目录

  • 实时显示命令输出(通过 spawnstdio: 'inherit'

  • 返回命令执行结果或错误信息

  • 通过 tool() 封装成可复用工具,支持 Agent 调用

  • spawn 的使用方法

    spawn 是 Node.js 里 执行外部命令或启动子进程的核心工具 ,优势是 流式输出、低内存消耗、适合长任务

    arduino 复制代码
    const child = spawn(cmd, args, {
        cwd,          // 命令执行的工作目录
        stdio: 'inherit', // 输出继承父进程,命令的 stdout/stderr 直接打印到控制台
        shell: true   // 使用系统 shell 解析命令字符串,允许执行复杂命令
    })
    • cmd / args

      • command.split(' ') 将命令拆成命令和参数数组
      • spawn(cmd, args, options) 启动子进程
    • cwd:指定执行目录

    • stdio: 'inherit' :让子进程输出直接打印到父进程终端

    • shell: true :允许执行类似 npm installls -l 这样的 shell 命令

  • child.on('error', ...) 的作用

    js 复制代码
    child.on('error', (error) => {
        errorMsg = error.message;
    })
    • 作用 :监听 子进程启动时可能出现的错误

      • 比如命令不存在、权限不足等
    • 原理spawn 启动子进程时,某些错误无法通过 close 事件捕获

    • 处理逻辑 :将错误信息保存到 errorMsg 变量,close 事件触发时再统一处理

  • child.on('close', ...) 的作用

    js 复制代码
    child.on('close', (code) => { ... })
    • 作用:监听子进程退出

    • 参数 code:子进程退出码

      • 0 → 成功
      • 非 0 → 失败
    • 逻辑

      1. 成功 → resolve 返回成功信息

      2. 失败 → 打印 errorMsg 并退出进程(process.exit(code)

    注意:close 事件和 exit 类似,但 close 会在 stdio 流完全关闭后触发,适合做流式输出的处理。

  • 整体流程总结

    1. 接收命令和工作目录参数
    2. 设置默认目录为 process.cwd()
    3. 拆分命令字符串 → cmd + args
    4. spawn 启动子进程,实时打印输出
    5. 监听 error → 捕获启动错误
    6. 监听 close → 根据退出码返回成功或错误信息
    7. Tool 封装 → 提供 namedescription、输入校验 schema,可被 Agent 调用
好了相信大家都会写tool了,那怎么去调用tool呢?
(1) 初始化模型

要调用tool肯定需要一个大模型,这里我们使用的是langchain内的openai包去调用千问的qwen-plus模型,注意在调用大模型时要使用env去存储我们的密钥,确保安全,与env配套使用的还有import 'dotenv/config';这个包,引入之后,目录下的env文件内声明的变量就会自动引入,且不会被提交到git上面

js 复制代码
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
const model = new ChatOpenAI({
    modelName: process.env.MODEL_NAME,
    apiKey: process.env.OPENAI_API_KEY,
    temperature: 0,
    configuration: { baseURL: process.env.OPENAI_BASE_URL }
});
  • 使用 ChatOpenAI 创建一个语言模型实例。
  • 温度 temperature: 0 表示输出稳定,不随机,不带任何感情。
  • 可通过环境变量控制模型名称和 API Key。
(2) 绑定工具到模型
js 复制代码
const tools = [readFileTool, writeFileTool, executeCommanTool, listDirectoryTool];
const modelWithTools = model.bindTools(tools);
  • bindTools 是 LangChain 提供的功能,让模型知道它可以调用哪些工具。

  • 绑定后,模型在推理过程中可以直接发起工具调用(Tool Call)。

  • 工具调用本质

    1. 模型生成一个 tool_call 指令(类似 JSON:调用哪个工具 + 传入参数)。
    2. 代码解析这个调用,并执行相应工具函数。
    3. 工具结果回传给模型,模型可以继续"思考下一步"。
(3) 核心函数:runAgentWithTools
js 复制代码
async function runAgentWithTools(query, maxIterations = 30) { ... }
  • 参数:

    • query: 用户任务或指令。
    • maxIterations: 最大循环次数,防止无限循环。
  • 核心流程:

步骤 A:初始化消息
js 复制代码
const messages = [
    new SystemMessage(`你是一个项目管理助手...工具和规则说明`),
    new HumanMessage(query)
];
  • 系统消息:告诉模型它的身份、可用工具和规则。
  • 人类消息:任务或问题。
步骤 B:循环迭代模型
js 复制代码
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) {
        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
            }))
        }
    }
}
  • 核心逻辑

    1. 模型根据已有消息生成响应。

    2. 如果响应包含 tool_calls

      • 逐个执行工具。
      • 将结果用 ToolMessage 添加回消息列表。
      • 模型可以使用工具输出继续生成下一步指令。
    3. 如果没有工具调用,则认为任务完成,返回最终内容。

  • 重点

    • 工具调用是模型自动生成的,不是我们手动调用。
    • 工具结果回传模型,模型可以动态调整下一步行为(比如先写文件再执行命令)。
提问

我有几个比较困扰的问题:

1. LLM 如何决定调用哪个 Tool
  • 当你用 tool() 定义工具时,你提供了三个重要信息:

    1. name:工具的唯一标识。
    2. description:文字描述这个工具做什么。
    3. schema:参数结构和类型,用 zod 或 JSON Schema 定义。
  • 模型的推理过程

    1. 模型看到用户的指令(HumanMessage)和系统消息(包含可用工具描述)。

    2. 模型根据 description 理解哪个工具最适合完成当前任务。

      • 比如看到"执行命令",模型会匹配到 execute_command 的描述。
    3. 模型生成一个 tool_call 对象:

      json 复制代码
      {
        "name": "execute_command",
        "args": {
          "command": "pnpm install",
          "workingDirectory": "react-todo-app"
        }
      }
  • 所以 description 主要是给 LLM 提供语义理解依据,让模型知道哪个工具做什么。


2. 为什么我们在 Tool 中使用 {} 解构参数
dart 复制代码
const executeCommanTool = tool(
    async ({ command, workingDirectory }) => {
        // 这里直接拿到模型生成的参数
    }
)
  • 模型生成的 tool_call.args 是一个对象:

    json 复制代码
    { "command": "pnpm install", "workingDirectory": "react-todo-app" }
  • 所以在工具函数中:

    • async ({ command, workingDirectory }) => {...}

      等价于:

      ini 复制代码
      async (args) => {
        const command = args.command;
        const workingDirectory = args.workingDirectory;
        ...
      }
  • 解构的作用:直接拿到我们想要的参数,更简洁。


3. LLM 怎么知道参数具体值
  • LLM 会根据:

    1. 用户输入(HumanMessage)
    2. 系统消息/工具描述(描述工具做什么,参数叫什么)
  • 生成 JSON 风格的 args 对象。

  • 不是代码执行时自动解析,而是模型在"推理阶段"生成了参数值。

举例:

HumanMessage:

复制代码
在 react-todo-app 目录下安装依赖
  • 模型看到:

    • 工具 execute_command 描述:"执行系统命令,支持指定工作目录"
    • schema:{command, workingDirectory}
  • 模型就推理出:

json 复制代码
{
  "name": "execute_command",
  "args": {
    "command": "pnpm install",
    "workingDirectory": "react-todo-app"
  }
}

然后我们的 Node 代码接收到 toolCall.args,再解构 {command, workingDirectory} 执行。

4. invoke 的作用是什么?

本质invoke调用某个对象执行它的核心逻辑,并返回异步结果。

  • 模型 (modelWithTools) 来说:

    ini 复制代码
    const response = await modelWithTools.invoke(messages);
    • invoke(messages) 表示:"用这个 LLM 模型去处理消息列表 messages,并生成一个响应"。

    • 结果可能包含:

      • 普通文本回复 (response.content)
      • 工具调用 (response.tool_calls),告诉你 LLM 想用哪个工具做什么。
  • 工具 (foundTool) 来说:

    ini 复制代码
    const toolResult = await foundTool.invoke(toolCall.args);
    • invoke(args) 表示:"执行这个工具的异步逻辑,传入 LLM 提供的参数 args"。
    • 返回工具执行结果(字符串或对象),可以反馈给 LLM。

🔑 总结:invoke 就是"执行对象功能并拿结果"的统一接口

  • 对模型 → "生成响应/工具调用"。
  • 对工具 → "执行工具逻辑,返回结果"。
5.大模型各个阶段的返回值到底长什么样

这是llm第一次思考后返回的response,仔细观察会发现"content": "",因为当前主要是工具调用,去调用工具创建文件,运行命令,无返回值

  • id:消息唯一标识。

  • content :模型文本输出,这里是空字符串 "",因为模型这次的输出主要是 工具调用,不是普通文本。

  • additional_kwargs :OpenAI 返回的额外信息,包括 tool_calls 的原始对象。

  • response_metadata:关于模型使用的元信息,例如:

    • token 消耗
    • finish_reason: "tool_calls" → 模型停止生成是因为它有工具要调用。
  • tool_calls:这是核心,我们来重点分析。

  • invalid_tool_calls:无效的工具调用列表,这里为空。

bash 复制代码
AIMessage {
  "id": "chatcmpl-cedf3104-5460-9e73-804a-50f95978f87d",
  "content": "",
  "additional_kwargs": {
    "tool_calls": [
      {
        "index": 0,
        "id": "call_f9585a359cb74d93b5d6cb",
        "type": "function",
        "function": "[Object]"
      },
      {
        "index": 1,
        "id": "call_b6334e904dd94db995e53f",
        "type": "function",
        "function": "[Object]"
      }
    ]
  },
  "response_metadata": {
    "tokenUsage": {
      "promptTokens": 990,
      "completionTokens": 59,
      "totalTokens": 1049
    },
    "finish_reason": "tool_calls",
    "model_provider": "openai",
    "model_name": "qwen-plus"
  },
  "tool_calls": [
    {
      "name": "execute_command",
      "args": {
        "command": "echo -e \"n\\nn\" | pnpm create vite react-todo-app --template react-ts"
      },
      "type": "tool_call",
      "id": "call_f9585a359cb74d93b5d6cb"
    },
    {
      "name": "list_directory",
      "args": {
        "directoryPath": "."
      },
      "type": "tool_call",
      "id": "call_b6334e904dd94db995e53f"
    }
  ],
  "invalid_tool_calls": [],
  "usage_metadata": {
    "output_tokens": 59,
    "input_tokens": 990,
    "total_tokens": 1049,
    "input_token_details": {
      "cache_read": 512
    },
    "output_token_details": {}
  }
}

具体代码与运行结果

别急,我先将我全部的代码给到大家,有全部的代码,会更加清楚点,然后先看一下运行结果

js 复制代码
// all_tools.mjs

// langchain tool 工具
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 }) =>  {
        try {
        console.log(`[工具调用] read_file("${filePath}") 成功读取 ${content.length} 字节`) 
        const content = await fs.readFile(filePath, 'utf-8');
        return `文件内容:\n${content}`;
        } catch (error) {
            console.log(`工具调用 read_file("${filePath}") 失败: ${error.message}`);
            return `错误:${error.message}`;
        }
    },
    {
        name: 'read_file',
        description: '读取制定路径的文件内容',
        schema: z.object({
            filePath: z.string().describe('文件路径')
        })
    }
)

// 写入文件工具
const writeFileTool = tool(
    async ({ filePath, content }) => {
        try {
            // /a/b/c.txt /a/b/ 目录 
            const dir = path.dirname(filePath);
            // make directory 创建目录, recursive: true 递归创建
            await fs.mkdir(dir, { recursive: true });
            await fs.writeFile(filePath, content, 'utf-8');
            console.log(`[工具调用] write_file("${filePath}") 成功写入 ${content.length} 字节`) 
            return `文件写入成功: ${filePath}`;
        } catch(error) {
            console.log(`[工具调用] write_file("${filePath}") 失败: ${error.message}`);
            return `写入文件失败:${error.message}`;
        }
    }, 
    {
        name: 'write_file',
        description: '向指定路径写入文件内容,自动创建目录',
        schema: z.object({
            filePath: z.string().describe('文件路径'),
            content: z.string().describe('要写入的文件内容')
        })
    }
)

// 执行命令工具

const executeCommanTool = tool(
    async ({ command, workingDirectory }) => {
        const cwd = workingDirectory || process.cwd(); // 默认当前工作目录
        console.log(`[工具调用] execute_command("${command}") 在目录 ${cwd} 执行命令`);
        return new Promise((resolve, reject) => {
            const [cmd, ...args] = command.split(' ');
            const child = spawn(cmd, args, {
                cwd,
                stdio: 'inherit',
                shell: true
            })
            let errorMsg = '';
            child.on('error', (error) => {
                errorMsg = error.message;
            })
            child.on('close', (code) => {
                if (code === 0) {
                    // 成功退出
                    console.log(`[工具调用] execute_command("${command}") 命令执行成功`);
                    const cwdInfo = workingDirectory? 
                    `
                    \n\n重要提示:命令在目录"${workingDirectory}"中执行成功。
                    如果需要在这个项目目录中继续执行命令,请使用 workingDirectory 
                    "${workingDirectory}" 参数, 不要使用cd 命令
                    `:``
                    resolve(`命令执行成功: ${command} ${cwdInfo}`);
                } else {
                    if (errorMsg) {
                        console.error(`错误:${errorMsg}`);
                    }
                    process.exit(code || 1);
                }
            })
        })
    },
    {
        name: 'execute_command',
        description: '执行系统命令,支持指定工作目录,实时显示输出',
        schema: z.object({
            command: z.string().describe('要执行的命令'),
            workingDirectory: z.string().optional().describe('指定工作目录,默认当前工作目录')
        })
    }
)

// 列出目录工具
const listDirectoryTool = tool(
    async ({ directoryPath }) => {
        try {
            // 读取目录内容
            const files = await fs.readdir(directoryPath);
            console.log(`[工具调用] list_directory("${directoryPath}") 成功列出 ${files.length} 个文件`);
            return `目录内容:\n ${files.map(f => `- ${f}`).join('\n')}`

        } catch(error) {
            console.log(`[工具调用] list_directory("${directoryPath}") 失败: ${error.message}`);
            return `列出目录失败:${error.message}`
        }
    },
    {
        name: 'list_directory',
        description: '列出指定目录下的所有文件和文件夹',
        schema: z.object({
            directoryPath: z.string().describe('目录路径')
        })
    }
)

export {
    readFileTool,
    writeFileTool,
    executeCommanTool,
    listDirectoryTool
} 
js 复制代码
// main.mjs

import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import {
    HumanMessage,
    SystemMessage,
    ToolMessage
} from '@langchain/core/messages';
import {
    readFileTool,
    writeFileTool,
    executeCommanTool,
    listDirectoryTool
} from './all_tools.mjs'
import chalk from 'chalk'; // 彩色输出

const model = new ChatOpenAI({
    modelName: process.env.MODEL_NAME, // 比qwen-coder-turbo 更强大 
    apiKey: process.env.OPENAI_API_KEY,
    temperature: 0,
    configuration: {
        baseURL: process.env.OPENAI_BASE_URL
    }
});

const tools = [
    readFileTool,
    writeFileTool,
    executeCommanTool,
    listDirectoryTool
]
// modelWithTools
const modelWithTools = model.bindTools(tools);
// web 4.0 AI earn money 
async function runAgentWithTools(query, maxIterations = 30) {
    // 检测任务完成情况
    // 不用tool 
    // 在用tool llm还在自动进行中
    const messages = [
        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" }
        这是错误的!因为 workingDirectory 已经在 react-todo-app 目录了,再 cd react-todo-app 会找不到目录
        - 正确示例: { command: "pnpm install", workingDirectory: "react-todo-app" }
        这样就对了!workingDirectory 已经切换到 react-todo-app,直接执行命令即可

        回复要简洁,只说做了什么

        `),
        new HumanMessage(query),
    ];
    // 循环是agent的核心 llm 思考,规划, 调整 不断迭代 直到任务完成。 更加智能化
    for (let i = 0; i < maxIterations; i++) {
        console.log(chalk.bgGreen('⏳正在等待AI思考...'));
        const response = await modelWithTools.invoke(messages);
        messages.push(response);
        // console.log(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
                }))
            }
        }

    }

    return messages[messages.length - 1].content;

}

const case1 = `
创建一个功能丰富的 React TodoList 应用:

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

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

之后在 react-todo-app 项目中:
1. 使用 pnpm install 安装依赖
2. 使用 pnpm run dev 启动服务器
`
try {
    await runAgentWithTools(case1);
} catch(error) {
    console.error(`\n错误: ${error.message}\n`);
}
相关推荐
Tony沈哲2 小时前
OpenVitamin 整体架构设计—— 一个本地 AI 推理平台是如何构建的
算法·llm·agent
MarsBighead2 小时前
OpenClaw(Docker)极简安装配置教程
ai·llm·agent·openclaw
数据智能老司机3 小时前
从"推理思考"到"智能体思考":AI 范式迁移的深度解读与产业验证
agent
Entropy-Go3 小时前
一图了解AI热门词汇 - OpenClaw/Prompt/Agent/Skill/MCP/LLM/GPU
人工智能·agent·skill·mcp·openclaw
星浩AI3 小时前
MCP 系列(实战篇):从可跑通到可上线的 MCP 开发指南
后端·langchain·agent
code bean3 小时前
【Cursor】添加规则:不用 `dotnet build`:一条规则走 Visual Studio 链路
visual studio·cursor
小凡同志3 小时前
Cursor 和 Claude Code:AI 编程的两种哲学
人工智能·claude·cursor
trashwbin4 小时前
CLI vs MCP vs Skills:整个争论都问错了问题
agent
handsomeW4 小时前
给 Agent 装上双层记忆:从会话连续到长期知识沉淀
agent