前兩篇我們聊過 vLLM Decision 3.0 地端實測 與 來訓練自己的 Jev 模型吧:),大家對 Jev 與非自回歸決策架構應該都不陌生了。
但回到日常開發的現場,痛點往往更現實:每次 GitLab CI 或 GitHub Actions 跑完 Trivy 容器掃描,吐出一整大坨 CVE 清單,如果無腦拿去餵一般 LLM,寫一堆「請輸出包含 severity、attack_vector、remediation_sla 的 JSON 格式」,後果通常很慘烈。
當模型收到文字要先跑 Prefill,接著解碼 token 吐括號、鍵值和雙引號,而遇到新漏洞沒有明確 Patch 時,模型常在解碼階段胡思亂想,直接把下游的 Webhook 或自動化腳本炸爛。
設計決策目標
這次選用的標準資料集是美國國家漏洞資料庫的 harinpurumandla/cve-severity-dataset,收錄超過 15 萬筆資料。每筆資料包含原始 CVE 描述與標準 CVSS v3.1 向量字串(例如 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)。
我們寫了一套專屬的 CVSS 解析邏輯,直接從向量欄位抓取特徵,並將整體分類任務拆解為 3 個工程維運決策頭:
-
嚴重度分類 (
severity):critical、high、medium、low四個等級。 -
攻擊路徑 (
attack_vector):network(網路攻擊)、adjacent(相鄰網路)、local(本機攻擊)、physical(實體接觸)。 -
修補 SLA 期限 (
remediation_sla):資安應變中心(SOC)的處理規範,包含4h_emergency(4 小時內緊急處置)、24h_hotfix(24 小時內上熱修補)、next_sprint(併入下個迭代處理)。
把這 3 個欄位封裝成 Decision Head 定義,整個架構就成了標準的多工分類器。模型完全不需要花心力學習怎麼生逗號或引號,只要在潛在空間把注意力集中在描述中的關鍵漏洞 Pattern 即可:
# 3 個決策頭的題目與選項結構
questions = {
"severity": {
"question": "What is the severity of this vulnerability?",
"choices": ["critical", "high", "medium", "low"],
},
"attack_vector": {
"question": "What is the attack vector of this CVE?",
"choices": ["network", "adjacent", "local", "physical"],
},
"remediation_sla": {
"question": "What is the remediation SLA policy?",
"choices": ["4h_emergency", "24h_hotfix", "next_sprint"],
},
}
# 單次 Forward Pass 同步推論 3 個頭(僅 296 ms)
prediction = FastDecisionModel.predict(
model, tokenizer, state=cve_description, questions=questions
)
# 輸出結果保證合法,包含各頭的信心度分數:
# {
# "severity": {"label": "critical", "confidence": 0.884},
# "attack_vector": {"label": "network", "confidence": 0.921},
# "remediation_sla": {"label": "4h_emergency", "confidence": 0.852}
# }
實測單卡訓練
測試硬體是單張 NVIDIA GeForce RTX 3090 24GB,軟體環境為 PyTorch 2.6.0、CUDA 12.4 與 Unsloth 2026.10.3,底層模型挑選 unsloth/Qwen3.5-4B,載入 4-bit 量化權重,並設定 LoRA rank 16、alpha 16。
我們從資料庫抽取 1,000 筆作為訓練集,250 筆作為完全沒見過的獨立測試集。在微調前,我們先對驗證集進行 Zero-shot 評估,未訓練的隨機決策頭在 3 個目標上的綜合準確率只有 29.0%,而且 3 個頭同時猜對的機率直接見底掛零。
訓練過程:
Step 10: Loss = 1.1505 | Grad Norm = 1.76 | LR = 1.80e-4
Step 20: Loss = 0.8963 | Grad Norm = 1.17 | LR = 1.84e-4
Step 30: Loss = 0.7319 | Grad Norm = 10.32| LR = 1.36e-4
Step 40: Loss = 0.7806 | Grad Norm = 5.99 | LR = 7.51e-5
Step 50: Loss = 0.7162 | Grad Norm = 2.35 | LR = 2.29e-5
Step 60: Loss = 0.6414 | Grad Norm = 1.05 | LR = 1.97e-7
整整 60 個 Step 總共只跑了 547.8 秒(約 9.1 分鐘),最驚訝的是訓練過程中的 VRAM 峰值只有 7.453 GB,這也代表可以在同張卡背景掛另一個小模型做監控,VRAM 依然綽綽有餘。
訓練完成後重新驗證:
- 驗證集總體準確率從 29.0% 暴增到 75.67%,提升 +46.67%。
- 3 個決策全部同時命中的比例,直接從 0% 飆升到 55.0%。
在完全未參與訓練與校準的 250 筆獨立測試集(Held-out Test Set)上,模型拿到了 52.67% 的準確率,3 個決策頭同時命中率為 26.8%,對於一個只餵了不到 1,000 筆資料、跑不到 10 分鐘的 4B 模型來說,它已經能精準抓出 CVE 描述裡的關鍵攻擊特徵,應該已經很有潛力繼續往下探索了。
推論測試
模型訓練出來之後,上不上線完全取決於推論延遲速度。
我們抽取 50 筆測試案例讓微調後的 Decision Model 推論,因為是單次 Forward Pass 同時算完 3 個分類頭,50 筆全部跑完只花了 14.8 秒,平均每筆樣本耗時 296.2 毫秒。
為了公平比對,我們載入同樣是 4-bit 量化的 unsloth/Qwen3.5-4B 標準語言模型(FastLanguageModel),使用系統 Prompt 要求它解析相同 CVE 並輸出乾淨的 JSON 欄位。
在同樣的GPU上,生成平均產生 60 個 tokens 就花了 4,076.28 毫秒。
| 評測維度 | 傳統生成式 LLM (JSON Prompt) | Unsloth 決策模型 (FastDecisionModel) |
|---|---|---|
| 單次推論延遲 | 4,076.28 ms | 296.20 ms |
| 推論吞吐量 (Throughput) | ~0.24 筆/秒 | ~3.38 筆/秒 |
| 訓練 VRAM 峰值 | ~14-16 GB (全量文字微調) | 7.453 GB (LoRA + 決策頭) |
| 結構化保證 | 依賴 Regex 驗證或重試 | 100% 型別與合法值約束 |
| 輸出內容 | 自回歸生成 token 字串 | 各決策頭 Logits 與信心度數值 |
296 毫秒對 CI/CD 與資安通報意味著什麼?過去每分鐘只能處理 14 筆通報的單卡 Worker,換成決策模型後每分鐘可以消化超過 200 筆。當 Pipeline 掃完幾十個容器映像檔、湧入幾百筆 CVE 需要快速分流時,這 13 倍的差距就是 Pipeline 在 10 秒內跑完跟直接Timeout的差別。
執行溫度校準:讓決策頭信心度貼近真實防禦門檻
決策模型還有一個生成式 LLM 難以望其項背的優勢:機率校準。
生成式模型說「我 90% 確定這是 Critical」,通常只是當下 token 預測機率產生的假象,但決策模型輸出的是標準 Softmax 分佈,這讓我們可以透過驗證集計算溫度係數(Temperature Scaling)來縮放各決策頭的 Logits,讓模型回傳的信心度能真實對應到現實世界的命中機率。
實測結果滿有趣的,在 250 筆未參與微調的獨立測試集上,校準前的預期校準誤差(ECE, Expected Calibration Error)為 0.1457,但經過 100 筆驗證集算出來後,ECE 反而微幅飄到了 0.1799。
當驗證集只有 100 筆時,溫度參數容易過度貼合該小樣本的特定邊界,一旦推論到真實分佈稍有偏移的外部分佈(OOD)測試集,就會造成輕微的「過度平滑(Over-smoothing)」,這也告訴我們在正式環境裡,拿來算校準溫度的驗證集規模至少要拉到 500 到 1,000 筆或更多才夠讓表現更穩健。
不過即使是 0.1799 的水準,也足以讓我們在自動化流程中設定合理的門檻:例如「只有當 severity 信心度超過 0.85 時才自動觸發 PagerDuty 或開 Jira Ticket,其餘降級為人工二次檢核」,避免日常工作被垃圾告警疲勞轟炸。
盤點實作踩坑
在將 Qwen3.5 移植為決策模型的過程中,有兩個 Note:
第一,ClefDecisionModel 沒有 .generate() 方法。當你用 FastDecisionModel.from_pretrained() 載入模型時,Unsloth 會把 Transformer Backbone拔掉底層的 lm_head 改接 JointClassifierHead,如果你想在同一個物件上呼叫 .generate() 測試生成,程式會直接拋出 AttributeError 錯誤給你看,要進行生成式比對,還是必須另外透過 FastLanguageModel 載入標準權重。
第二,Qwen3.5 的 Processor 輸入格式要求。Qwen3.5 預設載入的是多模態的 Qwen2_5_VLProcessor,在呼叫 Tokenizer 時,如果直接寫 tokenizer(prompt),在某些版本的 Hugging Face 套件中,會被誤判成帶有未指定影像特徵的參數,請明確指定關鍵字參數:tokenizer(text=prompt, return_tensors="pt"),才能保證單純的文字輸入正確被轉換。
評估架構選型:高併發 AIOps 該選生成還是決策?
當所有人都在談 Agent、都在想著怎麼讓模型用 Function Calling 寫 JSON 時,我們常常忘了工程的核心是「用最低成本、最快速度、最高可靠度解決問題」。
文字生成模型適合寫長篇分析、出滲透測試報告,但面對 Trivy 掃出來成百上千筆的 CVE 分流、Log 等級降噪、SRE 告警過濾這些需要確定性的日常雜工,把它們丟進一般的 LLM 根本是大砲打蒼蠅,還很容易給未來的自己挖坑。
而簡單訓練過後的決策模型,不到 10 分鐘的訓練時間、7.5 GB 的顯卡記憶體,換來的是 296 毫秒的極速反應與 100% 格式合規,在追求吞吐量與極致穩定的 CI/CD 現場,這才是把大模型落地最乾淨、最不折磨人的工程實踐。
Top comments (0)