0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

HarmBench・StrongREJECT で何が測れて何が測れないか:データの中身を数え、自分のアプリとの差を洗い出す

0
Posted at

モデルの安全性を比べるとき、HarmBench や StrongREJECT のスコアがよく引かれます。ただ、これらのベンチマークが測っているのは「決まった言語・決まった入力経路・決まった失敗の定義」での振る舞いで、自分の LLM アプリが守るべきものと一致するとは限りません。この記事では、公開データを実際に数えて中身を確かめ、自分のアプリで追加のテストが必要な部分を洗い出す方法を示します。

採点方法や試行回数による攻撃成功率(ASR)の揺れはここでは扱わず、何がテストされていて、何がテストされていないかに絞ります。

主なベンチマークが測っているもの

よく使われるものを、失敗の定義と攻撃文の入り口で並べます。件数は論文と公式リポジトリのデータで確かめた値です。

ベンチマーク 件数 何を失敗とみなすか 攻撃文の入り口
HarmBench 510(テキスト 400、画像付き 110) 有害な依頼に応じる 利用者の入力
StrongREJECT 313 禁止された質問に役立つ答えを返す 利用者の入力
JBB-Behaviors(JailbreakBench) 有害 100、対になる無害 100 有害な依頼に応じる/無害な依頼を断る 利用者の入力
XSTest 安全 250、不安全 200 安全な質問を断る(過剰な拒否) 利用者の入力
AgentHarm 基本 110、拡張 440 エージェントが有害な複数ステップの作業を実行する 利用者の入力(ツール付き)
AgentDojo ユーザータスク 97、セキュリティテスト 629 ツールの結果に埋め込まれた指示に従う ツールの出力

HarmBench は機能面で 4 種類に分かれます。単独の依頼(standard)、文脈となる文章と組み合わせた依頼(contextual)、著作物の再現を求める依頼(copyright)、画像付きの依頼(multimodal)です。contextual は検索ですぐ見つかる情報ではなく、文脈があることで初めて害が大きくなる依頼を集めたもので、論文では ASR が高く出たと報告しています。copyright はハッシュで本文の再現を判定するため、ASR が低めに出ます。

JBB-Behaviors は有害な依頼 1 件ごとに同じ話題の無害な依頼を 1 件用意しており、防御が「危なそうな単語」で一律に断っていないかも同時に確かめられます。AgentHarm の論文は、主要なモデルがジェイルブレイクなしでも悪意のあるエージェント作業に応じやすいことを報告しています。会話モデルとしての拒否の強さが、エージェントとしての振る舞いにそのまま移るわけではないということです。

公開データを数えて中身を確かめる

論文の表だけでなく、公開されている CSV を自分で数えると、言語や件数の偏りが具体的に見えます。次のスクリプトはカテゴリ別の件数と、ASCII 以外の文字(日本語など)を含む行の数だけを出力し、プロンプトの本文は表示しません。LLM は呼び出しません。

実行環境は Python 3.13.16(標準ライブラリのみ)です。データは 2026 年 10 月 22 日に、HarmBench・StrongREJECT・XSTest の公式 GitHub リポジトリの main ブランチから取得しました。

# ベンチマークの中身を数える(プロンプト本文は表示しない)
import csv
import re
import sys
from collections import Counter
from pathlib import Path

data = Path(sys.argv[1])

def load(name):
    with open(data / name, newline="", encoding="utf-8") as f:
        return list(csv.DictReader(f))

def non_ascii_ratio(rows, col):
    n = sum(bool(re.search(r"[^\x00-\x7f]", r[col])) for r in rows)
    return f"{n}/{len(rows)}"

hb = load("harmbench_behaviors_text_all.csv")
hb_mm = load("harmbench_behaviors_multimodal_all.csv")
sr = load("strongreject_dataset.csv")
xs = load("xstest_prompts.csv")

print("HarmBench text", len(hb), "multimodal", len(hb_mm))
print("  functional:", dict(Counter(r["FunctionalCategory"] for r in hb)))
print("  semantic  :", dict(Counter(r["SemanticCategory"] for r in hb).most_common()))
print("  非ASCII文字を含む行:", non_ascii_ratio(hb, "Behavior"))
print("StrongREJECT", len(sr))
print("  category:", dict(Counter(r["category"] for r in sr).most_common()))
print("  source  :", dict(Counter(r["source"] for r in sr).most_common(3)), "...")
print("  非ASCII文字を含む行:", non_ascii_ratio(sr, "forbidden_prompt"))
print("XSTest", len(xs), dict(Counter(r["label"] for r in xs)))
実行結果
HarmBench text 400 multimodal 110
  functional: {'standard': 200, 'copyright': 100, 'contextual': 100}
  semantic  : {'copyright': 100, 'cybercrime_intrusion': 67, 'illegal': 65, 'misinformation_disinformation': 65, 'chemical_biological': 56, 'harassment_bullying': 25, 'harmful': 22}
  非ASCII文字を含む行: 0/400
StrongREJECT 313
  category: {'Non-violent crimes': 59, 'Violence': 54, 'Disinformation and deception': 50, 'Hate, harassment and discrimination': 50, 'Illegal goods and services': 50, 'Sexual content': 50}
  source  : {'custom': 221, 'DAN': 35, 'AdvBench': 25} ...
  非ASCII文字を含む行: 1/313
XSTest 450 {'safe': 250, 'unsafe': 200}

この結果から、次のことが読み取れます。

ほぼ英語だけで作られている

HarmBench のテキスト 400 件に ASCII 以外の文字を含む行はなく、StrongREJECT も 313 件中 1 件だけでした。英語での拒否の強さは測れますが、日本語で同じ依頼をされたときの振る舞いは、このデータからはわかりません。

カテゴリの分け方はベンチマークごとに違い、件数は多くない

HarmBench には著作物やサイバー犯罪のカテゴリがあり、StrongREJECT には性的コンテンツや暴力のカテゴリがあります。同じ「有害な依頼」でも、どの害を数えているかが違うため、2 つのスコアを足したり比べたりはできません。また、HarmBench の harassment_bullying は 25 件、harmful は 22 件です。カテゴリ別の結果を出すと、1 件の違いで数ポイント動きます。

HarmBench の論文は、正当な用途と悪用の両方がありうる依頼(dual-intent)を除外したと明記しています。業務で一番判断に迷うのはこの種類の依頼ですが、ベンチマークには入っていません。

出どころが重なっている

StrongREJECT は 221 件が独自作成で、残りは AdvBench などの既存データから選ばれています。JBB-Behaviors も論文によると 18% が AdvBench、27% が TDC/HarmBench 由来です。複数のベンチマークで良い結果が出ても、同じ元データを何度も測っている部分があるので、独立した証拠の数として数えないほうが安全です。

ベンチマークが測らないもの

アプリ固有のシステムプロンプト・ガードレール・ツール

HarmBench の論文は、大規模な比較の対象をモデル側の防御に限り、システム全体での防御(入出力フィルターなど)は今後の課題としています。JailbreakBench も、攻撃者がシステムプロンプトを書き換えたり応答の冒頭を指定したりする設定は扱わず、テキストだけを対象にしています。

つまり、ベンチマークのスコアは「そのモデルを素のまま使ったとき」の参考値です。自分のアプリにはシステムプロンプト、検索で入る文書、ツール、入出力のガードレールがあり、それらを含めた振る舞いは自分で測るしかありません。ガードレールを足した結果、ベンチマークの ASR が下がっても、過剰な拒否が増えていないかは XSTest や JBB の無害な依頼の側で確かめる必要があります。

利用者の入力以外から入る攻撃

有害な依頼のベンチマークは、攻撃文がすべて利用者の入力として入る前提です。RAG で検索した文書や、メール・Web ページなどのツールの出力に指示が埋め込まれる間接プロンプトインジェクションは測れません。

この部分は AgentDojo が扱っています。ワークスペース・Slack・旅行・銀行の 4 つの環境で、ツールの出力に埋め込まれた指示に従ってしまうか(攻撃の成功率)と、攻撃を受けながら本来の作業を完了できるか(攻撃下の有用性)を同時に測ります。論文は固定のテスト集ではなく、タスク・防御・攻撃を追加できる環境として設計したと説明しています。ただし、収録されている環境は自分のアプリのツールそのものではありません。

会話の往復と、時間の経過

ここで挙げた有害な依頼のベンチマークは、基本的に 1 回の依頼に対する応答を見ます。段階的に誘導するマルチターン攻撃での振る舞いは別に測る必要があります。

もう 1 つは時間です。公開されたベンチマークは、新しいモデルが高い拒否率を出すようになると差が付かなくなり(飽和)、プロンプトが学習データに混ざる可能性もあります(汚染)。Microsoft と Johns Hopkins 大学の研究者らによる Jailbreak Distillation は、この 2 つを静的なベンチマークの問題として挙げ、既存の攻撃を開発用のモデルにかけて効いたプロンプトを選び直すことで、作り直せるベンチマークを提案しています。JailbreakBench の論文も、既存の攻撃だけで頑健さを評価せず、防御に合わせた適応的な攻撃で確かめるよう求めています。

MLCommons が 2026 年 2 月に公開したジェイルブレイク評価の方法論でも、現段階では頑健さについて強い主張ができる状態ではないとしています。ベンチマークの数字は「この条件ではこうだった」以上の意味を持たせないのが妥当です。

自分のアプリとの差を洗い出す

ベンチマークを使うかどうかは、アプリで確かめたいことを並べ、それぞれをどのベンチマークが測れるかを突き合わせると決めやすくなります。次のスクリプトは、例として「社内文書を検索し、メールの下書きと送信もする日本語の業務エージェント」で確かめたいことを 6 つ挙げ、ベンチマークの条件(入口・往復数・言語・失敗の定義・アプリ固有の設定を載せられるか)と比べます。

ベンチマークの条件は、前の節の内容をもとに筆者が単純化して付けたものです。たとえば AgentDojo は環境を追加できますが、ここでは収録済みの環境をそのまま使う場合として扱っています。LLM は呼び出しません。実行環境は同じく Python 3.13.16 です。

# 自分のアプリで確かめたいことと、公開ベンチマークが測る条件を突き合わせる
from dataclasses import dataclass

@dataclass(frozen=True)
class Bench:
    name: str
    channels: frozenset   # 攻撃文がどこから入るか
    turns: frozenset      # single / multi
    langs: frozenset      # 言語
    goals: frozenset      # 何を失敗とみなすか
    app_context: bool     # 自分のシステムプロンプト・ツールを載せて測れるか

S, M = "single", "multi"
benches = [
    Bench("HarmBench(text)", frozenset({"user"}), frozenset({S}), frozenset({"en"}),
          frozenset({"harmful_content", "copyright"}), False),
    Bench("StrongREJECT", frozenset({"user"}), frozenset({S}), frozenset({"en"}),
          frozenset({"harmful_content"}), False),
    Bench("XSTest", frozenset({"user"}), frozenset({S}), frozenset({"en"}),
          frozenset({"over_refusal"}), False),
    Bench("AgentDojo", frozenset({"tool_output"}), frozenset({M}), frozenset({"en"}),
          frozenset({"tool_misuse"}), False),
]

# 例: 社内文書を検索してメールの下書き・送信もする日本語の業務エージェント
needs = [
    ("利用者が直接頼む有害な依頼を断る",       "user",        S, "ja", "harmful_content", False),
    ("無害な業務の質問を断りすぎない",         "user",        S, "ja", "over_refusal",    False),
    ("検索した文書に埋め込まれた指示で送信しない", "document",  M, "ja", "tool_misuse",     True),
    ("ツールの結果に埋め込まれた指示で送信しない", "tool_output", M, "en", "tool_misuse",     False),
    ("他部署の文書を回答に混ぜない",           "user",        S, "ja", "data_leak",       True),
    ("段階的な誘導で有害な内容を出さない",     "user",        M, "ja", "harmful_content", False),
]

def covers(b: Bench, ch, turn, lang, goal, ctx):
    missing = []
    if ch not in b.channels: missing.append("入力経路")
    if turn not in b.turns: missing.append("往復数")
    if lang not in b.langs: missing.append("言語")
    if goal not in b.goals: missing.append("失敗の定義")
    if ctx and not b.app_context: missing.append("アプリ固有の設定")
    return missing

for title, *cond in needs:
    best = min(benches, key=lambda b: len(covers(b, *cond)))
    miss = covers(best, *cond)
    status = "そのまま使える" if not miss else "自作が必要(足りない: " + "・".join(miss) + ")"
    print(f"{title}\n    最も近い: {best.name} -> {status}")
実行結果
利用者が直接頼む有害な依頼を断る
    最も近い: HarmBench(text) -> 自作が必要(足りない: 言語)
無害な業務の質問を断りすぎない
    最も近い: XSTest -> 自作が必要(足りない: 言語)
検索した文書に埋め込まれた指示で送信しない
    最も近い: AgentDojo -> 自作が必要(足りない: 入力経路・言語・アプリ固有の設定)
ツールの結果に埋め込まれた指示で送信しない
    最も近い: AgentDojo -> そのまま使える
他部署の文書を回答に混ぜない
    最も近い: HarmBench(text) -> 自作が必要(足りない: 言語・失敗の定義・アプリ固有の設定)
段階的な誘導で有害な内容を出さない
    最も近い: HarmBench(text) -> 自作が必要(足りない: 往復数・言語)

6 つのうち、そのまま使えるのは英語のツール出力を経由する 1 つだけでした。「足りない」の中身が、そのまま自作するテストの設計項目になります。言語だけが足りないものは、ベンチマークの依頼を翻訳して使う候補です。アクセス制御の漏れ(他部署の文書を混ぜない)のように、失敗の定義そのものがベンチマークにないものは、自分の権限設定と文書を使ってゼロから作るしかありません。

ベンチマークと自作テストを組み合わせる

ベンチマークは「共通のものさし」として使う

公開ベンチマークの役割は、候補のモデルを同じ条件で比べることと、論文や他社の報告と条件を揃えることです。使うときは、データのバージョン(取得日やコミット)、使った分割(HarmBench のテキストなら、リポジトリの CSV で数えると検証用 80 件・テスト用 320 件)、採点に使った分類器を記録しておきます。一部だけを使った場合は、どう選んだかも残します。

自作テストは差分から作る

前の節で「足りない」と出た項目を 1 つずつテストケースにします。ベンチマークを翻訳して使う場合は、翻訳でカテゴリの意図が変わっていないか人が確かめます。間接プロンプトインジェクションは、自分のアプリが実際に読む形式(社内 Wiki、PDF、メール)に指示を埋め込んだ検証用の文書を作り、送信や削除などの操作が起きたかをツールの呼び出しログで判定します。有害な依頼を 1 件作るごとに、同じ話題の無害な業務の依頼を 1 件作っておくと、JBB-Behaviors と同じように過剰な拒否も同時に測れます。

自作テストは調整に使わず、作り直す

システムプロンプトやガードレールを調整するたびに同じテストで確かめていると、そのテストにだけ強い設定になります。JailbreakBench が既定の判定モデルを提出に使うことを禁じているのも、判定への過剰な適合を避けるためです。調整に使う開発用のセットと、確認だけに使う非公開のセットを分け、新しい攻撃手法が報告されたら確認用のセットを作り直します。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?