diff --git a/.env.example b/.env.example index 25fadb9..e20e09d 100644 --- a/.env.example +++ b/.env.example @@ -342,6 +342,7 @@ RAG_DEFAULT_TOP_K=5 RAG_EMBED_MODEL=bge-m3:latest RAG_EMBED_DIM=1024 RAG_EMBED_NORMALIZE=true +EMBED_CONSISTENCY_INCLUDE_111=false PPT_VISION_ENABLED=true PPT_VISION_MODEL=minicpm-v:latest PPT_VISION_TIMEOUT=120 diff --git a/config.py b/config.py index cc5974e..55b3e8c 100644 --- a/config.py +++ b/config.py @@ -325,7 +325,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.417" +SYSTEM_VERSION = "V10.418" 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 6002f4e..c3ba6c3 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -2,7 +2,7 @@ > **最後更新**: 2026-05-24 (台北時間) > **狀態**: 🟢 四 AI Agent 自動化閉環已落地;LLM 路由紅線升級為 Ollama-first 三主機級聯,Gemini 備援預設關閉 -> **適用版本**: V10.417 +> **適用版本**: V10.418 --- @@ -21,6 +21,7 @@ - Code Review Hermes LLM scan 啟用時才使用本地模型矩陣,且預設只跑 GCP-A `qwen2.5-coder:7b` → GCP-B `gemma3:4b`;`CODE_REVIEW_ALLOW_111_FALLBACK=true` 時才允許落到 111,並由 `OllamaService` 降級到 `llama3.2:latest`。不啟用 Gemini 備援,本地掃描失敗時只回空 findings 並交由 OpenClaw 本地矩陣續跑。 - Code Review OpenClaw assessment 預設只跑 GCP-A → GCP-B:GCP-A `qwen2.5-coder:7b`、GCP-B `gemma3:4b`;primary timeout 預設 `15s`、secondary timeout 預設 `60s`,讓 A 掛時快速讓位給 B,且 B 有足夠時間完成審查 prompt。111 是最後救急節點,但部署後重分析預設不打 111;只有 `CODE_REVIEW_ALLOW_111_FALLBACK=true` 才允許 111 接手,並降級到 `llama3.2:latest`。Code Review 的 Ollama `keep_alive` 預設為 `5m`,不得再用 `24h` 長駐 runner 壓住 GCP-B/111。GCP-A/GCP-B 都失敗且 Claude/Gemini 未顯式開啟時,必須回 deterministic 本地降級摘要,不呼叫 Gemini、不落 111、不走其他雲端模型。 - Embedding / semantic RAG 背景任務預設只跑 GCP-A → GCP-B:`OpenClawLearningService` embedding worker 與 `RAGService` 查詢 embedding 呼叫 `OllamaService.generate_embedding(..., allow_111_fallback=False)`;111 只可作人工明確指定的救急路徑,不承接 `bge-m3` 背景批次。`OLLAMA_EMBED_TIMEOUT` / `OLLAMA_EMBED_MAX_TIMEOUT` 預設 `15s`、`OLLAMA_EMBED_KEEP_ALIVE=1m`、`OLLAMA_EMBED_MAX_CHARS=4000`,避免 embedding worker 長時間卡住 GCP-B 或 111。 +- BGE-M3 一致性檢查是監測任務,不是 fallback 壓測;預設只比對 GCP-A / GCP-B。111 Mac fallback 只有 `EMBED_CONSISTENCY_INCLUDE_111=true` 時才納入,避免每週背景檢查把 `bge-m3` 載入 111。 - OpenClaw Telegram Q&A 主路徑也不得綁單一 host:`_call_qwen3_qa()` 必須透過 `OllamaService` 跑 GCP-A → GCP-B → 111,並把實際落點寫入 `ai_calls.provider`。 - OpenClaw Telegram 圖片商品辨識也必須 Ollama-first:`_identify_product_name_with_ollama_vision()` 透過 `OllamaService` 嘗試 GCP-A → GCP-B → 111;Gemini 只允許以 `openclaw_bot_image_gemini` caller 作為失敗後備援。 - OpenClaw 週報、月報、Meta analysis、日報洞察、Telegram PPT 分析與 MCP fallback 也必須 Ollama-first;Gemini caller 只能帶 `_gemini_fallback` 或明確 fallback caller 語意,且不得先於 Ollama/NIM 被呼叫。OpenClaw strategy 的 Ollama `keep_alive` 預設為 `5m`,避免報告型任務把 GCP-B/111 runner 長駐 24h。 diff --git a/docs/memory/history_logs.md b/docs/memory/history_logs.md index 11d507e..22a11b7 100644 --- a/docs/memory/history_logs.md +++ b/docs/memory/history_logs.md @@ -13,6 +13,7 @@ ## 📅 詳細更新日誌 (考古存檔) ### 2026-05-24:PChome 近門檻身份回收第二輪 +- **V10.418 bge-m3 一致性檢查不打 111**: `verify_embedding_consistency()` 預設只比對 GCP-A / GCP-B,不再每週把 111 Mac 納入 bge-m3 背景驗證;新增 `EMBED_CONSISTENCY_INCLUDE_111=false` 預設,只有救急需要驗證 fallback 模型時才 opt-in。這補上 V10.417 後仍會由監測任務載入 111/GCP-B embedding runner 的缺口。 - **V10.417 Embedding/RAG 背景負載保護**: `OllamaService.generate_embedding()` 新增 `allow_111_fallback`、timeout cap、輸入長度 cap 與 `/api/embed keep_alive=1m`;OpenClaw learning worker 與 RAG 查詢預設只跑 GCP-A → GCP-B,不再把 `bge-m3` 背景 embedding / semantic RAG 轉嫁到 111。預設 `OLLAMA_EMBED_TIMEOUT=15`、`OLLAMA_EMBED_MAX_TIMEOUT=15`、`OLLAMA_EMBED_MAX_CHARS=4000`,避免 embedding worker 在 GCP-B/111 長時間常駐或拖住 runner。 - **V10.416 私密清潔 / 彩妝用途 / 棉棒 / 蘭蔻品線防錯配**: marketplace matcher 追加窄範圍 hard-veto guard,讓 SAUGELLA 日用/加強 vs 黃金女郎型、Lactacyd 清新舒涼 vs 生理呵護、LUNASOL 頰彩 vs 眼彩、MUJI 細軸棉棒 vs 黑色棉棒、LANCOME 超極光晶露 vs 超極限肌因精華露不再停留在模糊 `true_low_confidence`,而是以 `*_variant_conflict` / `makeup_usage_conflict` / `lancome_line_conflict` 明確拒絕;不調整 `MIN_MATCH_SCORE`,也不放寬真同款進 matched 的門檻。 - **V10.416 production pilot**: 正式回刷 7 筆近門檻錯配樣本,SAUGELLA 2 筆、LUNASOL 頰彩 vs 眼彩、LANCOME 超極光 vs 超極限、我的心機兒童防曬 vs 海洋友善防曬、Lactacyd 清新舒涼 vs 生理呵護、MUJI 細軸棉棒 vs 黑色棉棒皆更新為 `identity_veto`;`matched` 維持 1619、`true_low_confidence` 759→753、`recoverable_low_score` 1→0、`identity_veto` 4004→4011,無正式 `competitor_prices` 覆寫。 diff --git a/run_scheduler.py b/run_scheduler.py index 61f5387..8915102 100644 --- a/run_scheduler.py +++ b/run_scheduler.py @@ -189,10 +189,10 @@ def _register_schedules(): schedule.every(4).hours.do(run_expire_stale_reviews) logger.info("📅 每 4 小時:expire_stale_reviews(24h 無回應降權 0.5)") - # Phase 11.0 護欄 #3:BGE-M3 跨主機一致性驗證(ADR-033) + # Phase 11.0 護欄 #3:BGE-M3 跨 GCP Ollama 一致性驗證(ADR-033) # 每週一次足夠(驗證模型版本未漂移;不需每次啟動) schedule.every().sunday.at("04:30").do(run_embed_consistency_check) - logger.info("📅 每週日 04:30:bge-m3 跨主機一致性驗證") + logger.info("📅 每週日 04:30:bge-m3 GCP-A/GCP-B 一致性驗證(111 opt-in)") # Phase 42: 三主機 Ollama 健康探針(即使無人開觀測台頁面也持續累積歷史) schedule.every(15).minutes.do(run_host_health_probe) @@ -980,10 +980,11 @@ def run_roi_monthly_report_if_new_month(): def run_embed_consistency_check(): - """每週日 04:30 — BGE-M3 跨主機一致性驗證(ADR-033 護欄 #3)。 + """每週日 04:30 — BGE-M3 跨 GCP Ollama 一致性驗證(ADR-033 護欄 #3)。 跑 verify_embedding_consistency,不一致時 logger.error;ok 時 logger.info。 - 每週一次足夠(驗證模型版本未漂移;過頻會打三主機 Ollama 浪費)。 + 每週一次足夠(驗證模型版本未漂移;過頻會打 Ollama 浪費)。 + 111 Mac fallback 預設不參與,避免背景檢查載入 bge-m3 壓住 16GB 主機。 """ try: from services.rag_service import verify_embedding_consistency @@ -1003,7 +1004,7 @@ def run_embed_consistency_check(): ) logger.error( "[EmbedConsistency] ⚠️ INCONSISTENT — RAG 召回率將下降;" - "檢查三主機 bge-m3 模型版本是否同步(ollama list)" + "檢查 GCP-A/GCP-B bge-m3 模型版本是否同步(ollama list)" ) _notify_scheduler_failure( "run_embed_consistency_check", diff --git a/services/rag_service.py b/services/rag_service.py index df24714..6146d19 100644 --- a/services/rag_service.py +++ b/services/rag_service.py @@ -129,6 +129,10 @@ def get_embedding_signature( EMBED_CONSISTENCY_TEST_TEXT = "momo電商競品分析測試向量一致性檢查" EMBED_CONSISTENCY_MAX_DIFF = 1e-4 # cosine 距離上限(浮點誤差容忍) EMBED_CONSISTENCY_TIMEOUT_SEC = 10.0 # 各主機 embedding 探測 timeout +EMBED_CONSISTENCY_INCLUDE_111 = os.getenv( + 'EMBED_CONSISTENCY_INCLUDE_111', + 'false', +).strip().lower() in ('true', '1', 'yes', 'on') def _cosine_distance(vec_a: List[float], vec_b: List[float]) -> float: @@ -147,11 +151,13 @@ def verify_embedding_consistency( test_text: str = EMBED_CONSISTENCY_TEST_TEXT, max_diff: float = EMBED_CONSISTENCY_MAX_DIFF, ) -> Dict[str, Any]: - """跨三主機(GCP Primary / Secondary / 111)BGE-M3 embedding 一致性驗證。 + """跨 GCP Ollama 節點 BGE-M3 embedding 一致性驗證。 Owen v5.0 護欄 #3(ADR-033)— RAG 啟動時驗證;不一致則 log warning。 fail-safe:任何主機失敗(連線、超時)都跳過,只比對能拿到的 embeddings。 最少 2 個主機可達才能比對;只有 1 個 → 回 ok=True + warning「無法比對」。 + 111 是 Mac final fallback,預設不參與背景一致性檢查;只有 + EMBED_CONSISTENCY_INCLUDE_111=true 才納入救急驗證,避免載入 bge-m3 壓住 111。 回傳: { @@ -171,8 +177,9 @@ def verify_embedding_consistency( hosts = { 'gcp_ollama': OLLAMA_HOST_PRIMARY, 'ollama_secondary': OLLAMA_HOST_SECONDARY, - 'ollama_111': OLLAMA_HOST_FALLBACK, } + if EMBED_CONSISTENCY_INCLUDE_111: + hosts['ollama_111'] = OLLAMA_HOST_FALLBACK embeddings: Dict[str, List[float]] = {} errors: List[str] = [] @@ -185,6 +192,7 @@ def verify_embedding_consistency( model=RAG_EMBED_MODEL, host=host, # 顯式指定(避免 retry 鏈干擾驗證) timeout=int(EMBED_CONSISTENCY_TIMEOUT_SEC), + allow_111_fallback=(label == 'ollama_111'), ) elapsed = time.monotonic() - t0 if vec and len(vec) == RAG_EMBED_DIM: diff --git a/tests/test_rag_service.py b/tests/test_rag_service.py index 028044e..118aee5 100644 --- a/tests/test_rag_service.py +++ b/tests/test_rag_service.py @@ -278,6 +278,53 @@ class TestEmbeddingSignature: assert len(result.hits) == 1 +class TestEmbeddingConsistencyRouting: + def test_consistency_check_skips_111_by_default(self, monkeypatch): + from services import rag_service as rs + from services import ollama_service as oss + + calls = [] + + def fake_embed(text, model, host, timeout, **kwargs): + calls.append((host, kwargs)) + return _fake_embedding() + + monkeypatch.setattr(rs, 'EMBED_CONSISTENCY_INCLUDE_111', False) + monkeypatch.setattr(oss.ollama_service, 'generate_embedding', fake_embed) + + result = rs.verify_embedding_consistency() + + hosts = [host for host, _kwargs in calls] + assert result['ok'] is True + assert hosts == [oss.OLLAMA_HOST_PRIMARY, oss.OLLAMA_HOST_SECONDARY] + assert oss.OLLAMA_HOST_FALLBACK not in hosts + assert all(kwargs.get('allow_111_fallback') is False for _host, kwargs in calls) + + def test_consistency_check_can_include_111_when_explicitly_enabled(self, monkeypatch): + from services import rag_service as rs + from services import ollama_service as oss + + calls = [] + + def fake_embed(text, model, host, timeout, **kwargs): + calls.append((host, kwargs)) + return _fake_embedding() + + monkeypatch.setattr(rs, 'EMBED_CONSISTENCY_INCLUDE_111', True) + monkeypatch.setattr(oss.ollama_service, 'generate_embedding', fake_embed) + + result = rs.verify_embedding_consistency() + + hosts = [host for host, _kwargs in calls] + assert result['ok'] is True + assert hosts == [ + oss.OLLAMA_HOST_PRIMARY, + oss.OLLAMA_HOST_SECONDARY, + oss.OLLAMA_HOST_FALLBACK, + ] + assert calls[-1][1].get('allow_111_fallback') is True + + # ───────────────────────────────────────────────────────────────────────────── # Test 4: fire-and-forget log 失敗不影響主流程 # ─────────────────────────────────────────────────────────────────────────────