Claude Media
Claudeでカスタマーサポートチャットボットを作る — tool useで見積もり生成まで実装

Claudeでカスタマーサポートチャットボットを作る — tool useで見積もり生成まで実装

Anthropic公式のカスタマーサポート実装ガイドを、Python + Streamlitの構成でたどります。システムプロンプト、get_quoteツール、ChatBotクラスの実装と、本番前に直したい箇所を扱います。

公式ガイドが作るチャットボットの全体像

Anthropicの公式ガイド「Customer support agent」は、架空の保険会社Acme Insurance向けチャットボット「Eva」を作る手順書です。Evaがこなす仕事は3つあります。製品Q&Aへの回答、保険と無関係な話題からの脱線防止、そして見積もりの生成です。見積もりだけがtool useを使い、残りはシステムプロンプトで実現します。

構成はconfig.py・chatbot.py・app.pyの3ファイルと、APIキーを置く.envです。

ファイル役割
config.py役割システムプロンプトの部品、モデル名、ツール定義、get_quoteの仮実装
chatbot.py役割ChatBotクラス。API呼び出しとtool useの往復を担う
app.py役割StreamlitのチャットUI
.env役割ANTHROPIC_API_KEYを置く。コードがload_dotenv()で読むので、環境変数に設定済みなら不要

前提はClaude APIキーとPython 3.10以降です。パッケージは次の3つを入れます。

pip install anthropic streamlit python-dotenv

以降は、公式ガイドの順に「設計 → プロンプト → ツール → 実装 → 評価」とたどります。最後に、サンプルをそのまま本番に出すと困る箇所を洗い出します。

コードを書く前に決めておく3つのこと

公式ガイドは、実装の前に設計の作業を置いています。ここを飛ばすと、プロンプトが長いだけで方針のないボットになります。

理想の会話を書き出し、タスクに分解する

まず、顧客とボットの理想のやり取りを台本にします。ガイドの例では、挨拶、EV保険の質問、関連する質問への出典リンク付き回答、脱線した質問の切り返し、見積もり依頼、その後の質問と締め、の流れです。

次に、その台本を独立したタスクに割ります。ガイドは4つに分けています。

  1. 挨拶と一般案内
  2. 製品情報の回答(コンテキストに情報が必要で、RAGが要る可能性がある)
  3. 会話の管理(話題の維持と脱線の切り返し)
  4. 見積もり生成(質問の組み立て、API送信、結果の提示)

この分解が、後のプロンプトの区画割りとツールの数にそのまま対応します。

成功基準を数字で置く

ガイドは評価指標を2系統で挙げています。回答の質を見る指標と、事業への効果を見る指標です。

数字

ガイドが置く成功基準の目安

  • クエリ理解の正確さ

    95%以上

  • 回答の正確さ

    100%

    会社・商品の基本情報

  • 回答の関連性

    90%以上

  • 出典リンクの提示

    80%

    追加情報が役立つ会話のうち

  • 話題の維持

    95%

    応答のうち保険関連の割合

  • 見積もりの正確さ

    100%

  • エスカレーション精度

    95%以上

  • 顧客感情の維持・改善

    90%

    会話のうち

  • 自己解決率(deflection rate)

    70〜80%

    問い合わせの複雑さ次第

  • 満足度(CSAT)

    4点以上

    5点満点

ガイドの目安から主なものを抜粋しています。100%を置くのは回答の正確さと見積もりの正確さです

数字はガイドが目安として挙げたものです。自社の数字に置き直すときの出発点は、現在の人間対応の実績値です。

モデルを選ぶ

くらべる

モデルについて、ガイドの本文とコードは食い違っている

Opus 5 / Haiku 4.5

ガイドの本文

複雑で長い多段の会話にはClaude Opus 5を勧めています。RAG・tool use・長文脈を重ねた複数プロンプトの流れで遅延を優先するなら、Claude Haiku 4.5が選択肢です。

claude-opus-5-5

サンプルコード

MODEL定数はclaude-opus-5-5です。モデル一覧では、これはClaude Opus 5.5のAPI IDにあたります。Haiku 4.5はclaude-haiku-4-5で指定できます。

食い違いが実害になるのは、tool_choiceを足したときです。Opus 5.5・Sonnet 5.5・Fable 5.1・Mythos 5.1は強制ツール使用に対応しておらず、tool_choice: {"type": "any"}か{"type": "tool", "name": "..."}を送ると400で返ります。エラー文言は次のとおりです。

tool_choice: type "tool" and "any" are not supported for this model.

ガイドのコードはtool_choiceを指定しない(既定のauto)ので動きます。「見積もりは必ずツール経由」と強制したい改修を足すと、本文のOpus 5のつもりで書いたコードがOpus 5.5で落ちます。autoとnoneは通ります。入力をスキーマどおりに揃えたいなら、次節のstrictが代わりの手段です。

システムプロンプトを5つの部品で組む

Evaの人格と知識は、config.pyの文字列定数を連結して作ります。部品は次の5つです。

  • IDENTITY: 「Acme Insuranceの親切なAIアシスタントEva」という役割
  • STATIC_GREETINGS_AND_GENERAL: 会社概要、取扱商品、営業時間、電話番号
  • STATIC_CAR_INSURANCE と STATIC_ELECTRIC_CAR_INSURANCE: 商品ごとの補償内容
  • EXAMPLES: 理想の応答例(ガイドは4〜5件以上を勧め、サンプルは5件)
  • ADDITIONAL_GUARDRAILS: 守るべき禁止事項

知識の部品は<static_context>タグで囲みます。応答例はH:とA:の対話形式で、見積もり依頼には「質問を並べたうえで見積もりツールを使う」と答える例を含めます。

ガードレールは箇条書きの規則で書く

脱線防止の中身は、意外なほど素朴です。

ADDITIONAL_GUARDRAILS = """Please adhere to the following guardrails:
1. Only provide information about insurance types listed in our offerings.
2. If asked about an insurance type we don't offer, politely state
that we don't provide that service.
3. Do not speculate about future product offerings or company plans.
4. Don't make promises or enter into agreements it's not authorized to make.
You only provide information and guidance.
5. Do not mention any competitor's products or services.
"""

「扱わない商品は断る」「将来の計画を推測しない」「権限のない約束をしない」「競合に触れない」の4系統です。日本の窓口に置き換えるなら、保険業法や景品表示法に触れる断定表現の禁止など、自社の規程をここに足すことになります。

知識はどこに置くか

ガイドはTipで、指示を全部システムプロンプトに入れたくなるが、Claudeは役割付けを除く本文の大半を最初のユーザー発話に書いた方がうまく働く、と述べています。その言葉どおり、app.pyは知識と例をまとめたTASK_SPECIFIC_INSTRUCTIONSを最初のuserメッセージに置き、直後にassistantの「Understood」を挟みます。systemに渡すのはIDENTITYだけです。

get_quoteツールを定義する

見積もりは、Claudeが計算するのではなく、アプリ側の関数を呼ばせます。ガイドはこの点をTipで強調しています。ツールはClaudeが自分で計算する仕組みではなく、指定した引数でツールを使うべきだとアプリに知らせるだけです。

TOOLS = [
    {
        "name": "get_quote",
        "description": "Calculate the insurance quote based on user input. "
                       "Returned value is per month premium.",
        "input_schema": {
            "type": "object",
            "properties": {
                "make": {"type": "string", "description": "The make of the vehicle."},
                "model": {"type": "string", "description": "The model of the vehicle."},
                "year": {"type": "integer", "description": "The year the vehicle was manufactured."},
                "mileage": {"type": "integer", "description": "The mileage on the vehicle."},
                "driver_age": {"type": "integer", "description": "The age of the primary driver."},
            },
            "required": ["make", "model", "year", "mileage", "driver_age"],
        },
    }
]

requiredに5項目がすべて入っているのは、スキーマ上の宣言です。聞き返しを保証する仕組みではありません。strictを付けない通常のツールでは、整数の2が文字列の"2"で返ったり、必須項目が抜けたりすることがあるとされています。ガイドは、応答例5で車種・年式・走行距離・運転者の年齢を先に聞く流れを示すことで、不足分の聞き返しを促しています。

入力をスキーマどおりに揃えるには、ツール定義に"strict": Trueを足し、input_schemaの各オブジェクトにadditionalProperties: Falseを付けます。

TOOLS = [
    {
        "name": "get_quote",
        "strict": True,
        "input_schema": {
            "type": "object",
            "properties": {...},  # 元の5項目と同じ
            "required": ["make", "model", "year", "mileage", "driver_age"],
            "additionalProperties": False,
        },
    }
]

関数本体はガイドでは仮実装です。1秒待って固定の100を返します。実運用ではここをHTTPエンドポイントや社内DBの呼び出しに差し替えます。

ChatBotクラスでtool useの往復を実装する

chatbot.pyのChatBotは、主に2つのメソッドを持ちます。

  • generate_message: messages.createを呼ぶ。model、system、max_tokens、messages、toolsを渡す
  • process_user_input: 入力を履歴に足し、応答を見て、ツール呼び出しなら実行して再度APIを呼ぶ

ツール呼び出しの往復は、tool useドキュメントの手順どおりに進みます。

手順

見積もり依頼1件の往復

  1. 1

    Claudeがツールを呼ぶ

    応答のstop_reasonがtool_useになります。tool_useブロックにid・name・inputが入っています。

  2. 2

    アプリがツールを実行する

    nameとinputをもとに、アプリ側でget_quoteを動かします。

  3. 3

    結果を返す

    roleがuserのメッセージにtool_resultブロックを入れ、対応するtool_use_idを持たせて送ります。

  4. 4

    Claudeが金額を案内する

    2回目の応答にテキストブロックが入り、利用者に返す文面になります。

response_message = self.generate_message(messages=self.session_state.messages, max_tokens=2048)
 
if response_message.content[-1].type == "tool_use":
    tool_use = response_message.content[-1]
    result = self.handle_tool_use(tool_use.name, tool_use.input)
    self.session_state.messages.append(
        {"role": "assistant", "content": response_message.content}
    )
    self.session_state.messages.append(
        {"role": "user", "content": [
            {"type": "tool_result", "tool_use_id": tool_use.id, "content": f"{result}"}
        ]}
    )
    follow_up = self.generate_message(messages=self.session_state.messages, max_tokens=2048)

handle_tool_useはget_quoteならQuote generated: $100.00 per monthのような文字列を返し、未知のツール名では例外を投げます。2回目の応答からテキストブロックを取り出して履歴に足し、利用者に返す、という流れです。

StreamlitでUIを付けて動かす

app.pyはst.chat_messageとst.chat_inputでチャット画面を作ります。セッションのmessagesが空なら、先ほどのTASK_SPECIFIC_INSTRUCTIONSと「Understood」を入れて初期化します。表示は先頭2件を飛ばし、contentが文字列のメッセージだけを描画します。ツール呼び出しのブロックを含むメッセージは画面に出ません。

streamlit run app.py

動作確認では、公式ガイドの台本と同じ順に試すと抜けが出ません。

  1. 取扱商品を聞く(製品Q&A)
  2. 「商業保険はありますか」と聞く(扱わない商品を断るか)
  3. 天気やレシピを聞く(脱線の切り返し)
  4. 見積もりを頼み、4項目を一部だけ答える(不足分を聞き返すか)
  5. 全項目を答える(get_quoteが呼ばれ、金額が案内されるか)

ガイドは、プロンプトの出来はデプロイして評価しないと分からないと述べ、成功基準に基づく評価を勧めています。Claudeアプリ全般での評価の組み方は、Claudeアプリの成功基準を測る4つのeval実装にあります。

サンプルをそのまま本番に出さないための改修点

ガイドのコードは、動く最小形です。本番に持ち込む前に手を入れたい箇所を、tool useドキュメントと照らして挙げます。

tool_useが複数返る場合を拾えていない

サンプルはcontent[-1]だけを見ます。ところがtool useドキュメントは、応答に「1つ以上のtool_useブロック」が入りうると書いています。同じ応答にツール呼び出しが2つ入っていれば、片方の結果しか返せず、次のリクエストが失敗します。ツールがget_quote1本の間は表面化しませんが、注文照会などを足した時点で顔を出します。

tool_useブロックを全部拾って結果を1メッセージにまとめ、ツール呼び出しがなくなるまで回す形が安全です。例えば次のような形になります(ドキュメントの手順に沿った書き換え例)。

def process_user_input(self, user_input):
    self.session_state.messages.append({"role": "user", "content": user_input})
    for _ in range(5):  # ツール往復の上限
        response = self.generate_message(self.session_state.messages, 2048)
        self.session_state.messages.append(
            {"role": "assistant", "content": response.content}
        )
        tool_uses = [b for b in response.content if b.type == "tool_use"]
        if not tool_uses:
            return next(b.text for b in response.content if b.type == "text")
        results = []
        for tu in tool_uses:
            try:
                out = self.handle_tool_use(tu.name, tu.input)
                results.append({"type": "tool_result", "tool_use_id": tu.id, "content": out})
            except Exception as e:
                results.append({"type": "tool_result", "tool_use_id": tu.id,
                                "content": str(e), "is_error": True})
        self.session_state.messages.append({"role": "user", "content": results})
    return "処理を完了できませんでした。担当者におつなぎします。"

並列呼び出しを避けたいときは、tool_choiceを{"type": "auto", "disable_parallel_tool_use": True}にします。1応答で呼ばれるツールは最大1つになります。disable_parallel_tool_useはtool_choiceの中に置き、トップレベルの引数ではありません。

履歴の積み方とtool_resultの並び順

tool_resultブロックは、対応するtool_useの直後のユーザーメッセージに入れる必要があります。間に別のメッセージは挟めません。テキストを同じメッセージに足すなら、tool_resultより後ろに置きます。前に置くと400エラーになります。結果が欠けたときは、tool_use ids were found without tool_result blocks immediately afterという趣旨のエラーが返ります。上の書き換え例がresultsだけを1メッセージに入れているのは、この規則を守るためです。

サンプルは、ツール呼び出しを含むassistantのcontentをブロックのまま履歴に残し、続けてtool_resultを積みます。この対の形を崩さなければ、次のリクエストでも履歴が整合します。app.pyはcontentが文字列のメッセージだけを描画するので、対の部分は画面に出ません。

ツールの失敗を返す

サンプルのhandle_tool_useは、未知のツール名で例外を投げ、そのままアプリが落ちます。tool useドキュメントは、ツール実行が失敗したらtool_resultにis_error: trueとエラー内容を入れて返すよう案内しています。Claudeはそのエラーを踏まえて利用者に説明します。エラー文は「failed」だけにせず、何が起きたか・次に何を試すかまで書くと、Claudeが立て直しやすくなります(ドキュメントのTip)。必須引数の不足を返す場合も同じ形で、Claudeは2〜3回、引数を直して呼び直します。見積もりAPIが落ちたときに、利用者へ「現在お見積もりを出せません」と返すか、画面が例外で止まるかの差になります。

APIエラーの扱い

generate_messageは例外を握りつぶして{"error": ...}を返し、呼び出し側が"error" in response_messageで判定します。エラーを文字列にして返すため、原因の種類は呼び出し側から見えなくなります。stop_reasonで成功と失敗を切り分ける考え方は、stop_reasonとエラーの違いで扱っています。SDKの非同期実行やtool_runnerで往復ループそのものを任せる手もあり、ClaudeのPython SDKで非同期実行とtool_runnerを実装するが参考になります。

製品知識が増えたときの手当て

ガイドの最後の章は、規模が大きくなったときの選択肢を示しています。

課題ガイドの対処
静的コンテキストが長くなり遅く高くなるガイドの対処RAGで必要な情報だけ取り込む(埋め込みにはVoyageなど)
口座残高や注文状況などリアルタイム情報ガイドの対処RAGでは足りない。tool useで顧客情報の照会や注文の取消を実装する
幻覚・脱獄・競合への言及ガイドの対処出典付きの回答、harmlessness screen(有害入力の事前スクリーニング)、入力検証、PIIの除去
応答が遅く感じるガイドの対処ストリーミングで少しずつ表示する
用件が多岐にわたるガイドの対処意図分類器を前段に置き、専用のプロンプトとツールへ振り分ける(実装例)

ツールを会話の途中で増減させる場合のキャッシュとの関係はmid-conversationのツール変更でtools配列をキャッシュごと保つにまとまっています。

本番運用に向けたサービス化

StreamlitはPythonの関数を画面に出す検証用の器です。ガイドは、実際のリアルタイムサポートにはAPIサービスが必要だとして、FlaskやFastAPIでラップする案を示しています。サービスに持たせたい性質は3つです。

まとめ

ガイドが挙げるサービスの3性質

  • SSEで逐次送る

    Server-Sent Eventsで、生成中の応答をクライアントへ少しずつ流します。

  • キャッシュを使う

    応答時間と不要なAPI呼び出しを減らします。

  • 文脈を保持する

    利用者が画面を離れて戻っても、会話が続くようにします。

サンプルの会話履歴はst.session_stateに置くだけなので、サーバーを再起動すれば消えます。保存先を別に用意する場合は、会話に含まれる個人情報の扱いも合わせて決めることになります。大手のサポートプラットフォームが解決率や切り戻しをどう設計しているかは、Claudeカスタマーサポート導入事例で比較しています。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. platform.claude.com/docs/en/about-claude/use-case-guides/customer-support-chat
  2. platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls
  3. platform.claude.com/docs/en/agents-and-tools/tool-use/overview
  4. platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools
  5. platform.claude.com/docs/en/agents-and-tools/tool-use/strict-tool-use
  6. platform.claude.com/docs/en/agents-and-tools/tool-use/parallel-tool-use
  7. platform.claude.com/docs/en/api/errors
  8. platform.claude.com/docs/en/about-claude/models/overview
この記事を共有:XはてブLinkedIn