# ADR-034: Caller × Context 動態 Model Router - **Status**: Accepted (待整合到 caller 後 Active) - **Date**: 2026-05-04 - **Decision Maker**: 統帥 - **Author**: Operation Ollama-First v5.0 / Phase 21 - **Related**: ADR-028(LLM 路由)、ADR-029(雙塔分工)、ADR-030(多供應商) --- ## Context 戰役 v5.0 累積完成 Primary + Secondary 兩台 GCP × 各 10 個 Ollama 模型(~67GB)。但既有 caller 多用單一寫死 model(如 sales_copy 永遠用 `llama3.1:8b`),無法動態根據 context 選最佳 model。 **痛點**: 1. **資源浪費**:sales_copy 短文(< 100 字)也用 8B 模型 → 應走 `gemma3:4b`(4GB vs 5GB,延遲 -50%) 2. **品質瓶頸**:Hermes 競價遇複雜 SKU(gap > 20%)仍用 `hermes3:latest`(8B)→ 應升 `qwen3:14b` 3. **重構斷層**:AiderHeal 大型重構(diff > 200 行)用 `qwen2.5-coder:7b` 不夠 → 應升 `qwen2.5-coder:32b` 4. **推理空缺**:EA HITL 需 chain-of-thought 時無 deepseek-r1 路徑 **前置已完成**: - Primary + Secondary 各 10 模型完整對稱 - `services/llm_caller_registry.py` 30+ caller 集中 - `services/cost_throttle_service.py` 成本守門 本 ADR 鎖定**動態路由規則**設計。 --- ## Decision ### 1. 純規則引擎,零 LLM 成本 ```python # services/llm_model_router.py ROUTING_RULES: Dict[str, list] = { 'sales_copy': [ (lambda ctx: ctx.get('expected_length', 0) < 100, 'gemma3:4b'), (lambda ctx: True, 'llama3.1:8b'), ], 'hermes_analyst': [ (lambda ctx: ctx['max_gap_pct'] > 20 or ctx['min_sales_delta'] < -50, 'qwen3:14b'), (lambda ctx: True, 'hermes3:latest'), ], # ... 6 個 caller 共 12 條規則 } ``` ### 2. 路由規則對應表 | Caller | Context 觸發條件 | 升級 Model | 預設 Model | |---|---|---|---| | `sales_copy` | expected_length < 100 字 | `gemma3:4b` | `llama3.1:8b` | | `hermes_analyst` | max_gap_pct > 20% 或 銷量 < -50% | `qwen3:14b` | `hermes3:latest` | | `aider_heal` | diff_lines > 200 | `qwen2.5-coder:32b` | `qwen2.5-coder:7b` | | `openclaw_qa` | query_length > 200 或 multi_turn | `qwen3:14b` | `qwen2.5:7b-instruct` | | `ppt_vision` | minicpm_unhealthy | `llava:latest` | `minicpm-v:latest` | | `ea_engine` | require_chain_of_thought | `deepseek-r1:14b` | (回 default = Gemini)| ### 3. Feature Flag 灰度 - `MODEL_ROUTER_ENABLED` 預設 OFF - caller 端 `select_model(caller, context, default='既有 model')` - flag OFF → 直接回 default(不評估規則)→ 行為與戰前完全相同 ### 4. 失敗安全 - predicate 拋例外 → log warning + skip 到下一條 - caller 不在 ROUTING_RULES → 回 default - 所有規則都不命中 → 回 default ### 5. 整合方式(建議分階段) ```python # Caller 範例(如 ollama_service.generate_sales_copy): from services.llm_model_router import select_model def generate_sales_copy(self, product_name, ...): model = select_model( caller='sales_copy', context={'expected_length': len(product_name) * 3}, default='llama3.1:8b', ) return self.generate(prompt=..., model=model, ...) ``` **戰略性遷移**: - Phase 21.1: model_router service + test 落地(本 commit)✅ - Phase 21.2: sales_copy 整合(低風險示範)⏳ - Phase 21.3: aider_heal 整合(中風險,需 diff_lines 取得) - Phase 21.4: hermes_analyst 整合(高風險,動戰術主流程) - Phase 21.5: 全 caller 遷移完成 → MODEL_ROUTER_ENABLED 預設 ON --- ## Alternatives Considered | 方案 | 否決理由 | |---|---| | **A. LLM-based routing**(用 LLM 決定用哪個 model)| 循環燒錢 + 引入新延遲 | | **B. caller 各自 hardcode 多 model**(不集中)| 規則漂移無 single source of truth | | **C. 直接統一升級到大模型**(如全用 qwen3:14b)| 浪費資源,短文不需 14B | | **D. 配置檔 YAML/JSON**(運行時讀檔)| 過度工程;Python lambda 已夠彈性 | --- ## Consequences ### 正面(5) 1. **資源節省**:短文 sales_copy 用 4GB gemma3 vs 5GB llama3.1,延遲 -50% 2. **品質提升**:複雜場景自動升大模型(hermes 14B / aider 32B) 3. **零 LLM 成本**:純 Python lambda 規則 4. **失敗安全**:規則例外不阻擋主流程 5. **集中治理**:規則改動只需 PR `llm_model_router.py`,不動 caller ### 負面(3) 1. **規則維護成本**:新 caller / 新 context 條件需更新 rules(但這正是 ADR 治理目標) 2. **context 取得負擔**:caller 必須先計算 context(如 diff_lines)才能呼叫 router 3. **debug 複雜度**:路由命中哪條規則需看 logger.debug ### 風險(3) 1. **規則設計失誤**:閾值(20% / 200 lines)可能不準 → mitigate by Phase 21.2-21.5 灰度觀察 2. **GCP 主機沒拉到對應 model**:select 回的 model 不存在 → mitigate by 拉模型前提(已完成 10 模型對稱) 3. **caller 整合不完整**:部分 caller 仍 hardcode → 文件化遷移計畫 --- ## Verification ### V1:unit test ```bash pytest tests/test_llm_model_router.py -v # 預期 18 tests 全綠 ``` ### V2:caller 整合後 ai_calls 觀察 ```sql SELECT model, COUNT(*), AVG(duration_ms) FROM ai_calls WHERE caller = 'sales_copy' AND called_at > NOW() - INTERVAL '7 days' GROUP BY model; -- 期望:gemma3:4b 短文佔 60%+,llama3.1:8b 長文佔 40%- -- 平均 duration: gemma3 < llama3.1 約 50% ``` ### V3:cost throttle 整合 ```python # Phase 22 規劃:cost_throttle 觸發時自動切便宜 model # 例:claude throttled → select_model 改回 default Gemini Flash ``` --- ## Migration Plan | Phase | 工作 | 狀態 | |---|---|---| | 21.1 | services/llm_model_router.py + 18 tests | ✅ 本 commit | | 21.2 | sales_copy 整合(generate_sales_copy 加 select_model)| ⏳ | | 21.3 | aider_heal 整合(需 diff_lines context)| ⏳ | | 21.4 | hermes_analyst 整合(需 max_gap_pct context)| ⏳ | | 21.5 | openclaw_qa / ppt_vision / ea_engine | ⏳ | | 21.6 | MODEL_ROUTER_ENABLED 預設 ON(觀察 1 週後)| ⏳ | --- ## References - `services/llm_model_router.py`(本 commit) - `tests/test_llm_model_router.py`(18 tests) - `docs/llm_model_full_evaluation_20260504.md` 路由優化建議 - ADR-028(LLM 路由統一準則) - ADR-029(Hermes-First 雙塔分工) - ADR-030(Frontier 多供應商策略)