Skip to content

feat(agent): harden agent loop against unknown tools and tool failures#4525

Open
benym wants to merge 10 commits intoalibaba:mainfrom
benym:feat_unknowTool
Open

feat(agent): harden agent loop against unknown tools and tool failures#4525
benym wants to merge 10 commits intoalibaba:mainfrom
benym:feat_unknowTool

Conversation

@benym
Copy link
Copy Markdown
Contributor

@benym benym commented Apr 8, 2026

Describe what this PR does / why we need it

Current Situation:

  1. When describing an unknown tool in a Skill (e.g., describing the use of arxiv-search in a websearch SKILL.md), the model has a certain probability (not 100%) of returning a Tool Call that does not exist in the system. More commonly, the model itself may be misled, leading to the invocation of a non-existent Tool.
image
  1. For Tool Calls that do not exist in the system, the current default is to throw an exception, com.alibaba.cloud.ai.graph.agent.node.AgentToolNode#executeToolCallWithInterceptors. This causes ReActAgent to be forced to terminate in long-running tasks due to a single erroneous tool call.
image
  1. There is no universal fallback mechanism for Tool call failures, unlike ToolRetryInterceptor which directly attempts to retry a single tool call.

Based on the above issues, this PR adds two new tool error handling mechanisms: Unknown Tool Guard and Tool Execution Failure Guard, to address two typical error scenarios that ReActAgent may encounter during the tool invocation phase:

  1. Unknown Tool: The model experiences a tool illusion, leading to a request for a non-existent tool, causing the fallback agent to throw an exception and be forced to terminate.

  2. Tool Execution Failure: An exception or timeout occurs during tool execution, and the fallback agent's continuous retries fail to recover.

Both Guards employ a unified two-phase upgrade strategy: First, the model is allowed to self-repair and retry, providing a default list of failed tools and currently available tools, with one retrieval attempt. If consecutive failures occur, the system switches to final-answer mode (disabling the tool and forcing the model to directly answer the user), thus gracefully terminating the error loop rather than simply throwing an exception.

Major Changes:

  • Added AbstractToolCallGuardHook abstract base class: A unified guard hook template that implements core logic such as two-phase upgrade (self-repair → final-answer), failure counting, and synthetic instruction injection.

  • Added UnknownToolGuardHook: Detects unknown tool requests and provides a list of registered tools to help the model self-correct.

  • Added ToolExecutionFailureGuardHook: Detects tool execution failures (runtime exception / timeout) and collaborates with ToolRetryInterceptor.

  • Added AbstractFinalAnswerInterceptor and two implementations (UnknownToolFinalAnswerInterceptor, ToolExecutionFailureFinalAnswerInterceptor): Separates the model's tool access capabilities in the final-answer round.

  • Added ToolCallGuardConstants: A unified cross-module metadata key constant.

  • Enhanced AgentToolNode: Enriches error metadata generation (error type, failure type, tool names, allFailed). - Enhanced ToolRetryInterceptor: Supports exponential backoff retries, jitter, and retry exhaustion metadata flags, cascading with guard hooks.

  • Enhanced ReactAgent and Builder: Automatically register default guard hooks and provide fine-grained disabling options (disableDefaultUnknownToolGuard(), disableDefaultToolExecutionFailureGuard(), disableDefaultGuards()).

  • Added complete unit test and integration test coverage.

中文版

现状:

  1. 在Skill中描述一个未知的Tool(如在一个websearch的SKILL.md中描述采用arxiv-search工具进行搜索),模型有一定概率(非100%)返回调用系统中不存在的Tool Call,更通用的情况是模型自身有幻觉导致调用一个不存在的Tool
  2. 对于系统中不存在的Tool Call调用,目前默认的是抛出异常,com.alibaba.cloud.ai.graph.agent.node.AgentToolNode#executeToolCallWithInterceptors,这会导致ReActAgent在长程任务中仅因为一次错误的toolcall调用就被迫中断,如果需要额外处理,这将强制开发者在使用的时候在外层捕获这个异常,这样代码显得写起来很难受,使得ReActAgent并不能做到长程任务开箱即用
  3. Tool调用失败没有一种通用的兜底机制,区别于ToolRetryInterceptor直接尝试重试单个工具调用

基于上述问题本 PR 新增了两种 Tool 错误处理机制:Unknown Tool GuardTool Execution Failure Guard,用于解决 ReActAgent 在工具调用阶段可能出现的两类典型错误场景:

  1. Unknown Tool(未知工具):模型幻觉导致请求了不存在的工具,兜底Agent 抛出异常被迫中断。
  2. Tool Execution Failure(工具执行失败):工具执行过程中抛出异常或超时,兜底Agent 持续重试无法恢复。

两种 Guard 采用统一的 两阶段升级策略:先允许模型自行修复(self-repair retry),默认给出调用失败的工具和当前可用工具,重试次数为1次,若连续失败则切换为 final-answer 模式(禁用工具,强制模型直接回答用户),从而优雅地终止错误循环,而非简单地抛出异常。

主要变更:

  • 新增 AbstractToolCallGuardHook 抽象基类:统一的 guard hook 模板,实现两阶段升级(self-repair → final-answer)、失败计数、合成指令注入等核心逻辑
  • 新增 UnknownToolGuardHook:检测未知工具请求,提供已注册工具列表帮助模型自纠正
  • 新增 ToolExecutionFailureGuardHook:检测工具执行失败(runtime exception / timeout),与 ToolRetryInterceptor 协作
  • 新增 AbstractFinalAnswerInterceptor 及两个实现(UnknownToolFinalAnswerInterceptorToolExecutionFailureFinalAnswerInterceptor):在 final-answer 轮次中剥离模型的工具访问能力
  • 新增 ToolCallGuardConstants:统一的跨模块元数据 key 常量
  • 增强 AgentToolNode:丰富错误元数据生成(error type、failure type、tool names、allFailed flag),支持 guard hook 的故障检测
  • 增强 ToolRetryInterceptor:支持指数退避重试、jitter、重试耗尽元数据标记,与 guard hook 级联配合
  • 增强 ReactAgentBuilder:自动注册默认 guard hook,提供细粒度的禁用选项(disableDefaultUnknownToolGuard()disableDefaultToolExecutionFailureGuard()disableDefaultGuards()
  • 新增完整的单元测试和集成测试覆盖

Does this pull request fix one issue?

#4337

#4337 (comment)

Describe how you did it

Overall Architecture Design:

AgentToolNode(执行层)
  → ToolRetryInterceptor(自动重试层,指数退避)
    → Guard Hook beforeModel(检测层,失败计数与升级决策)
      → FinalAnswerInterceptor(执行层,禁用工具)
        → Guard Hook afterModel(校验层,兜底回答)

Core Implementation Details:

  1. AbstractToolCallGuardHook (Two-Phase Upgrade Template):

    • beforeModel: Reads the guard-specific failure flag from the RunnableConfig metadata, increments the consecutive failure counter, and injects a synthesized AgentInstructionMessage when the count exceeds maxSelfRepairRetries, instructing the model to answer directly.

    • afterModel: Verifies whether the model follows the final-answer instruction. If a tool call is still issued, it is replaced with a fallback answer message and jumps to the end node.

    • State is isolated through the RunnableConfig context key, ensuring thread safety.

  2. UnknownToolGuardHook:

    • Reads the allToolCallsUnknown flag set by AgentToolNode.

    • Includes the requested tool name and a list of available tools in the final-answer instruction to help the model self-correct.

    • Priority HIGHEST_PRECEDENCE + 100 (higher than the execution failure guard).

    • Default registration (even without tool configuration, as the model can illusionize tools)

  3. ToolExecutionFailureGuardHook:

    • Reads the allToolCallsFailed flag and failure type (runtime_exception / timeout)

    • Collaborates with ToolRetryInterceptor: triggers the guard only after retries are exhausted

    • Priority HIGHEST_PRECEDENCE + 110

    • Registers only when the Agent has tools configured

  4. AgentToolNode Enhancements:

    • Generates structured error metadata (errorType, failureType, requestedToolNames, availableToolNames) for each failed tool call

    • Adds a unified allToolCallsErrored flag, supporting mixed failure scenarios

    • Uses AtomicReferenceArray + CAS in parallel execution mode to prevent race conditions between timeout and completion

  5. ToolRetryInterceptor Enhancements:

    • Supports exponential backoff (initialDelay × backoffFactorretryNumber), with an upper limit of maxDelay

    • Supports jitter (±25% random offset)

    • Marks retryAttempts and retryExhausted in the response metadata after retries are exhausted, for downstream guard hooks to read

  6. Builder Enhancements:

    • disableDefaultUnknownToolGuard(): Disables unknown tool guard

    • disableDefaultToolExecutionFailureGuard(): Disables execution failure guard

    • disableDefaultGuards(): Disables both

    • Guard Hook supports custom maxSelfRepairRetries, customFinalAnswerInstruction, and customFallbackAnswerMessage

中文版

整体架构设计:

AgentToolNode(执行层)
  → ToolRetryInterceptor(自动重试层,指数退避)
    → Guard Hook beforeModel(检测层,失败计数与升级决策)
      → FinalAnswerInterceptor(执行层,禁用工具)
        → Guard Hook afterModel(校验层,兜底回答)

核心实现细节:

  1. AbstractToolCallGuardHook(两阶段升级模板):

    • beforeModel:从 RunnableConfig 元数据中读取 guard 专属的失败标记,递增连续失败计数器,当计数超过 maxSelfRepairRetries 时注入合成 AgentInstructionMessage 指示模型直接回答
    • afterModel:校验模型是否遵循 final-answer 指令,若仍发出 tool call,则替换为兜底回答消息并跳转到 end 节点
    • 状态通过 RunnableConfig context key 隔离,保证线程安全
  2. UnknownToolGuardHook:

    • 读取 AgentToolNode 设置的 allToolCallsUnknown 标记
    • 在 final-answer 指令中包含请求的工具名和可用工具列表,帮助模型自纠正
    • 优先级 HIGHEST_PRECEDENCE + 100(高于执行失败 guard)
    • 默认注册(即使无工具配置,因模型可以幻觉工具)
  3. ToolExecutionFailureGuardHook:

    • 读取 allToolCallsFailed 标记和失败类型(runtime_exception / timeout)
    • ToolRetryInterceptor 协作:重试耗尽后才触发 guard
    • 优先级 HIGHEST_PRECEDENCE + 110
    • 仅在 Agent 配置了工具时注册
  4. AgentToolNode 增强:

    • 为每个失败的 tool call 生成结构化错误元数据(errorType、failureType、requestedToolNames、availableToolNames)
    • 新增 allToolCallsErrored 统一标记,支持混合失败场景
    • 并行执行模式下使用 AtomicReferenceArray + CAS 防止超时与完成之间的竞争条件
  5. ToolRetryInterceptor 增强:

    • 支持指数退避(initialDelay × backoffFactor^retryNumber),上限 maxDelay
    • 支持 jitter(±25% 随机偏移)
    • 重试耗尽后在响应元数据中标记 retryAttemptsretryExhausted,供下游 guard hook 读取
  6. Builder 增强:

    • disableDefaultUnknownToolGuard():禁用未知工具 guard
    • disableDefaultToolExecutionFailureGuard():禁用执行失败 guard
    • disableDefaultGuards():同时禁用两者
    • Guard Hook 支持自定义 maxSelfRepairRetriescustomFinalAnswerInstructioncustomFallbackAnswerMessage

Describe how to verify it

  1. Core Test Classes:

    • ReactAgentUnknownToolRecoveryTest: Verifies the Agent's self-healing and final-answer degradation in the Unknown Tool scenario.

    • ReactAgentToolExecutionFailureRecoveryTest: Verifies the recovery process in the Tool Execution Failure scenario.

    • UnknownToolGuardHookTest: Guard hook unit test (failure counting, instruction injection, fallback answer).

    • ToolExecutionFailureGuardHookTest: Execution failure guard unit test.

    • UnknownToolFinalAnswerInterceptorTest: Final-answer interceptor test (tool stripping verification).

    • ToolExecutionFailureFinalAnswerInterceptorTest: Execution failure interceptor test.

    • ToolRetryInterceptorStructuredFailureTest: Retry interceptor structured failure test (metadata tagging verification).

  2. Verification Key Points:

    • Unknown Tool Scenario: Model requests a non-existent tool → Self-healing retry → After exceeding the threshold... Final-answer downgrade → Returns a meaningful answer to the user

    • Execution Failure scenario: Tool execution throws an exception → ToolRetryInterceptor automatically retryes → Retry exhaustion → Guard hook upgrades to final-answer → Returns a fallback answer

    • Two guards can coexist and work independently

    • Builder's disableDefaultGuards() can correctly disable the default guard

中文版

  1. 核心测试类:

    • ReactAgentUnknownToolRecoveryTest:验证 Unknown Tool 场景下 Agent 的自修复和 final-answer 降级
    • ReactAgentToolExecutionFailureRecoveryTest:验证 Tool Execution Failure 场景下的恢复流程
    • UnknownToolGuardHookTest:guard hook 单元测试(失败计数、指令注入、兜底回答)
    • ToolExecutionFailureGuardHookTest:执行失败 guard 单元测试
    • UnknownToolFinalAnswerInterceptorTest:final-answer 拦截器测试(工具剥离验证)
    • ToolExecutionFailureFinalAnswerInterceptorTest:执行失败拦截器测试
    • ToolRetryInterceptorStructuredFailureTest:重试拦截器结构化失败测试(元数据标记验证)
  2. 验证要点:

    • Unknown Tool 场景:模型请求不存在的工具 → 自修复重试 → 超过阈值后 final-answer 降级 → 返回用户有意义的回答
    • Execution Failure 场景:工具执行抛异常 → ToolRetryInterceptor 自动重试 → 重试耗尽 → guard hook 升级为 final-answer → 返回兜底回答
    • 两种 guard 可以共存、独立工作
    • Builder 的 disableDefaultGuards() 可正确禁用默认 guard

Special notes for reviews

  1. Backward Compatibility: Both guard hooks are automatically registered to ReactAgent as default behavior, providing protection to existing code without modification. They can be explicitly disabled via the Builder method.

  2. Design Decisions:

    • UnknownToolGuardHook is always registered (even if the Agent doesn't have a tool configured), as the model might mistakenly believe a tool is being called.

    • ToolExecutionFailureGuardHook is only registered if the Agent has a tool configured.

    • Guard Hook priority: Unknown Tool (+100) > Execution Failure (+110), ensuring unknown tools are handled first.

  3. Immature Message Pattern: All synthetic instructions are injected using AgentInstructionMessage, without modifying the original message list; AbstractFinalAnswerInterceptor uses ModelRequest.builder() to create a new copy of the request instead of modifying the original object.

  4. Extensibility of AbstractToolCallGuardHook: As an abstract template class, it can be inherited to support more types of tool call error handling (such as insufficient permissions, rate limiting, etc.), by simply implementing template methods such as buildFinalAnswerInstruction() and buildFallbackAnswerMessage().

中文版

  1. 向后兼容性:两种 guard hook 作为默认行为自动注册到 ReactAgent,已有代码无需修改即可获得保护。如需禁用,可通过 Builder 方法显式关闭。

  2. 设计决策

    • UnknownToolGuardHook 始终注册(即使 Agent 未配置工具),因为模型可能幻觉出工具调用
    • ToolExecutionFailureGuardHook 仅在 Agent 配置了工具时注册
    • Guard Hook 优先级:Unknown Tool(+100)> Execution Failure(+110),确保未知工具优先处理
  3. 不可变消息模式:所有合成指令使用 AgentInstructionMessage 注入,不修改原始消息列表;AbstractFinalAnswerInterceptor 使用 ModelRequest.builder() 创建新请求副本而非修改原对象。

  4. AbstractToolCallGuardHook 的可扩展性:作为抽象模板类,后续可通过继承支持更多类型的 tool call 错误处理(如权限不足、速率限制等),只需实现 buildFinalAnswerInstruction()buildFallbackAnswerMessage() 等模板方法。

@benym
Copy link
Copy Markdown
Contributor Author

benym commented Apr 8, 2026

PTAL, thanks @yuluo-yx @chickenlj @robocanic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant