Azure Cognitive Search を ChatGPT Plugin 経由で呼び出し、AI オーケストレーターと統合する

# はじめに
Build 2023 にて Microsoft が発表した開発フレームワークである「[Copilot stack](https://news.microsoft.com/ja-jp/features/230524-microsoft-outlines-framework-for-building-ai-apps-and-copilots-expands-ai-plugin-ecosystem/)」に従って、プラグインエコシステムを構築しましょう。

今回は Azure Cognitive Search に新たに追加されたベクトル検索とハイブリッド検索の機能を ChatGPT プラグインとして実装してローカル開発環境からテストしてみましょう。

https://qiita.com/nohanaga/items/c3e92c9adeca7c84a8d5

# プラグイン開発
ChatGPT 規格の[プラグイン](https://platform.openai.com/docs/plugins/introduction)を開発すれば、以下のようにブラウザ版 GPT-4 だけでなく、Bing や Microsoft 365 Copilot、Windows Copilot などからも利用可能になります。当然、組織独自の Copilot と統合することもできます。このプラグインによって、最新の情報や組織内のデータを反映させた（Grounding）回答を行ったり、ReAct などのタスクを実行させることが可能になります。
![007.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/a81aed4d-46ae-7f92-0e4e-2938abc95ba4.png)
Azure Cognitive Search のベクトルインデックスを利用して Grounding するプラグインはすでに OpenAI 公式より、[ChatGPT Retrieval Plugin](https://github.com/openai/chatgpt-retrieval-plugin) が公開されていますし、Azure Cognitive Search も対応しましたので、さっそくこちらを見ていきましょう。
![003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/0c2325d4-ce22-c8db-7093-cfcd3538b383.png)

# ChatGPT Retrieval Plugin
このプラグインは、OpenAI の `text-embedding-ada-002` Embeddings モデルを使用してドキュメント チャンクの Embeddings を生成し、バックエンドのベクトル データベースを使用してそれらを保存およびクエリします。オープンソースの自己ホスト型ソリューションとして、開発者は独自の Retrieval プラグインをデプロイし、ChatGPT に登録できます。Azure Cognitive Search のデータソースプロバイダー は Microsoft の DE である Pablo Castro によってコミットされました。

プラグインは Python の FastAPI サーバー上で稼働し、ドキュメントの更新挿入、クエリ、削除を行うためのプラグインのエンドポイントを公開します。ユーザーは、ソース、日付、作成者、またはカスタム可能なメタデータ フィルターを使用して、検索結果を絞り込むことができます。

https://github.com/openai/chatgpt-retrieval-plugin

# AI オーケストレーション
このプラグインの威力はアプリ利用だけに留まりません。Copilot stack における「AI オーケストレーター」からの呼び出しも可能になりますので、組織独自の Copilot から ReAct の Tools や Skills として組織内の様々なデータソース、API 化したアプリケーションと接続できるようになります。Build 2023 では Azure AI Studio と [Azure Cognitive Search の連携](https://www.youtube.com/watch?v=28yGXLN-n6M)も発表されていましたので、わざわざプラグイン開発しなくてもいい気がしますが、内部の理解のために実施します。

今回は AI オーケストレーターとして LangChain を利用してプラグインを呼び出しています。

# ローカルデバッグ
OpenAI のモデルを利用したエンタープライズナレッジサーチのテストでは、ブラウザ版 GPT-4 を使用してプラグインをアタッチすると、データが OpenAI 社に行ってしまいます。そのため、Azure OpenAI Service を用いて仮想ネットワークを構築して、プライベートネットワーク経由でモデルを利用できるようにしてローカル環境に用意した LangChain からプラグインを呼ぶことにします。LangChain に追加されている ChatGPT Plugin を利用するための ChatGPTPluginRetriever を利用してローカルでデバッグします。
![006.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/bdeb7e18-5bba-d79e-c529-960e49729d10.png)

## フロー解説
説明のため、コードはシンプル化しています。

1. UI からユーザーの質問や指示が投げられます
1. クエリは LangChain が受け取り、[RetrievalQA Chain](https://python.langchain.com/docs/modules/chains/popular/vector_db_qa) を用いて Retrieval Augmented Generation(RAG) を実施します。RetrievalQA は [ChatGPTPluginRetriever](https://python.langchain.com/docs/modules/data_connection/retrievers/integrations/chatgpt-plugin) を通じて ChatGPT Retrieval Plugin ローカルサーバーへクエリを投げます。
1. ローカルサーバーは Embeddings API を呼んでクエリをベクトルに変換し、Azure Cognitive Search にベクトル検索を行います
1. Azure Cognitive Search が検索結果を返却します
1. ChatGPT Retrieval Plugin が整形を行って呼び出し元の LangChain に返却します
1. RetrievalQA が ChatGPT を使って回答を生成し、UI に返却します


# 環境構築
[Quick Start ](https://github.com/openai/chatgpt-retrieval-plugin#quickstart)に従って構築するだけで簡単に実行できます。あらゆる設定が環境変数の定義だけで反映されるように作られており、非常にスマートです。

`BEARER_TOKEN` はプラグインの呼び出し元を認証するためのトークンで、[jwt.io](https://jwt.io/) などを使って任意に生成してセットします。このトークンを LangChain の `ChatGPTPluginRetriever` で指定します。

```bash
export DATASTORE=azuresearch
export BEARER_TOKEN=<your_bearer_token>
export OPENAI_API_KEY=<your_openai_api_key>
export OPENAI_API_BASE=https://<AzureOpenAIName>.openai.azure.com/
export OPENAI_API_TYPE=azure
export OPENAI_EMBEDDINGMODEL_DEPLOYMENTID=<Name of text-embedding-ada-002 model deployment>
```

### Azure Cognitive Search 接続定義
```bash
# Azure Cognitive Search
export AZURESEARCH_SERVICE=<your_search_service_name>
export AZURESEARCH_INDEX=<your_search_index_name>
export AZURESEARCH_API_KEY=<your_api_key> (optional, uses key-free managed identity if not set)
```

### Azure Cognitive Search インデックス定義
Azure Cognitive Search のインデックス定義とのマッピングを行う。対応するマッピングが無い場合は未定義で OK。`AZURESEARCH_FIELDS_SOURCE` だけは `file` 等にする必要がある。詳細な定義は `azuresearch_datastore.py` を参照。

```bash
export AZURESEARCH_FIELDS_ID=<your_vectorindex_id_field>
export AZURESEARCH_FIELDS_TEXT=<your_vectorindex_content_field>
export AZURESEARCH_FIELDS_EMBEDDING=<your_vectorindex_vector_field>
export AZURESEARCH_FIELDS_SOURCE=file
export AZURESEARCH_FIELDS_SOURCE_ID=<your_vectorindex_filename_field>
```
### Azure Cognitive Search 日本語アナライザー定義
日本語ドキュメントのキーワード検索やハイブリッド検索を実施する際には、`content` フィールドの `analyzer` に適切な日本語アナライザーを設定してください。クエリが自然言語に近い形になっていくと、`ja.microsoft` では検索ノイズが増える可能性がありますので、`ja.lucene` を推奨します。アナライザーの比較は[こちら](https://qiita.com/nohanaga/items/7296505f7b63e23f94a6)で解説しています。

# REST で呼び出し
REST API のエンドポイント docs は、`http://localhost:3333/docs#` にアクセスすれば閲覧できます。ドキュメント検索は `/query` エンドポイントを使用し、Authorization ヘッダに Bearer Token をセットします。

```json
POST http://localhost:3333/query
{
  "queries": [{
      "query": "三浦義澄って何した人",
      "top_k": 3
    }]
}
```


# LangChain からの呼び出し
```python
from langchain.retrievers import ChatGPTPluginRetriever

BEARER_TOKEN=<your_bearer_token>
retriever = ChatGPTPluginRetriever(url="http://localhost:3333", bearer_token=BEARER_TOKEN)
result = retriever.get_relevant_documents("三浦義澄って何した人")
print(result)
```
プラグインを直接呼び出すには上記コードを使います。実行すると以下のように生の検索結果が返却されます。

```python
[Document(page_content='三浦 義澄（みうら よしずみ）は、平安時代末期、鎌倉時代初期の武将。鎌倉幕府の御家人。桓武平氏の流れを汲む三浦氏の一族で、三浦介義明の次男。十三人の合議制の一人。  経歴 相模国三浦郡矢部郷の出身。具体的な時期は不明ながら、上総常澄の加冠によって元服し義澄と名乗ったとされる[1][注釈 1]。  平治元年（1159年）の平治の乱では源義平に従うが、平家方に敗れて京都から郷里に落ち延びる。長寛2年（1164年）、兄・杉本義宗が亡くなり、それによって三浦氏の家督を継ぐ。', metadata={'id': '5LiJ5rWm576p5r6ELTAudHh0', 'metadata': {'source': 'file', 'source_id': '三浦義澄-0.txt', 'url': '', 'created_at': '', 'author': '', 'document_id': ''}, 'embedding': None, 'score': 0.8817653})]
```

# LangChain から ChatGPT 質問応答
```python
from langchain.chains import RetrievalQA
from langchain.chat_models import ChatOpenAI
from langchain.retrievers import ChatGPTPluginRetriever

retriever = ChatGPTPluginRetriever(url="http://localhost:3333", bearer_token=BEARER_TOKEN)

llm = ChatOpenAI(model_name="gpt-35-turbo", temperature=0, model_kwargs={"deployment_id":"gpt-35-turbo"})

chain = RetrievalQA.from_chain_type(llm=llm,
                                    chain_type="stuff",
                                    retriever=retriever,
                                    return_source_documents=True)
answer = chain({'query': "三浦義澄って何した人"})
print(answer['result'])
```
ChatGPT(gpt-35-turbo) に回答を生成させると、以下のような出力が得られます。
```python
三浦義澄は、平安時代末期から鎌倉時代初期にかけての武将で、鎌倉幕府の御家人でした。三浦氏の一族で、十三人の合議制の一人でもありました。彼は平治の乱に参加し、源義平に従いましたが、平家方に敗れて郷里に落ち延びました。その後、治承4年の以仁王の挙兵時に大番役として在京していた彼は、関東に下り、源頼朝の下に参上し、以仁王の挙兵の詳細を報告しました。彼は石橋山の戦いにも参加し、その後、父・義明が河越重頼や江戸重長に討ち取られたため、安房国へ渡りました。
```

# 課題　チャンク分割
`return_source_documents` を `True` にしているので、`print(answer['source_documents'])` することで検索結果の中身を表示することができるのですが、前回から使いまわしの[戦国武将データ](https://qiita.com/nohanaga/items/803c09b5a3a4e2d1776f)ではベクトル検索だけでは必要なドキュメントを上位に持ってくることができませんでした。特に 2 位以降の精度が悪いです。そういえば Embeddings の生成に使用したチャンクが小さすぎました。`text-embedding-ada-002` は最大 **8,191** トークンまで入力できるので、こちらを考慮して再度データを作ろうかと思います。

# ハイブリッド検索
ChatGPT Retrieval Plugin はデフォルトでハイブリッド検索が有効になっていますが、`AZURESEARCH_DISABLE_HYBRID` を `1` に設定するとベクトル検索のみになります。ベクトル検索の精度が悪い場合、別の人物の検索結果を上位に持ってきてしまい、そのまま ChatGPT によって回答が統合されてしまいます。このような場合は、キーワード検索を組み合わせた**ハイブリッド検索**が有効です。（もちろんキーワード検索単独も確実ですが…）用途に応じて検証することが必要です。

# セマンティックハイブリッド検索
`AZURESEARCH_SEMANTIC_CONFIG` にセマンティック構成名をセットすることで、[セマンティックハイブリッド検索](https://qiita.com/nohanaga/items/f710cac82072b63bc73f#%E3%82%BB%E3%83%9E%E3%83%B3%E3%83%86%E3%82%A3%E3%83%83%E3%82%AF%E3%83%8F%E3%82%A4%E3%83%96%E3%83%AA%E3%83%83%E3%83%89%E6%A4%9C%E7%B4%A2)が有効になります。セマンティックハイブリッド検索はハイブリッド検索の結果を Microsoft の Turing モデルを利用した Re-ranker で結果を並べ替えることでさらに精度を高める独自の手法です。

# 試してみたい機能
- Memory 機能
プラグインの `/upsert` エンドポイントを利用することにより、ChatGPT は対話データをベクトルデータベースに保存して、後で参照できるようにすることができます。Azure Cognitive Search に直接対話データ入れちゃうのはアリなのか…(笑)　過去の記憶もハイブリッド検索で正確性上げていくアプローチ。
- OAuth 認証
OAuth プロバイダーを使用して、プラグインを追加するユーザーを認証し、API へのアクセスを許可できます。
- デプロイ
[Azure Container Apps](https://github.com/openai/chatgpt-retrieval-plugin/blob/main/docs/deployment/other-options.md) へのデプロイ機能

6/13 ちなみに LangChain の Retriever として Azure Cognitive Search でキーワード検索ができる [AzureCognitiveSearchRetriever](https://python.langchain.com/docs/modules/data_connection/retrievers/integrations/azure_cognitive_search) やベクトル検索に対応した [VectorStore](https://python.langchain.com/docs/modules/data_connection/vectorstores/integrations/azuresearch) も追加され直接利用することが可能になりました。

## Microsoft 365 Copilot 用プラグイン開発
[Microsoft 365 Copilot 用のプラグイン](https://developer.microsoft.com/en-us/microsoft-teams/ai)は、早期アクセス プレビュー段階であり、参加するには[開発者プレビュープログラム](https://forms.office.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR06na7ADVcFBoFXEJ5M8YD1UNkdQODE5VDNKOUQ2UlBSVUJJRlJDVktTVS4u)への申請が必要です。Microsoft 365 Copilot では、Teams メッセージ拡張機能や [Microsoft Graph コネクタ](https://developer.microsoft.com/en-us/graph/connectors)が提供されます。

# おわりに
[こちら](https://qiita.com/nohanaga/items/803c09b5a3a4e2d1776f)のエンタープライズサーチの MRKL アプローチに複数の ChatGPT Plugin を組み合わせたアプリを開発すると面白そうですね。

AI を中心とした Copilot stack やプラグイン エコシステムについては 7 月に開催予定の [Azure OpenAI Developers セミナー](https://www.youtube.com/watch?v=tFgqdHKsOME) 第 2 回でも詳しく解説する予定です。