V10.556 補 GCP-B 模型 fallback 防 111 過載

This commit is contained in:
OoO
2026-06-01 14:55:13 +08:00
parent 24597dcebb
commit 66f2c50964
6 changed files with 87 additions and 6 deletions

View File

@@ -4,6 +4,7 @@
================================================================================
【已完成】
- V10.556 修 Ollama GCP-B model fallbackGCP-B 若缺 coder/large 指定模型,先用 host-compatible fallback `gemma3:4b` 留在 GCP-B不直接把流量推到 111`model not found` 404 視為模型缺失,不再把整台 GCP-B 標 unhealthy。主機順序仍維持 GCP-A → GCP-B → 111。
- V10.555 補 focused total-price reason-based 回刷:`_fetch_retryable_candidate_skus()` 新增一條結構化 reason 窄門,只要舊 attempt 已帶 `focused_exact_total_price_safe` 且命中 matcher 的 `FOCUSED_IDENTITY_TOTAL_PRICE_REASONS`,即可進近門檻重評;仍要求無 hard veto、`exact_identity`、分數下限,並排除 commercial / variant / count / bundle 等阻擋理由。這讓已經被 matcher 明確判為 total-price exact 的舊候選不再依賴手寫商品名 SQL 才能回刷,同時 rom&nd / Solone / Summers Eve 等 review-only 品線仍不會進自動價差線。
- V10.554 接線香氛 / 精油 focused exact 回刷Herb24 晨霧純精油擴香儀黑色、Pavaruni 40 香味 10ml 精油、Pavaruni 20 香味 450g 香氛蠟燭、Derma 大地有機植萃護膚油 150ml 現在明確標記 `focused_exact_total_price_safe`,並接進 `_fetch_retryable_candidate_skus()` 近門檻舊候選回刷。此入口只收 `low_score / refresh_low_score / true_low_confidence` 中命中精準名稱錨點、無 hard veto、`exact_identity` 且沒有變體 / 商業條件 / 件數衝突的候選Laundrin、好物良品融蠟燈、Yuskin 等仍保留人工覆核,不為了拉覆蓋率強推自動價差。
- V10.553 優化 current PPT/AI 比價結果查詢:`fetch_competitor_comparison_results()` 的 current/latest MOMO 價格改用 `JOIN LATERAL` 取單品最新價,移除 `ROW_NUMBER() OVER (PARTITION BY p.id ...)` window scan歷史報表的 `end_date` cutoff 仍保留在 lateral 子查詢內,維持「指定期間截止日前最新 MOMO 價」語意不變。這能降低簡報、OpenClaw/AI payload 與比價匯出在大量 price_records 下的查詢成本。

View File

@@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
SYSTEM_VERSION = "V10.555"
SYSTEM_VERSION = "V10.556"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示

View File

@@ -33,6 +33,7 @@
- OpenClaw 週報、月報、Meta analysis、日報洞察、Telegram PPT 分析與 MCP fallback 也必須 Ollama-firstGemini caller 只能帶 `_gemini_fallback` 或明確 fallback caller 語意,且不得先於 Ollama/NIM 被呼叫。OpenClaw strategy 的 Ollama `keep_alive` 預設為 `5m`,避免報告型任務把 GCP-B/111 runner 長駐 24h。
- OpenClaw 週報、月報、Meta analysis、日報洞察與每日報告的 Gemini/NIM 備援 caller 必須登錄在 caller registry、AI 觀測台 agent group 與 Telegram 狀態統計,避免 fallback 用量被歸類為未知或漏算。
- `ai_calls.provider='ollama_other'` 只允許作為 unresolved/unknown Ollama telemetry bucket例如全 host 失敗、尚未選定實際 GCP-A/GCP-B/111 host 或舊 caller 未帶 host不得把 `ollama_other` 當成實際路由目標或新增非核准 Ollama host。
- GCP-B 若缺 caller 指定的 coder/large 模型,`OllamaService` 必須先在 GCP-B 改用 `OLLAMA_SECONDARY_MODEL_FALLBACK`(預設 `gemma3:4b`),不可因 model 404 把整台 GCP-B 標成 unhealthy 後直接推到 111真正 timeout / HTTP 5xx 才標 host unhealthy。
- Gemini API 出站有第二道 kill switch`GEMINI_FALLBACK_ENABLED` 預設為 `false`。即使 `GEMINI_API_KEY` 存在,通用 AI fallback、OpenClaw 報告/QA/PPT/圖片、MCP Grounding 與 Code Review L3 都不得呼叫 Gemini只有操作員明確設為 `true`Gemini 才能作緊急備援。
- `docker-compose.yml``momo-app``scheduler``telegram-bot` 必須明確設定 `GEMINI_API_HARD_DISABLED=${GEMINI_API_HARD_DISABLED:-true}``GEMINI_FALLBACK_ENABLED=${GEMINI_FALLBACK_ENABLED:-false}``.env` 可保留 `GEMINI_API_KEY`,但不得因 key 存在就讓核心容器產生 Gemini 付費出站。
- Gemini 不可被任何狀態面板或 router 推薦為主提供者:`AIProviderService._get_recommended_provider()` 不得回傳 `gemini`,只能顯示為 fallback 狀態;`llm_model_router``ea_engine` 若收到 `gemini-*` default 必須改回 `hermes3:latest`,需要深推理時才升本地 `deepseek-r1:14b`

View File

@@ -13,6 +13,7 @@
## 📅 詳細更新日誌 (考古存檔)
### 2026-06-01PChome 比價新鮮度操作閉環
- **V10.556 GCP-B model fallback 防 111 過載**: `OllamaService.generate()` 現在會在 GCP-B 對 coder/large 模型使用 host-compatible fallback預設 `gemma3:4b`),避免 GCP-B 缺 `qwen2.5-coder:7b` 時直接被標成 unhealthy 並把流量推到 111。HTTP 404 且訊息為 model not found 時視為模型缺失,不再 mark 整台 host unhealthy其他 HTTP / timeout 仍照舊標 unhealthy。主機順序仍是 GCP-A → GCP-B → 111。
- **V10.555 focused total-price reason-based 回刷窄門**: `_fetch_retryable_candidate_skus()` 新增結構化 diagnostics reason 回刷入口,舊 attempt 若已帶 `focused_exact_total_price_safe` 且命中 matcher 的 `FOCUSED_IDENTITY_TOTAL_PRICE_REASONS`,即可進近門檻重評,不再完全依賴手寫商品名 SQL。此路徑仍要求分數下限、無 hard veto、`exact_identity`,並套用 commercial / variant / count / bundle 等阻擋理由rom&nd、Solone、Summers Eve 等 review-only focused line 不在 total-price reason set仍不會被推入自動價差。
- **V10.554 香氛 / 精油 focused exact 回刷接線**: Herb24 晨霧純精油擴香儀黑色、Pavaruni 40 香味 10ml 精油、Pavaruni 20 香味 450g 香氛蠟燭、Derma 大地有機植萃護膚油 150ml 已明確列入 matcher 的 `focused_exact_total_price_safe`,並接入 `_fetch_retryable_candidate_skus()` 的近門檻舊候選回刷入口。SQL 仍要求 `low_score / refresh_low_score / true_low_confidence`、分數下限、無 hard veto、`exact_identity`且排除變體、商業條件與件數衝突Laundrin、好物良品融蠟燈、Yuskin 等帶人工覆核訊號的品線不納入本輪自動回刷。
- **V10.553 current PPT/AI 比價結果查詢瘦身**: `fetch_competitor_comparison_results()` 不再用 `ROW_NUMBER() OVER (PARTITION BY p.id ...)``price_records` 取得 current/latest MOMO 價格,改為 `JOIN LATERAL` 逐商品取最新價;有指定 `end_date` 的歷史報表仍把 `pr.timestamp < DATE(:end_date) + INTERVAL '1 day'` 放在 lateral 子查詢中,保留「截止日前最新 MOMO 價」語意。這降低簡報、OpenClaw/AI payload 與比價匯出在大量價格紀錄下的查詢成本。

View File

@@ -80,6 +80,15 @@ FALLBACK_111_MODEL_PATTERNS = tuple(
).split(',')
if pattern.strip()
)
SECONDARY_MODEL_FALLBACK = os.getenv('OLLAMA_SECONDARY_MODEL_FALLBACK', 'gemma3:4b')
SECONDARY_MODEL_FALLBACK_PATTERNS = tuple(
pattern.strip().lower()
for pattern in os.getenv(
'OLLAMA_SECONDARY_MODEL_FALLBACK_PATTERNS',
'qwen2.5-coder:*,qwen2.5:*,qwen3:14b,deepseek-r1:14b',
).split(',')
if pattern.strip()
)
# ── GCP 優先 / 111 備援:解析實際可用的 Ollama 主機 ──────────────────────────
# ADR-027 Phase 2 強化:
@@ -124,6 +133,10 @@ def _is_111_fallback_host(host: str) -> bool:
return '192.168.0.111:11434' in (host or '')
def _is_secondary_ollama_host(host: str) -> bool:
return '34.21.145.224:11434' in (host or '') or '192.168.0.110:11436' in (host or '')
def _env_flag(name: str, default: bool = False) -> bool:
raw = os.getenv(name)
if raw is None:
@@ -321,12 +334,25 @@ def _fallback_111_block_reason(host: str) -> Tuple[bool, str]:
def _effective_model_for_host(model: str, host: str) -> str:
"""
111 是 Mac/HDD final fallback不承接 7B+ / vision / long-context 等模型。
GCP-A/GCP-B 仍照 caller 指定模型;只有落到 111 才降級,避免 16GB RAM
被 hermes3/qwen/gemma 的大 context runner 長時間壓到 swap。
GCP-B 是第二順位運算節點;若該節點未安裝 caller 指定的 coder/large
模型,先用 host-compatible fallback 留在 GCP-B避免直接把流量推到
111。111 仍是最後救急節點,且會更嚴格降級。
"""
model_lower = (model or '').lower()
if (
_is_secondary_ollama_host(host)
and SECONDARY_MODEL_FALLBACK
and any(fnmatch.fnmatch(model_lower, pattern) for pattern in SECONDARY_MODEL_FALLBACK_PATTERNS)
):
logger.warning(
"[Ollama] Secondary host model fallback model=%s%s host=%s",
model,
SECONDARY_MODEL_FALLBACK,
host,
)
return SECONDARY_MODEL_FALLBACK
if not _is_111_fallback_host(host):
return model
model_lower = (model or '').lower()
if any(fnmatch.fnmatch(model_lower, pattern) for pattern in FALLBACK_111_MODEL_PATTERNS):
logger.warning(
"[Ollama] 111 fallback 不承接重模型 model=%s,改用 %s",
@@ -682,8 +708,16 @@ class OllamaService:
)
# HTTP 非 200標 unhealthy + 嘗試下一台
last_error = f"HTTP {response.status_code}: {response.text[:200]}"
logger.warning(f"[Ollama] {current_host} HTTP 失敗 → mark_unhealthy + retry: {last_error}")
_mark_unhealthy_best_effort(current_host)
if response.status_code == 404 and "model" in (response.text or "").lower():
logger.warning(
"[Ollama] %s model unavailable model=%s → retry next host without marking host unhealthy: %s",
current_host,
effective_model,
last_error,
)
else:
logger.warning(f"[Ollama] {current_host} HTTP 失敗 → mark_unhealthy + retry: {last_error}")
_mark_unhealthy_best_effort(current_host)
except requests.Timeout:
last_error = f"timeout ({effective_timeout}s)"
logger.warning(f"[Ollama] {current_host} timeout → mark_unhealthy + retry")

View File

@@ -167,6 +167,50 @@ def test_generate_forces_final_fallback_when_unhealthy_ttl_expires_mid_request()
assert 'all 3 hosts failed' in (resp.error or '')
def test_generate_uses_secondary_model_fallback_before_111():
"""GCP-B 缺 coder 模型時,先改用 secondary fallback不直接推到 111。"""
from services import ollama_service as oss
from services.ollama_service import OllamaService
svc = OllamaService()
fake_ok = MagicMock(status_code=200)
fake_ok.json.return_value = {
'response': 'OK from secondary fallback',
'prompt_eval_count': 10,
'eval_count': 5,
}
with patch('services.ollama_service.resolve_ollama_host', return_value=oss.OLLAMA_HOST_SECONDARY), \
patch('services.ollama_service.requests.post', return_value=fake_ok) as mock_post:
resp = svc.generate('test prompt', model='qwen2.5-coder:7b')
assert resp.success is True
assert resp.host == oss.OLLAMA_HOST_SECONDARY
assert resp.model == 'gemma3:4b'
payload = mock_post.call_args.kwargs['json']
assert payload['model'] == 'gemma3:4b'
def test_generate_model_404_does_not_mark_host_unhealthy():
"""模型不存在是 model availability不應把整台 GCP-B 標成 unhealthy。"""
from services import ollama_service as oss
from services.ollama_service import OllamaService
svc = OllamaService()
fake_404 = MagicMock(status_code=404, text='{"error":"model not found"}')
fake_ok = MagicMock(status_code=200)
fake_ok.json.return_value = {'response': 'OK', 'prompt_eval_count': 3, 'eval_count': 2}
with patch('services.ollama_service.resolve_ollama_host', side_effect=[
oss.OLLAMA_HOST_SECONDARY,
oss.OLLAMA_HOST_FALLBACK,
]), patch('services.ollama_service.requests.post', side_effect=[fake_404, fake_ok]):
resp = svc.generate('test prompt', model='tiny-missing-model')
assert resp.success is True
assert oss.OLLAMA_HOST_SECONDARY not in oss._unhealthy_marks
def test_generate_can_disable_111_fallback_for_batch_llm_work():
"""批量 LLM 任務可選擇只跑 GCP-A/GCP-B避免 111 承接長分析。"""
import requests