はじめに
ChatGPT、Gemini、Copilotは過去のやり取りを踏まえて回答してくれるのでチャット形式でやり取りが可能です。
しかし、自作のRAGシステムでは、「会話を覚える仕組み」 を自分で組み込む必要があります。
そこで今回は、RAGでチャット形式の対話を実現するための実装方法について解説したいと思います。
RAGについて知りたい方はこちらを参照
会話を覚える仕組みはMemoryで実現できる
チャット形式で対話するには
過去の会話履歴を保持し、ユーザからの質問に対して、会話履歴の内容も踏まえた回答が必要になります。
それには LangChain の Memory を使います。
ConversationBufferMemoryを使って実装してみる
Memoryには様々な種類はありますが今回は ConversationBufferMemory を使って実装します。
【参考】その他のMemory機能
- ConverSationBufferWindowMemory
- ConverSationSummaryMemory
- ConverSationSummaryBufferMemory
- ConverSationTokenBufferMemory
処理フロー
1〜6の流れでユーザに回答を返却します。
今回は 3〜6 に焦点を当て説明します。

システム構成
| 項目 | 採用技術 | 役割・用途 |
|---|---|---|
| ベクトルDB | ChromaDB | 知識(ドキュメント)の保管・検索 |
| 会話履歴の保存 | Redis | セッション管理・短期記憶の保持 |
| LLM | gpt-4o | 推論・回答生成・クエリ書き換え |
【3,6】Redisを使った会話履歴の保存と読み込み
会話履歴はセッションID単位でRedisに保存します。
Redisとは
NoSQLであり、メモリ上でデータ管理を行うインメモリデータベース
RedisとRDBの比較
| 項目 | Redis | RDB |
|---|---|---|
| データ保管 | メモリ (RAM) | ディスク (HDD/SSD) |
| 処理速度 | 超高速(マイクロミリ秒単位) | 高速だがRedisには劣る(ミリ秒単位) |
| データ後続 | キー・バリュー型 | テーブル形式 |
-
Redis
- メモリ上に保存され処理が高速に行われることからキャッシュ、チャット履歴の保存取得、即時反映が必要なデータ(日次ランキング等)でよく利用される
-
RDB
- 複雑なデータを安全に保存できることから顧客情報や決済情報など機密性の高いデータから長期保存が必要なデータ管理でよく使われる
1 / 2 実装例(会話履歴の保存)
# 新規セッションを生成
session_id = str(uuid.uuid4())
# 1.会話履歴をRedisに保存するための定義
message_history = RedisChatMessageHistory(
session_id=session_id,
url="redis://localhost:6379/0" # デフォルトのRedis接続
)
# 2.AgentExecutorで使用するためにmemoryオブジェクトを作成
memory = ConversationBufferMemory(
memory_key="chat_history",
chat_memory=message_history,
return_messages=True
)
# 3.エージェント生成
llm = ChatOpenAI(model="gpt-4o", temperature=0)
prompt = PromptTemplate.from_template("テンプレート.txt")
tools = [toolA,toolB]
agent = create_react_agent(llm, tools, prompt)
# 4.エージェントExecutor生成
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
memory=memory, # メモリを追加して会話履歴を記録
verbose=True,
max_iterations=10,
handle_parsing_errors=True,
max_execution_time=120,
)
解説
- Redisに保存・取得を行うため
RedisChatMessageHistoryを定義する
引数にはsessin_idとDB保存先urlを指定します -
ConversationBufferMemoryを使って保存する-
memory_key:保存した会話を登録する際に利用する変数名 -
chat_memory:保存先を外部ストレージ(Redis,MongoDB)にする場合に設定
※ デフォルトの保存先はメモリ上 -
return_message:- False:メッセージを文字列で返却(デフォルト)
- True:メッセージをオブジェクトリストで返却(GPTモデル利用時はこちらを設定)
-
- LLM、プロンプト、Toolを定義しエージェントを生成する
プロンプトやToolに関してはこちらの記事を参考にしてください。 - エージェントの実行環境(AgentExecutor)を生成する
ここで2で定義したmemoryを設定することでエージェントがRedisと連携して自動で会話履歴を保存する
2 / 2 実装例(会話履歴の取得)
# 1.会話履歴をRedisに保存するための定義
message_history = RedisChatMessageHistory(
session_id=session_id,
url="redis://localhost:6379/0" # デフォルトのRedis接続
)
# 2.AgentExecutorで使用するためにmemoryオブジェクトを作成
memory = ConversationBufferMemory(
memory_key="chat_history",
chat_memory=message_history,
return_messages=True
)
# 3.会話履歴の取得
chat_history = memory.load_memory_variables({})["chat_history"]
解説
- 保存と同じ処理
- 保存と同じ処理
-
load_memory_variablesでmemory_keyを指定して読み込む
【4,5】 プロンプト再構成と回答生成
プロンプト再構成(Query-rewriting)について
RAGにおいてチャット形式で会話するためにはプロンプト再構成が必要です。
なぜなら、過去の会話を踏まえたプロンプトの場合だと、RAGでは意味を解釈できないためです。
ex.「それの充電端子は何?」と質問した場合
- LLMがベクトルDBに対して「それの充電端子は何?」というプロンプトで検索をかける
- それ がわからないため検索結果がヒットしない
- 適切な回答を得られない
そのため、過去の会話内容を踏まえた スタンドアロンなプロンプト に再構成する必要があります。
スタンドアロンなプロンプト
過去の会話記録等の前提条件を知らなくても意味が通じるプロンプト
ユーザー: 「最近、iPhone 15を買ったんだ。」
AI: 「いいですね!最新モデルですね。」
ユーザー: 「それの充電端子は何?」
スタンドアロンな質問への変換:
❌ そのまま: 「それの充電端子は何?」
✅ 変換後: 「iPhone 15の充電端子は何ですか?」
実装例
def create_query_rewriter():
# 2.基本設定
llm = ChatOpenAI(model="gpt-4o", temperature=0)
system_prompt = "テンプレートは下記参照"
# 3.プロンプト定義
query_rewrite_prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
# MessagePlaceholder = 会話履歴をプロンプトに挿入する。"chat_history"に紐づくデータを取得してプロンプトに挿入する
MessagesPlaceholder(variable_name="chat_history"),
("human", "{question}"),
("system", "上記の質問を、会話履歴を考慮して検索に適した形式に書き換えてください。書き換えた質問のみを返してください。")
])
# 4.プロンプトをLLMに入力して生成させ、その結果を文字列にする
query_rewriter = query_rewrite_prompt | llm | StrOutputParser()
return query_rewriter
# 1.Query-rewrite定義
query_rewriter = create_query_rewriter
# 5.Query-rewrite実行
rewritten_query = query_rewriter.invoke({
"question": user_input, # ユーザからのプロンプト
"chat_history": chat_history # Redisから取得した過去の会話履歴
})
解説
- プロンプト再構成するための定義をする
- LLMとテンプレート(下記参照)を設定する
- ユーザのプロンプトを再構成するためのプロンプトを定義する
この時、LLMに複数のプロンプトを優先順位をつけて渡す-
system_prompt: システムプロンプト -
chat_history: Redisに登録している会話履歴 -
question: ユーザの入力プロンプト - 書き換えた質問のみを返すという制約
※ 1〜3で不要な文字列が含まれた場合に取り除くための補助的な指示
-
- 3で生成したプロンプトをLLMに渡す
この際に3で設定した優先順位に従ってLLMがプロンプトを再構成します
処理の流れ
①システムプロンプトの読み込み
↓
②Redisから会話履歴を取得
↓
③ユーザの入力プロンプトを取得
↓
④LLMが①のルールに従い②の内容を踏まえて③を再構成する - ユーザプロンプト、Redisの履歴を引数としてQuery-rewiteを実行する
この手順で会話履歴の内容も踏まえたスタンドアロンなプロンプトが生成できます。
テンプレート
あなたは質問を再構成する専門家です。
過去の会話履歴と最新の質問を見て、検索エンジンで使用するための
完全にスタンドアロンな質問に書き換えてください。
ルール:
1. 会話履歴の情報を使って、曖昧な代名詞(それ、その、これ)を具体的な名詞に置き換える
2. 前の質問への参照(「さっきの」「前の」)を具体的な内容に置き換える
3. 質問の意図を保ちながら、検索に最適化された明確な質問文にする
4. 会話履歴がない場合は、元の質問をそのまま返す
例:
会話履歴: User: "Singletonパターンについて教えて" | AI: "Singletonは..."
最新の質問: "それのメリットは?"
書き換え後: "Singletonパターンのメリットは何ですか?"
会話履歴: User: "MVVMとは?" | AI: "MVVMは..."
最新の質問: "具体的な実装例を教えて"
書き換え後: "MVVMパターンの具体的な実装例を教えてください"
まとめ
RAGにおけるチャット形式の実装は、単に履歴を保存するだけでなく、履歴を使ってプロンプトを再構成する(Query-Rewriting) プロセスが重要です。
- Redisで高速に履歴を読み書きする
- スタンドアロンなプロンプトに変換して検索精度を高める
この2つを意識すると、今まで以上にRAGとストレスなく対話することができるようになります。
その他参考記事