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を使い、残りはシステムプロンプトで実現します。

構成は次の4ファイルです。

ファイル役割
config.py役割システムプロンプトの部品、モデル名、ツール定義、get_quoteの仮実装
chatbot.py役割ChatBotクラス。API呼び出しとtool useの往復を担う
app.py役割StreamlitのチャットUI
.env役割ANTHROPIC_API_KEY(python-dotenvが読み込む)

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

pip install anthropic streamlit python-dotenv

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

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

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

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

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

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

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

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

成功基準を数字で置く

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

指標ガイドが示す目安
回答の関連性ガイドが示す目安90%以上
出典リンクの提示ガイドが示す目安追加情報が役立つ会話の80%
話題の維持ガイドが示す目安応答の95%が保険関連
見積もりの正確さガイドが示す目安100%(金額を扱うため)
エスカレーション精度ガイドが示す目安95%以上
顧客感情の維持・改善ガイドが示す目安会話の90%
自己解決率(deflection rate)ガイドが示す目安問い合わせの複雑さ次第で70〜80%
満足度(CSAT)ガイドが示す目安5点満点で4点以上

数字はガイド自身が「例」として置いたものです。自社では、まず現在の人間対応の実績値を測ってから置き直すことになります。

モデルを選ぶ

ガイドは、複雑で長い多段の会話にはClaude Opus 5を、RAG・tool use・長文脈を組み合わせた複数プロンプトの流れで遅延を優先したい場合はClaude Haiku 4.5を勧めています。サンプルコードのMODEL定数はclaude-opus-5-5です。

システムプロンプトを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項目がすべて入っているのがポイントです。項目が揃うまでは、Claudeがツールを呼ばずに不足分を聞き返します。ガイドの応答例5も、見積もり依頼に対して車種・年式・走行距離・運転者の年齢を先に聞く形になっています。

関数本体はガイドでは仮実装です。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ドキュメントが定める手順どおりです。応答のstop_reasonはtool_useになり、tool_useブロックにid・name・inputが入ります。アプリはツールを実行し、roleがuserのメッセージにtool_resultブロックを入れて返します。tool_resultには対応するtool_use_idを持たせます。

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にdisable_parallel_tool_use: trueを指定する方法もドキュメントにあります。

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

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

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

ツールの失敗を返す

サンプルのhandle_tool_useは、未知のツール名で例外を投げ、そのままアプリが落ちます。公式ドキュメントは、ツール実行が失敗したらtool_resultにis_error: trueとエラー内容を入れて返すよう案内しています。すると、Claudeはエラーを踏まえて利用者に説明します。見積もり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つです。

  • SSE(Server-Sent Events)で応答を逐次送る
  • キャッシュで応答時間とAPI呼び出しを減らす
  • 利用者が画面を離れても戻れるよう、会話の文脈を保持する

サンプルの会話履歴はst.session_stateに置くだけなので、サーバーを再起動すれば消えます。保存先を別に用意する場合は、会話に含まれる個人情報の扱いも合わせて決めることになります。

よくある質問

見積もり以外のツールは何から足せばよいか

ガイド自身が挙げるのは、顧客情報の検索、注文情報の取得、注文の取消です。注文の取消のように取り消せない操作を持つツールは、実行前に利用者へ確認を取る流れをプロンプトの応答例で示しておく方法があります。

導入企業はどう作っているのか

大手のサポートプラットフォームが、解決率や切り戻しをどう設計しているかは、Claudeカスタマーサポート導入事例で比較しています。

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