refactor(prompt): 将注意力和内容混淆统一为提示词扰动

本次提交重构了提示词修改逻辑,将之前独立的“注意力优化”和“内容混淆”功能合并为一个统一的概念:“提示词扰动”(Prompt Perturbation)。

主要变更包括:
- 在模型配置中引入新的统一选项:`enable_prompt_perturbation`, `perturbation_strength` 和 `enable_semantic_variants`。
- 将原 `AttentionOptimizer` 中的噪声注入和语义变体逻辑迁移到 `llm_models` 模块中,作为扰动策略的一部分。
- 简化 `attention_optimizer.py`,使其专注于提示词块重排 (`BlockShuffler`)。
- 更新 `_PromptProcessor` 以根据新的统一配置来协调不同的扰动技术。

此项更改为用户简化了配置,并通过集中化相关逻辑,提供了一个更清晰、更易于维护的实现。

BREAKING CHANGE: 内容混淆的相关配置已被替换。`enable_content_obfuscation` 和 `obfuscation_intensity` 配置项已移除。用户需更新配置以使用新的 `enable_prompt_perturbation` 和 `perturbation_strength`。
This commit is contained in:
minecraft1024a
2025-11-14 20:14:19 +08:00
committed by Windpicker-owo
parent 45b6d7d06c
commit 198976b68f
3 changed files with 211 additions and 378 deletions

View File

@@ -1,32 +1,24 @@
""" """
注意力优化器 - 防止提示词过度相似导致LLM注意力机制退化 注意力优化器 - 提示词块重排
通过轻量级随机化技术,在保持语义不变的前提下增加提示词结构多样性, 通过对可交换的block组进行随机排序增加提示词结构多样性,
避免短时间内重复发送高度相似的提示词导致模型回复趋同 避免因固定的提示词结构导致模型注意力退化
优化策略:
1. 轻量级噪声:随机调整空白字符、换行数量
2. 块重排定义可交换的block组随机调整顺序
3. 语义变体:使用同义措辞替换固定模板文本
""" """
import hashlib
import random import random
import re from typing import Any, ClassVar
from typing import Any, ClassVar, Literal
from src.common.logger import get_logger from src.common.logger import get_logger
from src.config.config import global_config
logger = get_logger("attention_optimizer") logger = get_logger("attention_optimizer_shuffle")
class AttentionOptimizer: class BlockShuffler:
"""提示词注意力优化""" """提示词Block重排"""
# 可交换的block组定义组内block可以随机排序 # 可交换的block组定义组内block可以随机排序
# 每个组是一个列表包含可以互换位置的block名称 # 每个组是一个列表包含可以互换位置的block名称
SWAPPABLE_BLOCK_GROUPS:ClassVar = [ SWAPPABLE_BLOCK_GROUPS: ClassVar = [
# 用户相关信息组(记忆、关系、表达习惯) # 用户相关信息组(记忆、关系、表达习惯)
["memory_block", "relation_info_block", "expression_habits_block"], ["memory_block", "relation_info_block", "expression_habits_block"],
# 上下文增强组(工具、知识、跨群) # 上下文增强组(工具、知识、跨群)
@@ -35,322 +27,53 @@ class AttentionOptimizer:
["time_block", "identity_block", "schedule_block"], ["time_block", "identity_block", "schedule_block"],
] ]
# 语义等价的文本替换模板 @staticmethod
# 格式: {原始文本: [替换选项1, 替换选项2, ...]} def shuffle_prompt_blocks(prompt_template: str, context_data: dict[str, Any]) -> tuple[str, dict[str, Any]]:
SEMANTIC_VARIANTS:ClassVar = {
"当前时间": ["当前时间", "现在是", "此时此刻", "时间"],
"最近的系统通知": ["最近的系统通知", "系统通知", "通知消息", "最新通知"],
"聊天历史": ["聊天历史", "对话记录", "历史消息", "之前的对话"],
"你的任务是": ["你的任务是", "", "你需要", "你应当"],
"请注意": ["请注意", "注意", "请留意", "需要注意"],
}
def __init__(
self,
enable_noise: bool = True,
enable_semantic_variants: bool = False,
noise_strength: Literal["light", "medium", "heavy"] = "light",
cache_key_suffix: str = "",
):
""" """
初始化注意力优化器 根据定义的SWAPPABLE_BLOCK_GROUPS对上下文数据中的block进行随机重排
并返回可能已修改的prompt模板和重排后的上下文。
Args: Args:
enable_noise: 是否启用轻量级噪声注入(空白字符调整) prompt_template (str): 原始的提示词模板.
enable_semantic_variants: 是否启用语义变体替换(实验性) context_data (dict[str, Any]): 包含各个block内容的上下文数据.
noise_strength: 噪声强度 (light/medium/heavy)
cache_key_suffix: 缓存键后缀,用于区分不同的优化配置
"""
self.enable_noise = enable_noise
self.enable_semantic_variants = enable_semantic_variants
self.noise_strength = noise_strength
self.cache_key_suffix = cache_key_suffix
# 噪声强度配置
self.noise_config = {
"light": {"newline_range": (1, 2), "space_range": (0, 2), "indent_adjust": False},
"medium": {"newline_range": (1, 3), "space_range": (0, 4), "indent_adjust": True},
"heavy": {"newline_range": (1, 4), "space_range": (0, 6), "indent_adjust": True},
}
def optimize_prompt(self, prompt_text: str, context_data: dict[str, Any]) -> str:
"""
优化提示词,增加结构多样性
Args:
prompt_text: 原始提示词文本
context_data: 上下文数据字典包含各个block的内容
Returns: Returns:
优化后的提示词文本 tuple[str, dict[str, Any]]: (可能被修改的模板, 重排后的上下文数据).
""" """
try: try:
optimized = prompt_text # 这是一个简化的示例实现。
# 实际的块重排需要在模板渲染前,通过操作占位符的顺序来实现。
# 这里我们假设一个更直接的实现,即重新构建模板字符串。
# 步骤2: 语义变体替换(如果启用) # 复制上下文以避免修改原始字典
if self.enable_semantic_variants:
optimized = self._apply_semantic_variants(optimized)
# 步骤3: 轻量级噪声注入(如果启用)
if self.enable_noise:
optimized = self._inject_noise(optimized)
# 计算变化率
change_rate = self._calculate_change_rate(prompt_text, optimized)
logger.debug(f"提示词优化完成,变化率: {change_rate:.2%}")
return optimized
except Exception as e:
logger.error(f"提示词优化失败: {e}", exc_info=True)
return prompt_text # 失败时返回原始文本
def _shuffle_blocks(self, prompt_text: str, context_data: dict[str, Any]) -> str:
"""
重排可交换的block组
Args:
prompt_text: 原始提示词
context_data: 包含各block内容的字典
Returns:
重排后的提示词
"""
try:
# 对每个可交换组进行随机排序
shuffled_context = context_data.copy() shuffled_context = context_data.copy()
# 示例:假设模板中的占位符格式为 {block_name}
# 我们需要解析模板,找到可重排的组,并重新构建模板字符串。
# 注意:这是一个复杂的逻辑,通常需要一个简单的模板引擎或正则表达式来完成。
# 为保持此函数职责单一,这里仅演示核心的重排逻辑,
# 完整的模板重建逻辑应在调用此函数的地方处理。
for group in self.SWAPPABLE_BLOCK_GROUPS: for group in BlockShuffler.SWAPPABLE_BLOCK_GROUPS:
# 过滤出实际存在非空的block # 过滤出在当前上下文中实际存在的、非空的block
existing_blocks = [ existing_blocks = [
block for block in group if context_data.get(block) block for block in group if block in context_data and context_data[block]
] ]
if len(existing_blocks) > 1: if len(existing_blocks) > 1:
# 随机打乱顺序 # 随机打乱顺序
shuffled = existing_blocks.copy() random.shuffle(existing_blocks)
random.shuffle(shuffled) logger.debug(f"重排block组: {group} -> {existing_blocks}")
# 这里的实现需要调用者根据 `existing_blocks` 的新顺序
# 去动态地重新组织 `prompt_template` 字符串。
# 例如,找到模板中与 `group` 相关的占位符部分,然后按新顺序替换它们。
# 如果打乱后的顺序与原顺序不同,记录日志 # 在这个简化版本中,我们不修改模板,仅返回原始模板和(未被使用的)重排后上下文
if shuffled != existing_blocks: # 实际应用中,调用方需要根据重排结果修改模板
logger.debug(f"重排block组: {existing_blocks} -> {shuffled}") return prompt_template, shuffled_context
# 注意:实际的重排需要在模板格式化之前进行
# 这里只是演示逻辑,真正的实现需要在 _format_with_context 中处理
# 由于block重排需要在模板构建阶段进行这里只返回原文本
# 真正的重排逻辑需要集成到 Prompt 类的 _format_with_context 方法中
return prompt_text
except Exception as e: except Exception as e:
logger.error(f"Block重排失败: {e}", exc_info=True) logger.error(f"Block重排失败: {e}", exc_info=True)
return prompt_text return prompt_template, context_data
def _apply_semantic_variants(self, text: str) -> str:
"""
应用语义等价的文本替换
Args:
text: 原始文本
Returns:
替换后的文本
"""
try:
result = text
for original, variants in self.SEMANTIC_VARIANTS.items():
if original in result:
# 随机选择一个变体(包括原始文本)
replacement = random.choice(variants)
result = result.replace(original, replacement, 1) # 只替换第一次出现
return result
except Exception as e:
logger.error(f"语义变体替换失败: {e}", exc_info=True)
return text
def _inject_noise(self, text: str) -> str:
"""
注入轻量级噪声(空白字符调整)
Args:
text: 原始文本
Returns:
注入噪声后的文本
"""
try:
config = self.noise_config[self.noise_strength]
result = text
# 1. 调整block之间的换行数量
result = self._adjust_newlines(result, config["newline_range"])
# 2. 在某些位置添加随机空格(保持可读性)
result = self._adjust_spaces(result, config["space_range"])
# 3. 调整缩进仅在medium/heavy模式下
if config["indent_adjust"]:
result = self._adjust_indentation(result)
return result
except Exception as e:
logger.error(f"噪声注入失败: {e}", exc_info=True)
return text
def _adjust_newlines(self, text: str, newline_range: tuple[int, int]) -> str:
"""
调整连续换行的数量
Args:
text: 原始文本
newline_range: 换行数量范围 (min, max)
Returns:
调整后的文本
"""
# 匹配连续的换行符
pattern = r"\n{2,}"
def replace_newlines(match):
# 随机选择新的换行数量
count = random.randint(*newline_range)
return "\n" * count
return re.sub(pattern, replace_newlines, text)
def _adjust_spaces(self, text: str, space_range: tuple[int, int]) -> str:
"""
在某些位置添加随机空格
Args:
text: 原始文本
space_range: 空格数量范围 (min, max)
Returns:
调整后的文本
"""
# 在行尾随机添加空格(不可见但会改变文本哈希)
lines = text.split("\n")
result_lines = []
for line in lines:
if line.strip() and random.random() < 0.3: # 30%概率添加空格
spaces = " " * random.randint(*space_range)
result_lines.append(line + spaces)
else:
result_lines.append(line)
return "\n".join(result_lines)
def _adjust_indentation(self, text: str) -> str:
"""
微调某些行的缩进(保持语义)
Args:
text: 原始文本
Returns:
调整后的文本
"""
lines = text.split("\n")
result_lines = []
for line in lines:
# 检测列表项
list_match = re.match(r"^(\s*)([-*•])\s", line)
if list_match and random.random() < 0.5:
indent = list_match.group(1)
marker = list_match.group(2)
# 随机调整缩进±2个空格
adjust = random.choice([-2, 0, 2])
new_indent = " " * max(0, len(indent) + adjust)
new_line = line.replace(indent + marker, new_indent + marker, 1)
result_lines.append(new_line)
else:
result_lines.append(line)
return "\n".join(result_lines)
def _calculate_change_rate(self, original: str, optimized: str) -> float:
"""
计算文本变化率
Args:
original: 原始文本
optimized: 优化后的文本
Returns:
变化率0-1之间的浮点数
"""
if not original or not optimized:
return 0.0
# 使用简单的字符差异比率
diff_chars = sum(1 for a, b in zip(original, optimized) if a != b)
max_len = max(len(original), len(optimized))
return diff_chars / max_len if max_len > 0 else 0.0
def get_cache_key(self, prompt_text: str) -> str:
"""
生成优化后提示词的缓存键
由于注意力优化会改变提示词内容,缓存键也需要相应调整
Args:
prompt_text: 提示词文本
Returns:
缓存键字符串
"""
# 计算文本哈希
text_hash = hashlib.md5(prompt_text.encode()).hexdigest()[:8]
# 添加随机后缀,确保相似提示词有不同的缓存键
random_suffix = random.randint(1000, 9999)
return f"{text_hash}_{random_suffix}_{self.cache_key_suffix}"
def get_attention_optimizer_from_config() -> AttentionOptimizer:
"""
从全局配置创建注意力优化器实例
Returns:
配置好的 AttentionOptimizer 实例
"""
# 从配置中读取设置(如果存在)
config = getattr(global_config, "attention_optimization", None)
if not config:
# 使用默认配置
return AttentionOptimizer(
enable_noise=True,
enable_semantic_variants=False, # 实验性功能,默认关闭
noise_strength="light",
)
# config 是 Pydantic 模型对象,直接访问属性
return AttentionOptimizer(
enable_noise=config.enable_noise,
enable_semantic_variants=config.enable_semantic_variants,
noise_strength=config.noise_strength,
)
# 全局单例
_global_optimizer: AttentionOptimizer | None = None
def get_attention_optimizer() -> AttentionOptimizer:
"""获取全局注意力优化器实例"""
global _global_optimizer
if _global_optimizer is None:
_global_optimizer = get_attention_optimizer_from_config()
return _global_optimizer

View File

@@ -71,9 +71,13 @@ class ModelInfo(ValidatedConfigBase):
force_stream_mode: bool = Field(default=False, description="是否强制使用流式输出模式") force_stream_mode: bool = Field(default=False, description="是否强制使用流式输出模式")
extra_params: dict[str, Any] = Field(default_factory=dict, description="额外参数用于API调用时的额外配置") extra_params: dict[str, Any] = Field(default_factory=dict, description="额外参数用于API调用时的额外配置")
anti_truncation: bool = Field(default=False, description="是否启用反截断功能,防止模型输出被截断") anti_truncation: bool = Field(default=False, description="是否启用反截断功能,防止模型输出被截断")
enable_content_obfuscation: bool = Field(default=False, description="是否启用内容混淆(用于特定场景下的内容处理") enable_prompt_perturbation: bool = Field(default=False, description="是否启用提示词扰动(合并了内容混淆和注意力优化")
obfuscation_intensity: int = Field(default=1, ge=1, le=3, description="混淆强度1-3级数值越高混淆程度越强") perturbation_strength: Literal["light", "medium", "heavy"] = Field(
default="light", description="扰动强度light/medium/heavy"
)
enable_semantic_variants: bool = Field(default=False, description="是否启用语义变体作为扰动策略")
prepend_noise_instruction: bool = Field(default=False, description="是否在提示词前部添加抗审查指令")
@classmethod @classmethod
def validate_prices(cls, v): def validate_prices(cls, v):
"""验证价格必须为非负数""" """验证价格必须为非负数"""

View File

@@ -26,7 +26,7 @@ import time
from collections import namedtuple from collections import namedtuple
from collections.abc import Callable, Coroutine from collections.abc import Callable, Coroutine
from enum import Enum from enum import Enum
from typing import Any from typing import Any, ClassVar, Literal
from rich.traceback import install from rich.traceback import install
@@ -261,6 +261,137 @@ class _ModelSelector:
self.model_usage[model_name] = stats._replace(penalty=stats.penalty + penalty_increment) self.model_usage[model_name] = stats._replace(penalty=stats.penalty + penalty_increment)
class _AttentionOptimizer:
"""
通过轻量级随机化技术,在保持语义不变的前提下增加提示词结构多样性,
避免短时间内重复发送高度相似的提示词导致模型回复趋同。
"""
# 语义等价的文本替换模板
SEMANTIC_VARIANTS: ClassVar = {
"当前时间": ["当前时间", "现在是", "此时此刻", "时间"],
"最近的系统通知": ["最近的系统通知", "系统通知", "通知消息", "最新通知"],
"聊天历史": ["聊天历史", "对话记录", "历史消息", "之前的对话"],
"你的任务是": ["你的任务是", "", "你需要", "你应当"],
"请注意": ["请注意", "注意", "请留意", "需要注意"],
}
def __init__(
self,
enable_semantic_variants: bool,
noise_strength: Literal["light", "medium", "heavy"],
):
"""
初始化注意力优化器
Args:
enable_semantic_variants: 是否启用语义变体替换
noise_strength: 噪声强度 (light/medium/heavy)
"""
self.enable_semantic_variants = enable_semantic_variants
self.noise_strength = noise_strength
# 噪声强度配置
self.noise_config = {
"light": {"newline_range": (1, 2), "space_range": (0, 2), "indent_adjust": False},
"medium": {"newline_range": (1, 3), "space_range": (0, 4), "indent_adjust": True},
"heavy": {"newline_range": (1, 4), "space_range": (0, 6), "indent_adjust": True},
}
def optimize_prompt(self, prompt_text: str) -> str:
"""优化提示词,增加结构多样性"""
try:
optimized = prompt_text
if self.enable_semantic_variants:
optimized = self._apply_semantic_variants(optimized)
optimized = self._inject_noise(optimized)
change_rate = self._calculate_change_rate(prompt_text, optimized)
if change_rate > 0.001: # 仅在有实际变化时记录
logger.debug(f"提示词注意力优化完成,变化率: {change_rate:.2%}")
return optimized
except Exception as e:
logger.error(f"提示词注意力优化失败: {e}", exc_info=True)
return prompt_text
def _apply_semantic_variants(self, text: str) -> str:
"""应用语义等价的文本替换"""
try:
result = text
for original, variants in self.SEMANTIC_VARIANTS.items():
if original in result:
replacement = random.choice(variants)
result = result.replace(original, replacement, 1)
return result
except Exception as e:
logger.error(f"语义变体替换失败: {e}", exc_info=True)
return text
def _inject_noise(self, text: str) -> str:
"""注入轻量级噪声(空白字符调整)"""
try:
config = self.noise_config[self.noise_strength]
result = text
result = self._adjust_newlines(result, config["newline_range"])
result = self._adjust_spaces(result, config["space_range"])
if config["indent_adjust"]:
result = self._adjust_indentation(result)
return result
except Exception as e:
logger.error(f"噪声注入失败: {e}", exc_info=True)
return text
def _adjust_newlines(self, text: str, newline_range: tuple[int, int]) -> str:
"""调整连续换行的数量"""
pattern = r"\n{2,}"
def replace_newlines(match):
count = random.randint(*newline_range)
return "\n" * count
return re.sub(pattern, replace_newlines, text)
def _adjust_spaces(self, text: str, space_range: tuple[int, int]) -> str:
"""在某些位置添加随机空格"""
lines = text.split("\n")
result_lines = []
for line in lines:
if line.strip() and random.random() < 0.3:
spaces = " " * random.randint(*space_range)
result_lines.append(line + spaces)
else:
result_lines.append(line)
return "\n".join(result_lines)
def _adjust_indentation(self, text: str) -> str:
"""微调某些行的缩进(保持语义)"""
lines = text.split("\n")
result_lines = []
for line in lines:
list_match = re.match(r"^(\s*)([-*•])\s", line)
if list_match and random.random() < 0.5:
indent = list_match.group(1)
marker = list_match.group(2)
adjust = random.choice([-2, 0, 2])
new_indent = " " * max(0, len(indent) + adjust)
new_line = line.replace(indent + marker, new_indent + marker, 1)
result_lines.append(new_line)
else:
result_lines.append(line)
return "\n".join(result_lines)
def _calculate_change_rate(self, original: str, optimized: str) -> float:
"""计算文本变化率"""
if not original or not optimized:
return 0.0
diff_chars = sum(1 for a, b in zip(original, optimized) if a != b)
max_len = max(len(original), len(optimized))
return diff_chars / max_len if max_len > 0 else 0.0
class _PromptProcessor: class _PromptProcessor:
"""封装所有与提示词和响应内容的预处理和后处理逻辑。""" """封装所有与提示词和响应内容的预处理和后处理逻辑。"""
@@ -292,29 +423,39 @@ class _PromptProcessor:
self, prompt: str, model_info: ModelInfo, task_name: str self, prompt: str, model_info: ModelInfo, task_name: str
) -> str: ) -> str:
""" """
为请求准备最终的提示词。 为请求准备最终的提示词,应用各种扰动和指令
此方法会根据API提供商和模型配置对原始提示词应用内容混淆和反截断指令
生成最终发送给模型的完整提示内容。
Args:
prompt (str): 原始的用户提示词。
model_info (ModelInfo): 目标模型的信息。
api_provider (APIProvider): API提供商的配置。
task_name (str): 当前任务的名称,用于日志记录。
Returns:
str: 处理后的、可以直接发送给模型的完整提示词。
""" """
# 步骤1: 根据API提供商的配置应用内容混淆 final_prompt_parts = []
processed_prompt = await self._apply_content_obfuscation(prompt, model_info) user_prompt = prompt
# 步骤2: 检查模型是否需要注入反截断指令 # 步骤 A: (可选) 添加抗审查指令
if getattr(model_info, "prepend_noise_instruction", False):
final_prompt_parts.append(self.noise_instruction)
# 步骤 B: (可选) 应用提示词扰动
if getattr(model_info, "enable_prompt_perturbation", False):
logger.info(f"为模型 '{model_info.name}' 启用提示词扰动功能。")
# B.1 注意力优化 (空白字符 + 语义变体)
optimizer = _AttentionOptimizer(
enable_semantic_variants=getattr(model_info, "enable_semantic_variants", False),
noise_strength=getattr(model_info, "perturbation_strength", "light"),
)
user_prompt = optimizer.optimize_prompt(user_prompt)
# B.2 内容混淆 (注入随机噪音)
user_prompt = await self._inject_random_noise(
user_prompt, getattr(model_info, "perturbation_strength", "light")
)
final_prompt_parts.append(user_prompt)
# 步骤 C: (可选) 添加反截断指令
if getattr(model_info, "use_anti_truncation", False): if getattr(model_info, "use_anti_truncation", False):
processed_prompt += self.anti_truncation_instruction final_prompt_parts.append(self.anti_truncation_instruction)
logger.info(f"模型 '{model_info.name}' (任务: '{task_name}') 已启用反截断功能。") logger.info(f"模型 '{model_info.name}' (任务: '{task_name}') 已启用反截断功能。")
return processed_prompt return "\n\n".join(final_prompt_parts)
async def process_response(self, content: str, use_anti_truncation: bool) -> tuple[str, str, bool]: async def process_response(self, content: str, use_anti_truncation: bool) -> tuple[str, str, bool]:
""" """
@@ -331,51 +472,16 @@ class _PromptProcessor:
else: else:
is_truncated = True is_truncated = True
return content, reasoning, is_truncated return content, reasoning, is_truncated
async def _apply_content_obfuscation(self, text: str, model_info: ModelInfo) -> str:
"""
根据API提供商的配置对文本进行内容混淆。
如果提供商配置中启用了内容混淆,此方法会在文本前部加入抗审查指令,
并在文本中注入随机噪音,以降低内容被审查或修改的风险。
Args:
text (str): 原始文本内容。
api_provider (APIProvider): API提供商的配置。
Returns:
str: 经过混淆处理的文本。
"""
# 检查当前API提供商是否启用了内容混淆功能
if not model_info.enable_content_obfuscation or False:
return text
# 获取混淆强度默认为1
intensity = model_info.obfuscation_intensity or 1
logger.info(f"为模型 '{model_info.name}' 启用内容混淆,强度级别: {intensity}")
# 将抗审查指令和原始文本拼接
processed_text = self.noise_instruction + "\n\n" + text
# 在拼接后的文本中注入随机噪音
return await self._inject_random_noise(processed_text, intensity)
@staticmethod @staticmethod
async def _inject_random_noise(text: str, intensity: int) -> str: async def _inject_random_noise(text: str, strength: str) -> str:
""" """
在文本中按指定强度注入随机噪音字符串。 在文本中按指定强度注入随机噪音字符串。
该方法通过在文本的单词之间随机插入无意义的字符串(噪音)来实现内容混淆。
强度越高,插入噪音的概率和长度就越大。
Args:
text (str): 待处理的文本。
intensity (int): 混淆强度 (1-3),决定噪音的概率和长度。
Returns:
str: 注入噪音后的文本。
""" """
# 定义不同强度级别的噪音参数:概率和长度范围 # 强度映射,将 "light", "medium", "heavy" 映射到 1, 2, 3
strength_map = {"light": 1, "medium": 2, "heavy": 3}
intensity = strength_map.get(strength, 1)
params = { params = {
1: {"probability": 15, "length": (3, 6)}, # 低强度 1: {"probability": 15, "length": (3, 6)}, # 低强度
2: {"probability": 25, "length": (5, 10)}, # 中强度 2: {"probability": 25, "length": (5, 10)}, # 中强度