「せっかく作ったAIチャットボットが、数回前の会話をすぐに忘れてしまう…」
「LLMのAPIを叩くたびに、どんなコンテキストを渡すべきか悩む…」
LLM(大規模言語モデル)を使ってアプリケーションを開発していると、このようなコンテキスト管理の課題に直面することは少なくありません。単純に会話履歴をリストに追加していくだけでは、すぐにトークン上限に達してしまったり、重要な指示が過去のやり取りに埋もれてしまったりします。
この問題を解決するための一つの強力なデザインパターンが、本記事で紹介する 「Model Context Protocol (MCP)」 という考え方です。
「プロトコル」と聞くと難しく感じるかもしれませんが、要は 「AIモデルに渡す情報を管理するための、一貫したルールや設計思想」 のことです。正式な業界標準があるわけではありませんが、この考え方を取り入れるだけで、AIの応答の一貫性や精度を劇的に向上させることができます。
この記事では、そのMCPの考え方に基づいたシンプルなコンテキスト管理クラスを、外部ライブラリに頼らずわずか50行程度のPythonコードで実装する方法をハンズオン形式で解説します。この記事を読み終える頃には、あなたのAIアプリケーションの対話品質を一段階引き上げるための、実践的な武器が手に入っているはずです。
改めて、Model Context Protocol (MCP) をもう少し詳しく見ていきましょう。これは、AIモデルとの対話を円滑に進めるために、モデルに渡す「コンテキスト(文脈)」を構成する要素を定義し、それらをどのように組み立てるかを定めた「約束事」です。
-
システムプロンプト (System Prompt)
AIの役割、性格、応答スタイル、禁止事項などを定義する、不変の基本指示です。
例:「あなたは親切で丁寧なアシスタントです。常に日本語で回答してください。」
これが一貫して提供されることで、AIの振る舞いが安定します。 -
対話履歴 (Conversation History)
ユーザーとAIの直近のやり取りです。文脈を維持するために不可欠です。
しかし、無限に保持することはできないため、「直近のN回」のように制限を設ける戦略が必要になります。 -
長期記憶 / ユーザー情報 (Long-term Memory / User Profile)
対話を通じて得られた、忘れてほしくない重要な情報です。
例:「ユーザーの名前は田中さん」「ユーザーはPythonが好き」
今回のシンプルな実装では扱いませんが、高度なMCPではこの情報を外部DB(VectorDBなど)に保存し、関連する情報を都度コンテキストに含めます。 -
動的コンテキスト (Dynamic / Transient Context)
その場の対話で必要となる一時的な情報です。RAG (Retrieval-Augmented Generation) で検索してきたドキュメントなどがこれにあたります。
例:「ユーザーが質問した製品XXXの仕様書はこれです…」
MCPの核心は、これらの要素を 「どの順番で」「どのくらいの量」「どのように組み合わせて」 モデルに渡すかをルール化することにあります。これにより、場当たり的なコンテキスト管理から脱却し、予測可能で精度の高いAI応答を実現できるのです。
50行で実装するシンプルなMCP
それでは、いよいよ実装に入りましょう。
今回は、上記要素のうち「システムプロンプト」と「対話履歴」を管理する、最もシンプルかつ効果的なコンテキストマネージャーを作成します。対話履歴は、古いものから自動的に削除されるように collections.deque を使うのがポイントです。
使用するライブラリはPythonの標準ライブラリとopenaiのみです。
実装コード全体
以下が、今回作成するSimpleContextManagerクラスの全コードです。コメントや空行を含めても、クラス定義部分はおよそ50行程度に収まります。
import os
from collections import deque
from openai import OpenAI
import textwrap
# --- ここからが約50行の実装 ---
class SimpleContextManager:
"""
シンプルなModel Context Protocolを実装するクラス。
Attributes:
system_prompt (str): モデルに常に渡す基本的な指示。
history (deque): 対話履歴を保持する。古いものから自動的に削除される。
model_name (str): 使用するモデル名。
"""
def __init__(self, system_prompt: str, max_history_size: int = 5, model_name: str = "gpt-4o-mini"):
"""
コンテキストマネージャーを初期化する。
Args:
system_prompt (str): AIの役割を定義するシステムプロンプト。
max_history_size (int): 保持する対話の往復数。
model_name (str): 使用するOpenAIのモデル名。
"""
self.system_prompt = system_prompt
# ユーザーとAIのペアで履歴を管理するため、dequeのサイズは2倍にする
self.history = deque(maxlen=max_history_size * 2)
self.model_name = model_name
def add_message(self, role: str, content: str):
"""
対話履歴に新しいメッセージを追加する。
Args:
role (str): メッセージの役割 ('user' or 'assistant')。
content (str): メッセージの内容。
"""
self.history.append({"role": role, "content": content})
def build_messages(self, user_input: str) -> list[dict]:
"""
OpenAI APIに渡すためのメッセージリストを構築する。
Args:
user_input (str): 最新のユーザーからの入力。
Returns:
list[dict]: OpenAI APIの形式に合わせたメッセージのリスト。
"""
messages = [{"role": "system", "content": self.system_prompt}]
messages.extend(self.history)
messages.append({"role": "user", "content": user_input})
return messages
def get_response(self, user_input: str, client: OpenAI) -> str:
"""
ユーザーの入力からモデルの応答を生成し、対話履歴を自動で更新する。
Args:
user_input (str): 最新のユーザーからの入力。
client (OpenAI): 初期化済みのOpenAI APIクライアント。
Returns:
str: モデルからの応答テキスト。
"""
# プロトコルに従ってメッセージリストを構築
messages = self.build_messages(user_input)
# APIを呼び出し(本番環境ではtry-exceptでのエラーハンドリングを推奨)
completion = client.chat.completions.create(
model=self.model_name,
messages=messages,
)
ai_response = completion.choices[0].message.content
# 次の対話のために、今回のやり取りを履歴に追加
self.add_message("user", user_input)
self.add_message("assistant", ai_response)
return ai_response
# --- 実装はここまで ---
コードのポイント解説
- system_prompt
- AIのキャラクターを決定する最も重要な指示を受け取ります。
- max_history_size
- 何往復分の会話を記憶しておくかを設定します。5に設定すれば、直近5往復(ユーザー5回、AI5回)の計10メッセージを保持します。
- deque(maxlen=…)
- これがこのクラスの心臓部です。maxlen を設定した deque は、新しい要素が追加されて上限を超えると、自動的に最も古い要素から削除してくれます。これにより、トークン管理の最も基本的な形である「FIFO (First-In, First-Out)」を簡単に実現できます。
build_messages(self, ...):
- これがまさに「プロトコル」を実装している部分です。
-
- system ロールのメッセージをリストの先頭に必ず配置。
-
- extend を使って、保持している対話履歴 (self.history) を追加。
-
- 最後に、最新の user メッセージを追加。
この一貫した順序と構造が、AIの応答を安定させます。
- 最後に、最新の user メッセージを追加。
-
- このメソッドは、単純にAPIを叩くだけでなく、重要な役割を担っています。
- 応答を受け取った後、self.add_message を使って、今回の「ユーザー入力」と「AIの応答」を両方とも履歴に追加しています。これを忘れると、AIは自分が直前に何を言ったかを次のターンで忘れてしまいます。
では、このSimpleContextManagerを実際に使ったチャットボットのループを書いてみましょう。
def main():
# OpenAIクライアントの初期化(環境変数にAPIキーが設定されていること)
try:
client = OpenAI()
except Exception as e:
print(f"OpenAIクライアントの初期化に失敗しました。環境変数 'OPENAI_API_KEY' を確認してください。")
print(f"エラー詳細: {e}")
return
# 1. システムプロンプトを定義
system_prompt = "あなたは猫の「タマ」です。ユーザーの良き相棒として、少し生意気だけど親しみやすい口調で話します。語尾には必ず「〜ニャ」を付けてください。"
# 2. コンテキストマネージャーを初期化
context_manager = SimpleContextManager(system_prompt=system_prompt, max_history_size=3)
print("AIチャットボット「タマ」との会話を開始します。(終了するには '終了' と入力してください)")
print("--------------------------------------------------")
while True:
user_input = input("あなた: ")
if user_input.lower() in ["終了", "exit", "quit"]:
print("タマ: また話そうなのニャ!バイバイだニャン!")
break
# 3. 応答を取得(この中で履歴管理も行われる)
ai_response = context_manager.get_response(user_input, client)
# 整形して表示
wrapped_text = textwrap.fill(f"タマ: {ai_response}", width=80)
print(wrapped_text)
print("--------------------------------------------------")
if __name__ == "__main__":
main()
実行結果の例
【MCPなしの残念な例(イメージ)】
あなた: 僕の名前は健太だよ。よろしくね!
タマ: 健太っていうのかニャ!よろしくニャン!
… (数回のやり取り) …
あなた: ところで、僕の名前覚えてる?
タマ: うーん、聞いてなかったニャ。誰だったかニャ?
【SimpleContextManagerを使った場合の改善例】
あなた: 僕の名前は健太だよ。よろしくね!
タマ: 健太っていうのかニャ!これからよろしくニャン!
あなた: 最近、面白い映画とかあった?
タマ: 最近だと『ニャンタスティック・ビースト』が面白かったニャ。魔法動物がいっぱいで最高だったニャン!
あなた: へぇ、面白そうだね!ところで、僕の名前覚えてる?
タマ: もちろんだニャ!健太だろ?忘れるわけないニャン!
dequeによって直近の対話履歴が適切に保持されているため、AIは自分の役割(猫のタマ)を忘れず、ユーザーの名前もしっかりと覚えています。これがコンテキスト管理の力です。
このわずか50行の実装が、なぜこれほど効果的なのでしょうか?
-
一貫性の担保
system_promptが毎回必ず渡されるため、AIのキャラクターや振る舞いがブレません。
短期記憶の実現: dequeによる履歴管理で、AIは直前の文脈を正確に把握でき、チグハグな会話を避けられます。 -
トークン暴発の防止
履歴のサイズに上限があるため、対話が長くなってもAPIリクエストのトークン数が一定範囲内に収まり、コスト管理やエラー防止に繋がります。 -
コードの可読性向上
コンテキスト管理のロジックがクラスに集約されるため、メインのアプリケーションコードが非常にスッキリします。
さらなる発展へ:次のステップ
今回実装したのは、MCPの最も基本的な形です。しかし、この考え方を土台に、さらに高度な機能へと拡張していくことができます。
-
履歴の要約
古い履歴を単純に捨てるのではなく、「ユーザーは〇〇に興味がある」といった形で要約して、対話履歴の代わりに保持する。 -
Vector Databaseとの連携
ユーザーの基本情報(名前、趣味など)をVector DBに保存しておき、対話内容と関連性の高い情報を検索してbuild_messages時に動的にコンテキストに注入する。(いわゆるLong-term Memoryの実装) -
RAGの統合
外部ドキュメントを検索するRAGの仕組みを組み込み、検索結果を「動的コンテキスト」としてプロンプトに含めるようにbuild_messagesを拡張する。
実は、LangChainやLlamaIndexといったライブラリが提供しているMemoryクラス群は、これらの高度なMCPを洗練された形で実装したものです。しかし、今回のようにシンプルな形でもその本質を自ら実装してみることで、それらの強力なツールが「中で何をやっているのか」を深く理解できるようになるでしょう。
本記事では、AIの対話精度と一貫性を向上させるための「Model Context Protocol」という設計思想と、その非常にシンプルな実装方法を解説しました。
- コンテキスト管理は、場当たり的ではなく「プロトコル(ルール)」として設計することが重要。
- システムプロンプトと、上限付きの対話履歴を管理するだけでも、AIの品質は大きく向上する。
- collections.dequeを使えば、わずか50行程度のコードで効果的なコンテキスト管理を実装できる。
LLMを使った開発において、コンテキスト管理はアプリケーションの品質を左右する非常に重要な要素です。ぜひ、今回紹介したSimpleContextManagerの考え方を、あなたの次のプロジェクトに取り入れてみてください。きっと、あなたのAIがもっと賢く、もっと魅力的な対話相手になるはずです。
この記事が、皆さんのAI開発の一助となれば幸いです。
Happy Hacking!
Views: 0