🍣

LagChainによるRAG構築~Raggleの取り組み備忘録~

2025/02/15に公開

はじめに

こんにちは、shunです。

最近生成AIやLLMへのインプットを増やしており、その一環としてraggleというサイトの製薬企業向け高度なRAGシステムの精度改善コンペに参加しました。

結果は xx位/xx位となり、自分なりに取り組んだ内容をまとめたいと思います。

今回の記事で紹介しているRAGシステムはこちらのGitHubリポジトリで公開しておりますので、是非ご興味がございましたらご覧ください。

書籍やwebの記事化などを参考に構築したRAGシステムになりますので、基本的な知識があり、動くものを作ってみたいという方に参考になれば幸いです。

Raggleとは

Raggleは実際の企業課題を基にしたRAGの精度改善を競う実践型のコンペティションです。

  • 実際の企業課題に挑戦できる
  • 賞金制のコンペティション

製薬企業向け高度なRAGシステムの精度改善コンペは、PDFをデータソースにし回答精度を競う立て付けになっていました。

アプローチ

1.環境構築 & ベースモデルの作成

ありたがたいことに提出まで行えるサンプルコードが公開されていたので、こちらに準拠して1st submitを行いました。
環境構築をし、ローカル環境で動くコードを作りこれ以降はこのコードを改善しながら精度向上を計りました。

https://zenn.dev/galirage/articles/raggle_quickstart

2.[LangChainとLangGraphによるRAG・AIエージェント[実践]入門]に出てきた手法から試してみる

6章のAdvanced RAGの内容を元にステップを分けて、それぞれ改善を測りました。アプローチしたステップは下記です。

  • ドキュメントのベクトル化の工夫
  • 検索クエリの工夫
  • 検索後の評価の工夫
  • 生成時のプロンプトの工夫

https://www.amazon.co.jp/LangChainとLangGraphによるRAG・AIエージェント[実践]入門-エンジニア選書-西見-公宏/dp/4297145308

3.提出したスコアを元に次の改善点の仮説を立てる

ありがたいことに4つの指標でのスコアが算出されるので、こちらを元に改善指標の選定、howの選定、実装、提出後に評価を確認という流れでPDCAを回しました。

構成

最終的に実装したRAGシステムの構成は添付図のような形になりました。
このコンペでは、使用可能なライブラリが指定されていたのでやりたいことに対して、対応しているライブラリが存在するかを調べながら進めました。

下記添付図は検索クエリの工夫を行い、正確性のスコアが向上した時の記録です

工夫したアプローチ

1. ドキュメントのベクトル化

色々使ってみたり、パラメータをいじってみたりしたが大きな変化は感じられず。

text_splitter = CharacterTextSplitter(
    separator="\n",
    chunk_size=1000,
    chunk_overlap = 250
)

2.質問を抽象化し、複数のRetrieverで検索をかける

## ベクトル検索
chroma_retriever = retriever.with_config(
{"run_name": "chroma_retriever"}
)

## キーワード検索
bm25_retriever = BM25Retriever.from_documents(docs).with_config(
    {"run_name": "bm25_retriever"}
)

## 複数のretrieverを使う
hybrid_retriever = (
    RunnableParallel({
        "chroma_documents": chroma_retriever,
        "bm25_documents": bm25_retriever,
    })
    | (lambda x: [x["chroma_documents"], x["bm25_documents"]])
    | reciprocal_rank_fusion
)

3.複数の検索クエリの結果をリランクし、一つに決める

EmbeddingベースとBM25ベースのRetrieverを使って、RAG Fusionによりリランキングしたことで正確性のスコアが大きく向上しました。

def reciprocal_rank_fusion(retriever_outputs: list[list[Document]],k: int = 60,) -> list[str]:
    # 各ドキュメントのコンテンツ (文字列) とそのスコアの対応を保持する辞書を準備
    content_score_mapping = {}

    # 検索クエリごとにループ
    for docs in retriever_outputs:
        # 検索結果のドキュメントごとにループ
        for rank, doc in enumerate(docs):
            content = doc.page_content

            # 初めて登場したコンテンツの場合はスコアを0で初期化
            if content not in content_score_mapping:
                content_score_mapping[content] = 0

            # (1 / (順位 + k)) のスコアを加算
            content_score_mapping[content] += 1 / (rank + k)

    # スコアの大きい順にソート
    ranked = sorted(content_score_mapping.items(), key=lambda x: x[1], reverse=True)  # noqa
    return [content for content, _ in ranked]

4. 生成プロンプトの工夫

"簡潔に抜き出して"などプロンプトエンジニアリングのアプローチで簡潔性については少しスコアが向上しました。

hypothetical_prompt = ChatPromptTemplate.from_template("""\
与えられる質問を検索しやすいようにキーワードや聞かれていることをわかりやすく書き換えたクエリを作成してください。

質問: {question}
""")
template = """
# ゴール
私は、参考文章と質問を提供します。
あなたは、参考文章だけを踏まえて質問に簡潔に回答してください。質問と関係ないことは回答しないでください。

# 質問
{question}

# 参考文章
{context}
"""

雑感

自身で一から実装することでRAGの精度向上を目指す上で、変数になる部分について実感を持つことが出来ました。具体的には、ベクトルDBの作成方法、検索クエリの工夫、検索後の工夫、解答生成時の工夫といった各ステップでさまざまなアプローチ方法が提案されていることを知ることができました。

最後に

今回のコンペの取り組みも、記事としてまとめるのも下記ブログをとても参考にさせていただきました。ありがとうございました!

https://zenn.dev/iossu7/articles/6e806fa1df044c#各種アプローチ

Discussion