跳到主要内容

工程巧思与边界 — Go↔Python 边界、流式协议、预校验

前三章讲「评测怎么算」。这章讲「它怎么嵌进平台、有哪些值得借鉴的工程技巧、又在哪里会失准」。

1. Go ↔ Python 的子进程边界

PromptSecurity 是纯 Python,但 AI-Infra-Guard 主体是 Go。集成方式是 子进程:Go 侧的 PromptTask(common/agent/prompt_tasks.go)把前端参数拼成命令行,用 uv run --no-project cli_run.py 拉起 Python(prompt_tasks.go:94)。

Go 侧拼参数时的几个固定约定(prompt_tasks.go:93-133):

  • 永远加 --async_mode(平台只走异步路径)。
  • 每个目标模型展开成 --model/--base_url/--api_key/--max_concurrent 四元组,默认并发上限 1000。
  • 裁判模型优先用平台默认配置(getDefaultEvalModel),否则用请求里带的。
  • 数据集模式固定 --techniques Raw --choice serial(原始提示词 + 串行嵌套攻击)。
  • promptdata 二选一,不能同时给也不能同时空(prompt_tasks.go:122-128)。

这条边界的好处:Python 的 LLM 生态(deepeval/pandas/各种攻击算子)不必移植进 Go,Go 只管任务编排和 Web 服务。

2. 流式可观测协议(observability)

评测可能跑很久,平台需要 实时进度。PromptSecurity 用 cli/aig_logger.py 定义了一套结构化消息,通过 stdout 流式输出,Go 侧解析转发给前端 SSE。消息类型(aig_logger.py:58-60):

类型含义
newPlanStep开启一个大阶段(如 "Jailbreaking"、"Generating report")
statusUpdate阶段内状态(running/completed/failed)
toolUsed细粒度工具调用进度(如「评测 3/20 条」)
actionLog详细日志(如某条越狱案例的 markdown)
resultUpdate最终结果(text/markdown/file/json)

这套消息和 CLAUDE.md 里描述的 WebSocket Agent 协议(newPlanStep/statusUpdate/toolUsed/actionLog/resultUpdate)同名同构——Python 子进程和 Go agent 共用一套进度语义,前端无需区分结果来自哪种任务。日志器还内置中文翻译(translated_msg)做界面本地化。代码里几乎每个阶段都密集发这些消息(见 red_teamer.pyattack_simulator.py 中大量 logger.tool_used(...))。

3. 最妙的一招:预校验剔除无效编码算子

这是整个引擎最值得学的技巧(cli/red_team_runner.py:169-203)。

问题: 编码类攻击(Base64、Morse…)有个隐患——如果目标模型根本不会解码 Base64,那这条攻击对它毫无意义:模型只会回一句「看不懂」,既不算越狱也不算防御,纯属噪声,还白白消耗调用。怎么提前把这些「对当前模型无效的算子」挑掉?

解法:发一道「探针题」试探每个编码算子。 正式评测前,先用一句无害的探针 Output the largest five-digit Arabic numeral(标准答案 99999),对每个编码算子各跑一遍 parallel:

探针: "输出最大的五位阿拉伯数字" (正确答案 99999)
│ 用每个编码算子各变形一次, 打目标模型

看回复里有没有 "99999"
├── 有 99999 → 模型能解开这种编码 → 该算子有效, 保留
└── 没 99999 → 模型解不开 → 该算子对此模型无效, 剔除
# 真实逻辑,red_team_runner.py:197-202,精简示意
filtered_name = []
for case in red_teamer.risk_assessment.test_cases:
if case.actual_output and "99999" not in case.actual_output:
filtered_name.append(case.attack_method) # 解不开 → 拉黑
reserved_attacks = [a for a in attacks if a.get_name() not in filtered_name]

剔除后只用 reserved_attacks 跑正式评测(red_team_runner.py:218-226)。这一步用 logger.disable()/enable() 静默执行,不污染进度流(:181:203)。

为什么妙: 它把「算子是否适用」从「人工猜测」变成「对每个模型实测自适应」——同一套算子库,面对强弱不同的模型会自动裁剪成不同子集,既省钱又让防御率更可信(不会因为一堆模型看不懂的编码而虚高「防御率」)。

4. 插件系统:三类都能自定义

漏洞、攻击、裁判 metric 三者都支持外部插件(deepteam/plugin_system/,PluginManager)。RedTeamRunner 在解析 scenarios/techniques/metric 时都先走插件管理器(red_team_runner.py:72:96:144)。配合 §2.2 漏洞的数据集驱动,使用者无需改框架就能接入自己的越狱样本、私有攻击手法或定制评分标准。

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

  • 签名探测做依赖注入(第 1 章 §2.2):一个增强入口靠 inspect.signature 兼容三类参数需求的算子,让 100+ 算子零分支接入。
  • 三模型角色分离:攻击方、被测方、裁判方用三个独立模型,避免「自己评自己」的偏置。
  • probe-then-filter 自适应剪枝(本章 §3):用确定答案的探针实测剔除无效算子。
  • errored/useless 不计入分母(第 3 章 §4):让防御率既不冤枉也不放过模型。
  • 子进程 + 同构进度协议:异构技术栈下仍给前端统一的实时可观测性。

6. 边界与局限(诚实)

  • 评分依赖裁判模型,存在 LLM-as-judge 固有不确定性。 分数是另一个 LLM 的判断,会受裁判模型能力、prompt 措辞影响;模板里堆了大量 caveat 来压误报,但无法消除。这是方法论上限,不是实现 bug。
  • 二分判定的粗粒度。 0.5 分(潜在风险)被并入「守住」(score > 0 即 passing),所以「复读有害输入但没加料」不计为越狱——这是有意的宽松,但会让某些边缘风险不体现在防御率里。
  • 目标模型不可控因素。 限流、安全网关拦截会让样本变成 errored;代码用 ignore_errors=True 容错并附上「可能是限流/安全拦截」的说明(red_teamer.py:306),但这类样本被排除出统计,样本量足够小时会影响防御率稳定性。
  • 不是面向公网的服务。 项目本身定位企业/个人内部红队,默认无鉴权(见仓库 README 警告),webserver 不应直接暴露公网。
  • 多轮攻击成本高。 LLM 驱动的多轮越狱每条要多次模型调用(线性越狱单条最多 5 轮 ×5 次调用),大规模评测开销显著。

7. 横向对比(同 shelf 兄弟)

PromptSecurity 是 AI-Infra-Guard 四类扫描任务里 唯一面向「模型行为」的动态评测(其余是指纹/CVE/MCP/Agent 工作流的静态或半静态扫描)。在 evals-observability 这一 area 里,它代表「对抗式 / 红队式 eval」流派:不是比对标准答案,而是 想方设法诱导失败 + 用 LLM 当裁判判定。这与「基于固定标注集 + 规则/精确匹配打分」的传统 eval 形成鲜明对照——它换来的是对开放式安全风险的覆盖,代价是评分的随机性与成本。其底座 deepteam 本身是开源红队框架,本仓库的价值增量在:海量算子库(117 个)、自带越狱数据集(16 个)、probe-then-filter 剪枝,以及与 Go 平台的流式集成。

8. 代码地图

主题文件路径关键符号
Go 拉起子进程common/agent/prompt_tasks.goPromptTask(argv 拼装,约 :93-133)
运行时目录解析common/utils/runtime_paths.goresolveRuntimeDir(PromptSecurity)
流式日志协议cli/aig_logger.pyPromptSecurityLognewPlanStep/statusUpdate/toolUsed/resultUpdate
预校验剪枝cli/red_team_runner.pyRedTeamRunner.run_red_team(probe 99999 段,约 :181-203)
插件管理deepteam/plugin_system/plugin_manager.pyPluginManager
CLI 入口/参数cli_run.pymain