# ADR-018: LLM 測試策略

| 屬性 | 值 |
|------|-----|
| **狀態** | 延緩 (Deferred) - 方案 A 先行 |
| **建立日期** | 2026-03-26 |
| **決策者** | 首席架構師 + 統帥 |
| **關聯** | Phase 12.3, #68, #69 |

## 背景

現有 LLM 測試 (`test_model_regression.py`, `test_prompt_validation.py`) 存在以下問題：

1. **字串精確匹配** - 檢查 "AWOOOI" 或 "kubectl" 是否存在
2. **LLM 非確定性** - 相同 prompt 產生不同輸出
3. **CI 依賴 Live LLM** - Ollama 不穩定導致 CI 失敗
4. **無語意理解** - 無法判斷「意思相近」

## 決策

採用 **三層測試策略**：

### Tier 1: 結構驗證 (CI 必跑)

```
測試目標: 輸出符合 Schema
執行時機: 每次 CI
Mock 策略: 使用錄製的 Golden Responses
```

**原則**:
- 驗證 JSON Schema，不驗證內容
- 驗證 kubectl 語法有效性，不驗證具體命令
- 驗證風險等級為有效 Enum，不驗證「應該是 HIGH」

**實作**:
```python
# ❌ 錯誤: 字串匹配
lambda r: "CRITICAL" in r.upper()

# ✅ 正確: Schema 驗證
class LLMProposalOutput(BaseModel):
    risk_level: Literal["LOW", "MEDIUM", "HIGH", "CRITICAL"]
    kubectl_command: str
    reasoning: str

def validate_output(response: str) -> bool:
    try:
        parsed = LLMProposalOutput.model_validate_json(response)
        return True
    except ValidationError:
        return False
```

### Tier 2: 屬性測試 (Nightly)

```
測試目標: 輸出符合不變量
執行時機: 每晚 / PR Merge
Mock 策略: Live LLM + 寬鬆閾值
```

**不變量範例**:
- kubectl 命令必須可解析 (用 shlex.split)
- 風險等級必須為有效值
- 回應長度 < 500 字
- 繁體中文比例 > 30% (用 regex 檢測)

**實作**:
```python
def test_kubectl_parseable(llm_response):
    """kubectl 命令必須可被 shell 解析"""
    import shlex
    cmd = extract_kubectl(llm_response)
    if cmd:
        tokens = shlex.split(cmd)  # 若無法解析會拋 ValueError
        assert tokens[0] == "kubectl"

def test_risk_level_valid(llm_response):
    """風險等級必須是有效值"""
    risk = extract_risk_level(llm_response)
    assert risk in {"LOW", "MEDIUM", "HIGH", "CRITICAL"}
```

### Tier 3: 語意品質 (Weekly/Manual)

```
測試目標: 輸出語意品質
執行時機: 每週 / 手動觸發
Mock 策略: Live LLM + Embedding 比對
```

**原則**:
- 使用 Embedding 計算語意相似度
- 與 Golden Response 比對，相似度 > 0.7 為通過
- 統計通過率，8/10 通過 = 整體通過

**實作**:
```python
async def test_semantic_similarity(llm_response, golden_response):
    """語意相似度測試"""
    from sentence_transformers import SentenceTransformer

    model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
    emb1 = model.encode(llm_response)
    emb2 = model.encode(golden_response)

    similarity = cosine_similarity(emb1, emb2)
    assert similarity > 0.7, f"相似度 {similarity:.2f} 低於閾值 0.7"
```

## 測試分層總覽

| Tier | 執行時機 | Mock | 驗證內容 | 失敗處理 |
|------|---------|------|---------|---------|
| **1** | 每次 CI | Golden Response | Schema + 語法 | 阻擋合併 |
| **2** | Nightly | Live LLM | 不變量屬性 | 告警 + Issue |
| **3** | Weekly | Live LLM + Embedding | 語意品質 | 報告 + 趨勢追蹤 |

## 實作計畫

### Phase 1: Schema 強制 (1 天)

1. 修改 OpenClaw Prompt 強制 JSON 輸出
2. 建立 `LLMProposalOutput` Pydantic Model
3. 所有 LLM 回應經過 Schema 驗證

### Phase 2: Golden Response 錄製 (1 天)

1. 執行現有測試，錄製成功回應
2. 儲存為 `tests/fixtures/golden_responses.json`
3. CI 改用 Golden Response Mock

### Phase 3: 屬性測試重構 (2 天)

1. 將 lambda validators 改為屬性測試
2. 新增 kubectl 解析驗證
3. 新增風險等級 Enum 驗證

### Phase 4: 語意測試 (2 天)

1. 整合 sentence-transformers
2. 建立語意相似度基準線
3. 設定 Weekly 排程

## 替代方案

| 方案 | 優點 | 缺點 | 決定 |
|------|------|------|------|
| **Skip 測試** | 簡單 | 無品質保證 | ❌ |
| **增加 Retry** | 簡單 | 治標不治本 | ❌ |
| **降低閾值** | 簡單 | 漏檢問題 | ❌ |
| **三層策略** | 完整 | 實作成本 | ✅ |

## 風險

1. **Embedding 模型載入時間** - 使用輕量模型 (MiniLM)
2. **Golden Response 過時** - 定期更新機制
3. **語意閾值調校** - 初期從寬，逐步收緊

## 成功指標

- Tier 1 測試 100% 通過率 (CI 不再因 LLM 波動失敗)
- Tier 2 測試 > 90% 通過率 (Nightly)
- Tier 3 測試 > 80% 通過率 (Weekly)
- 無 LLM 相關 CI 失敗導致的開發阻塞

## 參考

- [LLM Evaluation Best Practices](https://www.anthropic.com/research/evaluations)
- [Property-Based Testing](https://hypothesis.readthedocs.io/)
- [Sentence Transformers](https://www.sbert.net/)

---

## 2026-03-26 決策更新

**狀態**: 延緩 (Deferred)

**實際採用方案**: 方案 A - 最小改動優先

| 項目 | 實作內容 |
|------|---------|
| 確定性參數 | `temperature: 0.0`, `seed: 42` |
| CI 分層 | LLM 測試移至 Nightly |
| 超時調整 | 300 秒 (CPU 推理) |
| 繁體中文 | System Prompt 強制繁中 |

**原因**:
1. 三層策略實作成本高 (~1000 行代碼)
2. 方案 A 已解決 CI 不穩定問題
3. 優先確保 CI 穩定，後續再評估是否需要完整框架

**評估文件**: `docs/evaluations/2026-03-26_llm_testing_evaluation.md`

**後續**: 若方案 A 效果良好，此 ADR 維持 Deferred；若仍有問題，再實施三層策略
