⚡ Promise.all 性能优化:让 Agent 的工具调用飞起来
摘要:Agent 一次要读 3 个文件,串行要 6 秒,并行只要 2 秒。本文从 Promise 基础讲起,手把手教你用 Promise.all 优化 Agent 的工具调用性能,附带完整的错误处理方案。
📌 前言
上一篇我们实现了 ReAct 循环,Agent 能自主规划、分步执行任务了。
但有一个问题:当 LLM 一次返回多个 tool_calls 时,我们是串行执行的。
javascript
// ❌ 串行执行:一个一个来
for (const toolCall of response.tool_calls) {
const result = await tool.invoke(toolCall.args); // 每个都要等
}
如果 LLM 一次要读 3 个文件,每个读取需要 2 秒,串行就是 6 秒。
用 Promise.all 并行执行,只需要 2 秒。
🎯 本文适合谁
- 对 Promise 了解但不深入的同学
- 想优化 Agent 工具调用性能的开发者
- 想搞懂 Promise.all / Promise.allSettled / Promise.race 区别的同学
📚 一、Promise 基础回顾
什么是 Promise?
Promise 是 ES6 提供的异步编程解决方案。它代表一个未来才会完成的操作。
javascript
// 创建一个 Promise
const promise = new Promise((resolve, reject) => {
// 异步操作
setTimeout(() => {
resolve('完成!'); // 成功
// reject('失败!'); // 失败
}, 1000);
});
Promise 的三个状态
scss
┌───────────┐
│ Pending │ ← 初始状态
│ 等待中... │
└─────┬─────┘
│
├──── resolve() ──→ ┌───────────┐
│ │ Fulfilled │
│ │ 已完成 ✅ │
│ └───────────┘
│
└──── reject() ──→ ┌───────────┐
│ Rejected │
│ 已拒绝 ❌ │
└───────────┘
关键规则:
- 状态只能从
Pending变为Fulfilled或Rejected之一 - 一旦变化,不可逆转
async / await:Promise 的语法糖
javascript
// Promise 写法
function getData() {
return fetch('/api/data')
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.error(err));
}
// async/await 写法(更优雅)
async function getData() {
try {
const res = await fetch('/api/data');
const data = await res.json();
console.log(data);
} catch (err) {
console.error(err);
}
}
💡
await会暂停当前函数的执行,等 Promise 完成后再继续。它把异步代码写成了同步的风格,可读性大大提升。
📚 二、串行 vs 并行
场景模拟
假设我们要同时获取天气和推文数据:
javascript
function getWeather() {
return new Promise((resolve) => {
setTimeout(() => resolve({ temp: 38, conditions: '晴转多云' }), 2000);
});
}
function getTweets() {
return new Promise((resolve) => {
setTimeout(() => resolve(['今天天气真好', '周末去爬山']), 500);
});
}
❌ 串行执行
javascript
async function serial() {
console.time('串行');
const weather = await getWeather(); // 等 2000ms
const tweets = await getTweets(); // 再等 500ms
console.log(weather, tweets);
console.timeEnd('串行'); // 总耗时 ≈ 2500ms
}
markdown
时间线:
0ms ──────────── 2000ms ──── 2500ms
|──── getWeather ────|─ getTweets ─|
总耗时:2500ms
✅ 并行执行
javascript
async function parallel() {
console.time('并行');
const [weather, tweets] = await Promise.all([getWeather(), getTweets()]);
console.log(weather, tweets);
console.timeEnd('并行'); // 总耗时 ≈ 2000ms
}
markdown
时间线:
0ms ──────────── 2000ms
|──── getWeather ────|
|─ getTweets ─|
总耗时:2000ms(取最慢的那个)
节省了 500ms! 如果任务更多、耗时差距更大,优化效果会更明显。
📚 三、Promise.all 详解
基本用法
javascript
const results = await Promise.all([promise1, promise2, promise3]);
// results = [result1, result2, result3]
核心特性
| 特性 | 说明 |
|---|---|
| 并行执行 | 所有 Promise 同时开始执行 |
| 全部成功才成功 | 所有 Promise 都 resolve 后,才返回结果数组 |
| 一个失败就失败 | 任意一个 reject,Promise.all 立即 reject |
| 结果有序 | 返回数组的顺序和传入 Promise 的顺序一致 |
结果顺序
javascript
const p1 = new Promise(resolve => setTimeout(() => resolve('第一个'), 3000));
const p2 = new Promise(resolve => setTimeout(() => resolve('第二个'), 1000));
const p3 = new Promise(resolve => setTimeout(() => resolve('第三个'), 2000));
const results = await Promise.all([p1, p2, p3]);
console.log(results); // ['第一个', '第二个', '第三个']
// ↑ p1的结果 ↑ p2的结果 ↑ p3的结果
// 顺序和传入顺序一致,不是完成顺序!
错误处理
javascript
// ❌ 错误示范:一个失败,全部失败
const p1 = Promise.resolve('成功');
const p2 = Promise.reject('失败');
const p3 = Promise.resolve('成功');
try {
const results = await Promise.all([p1, p2, p3]);
} catch (err) {
console.log(err); // '失败'
// results 拿不到!因为 Promise.all 直接 reject 了
}
📚 四、Promise.all 的替代方案
Promise.allSettled:不管成败,全部返回
javascript
const results = await Promise.allSettled([
Promise.resolve('成功'),
Promise.reject('失败'),
Promise.resolve('成功'),
]);
console.log(results);
// [
// { status: 'fulfilled', value: '成功' },
// { status: 'rejected', reason: '失败' },
// { status: 'fulfilled', value: '成功' },
// ]
适用场景:Agent 工具调用 ------ 某个工具失败了,其他工具的结果仍然需要。
Promise.race:谁先完成用谁
javascript
const result = await Promise.race([
fetch('https://api1.example.com/data'), // 可能慢
fetch('https://api2.example.com/data'), // 可能快
new Promise((_, reject) => setTimeout(() => reject('超时'), 5000)),
]);
适用场景:多源数据竞速、超时控制。
对比总结
| 方法 | 成功条件 | 失败条件 | 返回值 |
|---|---|---|---|
Promise.all |
全部成功 | 任一失败 | 结果数组 |
Promise.allSettled |
不关心 | 不关心 | 状态+结果数组 |
Promise.race |
第一个完成 | 第一个完成 | 单个结果 |
📚 五、在 Agent 中应用 Promise.all
串行版本(慢)
javascript
// ❌ 串行执行工具调用
for (const toolCall of response.tool_calls) {
const tool = tools.find(t => t.name === toolCall.name);
if (tool) {
const result = await tool.invoke(toolCall.args); // 每个都要等
messages.push(new ToolMessage({
content: result,
tool_call_id: toolCall.id,
}));
}
}
并行版本(快)
javascript
// ✅ 并行执行工具调用
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} 不存在`;
try {
return await tool.invoke(toolCall.args);
} catch (error) {
return `工具调用失败:${error.message}`;
}
})
);
// 把结果包装成 ToolMessage(顺序和 tool_calls 一致!)
response.tool_calls.forEach((toolCall, index) => {
messages.push(new ToolMessage({
tool_call_id: toolCall.id,
content: toolResults[index]
}));
});
性能对比
假设 LLM 一次返回 3 个 tool_calls,每个工具执行需要 2 秒:
| 方案 | 执行方式 | 总耗时 |
|---|---|---|
| 串行 | 2s + 2s + 2s | 6 秒 |
| 并行 | max(2s, 2s, 2s) | 2 秒 |
提速 3 倍! 如果工具调用更多,差距会更大。
📚 六、进阶:Promise.allSettled 在 Agent 中的应用
在 Agent 场景中,某个工具调用失败不应该影响其他工具。这时候用 Promise.allSettled 更合适:
javascript
const toolResults = await Promise.allSettled(
response.tool_calls.map(async (toolCall) => {
const tool = tools.find(t => t.name === toolCall.name);
if (!tool) throw new Error(`工具 ${toolCall.name} 不存在`);
return await tool.invoke(toolCall.args);
})
);
// 处理结果(不管成功失败都处理)
response.tool_calls.forEach((toolCall, index) => {
const result = toolResults[index];
const content = result.status === 'fulfilled'
? result.value
: `工具调用失败:${result.reason}`;
messages.push(new ToolMessage({
tool_call_id: toolCall.id,
content: content,
}));
});
优势:即使某个工具失败了,其他工具的结果仍然能正常返回给 LLM。
📚 七、手写一个 Promise.all
理解原理最好的方式是自己实现一遍:
javascript
function myPromiseAll(promises) {
return new Promise((resolve, reject) => {
const results = [];
let completedCount = 0;
const total = promises.length;
if (total === 0) {
resolve([]);
return;
}
promises.forEach((promise, index) => {
// 确保每个元素都是 Promise
Promise.resolve(promise)
.then((result) => {
results[index] = result; // 保持顺序
completedCount++;
if (completedCount === total) {
resolve(results); // 全部完成
}
})
.catch((err) => {
reject(err); // 一个失败,立即 reject
});
});
});
}
核心逻辑
- 创建一个结果数组
results - 遍历每个 Promise,监听它的完成
- 完成时把结果放到对应的索引位置(保持顺序)
- 全部完成后 resolve
- 任何一个 reject,立即 reject
💡 重点总结
-
串行 vs 并行:串行是"一个一个等",并行是"同时开始,等最慢的"。
-
Promise.all:全部成功才成功,一个失败就失败。结果顺序和传入顺序一致。
-
Promise.allSettled:不管成败,全部返回。适合 Agent 场景(某个工具失败不影响其他)。
-
Promise.race:谁先完成用谁。适合超时控制、多源竞速。
-
Agent 性能优化:多个独立的工具调用用 Promise.all 并行执行,可以成倍提升速度。
-
错误处理:用 try-catch 包裹每个工具调用,把错误信息返回给 LLM,让它自己调整策略。
🔗 参考资料
💬 交流讨论
这是 LangChain Agent 系列的第四篇(完结篇)。四篇文章从概念到实战,完整覆盖了 Agent 开发的核心知识:
- LangChain 入门 ------ Agent 的本质和 LangChain.js 基础
- Tool Calling ------ 工具定义、绑定、调用的底层原理
- ReAct 循环 ------ 自主规划、分步执行的实现
- Promise.all ------ 工具调用的性能优化
接下来计划写 RAG(检索增强生成) ------ 让 Agent 能查询私有知识库,敬请期待 👀
觉得有用?点个赞👍收藏⭐关注👆,后续带你搞定 RAG + Memory,让 Agent 真正"活"起来!
📎 标签 :
Promise、性能优化、AI Agent、JavaScript、LangChain