ClaudeのMessage Batches SDKをPython・TypeScript・Ruby・Javaで実装する
Message Batches SDKのcreate・retrieve・resultsをPython・TypeScript・Ruby・Javaで実装する手順と、言語ごとに異なるメソッド名や型の違いを解説します。
Message Batches SDKで書く前の準備
Message Batches APIは、複数のMessagesリクエストを1つのバッチにまとめて非同期に処理するClaude APIの機能です。SDKを使えば、リクエストの組み立てから結果の受け取りまでを言語ネイティブのオブジェクトで書けます。料金や24時間の処理枠、始め方の詳細はClaude Batch APIの使い方にまとまっています。ここでは、Python・TypeScript・Ruby・Javaの公式SDKで、バッチの作成(create)・状態確認(retrieve)・結果取得(results)をどう書くかだけを扱います。
4言語ともclient.messages.batches(Rubyも同じ、Javaのみclient.messages().batches())という共通の名前空間の下にAPIが並びます。ただしメソッド名や戻り値の型は言語ごとに細部が違い、Pythonのコードをそのまま他言語に「翻訳」すると動きません。公式ドキュメントはリクエストの構造自体を言語別に解説していますが、どこが同じでどこが違うかを並べて見られる形にはなっていないため、実装するときは4言語のリファレンスを行き来しながら埋めることになります。
インストールと動作要件は次のとおりです。
| 言語 | インストール | 必要バージョン |
|---|---|---|
| Python | インストールpip install anthropic | 必要バージョンPython 3.10以上 |
| TypeScript | インストールnpm install @anthropic-ai/sdk | 必要バージョンNode.js LTS |
| Ruby | インストールbundle add anthropic | 必要バージョンRuby 3.2.0以上 |
| Java | インストールGradle: com.anthropic:anthropic-java:2.60.0 | 必要バージョンJava 8以上 |
pip install anthropic
npm install @anthropic-ai/sdk
bundle add anthropicparamsにはVision・ツール利用(Web検索やコード実行などのサーバーツールを含む)・System messages・複数ターンの会話・拡張思考など、通常のMessagesリクエストとほぼ同じ内容を渡せます。異なるのは3点だけです。stream: trueはバッチでは使えません(結果が1本のファイルで返るため)。speed(Fast mode)も同期呼び出し向けのオプションなので無効です。max_tokens: 0(プロンプトキャッシュのプリウォーム用途)も、バッチ処理中にキャッシュが失効しうるため対応していません。既存の同期呼び出し用コードからparamsを流用するときは、この3つが残っていないか確認します。含めるとバリデーションエラーで返ってきます。
ステップ1: バッチを作成する(create)
requests配列に、custom_idと通常のMessages APIと同じparamsを持つオブジェクトを並べて渡します。ここではサポートチケットの一次回答を2件まとめて生成する例で統一します。1つのバッチに詰められるリクエストは最大100,000件、または合計サイズ256MBのどちらか早く達した方までです。数千件規模のチケットをまとめて処理する場合、この上限を超えたらrequests配列を分割してcreateを複数回呼び出す必要があります。
Python — 辞書リテラルをそのままrequestsに渡すだけで、4言語のなかでもっとも記述量が少なくなります。
import anthropic
client = anthropic.Anthropic()
message_batch = client.messages.batches.create(
requests=[
{
"custom_id": "ticket-1042",
"params": {
"model": "claude-opus-5",
"max_tokens": 512,
"messages": [
{"role": "user", "content": "次の問い合わせに一次回答の下書きを書いてください: 配送が3日遅れています。"}
],
},
},
{
"custom_id": "ticket-1043",
"params": {
"model": "claude-opus-5",
"max_tokens": 512,
"messages": [
{"role": "user", "content": "次の問い合わせに一次回答の下書きを書いてください: 返品方法を教えてください。"}
],
},
},
]
)
print(message_batch.id)TypeScript — オブジェクトの形はPythonの辞書とほぼ同じですが、paramsの各キーに型が付くため、存在しないフィールド名を書くとエディタの補完・型チェックの段階で気づけます。
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const messageBatch = await client.messages.batches.create({
requests: [
{
custom_id: "ticket-1042",
params: {
model: "claude-opus-5",
max_tokens: 512,
messages: [{ role: "user", content: "次の問い合わせに一次回答の下書きを書いてください: 配送が3日遅れています。" }]
}
},
{
custom_id: "ticket-1043",
params: {
model: "claude-opus-5",
max_tokens: 512,
messages: [{ role: "user", content: "次の問い合わせに一次回答の下書きを書いてください: 返品方法を教えてください。" }]
}
}
]
});
console.log(messageBatch.id);Ruby — ハッシュのキーはシンボルで書けるため、Pythonの辞書リテラルに近い感覚で書けます。
client = Anthropic::Client.new
batch = client.messages.batches.create(
requests: [
{
custom_id: "ticket-1042",
params: {
model: "claude-opus-5",
max_tokens: 512,
messages: [
{ role: "user", content: "次の問い合わせに一次回答の下書きを書いてください: 配送が3日遅れています。" }
]
}
},
{
custom_id: "ticket-1043",
params: {
model: "claude-opus-5",
max_tokens: 512,
messages: [
{ role: "user", content: "次の問い合わせに一次回答の下書きを書いてください: 返品方法を教えてください。" }
]
}
}
]
)
puts batch.idJava — ビルダーパターンで各フィールドを明示的に組み立てる必要があり、他の3言語よりコード量が増えます。そのぶん、paramsに何を渡しているかがコード上で追いやすくなります。
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
BatchCreateParams params = BatchCreateParams.builder()
.addRequest(
BatchCreateParams.Request.builder()
.customId("ticket-1042")
.params(
BatchCreateParams.Request.Params.builder()
.model(Model.CLAUDE_OPUS_5)
.maxTokens(512)
.addUserMessage("次の問い合わせに一次回答の下書きを書いてください: 配送が3日遅れています。")
.build()
)
.build()
)
.addRequest(
BatchCreateParams.Request.builder()
.customId("ticket-1043")
.params(
BatchCreateParams.Request.Params.builder()
.model(Model.CLAUDE_OPUS_5)
.maxTokens(512)
.addUserMessage("次の問い合わせに一次回答の下書きを書いてください: 返品方法を教えてください。")
.build()
)
.build()
)
.build();
MessageBatch messageBatch = client.messages().batches().create(params);
System.out.println(messageBatch.id());作成直後のprocessing_statusは必ずin_progressです。ここから完了までポーリングします。
ステップ2: 処理状況をポーリングする(retrieve)
retrieve(id)を一定間隔で呼び、processing_statusが終了を表す値になるまで待ちます。この「終了を表す値」の型が言語ごとに違う点が最初の分岐点です。
Python(文字列比較) — 標準ライブラリのtime.sleep()をそのまま使えるので、追加の依存なしにポーリングループを書けます。
import time
while True:
message_batch = client.messages.batches.retrieve(message_batch.id)
if message_batch.processing_status == "ended":
break
time.sleep(60)
print(message_batch)TypeScript(文字列比較) — PromiseとsetTimeoutを組み合わせて待機を書きます。このwhileループごとasync関数に切り出しておくと、呼び出し側からawait pollBatch(batch.id)のように使えます。
let batch = messageBatch;
while (true) {
batch = await client.messages.batches.retrieve(batch.id);
if (batch.processing_status === "ended") {
break;
}
await new Promise((resolve) => setTimeout(resolve, 60_000));
}
console.log(batch);Ruby(シンボル比較) — loop do ... endはRubyの慣用的な無限ループの書き方で、break ifで抜ける条件を1行で書けます。
loop do
batch = client.messages.batches.retrieve(batch.id)
break if batch.processing_status == :ended
sleep 60
endJava(enum比較) — MessageBatch.ProcessingStatusはenumなので、Python/TypeScriptのような文字列の綴り間違い("eneded"のようなタイポ)はコンパイル時に検出できます。
import com.anthropic.models.messages.batches.MessageBatch;
MessageBatch batch = messageBatch;
while (true) {
batch = client.messages().batches().retrieve(batch.id());
if (batch.processingStatus().equals(MessageBatch.ProcessingStatus.ENDED)) {
break;
}
Thread.sleep(60000);
}PythonとTypeScriptは"ended"という文字列と比較しますが、Rubyは:endedというシンボル、JavaはMessageBatch.ProcessingStatus.ENDEDというenum値です。文字列のつもりでJavaのenumを.equals("ended")のように比較すると、コンパイルは通らずビルドエラーになります。
ステップ3: 結果をストリーミングで取得する(results)
処理が終わったバッチには、リクエストごとにsucceeded・errored・canceled・expiredのいずれかの結果が付きます。結果は.jsonl形式で、メモリ効率を考えるとストリーミングで1件ずつ処理するのが基本です。
Python — match文(Python 3.10以降)でtypeごとに分岐できます。3.10未満の環境ではif/elifの連鎖に書き換えます。
for result in client.messages.batches.results(message_batch.id):
match result.result.type:
case "succeeded":
print(f"成功: {result.custom_id}")
case "errored":
print(f"エラー: {result.custom_id}")
case "expired":
print(f"期限切れ: {result.custom_id}")TypeScript — for await...ofで非同期イテレータを1件ずつ処理します。全件を配列にまとめてから処理すると、大きなバッチではメモリを圧迫するため避けます。
for await (const result of await client.messages.batches.results(batch.id)) {
switch (result.result.type) {
case "succeeded":
console.log(`成功: ${result.custom_id}`);
break;
case "errored":
console.log(`エラー: ${result.custom_id}`);
break;
case "expired":
console.log(`期限切れ: ${result.custom_id}`);
break;
}
}Ruby — メソッド名に_streamingが付くのはこのAPIだけの特徴で、ページネーションで使う.batches.list()にはこの接尾辞は付きません。
client.messages.batches.results_streaming(batch.id).each do |result|
case result.result.type
when :succeeded
puts "成功: #{result.custom_id}"
when :errored
puts "エラー: #{result.custom_id}"
when :expired
puts "期限切れ: #{result.custom_id}"
end
endJava — StreamResponseはAutoCloseableを実装しているためtryブロックで使います。閉じ忘れるとコネクションが残ったままになります。
import com.anthropic.core.http.StreamResponse;
import com.anthropic.models.messages.batches.BatchResultsParams;
import com.anthropic.models.messages.batches.MessageBatchIndividualResponse;
try (
StreamResponse<MessageBatchIndividualResponse> stream = client
.messages()
.batches()
.resultsStreaming(
BatchResultsParams.builder().messageBatchId(batch.id()).build()
)
) {
stream.stream().forEach(result -> {
if (result.result().isSucceeded()) {
System.out.println("成功: " + result.customId());
} else if (result.result().isErrored()) {
System.out.println("エラー: " + result.customId());
} else if (result.result().isExpired()) {
System.out.println("期限切れ: " + result.customId());
}
});
}Python/TypeScriptはbatches.results()という同じメソッド名ですが、Rubyはresults_streaming()、JavaはresultsStreaming()とストリーミングであることを名前に明示します。移植するときにメソッド名を混同しやすい箇所です。
errored結果の中身も、そのままリトライしていいエラーか、リクエストを直さないと再送しても失敗するエラーかを見分けるために構造を確認する必要があります。ここも言語ごとにアクセスの深さが違います。
| 言語 | invalid_request_errorの判定 |
|---|---|
| Python | invalid_request_errorの判定result.result.error.error.type == "invalid_request_error"(2段ネスト) |
| TypeScript | invalid_request_errorの判定result.result.error.type === "invalid_request_error"(1段) |
| Ruby | invalid_request_errorの判定result.result.error.type == :invalid_request(1段・シンボルは_errorが付かない) |
| Java | invalid_request_errorの判定result.result().asErrored().error().error().isInvalidRequestError()(2段ネスト) |
invalid_request_errorならリクエストのparamsを直さないと再送しても失敗し続けます。それ以外のエラー(サーバー側の一時的な問題など)は同じ内容のまま再送してよい、という判断に使います。Rubyだけシンボルの綴りが_error抜きになっている点は、コピペで移植するとハマりやすいところです。
expiredはエラーではなく、バッチの処理枠内に順番が回ってこなかったリクエストです。こちらもcustom_idさえ控えておけば、同じ内容で新しいバッチを作り直して再送できます。succeeded・errored・expiredのどれになったかをcustom_idごとに記録しておくと、再送が必要な分だけを次のバッチにまとめられます。
4言語で実装が食い違う5つの落とし穴
4言語のSDKはいずれもREST APIへの薄いラッパーで、リクエスト自体の内容(custom_idとparams)は共通です。違うのはSDKが型やメソッド名としてどう表現するかで、Pythonで動いたコードをそのまま他言語に移すと次のようなところで止まります。
| 差分 | 内容 |
|---|---|
| 結果取得のメソッド名 | 内容Python/TS: results() / Ruby: results_streaming() / Java: resultsStreaming() |
| processing_statusの型 | 内容Python/TS: 文字列 / Ruby: シンボル / Java: enum(MessageBatch.ProcessingStatus) |
| 結果の順序 | 内容4言語ともリクエストした順序を保証しない。必ずcustom_idで対応付ける |
| Javaのストリーム解放 | 内容resultsStreaming()の戻り値はtryブロックで閉じないとコネクションが残る |
| custom_idの文字種 | 内容英数字・ハイフン・アンダースコアのみ。絵文字や日本語のIDはAPI側でバリデーションエラーになる |
とくに結果の順序は誤解しやすいポイントです。2件目のリクエストの結果が1件目より先に返ることは普通に起きるため、配列のインデックスではなくcustom_idを突き合わせのキーにします。作成時にrequests配列へ入れた順番と、結果を受け取る順番は別物だと考えておきます。
バッチをキャンセルしたいときは、Python・TypeScript・Rubyは同じbatches.cancel(id)、Javaはbatches().cancel(id)とメソッド名がほぼ揃っており、この操作では言語差分は出ません。キャンセル直後のprocessing_statusはcancelingになり、確定までは同じポーリングで待ちます。
まとめ — どの言語でも書き方の骨格は同じ
4言語とも「create → retrieveでポーリング → resultsで取得」という骨格は共通で、違うのはメソッド名と型だけです。実装するときは、自分の言語のSDKドキュメントでprocessing_statusの比較対象と結果取得メソッドの名前だけ確認すれば、あとはPythonの例をそのまま読み替えられます。
どの言語を選ぶかは、既存のアプリケーションが何で書かれているかで決まることがほとんどです。バッチ処理だけを理由に言語を選ぶ場面は少なく、既存のバックエンドと同じ言語のSDKを使うのが素直な選択になります。バッチの料金体系・24時間の処理枠・つまずきやすい運用面(拒否レスポンスの扱いなど)はClaude Batch APIの使い方、キャッシュヒット率を上げる工夫はバッチ処理でプロンプトキャッシュのヒット率を上げる方法にまとめています。
Python固有の非同期実装やtool_runnerの組み方はClaudeのPython SDKで非同期実行とtool_runnerを実装する、TypeScriptのStreamingやMCPヘルパーはClaudeのTypeScript SDKで実装するStreaming・Tool・MCPヘルパー、Rubyのツール実行ループとページネーションはClaudeのRuby SDKでツール実行ループとページネーションを実装するで扱っています。