107 lines
4.5 KiB
Python
107 lines
4.5 KiB
Python
"""圆桌会商「模型自动容错(换下一个模型)」共享助手。
|
||
|
||
会商的第二步(leader / seat)与第三步(summary / report / dashboard / structure)单轮执行时,
|
||
若所用模型出现问题(抛异常 / idle 卡死 / 中间件兜底错误文案 / 交付为空),应自动切到
|
||
**下一个可用模型**继续,并把「『X』模型异常 → 已切换『Y』模型」告知用户。
|
||
|
||
本模块只放**与执行路径无关**的纯逻辑,供前台(``multi_agent.py``)与后台
|
||
(``roundtable_inprocess_gateway.py``)两条路径共用:
|
||
|
||
- ``model_fallback_enabled()``:总开关(env ``ROUNDTABLE_MODEL_FALLBACK``,默认开);
|
||
- ``fallback_model_chain(current)``:按 ``config.yaml`` ``models[]`` 顺序给出候选模型链
|
||
``[当前模型, …其余]``(去重保序);
|
||
- ``reason_text(reason)``:把失败原因码翻译成简短中文(用于用户提示);
|
||
- 复用 harness 的 ``is_llm_error_message`` 识别中间件兜底文案。
|
||
|
||
放在 app 层(依赖 ``deerflow.config`` + harness 的中间件常量,皆为 app→harness 合法方向)。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import os
|
||
|
||
from deerflow.agents.middlewares.llm_error_handling_middleware import (
|
||
is_llm_error_message as is_llm_error_message, # re-export for callers
|
||
)
|
||
from deerflow.config import get_app_config
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# 失败原因码 → 用户可见简短中文(用于「『X』模型<原因>,已切换…」提示)。
|
||
_REASON_TEXT: dict[str, str] = {
|
||
"quota": "额度不足/账单异常",
|
||
"auth": "鉴权失败/无访问权限",
|
||
"busy": "服务繁忙",
|
||
"transient": "网络/连接异常",
|
||
"upstream": "上游响应异常",
|
||
"circuit": "连续失败被熔断",
|
||
"generic": "调用失败",
|
||
"exception": "调用异常",
|
||
"timeout": "长时间无响应(卡死)",
|
||
"empty": "未产出有效内容",
|
||
"thinking_only": "仅产出思考、无有效正文",
|
||
"truncated": "输出中断、未完成交付",
|
||
}
|
||
|
||
|
||
def reason_text(reason: str | None) -> str:
|
||
"""把失败原因码翻译成简短中文(未知码 → 「调用失败」)。"""
|
||
if not reason:
|
||
return "调用失败"
|
||
return _REASON_TEXT.get(reason, "调用失败")
|
||
|
||
|
||
def model_fallback_enabled() -> bool:
|
||
"""模型自动容错总开关(默认开;设 ``ROUNDTABLE_MODEL_FALLBACK=0/false/off`` 关闭)。
|
||
|
||
关闭时 ``fallback_model_chain`` 只返回单个候选(原模型),即恢复旧行为:模型失败就失败,
|
||
不再尝试其它模型。
|
||
"""
|
||
raw = (os.getenv("ROUNDTABLE_MODEL_FALLBACK", "1") or "1").strip().lower()
|
||
return raw not in {"0", "false", "no", "off"}
|
||
|
||
|
||
def _max_attempts() -> int:
|
||
"""最多尝试的模型个数(含首个)。``ROUNDTABLE_MODEL_FALLBACK_MAX``,0/未设 = 不限(试完全部)。"""
|
||
try:
|
||
return max(0, int(os.getenv("ROUNDTABLE_MODEL_FALLBACK_MAX", "0") or "0"))
|
||
except (TypeError, ValueError):
|
||
return 0
|
||
|
||
|
||
def _config_model_names() -> list[str]:
|
||
try:
|
||
cfg = get_app_config()
|
||
return [m.name for m in (cfg.models or []) if getattr(m, "name", None)]
|
||
except Exception: # noqa: BLE001 — 拿不到配置(内存后端/测试桩)时退化为空
|
||
logger.debug("fallback_model_chain: get_app_config().models unavailable", exc_info=True)
|
||
return []
|
||
|
||
|
||
def fallback_model_chain(current: str | None) -> list[str]:
|
||
"""按 ``config.yaml`` ``models[]`` 顺序构造候选模型链:``[当前模型, …其余]``,去重保序。
|
||
|
||
- ``current`` 为空/None → 从 ``models[0]`` 起(即后端默认模型),后接其余;
|
||
- ``current`` 在配置内 → 它排第一,其余按配置顺序补齐;
|
||
- ``current`` 不在配置内(自定义/未知名)→ 仍排第一,再补齐全部配置模型;
|
||
- 总开关关闭 → 只返回单个候选(``current`` 或默认模型),等同旧行为;
|
||
- 拿不到任何配置模型 → 返回 ``[current]``(current 为空则空列表,调用方自行退化为不换模型)。
|
||
"""
|
||
all_names = _config_model_names()
|
||
cur = (current or "").strip()
|
||
ordered: list[str] = []
|
||
if cur:
|
||
ordered.append(cur)
|
||
for n in all_names:
|
||
if n and n not in ordered:
|
||
ordered.append(n)
|
||
if not ordered:
|
||
return [cur] if cur else []
|
||
if not model_fallback_enabled():
|
||
return ordered[:1]
|
||
cap = _max_attempts()
|
||
if cap > 0:
|
||
ordered = ordered[:cap]
|
||
return ordered
|