文章使用包含15万余条记录的 CVE 数据集,从 CVSS 向量提取标签,训练严重度、攻击路径和修补时限三个分类头。方案将漏洞分流建模为分类任务,减少生成式 JSON 输出对下游自动化的格式干扰。
前两篇我们聊过《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 毫秒。
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 条,甚至更多,才能让表现更加稳健。
不过,即使 ECE 为 0.1799,也足以让我们在自动化流程中设置合理的阈值。例如,“只有当 severity 的置信度超过 0.85 时,才自动触发 PagerDuty 或创建 Jira Ticket,其余交由人工二次核查”,避免日常工作被垃圾告警轰炸,陷入告警疲劳。
在将 Qwen3.5 改造成决策模型的过程中,有两点需要注意:
第一,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"),才能确保纯文本输入被正确转换。
当所有人都在谈 Agent,都在琢磨如何让模型通过 Function Calling 写 JSON 时,我们常常忘了,工程的核心是“以最低成本、最快速度和最高可靠性解决问题”。
文本生成模型适合写长篇分析、生成渗透测试报告。但面对 Trivy 扫出来的成百上千条 CVE 分流、Log 级别降噪、SRE 告警过滤这些需要确定性的日常杂活,把它们丢给一般的 LLM,实在是大炮打苍蝇,还很容易给未来的自己挖坑。
而经过简单训练的决策模型,只需不到 10 分钟的训练时间、7.5 GB 的显存,就能换来 296 毫秒的快速响应和 100% 的格式合规。在追求吞吐量和极致稳定的 CI/CD 现场,这才是让大模型落地最干净、最不折腾人的工程实践。
如需采取进一步措施,可以考虑屏蔽此人和/或举报滥用行为。