# 卒業研究でRAGパイプラインを作った

> ローカルLLMにRAGとCoT Promptingを組み合わせて精度を上げた

2025-02-13 · https://rxxuzi.com/blog/graduation-thesis-rag

---

もうすぐ卒業研究の発表があるので、自分の整理も兼ねてブログに書いておく。予稿のタイトルは「RAGとPromptingによるLLMの精度向上」で、要はローカルで動くLLMにRetrieval-Augmented Generation（RAG）とChain-of-Thought（CoT）Promptingを組み合わせて、回答の精度を上げようっていう研究。

卒論の予稿ってフォーマットが決まってるから堅い文章で書いたんだけど、中身自体はそんなに難しくない。やってることはシンプルで、「LLMに質問を投げる前に、関連する文書を探してきてプロンプトに突っ込む」っていうパイプラインを作った。

![青白く光るPC](https://cdn.rxxuzi.com/blog/img/img-1174-1781797336444.jpeg)
*午前3時の研究室のPC*

## なぜローカルLLMなのか

まず前提として、なぜクラウドのAPIじゃなくてローカルなのかっていう話がある。

企業内部で使うことを想定すると、社内文書とか顧客情報をOpenAIのAPIに流すのはセキュリティ的にまずい場合がある。クラウドに送れないデータがある。だからローカルで動くLLMが必要になる。

ただ、ローカルLLMは当然ながらGPT-4とかClaude比べるとモデルサイズが小さい。知識量も限られる。そこをRAGで補おうっていうのが基本的なアイデア。

## 従来のフロー vs 提案フロー

従来のやり方はこう。

```mermaid
graph LR
    A[User Question] --> B[LLM] --> C[Answer]
```

ユーザーの質問をそのままLLMに投げて、LLMが学習済みの知識だけで答える。これだと文脈情報も最新情報も参照できないし、ハルシネーション（もっともらしい嘘）のリスクも高い。

僕が作ったシステムのフローはこう。

```mermaid
graph TD
    A[User Question] --> B[Dense Retrieval]
    B -- 候補文書 --> C[Cross Encoder Rerank]
    C -- 上位文書 --> D[Prompt生成]
    A --> D
    D --> E[LLM + CoT Prompting]
    E --> F[Answer]
    G[(Vector DB)] --> B
    H[User Upload] -.-> D
```

3段階のパイプライン。これをそれぞれ説明する。

## Stage 1: Dense Retrieval

最初のステージは、ユーザーの質問に関連する文書をベクトルデータベースから引っ張ってくるところ。

事前に文書群をチャンクに分割して、各チャンクをembeddingモデルでベクトル化してVector DBに入れておく。質問が来たら、質問文もベクトル化して、コサイン類似度で近い文書を検索する。

```python
from sentence_transformers import SentenceTransformer
import numpy as np

model = SentenceTransformer('all-MiniLM-L6-v2')

# 文書のembedding（事前に計算してDBに保存）
doc_embeddings = model.encode(documents)

# 質問のembedding
query_embedding = model.encode(query)

# コサイン類似度で検索
similarities = np.dot(doc_embeddings, query_embedding) / (
    np.linalg.norm(doc_embeddings, axis=1) * np.linalg.norm(query_embedding)
)
top_k_indices = np.argsort(similarities)[-k:][::-1]
```

これがDense Retrievalの基本。BM25みたいなスパースな手法と違って、意味的な類似性で検索できるのが強み。「犬」と「ワンコ」が近いベクトルになる。

ただ、Dense Retrievalだけだと精度に限界がある。embeddingモデルはbi-encoderだから、質問と文書を独立にベクトル化する。質問と文書の細かい相互作用を捉えきれない。

## Stage 2: Cross Encoder Rerank

だからStage 2でCross Encoderを使って再ランキングする。

Cross Encoderはbi-encoderと違って、質問と文書を連結した状態でTransformerに通す。質問と文書のトークン同士がattentionで直接やり取りするから、より精密な関連性スコアが出る。

```python
from sentence_transformers import CrossEncoder

reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')

# Stage 1で取得した候補文書を再ランキング
pairs = [[query, doc] for doc in candidate_docs]
scores = reranker.predict(pairs)

# スコア順にソート
reranked_indices = np.argsort(scores)[::-1]
top_docs = [candidate_docs[i] for i in reranked_indices[:top_n]]
```

なぜ最初からCross Encoderを使わないのかっていうと、遅いから。Cross Encoderは質問×文書のペアごとにforwardパスを走らせるから、文書が1万件あったら1万回推論することになる。だから最初にDense Retrievalで候補を絞って（たとえば上位50件）、その50件だけをCross Encoderで精密にランキングする。粗い検索→精密なランキングの2段構え。情報検索ではよくあるパターン。

## Stage 3: CoT Prompting

最後のステージ。再ランキングで選ばれた上位文書と、ユーザーの質問を統合してプロンプトを作り、LLMに渡す。

ここでChain-of-Thought（CoT）Promptingを使う。CoTは「段階的に考えてから答えてね」ってLLMに指示するテクニック。Zero-shot CoTなら "Let's think step by step" を付けるだけ。

```python
def build_prompt(query, retrieved_docs, user_file=None):
    context = "\n\n".join([
        f"[Document {i+1}]\n{doc}"
        for i, doc in enumerate(retrieved_docs)
    ])

    prompt = f"""以下の参考情報を踏まえて、質問に回答してください。
回答する際は、段階的に考えてから結論を述べてください。

## 参考情報
{context}

## 質問
{query}

## 回答
まず、段階的に考えます。
"""

    if user_file:
        prompt += f"\n## 追加資料\n{user_file}\n"

    return prompt
```

これだけ？って思うかもしれないけど、これだけ。CoTの本質は「考える過程を出力させる」ことで、中間ステップを生成させることでLLMの推論精度が上がる。Wei et al. (2022) の論文で有名になったやつ。

ポイントは、RAGで取得した文書をcontextとしてプロンプトに入れて、さらにCoTで段階的に推論させること。文書という「外部知識」と、CoTという「推論の補助」を同時にやる。

## システムアーキテクチャ

全体のアーキテクチャとしては、AIモデルへのアクセスをFastAPIで実装して、Webインターフェースから使えるようにした。

```mermaid
graph LR
    A[Web UI] --> B[FastAPI Server]
    B --> C[RAG Pipeline]
    C --> D[Local LLM]
    C --> E[(Vector DB)]
    A -- file upload --> C
```

ユーザーは追加でファイルをアップロードすることもできて、その内容も質問と一緒にプロンプトに入る。

## 結果

評価は6人のユーザーに使ってもらって、5段階評価のアンケートを取った。評価項目は正確性、自然さ、思考力、網羅性、明瞭性の5つ。

結果として、RAG + CoT Promptingを実装したv3モデルは、何も実装してないv1モデルより全項目で高評価だった。特に思考力と網羅性で大きな差が出た。

思考力が上がったのはCoTの効果。段階的に推論するから、論理の飛躍が減る。網羅性が上がったのはRAGの効果。外部文書から関連情報を引っ張ってくるから、LLM単体の知識だけに依存しない。

## 正直な感想

卒論として書くとこういう感じになるんだけど、正直に言うと、このパイプライン自体は2024年の時点でそんなに新しくない。LangChainとかLlamaIndexを使えば似たようなことはできる。

僕がやったことの価値があるとしたら、ローカル環境で動くように全部組んだことと、Cross Encoder Rerankを挟んだことで検索精度を上げたこと。あとFastAPIでAPIとして切り出したから、フロントエンドを差し替えやすい設計にしたこと。

RAGの限界も感じた。検索で引っかからない情報は当然ながら使えないし、Vector DBに入れる文書の質がそのまま回答の質に直結する。ゴミを入れたらゴミが出る。あとembeddingモデルの性能にも依存する。日本語のembeddingは英語に比べるとまだ弱い。

それでも、追加学習なしでLLMの出力精度を上げられるっていうのはRAGの強み。ファインチューニングはコストが高いし、データも大量に要る。RAGならVector DBに文書を突っ込むだけでいい。手軽さが全然違う。

![](https://cdn.rxxuzi.com/blog/img/smoke-2025-feb.jpeg)
*発狂しそうになった。だから外に出てタバコを吸ってる。もう３泊してる。タスケテ～*

ハルシネーションの低減にも効く。根拠となる文書をプロンプトに含めるから、LLMが完全に創作する余地が減る。ゼロにはならないけど。

まあ、卒論のテーマとしてはちょうどいい粒度だったと思う。
