將 111 Ollama fallback 收斂到輕量模型
This commit is contained in:
@@ -360,9 +360,9 @@ OLLAMA_MODEL=gemma3:4b
|
||||
OLLAMA_TIMEOUT=120
|
||||
OLLAMA_COPY_TIMEOUT=180
|
||||
OLLAMA_EMBED_TIMEOUT=45
|
||||
# 111 是 Mac final fallback,不承接 14B+ 重模型長駐;落到 111 時自動降級與縮短常駐。
|
||||
OLLAMA_111_MODEL_FALLBACK=qwen2.5:7b-instruct
|
||||
OLLAMA_111_MODEL_DOWNGRADE_PATTERNS=qwen3:14b,deepseek-r1:14b,*:32b,*:70b
|
||||
# 111 是 Mac final fallback,不承接 7B+ / vision / long-context 模型長駐;落到 111 時自動降級與縮短常駐。
|
||||
OLLAMA_111_MODEL_FALLBACK=llama3.2:latest
|
||||
OLLAMA_111_MODEL_DOWNGRADE_PATTERNS=qwen3:*,deepseek-r1:*,hermes3:*,llama3.1:*,qwen2.5:*,qwen2.5-coder:*,gemma3:*,minicpm-v:*,llava:*,*:7b*,*:8b*,*:14b*,*:32b*,*:70b*
|
||||
OLLAMA_111_KEEP_ALIVE=5m
|
||||
OLLAMA_111_MAX_TIMEOUT=45
|
||||
|
||||
|
||||
@@ -323,7 +323,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.361"
|
||||
SYSTEM_VERSION = "V10.362"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> **最後更新**: 2026-05-21 (台北時間)
|
||||
> **狀態**: 🟢 四 AI Agent 自動化閉環已落地;LLM 路由紅線升級為 Ollama-first 三主機級聯,Gemini 備援預設關閉
|
||||
> **適用版本**: V10.361
|
||||
> **適用版本**: V10.362
|
||||
|
||||
---
|
||||
|
||||
@@ -18,14 +18,14 @@
|
||||
- PPT vision、PPT 文案 final fallback、MCP 離線 final fallback 等特殊 Ollama 路徑也不得只打單一 host;如需 `/api/generate`,一律透過 `OllamaService.generate()`。
|
||||
- Code Review pipeline 也必須 Ollama-first:Hermes scan 與 OpenClaw assessment 都走 `OllamaService` 三主機 retry;Gemini telemetry 只能以 `code_review_openclaw_gemini` 出現,表示 Ollama/可選 Claude 備援都失敗後才啟用。
|
||||
- Code Review Hermes scan 預設不呼叫 LLM,改用 deterministic fast static scan,避免部署後先卡三段 Ollama timeout;需要 LLM 掃描時才以 `CODE_REVIEW_HERMES_LLM_SCAN_ENABLED=true` 啟用本地矩陣。
|
||||
- Code Review Hermes LLM scan 啟用時才使用本地模型矩陣:GCP-A `qwen2.5-coder:7b`、GCP-B `gemma3:4b`、111 `hermes3:latest`;不啟用 Gemini 備援,三段本地掃描失敗時只回空 findings 並交由 OpenClaw 本地矩陣續跑。
|
||||
- Code Review OpenClaw assessment 保持主機順序 GCP-A → GCP-B → 111,但可使用主機適配本地模型:GCP-A `qwen2.5-coder:7b`、GCP-B `gemma3:4b`、111 `hermes3:latest`;三段本地 Ollama 全失敗後才允許雲端備援。
|
||||
- Code Review Hermes LLM scan 啟用時才使用本地模型矩陣:GCP-A `qwen2.5-coder:7b`、GCP-B `gemma3:4b`;落到 111 時由 `OllamaService` 降級到 `llama3.2:latest`。不啟用 Gemini 備援,三段本地掃描失敗時只回空 findings 並交由 OpenClaw 本地矩陣續跑。
|
||||
- Code Review OpenClaw assessment 保持主機順序 GCP-A → GCP-B → 111,但可使用主機適配本地模型:GCP-A `qwen2.5-coder:7b`、GCP-B `gemma3:4b`;落到 111 時由 `OllamaService` 降級到 `llama3.2:latest`。三段本地 Ollama 全失敗後才允許雲端備援。
|
||||
- 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 週報、月報、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 才能作緊急備援。
|
||||
- 111 `192.168.0.111` 只是最後一道 Mac fallback,不承接 14B+ 重模型長駐;`OllamaService.generate()` 落到 111 時會將 `qwen3:14b` / `deepseek-r1:14b` / 32B+ / 70B+ 依 `OLLAMA_111_MODEL_DOWNGRADE_PATTERNS` 降級到 `OLLAMA_111_MODEL_FALLBACK`,並以 `OLLAMA_111_KEEP_ALIVE=5m`、`OLLAMA_111_MAX_TIMEOUT=45` 封頂,避免 16GB RAM 主機被 14B 模型與 24h keep-alive 壓到 swap。
|
||||
- 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` 封頂,避免 16GB RAM 主機被大 context runner 與 24h keep-alive 壓到 swap。
|
||||
|
||||
## 一、四 AI Agent 路由架構
|
||||
|
||||
@@ -37,7 +37,7 @@ SQL漏斗(~300筆)
|
||||
任務: 競價威脅分類 → TOP 20 HIGH/MED/LOW
|
||||
↓
|
||||
[NemoTron / qwen3] — 派發器
|
||||
主路徑: qwen3:14b @ GCP-A/GCP-B;落到 111 時自動降級 7B
|
||||
主路徑: qwen3:14b @ GCP-A/GCP-B;落到 111 時自動降級 llama3.2
|
||||
備援: NVIDIA NIM meta/llama-3.1-8b-instruct
|
||||
任務: Tool Calling → Telegram 告警 / DB 寫入
|
||||
↓
|
||||
@@ -65,8 +65,8 @@ SQL漏斗(~300筆)
|
||||
| 角色 | 模型 | 主機 | 成本 | 每日限額 |
|
||||
|------|------|------|------|---------|
|
||||
| Hermes 分析師 | hermes3:latest / bge-m3 | GCP-A → GCP-B → 111 Ollama | 零 | 無限 |
|
||||
| NemoTron 派發器 | qwen3:14b;111 fallback 降級 7B;NIM fallback | GCP-A → GCP-B → 111;NVIDIA NIM 備援 | Ollama 零;NIM 配額內免費 | NIM 80 |
|
||||
| OpenClaw 策略師 | qwen3:14b;111 fallback 降級 7B;Gemini 鎖定場景 | Ollama-first;Gemini 備援 | Ollama 零;Gemini 需控管 | — |
|
||||
| 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 需控管 | — |
|
||||
| ElephantAlpha 編排者 | ElephantAlpha | 依部署環境 | 受控 | HITL / 任務制 |
|
||||
|
||||
---
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
## 📅 詳細更新日誌 (考古存檔)
|
||||
|
||||
### 2026-05-21:瀏覽器測試守門與 PChome 熱路徑優化
|
||||
- **V10.362 111 fallback shrink-to-3B**: 111 Mac 實測 `hermes3` / `qwen2.5-coder` 雖是 7B/8B,但 large context runner 仍會佔用 6-10GB RSS 並推高 swap;111 fallback 改為所有 7B+、vision 與 long-context 文字生成都降級到 `llama3.2:latest`,`ai_calls.model` 也會記錄實際降級模型並把原請求模型放入 `meta.requested_model`。
|
||||
- **V10.361 111 fallback resource guard**: 實測 111 Mac 高 load 主要來自 Codex app / WindowServer 前台負載,且 Ollama 曾因 fallback 載入 `qwen3:14b` 造成 16GB RAM / swap 壓力;已手動 unload 111 上的重模型,並讓 `OllamaService.generate()` 落到 111 時自動把 14B+ 模型降到 `OLLAMA_111_MODEL_FALLBACK`、`keep_alive` 縮至 `OLLAMA_111_KEEP_ALIVE=5m`、timeout 封頂 `OLLAMA_111_MAX_TIMEOUT=45`。GCP-A/GCP-B 仍可跑 `qwen3:14b`,111 只做短時最後備援。
|
||||
- **V10.360 browser smoke guard**: `tests/test_image_fetch.py` 改為預設 skip,只有 `RUN_MOMO_BROWSER_TESTS=1` 才會打開外部 MOMO 網站;手動執行時預設 headless,並關閉 Chrome password manager/autofill,避免一般 pytest 觸發瀏覽器與密碼允許提示。
|
||||
- **Scheduler Selenium 防彈窗**: `managed_scraper_resources()` 補 `credentials_enable_service=false`、`profile.password_manager_enabled=false` 與 Autofill/PasswordManager feature disable,降低背景 Selenium 觸發密碼管理提示的機率。
|
||||
|
||||
@@ -175,6 +175,11 @@ class _CallState:
|
||||
if provider:
|
||||
self.provider = provider[:32]
|
||||
|
||||
def set_model(self, model: str) -> None:
|
||||
"""更新實際模型。適用於 host-aware downgrade 後才知道落點模型的 caller。"""
|
||||
if model:
|
||||
self.model = model[:128]
|
||||
|
||||
def set_cache_hit(self, hit: bool = True) -> None:
|
||||
self.cache_hit = bool(hit)
|
||||
|
||||
|
||||
@@ -340,12 +340,15 @@ class CodeReviewPipeline:
|
||||
)
|
||||
actual_host = resp.host or host
|
||||
_ctx.set_provider(get_provider_tag(actual_host))
|
||||
_ctx.set_model(resp.model or model_name)
|
||||
_ctx.set_tokens(
|
||||
input=resp.input_tokens,
|
||||
output=resp.output_tokens,
|
||||
)
|
||||
_ctx.add_meta('host', actual_host)
|
||||
_ctx.add_meta('host_label', get_host_label(actual_host))
|
||||
if resp.model and resp.model != model_name:
|
||||
_ctx.add_meta('requested_model', model_name)
|
||||
if not resp.success:
|
||||
last_error = resp.error or 'ollama generate failed'
|
||||
_ctx.set_error(last_error)
|
||||
@@ -529,10 +532,12 @@ class CodeReviewPipeline:
|
||||
)
|
||||
actual_host = resp.host or host
|
||||
_ctx.set_provider(get_provider_tag(actual_host))
|
||||
_ctx.set_model(resp.model or model_name)
|
||||
_ctx.set_tokens(input=resp.input_tokens, output=resp.output_tokens)
|
||||
_ctx.add_meta('host', actual_host)
|
||||
_ctx.add_meta('host_label', get_host_label(actual_host))
|
||||
_ctx.add_meta('model', model_name)
|
||||
if resp.model and resp.model != model_name:
|
||||
_ctx.add_meta('requested_model', model_name)
|
||||
if resp.success and (resp.content or '').strip():
|
||||
return resp.content or ""
|
||||
last_ollama_error = resp.error or 'ollama generate failed'
|
||||
|
||||
@@ -237,12 +237,15 @@ class HermesAnalystService:
|
||||
keep_alive=HERMES_KEEP_ALIVE, # ADR-012:避免冷啟動 timeout
|
||||
)
|
||||
_ctx.set_provider(get_provider_tag(resp.host or ''))
|
||||
_ctx.set_model(resp.model or HERMES_MODEL)
|
||||
_ctx.set_tokens(
|
||||
input=resp.input_tokens,
|
||||
output=resp.output_tokens,
|
||||
)
|
||||
_ctx.add_meta('host', resp.host)
|
||||
_ctx.add_meta('host_label', get_host_label(resp.host or ''))
|
||||
if resp.model and resp.model != HERMES_MODEL:
|
||||
_ctx.add_meta('requested_model', HERMES_MODEL)
|
||||
if not resp.success:
|
||||
raise RuntimeError(resp.error or "ollama generate failed")
|
||||
raw = (resp.content or "").strip()
|
||||
@@ -516,9 +519,12 @@ class HermesAnalystService:
|
||||
keep_alive=HERMES_KEEP_ALIVE,
|
||||
)
|
||||
_ctx.set_provider(get_provider_tag(resp.host or ''))
|
||||
_ctx.set_model(resp.model or HERMES_MODEL)
|
||||
_ctx.set_tokens(input=resp.input_tokens, output=resp.output_tokens)
|
||||
_ctx.add_meta('host', resp.host)
|
||||
_ctx.add_meta('host_label', get_host_label(resp.host or ''))
|
||||
if resp.model and resp.model != HERMES_MODEL:
|
||||
_ctx.add_meta('requested_model', HERMES_MODEL)
|
||||
if not resp.success:
|
||||
raise RuntimeError(resp.error or "ollama generate failed")
|
||||
except Exception as e:
|
||||
|
||||
@@ -58,12 +58,16 @@ COPY_TIMEOUT = int(os.getenv('OLLAMA_COPY_TIMEOUT', '180')) # 文案生成專
|
||||
EMBED_TIMEOUT = int(os.getenv('OLLAMA_EMBED_TIMEOUT', os.getenv('EMBEDDING_TIMEOUT', '45')))
|
||||
FALLBACK_111_KEEP_ALIVE = os.getenv('OLLAMA_111_KEEP_ALIVE', '5m')
|
||||
FALLBACK_111_MAX_TIMEOUT = int(os.getenv('OLLAMA_111_MAX_TIMEOUT', '45'))
|
||||
FALLBACK_111_MODEL = os.getenv('OLLAMA_111_MODEL_FALLBACK', 'qwen2.5:7b-instruct')
|
||||
FALLBACK_111_MODEL = os.getenv('OLLAMA_111_MODEL_FALLBACK', 'llama3.2:latest')
|
||||
FALLBACK_111_MODEL_PATTERNS = tuple(
|
||||
pattern.strip().lower()
|
||||
for pattern in os.getenv(
|
||||
'OLLAMA_111_MODEL_DOWNGRADE_PATTERNS',
|
||||
'qwen3:14b,deepseek-r1:14b,*:32b,*:70b',
|
||||
(
|
||||
'qwen3:*,deepseek-r1:*,hermes3:*,llama3.1:*,'
|
||||
'qwen2.5:*,qwen2.5-coder:*,gemma3:*,minicpm-v:*,llava:*,'
|
||||
'*:7b*,*:8b*,*:14b*,*:32b*,*:70b*'
|
||||
),
|
||||
).split(',')
|
||||
if pattern.strip()
|
||||
)
|
||||
@@ -112,9 +116,9 @@ def _is_111_fallback_host(host: str) -> bool:
|
||||
|
||||
def _effective_model_for_host(model: str, host: str) -> str:
|
||||
"""
|
||||
111 是 Mac/HDD final fallback,不承接 14B+ 等重模型。
|
||||
111 是 Mac/HDD final fallback,不承接 7B+ / vision / long-context 等模型。
|
||||
GCP-A/GCP-B 仍照 caller 指定模型;只有落到 111 才降級,避免 16GB RAM
|
||||
被 qwen3:14b / deepseek-r1:14b 長時間壓到 swap。
|
||||
被 hermes3/qwen/gemma 的大 context runner 長時間壓到 swap。
|
||||
"""
|
||||
if not _is_111_fallback_host(host):
|
||||
return model
|
||||
|
||||
@@ -305,12 +305,15 @@ def _call_qwen3_qa(
|
||||
)
|
||||
actual_provider = get_provider_tag(resp.host or '')
|
||||
ctx.set_provider(actual_provider)
|
||||
ctx.set_model(resp.model or OPENCLAW_QA_OLLAMA_MODEL)
|
||||
ctx.set_tokens(
|
||||
input=resp.input_tokens,
|
||||
output=resp.output_tokens,
|
||||
)
|
||||
ctx.add_meta('host', resp.host)
|
||||
ctx.add_meta('host_label', get_host_label(resp.host or ''))
|
||||
if resp.model and resp.model != OPENCLAW_QA_OLLAMA_MODEL:
|
||||
ctx.add_meta('requested_model', OPENCLAW_QA_OLLAMA_MODEL)
|
||||
if not resp.success:
|
||||
ctx.set_error(resp.error or 'ollama generate failed')
|
||||
ctx.fallback_to_caller('openclaw_qa_gemini_fallback')
|
||||
@@ -1108,9 +1111,12 @@ def _call_ollama_strategy(
|
||||
options={"num_predict": predict},
|
||||
)
|
||||
ctx.set_provider(get_provider_tag(resp.host or ""))
|
||||
ctx.set_model(resp.model or model)
|
||||
ctx.set_tokens(input=resp.input_tokens, output=resp.output_tokens)
|
||||
ctx.add_meta("host", resp.host)
|
||||
ctx.add_meta("host_label", get_host_label(resp.host or ""))
|
||||
if resp.model and resp.model != model:
|
||||
ctx.add_meta("requested_model", model)
|
||||
if not resp.success:
|
||||
ctx.set_error(resp.error or "ollama generate failed")
|
||||
ctx.fallback_to_caller(fallback)
|
||||
|
||||
@@ -271,10 +271,36 @@ def test_111_fallback_keeps_light_model_but_caps_timeout(monkeypatch):
|
||||
|
||||
monkeypatch.setattr(oss, "FALLBACK_111_KEEP_ALIVE", "5m")
|
||||
monkeypatch.setattr(oss, "FALLBACK_111_MAX_TIMEOUT", 45)
|
||||
svc = oss.OllamaService(host="http://192.168.0.111:11434", model="hermes3:latest")
|
||||
svc = oss.OllamaService(host="http://192.168.0.111:11434", model="llama3.2:latest")
|
||||
|
||||
with patch("services.ollama_service.requests.post", side_effect=Timeout):
|
||||
resp = svc.generate("hi", timeout=120, keep_alive="24h")
|
||||
|
||||
assert resp.success is False
|
||||
assert "timeout (45s)" in resp.error
|
||||
|
||||
|
||||
def test_111_fallback_downgrades_hermes_context_heavy_model(monkeypatch):
|
||||
from services import ollama_service as oss
|
||||
|
||||
monkeypatch.setattr(oss, "FALLBACK_111_MODEL", "llama3.2:latest")
|
||||
monkeypatch.setattr(oss, "FALLBACK_111_KEEP_ALIVE", "5m")
|
||||
monkeypatch.setattr(oss, "FALLBACK_111_MAX_TIMEOUT", 45)
|
||||
monkeypatch.setattr(oss, "FALLBACK_111_MODEL_PATTERNS", ("hermes3:*",))
|
||||
|
||||
fake_resp = MagicMock(status_code=200)
|
||||
fake_resp.json.return_value = {
|
||||
"response": "ok",
|
||||
"prompt_eval_count": 3,
|
||||
"eval_count": 2,
|
||||
"total_duration": 1_000_000_000,
|
||||
}
|
||||
svc = oss.OllamaService(host="http://192.168.0.111:11434", model="hermes3:latest")
|
||||
|
||||
with patch("services.ollama_service.requests.post", return_value=fake_resp) as mock_post:
|
||||
resp = svc.generate("hi", timeout=120, keep_alive="24h")
|
||||
|
||||
payload = mock_post.call_args.kwargs["json"]
|
||||
assert payload["model"] == "llama3.2:latest"
|
||||
assert payload["keep_alive"] == "5m"
|
||||
assert resp.model == "llama3.2:latest"
|
||||
|
||||
Reference in New Issue
Block a user