query() vs ClaudeSDKClient — Python Agent SDKの使い分け
Python版Claude Agent SDKのquery()とClaudeSDKClientはセッション・会話継続・割り込みの扱いが異なります。1回限りの呼び出しと対話アプリ、それぞれどちらを選ぶべきかを比較表と実装コードで判断します。
query()とClaudeSDKClientは何が違うのか
Python版Claude Agent SDKには、Claudeとやり取りする方法がquery()関数とClaudeSDKClientクラスの2つあります。query()は呼び出すたびに新しいセッションを作る単発の関数、ClaudeSDKClientは同じセッションを保ったまま複数回のやり取りを続けるクラスです。
結論から言うと、CIスクリプトやサーバーレス関数のような1回限りのタスクはquery()、チャットUIのように次の入力がClaudeの応答に依存する対話アプリはClaudeSDKClientを選びます。どちらも同じclaude_agent_sdkパッケージからインポートするため、名前だけ見て機能差を見落としがちです。以降で両者の内部の違いと、選択を誤ったときに起きる具体的な症状を見ていきます。
比較対象 — 何と何を比べるか
query():promptとoptionsを渡すとメッセージをAsyncIterator[Message]として返す非同期関数。呼び出しごとに独立したセッションが既定の挙動です。ClaudeSDKClient:connect()/query()/receive_response()/interrupt()などのメソッドを持つクラス。1つのインスタンスが会話全体のライフサイクルを保持し、非同期コンテキストマネージャ(async with)としても使えます。
公式リファレンスはClaudeSDKClientを「TypeScript版のquery()が内部でやっていることのPython版の等価物」と説明しています。TypeScript版のquery()は継続的な会話を扱える設計になっている一方、Python版のquery()はあくまで単発呼び出し用の薄いラッパーです。同じ関数名でも言語間で担っている役割が違う点は、両言語のSDKを行き来するときに混乱しやすいところです。
評価軸
両者を選ぶ判断は、次の6つの軸で決まります。
- セッションの寿命: 呼び出しごとに使い捨てでよいか、会話をまたいで保持したいか
- 会話の往復回数: 1往復で完結するか、複数回のやり取りが必要か
- 割り込みの要否: 実行中のタスクを途中で止めたいか
- 接続管理の手間: SDK任せでよいか、明示的な接続制御が必要か
- 会話の継続方法:
continue_conversationやresumeを手動で渡すか、自動で継続してほしいか - セッション中の動的操作: モデルや権限モードを実行途中で切り替える必要があるか
比較表
| 項目 | query() | ClaudeSDKClient |
|---|---|---|
| セッション | query()既定で新規作成 | ClaudeSDKClient同じセッションを再利用 |
| 会話 | query()単発の1往復 | ClaudeSDKClient同じコンテキストで複数往復 |
| 接続管理 | query()自動 | ClaudeSDKClientconnect()/disconnect()で明示制御 |
| ストリーミング入力 | query()対応 | ClaudeSDKClient対応 |
| 割り込み | query()非対応 | ClaudeSDKClientinterrupt()で対応 |
| Hooks | query()対応 | ClaudeSDKClient対応 |
| カスタムツール | query()対応 | ClaudeSDKClient対応 |
| 会話継続 | query()continue_conversation/resumeを手動指定 | ClaudeSDKClient自動 |
| 向いている用途 | query()1回限りのタスク | ClaudeSDKClient継続的な対話 |
接続管理の手間はどちらが持つか
query()は接続の確立と終了をSDKが自動で行います。呼び出し側はpromptとoptionsを渡すだけで、内部的なプロセス起動や後片付けを意識する必要がありません。一方ClaudeSDKClientはconnect()とdisconnect()を自分で(あるいはasync withを介して)呼ぶ設計です。これは単なる書き方の違いではなく、接続をどれだけ長く保持したいかという要件の違いを反映しています。
1回のプロンプトで完結するタスクなら、接続を明示的に管理する理由がありません。逆に、Claudeの応答を見てから次の指示を組み立てるような対話では、接続を保ったまま複数回query()メソッド(クラス側のメソッド)を呼べるClaudeSDKClientの方が自然にコードが書けます。関数のquery()とクラスのメソッドquery(prompt, session_id)は名前が同じで役割も似ていますが、後者は既存の接続の上で新しいリクエストを送るだけで、セッション自体を作り直しません。
query()の実装 — 単発呼び出し
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions
async def main():
options = ClaudeAgentOptions(
system_prompt="You are an expert Python developer",
permission_mode="acceptEdits",
)
async for message in query(prompt="Create a Python web server", options=options):
print(message)
asyncio.run(main())query()は呼び出すたびに前回のやり取りを覚えていません。会話を続けたい場合はClaudeAgentOptionsにcontinue_conversation=True(直前の会話を継続)かresume="<session_id>"(特定のセッションIDを再開)を渡します。session_idで自分でIDを指定することもできますが、continue_conversationやresumeと併用する場合はfork_sessionも同時に指定する必要があります。この手動指定の手間こそが、対話が続くほどClaudeSDKClientへ寄せた方が楽になる理由です。
ClaudeSDKClientの実装 — 会話を継続する
import asyncio
from claude_agent_sdk import ClaudeSDKClient, AssistantMessage, TextBlock
async def main():
async with ClaudeSDKClient() as client:
await client.query("What's the capital of France?")
async for message in client.receive_response():
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
print(f"Claude: {block.text}")
# セッションは維持されたまま、直前の文脈を踏まえて応答する
await client.query("What's the population of that city?")
async for message in client.receive_response():
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
print(f"Claude: {block.text}")
asyncio.run(main())async withで接続すると、ブロックを抜けるときに自動でdisconnect()が呼ばれます。2回目のclient.query()は同じセッション内で送られるため、continue_conversationのような追加指定は不要です。
ストリーミング入力は両方対応だが組み方が変わる
ストリーミング入力(promptに非同期イテラブルを渡す方式)はquery()とClaudeSDKClientのどちらでも使えます。ただしClaudeSDKClient側では、ストリームを送った後にさらに別のメッセージを同じセッションへ追加で送れます。
import asyncio
from claude_agent_sdk import ClaudeSDKClient
async def message_stream():
yield {
"type": "user",
"message": {"role": "user", "content": "Analyze the following data:"},
}
await asyncio.sleep(0.5)
yield {
"type": "user",
"message": {"role": "user", "content": "Temperature: 25°C, Humidity: 60%"},
}
async def main():
async with ClaudeSDKClient() as client:
await client.query(message_stream())
async for message in client.receive_response():
print(message)
# ストリームを送り終えた後でも同じセッションで追加の質問ができる
await client.query("Should we be concerned about these readings?")
async for message in client.receive_response():
print(message)
asyncio.run(main())query()関数にストリームを渡す場合、そのストリームが尽きた時点で会話は終わりです。追加の質問を送りたければ、continue_conversationやresumeで新しいquery()呼び出しを起こす必要があります。ClaudeSDKClientならストリームの後にclient.query("...")を呼ぶだけで、同じ接続の上に会話を継ぎ足せます。
ClaudeSDKClientだけができること — 割り込み
interrupt()はClaudeSDKClientだけが持つ機能で、実行中のタスクを途中で止められます。
import asyncio
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, ResultMessage
async def interruptible_task():
options = ClaudeAgentOptions(allowed_tools=["Bash"], permission_mode="acceptEdits")
async with ClaudeSDKClient(options=options) as client:
await client.query("Count from 1 to 100 slowly, using the bash sleep command")
await asyncio.sleep(2)
await client.interrupt()
async for message in client.receive_response():
if isinstance(message, ResultMessage):
print(f"Interrupted task: terminal_reason={message.terminal_reason!r}")
await client.query("Just say hello instead")
async for message in client.receive_response():
if isinstance(message, ResultMessage) and message.subtype == "success":
print(f"New result: {message.result}")
asyncio.run(interruptible_task())interrupt()は停止シグナルを送るだけで、メッセージバッファはクリアされません。中断されたタスクが出したメッセージ(ResultMessageを含む)はそのままストリームに残るため、新しいクエリへの応答を受け取る前に必ずreceive_response()で読み切る必要があります。ここを飛ばすと、次のクエリへの応答のつもりで中断済みタスクの結果を受け取ってしまいます。中断されたターンのterminal_reasonは"aborted_streaming"または"aborted_tools"になります。
ClaudeSDKClientだけが持つセッション中の操作
interrupt()以外にも、ClaudeSDKClientはセッションが生きている間だけ呼べるメソッドを複数持っています。query()は呼び出しが終わればセッションも終わるため、これらに相当する操作はありません。
set_permission_mode(mode): 実行中のセッションの権限モードを変更するset_model(model): 実行中のセッションでモデルを切り替える(Noneで既定に戻す)rewind_files(user_message_id): 指定したユーザーメッセージ時点のファイル状態へ復元する。enable_file_checkpointing=Trueが前提get_mcp_status(): 接続中のMCPサーバーの状態を取得するreconnect_mcp_server(server_name): 失敗・切断したMCPサーバーへの再接続を試みるtoggle_mcp_server(server_name, enabled): セッション途中でMCPサーバーを有効/無効化する(無効化するとそのツールが失われる)stop_task(task_id): 実行中のバックグラウンドタスクを停止する
これらはいずれも「セッションを保持したオブジェクトが存在する」というClaudeSDKClientの性質があって初めて意味を持つ操作です。実行中にモデルや権限モードを動的に切り替えたい、MCPサーバーの死活を監視したいといった要件があるなら、この時点でquery()は選択肢から外れます。
強み・弱み
query()の強みは、接続管理を意識しなくていいシンプルさです。バッチ処理・ワンショットのコード生成・Webhookからの単発リクエストのように、1回のプロンプトで完結するタスクに向きます。弱みは、会話を続けたいときにcontinue_conversationやresumeを毎回手動で管理しなければならない点です。
ClaudeSDKClientの強みは、セッションの継続とタスクの割り込みを言語機能(async with)に沿って自然に書ける点です。チャットUIのバックエンドや、ユーザーの追加入力を待ちながら長時間タスクを制御するアプリケーションに向きます。弱みは、connect()/disconnect()のライフサイクルを自分で管理する分、単発タスクにはコードがやや冗長になることです。
使い分け早見表
| 用途 | おすすめ | 理由 |
|---|---|---|
| CIスクリプトでの単発コード生成 | おすすめquery() | 理由接続管理が不要でコードが短い |
| Webhookから届いた1件のリクエストを処理 | おすすめquery() | 理由セッションを持ち越す必要がない |
| チャットUIのバックエンド | おすすめClaudeSDKClient | 理由会話の文脈を自動で保持できる |
| 実行中のタスクをユーザー操作で止めたい | おすすめClaudeSDKClient | 理由interrupt()が使えるのはこちらだけ |
| 複数エージェントを都度使い捨てで並列実行 | おすすめquery() | 理由セッションが軽量に完結する |
| ツール実行の途中結果を見ながら次の指示を出す | おすすめClaudeSDKClient | 理由応答依存の分岐がしやすい |
よくあるつまずき
query()で会話が続いていると思い込む:continue_conversationやresumeを渡さない限り、query()は毎回まっさらなセッションから始まります。前回の文脈を踏まえた応答を期待するコードは静かに壊れます。interrupt()後にバッファを読み飛ばす: 中断直後に新しいクエリを送ってreceive_response()を1回しか呼ばないと、中断済みタスクのメッセージを新しい応答と誤認します。session_idとresume/continue_conversationを併用してエラーになる:session_idで独自IDを指定しながらresumeやcontinue_conversationも同時に使う場合は、fork_sessionを明示しないと組み合わせが拒否されます。
まとめ
query()は1回限りのタスクに向いた薄い関数で、会話を続けるにはcontinue_conversationかresumeを自分で管理します。ClaudeSDKClientはセッションの継続・割り込み・接続管理をクラスのライフサイクルに委ねられるため、次の入力がClaudeの応答に依存するアプリケーションに向きます。両者はHooksやカスタムツールなど大半の機能を共有しているので、選択基準は「セッションを1回で終えるか、続けるか」の1点に絞って考えると迷いません。SDK自体のセットアップから確認したい場合はClaude Agent SDK入門、Slack常駐botのように継続的な会話を扱う実装例はAgent SDKでSlack常駐botを作るを参照してください。
よくある質問
query()でもストリーミング入力は使えますか
使えます。promptに非同期イテラブルを渡せば、query()でもClaudeSDKClientでもストリーミング入力に対応しています。ストリーミング入力の可否と会話が続くかどうかは別の軸です。
ClaudeSDKClientはHooksやカスタムツールを使えますか
使えます。Hooksとカスタムツールはquery()とClaudeSDKClientの両方でサポートされており、この点は選択理由にはなりません。
resumeだけ渡せばClaudeSDKClientと同じことができますか
query()にresumeを渡せば同じセッションの会話を再開できますが、割り込みには対応しません。長時間タスクを途中で止める必要があるならClaudeSDKClientを使います。
TypeScript版でも同じ使い分けが必要ですか
TypeScript版のquery()はPython版のClaudeSDKClientに近い形で会話継続を内部的に扱う設計です。言語間でAPIの粒度が異なるため、Python版のこの使い分けをそのままTypeScript版のコードへ当てはめないよう注意してください。
実行中に割り込みが必要になったらquery()を書き直すべきですか
query()にはinterrupt()に相当する機能がありません。実行中のタスクを途中で止める要件が後から出てきた場合、query()のコードをClaudeSDKClientベースに書き直す必要があります。要件が固まる前の段階でも、割り込みの可能性が少しでもあるなら最初からClaudeSDKClientで組んでおくと手戻りを避けられます。
1つのClaudeSDKClientインスタンスで複数の会話を並行して扱えますか
ClaudeSDKClientは1つのインスタンスが1つのセッションを保持する設計です。並行する会話を扱いたい場合は、インスタンスを会話ごとに分けて用意します。1回限りのタスクを並列に大量実行したいだけなら、インスタンス管理が不要なquery()を都度呼び出す方が単純です。