Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 41 additions & 16 deletions astrbot/core/agent/runners/tool_loop_agent_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -1287,14 +1287,9 @@ async def _resolve_tool_exec(
)
if param_subset.tools and tool_names:
contexts = self._build_tool_requery_context(tool_names)
requery_resp = await self.provider.text_chat(
contexts=self._sanitize_contexts_for_provider(contexts),
func_tool=param_subset,
model=self.req.model,
session_id=self.req.session_id,
extra_user_content_parts=self.req.extra_user_content_parts,
tool_choice="required",
abort_signal=self._abort_signal,
requery_resp = await self._requery(
contexts=contexts,
param_subset=param_subset,
)
if requery_resp:
llm_resp = requery_resp
Expand All @@ -1313,20 +1308,50 @@ async def _resolve_tool_exec(
tool_names,
extra_instruction=self.SKILLS_LIKE_REQUERY_REPAIR_INSTRUCTION,
)
repair_resp = await self.provider.text_chat(
contexts=self._sanitize_contexts_for_provider(repair_contexts),
func_tool=param_subset,
model=self.req.model,
session_id=self.req.session_id,
extra_user_content_parts=self.req.extra_user_content_parts,
tool_choice="required",
abort_signal=self._abort_signal,
repair_resp = await self._requery(
contexts=repair_contexts,
param_subset=param_subset,
)
if repair_resp:
llm_resp = repair_resp

return llm_resp, subset

async def _requery(
self,
contexts: list,
param_subset: ToolSet,
) -> LLMResponse | None:
"""Send a re-query with tool_choice='required', falling back to 'auto' if
the provider does not support the 'required' value (e.g. deepseek-reasoner).
"""
try:
return await self.provider.text_chat(
Comment on lines +1320 to +1329
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Avoid catching and handling asyncio.CancelledError inside _requery

Catching Exception here will also swallow asyncio.CancelledError, breaking cooperative cancellation (e.g., aborted requests or upstream task cancels). Please re-raise CancelledError before handling other exceptions, e.g.:

        except Exception as e:
            if isinstance(e, asyncio.CancelledError):
                raise
            if "tool_choice" in str(e).lower():
                ...

This keeps cancellation semantics intact while still handling the tool_choice fallback.

contexts=self._sanitize_contexts_for_provider(contexts),
func_tool=param_subset,
model=self.req.model,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

在 re-query 过程中直接传递 self.req.model 可能会在启用备用提供商(fallback provider)时导致请求失败。备用提供商通常有自己特定的模型配置,可能无法识别原始请求中的模型名称。建议在当前提供商不是主提供商时,考虑允许其使用自身默认的模型配置(例如传递 None)。

session_id=self.req.session_id,
extra_user_content_parts=self.req.extra_user_content_parts,
tool_choice="required",
abort_signal=self._abort_signal,
)
except Exception as e:
if "tool_choice" in str(e).lower():
logger.info(
f"tool_choice='required' 不被当前模型支持,降级为 'auto' 重试。",
)
logger.debug(f"原始错误: {e}")
return await self.provider.text_chat(
contexts=self._sanitize_contexts_for_provider(contexts),
func_tool=param_subset,
model=self.req.model,
session_id=self.req.session_id,
extra_user_content_parts=self.req.extra_user_content_parts,
tool_choice="auto",
abort_signal=self._abort_signal,
)
raise
Comment on lines +1320 to +1353
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

新的 re-query 逻辑及其降级处理应该附带单元测试,以确保它能正确识别 tool_choice 错误并按预期进行重试。考虑到不同 LLM 提供商的错误消息多种多样,通过测试验证这一逻辑的健壮性尤为重要。

References
  1. 新功能(如处理 tool_choice 降级逻辑)应附带相应的单元测试。

Comment on lines +1338 to +1353
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

如果模型不支持 tool_choice='required',每次调用 _requery 都会先产生一次必然失败的 API 请求。由于 _resolve_tool_exec 可能会在一次执行中调用 _requery 两次,这会增加延迟。建议在 runner 实例中缓存该模型是否支持 required 的状态。在单线程 asyncio 事件循环中,同步修改此类共享状态是原子的,不会产生竞态条件。

References
  1. 在单线程 asyncio 事件循环中,同步函数(不含 'await' 的代码块)是原子执行的,在修改共享状态时是安全的。


def done(self) -> bool:
"""检查 Agent 是否已完成工作"""
return self._state in (AgentState.DONE, AgentState.ERROR)
Expand Down
8 changes: 5 additions & 3 deletions astrbot/core/provider/sources/openai_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -1122,15 +1122,17 @@ async def _handle_api_error(
image_fallback_used=True,
)

err_lower = str(e).lower()
if (
"Function calling is not enabled" in str(e)
or ("tool" in str(e).lower() and "support" in str(e).lower())
or ("function" in str(e).lower() and "support" in str(e).lower())
or ("tool" in err_lower and "support" in err_lower and "tool_choice" not in err_lower)
or ("function" in err_lower and "support" in err_lower)
):
# openai, ollama, gemini openai, siliconcloud 的错误提示与 code 不统一,只能通过字符串匹配
logger.info(
f"{self.get_model()} 不支持函数工具调用,已自动去除,不影响使用。",
)
logger.debug(f"原始错误: {e}")
payloads.pop("tools", None)
return (
False,
Expand All @@ -1143,7 +1145,7 @@ async def _handle_api_error(
)
# logger.error(f"发生了错误。Provider 配置如下: {self.provider_config}")

if "tool" in str(e).lower() and "support" in str(e).lower():
if "tool" in err_lower and "support" in err_lower and "tool_choice" not in err_lower:
logger.error("疑似该模型不支持函数调用工具调用。请输入 /tool off_all")

if is_connection_error(e):
Expand Down