Claude Media
Claudeでコンテンツモデレーションを実装する — 分類・評価・バッチ

Claudeでコンテンツモデレーションを実装する — 分類・評価・バッチ

投稿の可否をClaudeのAPIで判定する実装を、JSON出力の設計、リスクレベル、評価、カテゴリ定義、まとめ処理とMessage Batches APIの違いまで順に解説します。

Claudeでコンテンツモデレーションを実装する — 分類・評価・バッチ

コンテンツモデレーションは分類問題です。ユーザーの投稿を受け取り、カテゴリ定義に照らして「違反か、どのカテゴリか」を返させる。この形に落とせば、ClaudeのAPIは数十行で書けます。

ここではAnthropicのガイドの流れに沿って、判断材料、テストデータの作り方、モデル選定、プロンプトと出力形式、評価、精度改善、まとめ処理までを実装目線でたどります。対象は、自分のアプリ内のユーザー投稿を審査する用途です。Claude自身とのやりとりを守る話(ジェイルブレイクやプロンプトインジェクション対策)は別のテーマになります。

Claudeで判定するのはどんなときか

従来の機械学習やルールベースではなくClaudeを選ぶ判断材料は次の7つです。

指標中身
低コストで素早く作りたい中身従来の機械学習は工数・専門知識・インフラが要る
意味理解と即時判断の両立中身単純なパターン照合は口調や文脈に弱く、人手は時間がかかる
判定の一貫性中身複雑なガイドラインを均一に適用できる
ポリシーが変わりやすい中身再ラベリングなしで定義の追加・変更に追随できる
判定理由の説明中身ユーザーや規制当局向けに根拠を文章で出せる
多言語対応中身言語ごとにモデルを分けずに済む
マルチモーダル中身テキストと画像をまとめて評価できる

注意点が1つあります。Claudeの全モデルは安全面の挙動が組み込まれて学習されています。そのため、極めて危険な内容は、プロンプトで「審査しない」と指定してもモデレーション対象として扱われることがあります。たとえば成人向けサイトで露骨な性的投稿を許可すると指示しても、Claudeが要審査と判定するケースがあります。実装前に利用ポリシー(AUP)を読んでおく必要があります。

先にテスト用の例文を作る

実装より前に、「止めるべき投稿」と「止めてはいけない投稿」の両方を用意します。境界例を混ぜるのが要点です。例として次のような投稿を用意します。

  • 許可: 「主演が本当にkilled itだった」(比喩としてのkilled)、「月曜日が嫌い」、「今は金への投資の好機」
  • 拒否: 家族への脅迫、5Gが人を操るという陰謀論、賞金当選を装う詐欺文

許可側に「物騒な単語を含むが無害」な文を入れておくと、過剰検知を後の評価で拾えます。この例文からカテゴリの一覧を作ります。SNSを想定するなら、次の12個が出発点になります。

unsafe_categories = [
    "Child Exploitation", "Conspiracy Theories", "Hate",
    "Indiscriminate Weapons", "Intellectual Property",
    "Non-Violent Crimes", "Privacy", "Self-Harm", "Sex Crimes",
    "Sexual Content", "Specialized Advice", "Violent Crimes",
]

このカテゴリはSNSを想定した一例で、自社のサービスに合わせて増減する前提です。

モデルはコストで選ぶ

データ量が大きくコストが効くなら、Claude Haiku 4.5のような小さいモデルが向きます。ガイドの試算の前提は次のとおりです。

  • 月10億投稿、1投稿100文字、1トークン3.5文字で入力286億トークン
  • 違反判定は3%、違反1件あたりの出力50トークンで、出力は合計15億トークン
モデル入力出力月額
Claude Haiku 4.5入力$1.00/MTokで $28,600出力$5.00/MTokで $7,500月額$36,100
Claude Opus 5 / Opus 4.8(同額)入力$5.00/MTokで $143,000出力$25.00/MTokで $37,500月額$180,500

同じ規模でOpus系はHaiku 4.5の約5倍です($180,500 ÷ $36,100 = 約5.0倍)。実際のコストは試算と変わりえます。出力トークンは、レスポンスから explanation フィールドを外せばさらに減らせます。

プロンプトと出力形式を決める

基本形は、審査対象の投稿とカテゴリ一覧をXML風のタグで区切って渡し、JSONだけを返させるものです。以下はガイドのPython例に沿った形です。

import json
import anthropic
 
client = anthropic.Anthropic()
 
def moderate_message(message, unsafe_categories):
    category_str = "\n".join(unsafe_categories)
    prompt = f"""
Determine whether the following message warrants moderation,
based on the unsafe categories outlined below.
 
Message:
<message>{message}</message>
 
Unsafe Categories:
<categories>
{category_str}
</categories>
 
Respond with ONLY a JSON object, using the format below:
{{
"violation": <Boolean field denoting whether the message should be moderated>,
"categories": [Comma-separated list of violated categories],
"explanation": [Optional. Only include if there is a violation.]
}}
Do not include markdown formatting or code fences in your response."""
 
    response = client.messages.create(
        model="claude-haiku-4-5-20251001",
        max_tokens=200,
        messages=[{"role": "user", "content": prompt}],
    )
    text = next(b for b in response.content if b.type == "text").text
    result = json.loads(text)
    violation = result["violation"]
    cats = result.get("categories", []) if violation else []
    reason = result.get("explanation") if violation else None
    return violation, cats, reason

実装上の要点は3つです。

  1. 投稿は <message> タグ、カテゴリは <categories> タグで囲み、指示と審査対象を混ざらせない
  2. 出力はJSONのみと指定し、Markdownやコードフェンスを付けないよう明示する
  3. explanation は違反時だけ求める。ユーザーへの説明文としても使え、不要なら削ってコストを下げられる

投稿はユーザー由来の信頼できない入力です。タグで囲む書き方は、貼り付けテキストを指示と取り違えさせない対策とも同じ発想です。詳しくは貼り付けテキストへのタグ付けで扱っています。

json.loads はモデルの出力がそのままJSONでないと例外になります。本番では、例外を捕まえて1回だけ再試行し、それでも読めなければ人のレビューに回す分岐を足しておく実装が現実的です。次は筆者が書いた例です。

def moderate_safely(message, unsafe_categories, retries=1):
    for _ in range(retries + 1):
        try:
            return moderate_message(message, unsafe_categories)
        except (json.JSONDecodeError, KeyError):
            continue
    # 判定できない投稿は通さず、人のレビュー待ちに回す
    return None, [], "needs_human_review"

判定不能の投稿を黙って通すと、見逃しがそのまま公開されます。None を返して呼び出し側でキューに積む形にしておけば、モデルの出力が崩れた日も止まらずに運用できます。

二値でなくリスクレベルで返させる

違反か否かの二択ではなく、リスクの段階を返させる書き方もあります。段階を持たせると、審査の強さを調整できます。高リスクは自動でブロックし、中リスクが多いユーザーは人のレビューに回す、といった運用ができます。

プロンプト側の違いは、判定欄を次のように置き換えるだけです。

Assign a risk level based on your confidence that the user's message
should be moderated based on the defined unsafe categories:
 
0 - No risk
1 - Low risk
2 - Medium risk
3 - High risk
 
Respond with ONLY a JSON object, using the format below:
{
  "risk_level": <Numerical field denoting the risk level>,
  "categories": [Comma-separated list of violated categories],
  "explanation": <Optional. Only include if risk level is greater than 0>
}

この段階値は、アプリ側のしきい値と結びつけます。次は筆者が書いた例で、3は即ブロック、2はレビュー待ち、1以下は通す設定です。

BLOCK_AT = 3
REVIEW_AT = 2
 
def decide(result):
    level = result["risk_level"]
    if level >= BLOCK_AT:
        return "block"
    if level >= REVIEW_AT:
        return "review"
    return "pass"

しきい値を定数に切り出しておけば、ポリシーを厳しくしたい週は REVIEW_AT を1に下げるだけで済みます。プロンプトを書き換えると評価をやり直すことになりますが、しきい値の調整ならモデルの出力を変えずに済みます。具体値はサービスごとに決める領域で、ガイドは数値までは示していません。

評価で見るのは適合率と再現率

モデレーションは分類問題なので、分類用のcookbookと同じ手法で精度を測れます。運用中も、適合率(precision)と再現率(recall)を追跡し、プロンプトやカテゴリ定義、評価基準を繰り返し見直します。

先に作った例文を正解ラベル付きにして流せば、最小の評価ができます。次は筆者が書いた例で、ガイドのコードではありません。

labeled = [(c, False) for c in allowed_user_comments] + \
          [(c, True) for c in disallowed_user_comments]
 
tp = fp = fn = 0
for text, expected in labeled:
    predicted, _, _ = moderate_message(text, unsafe_categories)
    tp += predicted and expected
    fp += predicted and not expected
    fn += (not predicted) and expected
 
precision = tp / (tp + fp) if tp + fp else 0.0
recall = tp / (tp + fn) if tp + fn else 0.0
print(f"precision={precision:.2f} recall={recall:.2f}")

読み方は単純です。

  • 適合率が低い: 無害な投稿まで止めています。「killed it」のような比喩が落ちる場合、カテゴリ定義か例示の見直しが要る
  • 再現率が低い: 止めるべき投稿を通しています。見逃しの多いカテゴリの定義を具体化する

評価の設計そのものはプロンプト評価の実践ガイドが詳しく、モデルとeffortの探索は/claude-api hillclimbの記事で扱っています。

精度が足りないときはカテゴリ定義と例を足す

カテゴリ名を並べるだけでは境界がぶれることがあります。各カテゴリに定義と関連する言い回しを添える改善が効きます。ガイドは、辞書形式で名前と定義を持たせる書き方を示しています。

unsafe_category_definitions = {
    "Hate": "Content that is hateful toward people on the basis of "
            "their protected characteristics ...",
    "Privacy": "Content that contains sensitive, personal information "
               "about private individuals.",
    "Specialized Advice": "Content that contains financial, medical, "
                          "or legal advice. Financial advice includes "
                          "guidance on investments, stocks, bonds, "
                          "or any financial planning.",
    # ... 残りのカテゴリも同様に定義を書く
}
 
category_str = "\n".join(
    f"{name}: {definition}"
    for name, definition in unsafe_category_definitions.items()
)

ここで効くのは、境界の言語化です。たとえば「Specialized Advice」に金融助言が含まれると書いてあれば、「今は金への投資の好機」のような投稿が違反側に寄ることが想定できます。自社で許したい表現があるなら、定義文の側にその例外を書き込みます。例の定義文は英語なので、日本語サービスに転用するときは、日本語の言い回しに対応する定義に書き直してテストします。次は筆者が書いた日本語の定義例です。

unsafe_category_definitions_ja = {
    "Privacy": "個人の氏名・住所・電話番号・勤務先など、"
               "私人を特定できる情報を含む投稿。",
    "Specialized Advice": "投資・医療・法律に関する具体的な助言を"
                          "含む投稿。一般的な感想や体験談は含まない。",
}

末尾の「含まない」の一文のように、許したい表現を定義の側に書き込んでおくと、過剰検知を適合率の数字で確認しながら詰められます。

大量処理では「まとめ判定」と「Batches API」を区別する

最後の節「Consider batch processing」は、リアルタイム性が不要なときのコスト削減策です。1回のリクエストに複数の投稿を入れ、どれを審査すべきか一度に返させます。これは、Message Batches APIを使うことではありません。1つのプロンプトに投稿を詰める方式です。

messages_str = "\n".join(
    f"<message id={i}>{m}</message>" for i, m in enumerate(messages)
)
# 出力形式は違反した投稿だけを返す配列
# {"violations": [{"id": <message id>, "categories": [...],
#                  "explanation": "..."}]}
response = client.messages.create(
    model="claude-haiku-4-5-20251001",
    max_tokens=2048,  # バッチ用に出力上限を増やす
    messages=[{"role": "user", "content": assessment_prompt}],
)

プロンプトには「全投稿を分析すること」「該当する違反はいくつでも選ぶこと」と注意書きを入れています。IDでどの投稿かを対応づけ、違反がなければ空配列で返る想定です。

一方、Message Batches APIは別の仕組みです。リクエストを非同期に一括送信するAPIで、公式ドキュメントでは、標準のAPI価格の50% で処理でき、多くのバッチは1時間以内に終わるとされています。

項目プロンプトへの詰め込みMessage Batches API
仕組みプロンプトへの詰め込み1リクエストに複数投稿Message Batches API1投稿1リクエストを非同期に一括送信
結果の対応づけプロンプトへの詰め込みプロンプト内のIDMessage Batches APIリクエストごとの custom_id
上限プロンプトへの詰め込み出力トークンとコンテキストMessage Batches API1バッチ10万リクエストまたは256 MB
完了プロンプトへの詰め込み同期で返るMessage Batches API多くは1時間以内、24時間で期限切れ

Batches APIの結果は succeeded / errored / canceled / expired のいずれかで返り、errored・canceled・expired は課金されません。custom_id は英数字・ハイフン・アンダースコアの1〜64文字です。投稿IDをそのまま custom_id にすれば、結果の突き合わせが楽になります。

結果を取得できるのは29日間です。期限を過ぎると読めなくなるため、完了したら custom_id を投稿IDとして判定結果をデータベースに書き戻し、その時点で取り込みを終えておきます。errored や expired だった投稿は課金されないので、該当IDだけを集めて次のバッチで再送できます。

モデレーションに当てはめると、次の使い分けになります。

  • 投稿前のブロック: 同期の1件判定。待たせられない
  • 過去投稿の一括再審査、夜間バッチ、ポリシー改定後の全件再判定: Batches APIが向く

Haiku 4.5の月額試算 $36,100にBatchesの50% を単純に当てると $18,050です。これは筆者の単純計算で、ガイドの試算ではありません。詰め込み方式と併用したときの割引の掛かり方までは確認していません。

Batches APIのSDK実装はMessage Batches SDKの実装にまとめています。

本番運用の3点

本番へ出すときは、次の3点を押さえます。

  1. ブロックしたときは、なぜ止まったか、どう書き直せばよいかをユーザーに伝える。前の実装の explanation がそのまま使える
  2. 止めた投稿の種類を集計し、傾向と改善余地を見る
  3. 適合率と再現率を定期的に測り、プロンプト・キーワード・評価基準を回して直す

チャット型のサービスに組み込む場合の設計は、カスタマーサポートチャットボットの実装が参考になります。

まとめ

モデレーションの実装は、例文の用意、カテゴリの決定、JSON出力の設計、評価、定義の追加という順で進みます。大量処理は、プロンプトへの詰め込みとMessage Batches APIを混同しないことが出発点です。ポリシーに強弱が要るなら、二値ではなくリスクレベルで返させ、しきい値をアプリ側に持たせる構成が扱いやすくなります。

この記事を共有:XはてブLinkedIn