Claude Media
query() vs ClaudeSDKClient — Python Agent SDKの使い分け

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(): promptoptionsを渡すとメッセージをAsyncIterator[Message]として返す非同期関数。呼び出しごとに独立したセッションが既定の挙動です。
  • ClaudeSDKClient: connect() / query() / receive_response() / interrupt()などのメソッドを持つクラス。1つのインスタンスが会話全体のライフサイクルを保持し、非同期コンテキストマネージャ(async with)としても使えます。

公式リファレンスはClaudeSDKClientを「TypeScript版のquery()が内部でやっていることのPython版の等価物」と説明しています。TypeScript版のquery()は継続的な会話を扱える設計になっている一方、Python版のquery()はあくまで単発呼び出し用の薄いラッパーです。同じ関数名でも言語間で担っている役割が違う点は、両言語のSDKを行き来するときに混乱しやすいところです。

評価軸

両者を選ぶ判断は、次の6つの軸で決まります。

  1. セッションの寿命: 呼び出しごとに使い捨てでよいか、会話をまたいで保持したいか
  2. 会話の往復回数: 1往復で完結するか、複数回のやり取りが必要か
  3. 割り込みの要否: 実行中のタスクを途中で止めたいか
  4. 接続管理の手間: SDK任せでよいか、明示的な接続制御が必要か
  5. 会話の継続方法: continue_conversationresumeを手動で渡すか、自動で継続してほしいか
  6. セッション中の動的操作: モデルや権限モードを実行途中で切り替える必要があるか

比較表

項目query()ClaudeSDKClient
セッションquery()既定で新規作成ClaudeSDKClient同じセッションを再利用
会話query()単発の1往復ClaudeSDKClient同じコンテキストで複数往復
接続管理query()自動ClaudeSDKClientconnect()/disconnect()で明示制御
ストリーミング入力query()対応ClaudeSDKClient対応
割り込みquery()非対応ClaudeSDKClientinterrupt()で対応
Hooksquery()対応ClaudeSDKClient対応
カスタムツールquery()対応ClaudeSDKClient対応
会話継続query()continue_conversation/resumeを手動指定ClaudeSDKClient自動
向いている用途query()1回限りのタスクClaudeSDKClient継続的な対話

接続管理の手間はどちらが持つか

query()は接続の確立と終了をSDKが自動で行います。呼び出し側はpromptoptionsを渡すだけで、内部的なプロセス起動や後片付けを意識する必要がありません。一方ClaudeSDKClientconnect()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()は呼び出すたびに前回のやり取りを覚えていません。会話を続けたい場合はClaudeAgentOptionscontinue_conversation=True(直前の会話を継続)かresume="<session_id>"(特定のセッションIDを再開)を渡します。session_idで自分でIDを指定することもできますが、continue_conversationresumeと併用する場合は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_conversationresumeで新しい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_conversationresumeを毎回手動で管理しなければならない点です。

ClaudeSDKClientの強みは、セッションの継続とタスクの割り込みを言語機能(async with)に沿って自然に書ける点です。チャットUIのバックエンドや、ユーザーの追加入力を待ちながら長時間タスクを制御するアプリケーションに向きます。弱みは、connect()/disconnect()のライフサイクルを自分で管理する分、単発タスクにはコードがやや冗長になることです。

使い分け早見表

用途おすすめ理由
CIスクリプトでの単発コード生成おすすめquery()理由接続管理が不要でコードが短い
Webhookから届いた1件のリクエストを処理おすすめquery()理由セッションを持ち越す必要がない
チャットUIのバックエンドおすすめClaudeSDKClient理由会話の文脈を自動で保持できる
実行中のタスクをユーザー操作で止めたいおすすめClaudeSDKClient理由interrupt()が使えるのはこちらだけ
複数エージェントを都度使い捨てで並列実行おすすめquery()理由セッションが軽量に完結する
ツール実行の途中結果を見ながら次の指示を出すおすすめClaudeSDKClient理由応答依存の分岐がしやすい

よくあるつまずき

  • query()で会話が続いていると思い込む: continue_conversationresumeを渡さない限り、query()は毎回まっさらなセッションから始まります。前回の文脈を踏まえた応答を期待するコードは静かに壊れます。
  • interrupt()後にバッファを読み飛ばす: 中断直後に新しいクエリを送ってreceive_response()を1回しか呼ばないと、中断済みタスクのメッセージを新しい応答と誤認します。
  • session_idresume/continue_conversationを併用してエラーになる: session_idで独自IDを指定しながらresumecontinue_conversationも同時に使う場合は、fork_sessionを明示しないと組み合わせが拒否されます。

まとめ

query()は1回限りのタスクに向いた薄い関数で、会話を続けるにはcontinue_conversationresumeを自分で管理します。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()を都度呼び出す方が単純です。

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