← レッスンに戻る
第8章 · 実践プロジェクト

シンプルなチャットボット

Simple Chatbot · 約 14 分

重要キーワード

English日本語説明
Stateless API ステートレスAPI サーバーが履歴を保持しない設計。クライアントが履歴を持つ
Streaming ストリーミング 応答を逐次返す方式
Sliding Window スライディングウィンドウ 履歴の古い部分を切る or 要約する技法
Compaction 圧縮 長い会話を要約して短くする処理

シンプルなチャットボット

最小構成のチャットボットを Flask で作ります。

設計

サーバー側

from flask import Flask, request, Response, jsonify
from anthropic import Anthropic

app = Flask(__name__)
client = Anthropic()

SYSTEM = "あなたは丁寧な日本語アシスタントです。"

@app.post("/chat")
def chat():
    history = request.json["history"]   # [{role, content}, ...]
    def gen():
        with client.messages.stream(
            model="claude-sonnet-4-6",
            max_tokens=1024,
            system=SYSTEM,
            messages=history,
        ) as s:
            for t in s.text_stream:
                yield t
    return Response(gen(), mimetype="text/plain")

クライアント側 (擬似)

async function send(userMsg) {
  history.push({role: "user", content: userMsg});
  const res = await fetch("/chat", {
    method: "POST",
    headers: {"Content-Type": "application/json"},
    body: JSON.stringify({history}),
  });
  const reader = res.body.getReader();
  let assistant = "";
  for (;;) {
    const {value, done} = await reader.read();
    if (done) break;
    assistant += new TextDecoder().decode(value);
    render(assistant);
  }
  history.push({role: "assistant", content: assistant});
}

履歴の長期化対策: Sliding Window + Summary

会話が長くなると input が肥大化します。次の戦略を組み合わせます:

MAX_TURNS = 20

def trim(history: list[dict]) -> list[dict]:
    if len(history) <= MAX_TURNS:
        return history
    # 古いターンを要約
    old, recent = history[:-MAX_TURNS], history[-MAX_TURNS:]
    summary = summarize(old)
    return [{"role": "user", "content": f"<会話の要約>\n{summary}"}] + recent

summarize() も Claude で実装すれば自然な要約に。

ベストプラクティス

Cache をかける

長い system は cache_control: ephemeral で。 ユーザーが多いほどヒット率が上がる。

system=[
    {"type": "text", "text": SYSTEM_BASE},
    {"type": "text", "text": LONG_GUIDELINES, "cache_control": {"type": "ephemeral"}},
]

Tip: ユーザーごとのカスタム system

「このユーザーは英語で答える」「あのユーザーは詳細な技術解説を好む」など、 ユーザー設定 + 共通システム のハイブリッドが理想。

この academy アプリも参考に

本講座の Playground は同じパターンで実装されています (app.pyapi_playground)。

演習問題

演習 1: ミニ Flask チャットを書く

150 行以内で、次の機能を持つ Flask チャットを書いてください。

  • / で簡単な HTML (チャット UI)
  • /chat POST: 履歴を受け取り、Streaming で応答を返す
  • 履歴 30 ターン超なら古いターンを要約に置換
  • system は cache_control 付き
スタータープロンプト:
Python + Flask で 150 行以内のミニチャットアプリを書いてください。要件:
1. anthropic SDK でストリーミング応答
2. /chat POST 履歴を JSON で受け取る (stateless)
3. 履歴 30 ターン超なら古いターンを Claude で要約して 1 メッセージに圧縮
4. system プロンプトに cache_control: ephemeral
5. /clear ボタン (フロントで履歴をクリアするだけ)
シングルファイル app.py で完結。
ヒントを見る

本講座アプリ (app.py) のコードが良い参考になります。SSE のフォーマットも同じパターンを流用できます。

理解度チェック

  1. サーバー側で会話履歴を保持しなくてもよい理由は?
    1. Claude API が stateless だから
    2. ブラウザは記憶力が良いから
    3. 禁止されているから
    4. 履歴は不要だから
  2. 履歴が長くなったときの一般的対策は?
    1. 古いターンを要約して圧縮
    2. ランダムに削除
    3. 暗号化して送る
    4. max_tokens を増やすだけ
  3. ストリーミング応答を返すレスポンスの mimetype として一般的なのは?
    1. text/plain or text/event-stream
    2. application/zip
    3. image/png
    4. audio/mp3
解答と解説を見る
  1. A — stateless API なので履歴はクライアントが管理する設計が自然です。
  2. A — コンテキスト溢れを避けるため、要約による圧縮が基本です。
  3. A — プレーン or SSE (text/event-stream) で送るのが一般的です。