garak Harnesses 深入解析:探针调度、探测器选配与 LLM 漏洞扫描的三种执行模式
2026/9/16 16:06:49 网站建设 项目流程

garak Harnesses 深入解析:探针调度、探测器选配与 LLM 漏洞扫描的三种执行模式

【免费下载链接】garakthe LLM vulnerability scanner项目地址: https://gitcode.com/GitHub_Trending/ga/garak

garak(the LLM vulnerability scanner)的核心执行引擎是 Harness(调度器):它负责协调"探针(probe)生成攻击载荷 → 生成器(generator)产出响应 → 探测器(detector)评分 → 评估器(evaluator)判定"的完整闭环。本文以 index_harnesses.rst 为主线,结合仓库源码深入讲解Harness基类的运行机制,以及ProbewiseHarness(按探针推荐选择探测器)与PxD(探针 × 探测器全组合)两种内置实现的差异、选型依据与实战用法,帮助你在实际扫描任务中准确理解调度流程并做出正确的执行模式选择。

一、Harness 在 garak 中的作用与定位

garak 的扫描流水线由四个插件族协作完成:probes(构造攻击输入)、generators(被测模型接口)、detectors(判定模型输出是否"出问题")、evaluators(汇总评分),而harnesses正是把四者粘合在一起、控制整体执行顺序的调度层。文档 index_harnesses.rst 列出的三个模块即为仓库中的全部 harness 实现:

文档模块对应源码核心类
harnesses/basegarak/harnesses/base.pyHarness
harnesses/probewisegarak/harnesses/probewise.pyProbewiseHarness
harnesses/pxdgarak/harnesses/pxd.pyPxD

在 garak/harnesses/base.py 的模块 docstring 中,官方对 harness 的定义是:"A harness coordinates running probes on a generator, running detectors on the outputs, and evaluating the results."——即 harness 负责在 generator 上运行 probes、在输出上运行 detectors、并评估结果。所有 garak harness 都必须继承自Harness类。

从插件体系看,harnesses 与 probes、generators、buffs、detectors 一同在 _config.py 中被注册为插件类型(for plugin_type in ("probes", "generators", "buffs", "detectors", "harnesses")),并通过 _plugins.enumerate_plugins("harnesses") 自动枚举发现,说明 harness 本身也是一种可扩展、可插拔的组件。

二、基类 Harness:一次扫描的完整生命周期

Harness类继承自Configurable(配置驱动基类),其核心方法run()定义了所有 harness 共享的主循环。理解它的执行顺序,就理解了 garak 任何一次扫描的底层骨架。

2.1 初始化与运行时服务检查

Harness.__init__()(base.py)首先调用self._load_config(config_root)加载配置,随后调用模块级函数_initialize_runtime_services()(base.py):该函数依次检查langserviceintentservice两个运行时服务是否启用(service.enabled()),若启用则打印启动信息并执行service.load();一旦任一服务初始化抛出GarakException,会记录 critical 日志并直接终止本次运行。这意味着启用语言服务/意图服务的扫描在 harness 加载阶段就会完成依赖校验。

Harness的类属性还包括active = Trueextra_dependency_names = [](用于声明额外依赖模块名),以及唯一的默认参数DEFAULT_PARAMS = {"strict_modality_match": False}(base.py)。

2.2 run() 主循环:探测、检测、评估三步曲

Harness.run(model, probes, detectors, evaluator, announce_probe=True)(base.py)是核心调度方法,完整流程如下:

  1. 空输入校验:若detectorsprobes为空,分别抛出ValueError("No detectors, nothing to do")/ValueError("No probes, nothing to do")(verbose >= 2 时同步打印)。
  2. 运行钩子self._start_run_hook()记录当前各 HTTP 库的 User-Agent,并统一替换为_config.run.user_agentself._end_run_hook()在结束时恢复原值,保证扫描期间 HTTP 请求携带统一标识。
  3. 插件缓存快照_emit_plugin_cache_entry(self, model, *probes, *detectors, *_config.buffmanager.buffs)(base.py)将本次运行涉及的全部插件类路径与元数据以entry_type: plugin_cache写入 report.jsonl,便于事后还原运行环境。
  4. 逐探针调度:对每个 probe:
    • 模态匹配检查:调用_modality_match(probe.modality["in"], model.modality["in"], self.strict_modality_match)(base.py)。非严格模式(默认)下,要求生成器接受集合包含探针输入模态集合(set(probe_modality).intersection(generator_modality) == set(probe_modality));严格模式下要求两者完全相等。不匹配的探针被跳过并记录 warning——这正是strict_modality_match参数的实战意义。
    • 执行探测attempt_results = probe.probe(model),返回结果必须是 list 或 generator(有断言保护)。
    • 运行探测器:非IntentProbe时,对每个探测器调用self._run_detector(attempt_results, d)IntentProbe则走意图驱动的探测器选择路径(见下节)。
    • 落盘与评估:将每个 attempt 标记为ATTEMPT_COMPLETE并以 JSONL 追加写入 reportfile,最后evaluator.evaluate(attempt_results)汇总评分。
  5. 收尾self._end_run_hook()恢复 User-Agent。

_run_detector()(base.py)使用tqdm进度条逐条扫描 attempt,并将进度条描述设置为探针名/探测器名格式(对应 issue #324 的改进),长任务运行时用户能直观看到当前正在评分的是哪个探针-探测器组合;探测器若设置了skip属性则直接跳过。

2.3 IntentProbe 的探测器动态决议

当探针是IntentProbe时,探测器不再由调用方静态指定,而是由intentservice根据每个 attempt 携带的intent动态决定(base.py):harness 先汇总所有观测到的 intent,再为每个 intent 查询intentservice.get_detectors(intent)得到候选探测器集合;对某个 intent 未配置探测器时,回退使用调用方传入的探测器名集合;随后为每个决议出的探测器加载插件,并只对映射到该探测器的 attempt 子集执行评分。决议出的探测器还会补发plugin_cache条目,保证 report.jsonl 的插件快照完整。这是 garak 从"静态探测器列表"走向"意图驱动动态检测"的关键机制。

三、ProbewiseHarness:按探针推荐智能选配探测器

ProbewiseHarness(probewise.py)是默认执行模式:探测器不显式指定时,garak 会依据每个探针的推荐来选择探测器。其run(model, probenames, evaluator, buff_names=None)流程如下:

  1. buff_namesNone则置空列表,空探针列表抛出 "No probes, nothing to do"。
  2. 调用self._load_buffs(buff_names)实例化指定的 buff(base.py):逐个通过_plugins.load_plugin(buff_name)加载并挂到_config.buffmanager.buffs,加载失败会打印❌🦾 buff load error类错误但不会中断运行。
  3. 探针按名称排序后打印队列(🕵️ queue of probes: ...),随后逐个探针执行。
  4. 每个探针的探测器选择遵循文档中明确给出的判定公式:
    • 若探针声明了primary_detector,且全局配置_config.plugins.extended_detectorsTrue,则使用primary_detectorextended_detectors并集
    • 若探针声明了primary_detectorextended_detectorsFalse(或_config.args未设置),则仅使用primary_detector
    • 若探针未声明primary_detector(值为None),则回退到探针的recommended_detector列表(源码保留了对旧字段recommended_detector的兼容路径,并打印 deprecation notice,提示迁移版本为0.9.0.6)。
  5. 对每个探针调用基类super().run(model, [probe], detectors, evaluator, announce_probe=False),完成探测-检测-评估闭环。

--extended_detectors正是控制该逻辑的 CLI 开关(cli.py),其 help 文案明确指出:"If detectors aren't specified on the command line, should we run all detectors? (default is just the primary detector, if given, else everything)"。该配置项也出现在预置配置中:bag.yaml设为extended_detectors: true(bag.yaml),fast.json设为false(fast.json),供用户按扫描深度与耗时取舍。

适用场景:默认扫描、快速巡检、或希望"每个探针用最对口探测器"的场景。由于探测器按探针推荐动态加载,报告更聚焦、误报面更小,也是 interactive.py 交互式命令行采用的 harness。

四、PxD:探针 × 探测器全组合的穷举扫描

PxD(probes x detectors,见 pxd.py)采用"笛卡尔积"策略:运行所有指定探针,并用所有指定探测器分析每个探针的结果。其run(model, probe_names, detector_names, evaluator, buff_names=None)流程:

  1. 探针与探测器分别排序,打印两条队列信息(🕵️ queue of probes: ...🔎 queue of detectors: ...)。
  2. 调用_load_buffs(buff_names)加载 buff。
  3. 逐探针加载(加载异常打印probename load exception 🛑, skipping >>,加载失败打印load failed ⚠️, skipping >>,均不中断整个队列)。
  4. 对每个探针,遍历全部detector_names,通过_plugins.load_plugin(detector_name, break_on_fail=False)实例化,加载失败的探测器被跳过并记录 error;最后调用基类run()一次性对该探针跑完所有成功加载的探测器。

模块 docstring 明确说明了其定位与代价:"It's thorough, and might end up doing some comparisons that don't make so much sense, because not all detectors are designed to pick up failure modes in all situations."——它足够彻底,但由于并非所有探测器都为所有故障模式设计,可能产生一些意义不大的比较,计算成本也更高。

适用场景:完整基线评估、探测器效果横向对比、或需要"不遗漏任何检测视角"的合规性全面扫描。

五、两种模式的调度入口与实战命令

harness 的选择最终由 CLI 决定(cli.py):

if parsed_specs["detector"] == []: command.probewise_run( generator, parsed_specs["probe"], evaluator, parsed_specs["buff"] ) else: command.pxd_run( generator, parsed_specs["probe"], parsed_specs["detector"], evaluator, parsed_specs["buff"], )

即:未通过--detectors/-d指定探测器时走 ProbewiseHarness,指定了探测器列表则走 PxD。两个入口分别封装在 command.py 的probewise_run()pxd_run()中,各自实例化对应 harness 类并调用其run()

实战示例:

# 不指定探测器:ProbewiseHarness,按探针推荐检测 garak --model_type openai --model_name gpt-3.5-turbo --probes dan # 指定探测器:PxD,全组合扫描 garak --model_type openai --model_name gpt-3.5-turbo \ --probes dan --detectors always.Pass,always.Fail # 启用扩展探测器(probewise 模式下跑探针的 primary + extended 并集) garak --model_type openai --model_name gpt-3.5-turbo \ --probes dan --extended_detectors

扫描过程中,report.jsonl 会实时写入每次 attempt 的检测结果;--extended_detectors未指定时,probewise 模式默认只跑primary_detector(若探针给出),否则回退到recommended_detector列表。

六、源码与测试中的设计印证

  • 模态匹配语义:测试 test_harness_modality_match() 完整覆盖了严格/非严格模式的判定:严格模式下text探针配vision生成器返回False;非严格模式下{"text","image"}探针配{"text","vision","image"}生成器返回True(生成器接受超集即可)。
  • 进度条可见性:测试 test_harness_detector_progress_shows_probe_name() 用generators.test.Blankprobes.test.Blankdetectors.always.Pass验证探测器进度条必须携带探针名/探测器名描述,确保长任务可观测性(issue #324)。
  • 插件结构约束:参数化测试 test_harness_structure() 对每个自动枚举出的 harness 类断言:DEFAULT_PARAMS中的每个参数必须被_supported_params支持,即"有默认值的参数必须可配置",这是 garak 插件契约的一部分。
  • 健壮性保障:测试 test_harness_unscorable_outputs_do_not_halt_probe_queue() 验证单个探针产生不可评分输出时不会中断整个探针队列。
  • 交互模式一致性:interactive.py 中的交互式终端同样固定使用ProbewiseHarness执行单探针扫描,说明 probewise 是面向日常使用的主路径。

七、选型总结

维度ProbewiseHarnessPxD
触发方式未指定--detectors时自动使用显式指定--detectors时使用
探测器来源探针的primary_detector/extended_detectors/recommended_detector命令行指定的全部探测器
覆盖策略按探针推荐,聚焦、低误报探针×探测器全组合,穷举、彻底
代价快速,适合日常巡检与默认运行计算量大,可能产生语义牵强的组合
典型场景日常扫描、交互式终端、CI 快速回归全面基线评估、探测器横向对比、合规审计

无论选择哪种 harness,基类Harness都会保证:模态匹配过滤 → 探针执行 → 探测器评分 → 结果 JSONL 落盘 → 评估器汇总的完整链路,并将插件缓存与 User-Agent 管理等横切关注点统一处理。理解了这套调度架构,你就可以在 garak 的配置与命令行参数之间自如编排,让每次漏洞扫描既快又准。

【免费下载链接】garakthe LLM vulnerability scanner项目地址: https://gitcode.com/GitHub_Trending/ga/garak

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询