Oracle Database 23ai + LangChainを使いローカル環境のみでRAGを試してみた

23aiがリリースされて間もなく、LangChainがサポートするVector StoreにOracle Databaseが追加されました。
+ [Oracle AI Vector Search: Vector Store](https://python.langchain.com/v0.1/docs/integrations/vectorstores/oracle/)
+ [langchain_community.vectorstores.oraclevs.OracleVS](https://api.python.langchain.com/en/latest/vectorstores/langchain_community.vectorstores.oraclevs.OracleVS.html#langchain_community.vectorstores.oraclevs.OracleVS)

これで23aiから追加されたAI Vector Searchを、LangChainを通して活用頂けます。
という訳で早速23ai FreeとLangChainを組み合わせてRAGの動作確認をしてみました。
LLMとEmbeddingモデルですが、いずれもローカル環境で動作させて外部サービスを使わない構成としました。

## 検証環境

+ VM: OCI VM.Standard3.Flex (3 OCPU、48GB RAM)
+ OS: Oracle Linux Server release 8.9
※GPUは使っていません。。

## 事前準備

### Oracle Database 23ai Freeのインストール

Oracle Database 23ai Freeをインストールし、スキーマを作成します。
手順は下記をご参照ください。
+ [Oracle Database 23ai Freeインストール](https://qiita.com/ssfujita/items/34d9e3c39ca730d29351#oracle-database-23ai-free%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)
+ [アプリに使うスキーマの作成](https://qiita.com/ssfujita/items/34d9e3c39ca730d29351#%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AB%E4%BD%BF%E3%81%86%E3%82%B9%E3%82%AD%E3%83%BC%E3%83%9E%E3%81%AE%E4%BD%9C%E6%88%90)
    - ネットワークACL設定、ディレクトリ・オブジェクト作成はSkipしてください。

### パッケージのインストール

必要な各種パッケージをインストールします。
ローカル環境でLLMを稼働させるのに必要なOSパッケージをインストールします。

```bash
$ sudo dnf install -y gcc-c++
```

Python環境を準備します。
Python仮想環境を作るにあたり、今回はAnacondaを導入します。

```bash
# Anacondaインストール
$ mkdir -p ~/miniconda3
$ wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda3/miniconda.sh
$ bash ~/miniconda3/miniconda.sh -b -u -p ~/miniconda3

# 不要ファイル削除
$ rm -rf ~/miniconda3/miniconda.sh

# Anaconda初期セットアップ
$ ~/miniconda3/bin/conda init bash
```

Python仮想環境を作成し、アクティベートします。

```bash
$ conda create -n 23ailc python=3.12

# 仮想環境「23ailc」のアクティベート
$ conda activate 23ailc
```

Pythonパッケージをインストールします。

```bash
$ pip install langchain langchain-community llama-cpp-python oracledb pypdf python-dotenv sentence-transformers
```

gguf形式でexportされたLLMをダウンロードします。
LLMは日本語の扱いに長けた「vicuna-13b-v1.5」の量子化版を使います。

```bash
wget https://huggingface.co/TheBloke/vicuna-13B-v1.5-GGUF/resolve/main/vicuna-13b-v1.5.Q4_K_M.gguf
```

## ドキュメントEmbeddingの実装

ローカルの特定ディレクトリに配置したPDFファイルを読み込み、Embedding結果をOracle Database 23aiに格納します。

```python
import os
from dotenv import load_dotenv
import oracledb
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import oraclevs
from langchain_community.vectorstores.oraclevs import OracleVS
from langchain_community.vectorstores.utils import DistanceStrategy
from langchain_core.documents import Document

# 環境変数 (DB接続情報) の読込
load_dotenv()

# Oracle DB接続情報
user = os.environ['username']
pwd = os.environ['password']
dsn = os.environ['service']

# ベクトルデータ格納先テーブル名
table_name = "ovs"

# embeddingモデルの読み込み
embedding_model = HuggingFaceEmbeddings(
    model_name="intfloat/multilingual-e5-large"
)

# PDFファイルの読み込み
dirname = "docs"
files = []

for filename in os.listdir(dirname):
    full_path = os.path.join(dirname, filename)
    if os.path.isfile(full_path):
        files.append({"name": filename, "path": full_path})

# テキスト抽出と分割、ベクトル化、保存
lc_docs = []
for file in files:
    # PDF読み込み
    if file["path"].endswith(".pdf"):
        loader = PyPDFLoader(file["path"])
    else:
        raise ValueError(f"Unsupported file format: {file['path']}")

    # テキスト抽出
    pages = loader.load_and_split()

    # テキスト分割
    text_splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=40)
    docs = text_splitter.split_documents(pages)

    # DBに保存(途中で改行コードを除去)
    for doc in docs:
        lc_docs.append(Document(page_content=doc.page_content.replace("\n", ""), metadata={'source': file["path"]}))

# Oracle Vector Databaseに接続
with oracledb.connect(user = user, password = pwd, dsn = dsn) as connection:
    # テーブル「ovs」にEmbedding結果を保存
    ovs = OracleVS.from_documents(
        lc_docs,
        embedding_model,
        client=connection,
        table_name=table_name,
        distance_strategy=DistanceStrategy.COSINE,
    )

    # ベクトル型の列にIVF索引を作成
    oraclevs.create_index(connection, ovs, params={"idx_name": "ivf_idx1", "idx_type": "IVF"})
```

`load_dotenv()`で`.env`ファイルからDB接続情報を読み込んでいますが、`service`に指定する文字列は簡易接続を想定しています。
サービス名を指定する場合は`oracledb.connect`の引数に`config_dir`の指定が必要となります。

また25～27行目 のところで Hugging Face よりEmbeddingモデル「intfloat/multilingual-e5-large」をダウンロードし、ローカルで稼働させます。
2回目以降はダウンロード済みモデルを流用する内部動作となっています。
上記を例えば「ovs_embed.py」という名前で保存し、以下のように実行します。
実行する際は、コードを配置している階層に「docs」というディレクトリを作成し、そこにEmbeddingしたいPDFファイルをいくつか事前に格納しておきます。
本検証では「DXレポート」のサマリー資料を利用させて頂きました。
https://www.meti.go.jp/shingikai/mono_info_service/digital_transformation/pdf/20180907_01.pdf

```bash
$ python ovs_embed.py
```

上記を実行すると、LangChainによって `vector` スキーマに `OVS` という名前のテーブルが作成されます。
※テーブル名は任意に変更可能です。

```sql
SQL> desc OVS
 名前                                    NULL?    型
 ----------------------------------------- -------- ----------------------------
 ID                                        NOT NULL RAW(16)
 TEXT                                               CLOB
 METADATA                                           CLOB
 EMBEDDING                                          VECTOR(1024, FLOAT32)
```

こちらのテーブルにEmbedding対象テキスト、Embedding結果、ユーザ指定のメタデータ情報が格納されます。

## RAG実装

先ほど保存したEmbedding結果を活用し、RAGを試します。
Embeddingモデルはダウンロードした「intfloat/multilingual-e5-large」を流用します。
LLMは事前準備でダウンロードした「vicuna-13b-v1.5.Q4_K_M.gguf」を使います。

```python
import os
from dotenv import load_dotenv
import oracledb
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores.oraclevs import OracleVS
from langchain.chains import RetrievalQA
from langchain_community.llms.llamacpp import LlamaCpp
from langchain_core.prompts import PromptTemplate
from langchain_community.vectorstores.utils import DistanceStrategy
from langchain_core.documents import Document

load_dotenv()

# Oracle DB接続情報
user = os.environ['username']
pwd = os.environ['password']
dsn = os.environ['service']

# ベクトルデータ格納先テーブル
table_name = "ovs"

# embeddingモデルの読み込み
embedding_model = HuggingFaceEmbeddings(
    model_name="intfloat/multilingual-e5-large"
)

# ダウンロード済みLLMの絶対パス
model_path = "path_to_llm_dir/vicuna-13b-v1.5.Q4_K_M.gguf"

# モデルの読み込み
llm = LlamaCpp(
    model_path=model_path,
    n_ctx=2048,
    max_tokens=4096
)

# Oracle Vector Databaseに接続
with oracledb.connect(user = user, password = pwd, dsn = dsn) as connection:
    ovs = OracleVS(client=connection, embedding_function=embedding_model, table_name=table_name, distance_strategy=DistanceStrategy.COSINE)

    # プロンプト・テンプレートの定義
    template = """以下の文脈を利用して、最後の質問に簡潔に答えてください。答えがわからない場合は、わからないと答えてください。
{context}
質問: {question}
回答（日本語）:"""

    # プロンプトの設定
    question_prompt = PromptTemplate(
        template=template, # プロンプトテンプレートをセット
        input_variables=["question"] # プロンプトに挿入する変数
    )

    # RAG設定
    qa = RetrievalQA.from_chain_type(
        llm=llm,
        chain_type="stuff",
        retriever=ovs.as_retriever(search_kwargs={'k': 5}),
        return_source_documents=True,
        chain_type_kwargs={"prompt": question_prompt}
    )

    # 質問文
    question = "DXレポートのポイントを経営者の視点でまとめてください。"

    # LLMの回答生成
    response = qa.invoke(question)

    # 結果表示
    print("質問: "+response['query'])
    print("回答: "+response['result'])

    # 引用元表示
    print("引用元:")
    for citation in response['source_documents']:
        print("------------------")
        print(citation.page_content)
        print(citation.metadata['source'])
```

ベクトル検索の結果は上位5件を活用する設定にしています。
結果出力のパートでは、回答に加えて引用したテキストとファイル名を表示するようにしています。
上記を例えば「rag.py」という名前で保存し、以下のように実行します。

```bash
$ python rag.py
```

結果は以下のようになりました。

```text
質問: DXレポートのポイントを経営者の視点でまとめてください。
回答:  DXレポートは、デジタルトランスフォーメーション（DX）が企業においてどのような現状や課題があるかを整理したものであり、その中で経営者の視点から以下のポイントが挙げられます。

1. 事業部門ごとに構築されている既存システムが全社横断的なデータ活用ができず、過剰なカスタマイズがなされていることから複雑化・ブラックボックス化している。
2. 既存システムの問題を解決するためには、業務自体の見直しも求められる中で経営改革そのものが必要。
3. DXを実現することで、2030年実質GDP130兆円超の押上げを実現。ユーザーは技術的負債を解消し、デジタル技術の活用にシフトし、新たなビジネス創出が可能となる。
4. ベンダー企業は先課題として、計画的なシステム刷新を断行することが求められる。不要なシステムの廃棄やマイクロサービスの活用により段階的な刷新を行い、リスクを低減し、ブラックボックス状態を解消する必要がある。
5. システム刷新の計画的な進行により、2021年から2025年にかけて、経営戦略を踏まえたシステム刷新が最優先課題とされることが予想される。
引用元:
------------------
（＝DX ）の必要性について理解しているが・・・・既存システムが、 事業部門ごと に構築されて、全社横断的なデータ活用ができなかったり、 過剰なカスタマイズ がなされているなどによ り、複雑化 ・ブラックボックス化・経営者が DXを望んでも 、データ活用のために上記のような 既存システムの問題を解決し 、そのためには 業務自体の見直しも求められる 中（＝経営改革そのもの）、
docs/20180907_01.pdf
------------------
なものについて刷新 しつつ、DXを実現 すること により、 2030 年実質 GDP130 兆円超の 押上げ を実現。2030年ユーザ：技術的負債を解消し、人材・資金を維持・保守業務から新たなデジタル技術の活用にシフトデータ活用等を通じて、スピーディな方針転換やグローバル 展開への対応を可能にデジタルネイティブ世代の人材を中心とした新ビジネス創出へベンダー：
docs/20180907_01.pdf
------------------
先課題とし、計画的なシステム刷新を断行（業種・企業ごとの特性に応じた形で実施）不要なシステムの廃棄、マイクロサービスの活用による段階的な刷新、協調領域の共通プラットフォーム活用等により、リスク を低減ブラックボックス状態を解消し既存システム上のデータを活用した本格的な DXが可能に↓新たなデジタル技術を導入し、迅速な ビジネス・モデル変革
docs/20180907_01.pdf
------------------
システム刷新：経営判断／先行実施期間【～2020】「見える化」指標による 診断・仕分け「DX推進システムガイドライン」を踏まえたプランニングや体制構築システム刷新計画策定共通プラットフォームの 検討等システム刷新集中期間（ DXファースト期間 ）【2021 ～2025】経営戦略を踏まえたシステム刷新を経営の最優先課題とし、計画的なシステム刷新を断行
docs/20180907_01.pdf
------------------
ムとすべく、既存システムの刷新が 必要DXを実行する上での現状と課題「2025年の崖」、「 DX実現シナリオ」をユーザ企業・ベンダー企業等産業界全体で共有し、政府における環境整備を含め、諸課題に対応しつつ、 DXシナリオを実現。DXの推進に向けた対応策について２「DX推進システムガイドライン」の策定•既存システムの刷新や新たなデジタル技術を活用するに 当たっての「体制のあり
docs/20180907_01.pdf
```

引用テキストが回答に盛り込まれ、サマライズされてる感じですね。
今回はLLMもEmbeddingモデルもローカル環境で動作するものを使いましたが、思ったより使えそうな印象です。
ただGPUを搭載していないサーバで試したため、回答に数分要していました。
個人で遊ぶ程度なら問題ないですが、利用規模を大きくするなら当然GPUは必須です。