想直接上自动处置的团队,卡点通常不在模型能不能判断,而在于没人能证明初筛在这批告警上比人工稳定。把模型放进流程之前,有三件事必须先落地:这批告警的现状基线、处置动作的分级边界、以及一个随时能关掉的试点范围。缺任何一项就先补数据,而不是继续调提示词。
先别把处置权交给模型。用两到四周的真实告警算清日均量、误报口径和人工耗时,把动作分成可逆与不可逆两段,模型只在可逆区间出建议;再用样本覆盖、结论可解释、输出可解析、复核通道四条门槛卡住上线,小范围试点并预设回滚条件。任一条不满足,先补数据和流程,不要放量。
先算清这批告警的现状基线
没有基线,“初筛更准”就只是主观感受,后面所有门槛都没法设。基线通常取最近两到四周的真实告警,按告警源和规则 ID 分开统计,节假日、活动期单独标注,否则日均量会被个别高峰拉偏。下面是一个通用聚合骨架,字段名按你自己的日志结构替换:
# 按日期 + 规则统计告警条数,用于确认日均量和长尾规则
jq -r '[.date, .rule_id] | @tsv' alerts.ndjson \
| sort | uniq -c | sort -k2,2
# 只看某个规则最近 7 天,用于抽样人工复核
jq -r 'select(.rule_id=="RULE_ID") | [.ts, .src_ip, .asset] | @tsv' alerts.ndjson \
| tail -n 200
已确认误报的口径建议提前写死:需要两条以上人工复核确认,并且能指出是哪条规则在什么上下文里被误触发,才计入“已确认误报”;只是“看着不像攻击”的,归入“待定”。待定和误报混在一起统计,门槛就会被算歪。
人工处理耗时只记方法,不猜数值:有工单的,取“首次响应时间”到“关闭时间”的差值;没有工单的,用告警平台自身的状态变更时间戳计算,并按规则 ID 分组看分布。耗时的中位数和长尾比平均值更能说明哪些规则值得先交给初筛。
把处置动作分级,不让模型碰不可逆操作
分级要在接模型之前做完,否则接进来以后没人说得清哪些动作是“模型建议的”。建议按可逆性切分,而不是按严重程度:
| 级别 | 典型动作 | 可逆性 | 模型参与方式 |
|---|---|---|---|
| L0 通知 | 写工单、发消息、记录结论 | 可逆 | 可自动生成内容 |
| L1 取证 | 拉日志、查资产归属、抓取进程信息 | 可逆 | 可自动触发 |
| L2 临时处置 | 限流、短时封单 IP、会话踢下线、打标隔离 | 可逆,有失效时间 | 只出建议,人工确认后执行 |
| L3 不可逆 | 删账号、清数据、下线业务、批量改配置、永久封禁 | 不可逆 | 不参与,模型不输出此类动作 |
L2 的关键是动作本身带失效时间,并且能一条命令撤回;做不到这点的,都算 L3。模型侧只让它输出“结论 + 建议动作 + 依据字段”的结构化结果,执行层单独写权限判断,看到 L2 以上动作一律转人工,不要指望提示词里写“不要执行”来兜底。
给初筛设上线门槛
把“感觉能用”换成四条可检查的条件,每条都要有对应的验证动作:
- 样本覆盖:基线期内的每条规则都至少被抽到若干条真实告警跑过初筛,长尾规则单独列出。验证方式是从告警表里按规则 ID 抽样重放,而不是只挑几条典型告警演示。
- 结论可解释:模型给出的判定必须引用输入中的具体字段(规则名、时间、资产、关联告警条数)。验证方式是人工抽查若干条,删掉 reason 里的泛泛描述后看是否还站得住。
- 输出可解析:要求固定 JSON 结构,任何多余文字都视为失败并落到人工队列。验证方式是用下面这类校验函数批量跑一遍历史输出。
- 复核通道:每一条模型结论都必须能回看到原始告警并一键改判。验证方式是让不在项目里的人按流程走一遍改判。
import json
REQUIRED = {"alert_id", "verdict", "reason", "suggested_action", "evidence"}
# 输入:模型原始输出字符串;返回:(是否可用, 原因)
def check(raw: str):
try:
obj = json.loads(raw)
except json.JSONDecodeError as e:
return False, f"unparsable: {e}"
missing = REQUIRED - obj.keys()
if missing:
return False, f"missing_field: {sorted(missing)}"
if obj["verdict"] not in {"true_positive", "false_positive", "need_human"}:
return False, "bad_verdict"
if not obj.get("evidence"):
return False, "no_evidence"
return True, "ok"
提示词按这个输出契约写,模型名和参数以你实际可用的接口为准,下面只是输入输出骨架:
你是告警初筛助手,只做判断,不执行任何处置动作。
输入:告警原文、触发规则、前后 5 分钟内同类告警条数、资产标签。
输出严格 JSON,不要任何多余文字:
{"alert_id":"...",
"verdict":"true_positive|false_positive|need_human",
"reason":"不超过两句话,必须引用输入中的具体字段",
"suggested_action":"none|notify|collect_context",
"evidence":["字段名=值"]}
约束:无法从输入中确认的内容一律填 need_human,不要推测;
不要输出封禁、删除、下线类动作。
安排人工兜底的位置
兜底不是“模型拿不准就通知一声”,而是要有队列、有时限、有默认动作。
复核队列建议按风险而不是按时间组织:L2 相关结论、need_human 结论、以及解析失败的原始输出各排一个队列,按告警级别排序,值班人按队列顺序处理。每条记录要带上原始告警链接、模型结论、依据字段和触发规则,避免复核人自己再去翻日志。
超时未处理的处理规则提前定好:超过约定时限仍无人复核的,维持告警原有状态并升级到上一级值班,不做自动处置。宁可多留一条待处理告警,也不要在无人复核时执行阻断类动作。
模型输出缺失时的默认动作写死为 need_human:接口超时、返回非 JSON、字段缺失、evidence 为空,四种情况都直接进人工队列,不重试到成功为止,也不套用上一次的结论。这条默认规则要能在告警平台里直接看到,而不是只写在文档里。
小范围试点并留下回滚开关
试点范围按“一个维度”收窄,不要同时切多个维度:只接单一告警源、或只接某一组规则、或只在业务低峰时段启用,三者选其一先跑,观察没问题再扩。范围在配置里以白名单形式存在,扩量就是往白名单里加条目,回滚就是清空白名单,不要靠改代码回滚。
对照观察的指标沿用第一步的基线:同期误报数与待定数、人工复核改判模型结论的条数、单条告警的人工耗时、模型输出解析失败与超时的条数。人工耗时用同一套时间戳口径统计,才有可比性。
回滚触发条件写成可执行判断,阈值按第一步得到的基线填,不要拍脑袋:
rollback_if:
- 连续 3 个观察窗内,人工改判模型结论的条数高于该规则基线误报数
- 连续 2 个观察窗内,模型输出解析失败或超时占比高于 <按基线设定的阈值>
- 出现任何一条 L3 动作被模型建议并进入执行链路(应为 0)
- 复核队列积压超过 <值班可承受条数> 且无人处理
触发回滚时只做一件事:清空白名单,让这批告警回到纯人工流程,同时保留模型输出用于离线比对。把误报压到能用基线解释的水平,再谈把 L2 动作交给自动化,顺序反了,试点就会变成一次误封事故的复盘材料。