ローカルLLMと構造化を活用したRAGを構築

### この記事について
ローカルLLMを使用したRAGを構築する方法について共有します。
本記事中の構造化は**DNP ドキュメント構造化AI**を利用したものです。
構造化については以下の記事で詳しく説明されています。

https://qiita.com/dnp-nishikawa/items/3a9723a4666342cc4015#dnp-%E3%83%89%E3%82%AD%E3%83%A5%E3%83%A1%E3%83%B3%E3%83%88%E6%A7%8B%E9%80%A0%E5%8C%96ai-%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6

### 背景
例えば以下のような理由からローカル環境でシステムを構築することがあります。
* データ流出リスク
* 可用性
* 規制対応
* レイテンシ―

RAGについてもローカルで処理が完結することを想定したシステムも必要になるのではないか、ということで検討した内容を共有します。
どのような構成にしていくか検討する際の参考になりましたら幸いです。

### 目的
ローカルLLMを使ったRAG環境の構築確認を行う。

### 環境
今回は以下のような構成にしました。
あいにく手元にサーバーが無いため、ローカルと言いながらインフラはクラウド環境を利用します。Amazon EC2のGPUインスタンスを利用します。

### 構成図
サーバー1台の中に詰め込むので基本的な構成はシンプルです。

```mermaid
flowchart TD
    %% === コンポーネント定義 ===
    ChatApp[fa:fa-robot チャット<br>アプリケーション]
    EmbeddingModel[エンべディングモデル]
    LocalLLM[ローカルLLM]
    VectorDB[(fa:fa-database ベクトルデータベース)]

    subgraph M["サーバー"]
        direction LR
        ChatApp --- VectorDB
        ChatApp --- EmbeddingModel
        ChatApp --- LocalLLM
    end
    
    %% === スタイル設定 ===
    style ChatApp fill:#bbdefb,stroke:#1976d2,stroke-width:2px
    style LocalLLM fill:#f8bbd0,stroke:#c2185b
    style VectorDB fill:#d1c4e9,stroke:#512da8
    style EmbeddingModel fill:#ffe0b2,stroke:#f57c00
```

### 構成要素
AWS上に構築
| 項目 | 内容 |
|:-:|:--|
| サーバー | EC2（g4dn.xlarge / g6.xlarge） |
| 開発言語 | Python |
| ベクトルDB | PostgreSQL(pgvector) |

### モデル
モデルは以下を利用しました。
* SakanaAI/TinySwallow-1.5B-Instruct
* google/gemma-3-1b-it
* google/gemma-3-4b-it
* intfloat/multilingual-e5-small（embedding用）

g4dn.xlargeインスタンスは、NVIDIA T4 GPUを搭載していることから約16GBほどVRAMを利用可能ということで、上記のようなパラメータ数の少ないモデルには十分でした。
すべてのモデルを同時に動作させるには今回の条件ではややスペック不足となるため、適宜リソースを解放してモデルを切り替える必要があります。

なお、量子化したモデルは使用しませんでした。

### ベクトルデータベース
pgvectorの構築はインストール済みのdockerイメージを使いました。

### 処理イメージ
動作確認用にチャットボットの仕組みを作成しましたが、評価はバッチで処理しました。

```mermaid
graph TD
    subgraph "実行時フロー"
        A[ユーザー] -- 質問入力 --> B(Streamlitアプリ);
        B -- 回答表示 --> A;

        B -- [1]質問をベクトル化 --> D[埋め込みモデル];
        D -- ベクトル --> B;
        B -- [2]クエリ（ベクトル） --> E[[ベクトル検索]];
        E --- G[(PostgreSQL / pgvector)];
        G -- 関連性の高いコンテキスト --> B;
        B -- [3]プロンプト作成<br> & LLMに送信 --> F[LLM];
        F -- 回答生成 --> B;
    end

    subgraph "データ準備 (事前処理)"
        H[元データ] --> I[[テキスト分割]];
        I --> J[埋め込みモデル];
        J --> K[ベクトルデータ];
        K & I -- 保存 --> G;
    end

    %% スタイリング
    style B fill:#ffc,stroke:#333,stroke-width:2px
    style F fill:#f9f,stroke:#333,stroke-width:2px
    style G fill:#cde4ff,stroke:#333,stroke-width:2px
    style D fill:#d4fada,stroke:#333,stroke-width:2px
    style J fill:#d4fada,stroke:#333,stroke-width:2px
```

動作確認用のアプリはStreamlitで作成しました。ほぼ生成したコードで作成しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4098576/c07d53fb-9fe8-4769-981f-4a0044a9ab50.png)

### データ
弊社の決算短信のデータを構造化してからチャンクに加工して、ベクトルデータベースに投入しました。

> 大日本印刷株式会社
2023年3月期 決算短信〔日本基準〕(連結)
参考：https://www.dnp.co.jp/ir/library/result/

開発者の視点では、PDFからテキスト抽出したものを直接チャンクにするのではなく、一度構造化しておくと、パースしやすくなったり、チャンクの加工方法を変更する際に扱いやすいです。

### テスト
質問を入力して、回答を確認しました。
| 質問 | 回答 |
|:--|:--|
| 2023年3月期において、DNPグループはどのような事業に注力しましたか？ | 2023年3月期において、DNPグループは「Iot・次世代通信」「データ流通」「モビリティ」「環境」関連のビジネスを「注力事業」と定めました。 |

該当箇所はこちら
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4098576/a4ef83d4-35ef-4b22-a2b7-cefa769a63dc.png)


### 評価
評価用のデータはLLMに質問応答リストを作成させて、人間の目で確認したものを使用しました。評価にはragasを利用しスコアを取得して、スコアの低いチャンクを分析し、原因を特定しました。
例えば、チャンクの中身を調整することで、回答率が40%から90%に改善することを確認しました。

### 所感
内容によると思いますが、ragasによる評価は1度の処理に1時間以上掛かりました。分析も人間の目で確認すると手間が掛かりますが、目検では限界があるので数値である程度判断できるのはありがたいです。

継続的なモニタリングや運用を考えると、ユースケースによってはLLMOpsのような環境の構築も一緒に考えたり、分析環境が無い場合、可能なら分析はクラウド上で実施するなど、ハイブリッドな使い分けも検討する必要があるなと思いました。