GravityAgent(五):用 LangGraph 编排 26 个测绘工具
IPv6 网络测绘 GravityAgent Agent LLM
用 LangGraph 编排 26 个测绘工具
前面几篇讲了算法层、扫描引擎和 OS 识别。这一篇我们来看把它们串起来的Agent 编排层——agent/agent.py。
为什么测绘平台需要一个 Agent
传统的测绘工具是”参数式”的:你得记住一堆命令行参数,知道每个算法的输入格式,还要手动把输出接到下一步。这对研究者来说没问题,但对平台使用者来说门槛太高。
Agent 的价值在于:用户只需要说人话,系统自动选择工具、串联流程、汇总结果。
比如:
识别 202.194.68.10 的操作系统
对 UJN 的存活地址用 6Forest 做模式挖掘
为 CERN 生成 IPv6 候选地址并提交扫描
用 /path/to/seeds.txt 生成候选地址并扫描
这些请求会被 Agent 翻译成一系列工具调用。
技术选型:LangGraph ReAct
agent.py 基于 LangGraph 的 create_react_agent 构建,配合 LangChain 的 @tool 装饰器把 Python 函数注册成工具。
# 框架:LangGraph create_react_agent + LangChain @tool
# LLM:ChatOpenAI(model="deepseek-chat", temperature=0)
为什么用 ReAct 模式?因为测绘任务天然是多步的:
- 先查组织信息,拿到前缀;
- 再拉存活地址作为种子;
- 然后调用算法挖掘模式;
- 基于模式生成候选;
- 最后提交扫描。
ReAct 的”思考—行动—观察”循环正好适合这种链式任务。temperature=0 保证同样的输入尽量得到同样的工具调用路径。
网络层的细节
agent.py 显式构造了 httpx.Client/AsyncClient,并配置了代理和超时:
# 代理 http://127.0.0.1:7897
# 读写超时 LLM_HTTP_TIMEOUT_SEC(默认 7200s)
# 绕过环境中的 socks5h 代理
超时设置为 7200 秒(2 小时)是必要的——6Sense 训练一次可能要几十分钟,如果 HTTP 客户端提前超时,整个流程就断了。
26 个工具,六大类
Agent 绑定了 26 个工具,可以分为六类:
| 类别 | 工具 |
|---|---|
| 组织情报 | list_organizations、query_org_stats、query_org_alive_ips |
| 模式挖掘 | mine_org_patterns_6graph、mine_org_patterns_6forest、mine_org_patterns_entropy_ip |
| 候选生成 | predict_org_candidates_6graph/6forest/entropy_ip/6sense、predict_candidates_from_file |
| 扫描提交/查询 | submit_scan、submit_scan_prefix、get_batch_status、get_alive_ips |
| DNS 发现 | start_dns_server_discovery、list_dns_server_discovery_runs、get_dns_server_discovery_status、get_dns_server_discovery_results |
| OS 识别 | identify_os_single、identify_os_batch、identify_os_from_scan_batch、list_os_identify_runs、get_os_identify_status、get_os_identify_results、generate_os_confidence_report |
系统提示词的约束
工具能不能被正确调用,很大程度上取决于系统提示词。SYSTEM_PROMPT 里明确规定了:
- 用户要求”操作系统识别 / OS 指纹”时,调用
identify_os_single或identify_os_batch; - 用户要求”生成可信度报告”时,调用
generate_os_confidence_report; - 默认预测用
6Graph; - 用户明确指定算法时切换到对应工具;
- 用户直接提供本地种子文件时优先调用
predict_candidates_from_file; - 不允许在未真实调用工具时伪造文件路径或 batch id。
最后一条特别重要——LLM 很容易”编造”一个看起来合理的 batch_id 或文件路径,必须用提示词明确禁止。
候选生成算法
预测类工具的核心是”从模式生成地址”。这里有两套实现。
通配符模式采样(6Graph / 6Forest)
模式里 * 表示自由半字节。生成时按 seed_count 加权选择模式,再把 * 随机填 0–f:
# 模式池按 seed_count 加权(copies = min(32, seed_count))
# random.Random(20260309) 固定种子保证可复现
# 循环:从池中轮询模式 → 填 * → 去重 → 前缀内校验
# 直到凑满 budget(最大尝试 max(10000, budget*300))
用固定随机种子 20260309 是一个刻意的设计:同样的输入应该得到同样的输出,方便复现和调试。
Entropy/IP 规则采样
Entropy/IP 输出的是分段规则。生成时按段分组,每段保留覆盖率最高的规则:
# 规则按 (segment, bit_start, bit_stop) 分组
# bit 区间换算半字节区间
# 每段保留覆盖率最高的 max_rules_per_segment(默认 8)条
# 每次尝试:初始化 32 个 "0",逐段按覆盖率加权 rng.choices 选规则
# range 型:在 [value_start, value_stop] 随机取十六进制值
# const 型:取固定值
# 拼成 128 位地址 → 去重 + 前缀过滤
预测 → 自动提交扫描的闭环
这是整个 Agent 层最有价值的设计。所有 predict_* 工具在返回给 LLM 的同一轮调用内完成闭环:
1. 生成候选(通配符采样 / Entropy 规则 / 6Sense 输出文件)
│
▼
2. 超过阈值落盘到 download/<uid>/candidates/
│
▼
3. _auto_submit_prediction_candidates
批名 {算法}_{yyyyMMdd_HHMMSS}
用专用账号 realtime/realtime 调 _submit_batch_file
上传 runs/agent/tmp/predict_*.csv(首行 ip)
│
▼
4. _submit_batch_file
httpx multipart 优先(60s 超时)
→ curl 回退(--retry 2 --retry-connrefused)
→ 解析 batch_id
│
▼
5. _append_prediction_submit_result
把"提交者/批名/batch_id"追加进返回文本
这意味着用户说”为 CERN 生成候选地址并扫描”,Agent 不是只生成一个文件就完事,而是真的把候选地址提交进了扫描队列,并返回一个可以追踪的 batch_id。
用户随后可以说”查一下这个批次的状态”,Agent 会调用 get_batch_status(batch_id)。
为什么用 curl 回退
_submit_batch_file 优先用 httpx,失败时回退到 curl。原因是某些环境下 Python 的 HTTP 库和系统代理配置不兼容,而 curl 的行为更可预测。--retry 2 --retry-connrefused 则能容忍扫描服务的短暂重启。
数据源回退链
Agent 需要读取扫描数据,但数据可能在多个地方。系统设计了一条回退链:
批次列表
scanner API /api/batch
→ postgres_fallback.pg_list_batches(psql 直查)
→ 本地 master.db
存活地址
API /api/batch/{id}/txt
→ 本地 data/batch_<id>.db
(scan_results / service_observations 表 DISTINCT ip)
按组织取存活地址
_get_alive_ips_by_org 遍历所有批次 SQLite 按 org 列精确匹配,本地无结果时拉取最多 200 个批次的 export 过滤。
这条回退链的意义是:即使 scanner 服务暂时不可用,Agent 依然能从本地数据工作。
组织前缀二次过滤
扫描结果里的 org 字段质量并不总是可靠,所以 _get_alive_ips_for_org 会结合 org_ipv6_mapping.json 做二次清洗:
def _filter_ips_by_prefixes(ips, prefixes):
# 用 ipaddress.IPv6Network 判断 IP 是否落在组织前缀内
org_ipv6_mapping.json 里维护了 9 个组织的 ASN 和前缀映射,包括 CERNET、UJN、QLU、Google、Cloudflare、Amazon、Alibaba、China Telecom、China Unicom。
长任务与等待体验
测绘任务动辄几分钟到几十分钟。如果 Web 端同步等待,用户会以为页面卡死了。
解决方案是后台 job + 细分等待文案:
地址生成预测任务执行中
操作系统识别任务执行中
模式挖掘任务执行中
批次扫描提交任务执行中
扫描结果查询任务执行中
后台任务执行中
后端根据用户意图推断 pending_label,前端轮询时同时展示”已等待 xx 秒”。
防幻觉设计
LLM 最危险的行为是”假装调用了工具”。针对这个问题,系统做了两层防护。
第一层:提示词约束
系统提示词明确禁止伪造文件路径和 batch_id。
第二层:后端兜底
当用户上传附件并要求预测时,如果模型没有真正发起预测类 tool call,后端会直接执行:
# 识别"用户上传附件并要求做预测"的请求
# 如果模型没有真正发起预测类 tool call
# 则后端直接执行 predict_candidates_from_file
# 使用真实执行结果覆盖原本可能幻觉的自然语言回复
这解决了一个具体问题:模型可能会编造”已成功生成 1000 个候选地址”,而实际上什么都没做,Web 又去归档一个根本不存在的文件。后端兜底确保回复里提到的文件一定真实存在。
文件归档
当 Agent 回复中包含绝对路径时,Web 会:
- 等待文件真正落盘;
- 复制到
download/<uid>/; - 将回复文本中的原始路径替换成用户可见的归档项。
如果文件不存在或复制失败,就显示为:
未归档文件(<文件名>)
这个”未归档”标记本身就是一种可观测性——它明确告诉用户”这个文件有问题”,而不是假装成功。
运行期路径规范
Agent 的所有产物路径统一由 runtime_paths.py 管理,并且实现了用户隔离:
download/<uid>/
├── os_identify/ # OS 识别报告(JSON/Markdown)
├── candidates/ # 候选地址生成结果
├── patterns/ # 模式挖掘结果
├── alive_ips/ # 存活地址导出
├── batch_results/ # 批次扫描结果
└── service_probe/ # 服务探测结果
用户上下文通过 set_current_user(uid) / get_current_user() 管理,Web 请求期间会被设置为实际用户。
一个完整的调用链
把上面所有东西串起来,用户说”为 UJN 生成候选地址并扫描”时,实际发生的是:
用户输入
│
▼
LLM 判断意图 → 选择 predict_org_candidates_6graph
│
▼
_get_alive_ips_for_org("UJN")
├─ 遍历批次 SQLite 按 org 匹配
├─ 用 org_ipv6_mapping.json 前缀二次过滤
└─ 得到种子地址列表
│
▼
SixGraphMiner.mine_patterns(seeds)
├─ 动态加载 6Graph 模块(清理同名缓存)
├─ DHC 区域划分 + OutlierDetect
└─ 输出 pattern dict 列表
│
▼
_generate_candidates_from_wildcard_patterns
└─ 按 seed_count 加权采样,生成候选地址
│
▼
落盘 download/<uid>/candidates/6graph_UJN_<ts>.txt
│
▼
_auto_submit_prediction_candidates
└─ POST /api/batch → 拿到 batch_id
│
▼
返回文本:候选数量 + 文件路径 + batch_id
小结
Agent 编排层的设计要点:
| 设计 | 解决的问题 |
|---|---|
| LangGraph ReAct | 多步测绘任务自动串联 |
temperature=0 | 工具调用路径稳定可复现 |
| 固定随机种子 | 候选生成结果可复现 |
| 26 个工具分类 | 意图到能力的映射清晰 |
| 自动提交闭环 | 预测不止于文件,而是真的扫描 |
| 数据源回退链 | scanner 挂了也能工作 |
| 前缀二次过滤 | 修正不可靠的 org 标签 |
| 后台 job + 文案 | 长任务不再像卡死 |
| 双层防幻觉 | 回复里的文件一定真实存在 |
下一篇,我们来看 Web 多用户平台,看看这套能力是怎么被安全地开放给多个用户的。
订阅本站
通过 RSS 或邮箱,第一时间收到新文章。