4. 任务分解基础
工具执行引擎,说白了就是整个编排系统的「心脏」。你前面把任务拆得再漂亮,工具定义得再规范,最后落地执行时如果引擎拉胯,一切都白搭。我这些年见过太多系统,流程图画得天花乱坠,一跑就超时、一报错就崩溃,问题全出在执行引擎上。
这一章,我们就把引擎的核心机制掰开揉碎。同步还是异步?超时怎么设?重试几次合适?异常怎么传?这些都是实战中每天都要面对的问题。
4.1 同步调用 vs 异步调用
先解决最基础的选择题:你的工具调用,到底该同步还是异步?
| 特性 | 同步调用 | 异步调用 |
|---|---|---|
| 调用方式 | 发起后阻塞等待结果 | 发起后立即返回,通过回调/轮询获取结果 |
| 适用场景 | 耗时短、依赖强、需要即时结果 | 耗时长、可并行、结果不紧急 |
| 资源占用 | 线程/协程被占用等待 | 释放调用方资源 |
| 错误处理 | 直接抛出异常 | 通过回调或状态机传递 |
| 编排复杂度 | 低,线性流程 | 高,需要状态管理 |
我个人习惯的选型原则很简单:如果这个工具调用是下一个步骤的前置条件,且执行时间在 100ms 以内,用同步。否则,考虑异步。
举个例子,一个用户注册流程:
- 同步调用:校验手机号格式、检查用户名是否重复。这些操作毫秒级完成,必须拿到结果才能继续。
- 异步调用:发送欢迎邮件、同步到第三方 CRM。这些操作耗时不确定,而且失败也不影响主流程。
核心原则:同步保证强一致性,异步换取高吞吐。不要为了「省事」把所有调用都做成同步,也不要为了「炫技」把简单调用搞成异步。
4.2 超时机制:别让系统死等
没有超时的系统,就像没有刹车的车。我曾经接手过一个线上事故,一个第三方天气 API 挂了,结果整个编排流程卡在那里 30 分钟,所有请求都在等它超时。嗯,那个 API 的默认超时是 30 分钟。
超时设置有几个关键参数:
- 连接超时:建立 TCP 连接的最大等待时间。通常 3-5 秒。
- 读取超时:从连接建立到收到完整响应的最大时间。根据工具特性来定。
- 总超时:整个调用从开始到结束的最大时间。包含重试时间。
我建议的配置模板:
// 工具调用超时配置示例
{
"tool_id": "weather_api",
"timeout": {
"connect": 5000, // 5秒连接超时
"read": 15000, // 15秒读取超时
"total": 30000 // 30秒总超时(含重试)
},
"retry": {
"max_attempts": 3,
"backoff": "exponential",
"initial_delay": 1000
}
}
避坑指南:我曾经把连接超时设成 1 秒,结果用户网络波动时频繁失败。后来改成 5 秒,配合重试,成功率从 92% 提升到 99.7%。超时不是越小越好,要结合你的网络环境和工具响应分布来调。
4.3 重试机制:优雅地处理失败
重试不是简单的「失败了再来一次」。不加控制的重试,就是雪崩的导火索。
常见的重试策略:
- 固定间隔重试:每次等待相同时间。简单,但容易造成「惊群效应」。
- 指数退避:每次等待时间翻倍。比如 1s、2s、4s、8s。最推荐的方式。
- 抖动退避:在指数退避基础上加随机偏移。防止多个请求同时重试。
- 增量退避:每次增加固定时间。比如 1s、3s、5s。
你想想看,为什么指数退避最常用?因为大部分临时故障(网络抖动、服务重启)都会在几秒内恢复。指数退避既给了恢复时间,又不会在故障持续时浪费太多资源。
重试还需要注意:
- 幂等性检查:只有幂等的工具才能重试。扣款接口重试两次,用户就多付了一笔钱。
- 最大重试次数:通常 2-3 次。超过 5 次基本说明问题不是临时的。
- 重试风暴防护:全局限制同一工具的重试并发数。
我的经验:对于非幂等操作,不要自动重试。改为「标记失败,人工介入」或者「进入死信队列」。我在金融项目里就是这么干的,宁可慢一点,不能错一笔。
4.4 错误处理与异常传播
错误处理是执行引擎最容易写烂的部分。很多人的做法是:try-catch 一包,打印个日志,返回个「系统错误」。这等于把问题藏起来了。
好的错误处理应该分层:
| 层级 | 职责 | 处理方式 |
|---|---|---|
| 工具层 | 捕获原始异常,转换为标准错误码 | 返回错误对象,不吞异常 |
| 编排层 | 根据错误类型决定重试、跳过还是终止 | 更新 DAG 状态,触发补偿 |
| 业务层 | 处理业务语义错误,通知用户 | 生成友好提示,记录审计日志 |
异常传播的路径:
// 异常传播示例
工具A 调用失败
→ 工具层抛出 ToolExecutionException(code=503, message="服务暂时不可用")
→ 编排层判断:503 错误,可重试
→ 重试 2 次后仍失败
→ 编排层标记工具A 为 FAILED
→ 检查 DAG:工具A 是可选依赖,跳过
→ 继续执行后续工具
→ 最终返回部分成功状态
这里有个关键设计:异常一定要携带足够的上下文。光说「调用失败」没用,要告诉上层:哪个工具、什么错误码、耗时多久、重试了几次、原始异常是什么。
异常传播三原则:
- 不吞异常:除非你知道怎么处理,否则往上抛
- 不丢上下文:原始异常、调用链、参数快照都要带上
- 不泄露敏感信息:数据库密码、用户隐私不要传到上层
4.5 执行引擎核心架构
说了这么多,我们把执行引擎的整体架构画出来。这张图我每次做系统设计都会先画一遍,它决定了整个编排系统的可靠性。
这张图里,我特别想强调两个点:
- 重试管理器是独立组件:它不耦合在同步或异步执行器里。这样你可以统一配置重试策略,不管是同步调用还是异步调用,都走同一套重试逻辑。
- 错误处理器在重试之后:先重试,重试耗尽再进错误处理。顺序不能反,否则重试机制就形同虚设了。
4.6 实战中的几个坑
最后分享几个我踩过的坑,希望能帮你省点时间:
坑一:异步调用的回调丢失
我曾经用消息队列做异步调用,结果消费者挂了,回调消息丢了,整个流程卡在「等待中」状态。后来加了回调超时检测和死信队列,才算解决。
坑二:重试导致资源耗尽
有一次线上故障,所有工具都超时,重试管理器疯狂重试,把线程池打满了。正常请求反而进不来。后来加了全局重试限流和熔断,才避免这种「自我攻击」。
坑三:异常吞没
最隐蔽的坑。有些开发者在工具层 catch 了异常,打印一行日志就返回 null。上层判断 null 就跳过,结果数据对不上,查了半天才发现是异常被吞了。我的原则:要么处理,要么传播,不要沉默。
执行引擎是编排系统的基石。同步异步选型、超时重试配置、错误处理策略,这三块做好了,你的系统至少能扛住 90% 的异常场景。剩下的 10%,靠监控和人工兜底。