PythonとLLMでチャットボットを作る:会話管理とコンテキストメモリをゼロから構築する

Artificial Intelligence tutorial - IT technology blog
Artificial Intelligence tutorial - IT technology blog

初めてLLMのAPIを呼び出したとき、これだけでチャットボットができると思っていた―でも直前に話した内容をすべて忘れてしまう。ユーザーが「名前はナムです」と入力し、続けて「私の名前は何ですか?」と聞くと、ボットは初めて会ったかのように答える。その瞬間に気づいた:APIを呼び出せるのはただの第一歩で、チャットボットに本当に会話の文脈を記憶させることの方がずっと難しいと。

この記事ではその問題を直接解決する―PythonとLLMで本当に記憶を持つチャットボットを、アーキテクチャからすぐに動くコードまで構築する。

問題:なぜチャットボットは「記憶を失う」のか?

ほとんどのLLM APIはステートレス―APIを呼び出すたびに、モデルはそれ以前の会話について何も知らない。リクエストのたびに会話履歴全体を自分で送り直す必要がある。面倒に聞こえるが、これは意図的な設計だ:モデルが見るコンテキストを完全にコントロールできる。

historyなしで「一問一答」形式でAPIを呼び出すだけなら、単独の質問に答えるマシンを作っているだけで、チャットボットではない。だからこそ、コード自動生成チャットボットやカスタマーサポートアシスタントは、みんなこの会話管理の問題を解決しなければならない。

押さえておくべきコアコンセプト

Conversation Historyとは?

基本的には、これはただのPythonのリストだ。各要素はrole(user/assistant)とcontentを持つメッセージ。リスト全体をAPIに送ると、モデルがすべて読んでから回答する。

conversation_history = [
    {"role": "user",      "content": "Tên tôi là Nam"},
    {"role": "assistant", "content": "Chào Nam! Mình có thể giúp gì?"},
    {"role": "user",      "content": "Vậy tên tôi là gì?"},
]
# Gửi cả list → model biết context → trả lời đúng "Tên bạn là Nam"

System Prompt

System promptはrole: "system"の特別なメッセージで、会話の先頭に置く。一言で言えば:ここでモデルに「お前は何者で、何をして、何を許可されているか」を伝える。ボットに短く答えさせるか詳しく答えさせるか、日本語か英語か、特定のトピックだけか柔軟に対応するか―すべてここで定義する。

Context Window

各モデルには入力トークンの上限がある―Claude Haiku 200K、GPT-4o 128K。実際には、やり取り20回分(約3,000〜5,000トークン)がリクエストのたびに積み重なる:ユーザー1人なら大したことはないが、1,000人になると話が変わる。会話が長くなりすぎるとcontext_length_exceededエラーも起きるため、自分でhistoryをトリムする必要がある。

実践:会話メモリ付きPythonチャットボットの構築

ライブラリのインストール

Anthropic SDKを使う。APIの設計がシンプルで、メッセージのアーキテクチャを理解しやすいからだ。OpenAIやGeminiを使う場合も構造は似ていて、関数名が違うだけだ。

pip install anthropic python-dotenv

APIキーを保存する.envファイルを作成:

ANTHROPIC_API_KEY=sk-ant-api03-your-key-here

会話メモリ付き基本チャットボット

chatbot.pyを作成:

import os
from dotenv import load_dotenv
import anthropic

load_dotenv()

client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

SYSTEM_PROMPT = """Bạn là trợ lý IT thân thiện, chuyên giải thích kỹ thuật
bằng tiếng Việt dễ hiểu. Trả lời ngắn gọn, đúng trọng tâm."""

def chat(history: list, user_message: str) -> str:
    history.append({"role": "user", "content": user_message})

    response = client.messages.create(
        model="claude-haiku-4-5-20251001",
        max_tokens=1024,
        system=SYSTEM_PROMPT,
        messages=history
    )

    reply = response.content[0].text
    history.append({"role": "assistant", "content": reply})
    return reply


def main():
    print("Chatbot IT — gõ 'quit' để thoát\n")
    history = []

    while True:
        user_input = input("Bạn: ").strip()
        if not user_input:
            continue
        if user_input.lower() in ("quit", "exit", "q"):
            print("Tạm biệt!")
            break
        print(f"Bot: {chat(history, user_input)}\n")


if __name__ == "__main__":
    main()

実行してみる:

python chatbot.py
Chatbot IT — gõ 'quit' để thoát

Bạn: Docker là gì?
Bot: Docker là nền tảng container hóa ứng dụng...

Bạn: Cho ví dụ cụ thể hơn
Bot: (Tiếp tục từ câu hỏi về Docker, không bị mất context)

会話履歴の長さ制限を追加する

長い会話はトークンをたくさん消費してコストがかかる。シンプルな解決策:直近N組のメッセージのみ保持する。

MAX_HISTORY_PAIRS = 10  # Giữ 10 cặp hỏi-đáp gần nhất

def trim_history(history: list) -> list:
    max_messages = MAX_HISTORY_PAIRS * 2  # 1 cặp = 1 user + 1 assistant
    return history[-max_messages:] if len(history) > max_messages else history


def chat(history: list, user_message: str) -> str:
    history.append({"role": "user", "content": user_message})

    response = client.messages.create(
        model="claude-haiku-4-5-20251001",
        max_tokens=1024,
        system=SYSTEM_PROMPT,
        messages=trim_history(history)  # Gửi history đã trim
    )

    reply = response.content[0].text
    history.append({"role": "assistant", "content": reply})
    return reply

セッション間での会話の保存と読み込み

自分でボットを作るときに最もよく見落とされるポイント―ターミナルを閉じるとコンテキストがすべて消える。ユーザーが長時間デバッグして長いコンテキストを積み上げても、再起動すると最初からやり直しになる。historyをJSONに保存することでスッキリ解決できる:

import json
from pathlib import Path

HISTORY_FILE = Path("chat_history.json")

def save_history(history: list):
    HISTORY_FILE.write_text(
        json.dumps(history, ensure_ascii=False, indent=2),
        encoding="utf-8"
    )

def load_history() -> list:
    if HISTORY_FILE.exists():
        return json.loads(HISTORY_FILE.read_text(encoding="utf-8"))
    return []

完全なコード(約60行)

すべてをまとめた完全なchatbot.py

import os, json
from pathlib import Path
from dotenv import load_dotenv
import anthropic

load_dotenv()

client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
HISTORY_FILE = Path("chat_history.json")
MAX_HISTORY_PAIRS = 10

SYSTEM_PROMPT = """Bạn là trợ lý IT thân thiện, giải thích kỹ thuật bằng tiếng Việt
dễ hiểu. Trả lời ngắn gọn, đúng trọng tâm, dùng ví dụ thực tế khi cần."""


def trim_history(history: list) -> list:
    max_msg = MAX_HISTORY_PAIRS * 2
    return history[-max_msg:] if len(history) > max_msg else history


def chat(history: list, user_message: str) -> str:
    history.append({"role": "user", "content": user_message})
    response = client.messages.create(
        model="claude-haiku-4-5-20251001",
        max_tokens=1024,
        system=SYSTEM_PROMPT,
        messages=trim_history(history)
    )
    reply = response.content[0].text
    history.append({"role": "assistant", "content": reply})
    return reply


def save_history(history: list):
    HISTORY_FILE.write_text(
        json.dumps(history, ensure_ascii=False, indent=2), encoding="utf-8"
    )


def load_history() -> list:
    return (
        json.loads(HISTORY_FILE.read_text(encoding="utf-8"))
        if HISTORY_FILE.exists() else []
    )


def main():
    print("Chatbot IT — 'quit' thoát | 'clear' xóa lịch sử\n")
    history = load_history()
    if history:
        print(f"(Tải {len(history) // 2} cặp hội thoại cũ)\n")

    while True:
        user_input = input("Bạn: ").strip()
        if not user_input:
            continue
        if user_input.lower() in ("quit", "exit", "q"):
            save_history(history)
            print("Đã lưu lịch sử. Tạm biệt!")
            break
        if user_input.lower() == "clear":
            history.clear()
            print("Đã xóa lịch sử hội thoại.\n")
            continue
        print(f"Bot: {chat(history, user_input)}\n")


if __name__ == "__main__":
    main()

動作確認

最初に実行して会話し、quitと入力する。再実行すると―ボットは以前の会話をすべて覚えている:

python chatbot.py
# (Tải 5 cặp hội thoại cũ)
# Bạn: Hồi nãy mình hỏi gì vậy?
# Bot: Bạn hỏi về Docker và cách dùng volume...

まとめ

「APIを呼び出せれば完成」で止まる人が多い―でも本当に使えるチャットボットにするのは、conversation historyを正しく管理することだ。約60行のPythonだけで、記憶を持つボット、会話の保存・読み込み、context windowの超過防止を実現できる。

ここからの拡張は自然な流れだ:ボットが実際にAPIを呼び出したりコードを実行できるtool useの追加、社内ドキュメントを読み込むRAGの実装、あるいは全体をFastAPIでREST APIとしてラップするなど。でも先へ進む前に―ステートレスAPIとhistoryの自己管理という根本を理解しておくことで、ボットが誤動作したときのデバッグがずっとスムーズになる。

Share: