欢迎光临
我们一直在努力

LLM工具调用慢到崩溃?1套Promise.all并行方案,Agent性能直接翻倍

LangChain.js 手写代码Agent:多工具并发调用完整实战

前言

很多刚上手AI Agent开发的同学,都会踩一个致命性能坑:
模型一次返回多个工具调用,却用循环串行逐个执行工具。
比如同时读3个文件,串行要等全部读取时长叠加,并发只需要最慢文件的耗时。
在这里插入图片描述

我之前做代码助手Agent时,串行调用多工具,单次任务耗时直接拉到5秒+,用户反馈卡顿严重。
改用Promise.all并行执行工具后,响应速度直接提升60%,同时完善异常捕获、工具校验、多轮循环逻辑。

读完本文你能收获:

  • 搞懂Agent底层ReAct执行流程,LLM为什么必须搭配工具才能干活
  • LangChain.js 完整可运行代码Agent,兼容DeepSeek/OpenAI系列模型
  • Promise.all并行多工具原理,串行/并发性能对比
  • 生产级容错方案:工具不存在捕获、工具执行异常兜底
  • 完整多轮循环逻辑,自动持续调用工具直到任务完成
  • 一、先搞懂:普通LLM为什么做不了复杂任务?

    原生大模型有个天生缺陷:无状态(stateless)

  • 不会记忆上下文,每次对话清空历史,需要手动维护消息数组
  • 无法操作本地文件、命令行、接口、数据库,只能输出文字
  • 复杂任务不会拆分步骤,不会自主规划执行流程
  • Agent本质公式

    Agent = LLM + Memory记忆 + Tool工具调用 + RAG知识库
    工具(Tool)就是给大模型装上手脚,让AI能读写文件、执行命令、查询接口,自主完成完整任务。

    工具调用完整流程(ReAct范式)

  • 用户输入需求,存入HumanMessage,搭配系统提示词SystemMessage
  • LLM推理判断:是否需要调用工具,生成tool_calls数组(包含工具名、参数、唯一ID)
  • 后端并行执行所有工具,捕获执行结果,组装ToolMessage并绑定对应tool_call_id
  • 把工具执行结果追加进消息列表,再次传给LLM
  • 循环执行上述步骤,直到模型不再返回工具调用,输出最终答案
  • 二、串行VS并行工具调用,性能差距肉眼可见

    模拟两个异步任务示例

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>并行vs串行对比</title>
    </head>
    <body>
    <script>
    // 模拟两个耗时工具
    function getWeather() {
    return new Promise((resolve) => {
    setTimeout(() => {
    resolve({ temp: 38, conditions: 'Sunny with Clouds'})
    }, 2000)
    })
    }
    function getTweets() {
    return new Promise((resolve) => {
    setTimeout(() => {
    resolve(['I like cake', 'BBQ is good too!'])
    }, 500)
    })
    }

    // 串行写法:总耗时=2000+500=2500ms
    const runSerial = async () => {
    console.time("串行执行");
    const weatherData = await getWeather();
    const tweetsData = await getTweets();
    console.log(weatherData, tweetsData);
    console.timeEnd("串行执行");
    }

    // 并行写法:总耗时=Max(2000,500)=2000ms
    const runParallel = async () => {
    console.time("并行执行");
    const [weatherData, tweetsData] = await Promise.all([getWeather(), getTweets()]);
    console.log(weatherData, tweetsData);
    console.timeEnd("并行执行");
    }

    // runSerial();
    runParallel();
    </script>
    </body>
    </html>

    核心结论

    • 串行:工具排队执行,耗时累加,工具越多速度越慢
    • 并行:所有工具同时启动,总耗时等于最慢工具耗时
      开发生产级Agent,多工具场景必须用Promise.all并发执行

    三、完整实战:LangChain.js 代码读取Agent(可直接运行)

    1. 项目依赖安装

    npm install @langchain/openai @langchain/core dotenv zod fs-extra

    在这里插入图片描述

    2. .env 环境变量配置

    DEEPSEEK_API_KEY=你的密钥

    3. 完整可运行代码 tool.js

    import 'dotenv/config';
    import { ChatOpenAI } from '@langchain/openai';
    import { tool } from '@langchain/core/tools';
    import {
    HumanMessage,
    SystemMessage,
    ToolMessage,
    AIMessage
    } from '@langchain/core/messages';
    import fs from 'node:fs/promises';
    import { z } from 'zod';

    // 1. 初始化兼容OpenAI接口的大模型(DeepSeek)
    const model = new ChatOpenAI({
    modelName:'deepseek-v4-flash',
    apiKey: process.env.DEEPSEEK_API_KEY,
    temperature: 0, // 关闭随机性,工具调用更稳定
    configuration: {
    baseURL: 'https://api.deepseek.com/v1',
    },
    });

    // 2. 定义文件读取工具,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 tools = [readFileTool];
    // 给模型绑定工具,让模型知道可用函数
    const modelWithTools = model.bindTools(tools);

    // 初始化对话上下文消息数组
    const messages = [
    new SystemMessage(`
    你是专业代码分析助手,拥有本地文件读取工具read_file。
    执行规则:
    1. 用户需要查看文件内容时,必须调用read_file工具
    2. 拿到文件内容后,基于文件代码详细解读逻辑
    3. 一次可同时调用多个read_file读取多个文件
    `
    ),
    new HumanMessage('请读取 src/tool.js 文件并完整解释代码逻辑'),
    ];

    // Agent多轮循环核心:持续检测工具调用,直到模型输出最终回答
    let response = await modelWithTools.invoke(messages);
    messages.push(response);

    // 循环:只要存在工具调用就持续执行
    while(response.tool_calls && response.tool_calls.length > 0) {
    console.log(`\\n===== 检测到${response.tool_calls.length}个工具调用,开始并行执行 =====`);

    // 核心:Promise.all 并发批量执行所有工具
    const toolResults = await Promise.all(
    response.tool_calls.map(async (toolCall) => {
    // 1. 根据模型返回工具名匹配本地注册工具
    const targetTool = tools.find(t => t.name === toolCall.name);

    // 容错1:模型幻觉生成不存在的工具,直接返回错误文本
    if (!targetTool) {
    return `执行失败:未注册工具【${toolCall.name}`;
    }

    console.log(`[待执行工具] ${toolCall.name} 参数:${JSON.stringify(toolCall.args)}`);

    // 容错2:捕获工具内部报错(文件不存在、权限不足等)
    try {
    const result = await targetTool.invoke(toolCall.args);
    return result;
    } catch(err) {
    return `工具执行异常:${err.message}`;
    }
    })
    );

    // 将工具执行结果绑定对应tool_id,存入消息上下文
    response.tool_calls.forEach((toolCall, index) => {
    messages.push(
    new ToolMessage({
    content: toolResults[index],
    tool_call_id: toolCall.id
    })
    )
    });

    // 携带工具结果再次请求LLM,判断是否需要继续调用工具
    response = await modelWithTools.invoke(messages);
    messages.push(response);
    console.log("\\n===== 新一轮LLM推理完成 =====");
    }

    // 循环结束,输出最终回答
    console.log("\\n=== Agent任务完成,最终输出 ===");
    console.log(response.content);

    四、核心并行工具代码逐行拆解

    const toolResults = await Promise.all(
    response.tool_calls.map(async (toolCall) => {
    // 匹配本地注册工具
    const tool = tools.find(t => t.name === toolCall.name);
    // 工具不存在兜底
    if (!tool) {
    return `错误:找不到工具 ${toolCall.name}`
    }
    console.log(`[执行工具] ${toolCall.name}(${JSON.stringify(toolCall.args)})`);
    // 工具执行异常捕获
    try {
    const result = await tool.invoke(toolCall.args);
    return result;
    } catch(err) {
    return `错误:${err.message}`
    }
    })
    );

    逐段解析

  • response.tool_calls.map(async):把每一条工具调用转为异步Promise任务
  • Promise.all:并发执行全部Promise,等待所有工具执行完毕再往下走
  • tools.find:校验模型返回工具是否合法,防止模型幻觉
  • try/catch:单个工具崩溃不会中断整个批量任务,统一返回错误字符串
  • 返回数组toolResults顺序和tool_calls完全一一对应,方便绑定tool_call_id
  • 五、生产环境踩坑提醒

    坑1:Promise.all 一个工具卡死,整体阻塞

    解决方案:换成Promise.allSettled,区分成功/失败状态,单独处理超时工具,增加工具调用超时封装。

    坑2:忘记绑定tool_call_id,上下文错乱

    每个工具返回结果必须用ToolMessage绑定对应tool_call_id,否则LLM无法匹配哪条结果对应哪个工具调用,会出现逻辑混乱。

    坑3:串行执行多工具,性能极差

    文件读取、接口请求等IO密集型工具,务必并行;只有工具存在依赖关系时才串行。

    坑4:未做工具参数校验

    使用Zod定义工具schema,LLM生成参数不合规时,tool.invoke会自动抛出错误,提前拦截非法参数。

    坑5:无限循环调用工具

    生产环境需要增加循环次数上限,比如最多执行10轮工具调用,防止模型死循环。

    六、Agent完整工作流程总结

  • 初始化模型、定义工具、组装System+Human消息
  • 首次调用LLM,判断是否需要调用工具
  • 存在tool_calls则用Promise.all并行执行全部工具
  • 捕获工具执行结果,封装ToolMessage追加进消息列表
  • 携带完整上下文再次请求LLM,循环往复
  • LLM不再返回工具调用,输出最终答案,任务结束
  • 七、扩展方向

  • 新增CLI命令行工具,实现Vite项目自动创建、运行
  • 接入RAG知识库,让Agent读取私有文档
  • 使用LangGraph拆分多智能体,规划Agent、执行Agent分离
  • 增加Memory记忆模块,支持多轮长期对话
  • 接入MCP第三方工具协议,扩展联网、搜索能力
  • 赞(0)
    未经允许不得转载:171主机测评 » LLM工具调用慢到崩溃?1套Promise.all并行方案,Agent性能直接翻倍
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址