GravityAgent(七):工程化踩坑复盘——从研究代码到生产平台
工程化踩坑复盘:从研究代码到生产平台
这是系列的最后一篇。前六篇讲的是”设计成什么样”,这一篇讲真实踩过的坑,以及它们是怎么被定位和修复的。
把研究代码变成生产平台,最难的部分往往不是算法,而是这些琐碎但致命的工程问题。
坑一:同名模块冲突
现象
6Graph 和 6Forest 单独跑都正常,但在同一个 Agent 进程里先后调用时,第二个算法的结果会莫名其妙地不对。
根因
两个上游仓库都定义了同名模块:
- 6Graph:
SpacePartition、PatternMining - 6Forest:
SpacePartition、OutlierDetection
Python 的 sys.modules 会缓存已导入的模块。第一次导入 6Graph 的 SpacePartition 后,第二次 6Forest 再 import SpacePartition 时,拿到的是缓存里 6Graph 的版本。
修复
在加载前主动清理同名模块缓存:
sys.modules.pop("SpacePartition", None)
sys.modules.pop("PatternMining", None)
sys.modules.pop("OutlierDetection", None)
教训
多个研究仓库共进程运行时,模块命名空间是共享的。 上游作者默认自己的代码独占进程,不会考虑命名冲突。适配层必须主动做隔离。
坑二:6Sense 除零崩溃
现象
6Sense 生成过程中,某一轮如果没有任何命中,程序直接崩溃:
ZeroDivisionError: division by zero
根因
上游代码在计算生成效果时用了类似 hits / total 的分式。当:
- 某轮生成结果
hits = 0; all_allocations_thus_far也为空;
分母为 0,但代码没有做保护。
修复
在分母为 0 时,把差值直接置为 0.0:
# 若当前分母为 0,则把 diff 直接置为 0.0
# 使"没有命中"的场景被视为正常结果而不是异常崩溃
教训
“没有命中”是扫描场景的常态,不是异常。 研究代码常常假设实验环境总能拿到数据,但生产环境里空结果是必须处理的一等公民。
坑三:SQLite 空文件导致写入静默失败
这是最隐蔽的一个 bug。
现象
调用 start_os_identify() 返回成功,WAL 文件持续增长,但 os_results 表始终无数据。程序表面完全正常。
根因
如果 os_identify.db 是用 touch 创建的空文件,SQLite 连接后执行 CREATE TABLE IF NOT EXISTS 会静默失败——空数据库文件不会报错,导致所有数据写入 WAL 而不落盘。
修复
在 _ensure_db_file() 中检测文件大小:
# 检测文件大小 < 512 字节时尝试连接验证
# 无效则删除重建
# 同时对已产生的 WAL 用 PRAGMA wal_checkpoint(TRUNCATE) 将数据恢复回主库
教训
“没有报错”不等于”成功了”。 对于 SQLite 这种嵌入式数据库,文件有效性必须显式验证。这个 bug 的排查成本极高,因为所有日志都显示正常。
坑四:报告文件写入路径导致归档失败
现象
OS 识别报告生成成功,但 Web 端始终显示”未归档文件”。
根因
_export_os_report() 把报告写入 os_identify/runtime/ 目录,而 Web 的归档逻辑只允许 download/ 下的文件。路径不在允许范围内,归档自然失败。
修复
改用 runtime_paths.user_result_path(),把报告写入用户隔离目录:
# 修改 _export_os_report() 使用 runtime_paths.user_result_path()
# 将报告写入 download/<user>/os_identify/ 用户隔离目录
教训
“文件生成了”和”用户能看到”是两件事。 在有多层路径规范的系统里,任何产物路径都必须走统一的路径生成函数,不能硬编码。
坑五:参数名错位
现象
identify_os_from_scan_batch 调用后行为异常,参数被错误赋值。
根因
OsIdentifyManager 中方法定义使用 batch_id 参数,但 agent.py 调用时传入的参数顺序与定义不一致,导致参数被错误赋值。
修复
新增 start_os_identify_from_batch() 方法,参数顺序与调用方完全匹配。
教训
跨模块调用时,位置参数是脆弱的。 参数一多,顺序错了编译器也不会报错(Python 尤其如此),只会在运行时表现为”结果不对”。关键接口应该用关键字参数或明确的数据类。
坑六:上游输出污染日志
现象
调用 6Forest 后,系统日志里混入大量上游的调试打印。
根因
上游研究代码会往标准输出打印中间过程。
修复
用 redirect_stdout 静默:
mute_out = StringIO()
with redirect_stdout(mute_out):
for region in sp.DHC(arr):
p, _ = od.OutlierDetect(region)
patterns.extend(p)
教训
研究代码的输出是给人看的,生产代码的输出是给日志系统看的。 两者必须隔离。
坑七:LLM 幻觉与假工具调用
现象
用户上传种子文件要求预测,模型回复”已成功生成 5000 个候选地址”,但实际上根本没调用工具,Web 又去归档一个不存在的文件。
根因
LLM 倾向于生成”看起来合理”的回复。当工具调用失败或模型判断失误时,它会编造成功结果。
修复
两层防护:
- 提示词约束:明确禁止伪造文件路径和 batch_id;
- 后端兜底:识别附件预测请求,如果模型没真正发起 tool call,后端直接执行
predict_candidates_from_file,用真实结果覆盖幻觉回复。
return (
"检测到模型未实际调用预测工具,已由后端直接执行附件预测。\n\n" + result,
["predict_candidates_from_file"],
)
教训
不要相信 LLM 的”我做了”。 在涉及真实副作用的系统里,必须用后端逻辑验证工具是否真的被调用,并且让回复中的每个文件路径都可验证。
坑八:长任务让用户以为卡死
现象
6Sense 训练需要几十分钟,Web 页面一直显示”后台任务执行中”,用户反复刷新甚至重复提交。
根因
后端只有一个笼统的等待文案,前端没有耗时显示。
修复
- 后端根据意图推断细分
pending_label; - 前端轮询时展示”已等待 xx 秒”。
覆盖的文案包括:地址生成预测、操作系统识别、模式挖掘、批次扫描提交、扫描结果查询。
教训
长任务的体验问题本质是”不确定性”问题。 用户不怕等,怕的是不知道在等什么、还要等多久。细分状态 + 计时器能显著降低焦虑。
坑九:组织标签不可靠
现象
按组织取存活地址时,结果里混入了不属于该组织的 IP。
根因
扫描结果里的 org 字段来自 GeoIP 或 ASN 查询,质量并不总是可靠。
修复
结合 org_ipv6_mapping.json 做前缀二次过滤:
def _filter_ips_by_prefixes(ips, prefixes):
# 用 ipaddress.IPv6Network 判断 IP 是否落在组织前缀内
教训
任何单一数据源都可能有噪声。 关键业务逻辑应该做交叉验证,而不是盲信某个字段。
坑十:全局变量的并发取舍
现象
Web 平台为每个用户构建独立 Agent,但 agent.py 用的是模块级全局变量(SCANNER_API、LLM_API_KEY 等)。
处理
_configure_agent_runtime 在每次请求前覆写全局变量,并设置用户上下文:
am.SCANNER_API = cfg["scanner_api"]
am.LLM_API_KEY = cfg["llm_api_key"]
set_current_user(uid)
教训
这是一个有意识的取舍:与其把 3300 行的 CLI 工具函数全部重写成面向对象,不如接受”同进程内串行执行”的限制。对于团队规模的并发量,这个取舍是划算的。
但要清楚它的边界:如果未来需要真正的高并发多用户,这个设计必须重构。
工程化的十条经验
把上面十个坑抽象一下,可以总结成十条经验:
- 模块命名空间是共享的——多仓库共进程必须主动隔离。
- 空结果是常态——所有除法、索引、聚合都要考虑空输入。
- 没有报错不等于成功——关键写入要显式验证。
- 路径必须统一生成——硬编码路径迟早会破坏归档。
- 位置参数是脆弱的——关键接口用关键字参数。
- 研究输出要静默——日志系统需要干净的输入。
- 不要相信 LLM 的”我做了”——用后端逻辑验证副作用。
- 长任务要可观测——细分状态 + 计时器。
- 单一数据源有噪声——关键逻辑做交叉验证。
- 取舍要明确边界——知道什么情况下必须重构。
平台化的价值
回到最开始的问题:GravityAgent 的价值到底是什么?
它没有发明新算法——6Graph、6Forest、Entropy/IP、6Sense 都是别人的研究成果。它的价值在于:
- 把算法统一封装成 Agent 可调用的能力;
- 把 OS 识别建设成独立子系统;
- 把候选生成和真实扫描系统联动起来;
- 把研究代码改造成可运行、可复用、可排障的工程系统;
- 在 Web 层补齐用户管理、文件归档、后台任务和等待状态可视化。
从”演示级论文代码集合”到”可持续运行的工程平台”,中间隔着的就是这十个坑。
系列回顾
这个系列从架构讲到实现,再到复盘:
| 篇目 | 主题 | 核心 |
|---|---|---|
| 一 | 平台总览 | 五层架构与扫描闭环 |
| 二 | 算法适配层 | 统一接口、模块隔离、checkpoint 哈希 |
| 三 | 扫描执行引擎 | 双层存储、租约模型、无状态 Cookie |
| 四 | OS 识别子系统 | 双引擎、七维打分、可信度报告 |
| 五 | Agent 编排层 | 26 个工具、自动提交闭环、防幻觉 |
| 六 | 多用户 Web 平台 | 用户隔离、优先级队列、路径防护 |
| 七 | 工程化复盘 | 十个真实坑与经验总结 |
后续方向
如果继续演进,几个值得做的方向:
- 把全局变量重构为依赖注入,支持真正的多用户并发;
- 引入任务队列(如 Redis/Celery),替代进程内后台线程;
- 补充单元测试和集成测试,尤其是算法适配层和指纹匹配;
- 完善 OS 指纹库,支持自定义指纹导入;
- 增加测量数据的可视化分析,比如地址空间分布、组织覆盖热力图。
结语
IPv6 网络空间测绘是一个既有研究深度、又有工程挑战的方向。地址空间大到无法枚举,意味着我们必须依赖算法生成候选;而算法生成的结果必须经过真实扫描验证,才能形成闭环。
GravityAgent 尝试做的,就是把这条链路完整地打通,并且让它足够工程化,能够被一个团队持续使用。
如果你把这个仓库当成”若干论文代码的合集”,会低估它。 更准确的理解是:它是一个把 IPv6 地址挖掘研究成果包装成可运行扫描平台的系统工程仓库。
感谢阅读这个系列。如果你对 IPv6 测绘、网络资产发现或安全工程感兴趣,欢迎通过博客侧边栏的 GitHub 或邮箱交流。
订阅本站
通过 RSS 或邮箱,第一时间收到新文章。