ClaudeのRuby SDKでツール実行ループとページネーションを実装する
公式Ruby SDKのインストールからtool_runnerの自動実行ループ、ページネーション、ファイルアップロード、エラー処理まで実装例で解説します。
Ruby SDKの導入 — インストールと動作要件
Anthropic公式のRuby SDKは、Ruby 3.2.0以上のアプリケーションからClaude APIへ直接アクセスするためのライブラリです。HTTPトランスポートには標準ライブラリの net/http を使い、コネクションプールは connection_pool gemが担います。型定義はYard・RBS・RBIの3形式で同梱されています。
RubyGemsのメタデータ(1.76.0)では、実行時の依存gemは base64、connection_pool(2以上4未満)、standardwebhooks の3つで、required_ruby_version は >= 3.2.0 です。型のために sorbet-runtime を引き込むことはありません。
このSDKが担うのは、リクエストを送って応答を受け取る部分です。エージェントループやツール実行の実行基盤まで欲しい場合は、Claude Code、Claude Agent SDK、Claude Managed Agentsのように一段上の層を使います。Claude Codeはその一例です。リクエストを自分で送って応答を処理する用途には、今回扱うクライアントSDKが合います。
導入はBundlerで一行です。
bundle add anthropicAPIキーは環境変数 ANTHROPIC_API_KEY から自動で読み込まれます。個人アカウントキーやサービスアカウントキーが複数のワークスペースにアクセスできる場合は、リクエストヘッダー anthropic-workspace-id でワークスペースを指定します。Workload Identity Federationなど、APIキー以外の認証方式もあります。
anthropic = Anthropic::Client.new(
api_key: ENV["ANTHROPIC_API_KEY"] # デフォルト値なので省略可能
)
message = anthropic.messages.create(
max_tokens: 1024,
messages: [{role: "user", content: "Hello, Claude"}],
model: :"claude-opus-5-5"
)
message.content.each do |block|
puts block.text if block.type == :text
end応答を待たずに処理したい — ストリーミングとテキストヘルパー
長い応答を待たずに処理を始めたいとき、SDKはServer-Sent Eventsによるストリーミングをサポートします。messages.stream はメッセージ単位のイベントを返し、stream.text.each は生成中のテキスト断片だけを取り出せます。
anthropic = Anthropic::Client.new
stream = anthropic.messages.stream(
max_tokens: 1024,
messages: [{role: :user, content: "Say hello there!"}],
model: :"claude-opus-5-5"
)
stream.text.each do |text|
print(text)
endイベント単位で処理したい場合は stream.each にブロックを渡し、message.type を見て分岐します。途中経過を結合する蓄積(accumulation)のヘルパーもSDKが持つので、断片をつなぐコードは自前で書かずに済みます。ストリーミング中のエラー復旧の挙動はClaude APIストリーミングのエラー復旧がv4.6で変わったで扱っています。
Claudeにツールを実行させたい — tool_runnerの自動ループ
ツール呼び出しを自分でループ実装すると、応答を受け取り、ツールを実行し、結果を返す往復を手で書くことになります。tool_runner はこの往復を自動化します。必要なのは次の3段階だけです。
tool_runnerを使うまでの3段階
- 1
入力スキーマを宣言する
Anthropic::BaseModelを継承したクラスに、requiredでフィールドと型を並べます。列挙はAnthropic::InputSchema::EnumOfで書きます。 - 2
実行本体を書く
Anthropic::BaseToolを継承し、input_schemaで上のクラスを指定して、callに処理を書きます。 - 3
tool_runnerに渡す
tools:にインスタンスを渡し、each_messageで各ターンの応答を受け取ります。
anthropic = Anthropic::Client.new
class CalculatorInput < Anthropic::BaseModel
required :lhs, Float
required :rhs, Float
required :operator, Anthropic::InputSchema::EnumOf[:+, :-, :*, :/]
end
class Calculator < Anthropic::BaseTool
input_schema CalculatorInput
def call(expr)
expr.lhs.public_send(expr.operator, expr.rhs)
end
end
# ツール実行ループを自動で処理
anthropic.beta.messages.tool_runner(
model: "claude-opus-5-5",
max_tokens: 1024,
messages: [{role: "user", content: "What's 15 * 7?"}],
tools: [Calculator.new]
).each_message { |message| puts message.content }tool_runner はベータ名前空間 anthropic.beta.messages に置かれています。ツール呼び出しの一般的な設計方針はTool RunnerでAnthropic APIのツール呼び出しループを自動化するで扱っています。実行ループを自作する場合の考え方もそちらにあるので、Ruby以外の実装と比べたいときに役立ちます。
一覧を全件たどりたい — ページネーションとファイルアップロード
Claude APIの一覧系エンドポイントはページネーションされています。Ruby SDKは自動でページを進める auto_paging_each を提供するので、次ページの有無を自分で判定する必要がありません。
anthropic = Anthropic::Client.new
page = anthropic.messages.batches.list(limit: 20)
# 単一アイテムの取得
batch = page.data[0]
puts(batch.id)
# 必要に応じて自動で次ページを取得
page.auto_paging_each do |batch|
puts(batch.id)
end1ページずつ処理を挟みたいときは、next_page? と next_page でループを自分で回します。
page = anthropic.messages.batches.list(limit: 20)
loop do
page.data&.each { |batch| puts(batch.id) }
break unless page.next_page?
page = page.next_page
endファイルアップロードを伴うパラメータには、Pathname、StringIO、生の文字列コンテンツを渡せます。
anthropic = Anthropic::Client.new
require "pathname"
# Pathnameを使うとファイル名を送りつつ大きなファイルをメモリに全展開せずに済む
file_metadata = anthropic.files.upload(file: Pathname("/path/to/file"))
# ファイル内容を直接渡すことも可能
file_metadata = anthropic.files.upload(file: File.read("/path/to/file"))
# ファイル名とコンテンツタイプを自分で決めたいとき
file = Anthropic::FilePart.new(File.read("/path/to/file"), filename: "/path/to/file", content_type: "...")
file_metadata = anthropic.files.upload(file: file)呼び出しが失敗したら — エラー型・リトライ・タイムアウト
接続に失敗した場合やAPIが4xx/5xx応答を返した場合、Anthropic::Errors::APIError のサブクラスが送出されます。ステータスコードとエラー型の対応は次の通りです。
| ステータス | エラー型 |
|---|---|
| HTTP 400 | エラー型BadRequestError |
| HTTP 401 | エラー型AuthenticationError |
| HTTP 403 | エラー型PermissionDeniedError |
| HTTP 404 | エラー型NotFoundError |
| HTTP 409 | エラー型ConflictError |
| HTTP 422 | エラー型UnprocessableEntityError |
| HTTP 429 | エラー型RateLimitError |
| HTTP 500以上 | エラー型InternalServerError |
| 上記以外のHTTPエラー | エラー型APIStatusError |
| タイムアウト | エラー型APITimeoutError |
| ネットワークエラー | エラー型APIConnectionError |
rescue は個別の型から順に並べ、最後に APIStatusError を置くのが公式の例の形です。APIConnectionError には e.cause で net/http 内部の例外が入り、APIStatusError には e.status でステータスコードが入ります。
begin
message = anthropic.messages.create(
max_tokens: 1024,
messages: [{role: "user", content: "Hello, Claude"}],
model: :"claude-opus-5-5"
)
rescue Anthropic::Errors::APIConnectionError => e
puts("The server could not be reached")
puts(e.cause)
rescue Anthropic::Errors::RateLimitError
puts("A 429 status code was received; we should back off a bit.")
rescue Anthropic::Errors::APIStatusError => e
puts(e.status)
end自動リトライとタイムアウトの既定値は次のとおりです。
SDKの既定値
自動リトライ
最大2回
短い指数バックオフつき。max_retries: 0で無効化
タイムアウト
10分
timeoutオプションの単位は秒
リトライの対象は、接続エラー・408・409・429・500番台・タイムアウトです。つまり、タイムアウトした要求も既定では再試行されます。timeout を短くしても、max_retries を下げない限り1回の呼び出しがその秒数で終わるとは限りません。両方を揃えて指定します。
anthropic = Anthropic::Client.new(
max_retries: 0, # デフォルトは2
timeout: 20 # 秒。デフォルトは10分
)
# リクエスト単位で上書きする場合
anthropic.messages.create(
max_tokens: 1024,
messages: [{role: "user", content: "Hello, Claude"}],
model: :"claude-opus-5-5",
request_options: {max_retries: 5, timeout: 5}
)リトライ回数やタイムアウトの既定値はSDKの言語ごとに違います。hooksや自作のMCPサーバーから呼ぶ場合の比較はAnthropic SDKの既定リトライ・タイムアウトは言語で違うにあります。
並行処理で詰まらせない — コネクションプールとfork
Anthropic::Client のインスタンスはスレッドセーフですが、フォークセーフなのは進行中のHTTPリクエストが無いときに限られます。SDKの他のクラスは、内部のデータ構造をロックで守っていません。
並行処理で押さえる3点
クライアントは1つ作って使い回す
インスタンスごとに独自のコネクションプールを持ち、既定のサイズは99です。公式の推奨は、ほとんどの環境でアプリケーションごとに1回だけ作ることです。
プールが埋まると待たされる
使える接続がすべて貸し出し中だと、リクエストは空きが出るまで待ちます。この待ち時間もタイムアウトに数えられます。
forkの前にリクエストを残さない
ワーカープロセスをforkするサーバーでは、fork時点で進行中のリクエストがあるとフォークセーフの条件から外れます。
クライアントをリクエストごとに作るコードは、この前提と逆向きです。プールが毎回別になり、接続を使い回せません。
クラウド別クライアントと対応状況
Claude APIの直接利用以外に、クラウド別のクライアントも用意されています。必要なgemはクライアントごとに異なり、anthropic gemに自動では入りません。
| クライアント | 必要なgem | 向く場面 |
|---|---|---|
Anthropic::VertexClient | 必要なgemgoogleauth | 向く場面Agent Platform経由 |
Anthropic::BedrockMantleClient | 必要なgemaws-sdk-core | 向く場面Bedrockの新規プロジェクト |
Anthropic::BedrockClient | 必要なgemaws-sdk-bedrockruntime | 向く場面既存の InvokeModel APIを使うアプリ |
Anthropic::AWSClient | 必要なgemaws-sdk-core | 向く場面Claude Platform on AWS(ベータ) |
AWSClient はメインの anthropic gemに含まれています。コンストラクタに workspace_id: を渡すか、環境変数 ANTHROPIC_AWS_WORKSPACE_ID を設定します。
型とシリアライズ — Sorbet、BaseModel、未文書化の拡張点
公式が提供するクライアントSDKはPython・TypeScript・C#・Go・Java・PHP・Rubyの7言語です。Ruby版はRBI定義を提供し、sorbet-runtimeには依存しません。そのため列挙型は、実行時には常にプリミティブとして振る舞う「tagged symbol」で表現されます。
Anthropic::MessageCreateParams::ServiceTier::AUTO のような定数を渡しても、リテラルの :auto を渡しても受け付けられます。型の情報を残したいときは定数、手軽に書きたいときはリテラルと使い分けられます。リクエストパラメータも、型付きの Anthropic::MessageParam.new(...) と素のHashのどちらでも渡せます。
このパッケージはSemVer(セマンティックバージョニング)に従います。ただし .rbi / .rbs の型定義ファイルへの改善は、実行時の振る舞いに影響しないため非破壊的変更として扱われます。型定義だけが更新されるパッチリリースを破壊的変更と誤認しないための取り決めです。
すべてのパラメータ・レスポンスオブジェクトは Anthropic::Internal::Type::BaseModel を継承しており、いくつかの便利な機能を共有しています。未知のフィールドを含むすべてのフィールドに obj[:prop] 構文でアクセスでき、obj => {prop: prop} のようなパターンマッチングで分解代入もできます。2つのAPI呼び出しが同じ値を返せば、== の比較で構造的等価性が成立します。
#to_h ・#deep_to_h ・#to_json ・#to_yaml といったヘルパーも共通で使えます。レスポンスをログに残す、テストでスナップショット比較するといった用途では、独自のシリアライズコードを書かずに済みます。
ドキュメント化されていないパラメータを送りたい、あるいはドキュメント化されていないレスポンスプロパティを読みたい場合は、request_options に extra_query ・extra_body ・extra_headers を渡します。
anthropic = Anthropic::Client.new
message = anthropic.messages.create(
max_tokens: 1024,
messages: [{role: "user", content: "Hello, Claude"}],
model: :"claude-opus-5-5",
request_options: {
extra_query: {my_query_parameter: "example"},
extra_body: {my_body_parameter: "example"},
extra_headers: {"my-header": "example"}
}
)
puts(message[:my_undocumented_property])ドキュメント化されていないエンドポイントそのものを叩きたい場合は、認証・リトライなどクライアントの機能を保ったまま anthropic.request で直接リクエストできます。
response = anthropic.request(
method: :post,
path: "/undocumented/endpoint",
query: {"dog" => "woof"},
headers: {"useful-header" => "interesting-value"},
body: {"hello" => "world"}
)よくあるつまずき
- ワークスペースヘッダーの付け忘れ: 個人・サービスアカウントキーが複数ワークスペースにアクセスできる場合は、
anthropic-workspace-idヘッダーでワークスペースを指定します - BedrockClientとBedrockMantleClientの取り違え: 新規プロジェクトは
BedrockMantleClientが前提で、BedrockClientは既存のInvokeModelAPI利用アプリのために残されています
まとめ
Ruby SDKは bundle add anthropic で入り、ツール呼び出しの往復は tool_runner、一覧取得は auto_paging_each に任せられます。本番で効いてくるのは、タイムアウトとリトライをセットで決めることと、クライアントをアプリ内で1つに保つことです。
料金・モデル選択・認証まわりの全体像はAnthropic API完全ガイド、トークン数を事前に把握したい場合はcount_tokensで送信前にトークン数を数える方法も参考になります。