简介:一份基于Python的Web漏洞智能检测系统毕业设计论文资源,适合具备Python基础、关注Web应用安全与漏洞扫描的网络安全从业者、高校学生,也可供企业IT安全部门作为日常安全检测方案参考。文档以Django为技术背景,围绕漏洞库建设、登录注册、漏洞检测、后台用户管理等模块展开,系统阐述了从需求分析、架构设计、功能实现到测试验证的完整流程,重点覆盖漏洞扫描、入侵检测、病毒检测与安全建议生成等关键能力,并涉及漏洞库更新机制。资源为单个docx文档,压缩包约239KB,包含中英文摘要、原创性声明、目录、章节正文及参考文献等完整论文框架,结构清晰便于直接参照。目前已有170人学习浏览,对快速理解智能漏洞检测系统实现思路、补充安全课题写作素材有较高参考价值。
1. 为什么“安全设计”要排在“漏洞检测”前面
把 Python 基 Web 漏洞智能检测系统做成“能用”,最短路径是爬虫加三五个检测插件;做成“敢用”,安全设计必须排在功能前面。这套系统运行期间握着三类敏感数据:目标 URL 集合、登录态 Cookie 与表单凭证、出报告用的漏洞证据。只要有一类在存储或传输时裸奔,检测系统就比扫描目标更好打。
检测请求与攻击请求在行为上高度相似,系统必须一边在授权范围内模拟探测,一边防止自己变成跳板。鉴权、限速、资源隔离、凭证加密、结果脱敏,都要在架构阶段定下来,而不是等上线评审再补。下文按架构边界、检测引擎、智能融合、调度与数据保护、验证调优的顺序展开,适合正在写 web 安全巡检工具、或想把渗透经验固化成自动检测流程的工程师;想少绕弯路的可以直接看第 5、6 章的限速与误报口径。
2. Python基Web漏洞智能检测系统的架构与安全边界划分
2.1 分层架构与进程模型:调度、引擎、插件、存储各归其位
先立一个能支撑多次重构的骨架,环境按 Python 3.10+ 准备。常见做法是把系统切成四层:调度层只负责从任务队列取目标和下发配置;引擎层持有 requests.Session,负责真实网络请求;插件层只写“给定一个目标与响应,给出判定”的纯函数;存储层统一管 SQLite、加密字段和报告导出。四层之间用函数签名和数据结构约束,不共享全局对象,任何插件异常都不会污染会话池。
目录结构按这个边界落盘:
scan_suite/ ├── scheduler/ # 任务编排、限速、失败重试 ├── engine/ # Session 构造、请求发送、响应归一化 ├── plugins/ # sqli.py / xss.py / ssrf.py,白名单加载 ├── ml/ # 特征提取、模型推理、模型热加载 └── store/ # sqlite 访问、凭证加解密、报告导出进程模型上我一般用“一主多从”:主进程只编排和限速,扫描任务交给 worker 进程,worker 内部再用线程池控制并发。线程池能复用连接池,避免每个请求都新建 TCP 连接;worker 进程则保证某个插件或响应解析卡死时,只杀掉那个任务,而不是整个扫描服务。这里的边界原则是:插件永远拿不到 Session 对象和明文凭证,只能拿到目标 URL、脱敏后的参数和一次 fetch 回调。
2.2 数据流与攻击面收口
数据流是单向的:目标 JSON 从队列进入 worker,引擎发出请求,响应文本先归一化,再分发给插件和特征提取器,最后融合打分入库。每一跳都是信任边界,边界上只交换 JSON 结构体,不交换函数对象。攻击面则要单独盘点:管理面板端口、Redis 端口、模型文件路径、插件文件路径,四处是默认入口。管理面板默认只监听 127.0.0.1,用 Flask 或 Dash 实现都可以,但绝不能监听 0.0.0.0;Redis 用独立库和独立密码。
插件目录做白名单注册,在plugins/__init__.py里维护:
# plugins/__init__.py ALLOWED_PLUGINS = ("sqli", "xss", "ssrf", "path_traversal") def load_plugin(name: str): if name not in ALLOWED_PLUGINS: raise PermissionError(f"plugin {name} not allowed") mod = __import__(f"plugins.{name}", fromlist=[name]) return mod白名单的意义在于:即使扫描过程中目录被写入一个名字正常的脚本,只要没在 ALLOWED_PLUGINS 注册,调度层就不会 import 它。插件结果统一走 JSON 序列化并限制返回字段名,防止插件把任意对象塞进结果,带偏存储层的字段结构。
2.3 沙箱隔离与资源上限:插件为什么不裸跑在主进程里
插件代码质量参差,有的读文件不关句柄,有的正则写成灾难性回溯。把插件放进主进程线程,等于把扫描服务的可用性押在第三方代码上。常见处理方式有三种,对比如下:
| 执行方式 | 隔离强度 | 资源管控 | 部署成本 | 适用场景 |
|---|---|---|---|---|
| 线程内直接调用 | 无 | 无 | 零 | 内部白名单插件的快速验证 |
| 子进程 + resource 限制 | 中 | 内存、CPU 时间、文件数 | 低 | 默认推荐 |
| 容器进程 | 强 | 完整 cgroup 配额 | 高 | 高对抗、跑不可信插件 |
默认方案是子进程加资源限制。worker 入口独立成一个脚本,主进程用 subprocess 调用并设置 RLIMIT 参数:
# worker.py —— 每个插件独立进程的执行入口 import json import resource import sys def limit_resources(mem_mb: int = 256, max_fds: int = 64) -> None: resource.setrlimit(resource.RLIMIT_AS, (mem_mb * 1024 * 1024,) * 2) resource.setrlimit(resource.RLIMIT_NOFILE, (max_fds,) * 2) resource.setrlimit(resource.RLIMIT_CPU, (30, 30)) def main() -> None: limit_resources() plugin_name, target_json = sys.argv[1], sys.argv[2] from plugins import load_plugin # 白名单加载 result = load_plugin(plugin_name).check(json.loads(target_json)) print(json.dumps(result)) if __name__ == "__main__": main()主进程侧做超时与返回处理:
import json import subprocess import sys def run_plugin_sandboxed(plugin_name: str, target: dict, timeout: int = 30) -> dict: try: proc = subprocess.run( [sys.executable, "worker.py", plugin_name, json.dumps(target)], capture_output=True, timeout=timeout, check=False, ) except subprocess.TimeoutExpired: return {"plugin": plugin_name, "timeout": True} if proc.returncode != 0: return {"plugin": plugin_name, "error": proc.stderr.decode()[:200]} return json.loads(proc.stdout.decode())三个参数是必调的:RLIMIT_AS 限制地址空间,防止插件一次性分配海量内存;RLIMIT_CPU 限制 CPU 时间,兜住死循环;subprocess 的 timeout 负责硬杀。插件执行时间不均匀是常态,timeout 建议按插件配置,比如时间盲注插件给 45 秒、普通探测给 15 秒,而不是全局一个值。另外,插件的中间产物不要以.py结尾写入磁盘,避免响应内容被后续阶段误当作代码执行。
3. 用 Python 实现 Web 漏洞检测引擎:请求构造与响应判定
3.1 基于 requests 的 Session 模板与超时重试参数
引擎层所有请求走同一个 Session 工厂,保证 UA、证书策略、重试策略一致。连接池复用对漏洞检测尤其重要:同一站点要在一个会话里发几十个探测请求,复用 TCP 连接能把 RTT 影响降到最低,否则时间盲注这类判定会被握手延迟干扰。
import requests import urllib3 from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry urllib3.disable_warnings() # 检测阶段主动忽略证书告警 def build_session(retries: int = 2, backoff: float = 0.8) -> requests.Session: session = requests.Session() session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept": "text/html,application/json,*/*;q=0.8", }) session.verify = False # 证书校验交给策略层,不在引擎层丢弃请求 retry = Retry( total=retries, connect=retries, backoff_factor=backoff, status_forcelist=[502, 503, 504], allowed_methods=frozenset(["GET", "POST", "HEAD"]), ) adapter = HTTPAdapter(max_retries=retry, pool_connections=32, pool_maxsize=32) session.mount("https://", adapter) session.mount("http://", adapter) return session参数含义:total 和 connect 分别控制总重试次数与建连重试次数;backoff_factor 为 0.8 时重试间隔按 0.8、1.6、3.2 秒递增,避免目标恢复期间立刻打满;status_forcelist 只对 5xx 重试,4xx 重试没有意义。verify=False 是性能与可用性的权衡,真正的证书信任策略放在调度配置里,按目标环境决定是否指定 CA。读超时要单独用元组设置:session.request(method, url, timeout=(3.05, 12)),连接超时 3 秒、读超时 12 秒。
提示:探测时间盲注时,读超时必须大于最大 sleep 延时,否则 Timeout 异常会吞掉时间证据。一般把读超时配成“最大延时 x 2 + 3”秒。
3.2 SQL 注入与 XSS 检测插件的核心逻辑
插件接口要窄:接收url、params、fetch回调,fetch 由引擎注入。这样插件接触不到 Session 和 Cookie,只能按引擎给的规则发请求。SQL 注入检测先取基线响应,再做 true/false 对照探测:
# plugins/sqli.py import re import time ERROR_PATTERNS = [ re.compile(r"SQL syntax.*MySQL", re.I), re.compile(r"ORA-\d{5}", re.I), re.compile(r"PostgreSQL.*ERROR", re.I), ] class SqliPlugin: name = "sqli" def check(self, url: str, params: dict, fetch) -> dict: baseline_len = len(fetch(url, params).text) findings = [] for key in params: for payload, tag in ( ("1' AND '1'='1", "true"), ("1' AND '1'='2", "false"), ("1' OR SLEEP(3)--", "time"), ): t0 = time.time() resp = fetch(url, {**params, key: payload}) cost = time.time() - t0 if any(p.search(resp.text) for p in ERROR_PATTERNS): findings.append({"param": key, "type": "error_based"}) break if tag == "true" and abs(len(resp.text) - baseline_len) < 80: continue if tag == "false" and abs(len(resp.text) - baseline_len) > 200: findings.append({"param": key, "type": "boolean_based"}) break if tag == "time" and cost >= 2.8: findings.append({"param": key, "type": "time_based"}) break return {"plugin": self.name, "findings": findings}判定逻辑分三路:error_based 看数据库错误特征;boolean_based 看条件为真时响应长度贴近基线、为假时差异超 200 字节;time_based 看耗时是否达到 sleep 值附近。80 和 200 是经验阈值,换目标站点要可配置;目标页面如果本身带轮播数据,长度波动就会超阈值,这时要回到 3.3 的相似度比对。
XSS 插件关注回显,而不是匹配 payload 本身:
# plugins/xss.py import html as html_lib class XssPlugin: name = "xss" def check(self, url: str, params: dict, fetch) -> dict: marker = "qzv9x" findings = [] for key in params: payload = f"<script>alert('{marker}')</script>" resp = fetch(url, {**params, key: payload}) body = resp.text if payload in body: findings.append({"param": key, "type": "xss_raw", "risk": "high"}) elif marker in html_lib.unescape(body): findings.append({"param": key, "type": "xss_escaped_once", "risk": "medium"}) return {"plugin": self.name, "findings": findings}用随机 marker 而不是直接匹配script关键字,是因为很多框架会把尖括号转成实体再输出;html.unescape处理一次转义后仍能定位 marker,说明输出点存在二次注入可能。插件只上报 type 和 risk,不存响应全文,证据全文由引擎统一落盘,防止插件把大对象塞进队列。
3.3 响应归一化与页面相似度比对
布尔盲注最容易误报的场景是页面里有动态内容:时间戳、随机优惠券、会话令牌。归一化的目标是把这些噪声抹平后再算相似度:
# engine/normalize.py import difflib import re def normalize_html(raw: str) -> str: text = re.sub(r"<script.*?</script>", "", raw, flags=re.S | re.I) text = re.sub(r"\d{4,}", "NUM", text) text = re.sub(r"(?i)(session|token|csrf)[^\"'&\s]*=[^\"'&\s]*", r"\1=PH", text) return re.sub(r"\s+", " ", text) def similarity(a: str, b: str) -> float: return difflib.SequenceMatcher( None, normalize_html(a), normalize_html(b)).ratio()用法是:true/false 两次响应先算相似度,相似度大于 0.9 说明页面骨架没变,仅长度差异不构成盲注证据;低于 0.85 且长度差超阈值才记为差异。稀疏 diff 对 200KB 以上的响应页会比较慢,我一般先截断前 64KB 再比对。每种检测结果都带默认风险分,供第 4 章融合打分使用:
| 检测类型 | 判定依据 | 默认 risk_score |
|---|---|---|
| error_based | 数据库错误特征命中 | 0.95 |
| boolean_based | 页面骨架变化 + 长度差 | 0.80 |
| time_based | 响应耗时达到 sleep 延时 | 0.65 |
| xss_raw | payload 原样回显 | 0.90 |
| xss_escaped_once | 实体转义一次后可还原 marker | 0.55 |
time_based 分最低,因为受网络环境影响大;xss_escaped_once 只代表存在回显点,是否可执行要人工确认,所以分也压得低。
4. 智能检测模块:规则引擎与机器学习二分类的融合判定
4.1 从请求与响应中提取的 12 维特征
规则引擎擅长解释性,但阈值一多容易误报叠加。智能检测模块的作用是给规则结论加一道语义校验器:把每次探测总结成固定维度的特征向量,交给一个轻量二分类模型判断“这是漏洞证据还是普通页面波动”。两路输出融合,规则负责召回,模型负责压误报。
特征要在响应归一化之前提取,这一点不能省:归一化会抹掉证据,特征必须取原始值。默认 12 维如下:
| 特征 | 取值 | 说明 |
|---|---|---|
| payload_len | 数值 | payload 原始长度 |
| special_char_ratio | 0~1 | 引号、尖括号、分号占比 |
| sql_keyword_hits | 整数 | SELECT/UNION/SLEEP 命中次数 |
| html_tag_density | 0~1 | 响应中标签字符占比 |
| error_pattern_hits | 整数 | 数据库错误特征命中数 |
| response_len_delta | 整数 | 与基线长度差 |
| similarity_to_baseline | 0~1 | 归一化后相似度 |
| script_tag_count | 整数 | script 标签出现次数 |
| title_changed | 0/1 | 页面标题是否变化 |
| rtt_ms | 数值 | 本次请求耗时 |
| len_variance | 数值 | 同参数多次探测长度方差 |
| encoding_break | 0/1 | 响应头 charset 与 body 实际编码不一致 |
encoding_break 是个容易漏的特征:某些防护设备在拦截后返回的页面,编码声明与真实内容不一致,这个特征对识别“被拦截而不是被注入”非常有效。
4.2 轻量分类模型的热加载与推理
这个规模用随机森林就够,不需要上深度学习。100 棵树、最大深度 6,模型文件通常在几十 MB 内,单条推理毫秒级。模型和扫描器分离部署,日常只做推理,不回传业务数据:
# ml/model_holder.py import os import time import joblib FEATURE_KEYS = [ "payload_len", "special_char_ratio", "sql_keyword_hits", "html_tag_density", "error_pattern_hits", "response_len_delta", "similarity_to_baseline", "script_tag_count", "title_changed", "rtt_ms", "len_variance", "encoding_break", ] class ModelHolder: def __init__(self, path: str, check_interval: int = 60): self.path = path self.check_interval = check_interval self.mtime = None self.model = None self._last_check = 0.0 def predict_positive(self, features: dict) -> float: now = time.time() if now - self._last_check > self.check_interval: mtime = os.path.getmtime(self.path) if mtime != self.mtime: self.model = joblib.load(self.path) self.mtime = mtime self._last_check = now vec = [features.get(k, 0.0) for k in FEATURE_KEYS] return float(self.model.predict_proba([vec])[0][1])按 mtime 热加载,模型文件替换后最多 60 秒生效,不用重启 worker;check_interval 别设太短,否则每次推理都在 stat 文件,浪费 IO。特征缺失时用 0.0 填充,推理侧不抛 KeyError。多个 worker 共用一个模型文件时,加载语句外面加个短暂的跨进程锁即可。推理结果要带模型版本号,方便报告回溯是哪一版模型给出的概率。
4.3 规则置信度与模型概率的加权投票
融合打分用加权求和:
def final_score(rule_risk: float, model_prob: float, alpha: float = 0.6) -> float: if rule_risk <= 0: return round(model_prob, 3) return round(alpha * rule_risk + (1 - alpha) * model_prob, 3)rule_risk 取第 3 章表格里的默认分,没有被任何插件命中的请求直接返回模型概率。alpha 默认 0.6,说明默认信任规则多一点;当模型在同一类目标上表现好、误报低时,把 alpha 降到 0.5;当规则经过长期回归验证后,alpha 可以提到 0.75。最终分值与等级映射:
| 综合分值 | 等级 | 处理方式 |
|---|---|---|
| >= 0.80 | 高危 | 直接进报告 |
| 0.70 ~ 0.79 | 中危 | 进报告并附复核建议 |
| 0.60 ~ 0.69 | 待复核 | 仅记录,不告警 |
| < 0.60 | 忽略 | 丢弃 |
4.4 样本回流:让智能检测持续迭代
融合判定的上限取决于标注数据质量。每一条探测记录都带特征向量、规则结论、最终等级,人工复核后把结论写回 labeled 表,每周导出增量训练集:
python tools/retrain.py --data data/labeled.csv --model-out models/scan_v2.joblib训练和推理分离,训练机不接触线上 Redis;产物只出 joblib 文件与一份特征说明 JSON。迭代时优先补充误报样本,误报样本对模型的边际收益比再加真阳性样本高得多。
5. 扫描任务调度与敏感信息保护:检测系统自身的防护设计
5.1 基于 RQ 的异步任务队列与并发限速
任务调度我建议用 RQ 而不是自己写的线程循环。线程循环的问题在于:worker 重启后任务丢失、没有重试语义、并发数没法按目标动态调。RQ 只依赖 Redis 和 rq 包,语义比 Celery 轻得多,适合扫描器这种“任务量中等、单任务耗时几十秒”的场景。
# scheduler/tasks.py import os from redis import Redis from rq import Queue, Retry _redis = Redis.from_url(os.environ["SCAN_REDIS_URL"]) scan_queue = Queue("scan", connection=_redis, default_timeout=600) def submit_scan(target: dict) -> str: job = scan_queue.enqueue( "engine.scan_target", # worker 侧可按字符串路径 import target, retry=Retry(max=2, interval=[5, 30]), job_timeout=600, ) return job.idworker 侧启动命令是rq worker scan --max-jobs 16,前者指定队列名,后者限制单次拉取的任务数。retry 的 interval 列表表示重试间隔递增,前 5 秒、再 30 秒,避免目标短暂抖动时打满重试。Redis 地址从环境变量读,不写死在代码里。并发限速不能压在 RQ 的 worker 数量上,因为不同目标站点的容忍度不一样:
import threading import time class RateLimiter: """按固定 QPS 放行,跨线程共享一个限速器""" def __init__(self, qps: float = 5.0): self.interval = 1.0 / qps self.lock = threading.Lock() self.next_ok = time.monotonic() def wait(self) -> None: with self.lock: now = time.monotonic() sleep_for = max(0.0, self.next_ok - now) self.next_ok = now + sleep_for + self.interval if sleep_for: time.sleep(sleep_for)用单调时钟而不是 time.time(),防止系统时间回拨导致限速器失效。qps 默认 5.0 是保守值;对资源型目标可以调到 20,对生产站点建议 2 以下。限速器按目标域名建实例,不全局共享,避免 A 站点的慢速拖住 B 站点的扫描。
注意:限速保护的不只是目标,还有扫描器自己。请求过快触发对端防护时,扫描器 IP 会被封禁,后面所有任务全部失败,返工成本远高于慢扫。
5.2 Cookie 与登录凭证的加密存储
登录态扫描需要保存 Cookie 和表单凭证,这部分数据不能明文落库。常见做法是用 AES-256-GCM 加密后再写 SQLite,密钥从环境变量派生:
import hashlib import os from cryptography.hazmat.primitives.ciphers.aead import AESGCM def _derive_key(master: str, salt: bytes) -> bytes: return hashlib.pbkdf2_hmac("sha256", master.encode(), salt, 120_000) def encrypt_secret(plain: str, key: bytes) -> bytes: nonce = os.urandom(12) return nonce + AESGCM(key).encrypt(nonce, plain.encode(), None) def decrypt_secret(token: bytes, key: bytes) -> str: nonce, ct = token[:12], token[12:] return AESGCM(key).decrypt(nonce, ct, None).decode()AESGCM 的密文自带校验 tag,数据被篡改时解密直接抛异常,正好防住有人改库里的 Cookie 伪造身份。salt 每个部署环境随机生成一次,存在独立配置文件;主密钥只放环境变量或密钥管理服务,不写配置文件。最好再按目标做一层子密钥包裹,避免单个目标泄露拖出全部凭证。
| 存储方式 | 能否还原明文 | 适合场景 | 不建议的原因 |
|---|---|---|---|
| 明文写库 | 是 | 无 | 日志与备份都可能泄露 |
| SHA-256 哈希 | 否 | 防篡改校验 | 登录态要重放,哈希不可用 |
| AES-256-GCM | 是 | 必须重放的凭证 | 需要维护密钥生命周期 |
日志里禁止打印 Cookie 和 Authorization 头,统一用[REDACTED]占位;报告导出时同样脱敏,只保留凭证别名。
5.3 目标合法性校验与授权确认
扫描系统最容易变成“可通过网页控制的内网探测工具”,所以目标入库前必须过校验。网络侧默认拒绝私网和回环地址,业务侧要求目标带授权人和授权截止时间:
import ipaddress from urllib.parse import urlparse def check_target_allowed(raw_url: str, allowlist: list[str], allow_private: bool = False) -> bool: host = urlparse(raw_url).hostname or "" if not host: return False try: ip = ipaddress.ip_address(host.strip("[]")) except ValueError: pass else: if not allow_private and (ip.is_private or ip.is_loopback): return False return any(host == d or host.endswith(d.lstrip("*")) for d in allowlist)allow_private 默认 False,防的是外部入口把内网扫穿;内网自建实例通过配置显式打开。allowlist 用域名而不是 IP 段,通配*.corp.example只匹配二级及以下域名。时间侧在任务入队前校验授权截止时间,过期任务直接返回失败信息而不是排队等待,避免清队列时出现脏数据。
6. 用最小靶场验证智能检测规则:实验设计与参数调优
6.1 搭建一个三个用例的本地靶场
验证检测系统不要拿外网站点试,本地起一个 Flask 服务就够了,这比临时找 CTF 靶站更可控。故意留两个脆弱点和一个正常页面:
# lab.py 仅用于本机验证,不能部署到生产 from flask import Flask, request app = Flask(__name__) def query_db(s: str) -> str: # 靶场模拟器,不执行真实 SQL return "uid:1|name:admin" if "'1'='1" in s else "uid:0|name:unknown" @app.route("/api/user") def user(): return query_db(request.args.get("id", "")) @app.route("/echo") def echo(): return f"<div>{request.args.get('q', '')}</div>" @app.route("/ok") def ok(): return "<title>ok</title><p>health</p>"启动后跑一轮扫描,三个用例的预期结果要能对得上:
| 用例 | 期望命中 | 期望分值区间 |
|---|---|---|
| /api/user?id=1' AND '1'='1 | boolean_based | 0.70 ~ 0.85 |
| /echo?q=test | xss_raw | >= 0.80 |
| /ok 静态页 | 无 | < 0.60 |
分值区间只是验收基准,每个团队按自己的标注集定。跑完如果第一项落到待复核档,优先怀疑相似度阈值,而不是盲目调 alpha。
6.2 误报率与召回率的统计口径
没有统计口径,调参就是凭感觉。我在实战中固定两个指标:每 1000 条探测请求的误报数,以及在人工标注集上的召回率。评估命令长这样:
python tools/evaluate.py \ --labels data/labels.json \ --results data/results/ \ --threshold 0.7labels 文件由人工复核产出,results 目录保留每次探测的证据(响应头、归一化前响应体 hash、耗时时长),保证“报了能查、查了能复核”。threshold 调高,误报降但召回降,两个数的权衡要记录在配置注释里,方便后来人接手。
6.3 两个高频调参点
第一个是融合权重 alpha。规则在靶场和真实目标上都验证过,可以慢慢提到 0.75;模型在同一类目标上迭代了两轮以上,反而应该降到 0.5,把判断权交给模型。第二个是布尔盲注的长度差阈值。这个值跟目标页面的动态程度强相关,我一般先跑 30 次基线请求,统计响应长度标准差,把差异阈值设成max(200, 6 * 标准差),比写死 200 可靠得多。
每次调整都保留一份归一化响应哈希作为回归基线:python tools/regression.py --baseline data/baseline.json --latest data/latest.json,插件改动后跑一遍全量回归,看哪些历史用例从命中变成未命中。下次再遇到误报回升,先对比两类样本在 12 维特征上的分布差异,把分差最大的特征沉淀成新规则写进插件,而不是直接动 alpha。
本文还有配套的精品资源,点击获取