LOGBOOK + ADR-092 附錄 C — 2026-04-21 修復紀錄 E2E 驗證: telegram_approval_card_sent message_id=25045 (SignOzDown) ✓ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
205 lines
9.8 KiB
Markdown
205 lines
9.8 KiB
Markdown
# 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 壓縮(徹底解決)
|