一個很自然的想法是:既然 Day 7 的簡單規則最準、Day 9 的領域過濾能減少誤報,那把兩者「混合」起來,是不是就能拿到兩邊的優點?
今天就來實作這個混合檢測器,並用 Day 7 的評測系統檢驗它。先說結論:混合版 F1 = 0.388,是目前為止最差的版本。這篇文章的重點,是用實際檢測結果找出「為什麼更差」,並決定生產環境到底該用哪個版本。
第 1 週:讓 SRS 從黑盒變透明 → Day 1-7 完成架構、分塊、工作流與評測系統,Day 7 基線 F1 = 0.789
第 2 週:檢測優化與生產系統 → Day 8 規則擴展、Day 9 智能分析,今天 Day 10 嘗試混合方案
讀完這篇後,你會完成:
HybridConflictDetector(src/hybrid_detector.py)預期結果:平均 Precision 0.667、Recall 0.339、F1 0.388(比 Day 9 的 0.524 下降 26.0%)
回顧前三天:
| 版本 | 做法 | 問題 |
|---|---|---|
| Day 7 | 少量精準規則 | 規則數量少,覆蓋有限 |
| Day 8 | 大量擴展規則 | 誤報大增,Precision 0.722 |
| Day 9 | 領域分類 + 語義相似度 | Recall 只有 0.467 |
混合方案的假設是:
這個假設聽起來合理,但它有沒有成立,要交給評測系統判斷。
constraints(約束清單)
↓
層 1:簡單規則 ──────────────┐ 多用戶/單用戶、加密/明文、刪除/永久保存
↓ │
層 2:同領域數值衝突 → 去重 ──┤ 儲存大小差 > 5 倍、可用性差 > 0.5%
↓ │
層 3:置信度過濾(≥ 0.65)←──┘
↓
List[Conflict]
List[Dict],每筆含 id 與 text(格式與 Day 8、Day 9 的 detector 相同)List[Conflict],每個衝突帶 confidence 與 detection_method(simple / numeric)src/hybrid_detector.py(新增)tests/test_day10_hybrid.py 把結果轉成 Day 7 的 DetectedConflict 交給 Evaluator 評分新增後的專案結構:
srs-review-agent/
├── src/
│ ├── evaluation.py ← Day 7 評測系統(沿用)
│ ├── conflict_detector.py ← Day 8
│ ├── smart_detector.py ← Day 9
│ ├── hybrid_detector.py ← 【新增】Day 10 混合檢測器
│ └── ...
└── tests/
├── test_day10_hybrid_unit.py ← 【新增】單元測試
└── test_day10_hybrid.py ← 【新增】三組 SRS 評測
今天只用到 Python 標準庫(dataclasses、enum、re),不需要安裝新的 pip 套件。
建立 src/hybrid_detector.py,先定義衝突的資料結構:
from dataclasses import dataclass
from typing import Dict, List, Optional
from enum import Enum
import re
class Severity(str, Enum):
HIGH = "高"
MEDIUM = "中"
LOW = "低"
@dataclass
class Conflict:
req_id_1: str
req_id_2: str
conflict_type: "ConflictType" # 邏輯矛盾 / 安全性衝突 / 不兼容 ...
severity: Severity
description: str
confidence: float # 0.0-1.0 的置信度
detection_method: str # simple / numeric
和 Day 8 的 Conflict 相比,多了 confidence 與 detection_method 兩個欄位:前者給層 3 過濾用,後者讓我們在評測時看出每一層各貢獻了多少結果。ConflictType 列舉的定義與 src/conflict_detector.py 相同,完整代碼見 src/hybrid_detector.py。
在同一個檔案中加入 HybridConflictDetector:
class HybridConflictDetector:
def __init__(self):
self.conflicts: List[Conflict] = []
def detect_conflicts(self, constraints: List[Dict],
min_confidence: float = 0.65) -> List[Conflict]:
self.conflicts = []
# 層 1:簡單規則(高精度)
self.conflicts.extend(self._detect_simple_conflicts(constraints))
# 層 2:同領域數值衝突,已被層 1 報過的需求對不再重複加入
for conflict in self._detect_numeric_conflicts(constraints):
if not self._is_duplicate(conflict, self.conflicts):
self.conflicts.append(conflict)
# 層 3:置信度過濾
return [c for c in self.conflicts if c.confidence >= min_confidence]
層 1 放在最前面,是因為它的置信度最高(0.90-0.95);層 2 的結果只有在「這對需求還沒被層 1 報過」時才會加入。每次呼叫都會重設 self.conflicts,所以同一個 detector 實例可以重複使用。
_detect_simple_conflicts() 對所有需求兩兩比對,以「加密 vs 明文」為例:
text1 = c1.get("text", "").lower()
text2 = c2.get("text", "").lower()
# 規則 2:加密 vs 明文(雙向檢查)
if (("加密" in text1 or "encrypt" in text1) and
("明文" in text2 or "plaintext" in text2)) or \
(("加密" in text2 or "encrypt" in text2) and
("明文" in text1 or "plaintext" in text1)):
conflicts.append(Conflict(
req_id_1=c1.get("id", "unknown"),
req_id_2=c2.get("id", "unknown"),
conflict_type=ConflictType.SECURITY,
severity=Severity.HIGH,
description="加密與明文存儲衝突",
confidence=0.95,
detection_method="simple",
))
另外兩條規則結構相同:「多用戶 / 單用戶」(置信度 0.95)與「刪除 / 保存」(置信度 0.90)。注意這裡只做關鍵字共現,不看否定詞,這一點稍後會變成誤報的主要來源。
_detect_numeric_conflicts() 先呼叫 _are_related_domains() 過濾,只有兩條需求都命中同一組關鍵字(儲存:mb/gb/容量;性能:ms/req/s/並行;可用性:99/%)才繼續比數值:
@staticmethod
def _check_numeric_mismatch(c1, c2, text1, text2) -> Optional[Conflict]:
nums1 = re.findall(r'(\d+(?:\.\d+)?)', text1)
nums2 = re.findall(r'(\d+(?:\.\d+)?)', text2)
if not (nums1 and nums2):
return None
v1, v2 = float(nums1[0]), float(nums2[0])
# 儲存:差異 > 5 倍 → 置信度 0.75
if ("mb" in text1 or "gb" in text1) and ("mb" in text2 or "gb" in text2):
if v1 != v2 and max(v1, v2) / min(v1, v2) > 5:
return Conflict(..., description=f"儲存大小衝突:{v1} vs {v2}",
confidence=0.75, detection_method="numeric")
# 可用性:差異 > 0.5 個百分點 → 置信度 0.70(寫法同上)
return None
這段是示意片段(... 省略了與層 1 相同的欄位),完整代碼見 src/hybrid_detector.py。要留意兩件事:
100MB 和 1GB 被當成 100 vs 1 來比)_is_duplicate() 以「無方向的需求對」判斷重複,(A, B) 與 (B, A) 視為同一對:
@staticmethod
def _is_duplicate(conflict: Conflict, existing: List[Conflict]) -> bool:
pair = {conflict.req_id_1, conflict.req_id_2}
return any({e.req_id_1, e.req_id_2} == pair for e in existing)
這是等價的精簡寫法;src/hybrid_detector.py 中用明確的雙向比對實作,行為相同。
tests/test_day10_hybrid_unit.py 涵蓋 5 個測試:簡單規則、數值衝突、去重、置信度過濾、三層整合。
cd srs-review-agent
python3 -m pytest tests/test_day10_hybrid_unit.py -v
預期輸出:
tests/test_day10_hybrid_unit.py::test_simple_rules PASSED [ 20%]
tests/test_day10_hybrid_unit.py::test_numeric_rules PASSED [ 40%]
tests/test_day10_hybrid_unit.py::test_deduplication PASSED [ 60%]
tests/test_day10_hybrid_unit.py::test_confidence_filtering PASSED [ 80%]
tests/test_day10_hybrid_unit.py::test_three_layer_integration PASSED [100%]
============================== 5 passed in 0.01s ===============================
注意:測試檔開頭用
Path(__file__).resolve().parent.parent把專案根目錄加入sys.path,所以不論專案放在哪個路徑都能直接執行,不需要手動設定PYTHONPATH。
tests/test_day10_hybrid.py 使用 Day 7 src/evaluation.py 中的 GROUND_TRUTH_TEST_1/2/3(電商、醫療、社交媒體)評分:
python3 tests/test_day10_hybrid.py
輸出重點:
平均指標(3 個測試用例):
平均精確度:0.667
平均召回率:0.339
平均 F1 分數:0.388
版本 Precision Recall F1 vs Day 7
───────────────────────────────────────────────────────
Day 7 簡單規則 0.850 0.739 0.789 基線 ⭐
Day 8 規則擴展 0.722 0.422 0.468 -40.7%
Day 9 智能分析 0.651 0.467 0.524 -33.6%
Day 10 混合方案 0.667 0.339 0.388 -50.8%
按領域拆開看(F1,Day 7 → Day 10):
| 測試集 | Day 7 | Day 10 | TP / FP / FN | 變化 |
|---|---|---|---|---|
| 電商平台 | 0.750 | 0.333 | 1 / 1 / 3 | -55.6% |
| 醫療系統 | 0.727 | 0.286 | 1 / 0 / 5 | -60.7% |
| 社交媒體 | 0.889 | 0.545 | 3 / 3 / 2 | -38.7% |
Precision 其實比 Day 9 略高(0.651 → 0.667),真正崩掉的是 Recall(0.467 → 0.339)。
光看 F1 只知道「變差」,要知道「為什麼」,得把每一筆檢測結果和標準答案對照。
醫療系統的標準答案有 6 個衝突,混合版只抓到「病歷加密 vs 明文存儲」這 1 個:
| 漏掉的衝突 | 為什麼沒抓到 |
|---|---|
| 「多個主治醫生」vs「僅一個醫生可編輯」 | 規則只認「多用戶 / 單用戶」,不認「多個醫生 / 單醫生」 |
| 「僅一個醫生可編輯」vs「單醫生操作設計」 | 同上 |
| 「最多 50 名患者」vs「最多 1 名患者」 | 沒有命中任何領域關鍵字,層 2 直接跳過 |
| 「100 並行用戶」vs「1000 並行用戶」 | 通過了「性能」領域過濾,但數值比較沒有實作性能類型 |
| 「患者數據可公開訪問」vs「HIPAA 認證」 | 需要領域知識,關鍵字規則無法表達 |
層 2 原本是設計來補 Recall 的,但在三組資料中總共只產生 1 個結果(社交媒體的 100MB vs 1GB),幾乎沒有發揮作用。
所有 4 個誤報都來自層 1 的關鍵字共現:
| 誤報 | 問題 |
|---|---|
| 「支持明文顯示訂單細節」vs「不需要加密用戶的個人信息」 | 兩者都偏向不加密,其實不衝突 |
| 「帖子內容採用明文存儲」vs「不支持任何加密機制」 | 同上 |
| 「帖子內容採用明文存儲」vs「支持端到端加密」 | 描述的對象不同(存儲 vs 傳輸),不在標準答案內 |
| 「30 天內自動刪除」vs「數據永久保存」 | 分屬離線消息與帳戶資料,不是同一份資料 |
「加密」這個關鍵字同時出現在「必須加密」和「不需要加密」中,規則分不出兩者的差別。
直覺上會想:誤報多,那就把 min_confidence 調高。實際掃一次門檻(三組資料平均):
| min_confidence | Precision | Recall | F1 |
|---|---|---|---|
| 0.00 | 0.667 | 0.339 | 0.388 |
| 0.65(預設) | 0.667 | 0.339 | 0.388 |
| 0.80 / 0.90 | 0.633 | 0.272 | 0.340 |
| 0.95 | 0.611 | 0.206 | 0.290 |
0.65 和 0.00 結果完全相同,代表預設門檻沒有過濾掉任何東西。門檻調到 0.80 以上,被濾掉的反而是層 2 那個正確的「100MB vs 1GB」數值衝突,而誤報(置信度 0.90-0.95)全部留下。原因是置信度是依規則寫死的,不是依據這筆檢測本身有多可信,所以門檻無法分辨對錯。
去重機制本身運作正常(單元測試通過),但因為層 2 幾乎沒有產出,去重在這份資料上沒有機會發揮。
| 設計假設 | 實際情況 |
|---|---|
| 層 1 保 Precision | 關鍵字共現不看否定詞,產生 4 個誤報 |
| 層 2 補 Recall | 三組資料只產出 1 個結果 |
| 層 3 濾掉低品質結果 | 置信度按規則寫死,門檻無法分辨對錯 |
三層各自的弱點疊加,而不是互補。這也說明了關鍵字規則的天花板:它無法理解否定、描述對象和領域知識。
根據評測數據,生產環境不採用 Day 10 混合版。決策依據很單純:在同一組評測資料上,Day 7 的 F1 = 0.789 仍是最高分。
但這裡有一個需要誠實面對的限制:Day 7 的基線分數來自 tests/test_day7_evaluation.py 中的檢測結果,目前專案裡並沒有一個名為「Day 7 detector」、可以直接 import 的類別。可以實際呼叫、並支援 min_confidence 參數的,是今天的 HybridConflictDetector。
所以短期的部署策略是:
SRS 檔案
↓
約束提取(Day 5 LangGraph 工作流)
↓
HybridConflictDetector.detect_conflicts(min_confidence=0.65)
↓
衝突報告(類型、嚴重度、置信度、檢測方法)
↓
人工複核(特別是含「不」「無」等否定詞的配對)
在服務端程式中這樣使用(src/hybrid_detector.py 提供的公開介面):
from src.hybrid_detector import HybridConflictDetector
detector = HybridConflictDetector()
conflicts = detector.detect_conflicts(constraints, min_confidence=0.65)
for c in conflicts:
print(f"{c.req_id_1} ↔ {c.req_id_2}:{c.description}"
f"({c.confidence:.0%},{c.detection_method})")
constraints 需要是 [{"id": ..., "text": ...}, ...] 格式;缺少欄位時會以 "unknown" / 空字串代替,不會拋出例外min_confidence 維持 0.65:根據上面的門檻掃描,調高只會降低 F1detection_method,讓審查者知道哪些結果來自數值規則、哪些來自關鍵字規則git add src/hybrid_detector.py tests/test_day10_hybrid.py tests/test_day10_hybrid_unit.py
git commit -m "Day 10: 實現三層混合衝突檢測器與評測"
今天的逐筆分析指出,漏報與誤報的根源都是「規則看不懂語義」。明天 Day 11 我們換個方向:保留簡單規則做第一層快速篩選,再引入 LLM(本地 Ollama Mistral 7B)驗證有爭議的衝突、補充規則漏掉的隱性衝突,看看能不能突破 Day 7 的 0.789。