Skill 编排时如何做超时控制和熔断?如果一个 Skill 长时间无响应,整体流程如何处理?

面试官:“在编排多个 Skill 时,你怎么处理超时和熔断?如果某个 Skill 长时间没响应,整个流程会怎么办?”

候选人:

“超时和熔断是分布式编排的生命线。如果没有这套保护机制,一个挂死的 Skill 会像堵在下水道里的头发,慢慢把整个线程池耗光,最终拖垮整个 Agent 服务。

我一般分三层防线来设计:第一层是单次调用的超时控制,第二层是连续失败触发的熔断器,第三层是整体流程的异常处理策略。


第一层防线:单次调用超时

每个 Skill 在注册元信息时,必须声明自己的预期响应时间(SLA),比如 timeout_ms: 5000。编排引擎在调用 Skill 时,不会无条件等待,而是启动一个带超时的异步调用。

具体实现上,可以用 CompletableFutureorTimeout 方法,或者用调度线程池延迟触发超时取消:

CompletableFuture<SkillResult> future = CompletableFuture
    .supplyAsync(() -> skill.execute(ctx), skillExecutor)
    .orTimeout(skill.getTimeoutMs(), TimeUnit.MILLISECONDS);

一旦超时,编排引擎主动中断调用线程(通过 Future.cancel(true)),并把这个 Skill 标记为 TIMED_OUT,立即释放线程资源。这是最基础的保护,保证任何一个 Skill 都不会无限占用执行线程。


第二层防线:熔断器

超时只解决单次异常,但如果不保护,编排引擎会反复重试一个已经挂掉的 Skill,浪费时间并给下游服务施压。因此我在每个 Skill 的客户端侧封装了一个熔断器(Circuit Breaker)。

熔断器维护一个滑动时间窗口的调用统计。当连续超时或错误次数达到阈值(比如 30 秒内 5 次失败),熔断器自动跳闸,状态变为 OPEN。在 OPEN 状态下,编排引擎直接跳过该 Skill 的远程调用,根据配置走降级逻辑,而不再傻傻地等超时。经过一段冷却时间(比如 30 秒)后,熔断器进入 HALF_OPEN,允许少量探测请求通过,如果探测成功则恢复 CLOSED,否则重新 OPEN。这就是经典的熔断三态机。

这个熔断器可以做成 Skill 通用的拦截器,通过 AOP 或代理方式集成到 Skill 调度层,无需 Skill 开发者自己实现。


第三层防线:流程级的异常处理策略

超时或熔断发生后,编排引擎需要根据 DAG 中边的依赖强弱来决定整体流程怎么走。我在 DAG 配置里把 Skill 之间的依赖分为三类:

  1. 强依赖 —— 失败阻断 如果 B 强依赖 A,而 A 超时或熔断了,B 直接标记为 SKIPPED,整个 DAG 的这条执行链中断。编排引擎触发补偿动作,比如调用回滚 Skill,同时把任务状态置为 FAILED,通知用户“机票预订超时,本次行程安排失败”。这种通常用于支付、下单等核心环节。

  2. 弱依赖 —— 降级继续 如果 B 只是弱依赖 A,A 挂了不影响 B。B 使用预先配置的降级默认值或跳过该参数。比如“查天气”超时,发送行程的 Skill 用一句“请留意当地天气”代替具体温度,而不是整个出行安排失败。弱依赖的降级策略在 Skill 元信息的参数定义中写得很清楚:fallback_value: "未知"on_missing: "skip"

  3. 可选依赖 —— 忽略

比如“推荐当地美食”这种锦上添花的 Skill,超时直接忽略,对主流程零影响。