# 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 多供應商策略）
