news 2026/9/29 2:26:39

从零手写 AI 编程 Agent:用 LangChain + Tool Calling + ReAct 搭一个 Mini Cursor 并接入 TaoToken

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零手写 AI 编程 Agent:用 LangChain + Tool Calling + ReAct 搭一个 Mini Cursor 并接入 TaoToken

1. 从零手写 AI 编程 Agent:Mini Cursor 到底在做什么

你可能用过 Cursor、Trae 这类工具,输入一句“帮我建一个 React TodoList 并跑起来”,它就能自己创建项目、改代码、装依赖、启动服务。这背后其实不是什么黑魔法,而是一个很清晰的循环:大模型负责“想”,工具负责“做”,ReAct 循环把两者串起来。本文要做的,就是用 LangChain + Tool Calling + ReAct,从零搭一个 Mini Cursor,并接入 TaoToken 统一 Key/API 通道完成端到端验证。

适合谁看:已经会一点 Node.js,想搞懂 AI 编程 Agent 底层原理的开发者;想自己做一个能读写文件、执行命令的自动化助手的人;以及正在用 LangChain 但 Tool Calling 老是调不通、想找一份能直接跑通的参考实现的人。

我试过把整个链路拆开看,核心就三件事。第一,LLM 本身只会输出文本,它不能碰你的文件系统,也不能执行命令。第二,Tool Calling 机制给模型装上了“手”,模型输出一个结构化的调用请求,你的代码去执行,再把结果喂回去。第三,ReAct 循环让这个过程反复进行,直到模型认为任务完成、不再请求调用工具为止。理解了这三点,你就理解了所有 AI 编程工具的最小骨架。

本文会给出可复制的 Agent 初始化配置、四个核心工具的 schema 定义、ReAct 循环的完整代码,以及通过 TaoToken 接入模型后跑一次真实代码修改任务的验证过程。全程 Node.js + LangChain,代码可以直接落地。

2. 前置准备:用 TaoToken 统一模型通道

在写 Agent 之前,先把模型通道搞定。自己手写 Agent 最烦的一点是:换一个模型就要改一次 baseURL、改一次鉴权方式、改一次请求格式。TaoToken 的价值就在这里——它提供一个统一的 Key 和 API 通道,OpenAI 兼容格式,你只需要改 modelName 就能切换不同模型,Agent 代码完全不用动。

TaoToken 是什么:一个面向开发者的模型 API 聚合通道,提供统一的 API Key 和 OpenAI 兼容接口,适合用来做 Agent、Coding Plan、模型对话等场景。官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 地址是 https://taotoken.net/api (这个不加 UTM)。

你需要先拿到一个 API Key。操作路径:进入控制台 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,在 API Keys 页面 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 创建一个新 Key,复制出来备用。如果你只是想先验证模型能不能通,可以直接用模型对话页面 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 发一条消息试试。

拿到 Key 之后,项目初始化如下:

mkdir mini-cursor && cd mini-cursor pnpm init pnpm add @langchain/core @langchain/openai zod chalk dotenv

然后在项目根目录建一个.env文件:

TAOTOKEN_API_KEY=sk-你的key TAOTOKEN_BASE_URL=https://taotoken.net/api

这里有个容易踩的坑:baseURL 结尾不要多加/v1,TaoToken 的兼容层已经处理好了路径,多写反而会 404。如果你用的是其他兼容通道,习惯性加/v1,换到 TaoToken 时记得去掉。

注意:API Key 不要提交到 Git,.env记得加进.gitignore。生产环境建议用环境变量注入,不要硬编码。

3. 可复制配置:工具 schema 与 ReAct 循环骨架

这一节是全文的核心,代码可以直接复制。先定义四个工具:读文件、写文件、列目录、执行命令。每个工具都用 LangChain 的tool()函数包装,包含 name、description、schema 三要素。

3.1 读文件与写文件工具

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 }) => { const content = await fs.readFile(filePath, 'utf-8'); console.log(`[工具调用] read_file(${filePath}) 读取 ${content.length} 字节`); return content; }, { name: 'read_file', description: '读取指定文件的完整内容。当需要查看代码、分析文件时调用。', schema: z.object({ filePath: z.string().describe('要读取的文件路径'), }), } ); const writeFileTool = tool( async ({ filePath, content }) => { try { const dir = path.dirname(filePath); await fs.mkdir(dir, { recursive: true }); await fs.writeFile(filePath, content, 'utf-8'); console.log(`[工具调用] write_file(${filePath}) 写入 ${content.length} 字节`); return `成功写入 ${filePath}`; } catch (err) { return `写入失败: ${err.message}`; } }, { name: 'write_file', description: '向指定路径写入文件内容,自动创建不存在的父目录。', schema: z.object({ filePath: z.string().describe('文件路径'), content: z.string().describe('要写入的完整内容'), }), } );

fs.mkdir(dir, { recursive: true })等价于mkdir -p,会自动递归创建多级目录。不写recursive: true的话,父目录不存在会直接报错,这是新手最常踩的坑之一。

3.2 列目录与命令执行工具

const listDirectoryTool = tool( async ({ directoryPath }) => { try { const files = await fs.readdir(directoryPath); return `目录内容:\n${files.join('\n')}`; } catch (err) { return `列出目录失败: ${err.message}`; } }, { name: 'list_directory', description: '列出指定目录下的所有文件和子目录。', schema: z.object({ directoryPath: z.string().describe('目录路径'), }), } ); import { spawn } from 'node:child_process'; const executeCommandTool = tool( async ({ command, directoryPath }) => { const cwd = directoryPath || process.cwd(); console.log(`[工具调用] execute_command(${command}) cwd=${cwd}`); return new Promise((resolve) => { const child = spawn(command, [], { cwd, stdio: 'inherit', shell: true, }); let errorMsg = ''; child.on('error', (err) => { errorMsg = err.message; }); child.on('close', (code) => { if (code === 0) resolve(`命令执行成功: ${command}`); else resolve(`命令失败,退出码 ${code},错误: ${errorMsg}`); }); }); }, { name: 'execute_command', description: '执行系统命令,支持指定工作目录,实时显示输出。', schema: z.object({ command: z.string().describe('要执行的命令'), directoryPath: z.string().optional().describe('工作目录,推荐指定'), }), } );

关于spawn和shell: true有个关键细节:当shell: true时,必须把整个命令作为第一个参数传入,第二个参数传空数组。如果你习惯性把命令按空格拆成cmd + args,管道|、重定向>这些 shell 特性会失效,Node.js 还会抛出 DEP0190 警告。正确写法就是上面这样:spawn(command, [], { shell: true })。

3.3 ReAct 循环骨架

工具定义好了,接下来把它们和模型串起来。这里用 TaoToken 作为模型通道:

import 'dotenv/config'; import { ChatOpenAI } from '@langchain/openai'; import { HumanMessage, SystemMessage, ToolMessage } from '@langchain/core/messages'; import chalk from 'chalk'; const model = new ChatOpenAI({ modelName: 'deepseek-v4-flash', apiKey: process.env.TAOTOKEN_API_KEY, configuration: { baseURL: process.env.TAOTOKEN_BASE_URL, }, }); const tools = [executeCommandTool, readFileTool, writeFileTool, listDirectoryTool]; const modelWithTools = model.bindTools(tools); async function runAgent(query, maxIterations = 30) { const messages = [ new SystemMessage( `你是一个编程助手。当前工作目录:${process.cwd()} 可用工具:read_file / write_file / list_directory / execute_command 规则:directoryPath 参数直接切换目录,不要用 cd 命令。回复简洁。` ), new HumanMessage(query), ]; for (let i = 0; i < maxIterations; i++) { console.log(chalk.bgGreen(`第 ${i + 1} 次 AI 思考...`)); const response = await modelWithTools.invoke(messages); messages.push(response); if (!response.tool_calls || response.tool_calls.length === 0) { console.log(chalk.green(`[Agent] 任务完成: ${response.content}`)); return response.content; } 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, }) ); } } } return messages[messages.length - 1].content; }

这段循环就是 ReAct 的全部:模型思考 → 有工具调用就执行 → 结果回填 → 再思考。tool_call_id必须原样带回,因为模型可能一轮同时调用多个工具,没有这个 id 它就分不清哪个结果对应哪个请求,后续推理会乱套。

4. 验证请求:跑一次真实的代码修改任务

配置写完了,现在跑一个端到端任务验证。任务描述如下:

const task = ` 创建一个 React TodoList 应用: 1. 用 pnpm create vite react-todo-app --template react-ts 创建项目 2. 修改 src/App.tsx,实现添加/删除/标记完成/筛选/持久化 3. 添加样式 4. pnpm install 安装依赖 5. pnpm run dev 启动服务 `; runAgent(task).catch((err) => console.error(`错误: ${err.message}`));

执行node agent.mjs,你会看到类似这样的轨迹:

轮次LLM 决策工具结果
1创建项目脚手架execute_command成功
2写入 App.tsx 组件write_file成功
3写入 App.css 样式write_file成功
4安装依赖execute_command成功
5启动开发服务器execute_command成功
6确认任务完成无返回总结

成功的关键标志是第 6 轮:模型不再请求任何工具,直接返回文本总结。这说明它认为任务已经完成。如果模型在第 6 轮还在反复调用同一个工具,通常是工具返回结果里缺少明确的成功/失败信号,模型无法判断状态,就会一直重试。

验证模型通道是否正常,可以先用模型对话页面 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 发一条简单消息,确认 Key 和 baseURL 没问题,再跑 Agent。这样能把“模型通道问题”和“Agent 逻辑问题”分开排查。

5. 本篇常见错误排查

5.1 Schema 参数名和函数参数名不一致

现象:模型调用了工具,但函数收到的参数是undefined。

// 错误:Schema 里叫 directory,函数里用 directoryPath schema: z.object({ directory: z.string() }), async ({ directoryPath }) => { ... } // 正确:两边完全一致 schema: z.object({ directoryPath: z.string() }), async ({ directoryPath }) => { ... }

LangChain 是按 schema 的字段名去解构参数的,名字对不上就取不到值。这个错误不报异常,只是静默失败,最难查。

5.2 在子进程 close 回调里调用 process.exit()

现象:执行完第一个命令后整个 Node 进程直接退出,Agent 没来得及进入下一轮 ReAct。

原因是在child.on('close')里调了process.exit()。子进程结束只是子进程的事,主进程应该继续跑循环。解决方法是不要在 close 回调里退出进程,让主流程自然控制。

5.3 忘记 recursive: true 导致写文件失败

现象:fs.mkdir('a/b/c')报错,因为父目录a/b/不存在。

解决:始终用fs.mkdir(dir, { recursive: true }),等价于mkdir -p。写文件工具里这一行不能省。

5.4 baseURL 多写了 /v1

现象:请求返回 404 或路径错误。

TaoToken 的兼容层已经处理了路径,baseURL 写https://taotoken.net/api即可,不要画蛇添足加/v1。如果你从其他通道迁移过来,这是第一个要检查的地方。

5.5 工具 description 写得太模糊

现象:模型该调read_file的时候调了execute_command,或者该写文件的时候去列目录。

工具的description是给模型看的“使用说明书”,写得越清楚,模型选得越准。比如read_file的 description 要明确写“读取文件内容,查看代码时调用”,而不是只写“读文件”。schema 里的.describe()同理,参数含义要写清楚。

6. 继续深入:把 Mini Cursor 用起来

到这里,一个能读写文件、执行命令、自主循环的 Mini Cursor 就跑通了。它的核心结构可以归纳成三层:决策层是 LLM,负责理解意图和规划步骤;工具层是四个 LangChain Tool,负责实际的文件和命令操作;调度层是 ReAct 循环,把前两者串起来直到任务完成。

如果你想把它用在长期编码或 Agent 场景,可以走 Coding Plan 通道 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ,统一管理额度和 Key。接入细节和更多工具定义方式,可以查接入文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。如果你用的是 Claude Code 这类工具,Anthropic 兼容入口在 https://taotoken.net/claudecode-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claudecode-anthropic&utm_campaign=rewrite 。

最后给一个实用建议:先别急着加更多工具。把读、写、列、执行这四个跑稳,理解清楚tool_call_id的作用和 ReAct 的终止条件,比堆一堆花哨功能有用得多。等这四个工具稳定了,再考虑加代码搜索、Git 操作、测试运行这些能力,那时候你已经有足够的判断力知道该怎么设计了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 2:25:50

ZeroLaunch-rs办公应用:文档快速打开技巧

ZeroLaunch-rs办公应用&#xff1a;文档快速打开技巧 &#x1f680; 痛点&#xff1a;办公文档打开效率低下 在日常办公中&#xff0c;你是否经常遇到这样的场景&#xff1a; 需要快速打开某个Word文档&#xff0c;却在层层文件夹中苦苦寻找想要编辑Excel表格&#xff0c;却要经…

作者头像 李华
网站建设 2026/9/29 2:25:41

网络安全简答题文档的工程化构建方法

简介&#xff1a;本资源是一份面向网络安全初学者与备考学生的高频考点梳理文档&#xff0c;聚焦网络安全部分核心概念与典型简答题&#xff0c;适用于课程复习、期末备考及信息安全基础能力巩固。文件为单个140KB的Word文档&#xff08;.docx&#xff09;&#xff0c;内容结构…

作者头像 李华
网站建设 2026/9/29 2:25:13

FireDAC 下的 Sqlite [5]:插入、更新、删除的配置骨架与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 2:25:04

MCU产品EFT防护实战:从PCB布局到固件容错的系统设计指南

1. 从一次深夜整改说起&#xff1a;MCU的EFT到底难在哪做硬件这行十几年&#xff0c;最怕的不是功能调不通&#xff0c;而是功能全对、实验室里跑得好好的板子&#xff0c;一到客户现场就随机死机、复位、通信丢包。你查电源、查时钟、查固件&#xff0c;折腾几天几夜&#xff…

作者头像 李华
网站建设 2026/9/29 2:24:58

CCV NNC Dataframe 详解:以 Pull 模型驱动异步数据加载与训练

计算机视觉深度学习 【免费下载链接】ccv C-based/Cached/Core Computer Vision Library, A Modern Computer Vision Library 项目地址&#xff1a; https://gitcode.com/gh_mirrors/cc/ccv 点击查看 免费下载 导读 CCV&#xff08;C-based/Cached/Core Computer Vision Libr…

作者头像 李华