# ADR-092: AI Decision Layer — LLM 擴展架構

**狀態**: Accepted
**日期**: 2026-04-19 (台北時區)
**作者**: ogt + Claude Opus 4.7 (1M context, 亞太)
**關聯**: ADR-090 盲區治理 / MASTER §3 D1-D6 / feedback_ai_autonomous_direction

---

## 背景

2026-04-19 session 完成 Phase 7 盲區治理後（14 個新 scanner，8 張 0 writer 表全活化），首席架構師 Review 發現嚴重 Gap：

- **AI 層淺**：14 個新 scanner 中 **8 個純 threshold（rule-based）**，只 Hermes 1 個真用 LLM 做決策
- **autonomy_score 實測**：63/100（starter 起步級），`ai_diversity` 子項僅 6/20
- **統帥鐵律**：「朝 AI 自主化方向」— infrastructure 不等於 AI

違反 `feedback_ai_autonomous_direction.md` 核心原則：「禁寫死規則做最終決策」。

---

## 決策

### 1. AI Decision 層的 4 個 LLM Service（本 session 完成）

| Service | LLM 判斷 required_key | 觸發條件 | 職責 |
|---|---|---|---|
| **hermes_rule_quality** | `recommended_actions` | 每日 04:00 Taipei | 分析 noise_rate > 0.5 rule 的假報真因 + 改進建議 |
| **capacity_forecaster** | `priority_actions` | 每日 05:00 Taipei（predict_linear 命中）| 分析 7d 容量預測高風險 host 的修復策略 |
| **compliance_scanner** | `posture_grade` | 每日 03:00 Taipei（有 warning/violation）| 產整體 compliance posture 評級 + top 3 action |
| **coverage_evaluator** | `worst_dimension` | 每 1h（red_ratio > 30% 且 scanned > 50）| 分析 7 維 red 缺口 + 補覆蓋建議 |

### 2. LLM 調用統一 pattern（services/llm_json_parser.py）

**原問題**：4 個 LLM scanner 各自重複 3-path JSON parse 邏輯（~80 行 × 4 = 320 行）
**新 pattern**：`parse_llm_json_response(text, required_key, logger_context)`

```python
# 統一 3-path fallback
Path 1: 剝 markdown fence + 直接 JSON 含 required_key
Path 2: NemoTron wrapper — description/action_title/reasoning 內嵌 JSON
Path 3: 所有失敗 return None + logger.warning
```

所有 LLM service 都用此 helper，未來擴加 LLM service 直接呼叫。

### 3. LLM Service 架構約束（鐵律）

**必須遵守**：
1. **失敗永不 raise**：LLM 掛掉/parse 失敗 → return None → 呼叫者 fallback 硬編規則
2. **AI 只建議不動作**：所有 LLM 輸出都寫 `requires_human_decision=True`，推 Telegram 等人工
3. **openclaw 統一入口**：不直接呼叫 Ollama/NVIDIA/Gemini，走 `get_openclaw().call(prompt)` 多 provider fallback
4. **aol 留痕**：LLM 結果進 `automation_operation_log.output.llm_analysis`
5. **繁中 + JSON schema**：prompt 明確 required_key，system prompt 要求 LLM 產純 JSON

**禁止**：
- 硬編 threshold 做最終決策（只做「觸發討論」）
- LLM 結果直接執行破壞性動作（必過人工）
- 內嵌多 provider 判斷（統一走 openclaw）

### 4. 觸發條件設計原則

**節流避免 token 成本爆炸**：
- Daily cron：每日一次（compliance/forecaster/hermes）
- 事件觸發：coverage 只在 `red_ratio > 30% 且 scanned >= 50` 才跑 LLM
- 避免 bootstrap 首次必觸發的浪費

### 5. autonomy_score 量化追蹤

新增 `GET /api/v1/aiops/kpi` 回 5 子項 × 20 分總分（0-100）：
- `asset_coverage`: green 比例 × 20
- `rule_quality`: 1 - (noisy/total) × 20
- `capacity_health`: critical 扣 10 / warning 扣 3
- `automation_flow`: log10(24h_ops) 標準化 × 20
- `ai_diversity`: ai_generated rules + op_type 多樣性 × 20

Grade: mature(90+) / in_progress(70-90) / starter(50-70) / initial(<50)

---

## 實作成果

### 達成指標
- LLM decision service: **1/9 → 4/9**
- autonomy_score: 未量化 → **63/100 (starter)** 可持續追蹤
- LLM JSON parse 重複碼: **320 行 → 1 份共用 helper**（90 行）

### Commits
- `ba18ad2` Hermes LLM 升級（Hermes 從 threshold 改 LLM）
- `d6b854a` Gap 3.1 capacity_forecaster LLM
- `f6cb938` Gap 3.2 compliance_scanner LLM
- `2f5cab2` Gap 3.3 coverage_evaluator LLM
- `fa643eb` P1 重構：llm_json_parser helper + coverage 雙條件

### 相關檔案
- `apps/api/src/services/llm_json_parser.py`（共用 helper）
- `apps/api/src/services/openclaw.py`（多 provider）
- `apps/api/src/jobs/{hermes_rule_quality,capacity_forecaster,compliance_scanner,coverage_evaluator}_job.py`
- `apps/api/src/services/aiops_kpi_service.py`（autonomy_score 計算）
- `apps/api/src/api/v1/aiops_kpi.py`（KPI endpoint）

---

## 後續工作（下 session）

### P1 剩餘優化（首席架構師 Review 指出）
1. **Prometheus HTTP helper 抽出** — 5 scanner 重複 httpx + 錯誤處理 pattern
2. **14 scanner first_delay 錯開** — 避免並行啟動搶 DB pool（建議 60/90/120/180 階梯）
3. **LLM budget guard** — `aiops_kpi_service` 加 `llm_call_count_24h` 指標超過閾值 Telegram 告警
4. **asset_scanner 918 行拆分** — `providers/` + `writers/` 兩層

### 可擴展 LLM 方向
- `rule_catalog_sync`: 新規則 import 時 LLM 驗證 expr 合理性
- `asset_change_tracker`: 重大 lifecycle 變化時 LLM 評估爆炸半徑
- `drift_interpreter` (既有): 升級更精細 prompt

---

## 回滾計畫

若 LLM service 造成成本或穩定性問題，可分層關閉：

1. 改 config `AIOPS_LLM_ENABLED=false`（需新增，目前未實作）
2. 或 kubectl exec 進 Pod `kill` 單個 loop task
3. 各 LLM `try/except return None → fallback` 原設計保護主流程不受影響

---

## 相關 ADR

- ADR-067 Phase 30 AI 五大應用（OpenClaw + drift_narrator + runbook_generator 等原 LLM service）
- ADR-081 PreDecisionInvestigator + EvidenceSnapshot
- ADR-083 學習閉環重建
- ADR-090 監控盲區治理 + 自動化覆蓋矩陣（11 張 + 8 張新表）

---

## 附錄 A：2026-04-20 四修（B1-B4）— 全流程斷鏈緊急修復

**觸發**：統帥截圖 Telegram 顯示「採納失敗」+ 兩天無任何告警。
**指令**：全景、全流程、全節點、全架構比對，找到所有斷點再修。

### Root Cause 鏈

| 症狀 | 斷點 | 根因 |
|------|------|------|
| Playbook 學習失敗 163次/48h | `playbook_service.update_with_validation` | `evolver` 傳 `.value`（str）→ `setattr` 不觸發 Pydantic → `.value` 炸 |
| Telegram 永久靜默 2 天 | `approval_db.find_by_fingerprint` | PENDING = 「已發 TG」謊言；TG 失敗後 approval 永 PENDING → 永遠收斂跳過 |
| 採納變更失敗 | `telegram_gateway._handle_drift_action` | 呼叫 `DriftAdoptService.adopt_drift()` 但方法不存在（AttributeError） |
| AI 無法感知自身故障 | main.py 無 background watchdog | MASTER §1.1 盲區：SLO 計算只是 API endpoint |

### 修復（6 檔 / 230 行 / commit 156a52f）

1. **B1** `playbook_service.py` — `update_with_validation` setattr 前強制 enum 轉型
2. **B2a** `approval_db.py` — `find_by_fingerprint` debounce 窗外 PENDING 必須 Redis `tg_sent:{fp}` 確認；新 `mark_telegram_confirmed()`
3. **B2b** `webhooks.py` — `_push_to_telegram_background` 成功後 `setex tg_sent:{fp}` 24h；3 call site 傳 fingerprint
4. **Drift** `drift_adopt_service.py` — 新 `adopt_drift(report_id)` wrapper
5. **B3** `jobs/ai_slo_watchdog_job.py` — 新建：每 15 分鐘 W-1/W-2/W-3 → TYPE-8M
6. **B3** `main.py` lifespan 註冊 watchdog loop

---

## 附錄 B：2026-04-20 C1-C4 — Playbook 鏈路結構性斷點修復

**觸發**：統帥全景盤查後，發現 Playbook 自動修復鏈路有 3 個結構性斷點。

| # | 斷點 | 根因 | 修復 | commit |
|---|------|------|------|--------|
| C1 | evolver 封存 yaml_rule playbooks | 無 `YAML_RULE` source guard | `playbook_evolver.py` 兩處加 `if source == YAML_RULE: continue` | de2d34d |
| C2 | seeder 不復活 DEPRECATED yaml_rule | idempotency SQL 包含 DEPRECATED | `AND status != 'deprecated'` | de2d34d |
| C3 | AI 新規則不即時建立 Playbook | `_append_rule_to_yaml` 後無 seeder 呼叫 | `create_task(seed_playbooks_from_rules())` | de2d34d |
| C4 | watchdog 不偵測鏈路斷裂 | W-4 缺失 | `_count_approved_playbooks()`；為 0 → TYPE-8M | de2d34d |

**架構鐵律**：`PlaybookSource.YAML_RULE` playbooks 是自動修復鏈路的「基礎設施」，evolver 的 trust-based 退場邏輯不得觸及此類 playbooks。

---

## 附錄 C：2026-04-21 — BUTTON_DATA_INVALID 根治 + Gitea Code Review 修復

**觸發**：Telegram 所有 `devops_tool` 類別告警卡片發送失敗（HTTP 400 BUTTON_DATA_INVALID）+ Gitea PR Code Review 顯示「AI 分析失敗」。

### Root Cause 鏈

| 症狀 | 斷點 | 根因 |
|------|------|------|
| Telegram 400 BUTTON_DATA_INVALID | `generate_callback_nonce` | UUID(36) + action(20) + ts(10) + rand(8) + colons = 77B > 64B Telegram 限制 |
| Gitea PR "AI 分析失敗" | `_call_openclaw_code_review` | OpenClaw 只有 `/analyze/incident` 和 `/analyze/error`；`/analyze/code-review` 從未實作（404）|
| Push review AttributeError | `_call_openclaw_push_review` | `local_code_review_service.review_push()` 回傳 dict，呼叫端對 dict 做屬性訪問（`analysis.issues`）|

### 修復

1. **nonce 壓縮** `security_interceptor.py` — `generate_callback_nonce` 用 base64url encode UUID bytes（36→22 chars）；`parse_callback_data` 對應 decode；`host_restart_service` nonce = 63B
2. **code review 改 local** `gitea_webhook_service.py` — `_call_openclaw_code_review` 改用 `local_code_review_service.review_pr()`（Ollama + Gemini fallback）
3. **push review dict→model** `gitea_webhook_service.py` — `_call_openclaw_push_review` 加 dict→`CodeReviewResult` 轉換

### E2E 驗證（2026-04-21 21:57 台北）
- `host_restart_service` nonce = 63B ✓，所有 7 個 actions ≤ 64B ✓
- UUID round-trip decode = True ✓
- `telegram_approval_card_sent` message_id=25045（SignOzDown devops_tool）✓ 無 BUTTON_DATA_INVALID

### Commits
- `acab1cd` fix(gitea): code-review 改 local service + push review dict→CodeReviewResult
- `bd73548` fix(telegram): BUTTON_DATA_INVALID nonce 超 64B 根因修復
- `8fd31ec` fix(telegram): nonce UUID base64url 壓縮（徹底解決）
