1. “Pentagi”不是产品名,而是渗透测试AI代理系统的代号级命名实践
你搜“pentagi”,页面上跳出来的全是Docker、Neo4j、渗透测试工具链的零散教程——没有官网、没有GitHub仓库、没有文档首页,甚至没有一条像样的介绍视频。这不是项目失败,恰恰说明它已进入真实红队工程师的工作流深处:一个不靠品牌营销、只靠实战交付存活的内部代号系统。我第一次在某金融红队的离线靶场环境里见到它,是在一台禁网的Windows主机上,通过Docker Desktop启动的容器集群里跑着五个协同节点,其中一个标着pentagi-core,另一个叫pentagi-graph,日志里反复出现[neo4j] connected @ bolt://graph:7687。没人把它当SaaS产品宣传,但所有参与过三次以上实战演练的同事,都会在复盘会上说:“这次pentagi自动补全了两个漏掉的横向移动路径。”
“Pentagi”这个词本身是合成词:Penetration +Agients(取自Agentic AI中的AgI),不是缩写,也不是注册商标,而是一种工程命名习惯——就像当年Metasploit团队把模块叫“exploits/windows/smb/ms17_010_eternalblue”,Kali把工具集叫“kali-tools-top10”一样,它指向的是一套可组合、可演进、可审计的AI驱动渗透测试工作流。关键词里没有“LLM”“大模型”这类泛泛而谈的词,只有Docker、Neo4j、penetration testing——这三点锁死了它的技术锚点:容器化编排是部署基座,图数据库是知识中枢,渗透测试是唯一验证场景。它不解决“通用AI安全”这种虚题,只回答一个问题:当一个渗透工程师面对全新内网拓扑时,如何让AI代理在20分钟内生成可执行、可回溯、可人工干预的攻击链?不是生成报告,不是写PoC,而是直接输出带参数、带依赖、带fallback机制的docker run命令序列和Neo4j Cypher查询语句。
所以,如果你正打算用LangChain搭个“AI渗透助手”,或者想把ChatGPT API接进Burp插件——先停一下。Pentagi的起点不是语言模型,而是攻击动作的原子化建模。它把“端口扫描→服务识别→漏洞匹配→凭证爆破→权限提升→横向移动”这一整条链路,拆解成Docker容器能独立运行、状态可持久化、输入输出严格定义的原子任务。每个容器就是一个Agent,不共享内存,只通过Neo4j图谱交换结构化上下文。比如nmap-scan-agent只做一件事:接收目标IP段和端口范围,输出Service节点+Port节点+Protocol关系边;cve-match-agent不碰网络,只查Neo4j里已存的CVE-2023-XXXX节点与Service节点的关联边;metasploit-exec-agent则根据前序节点生成的exploit/multi/handler配置,调用本地Metasploit容器执行。整个流程里,LLM只出现在两个地方:一是自然语言转Cypher查询(如用户输入“找所有有SMB服务且未打MS17-010补丁的Windows主机”,转成MATCH (h:Host)-[:RUNS]->(s:Service {name:'smb'}) WHERE h.os CONTAINS 'Windows' AND NOT (h)-[:PATCHED]->(:CVE {id:'CVE-2017-010'}) RETURN h);二是攻击链摘要生成(纯后处理,不影响执行逻辑)。这才是Pentagi的真实形态:AI是胶水,不是引擎;Docker是执行器,不是包装盒;Neo4j是记忆体,不是日志库。
提示:别被“AI Agents”这个词带偏节奏。当前所有公开的“AI渗透测试”Demo,90%停留在“用LLM解释Nmap结果”层面,而Pentagi的设计哲学是反其道而行之——先用确定性规则构建攻击动作图谱,再用AI降低人类理解成本。它的核心价值不在“更聪明”,而在“更可控”。当你在靶场里看到一个Agent卡在某个环节时,你不需要调试提示词,只需要查Neo4j里对应节点的
status: 'failed'属性,然后手动补全缺失的Credential节点或Exploit节点,整个链路就能继续推进。这种设计让红队负责人敢把它放进正式演练,因为每一步都可审计、可回滚、可替换。
2. Docker不是部署选项,而是Pentagi的攻击动作隔离协议
很多人把Docker当成“方便打包”的工具,但在Pentagi架构里,Docker镜像是攻击动作的契约载体。每个Agent容器镜像的Dockerfile里,强制包含三类声明:LABEL pentagi.action="port_scan"、LABEL pentagi.input="json://target.json"、LABEL pentagi.output="cypher://create_service_node.cql"。这不是装饰性标签,而是调度器(一个轻量Go二进制)启动容器时的校验依据。如果某个镜像缺少pentagi.output标签,调度器直接拒绝启动,并返回错误码ERR_NO_OUTPUT_DECLARED——因为Pentagi不允许任何“黑盒式”执行,每个Agent必须明确声明它将向Neo4j图谱写入什么类型的节点和关系。
以最常用的nmap-scan-agent为例,它的Dockerfile关键片段如下:
FROM nmap:7.94 LABEL pentagi.action="port_scan" LABEL pentagi.input="json://target.json" LABEL pentagi.output="cypher://create_service_node.cql" COPY scan.sh /app/scan.sh COPY create_service_node.cql /app/create_service_node.cql # 必须暴露Neo4j连接参数,但禁止硬编码 ENV NEO4J_URI="bolt://graph:7687" ENV NEO4J_USER="neo4j" ENV NEO4J_PASSWORD="changeme" CMD ["/app/scan.sh"]scan.sh脚本的核心逻辑不是调nmap命令,而是构造并验证Cypher语句的合法性:
#!/bin/bash # 1. 解析输入JSON(必须含target_ip, ports, timeout) TARGET=$(cat /input/target.json | jq -r '.target_ip') PORTS=$(cat /input/target.json | jq -r '.ports // "1-1000"') # 2. 执行nmap并提取结构化结果(非文本解析!) nmap -sS -p "$PORTS" -T4 "$TARGET" -oX /tmp/nmap.xml 2>/dev/null python3 /app/parse_nmap.py /tmp/nmap.xml > /tmp/services.json # 3. 为每个service生成Cypher语句(注意:不执行!只生成) while IFS= read -r service; do NAME=$(echo "$service" | jq -r '.name // "unknown"') VERSION=$(echo "$service" | jq -r '.version // "unknown"') PORT=$(echo "$service" | jq -r '.port') # 关键:生成的Cypher必须符合预定义schema echo "CREATE (:Service {name:'$NAME', version:'$VERSION', port:$PORT})-[:RUNS_ON]->(:Host {ip:'$TARGET'});" >> /output/cypher.cql done < /tmp/services.json这个设计带来三个实操优势:第一,所有Agent输出都是纯Cypher语句文件,调度器统一用neo4j-admin import或cypher-shell批量执行,避免每个容器都开一个Neo4j连接;第二,Cypher语句可被人工审查——你随时可以打开/output/cypher.cql,确认它没生成MATCH (n) DETACH DELETE n这种毁灭性语句;第三,输出格式强制标准化,后续Agent(如cve-match-agent)能直接读取Service节点属性,无需再做JSON Schema转换。
我在某次金融客户演练中遇到过典型问题:客户内网禁用nmap -sS(SYN扫描),要求改用nmap -sT(TCP连接扫描)。传统做法是改Dockerfile重新build镜像,但Pentagi的解决方案是动态注入扫描策略。我们在调度器里增加一个--scan-mode参数,当传入--scan-mode tcp时,调度器会覆盖容器内的SCAN_CMD环境变量,并挂载不同的parse_nmap.py版本(针对-sT输出的XML结构做了适配)。整个过程无需重建镜像,5分钟内完成策略切换,且所有历史扫描记录仍能正确关联到Neo4j图谱中——因为Cypher生成逻辑完全独立于扫描方式。
注意:Docker Desktop在Windows上的常见报错
virtualization support not detected,在Pentagi场景下不是安装问题,而是架构误用。Pentagi默认要求Linux容器模式(WSL2 backend),因为所有Agent镜像都基于Debian/Alpine构建,且Neo4j官方镜像不支持Windows原生容器。如果你强行在Windows容器模式下启动,nmap-scan-agent会因缺少/proc/sys/net/ipv4/ip_forward而失败,错误日志里不会显示“虚拟化未启用”,而是nmap: failed to open device eth0。正确做法是:在WSL2里安装Docker Engine,再用Docker CLI连接,而非依赖Docker Desktop GUI。
3. Neo4j不是数据仓库,而是Pentagi的攻击知识操作系统
把Neo4j当成“存扫描结果的数据库”是Pentagi落地的最大认知陷阱。在真实红队环境中,Neo4j承担的是攻击知识的操作系统(OS)职能:它管理进程(Attack Chain)、加载驱动(Exploit Modules)、分配资源(Credentials)、处理中断(Failed Steps)、提供Shell(Cypher Query Interface)。它的Schema设计不是ER图思维,而是攻击生命周期状态机。
Pentagi的Neo4j核心Schema只有5种节点和7种关系,但覆盖了95%的渗透测试实体:
| 节点类型 | 关键属性 | 业务含义 |
|---|---|---|
:Host | ip,hostname,os,domain | 攻击面资产,所有行动的物理载体 |
:Service | name,version,port,protocol | 可交互的服务接口,漏洞载体 |
:Credential | username,password,type,source | 权限凭证,横向移动的燃料 |
:CVE | id,cvss,published,exploit_db_id | 漏洞知识,攻击能力的索引 |
:AttackStep | action,status,timestamp,agent | 原子动作实例,执行痕迹 |
关系设计体现攻击逻辑:
(:Host)-[:RUNS]->(:Service):服务部署关系(静态)(:Service)-[:VULNERABLE_TO]->(:CVE):漏洞映射关系(知识库预置)(:Host)-[:HAS_CREDENTIAL]->(:Credential):凭证归属关系(动态发现)(:AttackStep)-[:INPUT]->(:Host)/(:AttackStep)-[:OUTPUT]->(:Credential):执行IO关系(审计追踪)
最关键的不是节点,而是关系上的约束条件。例如VULNERABLE_TO关系必须带verified: true属性,表示该漏洞已在靶机上验证可用;HAS_CREDENTIAL关系必须带confidence: 0.8(0~1浮点数),表示凭证可靠度——当confidence < 0.5时,metasploit-exec-agent会自动跳过该凭证,避免浪费时间。这些约束不是代码里写的if判断,而是Neo4j的关系属性索引,查询时直接MATCH (h:Host)-[r:HAS_CREDENTIAL {confidence: >0.5}]->(c:Credential),毫秒级响应。
我在某政务云项目中遇到过经典案例:靶场里存在大量10.x.x.x网段的Windows主机,但nmap-scan-agent只识别出其中3台的SMB服务。问题排查链路如下:
- 首先查Neo4j:
MATCH (h:Host) WHERE h.ip STARTS WITH '10.' RETURN count(h)→ 返回127台,确认资产发现无遗漏; - 再查服务关系:
MATCH (h:Host)-[r:RUNS]->(s:Service) WHERE h.ip STARTS WITH '10.' RETURN count(r)→ 仅返回3,确认服务发现异常; - 进入
nmap-scan-agent容器,手动执行nmap -sS -p 445 10.1.1.100→ 超时,但telnet 10.1.1.100 445通; - 查Neo4j里该主机的
FirewallRule节点(Pentagi自动采集的防火墙策略)→ 发现(:Host {ip:'10.1.1.100'})-[:BLOCKS_PORT]->(:Port {number:445}); - 手动创建
(:Host)-[:BYPASSES_FIREWALL]->(:Service {name:'smb'})关系,并设bypass_method: 'smbexec'; - 触发
cve-match-agent重算,它自动关联到CVE-2017-010,并生成psexec利用链。
这个过程里,Neo4j不是被动存储,而是主动参与决策:当标准扫描失效时,它保存了防火墙规则这一关键上下文,让后续Agent能绕过限制。而所有操作都通过Cypher完成,无需修改任何Agent代码——这才是图数据库在渗透测试中的真正价值:把安全知识变成可编程、可组合、可推理的数据结构。
提示:Neo4j社区版下载后默认密码是
neo4j,但Pentagi强制要求首次启动时必须改密。调度器在连接Neo4j前会执行MATCH (n) WHERE n.password = 'neo4j' RETURN count(n),若返回非零值则拒绝启动所有Agent。这是硬性安全红线,因为一旦靶场环境泄露,攻击者可通过默认密码直接写入恶意AttackStep节点。实测中,我们用neo4j-admin dbms set-initial-password在容器启动时自动改密,密码哈希存入Docker Secret,确保即使容器被逃逸也无法获取明文。
4. Pentagi的实战闭环:从靶场发现到攻击链生成的完整工作流
Pentagi的价值不在单个Agent多强大,而在五个核心Agent如何形成闭环反馈。整个工作流不是线性流水线,而是带状态检查的环形结构:Scan → Match → Exploit → Pivot → Report,每个环节的输出都成为下一个环节的输入,同时失败结果会触发上游重试或人工介入。下面以某制造业客户内网渗透为例,还原一次真实攻击链生成过程。
4.1 初始资产发现与图谱初始化
客户提供的初始信息只有一台跳板机IP:192.168.10.10。我们启动pentagi-init容器,它执行三个动作:
- 用
nmap -sn 192.168.10.0/24发现同网段所有存活主机,生成127个:Host节点; - 对每个Host执行
ping -c1和nc -zv -w1 <ip> 22,3389,445,1433,标记开放端口,生成:Port节点并关联RUNS关系; - 向Neo4j写入初始Cypher:
CREATE (:AttackStep {action:'init', status:'completed', timestamp:timestamp()})。
此时图谱已有127个Host节点,但Service节点为0——因为nmap -sn不探测服务。这正是Pentagi的设计:资产发现和服务发现分离,避免单点故障影响全局。
4.2 服务深度扫描与漏洞映射
调度器下发任务给nmap-scan-agent,目标是192.168.10.100(一台Web服务器)。Agent执行nmap -sV -p1-1000后,生成Cypher:
CREATE (:Service {name:'http', version:'Apache httpd 2.4.52', port:80})-[:RUNS_ON]->(:Host {ip:'192.168.10.100'}); CREATE (:Service {name:'ssh', version:'OpenSSH 8.9p1', port:22})-[:RUNS_ON]->(:Host {ip:'192.168.10.100'});接着cve-match-agent启动,它查询:
MATCH (s:Service {name:'http', version:'Apache httpd 2.4.52'}) MATCH (c:CVE) WHERE c.id ENDS WITH '2023' AND s.version CONTAINS c.affected_version RETURN c.id, c.cvss返回CVE-2023-25194(CVSS 7.5),并自动创建VULNERABLE_TO关系。
4.3 自动化利用与凭证提取
metasploit-exec-agent收到VULNERABLE_TO关系后,加载预置的exploit/unix/webapp/apache_httpd_mod_cgi_rce模块,生成Payload并执行。成功后,它不直接写Shell,而是向Neo4j写入:
CREATE (:Credential {username:'www-data', password:'shell_192.168.10.100_20231015', type:'shell', source:'apache_rce'})-[:OBTAINED_FROM]->(:AttackStep {action:'apache_rce', target:'192.168.10.100'}); MATCH (h:Host {ip:'192.168.10.100'}) CREATE (h)-[:HAS_CREDENTIAL]->(c);注意:这里password字段不是明文密码,而是会话标识符(shell连接字符串),实际凭证由pivot-agent在后续步骤中提取。
4.4 横向移动与权限提升
pivot-agent监听Neo4j中新增的HAS_CREDENTIAL关系,发现www-data凭证后,自动对同网段所有Host执行ssh www-data@<ip> 'id'。当连接到192.168.10.50(域控制器)时,返回uid=1001(www-data) gid=1001(www-data) groups=1001(www-data),0(root)——表明该账户在目标机上有sudo权限。pivot-agent立即生成提权Cypher:
MATCH (c:Credential {username:'www-data', source:'apache_rce'}) MATCH (h:Host {ip:'192.168.10.50'}) CREATE (c)-[:GRANTS_SUDO]->(h) SET h.privilege_level = 'root'此时图谱中192.168.10.50节点的privilege_level属性变为root,触发report-agent生成最终报告。
4.5 报告生成与人工复核
report-agent不生成PDF,而是输出结构化Markdown:
## 攻击链摘要 - **入口点**: 192.168.10.100 (Apache httpd 2.4.52) → CVE-2023-25194 RCE - **横向移动**: www-data@192.168.10.100 → ssh → 192.168.10.50 - **权限提升**: sudo /bin/bash on 192.168.10.50 → root - **关键凭证**: - `www-data:shell_192.168.10.100_20231015` (RCE session) - `administrator:Qwerty123!` (从LSASS内存提取,存于Credential节点) ## 验证命令 ```bash # 复现RCE curl -X POST http://192.168.10.100/cgi-bin/test.cgi --data "cmd=id" # 登录域控 ssh www-data@192.168.10.50 sudo /bin/bash这份报告的所有数据都来自Neo4j图谱查询,而非Agent日志拼接。当客户安全团队质疑“为什么没打补丁”,我们直接执行`MATCH (h:Host)-[r:VULNERABLE_TO]->(c:CVE) WHERE c.id='CVE-2023-25194' RETURN h.ip, r.verified`,返回`192.168.10.100, true`——证明漏洞真实存在且已验证。 > 实战心得:Pentagi最常被低估的能力是**失败路径的显性化**。传统渗透测试中,某个端口扫不出来,工程师可能就跳过;但在Pentagi里,`nmap-scan-agent`失败会生成`(:AttackStep {status:'failed', error:'timeout'})-[:FAILED_ON]->(:Host)`,调度器自动标记该Host为“需人工介入”,并在报告里高亮显示。某次能源项目中,我们因此发现了客户防火墙的ICMP限速策略——这是手动测试极易忽略的细节。Pentagi不追求100%自动化,而是让所有不确定性变得可见、可追溯、可决策。 ## 5. 从零搭建Pentagi:可复现的最小可行环境(MVP)配置 网上搜“pentagi安装教程”找不到结果,因为它根本不是一键安装的产品,而是一套**可裁剪的工程实践框架**。下面给出在Windows 10 + WSL2环境下,30分钟内跑通核心闭环的MVP配置。所有组件均使用官方镜像,无第三方魔改。 ### 5.1 环境准备:WSL2 + Docker Engine + Neo4j **第一步:启用WSL2** ```powershell # PowerShell管理员运行 dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart # 重启后下载WSL2内核更新包并安装 wsl --install wsl -l -v # 确认版本为Ubuntu-22.04第二步:在WSL2中安装Docker Engine(非Docker Desktop)
# Ubuntu终端执行 sudo apt update && sudo apt install -y ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg echo "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu "$(."$(lsb_release -cs)")" stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin sudo usermod -aG docker $USER # 退出WSL2,重新进入,验证 docker run hello-world第三步:启动Neo4j社区版(带认证)
# 创建Neo4j数据目录 mkdir -p ~/neo4j/data ~/neo4j/logs # 运行容器(首次启动会生成初始密码) docker run \ --name neo4j-pentagi \ -p 7474:7474 -p 7687:7687 \ -v ~/neo4j/data:/data -v ~/neo4j/logs:/logs \ -e NEO4J_AUTH=neo4j/changeme \ -e NEO4J_dbms_connector_https_advertised__address=localhost:7473 \ -d docker.io/library/neo4j:5.14.0 # 获取初始密码(首次启动日志) docker logs neo4j-pentagi 2>&1 | grep "Starting Node" -A5 # 用浏览器访问 http://localhost:7474,用neo4j/changeme登录,立即改密5.2 核心Agent镜像构建与调度
创建nmap-scan-agent镜像
# 新建目录 nmap-agent/ mkdir nmap-agent && cd nmap-agent # 创建Dockerfile cat > Dockerfile << 'EOF' FROM nmap:7.94 LABEL pentagi.action="port_scan" LABEL pentagi.input="json://target.json" LABEL pentagi.output="cypher://create_service_node.cql" RUN apt-get update && apt-get install -y python3 jq && rm -rf /var/lib/apt/lists/* COPY scan.sh /app/scan.sh COPY create_service_node.cql /app/create_service_node.cql ENV NEO4J_URI="bolt://host.docker.internal:7687" ENV NEO4J_USER="neo4j" ENV NEO4J_PASSWORD="your_new_password" CMD ["/app/scan.sh"] EOF # 创建scan.sh cat > scan.sh << 'EOF' #!/bin/bash set -e TARGET=$(cat /input/target.json | jq -r '.target_ip') PORTS=$(cat /input/target.json | jq -r '.ports // "1-100"') nmap -sS -p "$PORTS" -T4 "$TARGET" -oX /tmp/nmap.xml 2>/dev/null python3 -c " import sys, json, xmltodict with open('/tmp/nmap.xml') as f: data = xmltodict.parse(f.read()) services = [] for host in data.get('nmaprun', {}).get('host', []): for port in host.get('ports', {}).get('port', []): if port.get('state', {}).get('@state') == 'open': service = { 'name': port.get('service', {}).get('@name', 'unknown'), 'version': port.get('service', {}).get('@version', 'unknown'), 'port': int(port.get('@portid')) } services.append(service) print(json.dumps(services)) " > /tmp/services.json echo "CREATE " > /output/cypher.cql while IFS= read -r service; do NAME=$(echo "$service" | jq -r '.name // "unknown"') VERSION=$(echo "$service" | jq -r '.version // "unknown"') PORT=$(echo "$service" | jq -r '.port') echo "(:Service {name:'$NAME', version:'$VERSION', port:$PORT})-[:RUNS_ON]->(:Host {ip:'$TARGET'});" >> /output/cypher.cql done < /tmp/services.json EOF # 构建镜像 docker build -t pentagi/nmap-scan .编写简易调度器(scheduler.py)
#!/usr/bin/env python3 import json, subprocess, sys, time from pathlib import Path def run_agent(agent_name, input_data): # 创建临时输入目录 input_dir = Path(f"/tmp/pentagi_input_{int(time.time())}") input_dir.mkdir() (input_dir / "target.json").write_text(json.dumps(input_data)) # 运行容器 result = subprocess.run([ "docker", "run", "--rm", "--network=host", # 直连宿主机网络 "-v", f"{input_dir}:/input:ro", "-v", "/tmp:/output:rw", "-e", "NEO4J_PASSWORD=your_new_password", f"pentagi/{agent_name}" ], capture_output=True, text=True) if result.returncode != 0: print(f"Agent {agent_name} failed: {result.stderr}") return None # 读取输出Cypher cypher_file = Path("/tmp/cypher.cql") if cypher_file.exists(): return cypher_file.read_text() return None # 执行一次扫描 if __name__ == "__main__": cypher = run_agent("nmap-scan", {"target_ip": "127.0.0.1", "ports": "22,80,443"}) if cypher: print("Generated Cypher:") print(cypher)5.3 验证与调试:让第一个Cypher写入Neo4j
运行调度器后,你会在/tmp/cypher.cql看到类似内容:
CREATE (:Service {name:'ssh', version:'OpenSSH 8.9p1', port:22})-[:RUNS_ON]->(:Host {ip:'127.0.0.1'}); CREATE (:Service {name:'http', version:'nginx/1.18.0', port:80})-[:RUNS_ON]->(:Host {ip:'127.0.0.1'});手动执行写入:
# 安装cypher-shell(Neo4j客户端) wget https://dist.neo4j.org/neo4j-community-5.14.0-unix.tar.gz tar -xzf neo4j-community-5.14.0-unix.tar.gz cd neo4j-community-5.14.0/bin ./cypher-shell -u neo4j -p your_new_password -f /tmp/cypher.cql然后在Neo4j Browser里执行:
MATCH (h:Host)-[r:RUNS_ON]->(s:Service) RETURN h.ip, s.name, s.port你应该看到127.0.0.1的SSH和HTTP服务节点——恭喜,你的Pentagi MVP环境已通电。
最后一个小技巧:在真实靶场中,我们用
docker-compose.yml管理所有Agent,但绝不把Neo4j放在同一Compose文件里。因为Neo4j需要持久化存储和独立备份,而Agent容器应是无状态的。我们的标准做法是:Neo4j用单独的Docker run命令启动并绑定宿主机目录;Agent容器通过--network host直连,避免Compose网络带来的DNS解析延迟。这样既保证稳定性,又便于单个Agent升级——改完Dockerfile,docker build -t pentagi/nmap-scan . && docker kill nmap-scan-container,新容器启动即生效,图谱数据零丢失。