Skip to content

[Task][P1] 将模型退避重试迁移为受控生命周期策略 #313

Closed

Description

@Aias00

本单的独立交付

仅同目标模型重试;明确 maxAttempts 包含第一次调用。

输入与输出

错误分类、最大总尝试次数和退避配置 → 有界 attempt 序列或明确终态。

必须通过的行为验收

maxAttempts=3 连续失败恰好调用 3 次;不可重试错误调用 1 次;退避期间取消后零新增调用;deadline 不足不再启动下一次;实际 usage 不重计。

非目标

不切换模型、不重放工具、不实现持久预算;#210 决定流式重试边界。

依赖与完成条件

父任务 #304;公共扩展契约/执行链复用 #298/#299,声明式装配复用 #300,引擎回调接线复用 #301。只依赖本单实际使用的最小阶段,不等待全部生命周期阶段。

提交需包含实际装配入口、配置示例、上述行为断言及旧配置保留/迁移说明;回调触发日志不足以验收,必须验证实际请求、结果或资源行为。离线夹具无需外部服务即可复现;真实服务保证单列证据。

实施约束

本节收窄此前范围,移出的职责由关联任务承担。建设任务,不是新 BUG;复用已有实现,独立 Agent/Workflow 不依赖 Graph/Loop,不强制新增模块。基线 Core 04c5686。破坏性变化须比较较小方案并说明收益、影响和迁移。核心可信身份、取消/预算、必经检查及提交保证不能被可选插件绕过。

Activity

  1. konlue commented on Nov 4, 2026

    @konlue
    Contributor

    您好,我想认领 #313。

    基于 main 03227f7b3 确认:

    1. 可重试性靠异常正文猜测:ModelRetryInterceptor.Builder.isRetryableException 按正文子串判定(i/o error、connection、timeout、handshake、socket),外加类名/cause 链匹配——正文里带这些词的永久性错误会被重试,而正文陌生的瞬时错误不会。[Refactor][P1] 将模型路由、重试与 Fallback 装配为生命周期策略插件 #304 已明确要求「根据结构化错误决策,不通过异常正文猜测」。
    2. 重试序列不认 run 的 deadline:interceptModel 的循环无条件调用 handler.call(request),整个类没有任何 deadline 概念;而 run 级 deadline 由 ExecutionControl / TrustedExecutionContext.deadline() 承担,只在交付边界(ReactAgent.deliver / finalizeUnderGuard)被检查——模型重试路径完全绕过了它。
    3. 运行取消后仍会再派发一次调用:循环只通过 checkInterrupted() 观察线程中断;而运行取消是经 ExecutionControl.cancel() 投递的,不打断线程。因此退避期间取消后,下一次 provider 调用照常发出。ExecutionGuard 本身已经具备 assertActive() / onLoss(Runnable) / deadline(),其 javadoc 也明确写了「阻塞等待需切片以在等待中执行 deadline」,但模型请求根本没有携带它的通道。
    4. 同一策略两套阻塞语义:非流式路径 pause(long) 用 Thread.sleep 阻塞调用线程,流式路径用 Flux.delaySubscription 响应式等待;阻塞期间两者都不检查运行控制。

    方案(单 PR,只做受控重试契约与守卫传播,不动既有默认行为)

    • 中立重试契约:在 argi-agent-api 新增 io.github.agentic.ai.agent.retry 包(ModelCallFailureCategory、ModelCallFailure、ModelCallFailureClassifier、TypeBasedModelCallFailureClassifier、RetryPolicy、RetryStopReason、RetryDecision、ModelRetryController)。该模块只依赖 argi-api 与 argi-runtime-api(无 Reactor、无 Graph/Loop),中立 ReAct 与框架 ReactAgent 两条路径共用同一套裁决。默认分类器只看失败类型与 cause 链,不解析任何异常正文;UNKNOWN 默认不可重试(fail-closed)。
    • 守卫传播:ModelRequest 增加可选(nullable)的 ExecutionGuard 字段——类型化字段,不进 getContext()/getAttributes(),因此绝不会透传给 provider;builder(ModelRequest) 拷贝时一并保留。接线两处生产构造点:ReActAgent.modelTurn 传 context.control(),AgentLlmNode 传 config.executionGuard()。不新增任何 runtime 抽象,直接复用既有 ExecutionGuard。
    • 拦截器改造:ModelRetryInterceptor 把裁决委派给 ModelRetryController。每次尝试前检查守卫与 deadline(已过期 / 剩余不足 deadlineReserve / 退避会越过 deadline → 都不再派发);有守卫时退避切片等待且每片前 assertActive(),实现「退避期间取消 → 新增调用 0 次」;ExecutionStoppedException 原样透传,不再被包装成「重试耗尽」而掩盖运行停止的真实原因。
    • 兼容性:不设新选项时行为与今天完全一致(既有 retryableExceptionPredicate 仍是默认判定,既有 Builder 选项与默认值全部保留);类型化分类器、retryableCategories、deadlineReserve、clock 均为 opt-in。退避计算上移到中立策略,框架内重复实现的退避辅助类随之删除(其增长/封顶/零延迟覆盖由新控制器测试接管)。

    测试(离线,模型无关)

    1. 尝试上界:maxAttempts=3 连续失败 → 恰好 3 次调用,第 3 次后以「maximum number of retries reached」终止。
    2. 不猜正文:显式分类器下,正文为 I/O error: connection timeout 的永久错误只调用 1 次;未配置分类器时既有谓词仍按原语义判定(回归)。
    3. 取消:退避期间取消运行 → 新增调用 0 次,抛出运行守卫的停止信号(ExecutionStoppedException),而非「重试耗尽」。
    4. deadline 门控:剩余时间不足以再装下一次尝试、或退避时长会越过 deadline → 不再启动下一次调用。
    5. usage 不重计:失败尝试不产生用量,返回给引擎的 usage 只来自成功的那一次。
    6. 守卫传播:独立 ReAct 与框架节点的模型请求都携带运行守卫;复制请求不丢守卫(否则上层语义被静默降级)。
    7. 既有不变量回归:既有 ModelRetryInterceptorTest、ModelRetryCancellationTest、AgentLlmNodeErrorHandlingTest 的断言不改、不动,全绿。
  2. added a commit that references this issue on Nov 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions