watsonx.data Milvus + watsonx.ai RAGハンズオン Part3: ベクトルDB Milvusとwatsonx.ai LLMでRAGを構成して、質問をしてみよう!

この内容は2024年11月27日(水)にホテル雅叙園東京で開催された「[IBM TechXchange Japan 2024](https://www.linkedin.com/posts/ibm_ibmtechxchange-activity-7270719294588542976-rA6u?utm_source=share&utm_medium=member_desktop)」で実施したwatsonxハンズオン「さわってみよう ベクトル・データベース watsonx.dataでRAG体験」の内容です。QiitaではPart1, Part2, Part3, Part4の四部構成で、　この投稿は**Part3**となります。

- [Part1: Excelをベクトル化してベクトルDB Milvusに入れよう](https://qiita.com/nishikyon/items/81a6de44aaf52f49eb0f/)
-  [Part2: ベクトルDB Milvusに入ったデータで類似検索してみよう!](https://qiita.com/nishikyon/items/9531d3cbbc893822e494/)
- **Parr3: ベクトルDB Milvusとwatsonx.ai LLMでRAGを構成して、質問をしてみよう!(当投稿)**
- [Part4: ベクトルDB Milvusとwatsonx.ai LLMでRAGを構成して、チャットアプリを作成してみよう!](https://qiita.com/nishikyon/items/7cd7949cd993cc449ca9/)

# 0.前提
- [Part1: Excelをベクトル化してベクトルDB Milvusに入れよう](https://qiita.com/nishikyon/items/81a6de44aaf52f49eb0f/)を実施済み

 - [Part1: Excelをベクトル化してベクトルDB Milvusに入れよう](https://qiita.com/nishikyon/items/81a6de44aaf52f49eb0f/)と同じ実施環境

尚、[Part2: ベクトルDB Milvusに入ったデータで類似検索してみよう!](https://qiita.com/nishikyon/items/9531d3cbbc893822e494/)は実施していなくいても、実行可能ですが、実施していたほうが順を追っていてわかりやすいはずです。

# 1. ベクトルDB Milvusとwatsonx.ai LLMでRAGを構成して、質問をしてみよう!

この内容はjupyter notebook https://github.com/IBM/japan-technology/blob/main/techxchange/2024-watsonx-handson-1/notebooks/techxchange_handson_02_similarity_search_03_RAG.ipynb の「3. ベクトルDB Milvusとwatsonx.ai LLMでRAGを構成して、質問をしてみよう!」 とほぼ同じです。Link先notebookはwatsonx.ai Studioでの実行を前提としていますが、当内容はLocalも含め一般的なpython実行環境が前提です。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294516/ab76ded8-19b0-f883-12d3-e3fe342c5296.png)

以下1〜5はPart1: Excelをベクトル化してベクトルDB Milvusに入れようの1〜5と全く同じです。
また6〜8はPart2: Part2: ベクトルDB Milvusに入ったデータで類似検索してみよう!の6〜8と全く同じです。
jupyter notebook https://github.com/IBM/japan-technology/blob/main/techxchange/2024-watsonx-handson-1/notebooks/techxchange_handson_02_similarity_search_03_RAG.ipynb は　Part2とPart3の内容が1つのnotebookになっているため、Part2で実施した値をそのまま使う内容となってます。


### 1. 必要なライブラリーのインストール
必要に応じてpipにて以下のライブラリーを導入します:
```sh
pip install 'ibm-watsonx-ai>=1.1.15'
pip install 'langchain>=0.3.3'
pip install 'langchain-ibm>=0.3.1'
pip install 'langchain-milvus>=0.1.6'
pip install 'langchain-community>=0.3.2'
pip install 'pymilvus>=2.4.8'
```

### 2. apikeyの設定
apikey をセットしてください。
IBM Cloud環境でのAPIKEYの取得方法は[こちら](https://qiita.com/nishikyon/items/fa8b951267efbe911fe7)を参考にしてください。

またwatsonx.dataとwatsonx.ai Studioは同じAPIKEYである前提で同じ変数を使用していますので、もし違う場合は2つ変数を準備するようにしてください。
```py
apikey = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
```

### 3. Milvus接続情報の設定
Milvus接続情報の値をセットしてください。
- Milvus Host名
- Milvus Port番号

Milvus接続情報の詳細取得手順は[こちら](https://github.com/IBM/japan-technology/tree/main/techxchange/2024-watsonx-handson-1/watsonx_data_get_milvus_info.md) を参照してください。

またmy_connection_argsに設定するkeyの詳細は[pymilvusのMilvusClientのパラメータ](https://milvus.io/api-reference/pymilvus/v2.5.x/MilvusClient/Client/MilvusClient.md)を参照してください。

```py
milvus_host="xxx.xxx.xxx.com"
milvus_port="YYYY"

# Milvus接続情報パラメータののセット
my_connection_args ={
 'uri': f'https://{milvus_host}:{milvus_port}', 
 'token': f'ibmlhapikey:{apikey}'
}
```

### 4. watsonx.ai Studio Project idの設定
watsonx.ai Studioで実行する場合は、このノートブックが実行されるプロジェクトからProject idを取得します。 watsonx.ai Studio以外で実行する場合は、Project idを入力してください。

**Hint**: project_id はプロジェクトを表示し、管理タブから project_id を取得可能です.

```py
import os
try:
    project_id = os.environ["PROJECT_ID"]
except KeyError:
    project_id = "<ProjrctID>"
```

### 5.watsonx.aiのAuthentication用のエンドポイントのURLの設定
Waston Machine Learningのインスタンスを作成したリージョンで決まります。 https://ibm.github.io/watson-machine-learning-sdk/setup_cloud.html#authentication より

Dallas: https://us-south.ml.cloud.ibm.com
London: https://eu-gb.ml.cloud.ibm.com
Frankfurt: https://eu-de.ml.cloud.ibm.com
Tokyo: https://jp-tok.ml.cloud.ibm.com

```py
watsonx_url = "https://us-south.ml.cloud.ibm.com" #ダラスの場合
```

### 6. 必要ライブラリーのImport
```py
import pandas as pd
from langchain.schema.document import Document
import json
from langchain_milvus import Milvus
import os
from ibm_watsonx_ai.metanames import EmbedTextParamsMetaNames
from langchain_ibm import WatsonxEmbeddings
from ibm_watsonx_ai.foundation_models.utils.enums import EmbeddingTypes

```

### 7. Embeddingモデルの取得
[Part1: Excelをベクトル化してベクトルDB Milvusに入れよう](https://qiita.com/nishikyon/items/81a6de44aaf52f49eb0f/)の11と全く同じです。
ここでは`intfloat/multilingual-e5-large`を使います

- https://huggingface.co/intfloat/multilingual-e5-large
- https://dataplatform.cloud.ibm.com/docs/content/wsj/analyze-data/fm-models-embed.html?context=wx&locale=ja#multilingual-e5-large

LangChainで使用できるwatsonx.aiのEmbeddingモデルintfloat/multilingual-e5-largeがあるので、今回はこちらを使用します:
- https://python.langchain.com/docs/integrations/text_embedding/ibm_watsonx/


> 尚、`intfloat/multilingual-e5-large`はオープンソースで公開されているので、watsonx.aiのEmbeddingモデルを使用しなくとも、ローカルにダウンロードすることで使用可能です。 その場合のコードはこちらです(今回は使用しません)
> ```py
> from langchain_huggingface import HuggingFaceEmbeddings
>from tqdm.autonotebook import tqdm
>
>embeddings = HuggingFaceEmbeddings(model_name="intfloat/multilingual-e5-large")
>```

<br>
下記は`apikey`はMilvusと同じものを使用しています。異なる場合は変数`apikey`を変更してください。

```py
# watsonx.aiのEmbeddingモデル取得
embed_params = {
    EmbedTextParamsMetaNames.TRUNCATE_INPUT_TOKENS: 512,
    EmbedTextParamsMetaNames.RETURN_OPTIONS: {"input_text": True},
}

embeddings = WatsonxEmbeddings(
    model_id="intfloat/multilingual-e5-large",
    url=watsonx_url,
    apikey=apikey,
    project_id=project_id
    )
```

### 8. ベクトルDB Milvusに接続
```py
from langchain_milvus import Milvus

vector_store = Milvus(
    embeddings,
    connection_args =my_connection_args,
    collection_name = my_collection
)
```

**ここからが、Part1とPart2とは異なる内容です。**

### 9. watsonx.ai LLMの取得

回答を生成するLLMを取得します。
今回は[mixtral-8x7b-instruct-v01](https://www.ibm.com/docs/ja/watsonx/saas?topic=solutions-supported-foundation-models#mixtral-8x7b-instruct-v01)を使用します。

Embeddingモデルと同様に下記は`apikey`はMilvusと同じものを使用しています。異なる場合は変数`apikey`を変更してください。

```py
# watsonx.ai LLM: 必要なライブラリーのImport
from ibm_watsonx_ai.metanames import GenTextParamsMetaNames as GenParams
from langchain_ibm import WatsonxLLM

# 使用するLLMのパラメータ
generate_params = {
    GenParams.MAX_NEW_TOKENS: 16384,
    GenParams.MIN_NEW_TOKENS: 0,
    GenParams.DECODING_METHOD: "greedy",
    GenParams.REPETITION_PENALTY: 1
}

# LangChainで使うllm
custom_llm = WatsonxLLM(
    model_id="mistralai/mixtral-8x7b-instruct-v01",
    url=watsonx_url,
    apikey=apikey,
    project_id=project_id,
    params=generate_params,
)
```

### 10. ベクトルデータベースからLangChain Retrieverの作成
参考: LangChain ドキュメント: [Retrievers](https://python.langchain.com/docs/concepts/retrievers/)

Retrieverは検索のレスポンスを返すlangchainのInterfaceです。
主にベクトルストアから作成可能ですが、Wikipedia検索などからでも作成できます。
Retrieverを使うとLLMと組み合わせたRAG構成がLangChainで簡単にできます。

「[Part2: ベクトルDB Milvusに入ったデータで類似検索してみよう!](https://qiita.com/nishikyon/items/9531d3cbbc893822e494/)」で使用したvector storeとの違いは以下です:

 - vector storeは1つ種類のベクトルDBに特化したもの。今回はMilvusのvector storeを使用しています。
- RetrieverはベクトルDBおよび他の検索システムに汎用的に使用できるインターフェースであり、query文字列を入力としLangchainのDocumentのListを出力する単純なものです。Chainを構成することができるため、RAG構成でよく使用されます。

ここではいろいろな条件のRetrieverをベクトルDBから作成してみます。

#### 10-1. as_retriever: オプションなし デフォルト構成
vector_store.as_retriever()でretrieverが取得できます。
retriever.invoke("検索したい文字列")で類似検索が可能です。類似度が高い順に4件出力されます。
戻り値は[Document](https://python.langchain.com/api_reference/core/documents/langchain_core.documents.base.Document.html)形式となります。

```py
# オプションなし デフォルト構成
query = "IBM TechXchange Japanとは?"
retriever = vector_store.as_retriever()

# retriever を使用したベクトルDBの類似検索
docs = retriever.invoke(query)

for doc in docs:
    print({"content": doc.page_content[0:100], "metadata": doc.metadata} )
    print("---------")
```
上記は最後に戻り値のDocumentのpage_contentの最初100文字(=Milvusコレクション上のtextの最初100文字)とmetadataを表示しています。
上記の出力:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294516/a960fb54-95f8-9afb-ab1d-ea62f69f85cd.png)


#### 10-2. as_retriever: 結果の取得数をkで指定(デフォルトは4)
引数kを追加して件数を指定します。

```py
# 結果の取得数をkで指定(デフォルトは4)
retriever = vector_store.as_retriever(search_kwargs={"k": 10})
docs = retriever.invoke(query)

for doc in docs:
    print({"content": doc.page_content[0:100], "metadata": doc.metadata} )
    print("---------")
```
k=10と指定したので類似度が高い順に10件出力されます。
上記は10-1と同様に最後に戻り値のDocumentのpage_contentの最初100文字(=Milvusコレクション上のtextの最初100文字)とmetadataを表示しています。
上記の出力:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294516/7d6d1085-656d-bb42-7c1a-5f2ccdf89fef.png)

#### 10-3.類似スコアの閾値を設定し、その閾値以上のスコアを持つ文書のみを返す

angchain_milvus.Mivusにはsearch_type="similarity_score_threshold"の設定がありません。
つまり [Similarity score threshold retrieval](https://python.langchain.com/docs/how_to/vectorstore_retriever/#similarity-score-threshold-retrieval) はlangchain_milvus v2.4.8では使えません。

しかし、やはり正確な回答を得るためには類似スコアの閾値を設定したいです。

vectorstore.similarity_search_with_scoreのscore値を使用して自分で「閾値以上の類似スコアを持つ文書のみを返す」CustomRetrieverを作成すれば可能なので、作成してみます。

##### 10-3-1. CustomRetrieverの作成
閾値以上の類似スコアを持つ文書のみを返す」CustomRetrieverを作成します。
引数score_thresholdで閾値を指定し、引数kで件数も指定できるようにしています。

```py
# 閾値以上の類似スコアを持つ文書のみを返す」CustomRetrieverを作成
# asyncの方は省略
from langchain.schema.vectorstore import VectorStoreRetriever
from langchain.schema.document import Document
from langchain.callbacks.manager import (
    CallbackManagerForRetrieverRun,
)
from typing import List

class CustomRetriever(VectorStoreRetriever):
    def _get_relevant_documents(
        self, query: str, *, run_manager: CallbackManagerForRetrieverRun
    ) -> List[Document]:
        top_k = self.search_kwargs.get("k", 4)
        docs_and_similarities = self.vectorstore.similarity_search_with_score(query, k=top_k)
                
        threshold = self.search_kwargs.get("score_threshold", 0)
       
        return [doc for doc, score in docs_and_similarities if score >= threshold and score <= 1]
```

使い方は以下になります。
retrieverは作成したCustomRetriever()で取得します。
vectorstoreでvectorstoreを指定、search_kwargsでscore_thresholdとkを指定しています。
```py
# 類似スコア 0.85以上のもの、　上位最大5件
retriever = CustomRetriever(
    vectorstore = vector_store,
    search_kwargs={"score_threshold": 0.85, "k":5},
)
docs = retriever.invoke(query)
# print(docs)
for doc in docs:
    print({"content": doc.page_content, "metadata": doc.metadata} )
    print("---------")
```
上記の出力:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294516/2abbebe4-b789-1001-b3ad-7434f53a77ef.png)
k=5としていますが、類似スコア 0.85以上のものは1件しかなかったようです。

### 11. ベクトル・データベース ＋ 生成AI(LLM)のRAG構成で質問に回答してみよう
プロンプトを準備してベクトル・データベースの検索結果を使って質問に回答してみましょう。
ベクトル・データベースの検索結果取得には10-1で作成したRetrieverを使います。
watsonx.ai LLMは9で取得しています。
LangChainのchainという機能を使って、組み合わせます。

```py
from langchain.prompts import PromptTemplate
from langchain.schema.runnable import RunnablePassthrough

# retrieverは単純なデフォルトのものにします（変えてもOK）
retriever = vector_store.as_retriever()

# Prompt Templateを設定します。これはmixtral-8x7b-instruct-v01に合わせて作成してあります
template = """
 
 
あなたは親切で、礼儀正しく、誠実なアシスタントです。常に安全を保ちながら、できるだけユーザーの役に立つように詳しく回答してください。
回答には、有害、非倫理的、⼈種差別的、性差別的、有毒、危険、または違法なコンテンツを含めてはいけません。回答は社会的に偏⾒がなく、本質的に前向きなものであることを確認してください。
質問が意味をなさない場合、または事実に⼀貫性がない場合は、正しくないことに答えるのではなく、その理由を説明してください。質問の答えがわからない場合は、誤った情報を共有しないでください。
questionに答えるために、以下のcontextを使用し必ず日本語でanswerを作成してください。
必ず⽇本語の文章で回答してください。知ったかぶりをしないでください。
回答を書くときは、context内の単語をできるだけ使⽤してください。context中に質問に対する回答がない場合は、「文書中に質問に対する回答が明記されていません。」とだけ回答してください。「文書中に質問に対する回答が明記されていません。」と回答した場合、そこで回答を終わりにしてください。
contextの内容がブランクの場合、「文書中に質問に対する回答が明記されていません。」とだけ回答してください。
セッションについて回答する場合は、タイトル、開始時刻、終了時刻、会場、概要、レベルを回答してください。回答が200文字以上の場合、回答はなるべく箇条書きを含めてわかりやすく回答してください。





 
 質問が質問の文章ではなく意味がわからない場合
もう少し詳しく説明していただけますか？


context: {context}
question: {question}
answer: 
"""

# Prompt Templateを作成します
rag_prompt = PromptTemplate.from_template(template)

# chainを作成します

rag_chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | rag_prompt
    | custom_llm
)
```

ではTechXchangeに関する質問をしてみましょう！
`rag_chain.invoke(<質問文>)`　でRAGの仕組みでベクトル検索の結果から生成AIが文章を作成し回答します。

お好みの質問文で置き換えるか、コメント`#` を外して質問してみてください。

```py
query="RAGに関するセッションの詳細を教えてください"
# query="watsonx.dataに関するセッションの詳細を教えてください"
# query="TechXchangeについて概要を教えてください"
# query="量子コンピューター関連のセッションを教えてください"
print(rag_chain.invoke(query))
```
出力:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294516/890e1e01-5fe4-07be-f399-68541071a1dc.png)

いい感じの回答になりました！

試しに情報をベクトルDBに入れていない、TechXchangeとは関係ない一般的な質問をしてみましょう！
```py
query="日本の首都はどこですか?"
print(rag_chain.invoke(query))
```
出力:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294516/68a6772b-185e-1e63-11ad-b37ab45bcc10.png)

ベクトルDBに入れていない情報は回答しないようにプロンプトで指示しているので、回答しません。これで不正確な回答が防げます。
ただしLLMによってそのような指示を書いても、無視するものもあったり、書き方で回答が変わったりするので、テストでプロンプトを調整したり、必要に応じて不要な回答をなくす後処理を追加してください。


以上で「Part3: ベクトルDB Milvusとwatsonx.ai LLMでRAGを構成して、質問をしてみよう!」は完了です。

次の「[Part4: ベクトルDB Milvusとwatsonx.ai LLMでRAGを構成して、チャットアプリを作成してみよう!](https://qiita.com/nishikyon/items/7cd7949cd993cc449ca9/)」に進んでください。