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

0 阅读6分钟

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

前言

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

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

读完本文你能收获:

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

一、先搞懂:普通LLM为什么做不了复杂任务?

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

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

Agent本质公式

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

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

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

二、串行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}`
        }
    })
);

逐段解析

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

五、生产环境踩坑提醒

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

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

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

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

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

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

屏幕截图 2026-07-19 094951.png

坑4:未做工具参数校验

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

坑5:无限循环调用工具

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

六、Agent完整工作流程总结

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

七、扩展方向

  1. 新增CLI命令行工具,实现Vite项目自动创建、运行
  2. 接入RAG知识库,让Agent读取私有文档
  3. 使用LangGraph拆分多智能体,规划Agent、执行Agent分离
  4. 增加Memory记忆模块,支持多轮长期对话
  5. 接入MCP第三方工具协议,扩展联网、搜索能力