將 111 Ollama fallback 收斂到輕量模型

This commit is contained in:
OoO
2026-05-21 12:38:08 +08:00
committed by AiderHeal Bot
parent d6ae216c8c
commit 00a808518e
10 changed files with 70 additions and 17 deletions

View File

@@ -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

View File

@@ -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 # 用於模板顯示

View File

@@ -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-firstHermes scan 與 OpenClaw assessment 都走 `OllamaService` 三主機 retryGemini 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 → 111Gemini 只允許以 `openclaw_bot_image_gemini` caller 作為失敗後備援。
- OpenClaw 週報、月報、Meta analysis、日報洞察、Telegram PPT 分析與 MCP fallback 也必須 Ollama-firstGemini 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:14b111 fallback 降級 7BNIM fallback | GCP-A → GCP-B → 111NVIDIA NIM 備援 | Ollama 零NIM 配額內免費 | NIM 80 |
| OpenClaw 策略師 | qwen3:14b111 fallback 降級 7BGemini 鎖定場景 | Ollama-firstGemini 備援 | Ollama 零Gemini 需控管 | — |
| NemoTron 派發器 | qwen3:14b111 fallback 降級 llama3.2NIM fallback | GCP-A → GCP-B → 111NVIDIA NIM 備援 | Ollama 零NIM 配額內免費 | NIM 80 |
| OpenClaw 策略師 | qwen3:14b111 fallback 降級 llama3.2Gemini 鎖定場景 | Ollama-firstGemini 備援 | Ollama 零Gemini 需控管 | — |
| ElephantAlpha 編排者 | ElephantAlpha | 依部署環境 | 受控 | HITL / 任務制 |
---

View File

@@ -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 並推高 swap111 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 觸發密碼管理提示的機率。

View File

@@ -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)

View File

@@ -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'

View File

@@ -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:

View File

@@ -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

View File

@@ -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)

View File

@@ -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"