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

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

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

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

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

ここではAnthropicのガイドの流れに沿って、判断材料、テストデータの作り方、モデル選定、プロンプトと出力形式、評価、精度改善、まとめ処理までを実装目線でたどります。評価の計算例、日本語のカテゴリ定義、しきい値のコード、詰め込みとBatches APIの区別、キャッシュの最小長は、ガイドの先を補う部分です。対象は、自分のアプリ内のユーザー投稿を審査する用途です。Claude自身とのやりとりを守る話(ジェイルブレイクやプロンプトインジェクション対策)は別のテーマになります。

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

ガイドは、従来の機械学習やルールベースではなくClaudeを選ぶ判断材料として7つの指標を挙げています。低コストで素早く作れること、意味理解と即時判断を両立できること、複雑なガイドラインを均一に適用できること。ポリシーの変更に再ラベリングなしで追随できること、判定理由を文章で出せること。多言語に1つのモデルで対応できること、テキストと画像をまとめて評価できること、です。

実装に直結するのは、ポリシー変更・判定理由・多言語の3つです。ポリシーが変わっても、カテゴリの定義文を差し替えれば追随できます(後の「定義と例」の節)。判定理由は explanation フィールドとしてそのまま返せます。日本語の投稿も同じプロンプトに流せます。

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

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

実装より前に、「止めるべき投稿」と「止めてはいけない投稿」の両方を用意します。境界例を混ぜるのが要点です。許可側に「物騒な単語を含むが無害」な文を入れておくと、過剰検知を後の評価で拾えます。

allowed_user_comments = [
    "This movie was great, I really enjoyed it. The main actor really killed it!",  # 比喩のkilled
    "I hate Mondays.",                                                               # 物騒でない否定語
    "It is a great time to invest in gold!",                                         # 金融の話題
]
disallowed_user_comments = [
    "Delete this post now or you better hide. I am coming after you and your family.",  # 家族への脅迫
    "Stay away from the 5G cellphones!! They are using 5G to control you.",             # 陰謀論
    "Congratulations! You have won a $1,000 gift card. Click here to claim your prize!",  # 詐欺
]

3つ目の許可例は、金融助言を違反側に含めるカテゴリ定義と衝突しうる境界例です(定義の節で扱います)。

この例文からカテゴリの一覧を作ります。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を想定した一例で、自社のサービスに合わせて増減します。ガイドは、未成年の投稿を防ぎたいサイトなら「Underage Posting」を足す例を挙げています。

モデルはコストで選ぶ

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

  • 月10億投稿、1投稿100文字、1トークン3.5文字で入力286億トークン
  • 違反判定は3%、違反1件あたりの出力50トークンで、出力は合計15億トークン
試算

月10億投稿を判定したときの月額

  • Claude Haiku 4.5

    $36,100

    入力$1.00/MTokで$28,600、出力$5.00/MTokで$7,500

  • Opus 5 / Opus 4.8

    $180,500

    両モデルとも同額。入力$143,000、出力$37,500

  • Haiku 4.5 + Batches API

    $18,050

    入力$0.50・出力$2.50/MTok。入力$14,300、出力$3,750

入力286億トークン・出力15億トークンの前提。実際のコストは試算と変わりえます

同じ規模でOpus系はHaiku 4.5の約5倍です($180,500 ÷ $36,100 = 約5.0倍)。Batches APIの価格はHaiku 4.5で標準の半額にあたります。出力トークンは、レスポンスから 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, TypeError,
                AttributeError, StopIteration):
            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に下げるだけで済みます。プロンプトを書き換えると評価をやり直すことになりますが、しきい値の調整ならモデルの出力を変えずに済みます。具体値はサービスごとに決める領域で、ガイドに数値の指定はありません。

ここまでを1件の投稿の流れにすると、次のようになります。

手順

投稿1件が通る判定の流れ

  1. 1

    JSONを解析する

    出力がJSONとして読めない、または必要なキーが無いときは例外を捕まえ、1回だけ再試行します。

  2. 2

    それでも読めなければ人に回す

    moderate_safely が返す needs_human_review をキューに積み、投稿は公開しません。

  3. 3

    しきい値で振り分ける

    decide が block / review / pass のどれかを返します。review の投稿には explanation を添えて人に渡し、判定結果は risk_level とあわせてログに残します。

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

モデレーションは分類問題なので、分類用の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」のような比喩が落ちる場合、カテゴリ定義か例示の見直しが要る
  • 再現率が低い: 止めるべき投稿を通しています。見逃しの多いカテゴリの定義を具体化する

例文が6件しかないと、1件の誤判定で数字が大きく動きます。たとえば「killed it」だけが誤検知され、違反の3件はすべて止められたとすると、TP=3・FP=1・FN=0で、適合率は3 ÷ 4 = 0.75、再現率は3 ÷ 3 = 1.00です。誤検知が1件で適合率が4分の1落ちる計算なので、境界例を増やしてから数字を比べます。

評価の設計そのものはプロンプト評価の実践ガイドが詳しく、モデルと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時間以内に終わるとされています。

くらべる

詰め込みとBatches APIの違い

同期・1リクエスト

プロンプトに詰め込む

1つのリクエストに複数の投稿を入れ、プロンプト内のIDで対応づけます。上限は出力トークン数とコンテキストの大きさで、結果は同期で返ります。

非同期・1投稿1リクエスト

Message Batches API

リクエストごとの custom_id で対応づけます。1バッチは10万リクエストか256 MBまでで、多くは1時間以内に終わり、24時間で処理が終わらなければ期限切れです。

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

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

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

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

Haiku 4.5のBatches価格は、入力$0.50/MTok・出力$2.50/MTokです。冒頭の試算は、この単価から出しています。

バッチにはリクエスト数の制限もあります。バッチ内で処理待ちのリクエスト数にもレート制限が掛かり、需要によっては処理が遅くなって、24時間で期限切れになるリクエストが増えることがあります。期限切れは課金されませんが、再送の手間は増えます。

Batches APIにキャッシュは効くか

Batches APIはプロンプトキャッシュと併用でき、割引は重なります。ただしキャッシュの命中はベストエフォートで、バッチのドキュメントは命中率が30%〜98%の範囲に収まるとしています。バッチは処理に5分を超えることがあるため、同じ資料を共有するバッチには1時間のキャッシュ期間を勧めています。

見落としやすいのは最小長です。Haiku 4.5でキャッシュできるプロンプトは4,096トークン以上で、これより短いと cache_control を付けてもキャッシュされず、エラーも返りません。冒頭の12カテゴリと指示文だけのプロンプトは、この長さに届きません。キャッシュが効き始めるのは、定義と例を書き足して共通部分が長くなってからです。キャッシュされたかどうかは、レスポンスの usage を見れば分かります。cache_creation_input_tokens と cache_read_input_tokens が両方0なら、キャッシュされていません。キャッシュ読み取りの単価は入力の10分の1($0.10/MTok)です。

もう1つは並び順です。キャッシュは先頭から続く共通部分に対して働きます。基本形は指示文のあとに <message> を置き、カテゴリ一覧はその後ろに来ます。共通なのは冒頭の短い指示文だけで、キャッシュに値する長さの共通部分がありません。キャッシュを狙うなら、カテゴリ定義と指示を前に、判定対象の投稿を最後に置く並びへ入れ替えます。

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

本番に出す前の運用

ブロックしたときは、なぜ止まったか、どう書き直せばよいかをユーザーに伝えます。ここまでの実装の explanation がそのまま使えます。止めた投稿の種類を集計して傾向を見ること、適合率と再現率を定期的に測ってプロンプト・キーワード・評価基準を回すことも、ガイドの挙げる運用です。

判定結果を保存するときは、投稿ID・モデル名・プロンプトのバージョン・risk_level を一緒に残します。後からしきい値やカテゴリ定義を変えたとき、どの設定の判定だったかを遡れます。

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

まとめ

着手の順は、境界例を含む少数のテストデータで適合率と再現率を測り、しきい値を決め、その後にBatches APIとキャッシュで単価を下げる流れが組みやすくなります。コストの最適化は、精度が数字で見えてからで間に合います。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. platform.claude.com/docs/en/about-claude/use-case-guides/content-moderation
  2. platform.claude.com/docs/en/build-with-claude/batch-processing
  3. platform.claude.com/docs/en/build-with-claude/prompt-caching
この記事を共有:XはてブLinkedIn