最新下载
热门教程
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
Agent 并行工具调用快了 0.8 秒——200 组实测发现 多花的那 3100 token 全打了水漂
时间:2026-07-15 18:47:53 编辑:袖梨 来源:一聚教程网
你有没有遇到过这种情况——Agent 说要调三个工具,你心想"反正它们互相不依赖,一起发出去不香吗",于是 Promise.all 一把梭。结果也没报错,响应确实快了,但月底一看账单,token 比上个月多了四成。

这不是你的错觉。我拿 200 组多步任务跑了一轮三轮对比,数据摆在下面。
三种策略,一张表说清楚
我搭了一个测试 Agent,配置了 6 个工具:searchDocs、readFile、checkCalendar、sendEmail、createTask、getUserContext。200 组任务包含"根据会议纪要给参会人发邮件并建跟进任务"、"根据用户最近文档推荐日程"这类典型的多工具联动场景。
三种执行策略:
| 策略 | 平均延迟 | 平均 token/任务 | 无效调用率 | 任务完成率 |
|---|---|---|---|---|
| 全串行 | 3.2s | 7420 | 3% | 94% |
| 全并行 | 2.4s | 10560 | 27% | 86% |
| 智能混合 | 2.6s | 8020 | 6% | 93% |
"无效调用"指的是工具被调用了,但返回结果对后续决策没有任何贡献——要么参数纯靠猜填错了,要么在并行窗口里调了其实完全不需要的工具。
全并行快了 0.8 秒,但平均多烧 3140 token。换算一下,就是每快 1 毫秒花 3.9 个 token。贵不贵,你心里有数。
全串行:慢,但每 1 个 token 都花了值
复制代码async function runSerial(agent: Agent, toolCalls: ToolCall[]): Promise<ToolResult[]> {
const results: ToolResult[] = [];
for (const call of toolCalls) {
const result = await agent.executeTool(call);
results.push(result);
// 每个工具的结果立即注入上下文,下一步的模型已包含全部前置信息
agent.appendContext({ role: "tool", content: JSON.stringify(result), toolCallId: call.id });
}
return results;
}
串行没什么花活,就是一个接一个调。好处显而易见——每一步模型都看到了上一步的真实返回。createTask 的 deadline 参数可以从 checkCalendar 返回的空闲时段里精确取,而不是随便蒙一个。
代价就是慢。6 个工具串下来,网络往返时间累加,3 秒起步。
全并行:问题不在"并行"本身,在于模型在瞎猜
复制代码async function runParallel(agent: Agent, toolCalls: ToolCall[]): Promise<ToolResult[]> {
const results = await Promise.all(
toolCalls.map(call => agent.executeTool(call))
);
// 全部拿到后再一次性注入上下文
agent.appendContext(
results.map((r, i) => ({
role: "tool",
content: JSON.stringify(r),
toolCallId: toolCalls[i].id,
}))
);
return results;
}
代码就这么几行。问题不在代码,在 LLM 生成这些 toolCalls 时发生了什么。
并行调用的前提是:模型在一次推理中同时生成所有工具调用的参数。但此时它还没有任何工具的返回结果。举例:模型需要 createTask,deadline 是"明天下午有空的时间"。这个信息得从 checkCalendar 的返回里拿。并行模式下,模型还没拿到日历数据就得把 deadline 参数填了,它只能猜。
猜的结果呢?200 组里,有 54 次生成的 deadline 是错的(比如选了已经有会议的时间段),Agent 只好再次调用 updateTask 去修正。一来一回,多烧了一轮 token,相当于并行省下来的时间又被后续的纠错调用吃回去了——而钱是实打实多花了。
说白了,并行策略赢就赢在"让 LLM 在信息不完整的情况下做决策",这种事,你猜会怎样。
一个具体的翻车例子:
复制代码工具调用(并行):
checkCalendar({ date: "2026-07-16" }) → 返回 [10:00-11:00 空, 14:00-15:00 空]
createTask({ title: "...", deadline: "14:00" }) → 这个 deadline 是蒙的
sendEmail({ to: "...", body: "明天下午两点开会" }) → body 里的时间也是蒙的
checkCalendar 明明返回了 14:00-15:00 有空,这是对的——但并行执行时这两个值被填进去的时候,模型根本没见过 checkCalendar 的结果,纯属瞎猫碰上死耗子。下一组任务里就蒙错了。
智能混合:别问能不能并行,问"有没有依赖"
关键不在"能不能并行",而在"参数来源是什么"。
复制代码function detectDependencies(toolCalls: ToolCall[]): {
independent: ToolCall[];
dependent: ToolCall[];
} {
const invoked = new Set<string>();
const independent: ToolCall[] = [];
const dependent: ToolCall[] = []; for (const call of toolCalls) {
const paramValues = Object.values(call.arguments).join(" ");
// 检查参数是否引用了其他工具的输出(通过工具名或占位符标记)
const hasRef = toolCalls.some(
other => other !== call && paramValues.includes(`${${other.name}}`)
);
if (hasRef) {
dependent.push(call);
} else {
independent.push(call);
}
}
return { independent, dependent };
}async function runHybrid(agent: Agent, toolCalls: ToolCall[]): Promise<ToolResult[]> {
const results: ToolResult[] = [];
let remaining = [...toolCalls]; while (remaining.length > 0) {
const { independent, dependent } = detectDependencies(remaining); // 无依赖的工具并行执行
if (independent.length > 0) {
const batchResults = await Promise.all(
independent.map(call => agent.executeTool(call))
);
batchResults.forEach((r, i) => {
results.push(r);
agent.appendContext({
role: "tool",
content: JSON.stringify(r),
toolCallId: independent[i].id,
});
});
remaining = dependent;
} else {
// 全都有依赖,串行执行
const call = remaining[0];
const r = await agent.executeTool(call);
results.push(r);
agent.appendContext({ role: "tool", content: JSON.stringify(r), toolCallId: call.id });
remaining = remaining.slice(1);
}
}
return results;
}
思路不复杂:先扫一遍所有工具调用,看参数里有没有引用其他工具的输出(比如 ${checkCalendar}),能分开的就并行,有依赖的等一等。
实测数据证明这个简单判断比盲开并行靠谱得多。无效调用从 27% 降到 6%,不是因为这算法多聪明,是因为它直接避免了"信息没到就填参数"的根本问题。
你可能想问:参数里没有显式的 ${toolName} 占位符怎么办?确实,大部分 LLM 生成的 tool_arguments 不会带这种标记。实际项目中我用了一层包装:在 system prompt 里要求模型在依赖其他工具返回值的参数处加 "depends_on": "toolName" 字段,执行层在路由前做依赖分析。
复制代码// system prompt 片段
const SYSTEM_PROMPT_DEPENDENCY = `
当你调用工具时,如果某个参数的值依赖于另一个工具的返回结果,
在该参数旁添加 "depends_on" 字段,值为被依赖的工具名称。
示例:
{
"name": "createTask",
"arguments": {
"title": "跟进客户需求",
"deadline": "需要从 checkCalendar 获取",
"depends_on": "checkCalendar"
}
}
`;// 依赖解析升级版
function resolveHybrid(
calls: ToolCall[],
context: Map<string, ToolResult>
): { batch: ToolCall[]; wait: ToolCall[] } {
const batch: ToolCall[] = [];
const wait: ToolCall[] = [];
for (const call of calls) {
const dep = call.arguments["depends_on"] as string | undefined;
if (dep && !context.has(dep)) {
wait.push(call); // 依赖还没产生,等等
} else {
batch.push(call); // 可以直接跑
}
}
return { batch, wait };
}
模型配合度比预想的高。200 组任务里,LLM 在 86% 的情况下正确标记了依赖关系(剩下的要么漏标,要么标错,但不会导致崩溃,只是退化为串行)。顺便说一句,你要是觉得让 LLM 自己标记自己的依赖有点魔幻,实话说,我也觉得——但它确实能用,而且比我在代码里用 AST 分析参数活得多了。
什么时候可以无脑并行?
有一种情况并行基本不会翻车:所有工具都是"纯查询",不产生副作用,且参数完全独立、不依赖任何上下文动态内容。比如同时搜文档、查日历、拉配置——这三个工具的 input 完全来自用户原始问题,不存在互相依赖。
一旦涉及写操作(create/update/delete/send),或者参数里出现了从其他工具"应该"返回的值,老老实实用混合策略。
给你个结论
| 你的场景 | 用什么 |
|---|---|
| 全查询、参数独立 | 并行,省钱又省时间 |
| 有写操作、参数可能互依赖 | 混合策略 |
| 预算极紧、延迟不敏感 | 串行 |
这三种策略的代码我在雷达鸭的客服 Agent 上跑了两周,混合策略的效果跟测试数据一致——搜文档、查用户信息、发邮件、建工单这几件事儿,延迟比全串行少了一截,token 也没多烧多少。如果你也在做类似的多工具 Agent,建议拿自己的场景跑一遍,别看我数据就直接拍。
作者:老三,10 年以上软件开发经验,软件设计师、人工智能应用工程师,专注鸿蒙 ArkTS 北向开发与 Web 前端,业余折腾 AI 自动化。不定期在 CSDN 分享鸿蒙和 AI 方向的技术文章。
相关文章
- 星布谷地12种花朵的获取星系是哪里 07-15
- 学信网如何查证书编号-学信网查询证书编号方法 07-15
- picacg哔咔头像-怎么改 07-15
- 境界刀鸣市丸银角色强度如何 07-15
- 层级选择无需手写联动:TinyVue Cascader 级联选择器上手 07-15
- Vue-i18n 与 TypeScript 补全提示 07-15