コンテンツにスキップ

第01回 学習メモ: Chainlitを使った基本的なチャットボット

全体像

3ファイルが「だんだん本格化していくチュートリアル」になっており、「枝豆の妖精」というキャラ設定(system prompt)が共通。

学習ステップの全体構成

flowchart LR
    Step1["chatgpt.py<br/>OpenAI API を単発で叩く<br/>(CLI / 状態なし)"]
    Step2["app.py<br/>Chainlit で UI だけ立てる<br/>(LLM 未接続 / 固定応答)"]
    Step3["chatbot.py<br/>Step1 + Step2 を統合<br/>+ 会話履歴の保持"]

    Step1 -->|LLM 呼び出しを覚える| Step3
    Step2 -->|Chainlit UI を覚える| Step3

「LLM 単体」と「UI 単体」を別々に学んでから合体する、というステップ分割が綺麗。

chatbot.py のメッセージ往復(ステートレス API でどう会話を成立させるか)

sequenceDiagram
    autonumber
    participant Browser as ブラウザ (Chainlit UI)
    participant Server as chatbot.py (Chainlit ハンドラ)
    participant Session as cl.user_session<br/>(プロセス内 dict)
    participant OpenAI as OpenAI API<br/>(ステートレス)

    Browser->>Server: チャットセッション開始
    Server->>Session: history = [system prompt]
    Note over Session: 妖精キャラの設定だけ入った状態

    Browser->>Server: ユーザー発話「こんにちは」
    Server->>Session: history.append(user)
    Server->>OpenAI: ChatCompletion.create(messages=history)
    Note over OpenAI: 履歴を毎回全送り<br/>これが「ステートレスでも会話できる」理由
    OpenAI-->>Server: assistant 応答
    Server->>Session: history.append(assistant)
    Server->>Browser: cl.Message(...).send()

    Browser->>Server: 次の発話「枝豆って美味しい?」
    Server->>Session: history.append(user)
    Server->>OpenAI: ChatCompletion.create(messages=history)
    Note over OpenAI: 1ターン目の応答も含めて<br/>すべての履歴を再送
    OpenAI-->>Server: assistant 応答
    Server->>Session: history.append(assistant)
    Server->>Browser: cl.Message(...).send()

重要なポイント: OpenAI API 自体はステートレス(過去の会話を覚えていない)。会話が成立しているように見えるのは、クライアントが毎リクエストで履歴を丸ごと送り直しているから。history の所在は Chainlit のプロセス内メモリcl.user_session)であって、永続化はされていない(タブを閉じれば消える)。

使用ライブラリ・原理

OpenAI Chat Completion API

  • messages 配列に {"role": "system" | "user" | "assistant", "content": "..."} を積んでLLMに送る
  • API自体はステートレス: 過去の会話を覚えていないので、クライアントが履歴を毎回まるごと送る
  • temperature: 0で決定的、1で創造的(0.7はキャラ会話の定番)
  • max_tokens: 出力上限。コスト制御と暴走防止

Chainlit

  • StreamlitのLLMチャット特化版のようなフレームワーク
  • chainlit run app.py でブラウザにチャットUIが立ち上がる
  • @cl.on_message: ユーザー送信時に呼ばれるハンドラ
  • @cl.on_chat_start: セッション開始時に1回だけ呼ばれるフック
  • cl.user_session.get/set: ユーザーごとに独立したセッションストア
  • cl.Message(content=...).send(): チャット欄に返信を出す

ファイル別の役割

ファイル 役割
chatgpt.py OpenAI APIをPythonから叩く最小例(CLI、状態なし)
app.py Chainlitの最小例。LLM未接続で固定応答を返すUI骨格
chatbot.py 上記2つを統合 + cl.user_session で会話履歴を永続化

学んだこと(要点)

  • Chat APIのステートレス性: 毎リクエストで履歴を全送するから「会話している」体験になる
  • トークン消費は履歴に比例して増える → 後の回で要約・圧縮の必要性につながる伏線
  • UIとLLMを分けて学ぶ構成: フレームワークの責務を切り分けて理解できる、教科書的に綺麗な順序

拡張アイデア

  • ストリーミング表示(cl.Message().stream_token(...) を使う)
  • 履歴のトークン数を概算し、N件を超えたら古いものを切り詰める
  • system promptをチャット内で動的に切り替える(キャラ選択UI)

現代版に移植するなら

  • APIキーは os.environ["OPENAI_API_KEY"] + .env で読む(ハードコード禁止)。本リポジトリは 1Password CLI 経由
  • openai.ChatCompletion.create(...)client.chat.completions.create(...)(v1.x SDK)
  • model="gpt-3.5-turbo"gpt-4o-mini / gpt-4.1-nano(コスト・性能とも良い)
  • @cl.on_message の引数は message: cl.Message 型推奨(message.content で本文取得)

記事参照

  • Software Design 2023年〜の連載 第01回
  • 動かし方は README.md を参照

作成: 2026-05-17 / 最終更新: 2026-06-10