为什么串行会撞上延迟墙
某运维平台每天凌晨要对200~600条告警工单做根因研判,最初用for循环逐条调用LLM。单条工单Agent处理约60秒(LLM推理40s+工具调用20s),600条串行就是36000秒——实测从22:00跑到次日8:20,10小时20分。
延迟的数学结构是:串行总延迟 = Σ(每跳LLM推理 + 每跳工具调用 + 每跳排队等待),这个求和不可压缩,第5跳必须等第4跳返回。而MapReduce的总延迟 = max(所有子任务延迟) + Reduce聚合延迟。从求和变成求最大值,才是600条工单从10小时压到28分钟的根源——不是每个子任务变快了,而是它们不再互相等待。
实测数据:串行约10h20m;并发10+失败重试约1h05m(加速9.6×);并发24+分层归并约28m(加速22×)。业务逻辑和Prompt一字未改,改动核心只有两个词:扇出、归并。
四条判据决定是否该上MapReduce
命中三条以上才值得付出编排复杂度:
- 子任务独立性:能明确列出N个互不影响的输入。
- 延迟敏感度:串行预估超过30分钟,或用户会明显感知等待。
- 结果可归并性:能写出一个
reduce(items) -> summary函数。 - 资源可并发:下游LLM配额、连接池、限流能吃住并发。
误用信号同样明确:子任务极小(一次字符串格式化)、扇出度N≤3、结果无法归并只需并排展示、所有子任务抢同一把全局锁(并行度实际是1)。
Map阶段:受控扇出是核心
Map阶段只做一件事:把大输入切成N个独立子任务并让它们同时跑起来。三个关键词缺一不可。
独立性:子任务不共享可变状态,否则不是Map而是黑板模式。
可分区:N个子任务重量应大致相当。若600条工单里500条是磁盘占用告警(30秒判完),100条是数据库连接池超时(要查5个指标、60秒),并发24时尾部延迟由最慢的那批决定。分区策略有三种:轮询(items[i::N],简单均衡但破坏局部性)、哈希(hash(key) % N,同key落同分区便于Reduce对齐,但可能热点)、预估耗时加权(尾部延迟最优,需历史数据)。实践建议:先用轮询,有了P95耗时分布再考虑加权。
并发控制:扇出不是一次性全扔出去。LLM API有并发配额和速率限制,一次扔200个请求会触发大量429、指数退避打乱吞吐、账单异常。正确做法是包装成受控并发池:信号量管"同时几个",令牌桶管"每秒几个"。只做前者,在600任务×60秒场景下等效速率随并发度线性上升,很容易撞RPM限制。
Shuffle:中间态路由的两个关键问题
Shuffle是Map和Reduce之间的胶水层,Agent场景下最容易被省略。它要回答两个问题。
Reduce收到的顺序重要吗? 多数情况不重要,但做"逐条对比找矛盾"时顺序会影响结果。稳妥做法是让每个MapResult携带key和index,由Reduce阶段显式决定是否排序,而非依赖返回顺序。
Reduce要不要一次性看到全部N份结果? 这是最关键的设计决策,答案是不要。N=600时把600份结果塞进上下文等于自杀,上下文窗口是有限资源,Reduce还需为最终报告预留预算。MapReduce必须配合分层归并才能规模化。
Reduce四种归并策略
按是否调用LLM分四类,成本和可靠性差异极大。
确定性拼接(无LLM):按模板直接拼,无随机性、完全可预测,但失去归纳能力。适合结果本身已是可展示结构的场景。
结构化抽取(单次LLM,裁剪后输入):先用代码把N份结果裁剪成精简摘要(每份只留key/结论/置信度/证据数),再让LLM归纳一次。这是生产环境最常用的默认策略。
投票裁决(多次LLM):同一输入交给k个采样或k个模型取多数。成本是策略二的k倍。在"这条工单是不是误报"的二元判断上,实测3次投票能把准确率从86%提到94%。
树形归并(多次LLM,递归):两两归并逐层收敛,600→300→150→75→38→19→10→5→3→2→1。每层只处理上一层一半,单次输入规模恒定,总调用次数O(N)但单次上下文规模恒定。这是突破规模上限的关键。
| 策略 | LLM调用 | 单次输入 | 成本 | 可靠性 | 适用 |
|---|---|---|---|---|---|
| 确定性拼接 | 0 | 全部 | 最低 | 最高 | 结果已是可展示结构 |
| 结构化抽取 | 1 | 裁剪后 | 低 | 中 | 通用默认 |
| 投票裁决 | k | 裁剪后 | 中 | 高 | 二元/少分类判断 |
| 树形归并 | ≈N | 恒定 | 高 | 高 | N很大、需深度归纳 |
两套实现骨架
asyncio原生方案:用asyncio.Semaphore限制在飞请求数,用asyncio.gather收集结果,每个worker返回Pydantic V2定义的强类型中间态。失败任务单独收集,不阻塞其他任务。
LangGraph Send方案:把扇出表达成图。用Send在条件边中动态派发N个子任务到同一节点,图编译后自动并行执行,Reduce节点作为汇聚点。Send的导入路径与图编译细节需以所安装的LangGraph版本文档为准。
选型上,asyncio方案依赖少、调试直观,适合快速验证;LangGraph方案把编排状态显式化,适合需要持久化、断点续跑和可视化追踪的生产场景;任务队列方案适合跨进程、跨机器的超大规模扇出。
三个典型陷阱
扇出度不是越高越好。理想加速比随并发上升而递减,拐点取决于下游配额和单任务耗时方差。并发24在600任务场景下已接近收益上限。
聚合阶段上下文爆炸。一次性归并600份结果会超窗,必须用树形归并或先裁剪再归并。
部分失败拖垮全局。容错语义应是部分失败优于整体失败:单个子任务失败不应让整批回滚,失败项进入重试队列或标记为"未定性",Reduce阶段显式处理缺失项。
适用边界
MapReduce适用于子任务相互独立、可并行、结果可结构化归并的场景。若子任务存在先后依赖,应回到Pipeline;若需运行时动态决定下一个派谁,应看Supervisor模式。它与Pipeline不是互斥而是嵌套的:Map阶段内部每个子任务往往本身就是一条小Pipeline,Reduce阶段聚合本身也可能是一条Pipeline。