为什么是本地模型,而不是直接调云端 API
把服务器日志发给云端大模型,听起来很省事,但对安全场景是个悖论:你在用「数据外泄」的方式解决「数据安全」问题。
访问日志里有什么?内网 IP 段、真实用户 ID、API 路径、偶尔还有误打进 URL 的 token。这些内容一旦离开你的网络,就脱离了你的控制。所以日志分析这个场景,本地模型不是「更便宜的替代品」,而是唯一合理的选择。
但本地模型有个硬约束:7B 级别的模型,上下文和推理能力都有限,不可能把几百万行日志直接丢进去。所以正确做法不是「让 AI 读日志」,而是——
用规则做粗筛,把 0.1% 的可疑对象挑出来,再让模型做研判。模型只负责最后一步:判断「这堆证据说明什么」。
一、架构:模型只做最后 5%
access.log
│
├─ ① 解析 + 结构化 (Python 正则,毫秒级)
├─ ② 规则打分 + 候选筛选 (阈值,无模型)
├─ ③ 证据打包 (每个 IP 一段结构化摘要)
├─ ④ LLM 研判 (Ollama,只处理几十条)
└─ ⑤ 输出 JSON + 人工复核
这个设计的核心取舍:召回靠规则,解释靠模型。规则便宜、确定、可审计;模型擅长的是「把零散证据串成一个结论」,而不擅长「在百万行里找异常」。
如果你反过来做(先让模型扫全量),结果一定是:慢、贵、上下文截断、漏报严重。
二、环境:Ollama 与模型选择
# 安装后拉模型
ollama pull qwen2.5:7b
ollama list
ollama run qwen2.5:7b "你好"
硬件门槛
| 模型规模 | 量化 | 显存需求 | 纯 CPU 表现 | 适用 |
|---|---|---|---|---|
| 7B | Q4_K_M | 约 5 GB | 可用,单条 5–15 s | 个人/小团队首选 |
| 14B | Q4_K_M | 约 9 GB | 偏慢 | 判定质量更好,需 12G 以上显卡 |
| 32B | Q4_K_M | 约 20 GB | 不实用 | 有显卡的团队 |
模型怎么选
| 模型 | 特点 | 适合 |
|---|---|---|
qwen2.5:7b |
中文强、指令遵循好、JSON 输出稳定 | 推荐起点 |
llama3.1:8b |
英文强,中文一般 | 英文日志环境 |
deepseek-r1:7b |
有推理链,但慢且啰嗦 | 复杂判定,不追求速度 |
qwen2.5:14b |
判定更细,误报更低 | 有显卡时优先 |
不要用带 “coder” 或对话专用的模型做研判,指令遵循会明显变差。
三、第一步:解析与规则粗筛
假设是标准 Nginx combined 格式。先做结构化统计,这一步完全不用模型:
import collections
import re
LINE = re.compile(
r'(?P<ip>\d+\.\d+\.\d+\.\d+) .*? '
r'"(?P<method>\w+) (?P<path>\S+) [^"]*" '
r'(?P<status>\d{3}) .*? "(?P<ua>[^"]*)"'
)
def scan(path):
stats = collections.defaultdict(
lambda: {"n": 0, "e4": 0, "e5": 0, "paths": collections.Counter(), "ua": set()}
)
with open(path, encoding="utf-8", errors="replace") as f:
for line in f:
m = LINE.search(line)
if not m:
continue
ip = m["ip"]
code = int(m["status"])
s = stats[ip]
s["n"] += 1
if code in (401, 403, 429):
s["e4"] += 1
if code >= 500:
s["e5"] += 1
s["paths"][m["path"][:80]] += 1
s["ua"].add(m["ua"][:100])
return stats
def pick(stats, limit=30):
"""规则打分,只挑出值得让模型看的对象"""
scored = []
for ip, s in stats.items():
score = s["e4"] * 2 + s["e5"] * 3
if s["n"] >= 300:
score += 5 # 高频访问
if len(s["paths"]) > 50:
score += 5 # 路径发散,疑似扫描
if s["e4"] >= 10:
score += 10 # 大量认证失败
if score > 0:
scored.append((score, ip, s))
scored.sort(key=lambda x: -x[0])
return scored[:limit]
这段代码的价值不在「聪明」,而在稳定:同样的输入永远得到同样的候选集,方便复现和审计。
四、第二步:证据打包
给模型的输入必须是结构化、有限、无歧义的。不要直接把原始日志行贴进去——模型会被格式噪声带偏。
def evidence(ip, s, top_paths=8):
return {
"ip": ip,
"requests": s["n"],
"auth_failures": s["e4"],
"server_errors": s["e5"],
"unique_paths": len(s["paths"]),
"top_paths": [p for p, _ in s["paths"].most_common(top_paths)],
"user_agents": sorted(s["ua"])[:3],
}
一份好的证据包应该满足三件事:字段名自解释、数值可核对、长度可控。这样模型不需要「猜」,判定才稳定。
五、Prompt 的三个原则
这是整篇文章里最容易被忽略、但影响最大的部分。
原则 1:明确禁止编造
模型非常擅长「补全合理但不存在」的细节,比如给一个根本没访问过的路径编出攻击意图。必须写死:
只依据给出的字段判断。
不要推测未提供的字段(例如地理位置、历史行为、是否为已知恶意 IP)。
如果证据不足以判断,verdict 填 "unknown",不要强行给结论。
原则 2:把分类收敛到少数几类
让模型自由发挥会产生几十种说法,无法统计。给定枚举值:
"verdict": "attack" | "suspicious" | "normal" | "unknown"
"category": "bruteforce" | "scanner" | "injection" | "crawler" | "error" | "none"
原则 3:强制 JSON,temperature 设 0
Ollama 支持 format: "json",能让输出可直接解析。再加上 temperature: 0,大幅降低同样输入得出不同结论的概率。
六、完整脚本
import json
import sys
import requests
MODEL = "qwen2.5:7b"
URL = "http://127.0.0.1:11434/api/chat"
SYSTEM = """你是网络安全分析师,负责研判 Web 访问日志中的可疑来源。
规则:
1. 只依据用户提供的字段判断,严禁编造未提供的字段。
2. 证据不足时 verdict 用 "unknown",不要强行下结论。
3. 输出必须是 JSON,不要有任何额外文字。
字段:ip, verdict(attack|suspicious|normal|unknown),
category(bruteforce|scanner|injection|crawler|error|none),
confidence(0-1 的小数), reason(50 字以内中文)"""
def judge(ev):
resp = requests.post(URL, json={
"model": MODEL,
"messages": [
{"role": "system", "content": SYSTEM},
{"role": "user", "content": json.dumps(ev, ensure_ascii=False)},
],
"format": "json",
"stream": False,
"options": {"temperature": 0, "num_ctx": 4096},
}, timeout=120)
resp.raise_for_status()
raw = resp.json()["message"]["content"]
try:
return json.loads(raw)
except json.JSONDecodeError:
return {"ip": ev["ip"], "verdict": "error", "reason": raw[:80]}
def main(log_path):
stats = scan(log_path)
cands = pick(stats)
print("候选 %d 个" % len(cands))
results = []
for score, ip, s in cands:
ev = evidence(ip, s)
r = judge(ev)
r["score"] = score
results.append(r)
print("[%s] %-10s %s" % (r.get("verdict"), ip, r.get("reason", "")))
with open("verdicts.json", "w", encoding="utf-8") as f:
json.dump(results, f, ensure_ascii=False, indent=2)
print("已写入 verdicts.json")
if __name__ == "__main__":
main(sys.argv[1] if len(sys.argv) > 1 else "access.log")
依赖只有 requests。跑起来:
pip install requests
python log_triage.py access.log
输出的 verdicts.json 可以直接丢进任何看板,或者再写一步自动封禁。
七、实际输出长什么样
[
{
"ip": "203.0.113.45",
"verdict": "attack",
"category": "scanner",
"confidence": 0.86,
"reason": "路径发散 217 个且集中 404/403,UA 为扫描器特征,判定为目录扫描",
"score": 38
},
{
"ip": "198.51.100.7",
"verdict": "suspicious",
"category": "bruteforce",
"confidence": 0.71,
"reason": "登录接口 401 达 143 次且时间集中,疑似撞库,但缺少失败账户多样性",
"score": 31
}
]
注意 reason 里出现了「但缺少…」——这正是模型的价值:它给出判断依据,而不是只给一个标签。人工复核时,看这行字比看分数有用得多。
八、怎么知道它准不准
这一节是本文和大多数「AI 文章」的区别:不测就不知道能不能用。
- 从历史日志里抽 50–100 个候选,人工标注一遍(正常 / 可疑 / 攻击)。
- 跑脚本,和人工标注对比,算三个数:
- Precision:判定为 attack 的里,多少是真的
- Recall:真正攻击里,被标出来的比例
- 稳定性:同一批数据跑 3 次,结果一致率
- 如果 Precision < 70%,先别接自动封禁,只做排序提示。
经验上,7B 模型在「取证辅助」这个定位上够用,在「自动处置」上不够。它最大的贡献是把人工翻日志的时间从两小时压到二十分钟,而不是替你做决定。
九、局限与边界
- 幻觉无法根除:即使写了「禁止编造」,模型仍可能给出听起来合理但错误的原因。所有结论必须可回溯到证据字段。
- 看不到上下文:它不知道你的业务、不知道这个 IP 是不是合作方,也不知道昨天发生过什么。这些要你在证据包里补。
- 不能替代 WAF / IDS:规则引擎在「确定性强」的场景远胜模型。模型适合「规则说不清、但人一看就懂」的灰区。
- 速度不是它的优势:7B 在 CPU 上单条 5–15 秒,只适合离线批处理,不适合实时拦截。
- 日志格式一变就挂:正则要跟着日志格式维护,这是长期成本。
十、部署建议
- 物理隔离:模型跑在内网机器,Ollama 默认只监听
127.0.0.1,不要暴露到公网。 - 日志脱敏:送模型前把 IP 做哈希、把用户 ID 替换成占位符,需要回溯时再查映射表。
- 结果只做辅助:先跑一个月「只看不动」,观察误报率,再考虑接入告警。
- 固化 prompt:prompt 进版本管理,改动要重新跑评估集,否则无法解释效果变化。
十一、小结
三句话总结这套方案:
- 规则负责找,模型负责判。顺序反了,成本和效果都会崩。
- prompt 的写法比模型大小更重要。禁止编造、枚举收敛、强制 JSON,这三条能让 7B 的表现接近 14B。
- 先评估再上线。没有标注集的「AI 赋能」都是自欺欺人。
延伸阅读:
免责声明:本文代码仅用于自有系统的日志分析。对他人系统进行探测、抓取或干扰均属违法,请遵守《网络安全法》相关规定。
- 1、本网站名称: 星宇安全实验室
- 2、本站永久网址:https://bk.zhaozhiqiang.pw/
- 3、本网站的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系站长进行删除处理。
- 4、本站一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
- 5、本站一律禁止以任何方式发布或转载任何违法的相关信息,访客发现请向站长举报
- 6、本站资源大多存储在云盘,如发现链接失效,请联系我们我们会第一时间更新。
- 7、文章版权归作者所有,未经允许请勿转载。













暂无评论内容