Pythonの基礎知識(実践編)

RAG環境の検索精度を高める!プロンプト設計と改善テクニック

あなたが作ったRAG環境、思ったよりも検索結果が的外れで「これじゃ使えない」と感じたことはありませんか?

pgvectorにデータを入れても、肝心の会話で意図通りに参照されず、まるで無関係な情報を答えてしまう。あるいは、せっかく検索された情報がプロンプトにうまく組み込まれず、AIが曖昧にごまかす返答を返す…。

これは多くの人が最初に直面する壁です。では、なぜ精度が出ないのか?原因は「Embeddingの扱い」「検索の閾値」「プロンプト設計」の3つにあります。

ベクトル検索は強力ですが、正しくチューニングしなければただのノイズになりますし、プロンプトが曖昧だとAIの応答も揺らぎます。

本記事では、検索精度を引き上げる工夫と、安定した回答を引き出すためのプロンプト設計を具体的に解説していきます。

RAG環境で直面する課題

RAG環境を構築すると、基本的な動作はすぐに実現できます。しかし実際の運用段階になると「検索精度が思ったように出ない」「プロンプトの回答が安定しない」といった課題に直面することが多いです。

これらは技術的な要因が複雑に絡み合っており、コード設計やパラメータ調整の工夫が求められます。

ここでは代表的な問題点を整理します。

検索精度が出ない典型的な問題

RAG環境では、ベクトル検索の精度が低いと回答全体の品質が大きく損なわれます。

例えば、チャンクを大きくしすぎると文脈がぼやけ、逆に小さくしすぎると関連情報が失われます。また、Embeddingモデルの次元数や学習データの性質によっても結果が変わり、特定の質問に対して全く関係のない文章がヒットしてしまうケースもあります。

さらに、類似度スコアのしきい値を適切に設定しないと、ノイズだらけの結果を返してしまうこともあります。これらは「RAGは動くが使い物にならない」と感じさせる典型的な要因です。

プロンプトが不安定になる原因

検索精度の問題に加えて、プロンプト設計が不十分だと応答が安定しません。

会話履歴を適切に組み込めていない場合、AIが直前の文脈を無視してしまい、矛盾した返答をすることがあります。また、役割を明示していないとAIが余計な解説を加えたり、ユーザーの意図を誤解する可能性が高まります。

さらに、長文応答を制御する仕組みがなければ、出力が途中で途切れたり、Discordの文字数制限を超えてエラーになることもあります。これらの要因は、コードレベルでの設計不足やプロンプトの構成不備によって引き起こされるものであり、検索精度と並んで改善の優先度が高い課題です。

bot.py の全体ソース

bot.py は、ユーザーからの入力を受け取り、Embedding生成・ベクトル検索・プロンプト構築・応答出力までを担う中核スクリプトです。RAG環境の検索精度やプロンプト設計は、このファイルの処理に大きく依存しています。ここではまずソースを掲載し、その後に重要要素を抜粋して解説します。

実行環境のディレクトリ構成

RAG環境は /opt/gpt-oss/ 配下に配置されています。

以下のようなディレクトリツリー構成で管理することで、役割ごとに整理された形で実行できます。

/opt/gpt-oss/
├─ bin/
│  └─ llama-server
├─ bot/
│  ├─ bot.py
│  ├─ .env
│  └─ modules/
│     └─ db_utils.py
└─ models/
    ├─ gpt-oss-20b-MXFP4.gguf <- 未使用(LMSTUDIOのGUI版で起動)
    └─ mxbai-embed-large-v1/
        └─ gguf/
            └─ mxbai-embed-large-v1-f16.gguf

また、RAG環境を安定して動作させるには、モデルの実行、Botの制御、データベース管理がそれぞれ明確に分かれた構成が必要です。

全体像としては以下のように整理できます。

ディレクトリ / ファイル(/opt/gpt-oss)役割
bin/llama-serverLLMサーバー本体。モデルを読み込みAPIとして提供します。
bot/bot.pyDiscord Bot スクリプト。ユーザー入力を処理し、Embedding生成やDB検索を実行します。
bot/.env環境変数設定ファイル。DiscordトークンやDB接続情報を管理します。
bot/modules/db_utils.pyデータベース処理モジュール。pgvectorを利用した会話検索や履歴保存を行います。
models/gpt-oss-20b-MXFP4.gguf大規模言語モデル(GPT-OSS-20B)のファイル。推論用に使用します。
models/mxbai-embed-large-v1/
gguf/mxbai-embed-large-v1-f16.gguf
Embeddingモデルファイル。テキストをベクトル化して検索可能にします。

ソース全文掲載

重要要素の解説

bot.py 内には検索精度やプロンプト安定性に直結する関数が複数存在します。以下に代表的な処理を抜粋しながら解説します。

chunk_text() によるチャンク分割

テキストをそのままEmbedding化すると長すぎて検索精度が低下するため、chunk_text() で適度な長さに分割します。分割幅とオーバーラップを調整することで、関連文脈を保持したまま効率的に検索できるようになります。

def chunk_text(text: str, chunk_size: int = 300, overlap: int = 50) -> list[str]:
    chunks = []
    start = 0

    while start < len(text):
        end = min(start + chunk_size, len(text))
        chunks.append(text[start:end])
        start += chunk_size - overlap
    return chunks

このようにオーバーラップを持たせることで文脈の途切れを防ぎ、精度を安定させます。

prompt 生成部の構成要素

ユーザー入力と検索結果を組み合わせ、AIに渡すプロンプトを構築します。プロンプトの設計によって応答の一貫性が左右されるため、非常に重要な部分です。

prompt = f"""
あなたは会話履歴を必ず参照して一貫性のある応答を返すアシスタントです。
以下のルールを厳格に守ってください。

1. 会話履歴に関連情報がある場合は、それを最優先してください。
2. 会話履歴に関連情報が無い場合は、あなたの知識を使って推測してください。
3. 会話履歴と知識が矛盾した場合は、会話履歴を優先してください。

[会話履歴]
{context}

[新しい質問]
User: {user_input}

[応答]
Assistant:
"""

このように「役割付与」「履歴の挿入」「新しい質問」「回答開始トリガー」という4要素で構成され、安定した回答を得る仕組みになっています。

split_message() による長文分割送信

Discordには1メッセージ2000文字という制限があるため、応答を分割して送信する関数が実装されています。これにより長文でも欠落なくユーザーに届けられます。

def split_message(text: str, limit: int = 1900) -> list[str]:
    return [text[i:i+limit] for i in range(0, len(text), limit)]

長文の途切れを防ぎ、運用上の安定性を確保します。

call_gptoss() によるモデル呼び出し

LMStudioで稼働するGPT-OSS-20Bを呼び出し、生成応答を取得する関数です。temperatureやmax_tokensを指定することで応答の多様性や長さを制御できます。

def call_gptoss(prompt: str) -> str:
    url = "http://127.0.0.1:1234/v1/completions"
    headers = {"Content-Type": "application/json"}

    payload = {
        "model": "openai/gpt-oss-20b",
        "prompt": prompt,
        "temperature": 0.3,
        "max_tokens": 1024,
        "stream": False
    }
    …(省略)

ここでのパラメータ設定が不適切だと、冗長な回答や途切れた応答が返ってくるため、実運用における調整が求められます。

db_utils.py の全体ソース

db_utils.py は、会話履歴やEmbeddingをデータベースに保存し、pgvectorを利用して類似度検索を行うための重要なモジュールです。検索精度を高めるための閾値設定や、会話ログを適切に保存する仕組みが実装されており、RAG環境全体の安定性を支える役割を果たしています。ここではソースを提示した上で、検索精度に関わる重要な関数を抜粋して解説します。

ソース全文掲載

重要要素の解説

db_utils.py には、ベクトル検索や会話履歴の保存を行う複数の関数が含まれています。以下では検索精度やプロンプト設計に直結する主要な処理を解説します。

search_conversations() と類似度検索

ユーザー入力をEmbedding化し、保存済み会話との類似度を計算して上位の文脈を取得する関数です。ここで適切に文脈を返せるかどうかが、AI応答の品質を左右します。

def search_conversations(query: str, session_id: int, limit: int = 100, min_score: float = 0.75):
    query_emb = get_embedding(query)

    cur.execute("""
    SELECT user_message, bot_messages, embedding <-> %s AS score
    FROM conversations
    WHERE session_id = %s
    ORDER BY embedding <-> %s
    LIMIT %s
    """, (query_emb, session_id, query_emb, limit))
    rows = cur.fetchall()

この処理により類似度スコアを基準とした検索が可能となり、RAGの文脈参照が実現します。

min_score による閾値設定

検索結果の中から、関連性の低いものを除外するために閾値を設けています。これによりノイズを含まない精度の高い情報だけを残すことができます。

results = [] for row in rows:
    if row[2] < min_score:
        continue
    results.append(row)

min_score の値を調整することで、情報の網羅性と精度のバランスを最適化できます。

insert_conversation() と会話の保存

ユーザーの発話とAIの応答をデータベースに保存する関数です。これにより次回以降の検索時に文脈が利用でき、連続性のある対話を実現します。

def insert_conversation(session_id: int, user_id: str, role: str, message: str, emb: list[float]):
    cur.execute("""
    INSERT INTO conversations (session_id, user_id, role, message, embedding)
    VALUES (%s, %s, %s, %s, %s)
    """, (session_id, user_id, role, message, emb))
    conn.commit()

履歴が正しく保存されることで、過去の発話を踏まえた回答が可能になります。

insert_embedding() によるベクトル登録

EmbeddingをベクトルDBに保存する処理です。これにより、後続の検索処理で高精度の類似度検索が可能になります。

def insert_embedding(text: str, emb: list[float]):
    cur.execute("""
    INSERT INTO embeddings (text, embedding) VALUES (%s, %s)
    """, (text, emb))
    conn.commit()

ベクトルが正しく登録されなければ検索の土台が崩れるため、RAG環境において不可欠な処理です。

改善の実践例


RAG環境を安定して運用するためには、ソースコードをそのまま利用するだけでは不十分です。検索精度やプロンプト設計をさらに改善するための工夫を取り入れることで、応答の信頼性を大きく向上させることができます。ここでは具体的な改善例を紹介します。

チャンクサイズとオーバーラップの調整


チャンクのサイズ設定が適切でないと、検索結果が不正確になります。大きすぎるチャンクは文脈がぼやけ、小さすぎるチャンクは情報が分断されます。オーバーラップを持たせることで文脈を維持しながら検索できるようになります。

def chunk_text(text: str, chunk_size: int = 300, overlap: int = 50) -> list[str]:
    chunks = []
    start = 0

    while start < len(text):
        end = min(start + chunk_size, len(text))
        chunks.append(text[start:end])
        start += chunk_size - overlap
    return chunks

チャンクサイズやオーバーラップを調整することで、文脈の途切れを防ぎながら検索精度を向上させることができます。

類似度スコアのしきい値を調整する工夫


検索で取得するデータの精度は、類似度スコアのしきい値によって大きく変わります。スコアを低く設定するとノイズが増え、高く設定すると関連情報が不足します。運用環境に合わせて適切に設定することが重要です。

results = [] for row in rows:
    if row[2] < 0.75:
        continue
    results.append(row)

このように閾値を設けることで、関連性の低い結果を除外し、安定した検索結果が得られます。

類似度検索のチューニング

RAGの運用において、過去に登録した情報が検索でうまく拾えないケースは珍しくありません。たとえば「Mac mini (M4)」と登録してあっても、「スペックを教えて」と質問すると関連度が低いと判定され、結果として検索から漏れてしまうことがあります。これはpgvectorが単語や表現の近さをベースに類似度を算出する仕組みだからです。

この問題を改善する方法はいくつかあります。まず有効なのは、検索時に利用するLIMITとmin_scoreの調整です。たとえば上位10件だけを返すのではなく、上位100件を対象にした上でスコア閾値を下げれば、多少関連度が低くても情報を拾える確率が高まります。

# ====== 類似履歴検索 ======
results = db_utils.search_conversations(user_input, session_id=session_id, limit=100)
context_chunks = []
token_count = 0
max_tokens = 1000
min_score = 0.75 # 類似度のしきい値

さらに、保存の仕方を工夫することも効果的です。単純なテキスト保存ではなく「質問と回答のペア」として登録すれば、「スペックを教えて」という入力と「マシンスペック」の回答が自然に結びつきます。また、表やリストを扱う場合はJSON形式などの構造化データで保存しておくと、検索の精度が大幅に改善されます。

補足として、LIMITを大きく設定すれば取りこぼしは減りますが、そのまま大量の候補をLLMに渡してしまうと処理が遅くなり、応答の品質も下がる可能性があります。そこで実運用では、まずLIMITを広めに設定して候補を取得し、その中から類似度スコアの閾値を基準に再度絞り込む方法が有効です。最終的にLLMに渡す件数を3〜10件程度に抑えることで、拾える情報量と処理速度のバランスを両立できます。

for user_msg, assistant_msgs, score in results: if score < min_score: continue # 類似度が低いものはスキップ print(f"DEBUG: score={score:.3f}, user={user_msg[:30]}") pair_text = f"### User: {user_msg}\n" for a in assistant_msgs: pair_text += f"### Assistant: {a}\n" tokens = count_tokens(pair_text) if token_count + tokens > max_tokens: break # 上限を超えたら終了 context_chunks.append(pair_text) token_count += tokens

検索結果取得の流れ

  1. LIMIT=100 → 広く拾う
  2. if score < min_score: continue → 類似度が低いものを除外
  3. 必要なら token_count で上限を制御

こうしたチューニングを組み合わせることで、「似ていない言い回しでも情報を拾える」「必要な情報を確実に返す」仕組みに近づけることが可能です。RAGを安定運用するには、類似度検索そのものの設計を意識的に改善していくことが不可欠です。

プロンプト設計の改良ポイント


プロンプトの構成を工夫することで応答の一貫性が高まります。役割付与や履歴の挿入、応答の開始トリガーを設けることで、無駄な回答や矛盾を減らせます。

prompt = f"""
あなたは会話履歴を必ず参照して一貫性のある応答を返すアシスタントです。
以下のルールを守ってください。
1. 履歴があればそれを最優先してください。
2. 履歴が無ければ知識を使って推測してください。
3. 履歴と知識が矛盾した場合は履歴を優先してください。 \

[会話履歴] {context} \
[新しい質問]
### User: {user\_input} \
[応答]
### Assistant:
"""

このようにルールを明確に与えることで、AIの回答が安定し、利用者にとってわかりやすい応答になります。

FAQテンプレートによる不足補完


検索結果が不足する場合に備え、FAQや定型文をプロンプトに追加することで安定性を高めることができます。あらかじめよくある質問を組み込んでおくことで、回答不能を減らすことができます。

faq = """
Q: インデックスを作成するには?
A: CREATE INDEX ... を利用してください。

Q: セッションが切断されるのはなぜ?
A: タイムアウト設定が原因の可能性があります。
"""

FAQを組み込むことで、検索が外れた場合でもユーザーに有益な回答を返すことが可能となり、システムの信頼性が向上します。

まとめ

ここまで、RAG環境における検索精度向上とプロンプト設計の工夫について、具体的なコード例と改善方法を交えて解説しました。検索の安定性とプロンプトの一貫性は、RAGを実用レベルに引き上げるための両輪です。

最後に本記事の要点を整理し、今後の展開について触れます。

検索精度とプロンプト設計の両輪でRAGは活きる

検索精度が低ければ、AIは不正確な文脈を取り込み誤った応答を返します。一方で、プロンプト設計が甘ければ、正しい検索結果を与えても安定した回答にはつながりません。

チャンク分割の工夫やスコア閾値設定といった検索側の改善と、役割付与や履歴の挿入、応答開始トリガーといったプロンプト側の工夫は、どちらも欠かすことができません。両者を組み合わせて初めて、RAG環境は実用的で信頼性のあるシステムとして機能します。

今後の改善ポイントと応用可能性

今後の改善ポイントとしては、より高性能なEmbeddingモデルの導入や、検索スコアを動的に調整する仕組みの実装が考えられます。また、ユーザーの利用傾向に基づいたFAQテンプレートの拡充や、プロンプト設計のさらなる最適化も効果的です。

応用の可能性としては、ドキュメント管理やFAQシステム、カスタマーサポート向けの自動応答など、業務効率化や知識共有の分野に幅広く展開できます。継続的に改善を重ねることで、個人環境においても企業並みの高品質なRAGシステムを構築することが可能になります。

よく読まれている記事

1

「私たちが日々利用しているスマートフォンやインターネット、そしてスーパーコンピュータやクラウドサービス――これらの多くがLinuxの力で動いていることをご存じですか? 無料で使えるだけでなく、高い柔軟 ...

2

Linux環境でよく目にする「Vim」という名前。サーバーにログインしたら突然Vimが開いてしまい、「どうやって入力するの?」「保存や終了ができない!」と困った経験をした人も多いのではないでしょうか。 ...

3

ネットワーク技術は現代のITインフラにおいて不可欠な要素となっています。しかし、ネットワークを深く理解するためには、その基本となる「プロトコル」と「レイヤ」の概念をしっかり把握することが重要です。 こ ...

4

この記事は、Linuxについて勉強している初心者の方向けに「Shellスクリプト」について解説します。最後まで読んで頂けましたら、Shellスクリプトはどのような役割を担っているのか?を理解出来るよう ...

5

Javaは世界中で広く使われているプログラミング言語であり、特に業務システムやWebアプリケーションの開発において欠かせない存在です。本記事では、初心者向けにJavaの基礎知識を網羅し、環境構築から基 ...

-Pythonの基礎知識(実践編)