はじめに
開発したRAGシステムを利用していただいた際に、回答を受け取った内容について深掘りした質問をしたいとご要望をいただきました。
ChatGPTなどのLLMではよくある機能ですが、RAGで実装する場合は単純にはいかないため、少し実装のやり方を考えることになりました。
今回はRAGで深掘り質問を可能とする際の実装の考え方やChatGPTとGeminiでの実装の手法を伝えます。
こんな人におすすめ
①RAGで開発を行っている方
②処理時間よりも性能を求めている方
RAGで深掘質問するのがちょっと厄介な理由
ChatGPTなどLLMにおいて、履歴を参照して深堀した質問が出来る機能は、一般的に利用されているものです。API連携でもプロンプトを連携して回答を得る際に過去履歴のやり取りを付与することで、同様の流れを構築することが可能です。
つまり、事前にやり取りした情報を加えたプロンプトを実行して、回答を得ることが出来ます。
さて、ここでRAGの場合の履歴情報を付与する場合の問題点、それはRAGが毎回回答生成する為の情報がプロンプトに依存して変わってしまうという点です。
LLMの知識から回答を得る場合は、LLMの膨大な情報の中から回答を生成する為、気にすることはいらないのですがRAGは提示された情報に回答を作る為、深掘り質問をしたいのであれば同じ情報から回答を生成する必要があります。
それを踏まえたうえで考えると、2回目以降の深堀質問をする場合、1回目と利用した情報を残しておくか、1回目と同じプロンプトで情報を抽出することで疑似的にRAGにおいても深掘り質問を行うことが可能です。
そんな厄介な理由を加味したうえで続いて実装について、話をしていきます。
加味するやり取りに制限を設ける
さて、実装にあたって1つだけ気にするべきことがあります。
それは、すべてのやり取りをLLMへ渡すべきではない、ということです。
これはLLMを利用している方なら感じたことがあると思いますが、履歴は増えれば増えるほど処理にかかる時間が増加していきます。
純粋にやり取りを全て持たせれば、どんな会話をしていたかも理解したうえで、現在のプロンプトの回答を生成する必要があり、その事前情報の理解に時間を要してしまうからです。
同様のことが、API連携による深堀質問の際にも言えます。
その為、可能な限りやり取りを制限して連携したいということです。
今回わたしが考えたのは、5回のやり取りを提供するというものでした。
ただしここは微調整が必要です。
イメージ的に、連携しているテキスト数と同じくらいのトークン数を基準として、大体倍くらいまでとしたほうがいいです。
わたしの場合は、大体20000トークンから30000トークン連携しており、インプットトークンとアウトプットトークンを合わせて4000から5000トークン程度でしたので5回のやり取りでちょうど倍程度となります。
そのくらいになるように設定するのがちょうど現実的に連携が可能なトークン数だと考えます。
この辺りの匙加減は、皆様が実際にやってみてご判断ください。
ChatGptにおける実装について
では、実際に実装を行ったプログラムを提示したいと思います。
# 1.最初のチャット情報を取得
first_history_sql = """
SELECT A.question
FROM CONVERSATION_HISTORY A
WHERE A.user_id = :1
AND A.CHAT_RENBAN = :2
AND A.CHAT_SUB_RENBAN = 1
"""
cursor.execute(first_history_sql, [user_id,chat_renban])
first_history_row = cursor.fetchone()
if first_history_row[0] is not None :
first_history_question = first_history_row[0]
else:
first_history_question = ""
# 2.複数履歴を取得
select_history_sql = """
SELECT A.question, A.answer
FROM CONVERSATION_HISTORY A
WHERE A.user_id = :1
AND A.CHAT_RENBAN = :2
ORDER BY A.CHAT_SUB_RENBAN DESC
FETCH FIRST 5 ROWS ONLY -- 必要な履歴数に応じて変更
"""
cursor.execute(select_history_sql, [user_id,chat_renban])
history_rows = cursor.fetchall()
# 3.入力プロンプトを保管し、最初のチャットのプロンプトと入れ替える
get_text = original_text -- 入力プロンプト
original_text = first_history_question --最初のチャットのプロンプト
# 4.履歴情報をhistory_listとして配列を持たせ、最後に当初のチャット情報を付属させる
history_list = []
for row in history_rows:
question, answer = row
history_list.append({"question": question, "answer": answer})
print("履歴の管理リスト後")
# 履歴がなければデフォルト値を設定
if not history_list:
history_list = [{"question": "", "answer": ""}]
# リクエストメッセージを構築
messages = [{"role": "system", "content": preamble}]
print(f"リクエストメッセージ:{messages}")
# 過去履歴を動的に追加
for idx, history in enumerate(history_list, start=1):
messages.append({
"role": "user",
"content": f"past_question{idx}: {history['question']} \npast_answer{idx}: {history['answer']}"
})
# 現在の質問を追加
messages.append({
"role": "user",
"content": f"質問: {get_text} \n根拠情報: {cleaned_text_str}"
})
# 5.ChatGPT API を呼び出し
response = openai.ChatCompletion.create(
model="gpt-4.1-2025-04-14",
messages=messages
)
content = response.choices[0].message.content
いかがでしょうか。
重要なのは、入力プロンプトと最初のチャットプロンプトを入れ替えて以降の処理を実行する点です。
これにより、今後のセマンティック検索などは最初のチャットプロンプトを用いて実行されます。RAGの深堀機能はこのようにして実装が可能です。
まとめ
以上が深掘り質問を行う為のやり方や考え方です。
深堀質問が出来るとより、システムとしてユーザー目線に立った使いやすいものとなります。
ただし、上述したように、大量にトークンを渡してしまうと処理速度が低下してしまうため、そのあたりの兼ね合いを踏まえたうえでぜひ構築してみてください