想象一家餐厅的晚高峰。如果只有一位主厨从头到尾独自完成每道菜——先做凉菜,再煲汤,再炒热菜,最后烤肉——客人要等两小时。现实中餐厅怎么做?主厨把订单拆开:凉菜台、汤锅台、热炒台、烤箱台同时开工,最后统一摆盘上桌。更妙的是,主厨心里有谱:汤要提前煲、烤肉要先腌制,这些是“依赖”;灶台就那几个,不能一口气接十桌单;某道菜炒砸了,其他菜照常上,绝不整桌重做。 AI Agent 调用工具,面临同样的选择。串行调用:问完天气再查航班,查完航班再比酒店,一次一问,耗时是所有工具耗时的累加。并行工具调用:把无依赖的多个查询同时发出去,总时延从“总和”变成“最慢的那一个”。这也是实时问答、多源数据聚合、并行信息检索等场景里 Agent 都在用的“并行术”。 这篇文章,我们就用这家餐厅的运转逻辑,把并行工具调用的依赖分析、并行识别、并发控制、结果合并、异常隔离,一桌一桌讲清楚。

岗位关联

学会并行工具调用,你的 Agent 响应时延能降 50%-80%。在实时问答、语音交互、多源聚合这些“秒回”场景里,这是从“能用”迈向“好用”的分水岭。

目录
  1. 可视化演示
  2. 并行基础:总时延从“求和”到“取最大”
  3. 依赖分析:先做哪道菜,心里要有谱
  4. 并行识别:找出能同时下锅的菜
  5. 并发控制:灶台有限,排队等候
  6. 结果合并:出菜口按桌号摆盘
  7. 异常隔离:一道菜失败,不连坐整桌
  8. 工程实践:把并行术跑进生产
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示并行工具调用三大机制:DAG依赖图识别可并行工具、串行vs并行时间对比、以及异常隔离与部分失败容错处理策略。

本节配有交互动效(进入视口后自动加载,约 3KB)。

DAG依赖图:自动识别可并行工具 构建工具调用有向无环图分析依赖 无依赖关系的工具自动标记为可并行 并行组内工具同时执行,有依赖的串行 DAG分析是并行调用的基础。

并行基础:总时延从“求和”到“取最大”

餐厅里如果只有一位厨师,晚上八点同时进来三桌客人,他只能一桌一桌做。这在信息技术里叫“串行”——一次只做一件事。坏处显而易见:总耗时等于每道菜耗时之和。Agent 也一样:一个请求要依次调用“天气查询→航班查询→酒店比价→门票预订”四个工具时,一次问答的时延就是四段网络往返的简单叠加,用户等得心焦。

主厨很快发现,凉菜和烤肉互不干扰,完全可以同时做。放到 Agent 里,工具调用之间也分两种:有依赖的(后一个需要前一个的输出)和无依赖的(各问各的)。无依赖的一组调用,可以组成一个“批次”同时发出。这就是并行工具调用的出发点:把等待时间重叠起来。

串行总时延是累加,并行总时延是取最大值。前提是这些调用彼此独立、互不读取对方的中间结果。实际收益受“最慢的那个工具”限制——就像一桌菜里烤鸭要四十分钟,凉菜再快也得等烤鸭上桌才算上齐。

回到餐厅:主厨拿到订单的第一步,不是埋头做菜,而是先判断哪些菜能“同时下锅”。能同时下锅的越多,整桌上齐越快;但只要有一道炖四十分钟的硬菜,整桌就快不过它。并行术的第一课,就是认清“总和”与“最慢”之间的天壤之别。

T串行=∑i=1nti,T并行=max⁡1≤i≤ntiT_{\text{串行}} = \sum_{i=1}^{n} t_i, \qquad T_{\text{并行}} = \max_{1 \le i \le n} t_i
串行 = 把每个工具的耗时全部加起来;并行 = 整个批次只看最慢的那个工具。
  1. 单个工具耗时 \(t_i\)第 i 个工具从发出到返回的完整时间,包含网络往返与工具自身处理时间,是时延计算的基本单位。
    ti=t网络往返+t工具执行t_i = t_{\text{网络往返}} + t_{\text{工具执行}}
  2. 串行求和 \(\sum\)串行时工具一个接一个,后一个只能干等前一个结束,总时间等于各耗时机械相加。
    ∑i=1nti=t1+t2+⋯+tn\sum_{i=1}^{n} t_i = t_1 + t_2 + \cdots + t_n
  3. 并行取最大 \(\max\)并行时所有工具同时开始,批次完成时刻由最慢者决定,其他工具在它执行期间已经返回并空闲。
    max⁡1≤i≤nti=max⁡(t1,t2,…,tn)\max_{1 \le i \le n} t_i = \max(t_1, t_2, \dots, t_n)

依赖分析:先做哪道菜,心里要有谱

不是所有菜都能同时做。熬高汤的汤锅台要先于借用高汤的热炒台开工;烤肉要先腌制两小时。主厨心里那张“先后关系图”,就是 Agent 的依赖图。工具 A 的输出是工具 B 的输入时,B 必须等 A 完成,强行并行只会让 B 拿到空参数而报错。

依赖图用有向边表达“谁等谁”:节点是工具,边表示数据流向。有了这张图,还要做拓扑排序——给每个工具排一个合法执行顺序,保证被依赖方永远排在依赖方前面。排序之后,同一层的工具互不依赖,天然适合放进同一个并行批次。

排序过程中一旦发现环路——A 需要 B、B 又需要 A——就说明任务描述自相矛盾,没有合法执行顺序。工程上的处理是直接报错或人工介入拆环,而不是让它进入“互相死等”的空转。分层分组的技巧在于:层号越小越先执行,同一层并行。

主厨心里没有这张“先后图”,就会把没腌好的肉塞进烤箱。Agent 心里没有依赖图,就会把需要航班号的余票查询和航班查询一起发出去,然后收到一堆“参数缺失”的报错。依赖分析,是并行术的地基。

∀ (i→j)∈E:  level(j)>level(i)\forall\, (i \to j) \in E:\; \text{level}(j) > \text{level}(i)
所有依赖边都必须满足“被依赖方层号更小”,这是并行调度合法性的底线。
  1. 依赖边 \(i \to j\)一条从工具 i 指向工具 j 的边,表示 tool_j 的输入引用了 tool_i 的输出,这是构建依赖图的原材料。
    E={(i,j)∣input(j) 引用 output(i)}E = \{(i, j) \mid \text{input}(j) \text{ 引用 } \text{output}(i)\}
  2. 分层号 \(\text{level}\)把工具按依赖深度编号,没有前驱的层号为 1,其余工具的层号 = 所有前驱最大层号 + 1。
    level(j)=1+max⁡i→jlevel(i)\text{level}(j) = 1 + \max_{i \to j} \text{level}(i)
  3. 严格大于 \(\gt\)每条边都必须满足 level(j) > level(i),保证被依赖方先执行完,所有并行调度都不允许违反这条约束。

并行识别:找出能同时下锅的菜

高汤没熬好,热炒台就闲着吗?不——主厨会先把不需要高汤的菜安排给热炒台。把工具分成“现在就能做”和“必须等别人”两组,这个筛选动作,就是并行识别。

识别的方法很直观:检查工具两两之间有没有依赖路径。所谓路径,不光是“直接依赖”,还包括“间接依赖”——A 的输出喂给 B、B 的输出喂给 C,那么 A 和 C 之间就存在一条可达路径,C 不能和 A 并行。只有两个工具双向都不可达,它们才是真正意义上的无依赖,可以安全地放进同一批次。

工程上,这一步通常在调用前做静态预分析:把整个工具调用图扫一遍,标记出所有无依赖对。参数独立性是另一个必要条件——即使两个工具在依赖图上没连边,如果它们读写同一个共享状态(比如同时写同一个用户缓存),也要小心并发冲突。预分析的结果就是一张“可并行矩阵”,运行时直接查表分组,省去每次调度的重复计算。

主厨扫一眼菜单,就能报出“这三道可以同时做,那两道得等汤好”。Agent 的并行识别做得也是同样的事:在动手前把“能同时下锅的菜”圈出来,剩下的任务自然沉淀到后面的批次。识别得越准,时间重叠得越多。

Para(i,j)  ⟺  ∄ path(i→j)  ∧  ∄ path(j→i)\text{Para}(i, j) \iff \nexists\, \text{path}(i \to j) \;\land\; \nexists\, \text{path}(j \to i)
工具 i 与 j 能并行,当且仅当依赖图中两个方向都不存在可达路径。
  1. 并行判定 \(\text{Para}(i, j)\)定义工具 i 与工具 j 能否放进同一个并行批次同时执行。
  2. 可达路径 \(\text{path}\)依赖图中“顺着边走几步能到对方”的关系,间接依赖也是依赖,必须一并计入。
    path(i→j)  ⇔  j 直接或间接需要 i 的输出\text{path}(i \to j) \;\Leftrightarrow\; j \text{ 直接或间接需要 } i \text{ 的输出}
  3. 双向不可达 \(\nexists\)两个方向都找不到路径,才说明两者互不等待,是真正可以同时下锅的“菜”。

并发控制:灶台有限,排队等候

厨房里灶台只有六个。同时开工的任务超过六个,帮厨互相撞车、煤气不够、出菜口堆满盘子。主厨定下的“最多同时开 N 个灶”,就是并发度上限。Agent 调用外部 API 同样有隐形的灶台:QPS 限制、连接池大小、下游服务的负载能力。

实现并发控制最常用的工具是信号量(Semaphore):一个计数器记录当前正在执行的工具数,达到上限时,新任务进入等待队列,等某个工具完成后让出一个“灶位”再放行。排队策略可以简单粗暴(先来先服务),也可以带优先级(关键路径上的工具插队)。

资源预算同样重要:内存、网络连接、第三方额度都是有限的。总预算除以单次调用开销,就能估算出一个安全的并发上限——宁可在队列里多等 50 毫秒,也不要一次性把下游打挂。并发度为 k 时,总耗时存在一个理论下界,它由“最慢工具”和“总工作量均摊”共同决定。

主厨不会因为客人催单就把二十道菜同时丢进六个灶台——那只会得到一片焦糊和更大的混乱。Agent 也一样:并发不是越多越好,而是“在资源允许的范围内,让每一个灶台都不闲着”。排队不是退步,而是对整体稳定性的保护。

Tmin⁡≥max⁡(max⁡1≤i≤nti,  ∑i=1ntik)T_{\min} \ge \max\left(\max_{1 \le i \le n} t_i,\; \frac{\sum_{i=1}^{n} t_i}{k}\right)
并发度为 k 时,总耗时不可能低于“最慢工具耗时”与“总工作量除以 k”两者中的较大者。
  1. 并发度 \(k\)同一时刻最多执行的工具数,对应“灶台总数”,是调度器的硬约束。
  2. 最慢工具下限 \(\max_i t_i\)无论怎么调度,最慢的那个工具始终是单点下限——它不结束,批次就不结束。
  3. 工作量均摊 \(\sum t_i / k\)把全部时长的工作量平摊到 k 个“工位”上,工位越少这个下界越高。
    ∑i=1ntik=t1+t2+⋯+tnk\frac{\sum_{i=1}^{n} t_i}{k} = \frac{t_1 + t_2 + \cdots + t_n}{k}
  4. 取两者较大者 \(\max\)真正的理论最低时延不会低于这两个下界中的任何一个,是评估调度优劣的标尺。

结果合并:出菜口按桌号摆盘

各工位完工时间不同——烤箱的烤肉先出炉,凉菜反而后做好?出菜口不能谁先到谁上桌,必须按桌号、按菜单顺序摆盘。Agent 的并行调用也一样:先返回的未必是第一个请求的结果,必须按原始序号重新排列,否则就会把“机票价格”填进“天气温度”的位置。

这就是乱序完成问题。并行发出四个请求后,它们各自走各自的网络路径,完成顺序完全是“谁先到家谁先敲门”。正确处理方式是:给每个工具调用挂一个序号或请求 ID,返回时按 ID 放进对应的格子,而不是按到达时间的先后追加进数组。

合并阶段还要做去重与聚合:多个工具可能返回重复数据(比如两次检索命中同一篇文章),先按主键去重;然后按业务规则聚合排序,比如把机票、酒店、景点按总预算排成一张比价表。最终交付给调用方的结果,必须保持与发出调用时一致的顺序,这叫顺序一致性——让上层代码感觉不到并行的存在,只享受并行的速度。

主厨在出菜口做的事,从来不是“谁先出锅谁上桌”,而是“每一桌的菜按菜单顺序齐活”。结果合并就是 Agent 的出菜口:外面看起来秩序井然,里面早已千帆竞发。

Rout=[rπ(1),  rπ(2),  …,  rπ(n)]R_{\text{out}} = \left[ r_{\pi(1)},\; r_{\pi(2)},\; \dots,\; r_{\pi(n)} \right]
输出结果 = 把所有返回结果按原始调用序号重新排列后的数组。
  1. 结果集合 \(\{r_i\}\)第 i 个工具调用返回的内容,每个结果都携带着自己的调用序号。
  2. 重排映射 \(\pi\)完成顺序五花八门,需要一个“序号 → 结果”的映射表来归位,而不是按完成时间排队。
  3. 按原始序号输出最终返回时严格恢复发出时的顺序,保证调用方看到的结果次序与请求次序一致。
    Rout[k]=rk(k=1,2,…,n)R_{\text{out}}[k] = r_k \quad (k = 1, 2, \dots, n)

异常隔离:一道菜失败,不连坐整桌

蒸锅台的火候没控制好,一条鱼蒸老了。主厨不会把整桌菜都倒掉——他会给这桌客人补一道菜,其他菜照常上。厨房里任何工位的失误,都不能连坐整桌。这就是异常隔离:并行调用中,单个工具失败不应该拖垮整个批次。

现实中最常见的故障是慢和挂:某个第三方 API 迟迟不返回,把整个批次的完成时刻无限拉长。对策是超时熔断——为每个工具设定等待上限 \(\tau\),超时就掐断这次调用并标记失败;连续失败多次后直接熔断,短时间内不再请求该工具,防止它继续拖累整体。降级补偿则是失败后的兜底:用缓存数据、默认值或占位文案填上失败的位置。

部分失败是并行场景的常态。四个调用成功三个,我们就返回三个真实结果加一个降级结果,而不是把整批结果全部作废。比起串行模式“一个出错全链报废”,并行加隔离让系统有了优雅降级的能力——用户至少拿到大部分答案,而不是一句冷冰冰的“服务错误”。

主厨的哲学是:一道菜失误是局部事故,整桌餐是整体承诺。异常隔离让 Agent 在部分工具失灵时依然能“把菜上齐”——用降级替代失败,用超时替代无限等待。这种韧性,正是生产环境里并行调用比串行调用更难也更有价值的地方。

Tbatch=max⁡1≤i≤nmin⁡(ti,  τi)T_{\text{batch}} = \max_{1 \le i \le n} \min(t_i,\; \tau_i)
每个工具生效耗时 = min(实际耗时,超时阈值),批次总耗时 = 所有生效耗时的最大值。
  1. 实际耗时 \(t_i\)工具的真实执行时长,网络挂起时可能趋于无穷,必须给它上“闹钟”。
  2. 超时阈值 \(\tau_i\)为每个工具设定的等待上限,超过即终止等待并进入失败处理流程。
  3. \(\min(t_i, \tau_i)\) 截断把“无限等待”截断成“有界等待”,这是熔断的核心动作。
    min⁡(ti,τi)={titi≤τiτiti>τi\min(t_i, \tau_i) = \begin{cases} t_i & t_i \le \tau_i \\ \tau_i & t_i > \tau_i \end{cases}
  4. \(\max_i\) 聚合完成时刻批次完成时间由最晚结束者决定,超时保证了它不会无限晚。

工程实践:把并行术跑进生产

晚高峰的餐厅,主厨把这套“同时开工、出菜口统一摆盘、单菜失败不连坐”的流程跑顺之后,翻台率翻了一倍。实时 Agent 也一样:把并行术嵌进架构,让用户产生“秒回”的体验。我们来盘点三个最典型的落地场景。

第一个是实时问答。语音助手被问到“明天去上海出差要带伞吗?顺便订最早航班”,串行查询日历、天气、航班三个工具要 3 秒以上,用户早就打断重问了;并行发出后 1.2 秒内全部返回,合并成一句话回答。第二个是多源数据聚合查询:旅行规划 Agent 拿到“杭州 3 日游”请求,同时拉取航班、酒店、景点、天气四个渠道,按“总预算”统一排序输出比价表。第三个是并行信息检索(RAG 多路召回):同步检索 Elasticsearch、向量库、Wiki API 三路,去重后交给 LLM 生成答案;某一检索源超时自动降级为两路结果,整体体验不受影响。

衡量这套并行术的价值,最直接的指标是加速比——改造前的串行耗时除以改造后的并行耗时。工程上的经验值通常在 2 到 5 倍之间,具体取决于无依赖工具的数量和耗时分布。别忘了配合可观测性:把每个工具的耗时、失败率、排队时间都记下来,持续优化“最慢的那道菜”。

餐厅的晚高峰不会消失,但主厨的调度术让每一桌都能按时上菜。Agent 的请求洪峰也不会消失,但并行工具调用让每一次问答都快人一步。把这张“并行术”的流程图挂在架构图上,你的 Agent 就有了从“能用”到“好用”的底气。

S=T串行T并行S = \frac{T_{\text{串行}}}{T_{\text{并行}}}
加速比 = 串行总耗时 / 并行总耗时,S 越大说明并行术带来的提速越明显。
  1. 串行基线 \(T_{\text{串行}}\)改造前逐个调用工具的耗时总和,是衡量提速的参照系。
    T串行=t1+t2+⋯+tnT_{\text{串行}} = t_1 + t_2 + \cdots + t_n
  2. 并行耗时 \(T_{\text{并行}}\)采用依赖分组、并发控制、结果合并后,整个批次的实际总耗时。
  3. 加速比 \(S\)两者之比大于 1 表示变快;2 就是快一倍,5 就是快四倍,是汇报优化成果时最有力的数字。

AI 应用场景

场景一:实时语音问答助手

阶段:Agent 工具编排与并行调度

解决的问题:语音交互要求首响时间低于 2 秒,串行调用日历、天气、航班三个工具要 3 秒以上,用户明显感到迟钝甚至直接打断。并行发出后将总时延压缩到最慢工具的水平,给“秒回”体验留出余量。

具体案例:智能音箱被问到“明天去上海出差要带伞吗?订最早航班”。Agent 先做依赖分析,确认三个查询互不依赖,于是同时调用日历 API 确认行程、天气 API 查降水概率、航班 API 查最早班次,最后按原始顺序合并成一句话回答:“明天上海有雨,请带伞;最早航班是 7:20,预计 9:00 到达。”

场景二:多源数据聚合的旅行比价

阶段:并发控制与结果合并

解决的问题:旅行规划需要聚合机票、酒店、景点、天气四类数据,串行查询要四次网络往返,且一旦中间某个渠道超时整个流程卡死。并行加隔离让四个渠道同时返回,任何一个超时都只降级那一项。

具体案例:用户说“帮我规划杭州三天两晚的行程,预算 3000 元”。Agent 同时请求航班比价 API、酒店预订 API、景点门票 API、天气 API,四路返回后去重合并,按“交通 + 住宿 + 门票”总价排序,输出三套方案让用户挑选。全程耗时从串行的 4 秒降到 1.5 秒。

场景三:RAG 并行多路检索

阶段:并行信息检索与异常隔离

解决的问题:单库检索命中率不够,多路召回若串行执行会让问答时延成倍增长;更重要的是,某一路检索源宕机不应让整个问答失败。并行检索加超时降级是标准解法。

具体案例:企业知识库问答机器人收到“今年报销新规是什么”,同时检索 Elasticsearch 文档库、向量库相似段落、公司 Wiki API 三路。向量库响应超时 800ms 被熔断降级,剩余两路结果去重后交给 LLM 生成答案,用户无感知地拿到完整回答。

Python 演示

python72 行
import timefrom concurrent.futures import ThreadPoolExecutor, as_completed  def fetch_weather(city):    time.sleep(0.5)  # 模拟 0.5s 网络耗时    return "sunny 25C" def fetch_flight(city):    time.sleep(1.2)  # 模拟 1.2s 网络耗时    return "flight avg 1280 CNY" def fetch_hotel(city):    time.sleep(0.8)  # 模拟 0.8s 网络耗时    return "hotel min 320 CNY" def fetch_sight(city):    time.sleep(0.4)  # 模拟 0.4s 网络耗时    return "sights: West Lake, Lingyin" TOOLS = {    "weather": fetch_weather,    "flight": fetch_flight,    "hotel": fetch_hotel,    "sight": fetch_sight,}  def call_sequential(city, tool_names):    results = {}    t0 = time.time()    # 串行:一个接一个调用,总耗时是各工具耗时之和    for name in tool_names:        results[name] = TOOLS[name](city)    return results, time.time() - t0  def call_parallel(city, tool_names, max_workers=4):    results = {}    t0 = time.time()    # 线程池:最多 max_workers 个工具同时执行(对应并发度 k)    with ThreadPoolExecutor(max_workers=max_workers) as pool:        # 同时提交所有无依赖的工具调用,future 上挂上工具名        futures = {pool.submit(TOOLS[name], city): name for name in tool_names}        # 谁先完成谁先被处理,完成顺序与提交顺序无关        for fut in as_completed(futures):            name = futures[fut]            try:                results[name] = fut.result()            except Exception as exc:                # 异常隔离:单个失败只记录该工具,不影响其他结果                results[name] = "failed: " + str(exc)    return results, time.time() - t0  def merge_by_order(results, tool_names):    # 结果合并:按原始调用顺序重排,而不是按完成顺序    parts = []    for name in tool_names:        parts.append(name + "=" + results[name])    return " | ".join(parts)  if __name__ == "__main__":    city = "Hangzhou"    tool_names = ["weather", "flight", "hotel", "sight"]    seq_res, seq_cost = call_sequential(city, tool_names)    par_res, par_cost = call_parallel(city, tool_names)    print("sequential:", merge_by_order(seq_res, tool_names))    print("parallel:  ", merge_by_order(par_res, tool_names))    print("seq cost %.2fs vs par cost %.2fs" % (seq_cost, par_cost))    print("speedup: %.2fx" % (seq_cost / par_cost))
目的:用线程池模拟 Agent 并行工具调用,对比串行与并行的耗时,演示依赖无关的四个工具如何“同时开工、按序归位”。
  1. with ThreadPoolExecutor(max_workers=max_workers) as pool:创建“灶台”线程池,同时最多跑 max_workers 个工具,这就是并发度上限。
  2. futures = {pool.submit(TOOLS[name], city): name for name in tool_names}把四个无依赖的工具一次性提交进线程池,每个 future 挂上自己的工具名,对应并行识别后的“同批派遣”。
  3. for fut in as_completed(futures):谁先完成谁先被收割,直观体现“乱序完成”——完成顺序与提交顺序无关。
  4. results[name] = fut.result()按 future 上挂的工具名把结果放进对应格子,天然实现“按名归位”。

常见误区

误区并行能省时间,那我干脆把 Agent 里所有工具调用都丢进并发池,响应一定最快。

正解有依赖的调用绝对不能并行——先查航班列表,才能查某航班余票;先算好总价,才能发起支付。盲目并行轻则参数缺失报错,重则拿到错误答案。正确做法是先做依赖分析,只把无依赖的工具子集组成批次,有依赖的按拓扑顺序分批执行。

为何常见:只看到并行省时的直观收益,忽略了“后一个工具需要前一个工具的输出”这个最基本的数据流约束。

误区反正要多快好省,把 max_workers 调到 100,所有工具一瞬间全发出去,总时延肯定趋近于零。

正解并发受三重约束:外部 API 的速率限制会直接拒绝超量请求、线程上下文切换消耗 CPU、下游服务负载过高会超时雪崩。并发开过头,大量请求被限流重试,反而比串行更慢。工程上通常取 3-8 个并发,配合信号量限流和排队策略。

为何常见:把“并行”等同于“无限并行”,忽略了每个真实服务都有吞吐上限这个物理事实。

误区我并行发出四个请求,哪个先返回,结果数组里就把它放在哪个位置,反正是同时发的。

正解完成顺序和调用顺序没有任何关系——网络延迟、服务端负载都会让结果乱序到达。必须给每个调用挂序号或请求 ID,返回时按 ID 归位,最后按原始调用顺序输出,否则就会把“机票价格”填进“天气温度”的位置,张冠李戴。

为何常见:串行时代完成顺序天然等于调用顺序,思维惯性没有切换到“乱序完成”的新模型。

误区并行调用中有一个工具抛异常,整体就应该 catch 后重试,或者直接告诉用户“服务出错”。

正解正确的做法是部分失败隔离:失败的那一项用降级结果兜底(缓存值、默认值、占位文案),其余成功结果照常使用;只有关键路径上的工具失败才考虑整组失败。重试必须配合熔断,连续失败多次就停手,防止无限重试把下游彻底打爆。

为何常见:程序员习惯了数据库事务“全有或全无”的思维,忘了读多写少的 Agent 场景大多能容忍部分信息缺失。

面试题

Q1. 串行调用 n 个独立工具,总时延大约是?

单选题 · easy

  • A.各工具耗时之和← 正确答案
  • B.最慢工具的耗时
  • C.所有工具的平均耗时
  • D.最快工具耗时的两倍

答案:A。串行就是排队,一个做完才做下一个,总时延等于所有工具耗时求和。选 B 混淆了串行与并行——并行才看最慢的那个。类比餐厅:一道菜一道菜做,一桌菜的时间就是所有菜的时间相加。

Q2. 并行调用 n 个相互独立的工具,整个批次的时延大约等于?

单选题 · easy

  • A.接近最慢工具的耗时← 正确答案
  • B.等于全部工具耗时的和
  • C.等于所有工具耗时的差
  • D.接近最快工具的两倍

答案:A。并行时所有工具同时开始,批次完成时刻由最慢者决定,其余工具在它执行期间已经返回。这是“总和”与“最大值”的区别,也是并行术最核心的收益来源。

Q3. 工具 A 的输出是工具 B 的输入,正确的执行方式是?

单选题 · mid

  • A.同批并行执行 A 与 B
  • B.先执行 A 再执行 B← 正确答案
  • C.只执行 B 跳过 A
  • D.先执行 B 再执行 A

答案:B。存在数据依赖就必须按拓扑序执行:先产出再消费。选 A 会让 B 拿不到输入直接报错;选 D 顺序颠倒必然出错。类比厨房:必须先熬高汤,才能做用高汤的菜,两件事不能同时下锅。

Q4. 依赖关系通常用什么结构表达并进行排序校验?

单选题 · mid

  • A.有向无环图加拓扑排序← 正确答案
  • B.单向链表加顺序遍历
  • C.哈希表做键值映射
  • D.最小堆做优先级队列

答案:A。依赖关系天然是“谁等谁”的有向关系,用有向图建模,用拓扑排序求出合法执行顺序,同时能发现环路。链表表达不了多对多依赖,哈希表管不了顺序,堆只适合按优先级出队,都不匹配。

Q5. 并发度为 k 时,总工作量均摊到每个工位的理论下界是?

单选题 · mid

  • A.总工作量除以并发度 k← 正确答案
  • B.总工作量乘以并发度 k
  • C.最慢工具耗时除以并发度 k
  • D.所有工具耗时之差的总和

答案:A。k 个“灶台”同时工作,理想情况下总工作量被均匀分摊,下界是总耗时之和除以 k。这是并行调度的理论下限之一,另一个下界是最慢工具的耗时,实际时延不会低于两者中的较大者。

Q6. 以下哪些情况会显著削弱并行调用的收益?

多选题 · mid

  • A.工具之间存在长依赖链← 正确答案
  • B.并发上限被压得很低← 正确答案
  • C.单工具耗时远超其他工具← 正确答案
  • D.工具返回结果全部完全相同

答案:ABC。长依赖链迫使任务只能逐层串行;并发上限低会让大量任务排队;单个工具耗时独占鳌头时,并行相比串行几乎没有提速。D 选项与耗时无关,不影响并行收益。

Q7. 并行调用返回结果乱序,应按什么恢复正确顺序?

单选题 · mid

  • A.按原始调用序号进行重排← 正确答案
  • B.按返回时刻先后进行排序
  • C.按工具名称字典序拼接
  • D.按结果内容长度进行排序

答案:A。每个工具调用在发出时就挂上序号,返回后按序号归位,最终按原始调用顺序输出。完成时刻、名称、内容长度都与调用语义无关,按它们排序会把结果张冠李戴。

Q8. 关于超时熔断与异常隔离,下列说法正确的是?

多选题 · hard

  • A.超时把无限等待截成有界等待← 正确答案
  • B.单个工具失败不影响其余结果← 正确答案
  • C.失败后应无限次地自动重试
  • D.失败工具可用降级结果兜底← 正确答案

答案:ABD。超时阈值保证批次不会因一个慢调用而无限拖延;异常隔离让单点失败不连坐整批;降级结果(缓存、默认值)兜住失败的位置。无限重试会打爆下游,必须配合熔断在连续失败后停手。

Q9. 并发度 k=3,6 个耗时相同的任务,理论最短完成时间约为串行的?

单选题 · easy

  • A.三分之一← 正确答案
  • B.二分之一
  • C.六分之一
  • D.三分之二

答案:A。6 个任务分到 3 个工位,每工位 2 个任务,并行耗时是串行的 2/6 = 1/3。类比餐厅:三个灶台同时开火,六道菜分两轮就能出齐。

Q10. 并行工具调用的工程实践包含哪些关键环节?

多选题 · hard

  • A.依赖分析后分组并行调度← 正确答案
  • B.并发度限流与排队等待← 正确答案
  • C.结果按请求序号合并← 正确答案
  • D.所有工具强制串行重试

答案:ABC。完整流程是:先画依赖图分组合并调度,再用信号量限制并发、超限排队,最后按请求序号把乱序结果重排输出。D 选项与并行术背道而驰——并行失败后的重试应该针对单个失败工具做熔断降级,而不是强制整批串行。