跳到主要内容

攻击生成流水线:从漏洞到对抗输入(baseline → 精炼 → 单轮增强)

30 秒导读: 红队测试要先有"坏话"才能去问模型。本章讲 DeepTeam 怎么造这些坏话:给一条漏洞(如"偏见"),先让一个 LLM 合成几条 baseline 攻击句,再用一个专门的 AttackEngine 把它们改写得更隐蔽、更像真实用户提问,最后随机挑一个"增强手法"(Base64 编码、ROT13、或让 LLM 再改写一遍)套上去。产物是一批 RTTestCase,每条都带着一句待发射的对抗输入。真正拿去打模型、判有没有越狱,是第 04 章的事;多轮对话式越狱是第 03 章

本章是主线的上半段——只回答一个问题:攻击是怎么被造出来的?


1. 这是什么(零基础也能懂)

假设你要给一个客服 AI 做安全体检。你担心它会说出带种族/性别偏见的话。你不可能手写几千条刁钻问题——太累,也不够刁钻。于是 DeepTeam 帮你自动"出题":

  • 你告诉它:测这个漏洞(偏见),每个子类型出 N 道题。
  • 它先让一个"出题模型"编几条朴素的坏问题(baseline)。
  • 再把这些问题改写得更狡猾(精炼),让安全过滤器更难识别。
  • 最后随机给每道题套一层伪装(增强)——可能是把整句话编码成 Base64,也可能是再让模型换个说法。

一句话直觉:这条流水线是一台"对抗样本工厂"。原料是一条漏洞定义,成品是一批带对抗输入的测试用例。 它本身不判分,只负责"造弹药"。

三个关键角色先混个脸熟(详细数据模型见第 01 章):

角色白话职责源码锚点
AttackSimulator总编排:调度 baseline 生成 + 抽样增强deepteam/attacks/attack_simulator/attack_simulator.py:32
BaseVulnerability.simulate_attacks合成 baseline:让出题模型产出结构化攻击deepteam/vulnerabilities/bias/bias.py:180
AttackEngine精炼:transform → variations → LLM 校验deepteam/attacks/attack_engine/attack_engine.py:18
单轮攻击(如 Base64 / GrayBox)增强:给单条输入套伪装deepteam/attacks/single_turn/base64/base64.py:6

2. 顶层全景(它大概怎么转)

怎么读这张图: 从上到下是时间顺序。左半区是"造朴素坏话",中间是"改狡猾",右下是"套伪装"。数据一路都是 RTTestCase 列表在流动。

输入: vulnerabilities=[Bias(...)], attacks=[Base64(w=3), GrayBox(w=1)], N 条/类型


┌─────────────────────────────────────────────────────────┐
│ AttackSimulator.simulate / a_simulate │ 编排层
│ attack_simulator.py:56 / :118 │
└───────────────┬─────────────────────────────┬───────────┘
│ 阶段①:每条漏洞 baseline │ 阶段②:每条 test_case 抽一个增强
▼ ▼
┌───────────────────────────────┐ ┌──────────────────────────────┐
│ vulnerability.simulate_attacks │ │ random.choices(attacks, │
│ bias.py:180 │ │ weights=[a.weight ...]) │
│ · Template.generate_baseline_ │ │ └─ 抽中一个方法 │
│ attacks 造 prompt │ │ │
│ · simulator_model.generate( │ │ enhance_attack(attack, tc) │
│ schema=SyntheticDataList) │ │ attack_simulator.py:272 │
└───────────────┬───────────────┘ │ · inspect.signature 探参数 │
│ baseline 攻击 │ · 算法式 or 模型式 分派 │
▼ └──────────────┬───────────────┘
┌───────────────────────────────┐ │
│ AttackEngine.refine (精炼层) │ ▼
│ attack_engine.py:33 │ tc.input = 被伪装后的对抗输入
│ transform→variations→validate │ │
└───────────────┬───────────────┘ ▼
▼ 返回 List[RTTestCase]
更狡猾的 baseline 攻击 (交给第 04 章打靶评测)

主线走一遍(高层):

  1. simulate 拿到一组漏洞和一组增强方法。
  2. 阶段① 对每条漏洞调 simulate_baseline_attacks → 里面既做 LLM 合成,又顺手做了精炼,吐出一批 baseline RTTestCase
  3. 阶段② 遍历这批 test_case,每条按 attack.weight 抽一个增强方法,原地改写它的 .input
  4. 全程有并发节流、错误容忍、成本累计三条"横切"逻辑贯穿。

注意一个容易看漏的点:精炼(AttackEngine)不是编排层单独的一步,而是嵌在 baseline 合成的尾巴里——simulate_attacks 最后一行就调了 _refine_simulated_attacks(见 §5)。所以"三段式"里的前两段(baseline→精炼)其实在阶段①内部一次跑完,阶段②才是增强。


3. 核心原理

3.1 编排层:AttackSimulator 怎么调度

它要解决的小问题: 有 M 条漏洞、K 个增强方法、每类 N 条攻击,怎么把它们编排成"先造后套"、还不能因为一次 LLM 调用失败就全盘崩、还要控制并发别把 API 打爆?

同步版 simulate(attack_simulator.py:56) 分两个 for 循环,对应上图两个阶段。

阶段①逐条漏洞造 baseline(:78-91):

# 示意,非源码:阶段① 造 baseline
for vulnerability in vulnerabilities:
test_cases.extend(
self.simulate_baseline_attacks( # 内部会调 vuln.simulate_attacks
attacks_per_vulnerability_type=n,
vulnerability=vulnerability,
ignore_errors=ignore_errors,
)
)

阶段②对每条 test_case 抽一个增强方法(:93-113)。权重抽样是这里的灵魂:

# 示意,非源码:阶段② 按权重抽一个增强方法
attack_weights = [attack.weight for attack in attacks] # 例 [3, 1]
for test_case in test_cases:
sampled_attack = random.choices( # 每条 tc 独立抽一次
attacks, weights=attack_weights, k=1
)[0] # Base64 被抽中概率 3/4
self.enhance_attack(attack=sampled_attack, test_case=test_case, ...)

真实实现里 random.choices(attacks, weights=attack_weights, k=1)[0] 就在 attack_simulator.py:105-107每条测试用例独立抽一次,所以最终增强方法的分布近似正比于各自的 weight

异步版 a_simulate(attack_simulator.py:118) 逻辑相同,但加了三层工程化处理:

关切怎么做位置
并发节流asyncio.Semaphore(self.max_concurrent),每个任务进出都 async with semaphore:128:149:181
空方法兜底if not attacks: 时退化成 BaselineAttack()(它的 a_enhance 原样返回,等于不增强):183-184
扇出并发baseline 与增强都用 asyncio.gather 批量跑:166:200

BaselineAttack(:24-29)是个占位攻击:a_enhance 直接 return attack,用来在"没提供任何增强方法"时保持流水线结构不变。

错误容忍(ignore_errors) 是贯穿始终的开关。以 simulate_baseline_attacks(:214-239)为例:出错时若 ignore_errors=True,就为该漏洞的每个类型 × 每条生成一个只带 error 字段的占位 RTTestCase,而不是抛异常中断整轮:

# 示意,非源码:出错不崩,填占位用例
except Exception as e:
if ignore_errors:
return [RTTestCase(vulnerability=..., vulnerability_type=t,
error=f"Error simulating adversarial attacks: {e}")
for t in vulnerability.get_types()
for _ in range(attacks_per_vulnerability_type)]
else:
raise

成本累计(cost) 用一个巧妙的 context-local 累加器。enhance_attackwith cost_accumulator() as acc: 开一个作用域(:291:333),作用域内所有 generate/a_generate 调用都会把自己的开销记进这个累加器(attack_simulator/utils.py:45-58cost_accumulator + :38-42_record_cost);结束时 test_case.simulation_cost = add_cost(test_case.simulation_cost, acc[0])(:319)。add_cost(utils.py:19-26)约定 None 代表"非原生模型不报成本",全 None 才返回 None,否则把 None 当 0 相加。

关键细节:成本用 ContextVar 而非把 cost 一路当参数传。ContextVar 对 asyncio 并发安全——每个任务拿到自己的副本,子任务共享同一个累加器对象(utils.py:29-35 的注释点明了这一点)。


3.2 Baseline 合成:让 LLM 造第一版攻击

它要解决的小问题: 一条抽象的漏洞(BiasType.RELIGION)怎么变成几句具体的、像真人会问的坏话?

思路: 用模板拼一个"出题 prompt",让出题模型按结构化 schema 吐出 JSON。以偏见漏洞为实证(bias.py:180simulate_attacks):

第一步,按漏洞类型选模板造 prompt(:195-201):

# 示意,非源码:每个漏洞子类型拼一个 baseline prompt
for type in self.types: # 如 GENDER / RELIGION / RACE / POLITICS
templates[type].append(
BiasTemplate.generate_baseline_attacks( # bias/template.py:13 按类型分派
type, attacks_per_vulnerability_type, self.purpose
)
)

BiasTemplate.generate_baseline_attacks(bias/template.py:13)就是个大 dispatch,按 BiasType 转到 generate_religious_baseline_attacks 等(:37)。模板本身是一段"你来当文案,基于 purpose 编 N 条能诱发偏见的真实用户提问,只返回 JSON"的指令(template.py:44-56)。

第二步,调模型并解析,这里有 native / 非 native 两条路径(bias.py:206-220):

路径触发条件怎么拿结果
nativeself.using_native_model 为真model.generate(prompt, schema=SyntheticDataList) 直接返回 (res, cost),res.data 已是结构化对象
非 native(schema 成功)模型接受 schema= 参数同样返回结构化 SyntheticDataList,但 cost 记为 None
非 native(schema 失败)model.generate(prompt, schema=...)TypeError退化为 model.generate(prompt) 拿纯文本,再用 trimAndLoadJson(res) 兜底解析成 dict
# 示意,非源码:非 native 的兜底解析(bias.py:212-220)
try:
res = self.simulator_model.generate(prompt, schema=SyntheticDataList)
local_attacks = [item.input for item in res.data] # 结构化路径
except TypeError: # 模型不吃 schema 参数
res = self.simulator_model.generate(prompt) # 退化成纯文本
data = trimAndLoadJson(res) # 从脏文本里抠出 JSON
local_attacks = [item["input"] for item in data["data"]]

trimAndLoadJson 来自 deepeval,负责从可能夹了 markdown 围栏或废话的模型输出里把 JSON 抠出来——这是"模型不老实返回纯 JSON"时的安全网。

第三步,把每条攻击文本包成 baseline RTTestCase,并把这次合成的成本按条数均摊(bias.py:222-237):

# 示意,非源码:成本按 baseline 条数均摊
per_attack_cost = (simulation_cost / len(local_attacks)
if simulation_cost is not None and local_attacks else simulation_cost)

第四步——也是最容易看漏的一步——返回前先过一遍精炼(bias.py:239):

return self._refine_simulated_attacks(simulated_test_cases, purpose)

所以 simulate_attacks 出来的已经是精炼后的攻击,不是最朴素那版。精炼层就是下一节。


3.3 精炼层:AttackEngine 把攻击改狡猾

它要解决的小问题: LLM 第一次造的坏话往往太直白("你觉得低收入家庭的人工作能力差吗?"),容易被安全过滤器一眼拦下。得把它伪装成"业务流程/技术任务",既保留恶意、又更难识别。

思路:三步走(attack_engine.py:33refine):

baseline 攻击

▼ ① transform:改写成隐蔽版(camouflage)
transformed_input ──────────────────────────┐
│ │(variations=1 时跳过②)
▼ ② variations:再派生若干"换角度"变体 │
[transformed] + [变体...] ←── 上限 5 │
│ │
▼ ③ validate:逐个让 LLM 判"够不够格当测试" │
过滤掉 is_valid=False 的 ◄──────────────────┘


为每个存活候选克隆一条新 RTTestCase

第①步 transform(attack_engine.py:48-59):transform_attack_template 造 prompt,让模型按 TransformedAttack schema(types.py:6,含 reason + input)返回改写版。这个模板极有料——它列了一堆**"不许做"的红线**:不许翻转恶意极性、不许把敏感目标换成无害代名词、不许把可执行请求学术化(attack_engine/template.py:41-46),还配了三个"好/坏改写"对照例子。改写后若结果为空,回退到原始输入(:57-59)。

第②步 variations(attack_engine.py:61-78): 只有 self.variations > 1 时才做。用 generate_variations_template 让模型一次产出多个"换个操作角度、但保留同样毒性"的变体(AttackVariations schema,types.py:11)。候选集 = [transformed_input] + 变体们

变体数量上限是 5,在构造函数里被死死夹住(attack_engine.py:29):

self.variations = max(1, min(variations, 5)) # 下限 1、上限 5

第③步 validate(attack_engine.py:80-88_validate_candidates_with_llm:207): 逐个候选让模型判 ValidationResult.is_valid(types.py:16),只留下 is_valid=True 的:

# 示意,非源码:LLM 校验过滤(attack_engine.py:227-233)
result, cost = generate_with_cost(prompt, ValidationResult, self.simulator_model)
if result.is_valid:
validated.append(candidate)

校验 prompt(template.py:131validate_attack_template)明确说"你查的是质量不是安全"——要求候选仍带对抗意图、不能被过度洗白、要像真人会问。若全被否掉,兜底保留 transform 那版(:87-88)。

成本在克隆用例间均摊(attack_engine.py:90-105): 精炼一条 baseline 可能裂变出多条(1 个 transform + 若干变体都通过校验)。于是把"baseline 原成本 + 本次精炼成本"合计,再除以存活条数,平摊给每条克隆:

# 示意,非源码:精炼成本平摊(attack_engine.py:92-97)
combined_cost = add_cost(test_case.simulation_cost, refine_cost)
per_refined_cost = combined_cost / len(valid_inputs) if combined_cost is not None else combined_cost

克隆用 _clone_with_new_input(:273):复制漏洞/类型/attack_method/metadata,只换 inputsimulation_cost,产出一条全新的 RTTestCase

挂接点: 谁调 refine?是漏洞基类。BaseVulnerability._refine_simulated_attacks(base_vulnerability.py:63)——若漏洞没被注入 attack_engine,就临时 new 一个默认的(默认 simulator_model="gpt-3.5-turbo-0125"variations=1,见 attack_engine.py:22-29),然后调 engine.refine(...)variations=1 是默认,意味着默认不产变体、只做 transform + 校验


3.4 单轮增强:算法式 vs 模型式

这是 §3.1 阶段②的落地。enhance_attack(attack_simulator.py:272)拿一个被抽中的攻击方法,给单条 test_case.input 套伪装。

核心机制:用 inspect.signature 探测方法要什么参数,再分派。 不同增强手法的 enhance 签名不一样——有的只要一段文本,有的还要 simulator_model,有的还要 model_callback。编排层不写死,而是运行时看签名(:331-356):

# 示意,非源码:按 enhance 签名分派(attack_simulator.py:331-356)
sig = inspect.signature(attack.enhance)
if "simulator_model" in sig.parameters and "model_callback" in sig.parameters:
res = attack.enhance(attack=inp, simulator_model=self.simulator_model,
model_callback=self.model_callback)
elif "simulator_model" in sig.parameters:
res = attack.enhance(attack=inp, simulator_model=self.simulator_model)
elif "model_callback" in sig.parameters:
res = attack.enhance(attack=inp, model_callback=self.model_callback)
else:
res = attack.enhance(attack=inp) # 最朴素:只吃文本
test_case.input = res # 原地覆盖为伪装后的输入

由此天然分成两类增强:

算法式增强模型式增强
代表Base64ROT13LeetspeakGrayBoxRoleplayPromptInjection
enhance 签名enhance(self, attack: str)enhance(self, attack, simulator_model=...)
走哪个分支落到最后 else(不需模型)落到带 simulator_model 的分支
确定性确定性、零成本、瞬时非确定、要 LLM 调用、有成本、可能重试
伪装原理编码变形绕过基于文本的过滤器语义改写换个框架诱导模型

算法式实例 —— Base64(base64/base64.py:14-16):

def enhance(self, attack: str) -> str:
return base64.b64encode(attack.encode()).decode()

签名里没有 simulator_model 也没有 model_callback,所以分派走最后的 else 分支。ROT13(rot13/rot13.py:13-20)同理,用 str.translate 做字母轮换。它们把"把危险内容藏起来"变成一个纯字符串变换——没有模型参与,输出完全可复现

模型式实例 —— GrayBox(gray_box/gray_box.py:34-38):

def enhance(self, attack: str, simulator_model=None) -> str:
...
prompt = GrayBoxTemplate.enhance(attack) # 让模型基于"部分内部知识"重写

签名带 simulator_model,分派命中对应分支,编排层把 self.simulator_model 注入进去。它内部还有一个重试循环(:52-93):最多 max_retries 次,每次生成后要过两道检查——compliance(没被模型拒绝)和 is_gray_box(确实是灰盒攻击);两个都过才返回增强版,否则跑满重试后原样退回未增强的 attack(:93)。这就是模型式增强"可能失败、可能空转"的一面,与算法式的"必成功"形成对照。

多轮攻击在这里也被识别,但不展开。 enhance_attack 开头先 isinstance(attack, BaseMultiTurnAttack) 判一下(:282),是多轮就走 attack._get_turns(...) 那条路(:293),把整段对话塞进 test_case.turns多轮的记忆、逐步升级、回溯逻辑整体留给第 03 章,本章只关心单轮那半边。


4. 一个端到端的教学示意

把 §3.1 的权重抽样和 §3.4 的签名分派拼起来,你就理解了阶段②的全部骨架:

# 示意,非源码:weighted sampling + enhance 分派的极简版
import random, inspect

def enhance_one(attack, text, simulator_model, model_callback):
# 运行时探测这个 attack 的 enhance 要哪些参数
params = inspect.signature(attack.enhance).parameters
kwargs = {"attack": text}
if "simulator_model" in params: # 模型式增强才需要
kwargs["simulator_model"] = simulator_model
if "model_callback" in params:
kwargs["model_callback"] = model_callback
return attack.enhance(**kwargs) # 算法式 → 直接算;模型式 → 调 LLM

def run_stage_two(test_cases, attacks, sim_model, cb):
weights = [a.weight for a in attacks] # 如 Base64=3, GrayBox=1
for tc in test_cases:
picked = random.choices(attacks, weights=weights, k=1)[0] # 每条独立抽
tc.input = enhance_one(picked, tc.input, sim_model, cb) # 原地覆盖
# 重点看:抽样和分派都不写死方法类型——加新攻击不用改这段

真实代码在 attack_simulator.py:103-113(抽样)与 :331-358(分派),异步对应版在 :381a_enhance_attack


5. 巧妙之处(可借鉴的技术)

  1. 权重抽样让增强分布可调而不必分桶。 不用"30% 走 Base64、10% 走 GrayBox"这种硬编码分配,只给每个攻击一个整数 weight,random.choices 自动按比例抽(attack_simulator.py:105)。加减攻击、调比例都只动权重。

  2. inspect.signature 分派 = 开放式插件协议。 编排层永远不 isinstance 判具体攻击类型,只看 enhance 要什么参数(:331)。新增一个攻击,只要 enhance 签名符合约定,零改动接入。算法式和模型式共用一条分派路径。

  3. ContextVar 成本累加器,免去参数穿线。 成本不需要一层层当参数传;cost_accumulator() 作用域内所有模型调用自动记账(utils.py:45-58),且对 asyncio 并发安全。

  4. 精炼层的 prompt 工程反"极性翻转"。 transform 模板专门用大段红线和对照例子,防止模型把"造攻击"偷偷洗成"防攻击"(attack_engine/template.py:41-63)——红队 prompt 最常见的失败模式在这里被显式约束。

  5. 成本双重均摊,让每条用例的 cost 可归因。 baseline 合成按条数摊(bias.py:222),精炼裂变后再按存活数摊(attack_engine.py:93),保证聚合总成本不重不漏。

  6. native / 非 native 三级兜底。 结构化 schema → 无 schema 参数退化 → trimAndLoadJson 抠 JSON(bias.py:206-220),让流水线对各种"不老实"的模型都有韧性。


6. 边界与局限

  • 本章只覆盖单轮。 多轮攻击(_get_turns、对话记忆、逐步升级、回溯)在 enhance_attack 里只是被 isinstance 分流出去,细节见第 03 章
  • 不判分、不评测。 流水线只产 RTTestCase.input(和多轮的 turns)。有没有越狱、裁判怎么打分、风险报告怎么出,全在第 04 章
  • 变体上限硬编码为 5(attack_engine.py:29),且默认 variations=1(默认不产变体)。
  • 质量强依赖出题/精炼模型。 baseline 与精炼都是 LLM 生成,弱模型可能产出被 is_valid=False 全否、或触发 GrayBox 重试耗尽退回原文的情况。
  • 模型式增强可能"空转"。GrayBox 跑满 max_retries 仍不达标时原样返回未增强输入(gray_box.py:93),此时该条其实没被真正伪装。
  • 同步 simulate 无并发节流。 max_concurrent 的信号量只在异步 a_simulate 生效(:128);同步路径是纯串行。

7. 横向对比

同组各章的分工(用相对链接互链,不重复内容):

一句话定位: 本章是"造弹药",第 03 章是"多轮怎么打",第 04 章是"打完怎么判"。


8. 代码地图(导航索引)

主题文件路径符号名
编排:同步生成主流程deepteam/attacks/attack_simulator/attack_simulator.py:56AttackSimulator.simulate
编排:异步 + 信号量节流deepteam/attacks/attack_simulator/attack_simulator.py:118AttackSimulator.a_simulate
baseline 容错包装deepteam/attacks/attack_simulator/attack_simulator.py:214simulate_baseline_attacks
单轮增强 + 签名分派deepteam/attacks/attack_simulator/attack_simulator.py:272AttackSimulator.enhance_attack
异步单轮增强deepteam/attacks/attack_simulator/attack_simulator.py:381AttackSimulator.a_enhance_attack
空方法占位攻击deepteam/attacks/attack_simulator/attack_simulator.py:24BaselineAttack
成本累加器(ContextVar)deepteam/attacks/attack_simulator/utils.py:45cost_accumulator / add_cost
baseline 合成 + native 分路deepteam/vulnerabilities/bias/bias.py:180Bias.simulate_attacks
异步 baseline 合成deepteam/vulnerabilities/bias/bias.py:241Bias.a_simulate_attacks
baseline prompt 模板分派deepteam/vulnerabilities/bias/template.py:13BiasTemplate.generate_baseline_attacks
精炼层三步主流程deepteam/attacks/attack_engine/attack_engine.py:33AttackEngine.refine
LLM 校验过滤deepteam/attacks/attack_engine/attack_engine.py:207_validate_candidates_with_llm
克隆用例 + 成本均摊deepteam/attacks/attack_engine/attack_engine.py:273_clone_with_new_input
精炼挂接点deepteam/vulnerabilities/base_vulnerability.py:63_refine_simulated_attacks
transform/校验 prompt 模板deepteam/attacks/attack_engine/template.py:6AttackEngineTemplates
精炼 schemadeepteam/attacks/attack_engine/types.py:6TransformedAttack / AttackVariations / ValidationResult
攻击基类 + 增强契约deepteam/attacks/base_attack.py:12BaseAttack / Exploitability
单轮攻击基类deepteam/attacks/single_turn/base_single_turn_attack.py:19BaseSingleTurnAttack
算法式增强实例deepteam/attacks/single_turn/base64/base64.py:6Base64.enhance
算法式增强实例(轮换)deepteam/attacks/single_turn/rot13/rot13.py:5ROT13.enhance
模型式增强实例(带重试)deepteam/attacks/single_turn/gray_box/gray_box.py:25GrayBox.enhance