From e3da4ffbb3045b54d8f81841769fe13a58b8741b Mon Sep 17 00:00:00 2001 From: OoO Date: Thu, 21 May 2026 16:18:21 +0800 Subject: [PATCH] =?UTF-8?q?=E9=98=BB=E6=AD=A2=20Gemini=20=E6=88=90?= =?UTF-8?q?=E7=82=BA=E6=8E=A8=E8=96=A6=E4=B8=BB=E8=B7=AF=E5=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.py | 2 +- docs/AI_INTELLIGENCE_MODULE_SOT.md | 6 ++++-- docs/memory/history_logs.md | 1 + services/ai_provider.py | 4 +--- services/elephant_alpha_orchestrator.py | 14 +++++++++----- services/llm_model_router.py | 25 +++++++++++++++++++++++-- tests/test_ai_provider_ollama_first.py | 10 ++++++++++ tests/test_elephant_alpha_engine.py | 12 ++++++++++++ tests/test_llm_model_router.py | 16 ++++++++++++++-- 9 files changed, 75 insertions(+), 15 deletions(-) diff --git a/config.py b/config.py index 99018a8..eaebefa 100644 --- a/config.py +++ b/config.py @@ -325,7 +325,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.376" +SYSTEM_VERSION = "V10.377" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/docs/AI_INTELLIGENCE_MODULE_SOT.md b/docs/AI_INTELLIGENCE_MODULE_SOT.md index eb4413c..221ab7a 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -2,7 +2,7 @@ > **最後更新**: 2026-05-21 (台北時間) > **狀態**: 🟢 四 AI Agent 自動化閉環已落地;LLM 路由紅線升級為 Ollama-first 三主機級聯,Gemini 備援預設關閉 -> **適用版本**: V10.364 +> **適用版本**: V10.377 --- @@ -25,6 +25,8 @@ - OpenClaw 週報、月報、Meta analysis、日報洞察、Telegram PPT 分析與 MCP fallback 也必須 Ollama-first;Gemini caller 只能帶 `_gemini_fallback` 或明確 fallback caller 語意,且不得先於 Ollama/NIM 被呼叫。 - OpenClaw 週報、月報、Meta analysis、日報洞察與每日報告的 Gemini/NIM 備援 caller 必須登錄在 caller registry、AI 觀測台 agent group 與 Telegram 狀態統計,避免 fallback 用量被歸類為未知或漏算。 - Gemini API 出站有第二道 kill switch:`GEMINI_FALLBACK_ENABLED` 預設為 `false`。即使 `GEMINI_API_KEY` 存在,通用 AI fallback、OpenClaw 報告/QA/PPT/圖片、MCP Grounding 與 Code Review L3 都不得呼叫 Gemini;只有操作員明確設為 `true` 時,Gemini 才能作緊急備援。 +- Gemini 不可被任何狀態面板或 router 推薦為主提供者:`AIProviderService._get_recommended_provider()` 不得回傳 `gemini`,只能顯示為 fallback 狀態;`llm_model_router` 的 `ea_engine` 若收到 `gemini-*` default 必須改回 `hermes3:latest`,需要深推理時才升本地 `deepseek-r1:14b`。 +- ElephantAlpha prompt / agent registry 不得再把 OpenClaw 描述為 Gemini 主模型;OpenClaw 是 `qwen2.5-coder:7b` / `qwen3:14b` Ollama-first 策略師,Gemini 僅能在 guard 顯式解鎖後作 emergency fallback。 - 111 `192.168.0.111` 只是最後一道 Mac fallback,不承接 7B+、vision、long-context 模型長駐;`OllamaService.generate()` 落到 111 時會將 `qwen3`、`deepseek-r1`、`hermes3`、`qwen2.5*`、`gemma3`、`llava`、`minicpm-v` 與 7B+ 模型依 `OLLAMA_111_MODEL_DOWNGRADE_PATTERNS` 降級到 `OLLAMA_111_MODEL_FALLBACK=llama3.2:latest`,並以 `OLLAMA_111_KEEP_ALIVE=5m`、`OLLAMA_111_MAX_TIMEOUT=45`、`OLLAMA_111_NUM_CTX=4096` 封頂,避免 16GB RAM 主機被大 context runner 與 24h keep-alive 壓到 swap。 ## 一、四 AI Agent 路由架構 @@ -66,7 +68,7 @@ SQL漏斗(~300筆) |------|------|------|------|---------| | Hermes 分析師 | hermes3:latest / bge-m3 | GCP-A → GCP-B → 111 Ollama | 零 | 無限 | | NemoTron 派發器 | qwen3:14b;111 fallback 降級 llama3.2;NIM fallback | GCP-A → GCP-B → 111;NVIDIA NIM 備援 | Ollama 零;NIM 配額內免費 | NIM 80 | -| OpenClaw 策略師 | qwen3:14b;111 fallback 降級 llama3.2;Gemini 鎖定場景 | Ollama-first;Gemini 備援 | Ollama 零;Gemini 需控管 | — | +| OpenClaw 策略師 | qwen2.5-coder:7b / qwen3:14b;111 fallback 降級 llama3.2 | Ollama-first;Gemini emergency fallback only | Ollama 零;Gemini 預設封鎖 | — | | ElephantAlpha 編排者 | ElephantAlpha | 依部署環境 | 受控 | HITL / 任務制 | --- diff --git a/docs/memory/history_logs.md b/docs/memory/history_logs.md index 627ad81..30bfc96 100644 --- a/docs/memory/history_logs.md +++ b/docs/memory/history_logs.md @@ -13,6 +13,7 @@ ## 📅 詳細更新日誌 (考古存檔) ### 2026-05-21:瀏覽器測試守門與 PChome 熱路徑優化 +- **V10.377 Gemini 主路徑防漏補強**: `AIProviderService._get_recommended_provider()` 不再於 Ollama 不通時推薦 `gemini` 作為主提供者;`llm_model_router` 的 `ea_engine` 即使 caller 傳入 `gemini-2.0-flash` default,也會改回 `hermes3:latest`,需要深推理才升 `deepseek-r1:14b`;`ElephantAlphaOrchestrator` 的 OpenClaw registry / system prompt 改為 Ollama-first,避免 L3 HITL prompt 繼續把 Gemini 當主模型描述。同步補 AI SOT 與防回歸測試。 - **V10.376 Recipe Box 同款防曬漂移比對**: `services/marketplace_product_matcher.py` 對 Recipe Box 多效提亮防曬霜新增 shared identity anchor 加分,當 MOMO 長標含兒童/無毒/天然彩妝等行銷詞、PChome 以「韓兔 多效提亮防曬霜」呈現時仍可判定同款;同步測試鎖住 `shared_identity_anchor_recipe_box_line`,避免平台名稱漂移讓同款價格告警漏報。 - **V10.375 過期活動爬蟲排程 opt-in**: `run_scheduler.py` 將固定 LPN 的 `edm_task` / `festival_task` 改為 `MOMO_ENABLE_LEGACY_EDM_SCHEDULE=true` 才註冊,季節活動 `mothers_day_2026` / `valentine_520_2026` / `labor_day_2026` 改為 `MOMO_ENABLE_SEASONAL_PROMO_SCHEDULE=true` 才註冊;`services/data/crawler_config.json` 同步暫停已失效的 mothers_day LPN,避免 scheduler 定時打過期 MOMO 活動頁造成 Selenium browser loop 與無效負載。手動 API / CLI 指定 LPN 仍保留;同版整合 NIVEA/OPI 等比價搜尋 noise 與 identity anchor 補強。 - **V10.374 EDM 失效頁告警止血**: `scheduler.py` 新增 MOMO EDM alert guard,`run_edm_task` / `run_festival_task` / `run_promo_event_task` 遇到「很抱歉此EDM不存在」時會接受 browser alert、寫入 `Skipped / edm_unavailable` stats,且不再送 EventRouter failure,避免 festival / mothers_day 過期活動頁重新累積 Telegram queue;同版整合 REJURAN 麗駐蘭唇膏同款在價格比過寬時的 exact-identity 價格懲罰豁免。 diff --git a/services/ai_provider.py b/services/ai_provider.py index 0dc2142..600f26e 100644 --- a/services/ai_provider.py +++ b/services/ai_provider.py @@ -158,13 +158,11 @@ class AIProviderService: return status def _get_recommended_provider(self, ollama_ok: bool, gemini_ok: bool, elephant_ok: bool) -> str: - """根據可用性推薦提供者""" + """根據可用性推薦主提供者;Gemini 僅能備援,不可被推薦為主路徑。""" if ollama_ok: return 'ollama' if self._default_provider == 'elephant' and elephant_ok: return 'elephant' - if gemini_ok: - return 'gemini' if elephant_ok: return 'elephant' return 'none' diff --git a/services/elephant_alpha_orchestrator.py b/services/elephant_alpha_orchestrator.py index 8b6d80f..8abe1aa 100644 --- a/services/elephant_alpha_orchestrator.py +++ b/services/elephant_alpha_orchestrator.py @@ -85,10 +85,9 @@ class ElephantAlphaOrchestrator: ), "openclaw": AgentCapability( name="OpenClaw Strategist", - # LOCKED-GEMINI: EA HITL 戰略決策影響統帥行動,要最高品質推理 - # 未來可升 Claude Sonnet 4.6 (agentic 工具使用佳) — Phase 7 任務 - # ADR-028 鎖定場景 #6 - model="gemini-2.0-flash", + # Ollama-first: OpenClaw strategy/reporting must use the approved + # GCP-A → GCP-B → 111 cascade; Gemini is emergency fallback only. + model="qwen2.5-coder:7b", strengths=["strategic_planning", "market_analysis", "insight_generation"], limitations=["real_time_execution", "direct_actions"], cost_per_token=0.0, @@ -125,11 +124,16 @@ AGENT CAPABILITIES: - Limitations: Limited strategic depth, short-term focus - Best for: Executing actions, immediate responses, tool-based operations -3. OPENCLAW (gemini-2.0-flash) +3. OPENCLAW (qwen2.5-coder:7b / qwen3:14b via Ollama cascade) - Strengths: Strategic planning, market analysis, insight generation - Limitations: No direct execution capabilities, analysis-only - Best for: Long-term strategy, market insights, recommendation generation +Gemini policy: +- Gemini is not a primary agent model. +- Gemini may only be used after the approved Ollama cascade fails and the + operator explicitly unlocks the emergency fallback guard. + YOUR SUPERVISORY CAPABILITIES: - Cross-agent coordination and optimization - Strategic long-term planning (weeks/months ahead) diff --git a/services/llm_model_router.py b/services/llm_model_router.py index e0cf064..2a4ff4b 100644 --- a/services/llm_model_router.py +++ b/services/llm_model_router.py @@ -80,15 +80,34 @@ ROUTING_RULES: Dict[str, list] = { 'minicpm-v:latest'), ], - # 推理增強場景(EA HITL 戰略決策;目前未啟用,預留) + # 推理增強場景(EA HITL 戰略決策;Gemini 不可作為預設模型) 'ea_engine': [ (lambda ctx: bool(ctx.get('require_chain_of_thought', False)), 'deepseek-r1:14b'), (lambda ctx: True, - None), # None → caller 用預設(gemini-2.0-flash) + 'hermes3:latest'), ], } +_CALLER_SAFE_DEFAULT_MODELS = { + 'ea_engine': 'hermes3:latest', +} + + +def _sanitize_default_model(caller: str, default: Optional[str]) -> Optional[str]: + """Model router must not hand Gemini back as an Ollama-route default.""" + if default and default.strip().lower().startswith('gemini'): + safe_default = _CALLER_SAFE_DEFAULT_MODELS.get(caller) + if safe_default: + logger.warning( + "[ModelRouter] %s default=%s rejected; using %s", + caller, + default, + safe_default, + ) + return safe_default + return default + def select_model( caller: str, @@ -107,6 +126,8 @@ def select_model( flag OFF 時直接回 default(不評估規則,向下相容) """ + default = _sanitize_default_model(caller, default) + if not is_model_router_enabled(): return default diff --git a/tests/test_ai_provider_ollama_first.py b/tests/test_ai_provider_ollama_first.py index 4af3b06..5797cc3 100644 --- a/tests/test_ai_provider_ollama_first.py +++ b/tests/test_ai_provider_ollama_first.py @@ -55,3 +55,13 @@ def test_gemini_is_called_only_after_ollama_failure(monkeypatch): assert result.success is True assert result.provider == "gemini" assert result.content == "gemini fallback" + + +def test_status_never_recommends_gemini_as_primary_provider(): + service = AIProviderService(default_provider="ollama") + + assert service._get_recommended_provider( + ollama_ok=False, + gemini_ok=True, + elephant_ok=False, + ) == "none" diff --git a/tests/test_elephant_alpha_engine.py b/tests/test_elephant_alpha_engine.py index afd2409..d081f17 100644 --- a/tests/test_elephant_alpha_engine.py +++ b/tests/test_elephant_alpha_engine.py @@ -486,3 +486,15 @@ def test_resource_optimization_cannot_use_legacy_autonomous_execution_template(m asyncio.run(engine._notify_telegram_executed(decision, trigger)) assert sent == [] + + +def test_elephant_alpha_openclaw_registry_is_ollama_first(): + from services.elephant_alpha_orchestrator import ElephantAlphaOrchestrator + + orchestrator = ElephantAlphaOrchestrator() + openclaw = orchestrator.agents["openclaw"] + + assert not openclaw.model.startswith("gemini") + assert openclaw.model == "qwen2.5-coder:7b" + assert "Gemini is not a primary agent model" in orchestrator.system_prompt + assert "Ollama cascade" in orchestrator.system_prompt diff --git a/tests/test_llm_model_router.py b/tests/test_llm_model_router.py index 039cfe9..e4a4eab 100644 --- a/tests/test_llm_model_router.py +++ b/tests/test_llm_model_router.py @@ -182,7 +182,7 @@ def test_ppt_vision_minicpm_unhealthy_routes_to_llava(monkeypatch): # ═══════════════════════════════════════════════════════════════════════════ def test_ea_engine_no_cot_returns_default(monkeypatch): - """規則命中但 model_name=None → 回 default(caller 用既有 Gemini)""" + """EA 不需要深推理時走免費 Ollama Hermes,不得回 Gemini 預設。""" monkeypatch.setenv('MODEL_ROUTER_ENABLED', 'true') from services.llm_model_router import select_model @@ -191,7 +191,19 @@ def test_ea_engine_no_cot_returns_default(monkeypatch): context={'require_chain_of_thought': False}, default='gemini-2.0-flash', ) - assert result == 'gemini-2.0-flash' + assert result == 'hermes3:latest' + + +def test_ea_engine_rejects_gemini_default_even_when_router_disabled(monkeypatch): + monkeypatch.delenv('MODEL_ROUTER_ENABLED', raising=False) + from services.llm_model_router import select_model + + result = select_model( + caller='ea_engine', + context={'require_chain_of_thought': False}, + default='gemini-2.0-flash', + ) + assert result == 'hermes3:latest' def test_ea_engine_cot_routes_to_deepseek_r1(monkeypatch):