feat(adr-004): NIM HTTP 429 → Hermes 規則引擎降級路由
All checks were successful
CD Pipeline / deploy (push) Successful in 1m10s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m10s
- _call_nim(): 429 不重試,立即拋出讓上層接管 - _hermes_rule_fallback(): 確定性四規則路由(gap/sales/risk 閾值), Telegram 告警加 🟡 降級前綴,行為與 NIM system prompt 一致 - dispatch(): 捕捉 HTTPError 429 → 轉 _hermes_rule_fallback(), 回傳 nim_stats.degraded=True 供監控追蹤 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -384,6 +384,11 @@ class NemotronDispatcher:
|
||||
break
|
||||
except (requests.Timeout, requests.HTTPError) as e:
|
||||
last_err = e
|
||||
# ADR-004: 429 不重試,立即拋出讓上層啟動 Hermes 規則引擎降級
|
||||
if isinstance(e, requests.HTTPError) and e.response is not None \
|
||||
and e.response.status_code == 429:
|
||||
logger.warning("[NIM] HTTP 429 速率限制,跳出 retry 迴圈")
|
||||
raise
|
||||
if _attempt < 2:
|
||||
_time.sleep(2 ** _attempt)
|
||||
logger.warning(f"[NIM] retry {_attempt + 1}/2 after {e}")
|
||||
@@ -431,6 +436,87 @@ class NemotronDispatcher:
|
||||
logger.info(f"[NIM] 收到 {len(results)} 個 tool_calls | tokens={nim_stats['total_tokens']}")
|
||||
return results, nim_stats
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# ADR-004:Hermes 規則引擎降級路由
|
||||
# ──────────────────────────────────────────────
|
||||
def _hermes_rule_fallback(self, threats: list, hermes_stats: Optional[dict] = None) -> dict:
|
||||
"""
|
||||
ADR-004 降級模式:NIM HTTP 429 時,改用確定性規則路由 Hermes 威脅清單。
|
||||
路由規則與 NIM system prompt 一致,所有 Telegram 告警加 🟡 降級前綴。
|
||||
|
||||
Rules(依序判斷,命中即停):
|
||||
1. gap_pct < 5% 且 sales_delta < -30% → flag_for_human_review(疑似缺貨/流量異常)
|
||||
2. gap_pct ≥ 5% 且 risk=HIGH → trigger_price_alert
|
||||
3. gap_pct < 0 且 sales_delta > 0 → add_to_recommendation(我方具競爭力)
|
||||
4. 其餘 → flag_for_human_review(信心不足/複雜情況)
|
||||
"""
|
||||
degraded_note = "🟡 [降級模式 ADR-004] NIM 配額耗盡,改用 Hermes 規則引擎決策"
|
||||
footprint = degraded_note + "\n" + _build_footprint_block(hermes_stats, None)
|
||||
|
||||
dispatched, errors = 0, []
|
||||
|
||||
for t in threats:
|
||||
try:
|
||||
if t.gap_pct < 5 and t.sales_7d_delta_pct < -30:
|
||||
# Rule 1:價差微小但銷量大跌 → 非定價問題,人工確認
|
||||
self._exec_flag_for_human_review(
|
||||
sku=t.sku, name=t.name,
|
||||
concern=(
|
||||
f"🟡 [規則引擎] 價差僅 {t.gap_pct:+.1f}% 但銷量大跌 "
|
||||
f"{t.sales_7d_delta_pct:+.1f}%,疑似缺貨/下架/平台流量異常,"
|
||||
"請人工走查前台。"
|
||||
),
|
||||
confidence=0.80,
|
||||
footprint=footprint,
|
||||
momo_price=t.momo_price, comp_price=t.pchome_price,
|
||||
gap_pct=t.gap_pct, sales_delta=t.sales_7d_delta_pct,
|
||||
)
|
||||
elif t.gap_pct >= 5 and t.risk == "HIGH":
|
||||
# Rule 2:高價差 HIGH 風險 → 競價告警
|
||||
self._exec_trigger_price_alert(
|
||||
t.sku, t.name,
|
||||
t.gap_pct, t.sales_7d_delta_pct,
|
||||
f"🟡 [規則引擎] {t.recommended_action}",
|
||||
t.confidence,
|
||||
momo_price=t.momo_price, comp_price=t.pchome_price,
|
||||
footprint=footprint,
|
||||
)
|
||||
elif t.gap_pct < 0 and t.sales_7d_delta_pct > 0:
|
||||
# Rule 3:我方具競爭力 + 銷量正成長 → 推薦
|
||||
self._exec_add_to_recommendation(
|
||||
t.sku, t.name,
|
||||
(
|
||||
f"🟡 [規則引擎] 我方比競品便宜 {abs(t.gap_pct):.1f}%,"
|
||||
f"銷量正成長 {t.sales_7d_delta_pct:+.1f}%"
|
||||
),
|
||||
t.confidence,
|
||||
footprint=footprint,
|
||||
threat=t,
|
||||
)
|
||||
else:
|
||||
# Rule 4:其餘複雜情況 → 人工覆核
|
||||
self._exec_flag_for_human_review(
|
||||
sku=t.sku, name=t.name,
|
||||
concern=(
|
||||
f"🟡 [規則引擎] 情況複雜或信心不足(信心 {t.confidence:.0%}),"
|
||||
f"建議:{t.recommended_action}"
|
||||
),
|
||||
confidence=t.confidence,
|
||||
footprint=footprint,
|
||||
momo_price=t.momo_price, comp_price=t.pchome_price,
|
||||
gap_pct=t.gap_pct, sales_delta=t.sales_7d_delta_pct,
|
||||
)
|
||||
dispatched += 1
|
||||
except Exception as e:
|
||||
errors.append(f"fallback({t.sku}): {e}")
|
||||
logger.error(f"[Dispatcher][ADR-004] Hermes fallback 失敗 {t.sku}: {e}")
|
||||
|
||||
logger.info(
|
||||
f"[Dispatcher][ADR-004] Hermes 規則引擎降級完成 "
|
||||
f"dispatched={dispatched} errors={len(errors)}"
|
||||
)
|
||||
return {"dispatched": dispatched, "skipped": 0, "errors": errors, "nim_stats": {"degraded": True}}
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# 語意化訊息格式器
|
||||
# ──────────────────────────────────────────────
|
||||
@@ -825,6 +911,23 @@ class NemotronDispatcher:
|
||||
|
||||
try:
|
||||
tool_calls, nim_stats = self._call_nim(nim_candidates)
|
||||
except requests.HTTPError as e:
|
||||
if e.response is not None and e.response.status_code == 429:
|
||||
logger.warning("[Dispatcher][ADR-004] NIM HTTP 429,啟動 Hermes 規則引擎降級")
|
||||
fb = self._hermes_rule_fallback(nim_candidates, hermes_stats)
|
||||
return {
|
||||
"dispatched": dispatched + fb["dispatched"],
|
||||
"skipped": skipped + fb["skipped"],
|
||||
"errors": errors + fb["errors"],
|
||||
"nim_stats": fb["nim_stats"],
|
||||
}
|
||||
logger.error(f"[Dispatcher] NIM HTTP 錯誤: {e}")
|
||||
return {
|
||||
"dispatched": dispatched,
|
||||
"skipped": len(nim_candidates),
|
||||
"errors": errors + [str(e)],
|
||||
"nim_stats": {},
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"[Dispatcher] NIM 呼叫失敗: {e}")
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user