Claude Codeでチャットボットを開発する方法
Claude Codeを使ってAIチャットボットを効率的に開発。会話履歴管理、ストリーミング応答、RAG構成まで実践コードで解説。
Claude Codeでチャットボットを開発する
AIチャットボットの開発には、UI実装、API連携、会話管理、ストリーミング処理など多くの要素があります。Claude Codeなら、これらを統合したチャットボットを短時間で構築できます。
基本的なチャットUIの実装
> Reactでチャットボットのコンポーネントを作って。
> ストリーミング応答対応、会話履歴表示、入力フォーム付きで。
import { useState, useRef, useEffect } from "react";
interface Message {
id: string;
role: "user" | "assistant";
content: string;
}
export function ChatBot() {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
const sendMessage = async () => {
if (!input.trim() || isLoading) return;
const userMessage: Message = {
id: crypto.randomUUID(),
role: "user",
content: input,
};
setMessages((prev) => [...prev, userMessage]);
setInput("");
setIsLoading(true);
const assistantId = crypto.randomUUID();
setMessages((prev) => [
...prev,
{ id: assistantId, role: "assistant", content: "" },
]);
try {
const response = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
messages: [...messages, userMessage].map(({ role, content }) => ({
role,
content,
})),
}),
});
const reader = response.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
setMessages((prev) =>
prev.map((m) =>
m.id === assistantId
? { ...m, content: m.content + chunk }
: m
)
);
}
} catch (error) {
setMessages((prev) =>
prev.map((m) =>
m.id === assistantId
? { ...m, content: "エラーが発生しました。再度お試しください。" }
: m
)
);
}
setIsLoading(false);
};
return (
<div className="flex flex-col h-[600px] border rounded-lg">
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map((msg) => (
<div
key={msg.id}
className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}
>
<div
className={`max-w-[70%] p-3 rounded-lg ${
msg.role === "user"
? "bg-blue-600 text-white"
: "bg-gray-100 text-gray-900"
}`}
>
{msg.content}
</div>
</div>
))}
<div ref={messagesEndRef} />
</div>
<div className="border-t p-4 flex gap-2">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && !e.shiftKey && sendMessage()}
placeholder="メッセージを入力..."
className="flex-1 p-2 border rounded-lg"
disabled={isLoading}
/>
<button
onClick={sendMessage}
disabled={isLoading}
className="px-4 py-2 bg-blue-600 text-white rounded-lg disabled:opacity-50"
>
送信
</button>
</div>
</div>
);
}
ストリーミング対応のAPIルート
バックエンドでAnthropicのAPIを呼び出し、ストリーミングで返すAPIルートです。
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
export async function POST(request: Request) {
const { messages } = await request.json();
const stream = await client.messages.stream({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
system: "あなたは親切で丁寧なアシスタントです。日本語で回答してください。",
messages,
});
const encoder = new TextEncoder();
const readable = new ReadableStream({
async start(controller) {
for await (const event of stream) {
if (
event.type === "content_block_delta" &&
event.delta.type === "text_delta"
) {
controller.enqueue(encoder.encode(event.delta.text));
}
}
controller.close();
},
});
return new Response(readable, {
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
}
会話履歴の永続化
会話をデータベースに保存して、後から続きを再開できるようにします。
import { db } from "@/lib/database";
export async function saveConversation(
userId: string,
messages: Message[]
) {
return db.conversation.upsert({
where: { id: `${userId}-current` },
update: {
messages: JSON.stringify(messages),
updatedAt: new Date(),
},
create: {
id: `${userId}-current`,
userId,
messages: JSON.stringify(messages),
},
});
}
export async function loadConversation(userId: string): Promise<Message[]> {
const conv = await db.conversation.findUnique({
where: { id: `${userId}-current` },
});
return conv ? JSON.parse(conv.messages as string) : [];
}
RAG(検索拡張生成)の組み込み
社内ドキュメントをもとに回答するチャットボットを作る場合、RAG構成が有効です。
import { searchDocuments } from "@/lib/vector-search";
async function generateRAGResponse(query: string, conversationHistory: Message[]) {
// 関連ドキュメントを検索
const relevantDocs = await searchDocuments(query, { limit: 5 });
const context = relevantDocs
.map((doc) => `---\n${doc.title}\n${doc.content}\n---`)
.join("\n");
const systemPrompt = `以下のドキュメントを参考に質問に回答してください。
ドキュメントに情報がない場合は「その情報は見つかりませんでした」と答えてください。
${context}`;
return client.messages.stream({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
system: systemPrompt,
messages: conversationHistory,
});
}
MCPサーバーとの連携で機能を拡張する方法はMCPサーバーガイドを、効果的なプロンプト設計はプロンプトを改善する5つのTipsをご覧ください。
まとめ
Claude Codeを使えば、チャットUI、ストリーミングAPI、会話管理、RAG構成まで含めたチャットボットを効率的に開発できます。段階的に機能を追加していくアプローチが効果的です。
詳しくはClaude Code公式ドキュメントとAnthropic API リファレンスを参照してください。
2026年版: 本番運用に向けたアップグレード
チャットボットとは、ユーザーの文章を受け取り、会話の文脈を見ながら次の返答を作る小さなアプリケーションです。難しく言えば自然言語インターフェースですが、実務では「問い合わせ、検索、案内、作業依頼をチャット画面で受ける窓口」と考えると設計しやすくなります。
Claude Codeで作る価値は、UIだけでなくAPIルート、ストリーミング、会話履歴、RAG、運用ログまで同じリポジトリ上で確認しながら詰められる点です。ただし、デモのチャット欄を置くだけでは収益化にも継続利用にもつながりません。誰のどの作業を短くするのか、失敗時にどこへ逃がすのか、どのログで改善するのかを最初に決める必要があります。
最初のスコープは狭いほど成功します。FAQを10件だけ答える、問い合わせ前の情報収集だけ担当する、社内手順書の検索だけに絞る、といった形です。最初から万能アシスタントを目指すと、回答品質、権限管理、コスト、監視が同時に膨らみ、公開前の検証が終わらなくなります。
アーキテクチャ全体像
| 層 | 役割 | 実装のポイント |
|---|---|---|
| React UI | 入力、会話表示、送信中状態を扱う | useStateだけで始め、フォーム送信とEnterキーの挙動を明確にする |
| APIルート | APIキーを隠し、Claude APIへ安全に中継する | ブラウザへ秘密情報を出さず、タイムアウトと入力サイズ制限を入れる |
| ストリーミング | 長い回答を少しずつ返す | ReadableStreamを使い、途中失敗時の表示を壊さない |
| 会話ストア | 再開、監査、改善のため履歴を保存する | 個人情報を最小化し、削除要求に対応できる形にする |
| RAG | 社内文書や商品情報を根拠に回答する | 検索結果がない場合は推測で答えない |
| 分析 | 成功率、離脱、有人引き継ぎを測る | Claude Codeの分析実装とつなげる |
API連携を先に固めたい場合はClaude Code API開発、外部フォームやCRMへ通知したい場合はClaude Code Webhook実装を先に読むと、後から作り直す範囲を減らせます。フロント側の状態管理はReactのuseState公式ドキュメント、ストリーミングの基礎はMDN Streams APIを参照してください。
コピペで動かせる最小ストリーミングデモ
次のコードは外部APIなしで動く確認用です。chatbot-stream-demo.mjsとして保存し、node chatbot-stream-demo.mjsで実行できます。UIに接続する前に、会話履歴とストリーミング表示の考え方を確認するための最小構成です。
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const faq = new Map([
["password", "Open the account page, choose Reset password, and follow the email link."],
["pricing", "The pricing page explains plans. For a custom quote, collect team size and required features."],
["refund", "Refund requests should be routed to support with the order id and purchase email."],
]);
const history = [];
function chooseAnswer(question) {
const normalized = question.toLowerCase();
for (const [keyword, answer] of faq) {
if (normalized.includes(keyword)) return answer;
}
return "I could not find a safe answer in the FAQ. I will hand this to a human operator.";
}
async function* streamText(text) {
for (const token of text.split(/(\s+)/)) {
await new Promise((resolve) => setTimeout(resolve, 15));
yield encoder.encode(token);
}
}
async function ask(question) {
history.push({ role: "user", content: question });
const answer = chooseAnswer(question);
process.stdout.write(`\nUser: ${question}\nAssistant: `);
let fullAnswer = "";
for await (const chunk of streamText(answer)) {
const token = decoder.decode(chunk);
fullAnswer += token;
process.stdout.write(token);
}
history.push({ role: "assistant", content: fullAnswer });
}
await ask("How do I reset my password?");
await ask("Can I see pricing before talking to sales?");
console.log(`\n\nSaved ${history.length} messages.`);
このデモで確認するのはAIの賢さではなく、表示の壊れにくさです。送信中に再送できないこと、途中の文字が順番通りに出ること、履歴にユーザー発話と回答がペアで残ることを見ます。本番ではchooseAnswerの部分をClaude API呼び出しに差し替えます。
実例とユースケース
1つ目のユースケースは、SaaSの導入前問い合わせです。料金、権限、セキュリティ、連携先の質問をチャットで受け、見込み度が高い場合だけ面談フォームへ案内します。営業担当がすべての初回質問に答えるより、商談化しやすい問い合わせに集中できます。
2つ目は、社内ヘルプデスクです。休暇申請、経費精算、VPN、端末交換など、同じ質問が繰り返される領域に向いています。RAGで社内文書を検索し、答えの根拠となるページを一緒に返せば、担当部署への確認回数を減らせます。
3つ目は、メディアや講座サイトの学習案内です。読者が「API連携を先に学ぶべきか、Webhookを先に学ぶべきか」と迷ったとき、現在の経験値に合わせて記事やトレーニングへ案内します。単なる回遊リンクより自然で、収益につながる導線を作りやすくなります。
4つ目は、障害時の一次切り分けです。エラー文、発生時刻、対象画面、再現手順をチャットで集め、必要な情報がそろったらWebhookでチケットを作ります。ここでは「解決する」より「担当者が調査を始められる状態にする」ことを成功条件にすると安定します。
失敗例と落とし穴
よくある失敗は、会話履歴をそのまま無制限にモデルへ渡すことです。長い履歴はコストを増やし、古い発言が新しい要件を邪魔します。一定件数で要約する、現在の問い合わせに必要な文だけを残す、機密情報を保存しない、というルールを先に置きます。
次の落とし穴は、RAGの検索結果がないのに自信満々に答えることです。これはユーザー体験だけでなく法務やサポート品質の問題になります。検索結果が弱い場合は「情報が見つからない」と答え、問い合わせフォームや担当者へ渡す分岐を作ります。
ストリーミングにも注意が必要です。ネットワークが切れたときに空の吹き出しだけ残る、二重送信で回答が混ざる、ブラウザの戻る操作で履歴が消える、といった小さな崩れが信頼を落とします。送信ボタンの無効化、AbortController、タイムアウト、再試行文言を実装チェックリストに入れてください。
最後に、ログを取らないチャットボットは改善できません。成功した会話だけでなく、有人引き継ぎ、無回答、途中離脱、CTAクリックを測る必要があります。収益化を狙うなら、回答精度より先に「どの質問が購入、相談、登録につながったか」を見えるようにします。
収益化CTAと次の導線
この構成を自社サイトや教材サイトに入れるなら、最初のCTAは1つに絞ります。導入相談、無料診断、講座申し込み、テンプレート販売などを同時に並べると、チャットボットの返答も計測もぼやけます。ClaudeCodeLabでは、実装レビューや社内導入の相談をトレーニングから受けられる導線にしています。
記事内リンクとしては、API設計はClaude Code API開発、外部サービス連携はClaude Code Webhook実装、改善計測はClaude Code分析実装へ送ると、読者が次に読む内容を選びやすくなります。単発記事で終わらせず、実装、連携、計測、相談の順に導線を作ることが monetization では重要です。
検証メモ
この記事で紹介した内容を実際に試した結果、最小デモはNode.js 20系で標準のTextEncoder、TextDecoder、トップレベルawaitだけで動きました。React側は送信中状態とスクロール追従を先に確認し、その後でAPIルートへ接続する順番の方が問題を切り分けやすいです。実務では、AIの回答品質を詰める前に、入力制限、エラー表示、ログ、有人引き継ぎを先に通しておくと公開後の事故が減ります。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
ObsidianメモをCLAUDE.mdに変えるClaude Code運用: 文脈を毎回説明しない仕組み
Obsidianの作業メモからCLAUDE.md用の運用ノートを作り、Claude Codeに安定した文脈を渡す方法。
Claude Code Revenue CTA Routing: 記事からPDF、Gumroad、相談へ送る設計
PVだけで終わらせず、読者の状態に合わせて無料PDF、Gumroad教材、導入相談へ分岐するCTA設計です。
Claude Codeチーム引き継ぎルール: レビュー、権限、収益導線まで渡す実務手順
Claude Codeの作業をチームで渡すための証拠、権限、ロールバック、無料PDF/Gumroad/相談導線の実務ルール。