PMFlow/backend/app/services/llm_service/mixin_tier_escalation.py

505 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
from .shared import *
class LLMTierEscalationMixin:
def _low_to_high_escalation_enabled(self) -> bool:
return _env_bool("LLM_LOW_TO_HIGH_ESCALATION_ENABLED", True)
@staticmethod
def _same_runtime_engine(a: dict[str, Any], b: dict[str, Any]) -> bool:
return (
str(a.get("provider") or "").strip().lower() == str(b.get("provider") or "").strip().lower()
and str(a.get("model") or "").strip().lower() == str(b.get("model") or "").strip().lower()
and str(a.get("base_url") or "").strip().rstrip("/").lower()
== str(b.get("base_url") or "").strip().rstrip("/").lower()
)
def _is_low_tier_function(self, function_name: str) -> bool:
rule = self._match_openclaw_rule(function_name)
if rule is not None:
tier = str(rule.get("tier") or "").strip().lower()
if tier:
return tier == "low"
return self._resolve_function_tier(function_name) == "low"
@staticmethod
def _looks_like_unanswerable_response(text: str) -> bool:
cleaned = str(text or "").strip()
lowered = cleaned.lower()
if not lowered:
return True
if len(cleaned) <= 8:
return True
return any(marker in lowered for marker in LOW_TO_HIGH_ESCALATION_FALLBACK_MARKERS)
def _should_escalate_low_to_high(self, *, function_name: str, response_text: str) -> bool:
if not self._low_to_high_escalation_enabled():
return False
if not self._is_low_tier_function(function_name):
return False
return self._looks_like_unanswerable_response(response_text)
@staticmethod
def _is_glm_47_model(model_name: str) -> bool:
normalized = re.sub(r"[\s_]+", "", str(model_name or "")).lower()
if not normalized:
return False
if normalized.startswith("qwen"):
return True
return "glm" in normalized and ("4.7" in normalized or "47" in normalized)
@staticmethod
def _is_glm_5_model(model_name: str) -> bool:
normalized = re.sub(r"[\s_]+", "", str(model_name or "")).lower()
if not normalized:
return False
return normalized in {"glm-5", "glm5"} or normalized.startswith("glm-5") or normalized.startswith("glm5")
def _should_retry_glm47_with_high_tier(
self,
*,
function_name: str,
failed_engine: dict[str, Any],
) -> bool:
if not self._low_to_high_escalation_enabled():
return False
failed_model = str(failed_engine.get("model") or "").strip()
if self._is_glm_47_model(failed_model):
return True
# 兜底兼容:低阶函数遇到 4.x GLM 也允许升级高阶重试。
if self._is_low_tier_function(function_name):
lowered = failed_model.lower()
return "glm" in lowered and "4." in lowered
return False
def _prefer_glm5_high_tier_retry_engine(
self,
*,
bundle: dict[str, Any],
engines: list[dict[str, Any]],
function_name: str,
failed_engine: dict[str, Any],
current_engine: dict[str, Any],
) -> Optional[dict[str, Any]]:
if self._is_glm_5_model(str(current_engine.get("model") or "")):
return current_engine
settings = self._normalize_engine_settings(bundle.get("settings"))
for item in engines:
candidate = dict(item)
if not self._is_glm_5_model(str(candidate.get("model") or "")):
continue
if self._same_runtime_engine(candidate, failed_engine):
continue
try:
candidate = self._ensure_engine_allowed_for_function(
engine=candidate,
function_name=function_name,
settings=settings,
engine_label="forced_glm5_failover",
)
except Exception as exc:
_runtime_log(
"LLM路由",
(
"GLM-5强制升级候选被拦截:"
f"function={function_name}, error={type(exc).__name__}:{str(exc)[:180]}"
),
)
continue
_runtime_log(
"LLM路由",
(
"GLM-5强制高阶升级命中:"
f"function={function_name}, provider={candidate.get('provider')}, model={candidate.get('model')}"
),
)
return candidate
return None
def _match_openclaw_high_tier_rule_for_escalation(self, function_name: str) -> Optional[dict[str, Any]]:
bundle = self._load_openclaw_routing_bundle()
rules = bundle.get("rules") if isinstance(bundle.get("rules"), list) else []
indexed: list[tuple[int, int, dict[str, Any]]] = []
for idx, row in enumerate(rules):
if not isinstance(row, dict):
continue
if not bool(row.get("enabled", True)):
continue
tier_name = str(row.get("tier") or "").strip().lower()
if tier_name != "high":
continue
if not self._rule_matches_function(row, function_name):
continue
priority = int(row.get("priority") or 0)
indexed.append((priority, -idx, row))
if not indexed:
_runtime_log("LLM路由", f"OpenClaw高阶规则未命中:function={function_name}")
return None
indexed.sort(reverse=True)
rule = dict(indexed[0][2])
_runtime_log(
"LLM路由",
(
"OpenClaw高阶规则候选:"
f"function={function_name}, rule={rule.get('name') or 'unnamed'}, "
f"priority={rule.get('priority')}, provider={rule.get('provider')}, model={rule.get('model')}"
),
)
return rule
async def _resolve_openclaw_high_tier_engine_for_escalation(
self,
*,
bundle: dict[str, Any],
engines: list[dict[str, Any]],
active_engine: dict[str, Any],
function_name: str,
failed_engine: dict[str, Any],
) -> Optional[dict[str, Any]]:
rule = self._match_openclaw_high_tier_rule_for_escalation(function_name)
if not isinstance(rule, dict):
return None
routed = self._resolve_engine_from_rule(
rule=rule,
engines=engines,
active_engine=active_engine,
)
if not isinstance(routed, dict):
_runtime_log(
"LLM路由",
(
"OpenClaw高阶规则命中但未解析到可用引擎:"
f"function={function_name}, rule={rule.get('name') or 'unnamed'}"
),
)
return None
routed = self._ensure_openclaw_control_plane_engine(routed)
if self._same_runtime_engine(routed, failed_engine):
_runtime_log(
"LLM路由",
(
"OpenClaw高阶升级命中但与失败引擎同源,跳过:"
f"function={function_name}, provider={routed.get('provider')}, model={routed.get('model')}"
),
)
return None
try:
routed = self._ensure_engine_allowed_for_function(
engine=routed,
function_name=function_name,
settings=self._normalize_engine_settings(bundle.get("settings")),
engine_label="openclaw_high_tier_failover",
)
except Exception as exc:
_runtime_log(
"LLM路由",
(
"OpenClaw高阶升级被拦截:"
f"function={function_name}, rule={rule.get('name') or 'unnamed'}, "
f"error={type(exc).__name__}:{str(exc)[:180]}"
),
)
return None
_runtime_log(
"LLM路由",
(
"OpenClaw高阶升级命中:"
f"function={function_name}, rule={rule.get('name') or 'unnamed'}, "
f"provider={routed.get('provider')}, model={routed.get('model')}"
),
)
return routed
async def _retry_with_high_tier_after_low_model_failure(
self,
*,
db: AsyncSession,
tenant_id: int,
function_name: str,
failed_engine: dict[str, Any],
prompt: str,
system_prompt: str,
temperature: float,
max_tokens: int,
response_mode: str,
error: Exception,
) -> tuple[Optional[str], Optional[dict[str, Any]]]:
if not self._should_retry_glm47_with_high_tier(function_name=function_name, failed_engine=failed_engine):
return None, None
try:
_, bundle = await self._get_or_init_bundle(db, tenant_id=tenant_id)
engines = [item for item in bundle.get("engines", []) if isinstance(item, dict)]
active_engine = self._resolve_active_engine_from_bundle(bundle)
_runtime_log(
"LLM路由",
(
"低阶模型失败准备升级高阶:"
f"function={function_name}, from={str(failed_engine.get('provider') or '').strip().lower()}:"
f"{str(failed_engine.get('model') or '').strip()}, reason={str(error)[:120]}"
),
)
high_engine = await self._resolve_openclaw_high_tier_engine_for_escalation(
bundle=bundle,
engines=engines,
active_engine=active_engine,
function_name=function_name,
failed_engine=failed_engine,
)
if not isinstance(high_engine, dict):
_runtime_log(
"LLM路由",
(
"OpenClaw高阶升级未命中,回退通用 high tier:"
f"function={function_name}, reason={str(error)[:120]}"
),
)
high_engine = self._resolve_tier_engine(
tier="high",
engines=engines,
active_engine=active_engine,
)
if not isinstance(high_engine, dict):
return None, None
high_engine = self._ensure_engine_allowed_for_function(
engine=high_engine,
function_name=function_name,
settings=self._normalize_engine_settings(bundle.get("settings")),
engine_label="high_tier_failover",
)
forced_glm5_engine = self._prefer_glm5_high_tier_retry_engine(
bundle=bundle,
engines=engines,
function_name=function_name,
failed_engine=failed_engine,
current_engine=high_engine,
)
if isinstance(forced_glm5_engine, dict):
high_engine = forced_glm5_engine
failed_provider = str(failed_engine.get("provider") or "").strip().lower()
failed_model = str(failed_engine.get("model") or "").strip()
high_provider = str(high_engine.get("provider") or "").strip().lower()
high_model = str(high_engine.get("model") or "").strip()
if failed_provider == high_provider and failed_model == high_model:
_runtime_log(
"LLM路由",
(
"高阶升级引擎与失败引擎相同,跳过:"
f"function={function_name}, provider={high_provider}, model={high_model}"
),
)
return None, None
_runtime_log(
"LLM路由",
(
"低阶模型失败自动升级:"
f"function={function_name}, from={failed_provider}:{failed_model}, "
f"to={high_provider}:{high_model}, reason={str(error)[:120]}"
),
)
text = await self._chat_completion(
provider=high_provider,
prompt=prompt,
system_prompt=system_prompt,
api_key=str(high_engine.get("api_key") or ""),
base_url=str(high_engine.get("base_url") or ""),
model=high_model,
temperature=temperature,
max_tokens=max_tokens,
function_name=function_name,
response_mode=response_mode,
)
if not text.strip():
raise ValueError("high_tier_empty_response")
if self._is_invalid_structured_output(function_name=function_name, response_text=text):
raise ValueError("high_tier_invalid_structured_output")
_runtime_log(
"LLM路由",
(
"高阶升级成功:"
f"function={function_name}, provider={high_provider}, model={high_model}, text_len={len(text)}"
),
)
return text, high_engine
except Exception as retry_exc:
_runtime_log(
"LLM路由",
(
"低阶模型失败升级高阶也失败:"
f"function={function_name}, error={type(retry_exc).__name__}:{str(retry_exc)[:180]}"
),
)
return None, None
async def _retry_structured_with_high_tier_after_low_model_failure(
self,
*,
db: AsyncSession,
tenant_id: int,
function_name: str,
failed_engine: dict[str, Any],
prompt: str,
system_prompt: str,
temperature: float,
max_tokens: int,
response_mode: str,
error: Exception,
) -> tuple[Optional[StructuredExtractionResult], Optional[dict[str, Any]]]:
fn = str(function_name or "").strip().lower()
force_intent_retry = fn in {"classify_intent_ai_pmo_kernel", "classify_intent_complex"}
if not force_intent_retry and not self._should_retry_glm47_with_high_tier(
function_name=function_name,
failed_engine=failed_engine,
):
return None, None
try:
_, bundle = await self._get_or_init_bundle(db, tenant_id=tenant_id)
engines = [item for item in bundle.get("engines", []) if isinstance(item, dict)]
active_engine = self._resolve_active_engine_from_bundle(bundle)
_runtime_log(
"LLM路由",
(
"低阶结构化失败准备升级高阶:"
f"function={function_name}, from={str(failed_engine.get('provider') or '').strip().lower()}:"
f"{str(failed_engine.get('model') or '').strip()}, reason={str(error)[:120]}, "
f"force_intent_retry={force_intent_retry}"
),
)
high_engine = await self._resolve_openclaw_high_tier_engine_for_escalation(
bundle=bundle,
engines=engines,
active_engine=active_engine,
function_name=function_name,
failed_engine=failed_engine,
)
if not isinstance(high_engine, dict):
_runtime_log(
"LLM路由",
(
"OpenClaw高阶结构化升级未命中,回退通用 high tier:"
f"function={function_name}, reason={str(error)[:120]}, "
f"force_intent_retry={force_intent_retry}"
),
)
high_engine = self._resolve_tier_engine(
tier="high",
engines=engines,
active_engine=active_engine,
)
if not isinstance(high_engine, dict):
return None, None
high_engine = self._ensure_engine_allowed_for_function(
engine=high_engine,
function_name=function_name,
settings=self._normalize_engine_settings(bundle.get("settings")),
engine_label="high_tier_structured_failover",
)
forced_glm5_engine = self._prefer_glm5_high_tier_retry_engine(
bundle=bundle,
engines=engines,
function_name=function_name,
failed_engine=failed_engine,
current_engine=high_engine,
)
if isinstance(forced_glm5_engine, dict):
high_engine = forced_glm5_engine
failed_provider = str(failed_engine.get("provider") or "").strip().lower()
failed_model = str(failed_engine.get("model") or "").strip()
high_provider = str(high_engine.get("provider") or "").strip().lower()
high_model = str(high_engine.get("model") or "").strip()
if failed_provider == high_provider and failed_model == high_model:
if not force_intent_retry:
_runtime_log(
"LLM路由",
(
"高阶结构化升级引擎与失败引擎相同,跳过:"
f"function={function_name}, provider={high_provider}, model={high_model}"
),
)
return None, None
fallback_engine: Optional[dict[str, Any]] = None
for fallback_tier in ("safety", "low"):
candidate = self._resolve_tier_engine(
tier=fallback_tier,
engines=engines,
active_engine=active_engine,
)
if not isinstance(candidate, dict):
continue
candidate = self._ensure_engine_allowed_for_function(
engine=candidate,
function_name=function_name,
settings=self._normalize_engine_settings(bundle.get("settings")),
engine_label=f"intent_structured_failover_{fallback_tier}",
)
candidate_provider = str(candidate.get("provider") or "").strip().lower()
candidate_model = str(candidate.get("model") or "").strip()
if candidate_provider == failed_provider and candidate_model == failed_model:
continue
fallback_engine = candidate
break
if fallback_engine is None:
return None, None
high_engine = fallback_engine
high_provider = str(high_engine.get("provider") or "").strip().lower()
high_model = str(high_engine.get("model") or "").strip()
_runtime_log(
"LLM路由",
(
"低阶结构化失败自动升级:"
f"function={function_name}, from={failed_provider}:{failed_model}, "
f"to={high_provider}:{high_model}, reason={str(error)[:120]}, "
f"force_intent_retry={force_intent_retry}"
),
)
completion = await self._chat_completion_raw(
provider=high_provider,
prompt=prompt,
system_prompt=system_prompt,
api_key=str(high_engine.get("api_key") or ""),
base_url=str(high_engine.get("base_url") or ""),
model=high_model,
temperature=temperature,
max_tokens=max_tokens,
function_name=(
"intent_structured_runtime_failover"
if force_intent_retry
else function_name
),
response_mode=response_mode,
)
extraction = self._extract_structured_object_from_response(
function_name=function_name,
response=completion.raw_response,
fallback_text=completion.text,
provider=high_provider,
model=high_model,
)
if extraction.payload is None:
raise ValueError(str(extraction.error_code or "structured_schema_invalid"))
_runtime_log(
"LLM结构化",
(
f"function={function_name}, structured_source={extraction.source_field or 'none'}, "
f"validation={extraction.validation}, fallback=true"
),
)
_runtime_log(
"LLM路由",
(
"高阶结构化升级成功:"
f"function={function_name}, provider={high_provider}, model={high_model}, "
f"structured_source={extraction.source_field or 'none'}"
),
)
return extraction, high_engine
except Exception as retry_exc:
_runtime_log(
"LLM路由",
(
"低阶结构化失败升级高阶也失败:"
f"function={function_name}, error={type(retry_exc).__name__}:{str(retry_exc)[:180]}"
),
)
return None, None