# 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;若仍有問題,再實施三層策略