Claude Media
自由記述の分類をClaude APIのバッチで回す — コードブック固定と人手検証

自由記述の分類をClaude APIのバッチで回す — コードブック固定と人手検証

顧客調査の自由記述数百件をMessage Batches APIで分類する手順。コードブックと出力スキーマを先に固定し、人手サンプルとの一致率を測ってからコンサル報告に載せる流れを書きます。

顧客調査の自由記述が数百件あり、その分類結果をコンサル報告に使いたい。このとき手を付ける順番は、バッチを投げる前に分類の枠(コードブック)を固定し、投げた後に人手のサンプルで一致を確かめる形です。Message Batches APIはその間の「1件ずつ同じ基準で機械的に回す」部分を、標準価格の50%で担います。

チャットに表を貼って分類させる方法は研修後アンケートの集計手順にあります。ここでは、APIで1件1リクエストに分け、結果を再現できる形で残す手順を扱います。バッチ機能そのものの説明はClaude Batch APIの使い方にまとめています。

数百件ならコストより運用の差が出る

数百件の自由記述は、Batch APIでなくても同期のMessages APIで足ります。差が出るのは金額よりも運用です。

仮に500件、1件あたり入力1,500トークン(コードブック1,200 + 回答300)、出力150トークンとします。入力は合計75万トークン、出力は7.5万トークンです。バッチ価格で計算すると次のとおりです。

モデルバッチ入力 / 出力(MTokあたり)500件のバッチ概算同期の概算(2倍)
Claude Haiku 5.5(100,000トークンまでのプロンプト)バッチ入力 / 出力(MTokあたり)$0.05 / $0.25500件のバッチ概算約$0.06同期の概算(2倍)約$0.11
Claude Sonnet 5.5バッチ入力 / 出力(MTokあたり)$1 / $5500件のバッチ概算約$1.13同期の概算(2倍)約$2.25
Claude Opus 5.5バッチ入力 / 出力(MTokあたり)$2 / $10500件のバッチ概算約$2.25同期の概算(2倍)約$4.50

どのモデルでも数ドルに収まります。500件なら、割引額で選ぶ理由は薄いと言えます。バッチを選ぶ理由は次の3点です。

  • 1件ごとに custom_id が付くので、元の回答IDと結果を確実に突き合わせられる
  • 失敗した分だけを集めて再投入できる
  • 処理中に手元の作業を止めずに済む。多くのバッチは1時間以内に終わる

件数が数千、数万と増える案件では、割引がそのまま効いてきます。同じ手順で件数だけを増やせる点も、数百件の段階でバッチに寄せておく利点です。

先にコードブックを固定する

バッチを投げた後でカテゴリを足したくなると、そのバッチの結果は使えなくなります。送信後のバッチは変更できないためです。直すなら取り消して新しいバッチを作ることになります。分類の枠は、最初のリクエストを作る前に文書として固定します。

コードブックに入れるのは、カテゴリ名、定義、含める例、含めない例、迷ったときの優先順位です。例えば次のような形です。

codebook_v1.md
# コードブック v1(2026-10-10 版)
 
各回答を、次のカテゴリのうち主たる1つに分類する。
 
## price(価格)
料金の高さ・安さ・請求方法への言及。
- 含める: 「月額が高い」「見積りの根拠が不明」
- 含めない: 「営業の対応が遅い」(→ support)
 
## support(サポート)
問い合わせ対応・営業担当・導入支援への言及。
 
## product(製品)
機能・使い勝手・性能への言及。
 
## other(その他)
上記のどれにも当てはまらない。内容が読み取れない回答もここに入れる。
 
## 判断ルール
- 複数の話題が混ざるときは、回答の最初に出てくる話題を主とする
- 肯定・否定は問わず、話題だけで分類する

ポイントは3つあります。

  1. 「その他」を最後に1つだけ置く。当てはまらない回答の逃げ場がないと、近いカテゴリに無理に押し込まれます
  2. 迷ったときの優先順位を文章にする。複数ラベルを許すのか、主たる1つなのかを決めておかないと、人手との突き合わせで揉めます
  3. 版を付ける。ファイル名とプロンプトの両方に v1 を入れ、結果にもその版を残します。報告に載せる件数が、どの基準で数えたものかを後から言えるようにするためです

出力スキーマで分類結果を縛る

自由な文章で答えさせると、後段の集計で表記ゆれを直す作業が発生します。output_config.format にJSON Schemaを渡し、カテゴリを enum で縛ります。Batch APIは構造化出力と併用でき、同じ50%割引が適用されます。

SCHEMA = {
    "type": "object",
    "properties": {
        "category": {
            "type": "string",
            "enum": ["price", "support", "product", "other"],
        },
        "evidence": {"type": "string"},
    },
    "required": ["category", "evidence"],
    "additionalProperties": False,
}

evidence には、分類の根拠になった回答中の語句を短く抜き出させます。後で人手が不一致を調べるとき、Claudeがどこを見て決めたかが分かります。

スキーマには、ドキュメントに載っている制約が3つあります。

制約

スキーマ設計で先に知っておくこと

  • enumの大文字小文字は保証されない

    出力が、スキーマ上の値と大文字小文字だけ違うことがあります。エラーも特別な stop_reason も出ません。比較は大文字小文字を区別せずに行い、Price と price のように綴りだけ違う値は作りません。

  • 数値の範囲指定は使えない

    minimum や maximum などの数値制約と、minLength などの文字列制約は未対応で、使うと400エラーです。確信度を0〜1で返させたい場合は、範囲をスキーマで縛れないため、受け取る側で検査します。

  • 思考過程を項目にすると拒否される場合がある

    モデルの思考や段階的な推論を求める項目は、reasoning_extraction の拒否を招くことがあります。理由が欲しいときは、短い説明を求めます。

1件1リクエストでバッチを組む

回答1件が1リクエストです。custom_id は1〜64文字の英数字・ハイフン・アンダースコアに限られるので、元データの行IDに接頭辞を付けて使います。

import json
import anthropic
 
client = anthropic.Anthropic()
 
codebook = open("codebook_v1.md", encoding="utf-8").read()
rows = json.load(open("responses.json", encoding="utf-8"))
# rows の例: [{"id": "0001", "text": "月額が高すぎる"}, ...]
 
requests = [
    {
        "custom_id": f"R-{row['id']}",
        "params": {
            "model": "claude-haiku-5-5",
            "max_tokens": 1024,
            "system": codebook,
            "messages": [
                {
                    "role": "user",
                    "content": f"<response>{row['text']}</response>",
                }
            ],
            "output_config": {
                "format": {"type": "json_schema", "schema": SCHEMA}
            },
        },
    }
    for row in rows
]
 
batch = client.messages.batches.create(requests=requests)
print(batch.id)

コードブックを system に置き、回答は最後に置いています。共通部分を前に集める並びは、プロンプトキャッシュを効かせる場合の基本でもあります。キャッシュの効き方はバッチ処理のキャッシュヒット率で扱っています。

1バッチは10万リクエストか256 MBまでです。数百件ならどちらにも届きません。投入前に1件だけ同期のMessages APIで同じ形のリクエストを試すと、スキーマ違反などの検証エラーを先に見つけられます。バッチのドキュメントも、この予行演習を勧めています。

完了までは processing_status を見て待ちます。

import time
 
while True:
    batch = client.messages.batches.retrieve(batch.id)
    if batch.processing_status == "ended":
        break
    time.sleep(60)

結果の取り込みで見る状態

結果は succeeded / errored / canceled / expired のいずれかで返ります。errored・canceled・expired は課金されません。順序は投入順と限らないので、突き合わせは custom_id で行います。

ここで注意が要るのは、succeeded が「分類できた」を意味しない点です。取り込み時に、次の3点を確かめます。

  • stop_reason が refusal でないか。拒否されたリクエストも succeeded で返ります。詳しくはBatch APIの拒否は成功扱いになる落とし穴にあります。顧客の苦情文には攻撃的な表現が含まれやすく、拒否が混ざる可能性を見ておきます
  • stop_reason が max_tokens でないか。途中で切れた出力はスキーマに合わず、JSONとして読めないことがあります
  • category が enum にあるか。大文字小文字を無視して照合します
VALID = {"price", "support", "product", "other"}
ok, retry = {}, []
 
for item in client.messages.batches.results(batch.id):
    cid = item.custom_id
    if item.result.type != "succeeded":
        retry.append(cid)      # errored / expired / canceled
        continue
    msg = item.result.message
    if msg.stop_reason != "end_turn":
        retry.append(cid)      # refusal / max_tokens など
        continue
    text = next(b.text for b in msg.content if b.type == "text")
    data = json.loads(text)
    category = data["category"].lower()
    if category in VALID:
        ok[cid] = {**data, "category": category, "codebook": "v1"}
    else:
        retry.append(cid)

retry に集めたIDだけで次のバッチを組み直します。結果はバッチ作成から29日間しか取得できません。取り込みが済んだら、ok をファイルやデータベースに書き出しておきます。

人手サンプルで一貫性を検証する

機械が出したラベルは、人手と照らして初めて報告に使えます。やることは3つあります。

1. 層化したサンプルに人手でラベルを付ける

全件のうち数十件を抜き出し、Claudeの結果を見せずに人がコードブックに沿って分類します。ランダムに抜くと件数の少ないカテゴリが入らないので、カテゴリごとに最低数件を確保する抜き方にします。件数は、案件の大きさと許せる誤差で決める数字です。ここでは一律の目安を置きません。

可能なら、2人が別々に付けます。人同士が一致しないカテゴリは、コードブックの定義があいまいな箇所です。Claudeの精度を測る前に、そこを直します。

2. 一致率と混同行列を見る

from sklearn.metrics import cohen_kappa_score, confusion_matrix
 
labels = ["price", "support", "product", "other"]
human = [...]   # 人手のラベル(サンプル順)
model = [...]   # 同じ回答に対するClaudeのラベル
 
print(cohen_kappa_score(human, model))
print(confusion_matrix(human, model, labels=labels))

全体の一致率だけでなく、混同行列でカテゴリごとの食い違いを見ます。「supportをproductに寄せがち」のような偏りは、全体の数字には現れません。

3. 不一致を読んでコードブックを直し、新しいバッチを回す

不一致の回答を読み、原因を分けます。コードブックの記述不足なら、境界例を足して v2 を作り、バッチを回し直します。人手のほうが誤っていたなら、人手のラベルを直します。回答自体が分類不能なら、「その他」の定義に入れるかを決めます。

同じバッチをもう一度回して、Claude同士の結果がどれだけ揃うかも見ておくと、ばらつきの大きさが分かります。温度を固定して揃える手は、現行のモデルでは使えません。Haiku 5.5・Sonnet 5.5・Opus 5.5などでは、既定値以外の temperature / top_p / top_k を指定すると、400エラーになります。揃えるための手段は、コードブックの明確さとスキーマによる制約です。

コンサル報告に載せるときの書き方

分類結果をそのまま棒グラフにして終わりにせず、手法の欄を付けます。

  • コードブックの版と、使用したモデル、実行日
  • 全件数と、取り込めなかった(再投入後も分類できなかった)件数
  • 人手サンプルの件数と、人手との一致率。一致率が低いカテゴリは名前を挙げる
  • カテゴリ別の件数に、一致率の低いカテゴリは幅を持たせて読むという注記

代表コメントとして原文を引用するときは、分類結果ではなく元の回答データから拾います。evidence に抜き出した語句は、根拠の確認用です。

顧客の自由記述には、氏名や社名、連絡先が混ざります。バッチの入力と出力は、作成から最長29日間保存されます。バッチはWorkspace内に隔離され、同じWorkspaceのAPIリクエストか、権限のあるユーザーだけが参照できます。処理が済んだバッチは DELETE /v1/messages/batches/{batch_id} で削除でき、処理中のものは先に取り消します。個人情報を含む回答を送ってよいかは、顧客との契約や調査時の同意で決まる事項です。送信前の伏せ字処理も含め、案件側のルールを確認してから投入します。

よくあるつまずき

custom_id が重複する

バッチ内で一意でないと、リクエストが通りません。行IDをそのまま使えば重複しません。

結果の順番が元データと違う

順序は保証されません。突き合わせは必ず custom_id で行います。

29日を過ぎて結果が読めない

29日はバッチ作成時点から数えます。完了時点からではありません。

同期のテストでは通ったのにバッチで落ちる

バッチで使えない stream: true などが紛れていないか確認します。1件の失敗が、他のリクエストの処理を止めることはありません。

Haiku 5.5でthinkingが有効になっている

このモデルはthinkingが既定でオンです。分類に長い推論が要らないなら、thinking: {type: "disabled"} を指定できます。ただしeffortが xhigh や max だとこの組み合わせは400になります。実際の出力トークン数は、結果の usage で確かめます。

まとめ

数百件の自由記述では、Batch APIの割引額よりも、custom_id による突き合わせと再投入のしやすさが効きます。分類結果を報告に使える形にするのは、バッチの外側にある作業です。コードブックを版付きで固定し、スキーマで出力を縛り、人手サンプルとの一致率を手法欄に書く。この3点が揃っていれば、結果を後から説明できます。

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