From b5cdb8f6507e1a1eb27f4b7b0abb777ca753ccf6 Mon Sep 17 00:00:00 2001 From: Soulter <37870767+Soulter@users.noreply.github.com> Date: Sun, 21 Dec 2025 12:57:54 +0800 Subject: [PATCH] fix: improve error handling in tool execution to prevent infinite tool call loops (#4143) * fix: improve error handling in tool execution to prevent infinite tool call loops - Enhanced error handling in `call_local_llm_tool` to provide more informative exceptions for ValueError and TypeError, including detailed parameter information. - Updated `ToolLoopAgentRunner` to yield appropriate messages for cases with no response or unsupported types, ensuring clearer communication to users. - Improved logging and messaging consistency across tool execution processes. * refactor: clean up unused router parameter in message retrieval functions - Removed the unused `router` parameter from `getSessionMessages` and related function calls in `Chat.vue` and `useMessages.ts`. - Commented out the `tool_calls` dictionary in `chat.py` for clarity, indicating it is not currently in use. * fix: enhance exception handling in tool execution for clearer error reporting - Improved exception handling in `call_local_llm_tool` by chaining exceptions for ValueError and TypeError, providing more context in error messages. - Ensured that traceback information is preserved in raised exceptions for better debugging. --- .../agent/runners/tool_loop_agent_runner.py | 52 ++++++++++++------- astrbot/core/astr_agent_tool_exec.py | 38 ++++++++++++-- astrbot/dashboard/routes/chat.py | 2 +- dashboard/src/components/chat/Chat.vue | 2 +- dashboard/src/composables/useMessages.ts | 8 ++- 5 files changed, 75 insertions(+), 27 deletions(-) diff --git a/astrbot/core/agent/runners/tool_loop_agent_runner.py b/astrbot/core/agent/runners/tool_loop_agent_runner.py index 5079d6484..7eb90f3fc 100644 --- a/astrbot/core/agent/runners/tool_loop_agent_runner.py +++ b/astrbot/core/agent/runners/tool_loop_agent_runner.py @@ -172,7 +172,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): self.run_context.messages.append( Message( role="assistant", - content=llm_resp.completion_text or "", + content=llm_resp.completion_text or "*No response*", ), ) try: @@ -402,35 +402,33 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): ), ) - # yield the last tool call result - if tool_call_result_blocks: - last_tcr_content = str(tool_call_result_blocks[-1].content) - yield MessageChain( - type="tool_call_result", - chain=[ - Json( - data={ - "id": func_tool_id, - "ts": time.time(), - "result": last_tcr_content, - } - ) - ], - ) - elif resp is None: # Tool 直接请求发送消息给用户 # 这里我们将直接结束 Agent Loop。 # 发送消息逻辑在 ToolExecutor 中处理了。 logger.warning( - f"{func_tool_name} 没有没有返回值或者将结果直接发送给用户,此工具调用不会被记录到历史中。" + f"{func_tool_name} 没有没有返回值或者将结果直接发送给用户。" ) self._transition_state(AgentState.DONE) self.stats.end_time = time.time() + tool_call_result_blocks.append( + ToolCallMessageSegment( + role="tool", + tool_call_id=func_tool_id, + content="*工具没有返回值或者将结果直接发送给了用户*", + ), + ) else: # 不应该出现其他类型 logger.warning( - f"Tool 返回了不支持的类型: {type(resp)},将忽略。", + f"Tool 返回了不支持的类型: {type(resp)}。", + ) + tool_call_result_blocks.append( + ToolCallMessageSegment( + role="tool", + tool_call_id=func_tool_id, + content="*工具返回了不支持的类型,请告诉用户检查这个工具的定义和实现。*", + ), ) try: @@ -452,6 +450,22 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): ), ) + # yield the last tool call result + if tool_call_result_blocks: + last_tcr_content = str(tool_call_result_blocks[-1].content) + yield MessageChain( + type="tool_call_result", + chain=[ + Json( + data={ + "id": func_tool_id, + "ts": time.time(), + "result": last_tcr_content, + } + ) + ], + ) + # 处理函数调用响应 if tool_call_result_blocks: yield tool_call_result_blocks diff --git a/astrbot/core/astr_agent_tool_exec.py b/astrbot/core/astr_agent_tool_exec.py index ed08e90a9..5d40f48fa 100644 --- a/astrbot/core/astr_agent_tool_exec.py +++ b/astrbot/core/astr_agent_tool_exec.py @@ -209,12 +209,42 @@ async def call_local_llm_tool( else: raise ValueError(f"未知的方法名: {method_name}") except ValueError as e: - logger.error(f"调用本地 LLM 工具时出错: {e}", exc_info=True) - except TypeError: - logger.error("处理函数参数不匹配,请检查 handler 的定义。", exc_info=True) + raise Exception(f"Tool execution ValueError: {e}") from e + except TypeError as e: + # 获取函数的签名(包括类型),除了第一个 event/context 参数。 + try: + sig = inspect.signature(handler) + params = list(sig.parameters.values()) + # 跳过第一个参数(event 或 context) + if params: + params = params[1:] + + param_strs = [] + for param in params: + param_str = param.name + if param.annotation != inspect.Parameter.empty: + # 获取类型注解的字符串表示 + if isinstance(param.annotation, type): + type_str = param.annotation.__name__ + else: + type_str = str(param.annotation) + param_str += f": {type_str}" + if param.default != inspect.Parameter.empty: + param_str += f" = {param.default!r}" + param_strs.append(param_str) + + handler_param_str = ( + ", ".join(param_strs) if param_strs else "(no additional parameters)" + ) + except Exception: + handler_param_str = "(unable to inspect signature)" + + raise Exception( + f"Tool handler parameter mismatch, please check the handler definition. Handler parameters: {handler_param_str}" + ) from e except Exception as e: trace_ = traceback.format_exc() - logger.error(f"调用本地 LLM 工具时出错: {e}\n{trace_}") + raise Exception(f"Tool execution error: {e}. Traceback: {trace_}") from e if not ready_to_call: return diff --git a/astrbot/dashboard/routes/chat.py b/astrbot/dashboard/routes/chat.py index c2b991ef7..71c3fecd3 100644 --- a/astrbot/dashboard/routes/chat.py +++ b/astrbot/dashboard/routes/chat.py @@ -436,7 +436,7 @@ class ChatRoute(Route): accumulated_parts = [] accumulated_text = "" accumulated_reasoning = "" - tool_calls = {} + # tool_calls = {} agent_stats = {} except BaseException as e: logger.exception(f"WebChat stream unexpected error: {e}", exc_info=True) diff --git a/dashboard/src/components/chat/Chat.vue b/dashboard/src/components/chat/Chat.vue index dcc52d4a5..df90eb0e7 100644 --- a/dashboard/src/components/chat/Chat.vue +++ b/dashboard/src/components/chat/Chat.vue @@ -310,7 +310,7 @@ async function handleSelectConversation(sessionIds: string[]) { isLoadingMessages.value = true; try { - await getSessionMsg(sessionIds[0], router); + await getSessionMsg(sessionIds[0]); } finally { isLoadingMessages.value = false; } diff --git a/dashboard/src/composables/useMessages.ts b/dashboard/src/composables/useMessages.ts index 44b0e59a2..47910a984 100644 --- a/dashboard/src/composables/useMessages.ts +++ b/dashboard/src/composables/useMessages.ts @@ -172,7 +172,7 @@ export function useMessages( } } - async function getSessionMessages(sessionId: string, router: any) { + async function getSessionMessages(sessionId: string) { if (!sessionId) return; try { @@ -188,7 +188,7 @@ export function useMessages( // 如果会话还在运行,3秒后重新获取消息 setTimeout(() => { - getSessionMessages(currSessionId.value, router); + getSessionMessages(currSessionId.value); }, 3000); } @@ -353,6 +353,10 @@ export function useMessages( const { done, value } = await reader.read(); if (done) { console.log('SSE stream completed'); + // 流式传输结束后,获取最终消息并重新渲染 + if (currSessionId.value) { + await getSessionMessages(currSessionId.value); + } break; }