LangChainでAzure AI Searchの情報を取得してAIに回答させる

Azure AI Searchを使って、データをリトリーブしてきて回答させるメモです。
Retrieverの作り方は、以前紹介した下記を参考にしてください。

https://qiita.com/umaxiaotian/items/6ccdd0a9052c5610d678

# やりたいこと
Azure AI Search内に入っている内容を元に、GPTに回答してほしい。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418662/d5bcee3b-d103-df8e-5e02-a278a1f174e7.png)
イメージは上記です。

このようにAzure AI Search内に入っている情報からリトリーブし、そのデータをもとに回答を行います。
Azure AI Searchには検索精度を高めるために、スコアリングプロファイルやセマンティック、フィルターなど様々な機能が含まれています。そんな便利な機能を使って今回はLangChainのカスタムリトリーバーを自作して精度を高めた検索を行って見たいと思います。

自作する必要はない？って声が聞こえそうですが、LangChainの標準のRetrieverは、スコアリングプロファイルやフィルターなどを実装していません。。。

https://api.python.langchain.com/en/latest/retrievers/langchain.retrievers.azure_cognitive_search.AzureCognitiveSearchRetriever.html

| パラメーター名     | 説明                                                                                             |
|------------------|-------------------------------------------------------------------------------------------------|
| `service_name`   | Azure Cognitive Searchサービスの名                                                          |
| `index_name`     | Azure Cognitive Searchサービス内のインデックスの名                                              |
| `api_key`        | APIキー　管理キーとクエリキーのどちらも使用可能ですが、データの読み取りにはクエリキーの使用が推奨されます。      |
| `api_version`    | APIバージョン                                                                              |
| `aiosession`     | ClientSession　接続を再利用してパフォーマンスを向上させたい場合に使用します。                         |
| `content_key`    | 取得した結果の中で、Documentのpage_contentとして設定されるキー                                    |
| `top_k`          | 取得する結果の数です。Noneに設定すると、すべての結果が取得される。                                       |

なので、ここにはスコアリングプロファイルなどは指定できないわけですね。
これは精度を上げる上では、非常に不便だという事がわかると思います。

# 実装
早速ですがAzure AI Searchからデータを取得するための、Retrieverのコードになります。

```python
from langchain.callbacks.manager import CallbackManagerForRetrieverRun
from langchain.schema import BaseRetriever, Document
from typing import List, Optional
import requests
import json

class AzureAISearchRetriever(BaseRetriever):
    """
    AzureAISearchRetriever retriever.
    """
    service_name: str
    api_key: str
    index_name: str
    api_version: str = '2020-06-30'
    qa_content_key: str
    qa_top: int
    qa_scoring_profile: str
    def _get_relevant_documents(
        self, query: str, *, run_manager: CallbackManagerForRetrieverRun
    ) -> List[Document]:
        # ヘッダーの設定
        headers = {
            'Content-Type': 'application/json',
            'api-key': self.api_key
        }
        # リクエストのパラメータ
        body = json.dumps({
            'search': query,  # ここにクエリを設定
            'scoringProfile': self.qa_scoring_profile,  # スコアリングプロファイルの適用
            'top': self.qa_top,  # 上位から指定された個数文を検索に加える
        })
        # リクエスト URL の構築
        url = f'https://{self.service_name}.search.windows.net/indexes/{self.index_name}/docs/search?api-version={self.api_version}'
        # リクエストの実行
        response = requests.post(url, headers=headers, data=body)
        # レスポンスの確認
        if response.status_code == 200:
            try:
                # JSONレスポンスの取得
                data = response.json()
                # 各結果を Document オブジェクトに変換
                documents = []
                for item in data.get("value", []):
                    answer = item.get(self.qa_content_key)
                    if answer:
                        metadata = {k: v for k, v in item.items() if k != self.qa_content_key}
                        doc = Document(page_content=answer, metadata=metadata)
                        documents.append(doc)
                return documents
            except Exception as e:
                # JSON解析エラーの場合
                print(f"JSON解析エラー: {e}")
                print("レスポンス内容:", response.text)
        else:
            # リクエスト失敗の場合
            print(f"リクエスト失敗: ステータスコード {response.status_code}")
            print("レスポンス内容:", response.text)
```
このクラスは、Azureの検索APIを用いて特定のクエリに対する関連文書を取得するためのカスタムリトリーバーを実装しています。各パラメータはAzureの検索サービス設定をほぼラップしています。下記のパラメータを適切に設定することで、精度を高めた検索が可能です。

| パラメータ名 | 説明 |
| ----------- | ----------- |
| `service_name` | Azureの検索サービス名 |
| `api_key` | Azureの検索APIを利用するためのAPIキー |
| `index_name` | 検索を実行するためのインデックス名 |
| `api_version` | 使用するAPIのバージョン　デフォルトは`2020-06-30`です。 |
| `qa_content_key` | 検索結果から回答の内容を抽出するためのキー |
| `qa_top` | 検索結果の上位から取得するドキュメントの数 |
| `qa_scoring_profile` | 検索結果のスコアリングに使用するプロファイル |

# 使ってみる
データで、適当にGPTで算数の問題と回答、解説を作成してAzure AI Searchに流し込みます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418662/b000d26a-1d80-4cc3-f563-18334f233a27.png)

また、Azure AI Search側でスコアリングプロファイルを作成しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418662/d7906d8a-fe82-662f-d322-8506ab97e16b.png)

実際にスコアリングプロファイルで掛け算を教えてもらうと以下のような結果が帰ってきました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418662/6a8f7116-3cf1-f44b-01db-21fe296a2130.png)

今回はここの検索結果に出てきているexplanation(問題の解説）をGPTに一緒に投げて回答をもらうようにしたいと思います。

```python
import os
from langchain.llms import OpenAI
from langchain.chains import RetrievalQA
from langchain.chat_models import ChatOpenAI
from azureairetriever import AzureAISearchRetriever
os.environ["OPENAI_API_KEY"] =  ""#あなたのAPIKEY
retriever = AzureAISearchRetriever(
        service_name = '', # Azureの検索サービス名
        api_key = '', #Azureの検索APIを利用するためのAPIキー
        index_name = '', # 検索を実行するためのインデックス名
        qa_content_key="", # 検索結果から回答の内容を抽出するためのキー
        qa_top=1, # 検索結果の上位から取得するドキュメントの数
        qa_scoring_profile="" # 検索結果のスコアリングに使用するプロファイル
)

llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0)

qa = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",
    retriever=retriever,
    verbose=True,
    return_source_documents=True
)

print(qa("掛け算を教えてください"))

```
`from azureairetriever import AzureAISearchRetriever`でクラスをazureairetriever.pyというファイルに切り分けています。皆様の環境に合わせてご利用ください。

## 結果
結果は以下のように出力されました。
```
> Entering new RetrievalQA chain...

> Finished chain.
{'query': '掛け算を教えてください', 'result': '掛け算は、2つ以上の数をかけ合わせる計算方法です。例えば、3個のりんごを買う場合、りんごの値段（80円）を3倍して計算します。具体的には、3 × 80 = 240円となります。同様に、他の商品の個数と値段をかけ合わせて合計金額を計算することができます。', 'source_documents': [Document(page_content='この問題では、りんごの個数を3個、バナナの本数を2本、オレンジの個数を4個としています。\n\nりんごの値段は1個80円 なので、3個買うと3 × 80 = 240円になります。\nバナナの値段は1本60円なので、2本買うと2 × 60 = 120円になります。\nオレンジの値段は1個50円なので、4個買うと4 × 50 = 200円になります。\n\nそれぞれの商品の値段を合計すると、240円 + 120円 + 200円 = 560円になります。\n\nしたがって、りんご3個、バナナ2本、オレンジ4個を買った場合の合計金額は560円になります。', metadata={'@search.score': 37.02698, 'id': '13', 'questionNo': '14', 'summary': 'この問題は「掛け算」と「足し算」の両方が含まれています。掛け算はりんごの個数やバナナの本数、オレンジの個数と商品の値段を掛け算して合計金額を求める部分です。足し算は各商品の合計金額を足し合わせる部分です。', 'question': '町のスーパーマーケットでりんごが1個80円、バナナが1本60円、オレンジが1個50円で販売されています。ある人がりんごを3個、バナナを2本、オレンジを4個買った場合、合計いくらになるか求めなさい。', 'answer': '470円'})]} 
```

解説からちゃんと拾って来て回答していますね。
内容も悪くないと思います。
やはりきちんと精度を高めて検索は必須ですね！！