RAGの回答精度を改善するためにやったこと

# はじめに
私たちのチームでは、自社プロダクト(パッケージソフトウェア)に関する質問に対して、自然言語で回答できるRAGの開発を進めています。
現在は、社員やパートナーの皆様を中心にご利用いただいています。
ユーザの皆様よりいただいたフィードバックを元に精度改善を行ったので、今回はその事例を紹介したいと思います。

## 想定読者
- RAGの構築経験がある人
- RAGの精度向上に取り組もうと考えている人

# 背景
## 改善前のRAG
シンプルなアルゴリズムのRAG(Naive RAG)を採用していました。
この時点では、いわゆるAdvanced RAGのような改善手法は採用していません。

ベクトル検索の上位10件の情報をコンテキストとして、LLMに回答させています。
検索対象は、製品ドキュメントやヘルプデスクの過去問合せをFAQ形式に整形した文書です。

## 技術要素
GoのEchoフレームワークを利用して、マイクロサービスとして作成しています。
LangChainやLangGraphなどのライブラリは使わず、自前で実装しています。
- LLM: GPT-4o
- Embedding: text-embedding-ada-002
- 検索エンジン: Elasticsearch v8.10.4

## 精度改善の取り組みのきっかけ
複数のユーザから **「質問の仕方によって回答の可否や質が変わる」** というご意見をいただきました。
実際に報告があった例を挙げてみます。

**Ex.1 プロダクト特有の機能や属性に関する質問に回答できるケースとできないケースがある。**
- 「属性Aとは」 ⇒ 「回答できません」と出力される
- 「属性Aとはどういった項目ですか」 ⇒ 属性Aの説明が出力される

**Ex.2 質問の仕方によって回答に含まれる情報量が変わるケースがある。**
- 「属性Bとは」 ⇒ 属性Bの説明が出力される
- 「属性Bについて教えてください」 ⇒ 属性Bの説明に加えて、初期値の設定方法が出力される

「良い回答が返ってきやすい質問の仕方を教えてほしい」というご意見もいただきましたが、現場の工夫に頼ってユーザの敷居を高くしてしまうのは良くないと判断し、精度改善に取り組むことにしました。

# 原因を調査する
## 調査方針
まずは、RAGの各ステップのどこに問題があるのかを特定します。
Microsoftのエバンジェリストであり、「[Azure OpenAI ServiceではじめるChatGPT/LLMシステム構築入門](https://gihyo.jp/book/2024/978-4-297-13929-2)」の著者である蒲生弘郷さんによると、RAGの各ステップには以下のような精度影響因子があるそうです。

| RAGのステップ | 精度低下に影響する因子 |
|:--|:--|
| **入力情報の加工**  | ユーザ入力に検索のための情報が足りない、整理されていない。 |
| **ドキュメント・クエリマッチング** | ユーザの入力内容と検索対象が意図した類似になっていない。 |
| **検索実行** | 検索エンジンの精度が悪い。 |
| **コンテキストベース回答** | 検索結果を正しく解釈できていない。 |

<script defer class="speakerdeck-embed" data-id="284a0f6dcddb4f2bb1a7389789104508?slide=152" data-ratio="1.7777777777777777" src="//speakerdeck.com/assets/embed.js"></script>
出典：ChatGPT - LLMシステム開発大全 - Hirosato Gamo https://speakerdeck.com/hirosatogamo/chatgpt-azure-openai-da-quan

こちらを参考にして、今回は以下の観点で調査することにしました。

**1. ユーザ入力に検索のための情報が足りているか**
**2. 入力された内容と検索対象が意図した類似になっているか**
**3. 回答の参考になるドキュメントがそもそも存在しているか**
**4. 回答の参考になるドキュメントが検索にヒットしているか**
**5. LLMが検索結果を正しく解釈して回答しているか**

## 調査結果

> **1. ユーザ入力に検索のための情報が足りているか**

今回は、特定の用語の意味や概要を尋ねるケースで問題が発生しています。
質問に含まれる用語が「検索のための情報」に当てはまるので、こちらは問題なさそうです。

> **2. 入力された内容と検索対象が意図した類似になっているか**

少なくとも回答できているケースでは、検索対象の中に意図した類似を持つドキュメントが含まれており、回答も質問の意図を汲んだ内容になっていました。

> **3. 回答の参考になるドキュメントがそもそも存在しているか**

質問された機能に関する説明は、検索対象の製品ドキュメントやFAQ文書にしっかりと含まれていました。

> **5. LLMが検索結果を正しく解釈して回答しているか**

回答の参考になるドキュメントがコンテキストに含まれている前提で何度か試行して、想定通りの回答を出力できることを確認しました。こちらも問題はなさそうです。

> **4. 回答の参考になるドキュメントが検索にヒットしているか**

ここに問題がありそうなことが分かりました。
回答できたケースはコンテキストに回答の参考になるドキュメントが含まれてましたが、回答できなかったケースではコンテキストにそれらが含まれていませんでした。
ここで起きている問題を整理してみます。

**問題①: 回答の参考になるドキュメントの数が少ない**
各ケースのコンテキストの内容を確認してみると、今回問題になっている質問はいずれも、回答の参考になるドキュメントが1～2件しか存在していませんでした。
質問文をそのままベクトル検索のクエリとして利用しているために、微小なニュアンスのズレでスコアが変動してしまい、上位10件に入ったり入らなかったりしているのでは...と推測しています。

**問題②: 関連性の低いドキュメントが検索結果の上位を占めている**
各コンテキストを確認する中で、質問と関係のない機能や項目を含むドキュメントのスコアが高くなってしまうケースが発生していることも判明しました。

具体例を挙げて説明します。
弊社のプロダクトは、データの種別や処理体系を表す属性として「Xパタン」という項目を持っています。
この「Xパタン」を含む文章をベクトル検索すると、以下のような単語を含むドキュメントが検索にヒットします。

- 「X'」(一般用語として「X」と類似の意味を持つ単語)
- 「Yパタン」(「Xパタン」と命名規則が同じ別の属性)

上記のような**類似の単語を含むドキュメントばかりが検索結果の上位を占めてしまい、肝心の「Xパタン」という単語を含むドキュメントが検索結果の上位に入らない**という事態が発生していました。

# 対策を講じる
調査によって、検索実行のステップに問題があることが判明しました。
検索精度を上げるための取り組みの候補はいくつかあるのですが、今回はハイブリッド検索を導入してみます。

## ハイブリッド検索とは
この文脈では、全文検索とベクトル検索の各検索結果を統合する手法のことを指します。

ベクトル検索は意味的な類似性を捉えた検索ができますが、特定の単語や語句を含むドキュメントを正確に抽出するのは苦手な傾向があります。
全文検索を並行して実行することで、単語や語句を加味した検索を実現できます。

## なぜハイブリッド検索か？

今回のケースで発見した「回答の参考になるドキュメント」は、適切な単語で全文検索すれば上位10件以内にヒットすることを確認できていました。
そのため、ハイブリッド検索を利用すればそれらのドキュメントを確実に上位10件以内に含めることができるのではと予想して、今回試してみることにしました。

## 実現方法
### 1. ベクトル検索を実行する

ユーザから入力された質問をEmbeddingします。
その後、Elasticsearchに構築したベクトル検索用のインデックスに対してクエリを実行し、コサイン類似度のスコアが高い順に検索結果を取得します。

### 2. 全文検索を実行する

まずは、ユーザの質問から全文検索用のクエリを生成します。
以下のようなプロンプトでLLMに問い合わせを行います。

```:systemPrompt
ユーザの質問からエンティティを抽出して、全文検索エンジンでの検索に適した形式に変換してください。
以下の背景とルールを考慮して実施してください。

# 依頼の背景
変換後の語句を利用して、全文検索エンジンを実行することを想定しています。
検索対象は、ソフトウェアの技術文書やFAQ形式でまとめられたドキュメントとします。

# ルール
- 変換後の語句のみを出力してください。
- 重要なエンティティが複数個ある場合は、それらをスペース区切りで連結して出力してください。
```

```:userPrompt
ユーザの質問: (ここにユーザの質問を記載する)
変換後の語句: 
```
続いて、Elasticsearchに構築した全文検索用のインデックスに対してクエリを実行し、スコアの高い順に検索結果を取得します。

全文検索用のインデックスは、n-gram解析と形態素解析を併用して構築しています。
以下の記事に詳細をまとめているので、興味のある方はぜひご覧ください。

https://qiita.com/s-itou/items/f5f85ae16b8143db6a9c

### 3. ベクトル検索の結果と全文検索の検索結果を統合する

ベクトル検索と全文検索の検索結果を統合します。
ベクトル検索はコサイン類似度、全文検索はBM25でスコア計算されているため、それぞれのスコアの取りうる値の範囲が異なります。単純にスコアの平均値を計算すればよい、というものでもありません。

今回は[Azure AI Searchのハイブリッド検索](https://learn.microsoft.com/ja-jp/azure/search/hybrid-search-overview)でも利用されている **RRF(Reciprocal Rank Fusion)** というアルゴリズムで検索結果を統合します。

RRFについてざっくり説明すると、各検索結果のリスト内の順位の逆数をスコアとみなして、各ドキュメントのスコアを再計算するアルゴリズムになります。順位を元にスコアを再計算するため、各検索結果のスコアの範囲が異なっていても利用できます。

RRFの詳細については、以下のリンク先をご確認いただくとより理解が深まると思います。

https://learn.microsoft.com/ja-jp/azure/search/hybrid-search-ranking

https://hironsan.hatenablog.com/entry/rank-fusion-algorithms#RRF

## 結果
ハイブリッド検索を実装することで、問題となった質問にも安定して回答できるようになりました。
全文検索の結果を利用できるようになったので、回答の参考になるドキュメントを的確に抽出してコンテキストに含められるようになりました。

# 総括

ハイブリッド検索を導入することで、今回の問題は解決できました。

振り返ってみると、最初に問題のある質問・コンテキスト・回答の実データを見ながら原因を特定しようとしたのが良かったのかなと感じています。

RAGに関する論文や改善手法を目にする機会が増えてきたなあという実感がありますが、それらをただ闇雲に試すのではなく、解決したい課題に泥臭く向き合って原因を特定することが大切であると、身をもって学ぶことができました。
(蒲生さんもそのことについてXで言及されていましたね。)

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">どんな事情があろうがRAGの精度出ないときの初手なんか、質の悪いパターン集めて<br><br>① User Input<br>② ①からLLMが加工したクエリ<br>③ 検索結果<br>④ ③を元にしたLLMの回答<br><br>を見るしかないのに、見もせずAdvanced RAGだのGraph RAGだの始めるのは止めよう。<br><br>①～④のどこに原因あるかで処方は違うんや。</p>&mdash; Hirosato Gamo | AI Cloud Solution Architect (@hiro_gamo) <a href="https://twitter.com/hiro_gamo/status/1830478958334447990?ref_src=twsrc%5Etfw">September 2, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

これからRAGの精度改善に取り組もうという方には、
**1. どのような質問に回答できるようにしたいかを明確化するための課題設定を行うこと**
**2. RAGの各ステップの精度影響因子を調査して、原因を特定すること**
を強くおすすめします。

最後まで読んでいただきありがとうございました。