Azure OpenAI Service + Azure AI Search(旧：Cognitive Search）でのRAG環境の精度向上を考えてみた

　私は元々データベースエンジニアなのですが、最近では世の中の流れに抗えず、生成AIの案件が大分と増えてきています。
　Azure OpenAI Serviceを社内で活用する場合、真っ先に上がるのが、Azure OpenAI Serviceを使って、「企業の中でセキュアに使えるChatGPTのような環境を構築する」という事になるのですが、「単純に生成AIを会話形式で利用できる」と言う機能だけではなく、チャットシステム＋αの機能が求めらる事も多いです。ただ、どのような機能を「＋α」として機能実装すればいいのかというと、なかなかアイデアは出ず、多くの場合に「まずはここから始めよう」という形で、以下の機能を実装することが非常に多いです。
 
 * **社内にあるデータを生成AIにセットし、そこから回答を生成させる。**
 * **ハルシネーションをできるだけ回避する方法で回答を生成させる。**

そう、いわゆるRAG（Retrieval-Augmented Generation）構成です。Azure OpenAI Service + Azure AI Search(旧：Cognitive Search)を使えば比較的容易に構築もでき、費用対効果も高い事から、多くのいわゆる「社内向けChatGPT」を環境構築する際には、同時に機能実装される構成となります。

RAGにも様々な方法がありますが、今回は社内にある様々なドキュメント（社内規定、就業規則、各種マニュアルなど）の情報をAI Searchに入れ、ユーザーからの質問の内容にそのドキュメントの情報を使って回答をするという仕組みについて、精度向上を考えてみたので、記事に残したいと思います。

また、どちらかと言うと本記事はエンジニア向けと言うよりかは、実際にRAG環境を利用するユーザー向けの記事となりますので、技術的な観点で曖昧だったり、すこし間違いがあるかもしれませんが、この点をご容赦いただければと思います。

# GPTの仕組みとなぜRAGが必要なのか
RAG環境の精度向上を考える前に、何故RAG環境が必要なのかと言うポイントを記載したいと思います。このポイントを理解することはとても大事です。

## トークン数の上限
GPTに限らず、多くのLLMでは会話の文字数、単語数は「トークン」と呼ばれる単位でカウントされます。「トークン」は英語と日本語ではカウントのされ方が微妙に違いますが、日本語の場合は大体１文字辺り1~2トークン程度でカウントされます。また、このトークン数はユーザーが入力する文字だけでなく、GPTが生成する回答でも同様にトークン数がカウント（消費）されます。

```plantuml
@startuml
<style>
root {
  Padding 0
  Margin 0
}
title {
  FontStyle normal
  FontColor black
  FontSize 17
}
</style>
title
<img:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697647/0fe8484c-9081-9b9b-02e9-de1e580bafc2.png>
GPTとの会話のイメージ
end title
@enduml
```
Azure OpenAI Serviceで提供されているGPTにはモデルによりトークン数の上限値が決まっています。なので、GPTと会話を何度か繰り返していると、このトークン数の上限値に引っかかり、これ以上会話ができないと言う事があります。

```plantuml
@startuml
<style>
root {
  Padding 0
  Margin 0
}
title {
  FontStyle normal
  FontColor black
  FontSize 17
}
</style>
title
<img:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697647/402e7de3-2b78-d7c3-101c-8183f2bc5b88.png>　
end title
@enduml
```

GPTモデルとトークン数の上限は以下の通り。
|モデル|トークン数上限|
|-----|-----|
|gpt3.5-turbo|4,000|
|gpt3.5-turbo-16k|16,000|
|gpt4|8,000|
|gpt4-32k|32,000|
|gpt4-turbo|128,000|


## 社内のデータからGPTに回答をさせる方法
GPTはある特定の期間までの公開されている情報からしか回答をする事が出来ません。企業の中にある機密情報は学習していないので、GPTに質問をしてもGPTは回答のしようがありません。では社内のデータからGPTに回答を生成してもらうためにはどのような方法があるのかと言いますと、これは大きく２つ存在します。１つが「ファインチューニング」と言う方法。もう一つが「RAG」と言う方法になります。

### ファインチューニング
ファインチューニングは学習済みのGPTモデルに対して、社内の情報を使って追加で学習を行うと言う事となります。これによって、ユーザーが例えば「社内の○○について教えて」とGPTに質問をすると、GPTは社内のデータを追加で学習しているので回答ができるようになるという方法です。

一見すると画期的な方法に見えますが、実際には学習コストが非常に高く、さらにはGPTに新しい事を覚えさせるというのは非常に難しいものとなります。なので、ファインチューニングにより、社内の情報を学習させて、GPTに回答させると言うのは基本的には現実的ではない方法になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697647/5ce51891-79ec-0c59-ef78-9b53022b257a.png)

### RAG
RAGでは、GPTとは別の領域、例えばAI Searchのようなものを用意しそこに、社内のデータを格納/配置しておきます。先ほどと同じようにユーザーは「社内の○○について教えて」とGPTに質問すると、GPTはまず、このユーザーの質問に対して、別の領域に配置されているデータの検索を行います。検索を行って取得してきたデータを会話の中にセットします。その後、GPTはユーザーの質問に対して、セットされた情報を使って、回答を生成するという動きになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697647/fb85645b-b6b8-a7e2-1d63-4a85f5d84620.png)

実際には社内のデータやナレッジをすべて会話の中にセットすることが出来れば、わざわざAI SearchのようなGPTとは別の領域を用意し検索をする必要はありません。しかし、先ほど紹介したように、GPTとの会話にはトークン数の上限があります。社内のデータを全部会話の中にセットするとすぐにトークン数が一杯になってしまい、GPTは回答を生成することが出来なくなります。

なので、AI Searchなどを使って、GPTが回答に必要な情報を適宜検索し、会話にセットして、セットされた情報を前提知識として回答を生成するという事が必要になります。

また、AI Searchに登録するデータも、必要に応じて、細切れに（**チャンク分割**）して登録してあげる必要があります。

これがRAGと言う仕組みです。

# RAG環境の仕組み
RAG環境の仕組みについて、「社内のデータのセット」と「ユーザーの質問に対するデータ検索と回答の生成」という２つのフェーズに分けて見ていきたいと思います。また、ここでは一旦、データの検索方法では、ベクトル値を用いた、ベクトル検索を例に記載したいと思います。

## 社内のデータのセット
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697647/696d01a2-73f2-3e5c-471e-d7d92daf3d03.png)
### テキストデータの抽出
GPTはGPT-4 Turbo with Vsionを除くと、原則としてテキストデータしか対応できません。なので、社内には様々な形式のファイルが存在しますが、ここからテキストデータを抽出することになります。

### チャンク分割
抽出したテキストデータは、GPTのトークン数の上限に引っかからないように、細切れにします。これをチャンク分割と言います。例えば文字数や、改行コード、句読点などを一定のルールにのっとってテキストデータの分割を行います。また、チャンク分割を行っていると、単体の文章として意図せず意味がくずれてしまう場合があります。これを機械的に回避できるよう、一定の割合で前の文章も含めるようにチャンク分割をしていきます。これをオーバーラップと言います。

### ベクトル値計算/インデックス登録
チャンク分割されたテキスト単位で、ベクトル値の計算をAzure OpenAI Serviceを使って行います（このベクトル数値の計算の事をEmbeddingと言います）。計算が終われば、AI Searchのインデックスに登録していきます。
AI Searchでは検索の際にベクトル検索だけでなく、以下のように様々な検索に対応しています。

・ ベクトル検索
・ フルテキスト検索
・ ハイブリッド検索
・ セマンティック ランク付け
※ここでは詳細は割愛させてください。


## ユーザーの質問に対するデータの検索と回答の生成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697647/5d5b4a23-4ade-f0bd-cdfb-74de4058d9e7.png)

### ユーザーの質問とベクトル値計算/キーワード抽出
ユーザーがGPTに対して質問を行うとまずは下準備として、ユーザーの質問に対して、ベクトル値を計算したり、フルテキスト検索が可能なように、質問からキーワードを抜き出します。
例えば
「社内の休暇制度について教えて」という質問に対して、ベクトル値を計算したり、フルテキスト検索ができるように「社内　休暇　制度　教えて」のように文字を分割します。

### データの検索/データの抽出
生成されたベクトル値、あるいはキーワード、あるいはその両方を用いて、AI Searchからチャンク分割されたデータの検索を行い、該当データを抽出します。この時に、データはスコアの高い上位いくつかのデータを抽出してくることになります。

### データセット/回答の生成
抽出されたデータを使って、GPTに最初のユーザーの質問への回答を生成させます。
今回の例で例えると以下の通りです。


|当社の休暇制度について教えて<br><br>上記の質問に以下の情報を使って回答しなさい。<br>【抽出したデータ①~~~】<br>【抽出したデータ②~~~】<br>【抽出したデータ③~~~】|
|:-|

## RAGの仕組みのまとめと精度向上のポイント
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697647/ce2f344f-c58b-e9d4-35f0-c172af08a698.png)

RAGの仕組みに関してはこれまでの説明の通り上記のような流れで処理されますが、それぞれの処理の流れの中で精度改善のポイントがありそうですと言うのが今回の本当の趣旨になります。

# RAGの精度改善、向上
## ポイント①：テキストデータの抽出
### 精度を求めるのであればそれなりに加工も必要
#### 不要なデータは削除
PDF、Word、EXCEL、PPTXなどのファイルからテキストデータだけを単純に抽出すると、不要なデータも一緒に抽出されます。不要なデータとは例えば、「ヘッダー/フッター」や「目次」などです。不要なデータがあると、正しく検索、抽出ができない可能性があります。不要なデータは可能な限り削除するのが望ましく、精度を求めるのであればある程度、加工は必要になる事が大半です。

#### 可能な限りマークダウン形式でデータは用意する事
ファイルからテキストデータを抽出すると、特に表などは、崩れてしまう可能性があります。可能な限りマークダウンを用いて記載することで、人間の可読性が上がりますが、これはGPTが回答を生成する際にも有効なポイントです。ファイルをマークダウン形式に変換するツールなどを使うのも有効な手段となります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697647/654908f0-ab37-ec38-ec5a-41926b25ed34.png)

## ポイント②：チャンク分割
### 文章が可能な限り意味のある単位でチャンク分割されることが重要
チャンク分割の最大の理由は「GPTのToken数上限に抵触しないように文章を適度な長さで分割する事」です。ただし、文章を細かく分割してしまい、文書の意味が分からなくなるとGPTから正確に回答を生成させることが困難になります。「適切な長さに分割し、意味合いも壊れない」と言うのが最も大事なポイントになります。

#### 機械的なチャンク分割
テキストデータを機械的にチャンク分割する際、何文字でテキストデータを分割するのか、オーバーラップサイズはどれくらいにするのか、あるいは区切り文字として、改行、句読点など意識するのかと言うのは重要なポイントになります。また、ファイルやドキュメントの種類によってもこの適切なサイズは異なりますが、これまでの経験上まずは、GPT3.5-turbo、GPT4では、以下の通りの設定からスタートするのが日本語ではよさそうです。

**・分割サイズ（文字数）**
> テキストデータは日本語の場合はまずは500文字を指定サイズとして、これを起点に文章によって調整を行う。

**・オーバーラップサイズ**
> 機械的に文章分割すると、文書の途中で文字が切れてしまう場合があり、文書が切れる事によって文書の意味が分からなくなってしまう事があります。オーバーラップとはこれを回避するために、文書が区切られた際に、オーバーラップサイズの文字数だけ、前の文書を含めるような動作の事を言います。経験上、分割サイズの20％程度で指定する事で、それなりに精度が出ますが、文章によって調整が必要です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697647/f4d3aa7c-0a2b-bb02-20c2-1c9fe149eb8e.png)

##### 機械的なチャンク分割を行う際の例：CSVファイル（表形式）のチャンク
***CSVファイルの分割サイズ（文字数）の推奨***
分割サイズは必ず、1行の情報が全て入るように設定するように指定する必要があります。このため、文字数での分割ではなく、改行(\n)などで分割するように指定するのも有効です。
１行当たりの文字数がそれほど多くない場合は文字数と改行（\n）を両方加味して機械的に分割するのも有効です。
> 例：500文字を越え、改行(\n)が現れたら分割する

***CSVファイルのオーバーラップサイズの推奨***
CSVファイルで、１行のデータの中に改行が含まれていない場合、オーバーラップサイズは指定する必要はありません。

##### 機械的なチャンク分割を行う際の例：文字数の多くないファイル（数ページ程度の文章）
文字数の多くない数ページ程度のドキュメントの場合には、ドキュメント内のすべてのデータをプロンプトに入れても回答可能です（トークン数の上限値に引っかかりません）。そのため、無理にチャンク分割などは行わず、すべてのデータを登録するようにします。

#### 人手による文書の分割
入力するデータを機械的なチャンク分割を行わず、人手により分割を行う事を検討することも、精度向上には非常に重要です。これにより登録されるデータとしては最も高品質になる事が期待できます。これは機械的な分割よりも文脈や意味をより正確に把握し、文章を分割することが出来るためです。ただし、文書量が多いとかなり分割に手間がかかり、且つ綺麗に文書を短く切れない可能性もあります。この場合はGPTを使い「以下の文章を数百文字程度で要約して分割して」などのように省力化を図る事も重要なポイントです。
※私が約200ページのマニュアルを実施した際には、GPTの補助を借りても数時間の作業となりました。















## ポイント③：データ検索/抽出
AI Searchに登録されている（インデックスが構成されている）データの検索を行う方法には様々な手法が用意されています。

**フルテキスト検索**
検索キーワードを特定し検索を実施する方式です。文書の中の固有名詞が多い場合、ベクトル検索よりも検索精度が高いと言われていますが、ベクトル検索のような文章の意味合いを理解した検索はできません。

**ベクトル検索**
文章の意味をAIがベクトル値に変換します。変換された数値を元に近傍探査により検索を実施します。文章の意味合いをある程度理解した検索を行う事が可能ですが、GPTが学習していないような固有名詞の多い文章はうまくベクトル値を計算することが出来ず検索精度が低くなる傾向があります。

**ハイブリッド検索**
フルテキスト検索のスコアとベクトル検索のスコアの両方を加味し、総合スコアから文書を特定する方法です。汎用的に使う事が可能で、精度も高いと言われていますが、フルテキスト、ベクトルのどちらの検索が効いているのかいまいちわかりにくいのが欠点です。

**セマンティック ランク付け**
フルテキスト検索やハイブリッド検索の検索結果の品質を向上させるための機能です。BingとMSリサーチにより開発され、Azure AI Searchに導入された機能になります。これを利用することにフルテキスト検索やハイブリッド検索の検索性能をさらに向上させられる場合があります。

### RAGで性能を求める場合は文章に合わせて検索方法を指定する事が大切
RAGで精度を求める場合には、文書によって、上記のどの検索方法が該当の文章では精度が出るのかをテストし、見定める事が重要です。なので、汎用的なRAG環境を構築する場合には検索方式をどれか１つに決めて構築するのではなく、文章によって選択が可能なように設計することも重要なポイントになります。

## ポイント④：データセット
### 回答生成する際のデータの個数の調整
検索した結果から、上位何個のドキュメント（データ）をGPTにセットするのか設定を行います。この時に、チャンク分割されている１つ当たりのドキュメントの文字数によって、セットするドキュメントの数を多くしたり、少なくしたりすることで回答の精度の向上を図ります。

ただし、あまり大量のデータをGPTにセットしてしまうと、Token数の上限に達してしまいエラーになる可能性もあります。以下がこれまで実施した経験上それなりに精度が出ている目安となります。（gpt3.5-turbo-16kの場合です）

| 分割サイズ（文字数） | GPTへセットするドキュメントの数 |
|:-:|:-:|
| 3,000~ | 2  |
| 1,000~3,000  | 3  |
| ~1,000  | 5  |

※上記はあくまでも目安であり、実際には適切なGPTへセットするドキュメントの数が異なる事も多くあります。

## その他Tips：システムプロンプトの工夫
### システムプロンプトを思料する子によりハルシネーションを抑制
RAGを行うい際には、通常GPTが学習している情報から勝手に回答を生成されることは望ましくなく、セットしたドキュメントの内容をからのみ回答を生成させ、回答に必要と判断されるドキュメントがセットされなければ、回答の生成を行わないようにすることが望ましい事が一般的です。（ハルシネーションの回避）
この時に、上記を実現する方法はシステムプロンプトを利用する事です。うまくシステムプロンプトを利用することにより学習データからの勝手な回答を抑制する事が可能です。

 **システムプロンプトの例**
 ```
あなたはITシステムのカスタマーサポートを補助するAIです。
あなたには様々な情報を与えますので、与えられた情報の内容から回答を生成してください。
質問の内容があなたに関係のない場合は、答えられない旨ユーザーに伝えてください。
また、ユーザーからの質問が与えられた情報から回答できない物であれば、情報が足りずに回答できない旨ユーザーに伝えて、
さらにより詳しい状況やキーワードをユーザーに入力するように、言ってください。
さらに、検索の精度を向上させるために、キーワード単体で入力することも有効な手段である旨、
ユーザーにアナウンスしてあげてください。

回答はできるだけわかりやすく詳細に教えてください。
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697647/083578ea-3519-5d55-939c-b24aa006be91.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697647/ed80debd-2d27-a4e5-6c2e-5979aeb039b7.png)