Dify×Weaviateを使ったRAG

# 記事の概要

Difyの登場により、LLMを使ったワークフローがローコードで構築できるようになったので、「Weaviate」というベクトルDBと「intfloat/multilingual-e5-large」というエンベディングモデルを使ったベクトル検索と、Groqが提供しているllama 3のAPIを使って、RAG（Retrieval-Augmented Generation）のワークフローを構築してみる。
（ワークフロー構築時、GroqはAPIが無料なので、これを利用する。）

このワークフローでは、以下のような入力を実行すると、ベクトルDBの情報（今回は個別に用意したQAリスト）を参考に、テキストを出力することができる。

■入力
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619695/a8912fd9-d79e-5496-1268-19d73aaff641.png)

■出力
![RAG_output.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619695/2a1b93fa-81e2-bd6a-c13f-3134a94428f9.png)

動画でも解説しています。

https://youtu.be/eO8dpAgLKVM

# 環境構築

以下のようなフローを構築する。

```mermaid
graph TB;
    subgraph local
        Dify
        Weaviate
        venv
    end

    subgraph groq[Groq Cloud]
        llama3[llama 3]
    end

    subgraph Dify[Dify Docker]
        q[質問]
        vector[ベクトル検索]
        tune[後処理]
        RAG[RAG]
        output[回答]
    end

    subgraph Weaviate[Weaviate Docker]
        vectorDB
    end

    subgraph venv[python仮想環境]
        flask
    end

    q --> vector
    vector --> tune
    tune --> RAG
    RAG --> output

    vector -.-> |HTTPリクエスト|flask
    flask -.-> |ベクトル検索|vectorDB
    vectorDB -.-> flask
    flask -.-> vector

    RAG -.-> |テキスト生成|llama3
    llama3 -.-> RAG

```

Dify上だと以下のようなフローとなる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619695/41c98a68-2dbe-eeb2-4e88-9b469bedcb02.png)

Docker・Dify・Weaviateの各種構築手順は、以下などを参考にしてください。

### Dify構築手順

https://qiita.com/sey323/items/8921bb767e2b0656f1cc

### Weaviate構築手順

https://qiita.com/kongo-jun/items/8be61733db5b3c810866



### Groq Cloud

LLMについてはGPT-4oでもClaude 3 Opusでも問題ないが、今回は無料であることと、レスポンスの速さから、Groqのllama 3を選定する。

https://console.groq.com/playground

API keyは以下から発行できる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619695/36c8d4a0-681b-a70a-c7d2-6179345bd58d.png)


# 各種設定

Difyのワークフローをベースに各種設定について解説する。

```mermaid
graph TB;
    subgraph Dify[Dify Docker]
        q[1.質問]
        vector[2.ベクトル検索]
        tune[3.後処理]
        RAG[4.RAG]
        output[5.回答]
    end

    q --> vector
    vector --> tune
    tune --> RAG
    RAG --> output
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619695/41c98a68-2dbe-eeb2-4e88-9b469bedcb02.png)

### 1.質問

Dify上の設定は以下となる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619695/e8372ffc-4b10-c6df-64d6-8fe5fc7c27df.png)

### 2.ベクトル検索

Difyからpython仮想環境に対して、HTTPリクエストを送信し、pythonでWeaviateへベクトル検索を実行する。

![Dify_2_ベクトル検索.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619695/275c6344-778b-7034-01c4-95a108538c4e.png)

メソッド名：POST

URL：
```
http://192.168.XXX.XXX:5000/embedding
```

「192.168.XXX.XXX」にはipconfigなどで「IPv4 アドレス」を入力する。


ボディ：
```json
{
  "text": "{{#00000000000.input_text#}}",
  "target_class": "{{#00000000000.target_class#}}",
  "certainty": {{#00000000000.certainty#}},
  "record_limit": {{#00000000000.record_limit#}}
}
```
「#00000000000」のような表記になっているが、登録するときはDifyのキャプチャのような形式で記載する。


### ベクトルDBへの接続

以下の手順で、Weaviateを構築している前提とする。

https://qiita.com/kongo-jun/items/8be61733db5b3c810866

以下のフォルダ構成となるように「source」フォルダを作成し、「source」フォルダに各種ファイルを配置する。

```
embedding_env/
├── source/
│   ├── text_list.csv
│   ├── insert_class.py
│   ├── select_class.py
│   ├── flask_select_class.py
│   └── drop_class.py
└── Scripts/
    └── activate
```

仮想環境をactivateしている状態で、以下のコマンドを実行する。

```powershell
pip install pandas
pip install flask
```

以下はベクトルDBに登録するデータ。

```csv:text_list.csv
id,question,answer
1,query: あなたの名前は？,ネムル
2,query: 好きなものは？,寝ること
3,query: 好き食べ物は？,芋
4,query: 元気がでるのは？,寝ること
5,query: 最近うれしかったことは？,今日晴れてたこと
6,query: 歳は？,知らない
7,query: 出身は？,ヨネズのコロニー
8,query: 好きな人は？,みはる
9,query: 嫌いなことは？,頑張ること
10,query: 得意な勉強は？,勉強とかできない
11,query: 朝型か夜型か？,特にない
12,query: 普段、何をしていますか？,だいたい寝てるかも
13,query: 趣味は？,寝る以外特にない
14,query: 最近読んだ本や漫画は？,本？
15,query: 印象に残っている旅行は？,シティより遠くに行ったことない？
16,query: 最近挑戦したことは？,しゃべること
17,query: 最近感動したことは？,最近暖かくて寝心地がいい
18,query: 最近見たドラマや映画は？,映画？
19,query: 運動は何が得意？,運動は苦手
20,query: 好き飲み物は？,雨水は嫌いだけど、水
21,query: 季節の中で好きなのは？,春が好き
22,query: お気に入りの音楽は？,あんまり知らない
23,query: 誰と一緒に住んでいますか？,最近はみはる
24,query: テレビ番組はよく見ますか？,テレビって何？
25,query: 異性の好みは？,異性って誰の事？
26,query: お気に入りの映画ジャンルは？,映画って何？
27,query: 空いた時間はどう過ごしますか？,寝てる
28,query: 今の生活で満足していますか？,特に不満はない
29,query: 将来の夢は？,ずっと寝ること
30,query: 自分を色で表すと？,茶色
31,query: 一番長く話したことは何ですか？,あんまり話せない
32,query: 友達は多いですか？,少ない
33,query: どんな映画が嫌い？,映画が何かわからない
34,query: 絵を描くことは好きですか？,絵はあまり得意じゃない
35,query: 好きな季節のイベントは？,晴れてる春
36,query: 休日の過ごし方は？,寝てる
37,query: 何色の服をよく着ますか？,茶色しかない
38,query: 最後に笑ったことは？,みはるが川でひっくり返ったとき
39,query: 携帯電話はよく使いますか？,携帯って何かわからない
40,query: 自分の長所と短所は？,一人じゃ何もできない
41,query: 自分を動物に例えると？,ダンゴムシ
42,query: 好きなスポーツは？,スポーツが何かわからないよ
43,query: 自炊はしますか？,まったくしない
44,query: 好きなお菓子は？,虫の幼虫
45,query: 最近買ったものは？,買うことなんてない
46,query: 好きなアニメはありますか？,アニメ？
47,query: パソコンは使いますか？,みはるがよく使ってるけど、ネムルはよくわからない
48,query: お気に入りのレストランは？,レストランとかはない
49,query: 自分の性格を一言で表すと？,寝坊助
50,query: 最近感じた小さな幸せは？,昼まで寝れた時
51,query: 目標は何ですか？,特にない
52,query: 好きな花は何ですか？,食べれる花か臭くない花
53,query: 最近行った場所は？,近くの川
54,query: 好きな言葉は？,おやすみ
55,query: 今一番欲しいものは？,やわらかい布団
56,query: 好きな動物は？,魚
57,query: 尊敬する人は誰ですか？,ヨネズ
58,query: 一番幸せな瞬間は？,寝る瞬間
59,query: 最近驚いたことは？,急に雨が降ったこと
60,query: 朝食は何を食べますか？,芋
61,query: 夜更かしはしますか？,しない
62,query: 人混みは好きですか？,苦手
63,query: 好きな色は？,特にない
64,query: 一番リラックスできる場所は？,布団
65,query: 好きなアーティストは？,知らない
66,query: 旅行先で必ず持っていくものは？,旅行とか行かない
67,query: 最近会った友人は誰ですか？,みはる
68,query: 好きな場所はどこですか？,布団の中
69,query: 最近始めたことはありますか？,新しい寝方
70,query: 子供の頃の思い出は？,覚えていない
71,query: 好きな遊びは？,寝れるところを探す
72,query: 最近見た夢は？,覚えてない
73,query: よく使うアプリは？,アプリって何？
74,query: 今の心境は？,眠い
75,query: 好きなタイプの天気は？,曇り
76,query: 最近の悩みは？,眠い
77,query: お気に入りの場所は？,布団
78,query: 最近習得したスキルは？,特にない
80,query: 好きな香りは？,芋を焼いた香り
81,query: 何か習い事をしていますか？,してない
82,query: 休日の過ごし方は？,寝てる
83,query: 最近のマイブームは？,昼寝
84,query: 最近感謝したことは？,みはるが食べ物をくれたこと
85,query: 一番のストレス解消法は？,寝ること
86,query: 旅行に行くならどこに行きたいですか？,特に行きたいところはない
87,query: 好きな果物は？,ブドウ
88,query: 好きなアイスクリームの味は？,食べたことない
89,query: 一番の宝物は？,布団
90,query: 最近作った料理は？,料理はしない
91,query: 今一番行きたい場所は？,特にない
92,query: 一番の憧れの人は？,みはる
93,query: 好きな映画は？,映画は見たことない
94,query: 最近覚えた言葉は？,おやすみ
95,query: 一番の思い出の場所は？,ヨネズのコロニー
```

以下のプログラムで、「nemuruClass」を作成し、text_list.csvのデータを登録する。

```python:insert_class.py
import csv
import torch
import weaviate
from torch import Tensor
from transformers import AutoModel, AutoTokenizer
from datetime import datetime, timezone
import torch.nn.functional as F
import pandas as pd
model_name = "intfloat/multilingual-e5-large"
tokenizer = AutoTokenizer.from_pretrained(model_name)

# CUDAが利用可能かチェックし、利用可能であればデバイスをCUDAに設定
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")

# モデルをデバイスに移動
model = AutoModel.from_pretrained(model_name).to(device)
client = weaviate.Client("http://localhost:8080")

def average_pool(last_hidden_states: Tensor, attention_mask: Tensor) -> Tensor:
    last_hidden = last_hidden_states.masked_fill(~attention_mask[..., None].bool(), 0.0)
    return last_hidden.sum(dim=1) / attention_mask.sum(dim=1)[..., None]

class_obj = {
    "class": "nemuruClass",
    "vectorizer": "none",
    "description": "テキストデータとそのエンベディング、作成日時を含むクラス",
    "properties": [
        {
            "name": "question",
            "dataType": ["string"],
            "description": "テキストデータ"
        },
        {
            "name": "answer",
            "dataType": ["string"],
            "description": "テキストデータ"
        },
        {
            "name": "create_date",
            "dataType": ["date"],
            "description": "作成日時"
        },
        {
            "name": "update_date",
            "dataType": ["date"],
            "description": "更新日時"
        },
        {
            "name": "delete_date",
            "dataType": ["date"],
            "description": "削除日時"
        }
    ]
}

client.schema.create_class(class_obj)

with open('text_list.csv', mode='r', encoding='utf-8') as csv_file:
    reader = csv.DictReader(csv_file)
    for row in reader:
        question = f"{row['question']}"
        answer = row['answer']
        inputs = tokenizer(question, return_tensors="pt", padding=True, truncation=True, max_length=512).to(device)
        
        with torch.no_grad():
            outputs = model(**inputs)
        
        embeddings = average_pool(outputs.last_hidden_state, inputs['attention_mask'])
        embeddings = F.normalize(embeddings, p=2, dim=1)
        
        data_object = {
            "question": question,
            "answer": answer,
            "create_date": datetime.now(timezone.utc).isoformat()
        }
        client.data_object.create(data_object, "nemuruClass", vector=embeddings.tolist()[0])
```

以下のプログラムで、データが登録されていることを確認する。

```python:select_class.py
import torch
import weaviate
from torch import Tensor
from transformers import AutoModel, AutoTokenizer
import torch.nn.functional as F

model_name = "intfloat/multilingual-e5-large"
tokenizer = AutoTokenizer.from_pretrained(model_name)

# CUDAが利用可能かチェックし、利用可能であればデバイスをCUDAに設定
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")

# モデルをデバイスに移動
model = AutoModel.from_pretrained(model_name).to(device)

def average_pool(last_hidden_states: Tensor, attention_mask: Tensor) -> Tensor:
    last_hidden = last_hidden_states.masked_fill(~attention_mask[..., None].bool(), 0.0)
    return last_hidden.sum(dim=1) / attention_mask.sum(dim=1)[..., None]

# 単語をエンベディング
question_text = "出身地は？"
inputs = tokenizer(question_text, return_tensors="pt", padding=True, truncation=True, max_length=512).to(device)
with torch.no_grad():
    outputs = model(**inputs)

embeddings = average_pool(outputs.last_hidden_state, inputs['attention_mask'])
embeddings = F.normalize(embeddings, p=2, dim=1)

# Weaviateクライアントの初期化
client = weaviate.Client("http://localhost:8080")

# エンベディングに最も近いテキストを検索するためのクエリを作成
# 厳しい条件でのクエリ
high_certainty_query = {
    "vector": embeddings.tolist()[0],
    "certainty": 0.9  # 高い確信度
}

high_certainty_result = client.query.get("nemuruClass", ["question", "answer"]).with_near_vector(high_certainty_query).with_additional(['certainty']).do()
print("High certainty result:", high_certainty_result)

```

Difyでベクトル検索するときは、以下のプログラムを実行し、常時リクエストを受け付ける状態にする。

```python:flask_select_class.py
# -*- coding: utf-8 -*-
from flask import Flask, request, jsonify
import torch
import weaviate
from torch import Tensor
from transformers import AutoModel, AutoTokenizer
import torch.nn.functional as F

app = Flask(__name__)

model_name = "intfloat/multilingual-e5-large"
tokenizer = AutoTokenizer.from_pretrained(model_name)

# CUDAが利用可能かチェックし、利用可能であればデバイスをCUDAに設定
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")

# モデルをデバイスに移動
model = AutoModel.from_pretrained(model_name).to(device)

def average_pool(last_hidden_states: Tensor, attention_mask: Tensor) -> Tensor:
    last_hidden = last_hidden_states.masked_fill(~attention_mask[..., None].bool(), 0.0)
    return last_hidden.sum(dim=1) / attention_mask.sum(dim=1)[..., None]

# Weaviateクライアントの初期化
client = weaviate.Client("http://localhost:8080")

@app.route('/embedding', methods=['POST'])
def get_embedding():
    data = request.get_json()
    question_text = data.get('text', '')
    target_class = data.get('target_class', '')
    certainty = data.get('certainty', '')
    record_limit = data.get('record_limit', '')
    
    # テキストが提供されているか確認
    if not question_text:
        return jsonify({"error": "No question text provided"}), 400
    if not target_class:
        return jsonify({"error": "No target class provided"}), 400
    if not certainty:
        certainty = 0.9
    if not record_limit:
        record_limit = 5
    
    # テキストをエンベディング
    inputs = tokenizer(question_text, return_tensors="pt", padding=True, truncation=True, max_length=512).to(device)
    with torch.no_grad():
        outputs = model(**inputs)
    
    embeddings = average_pool(outputs.last_hidden_state, inputs['attention_mask'])
    embeddings = F.normalize(embeddings, p=2, dim=1)
    
    # エンベディングに最も近いテキストを検索
    high_certainty_query = {
        "vector": embeddings.tolist()[0],
        "certainty": certainty  
    }
    
    high_certainty_result = client.query.get(target_class, ["question", "answer"]).with_near_vector(high_certainty_query).with_limit(record_limit).with_additional(['certainty']).do()
    results = high_certainty_result['data']['Get']['NemuruClass']
    processed_results = []
    
    for result in results:
        question = result['question']
        answer = result['answer']
        certainty = result['_additional']['certainty']
        processed_results.append({
            'question': question,
            'answer': answer,
            'certainty': certainty
        })
    
    return processed_results

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0')
```

テーブル再構築したい時に、以下のプログラムでclassを削除する。

```python:drop_class.py
# WeaviateのPythonクライアントをインポートします。
import weaviate

# Weaviateサーバーへの接続を初期化します。ここではローカルホスト上の標準ポート(8080)を指定しています。
client = weaviate.Client("http://localhost:8080")

# 'nemuruClass'クラスを削除します。
client.schema.delete_class("nemuruClass")
```

### 3.後処理

Dify上の設定は以下となる。
LLMにはstringのみが送信可能だが、json形式で送信したいため、jsonの見た目のstringへと処理する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619695/3c6acf5e-65ac-05ec-4cab-b106e3975edb.png)


### 4.RAG

Dify上の設定は以下となる。
ベクトル検索の結果からのみ情報を取得するようにシステムプロンプトで実施する。

![Dify_4_RAG.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619695/23f6841a-b560-2e6a-6c1d-36433292500e.png)

```
日本語で出力してください。
入力がどのような形式でも日本語のみで出力してください。

「参考情報」の情報のみを参考にし、「ユーザーの質問」：の質問について回答してください。
「ユーザーの質問」：{{#00000000000.input_text#}}
「参考情報」: {{#00000000000.result#}}
```

今回の場合、参考情報となるベクトル検索結果は以下のような形式となる。

```json
{
  "result": "[{'answer': 'だいたい寝てるかも', 'certainty': 0.9656796455383301, 'question': 'query: 普段、何をしていますか？'}, {'answer': '寝てる', 'certainty': 0.9464884400367737, 'question': 'query: 空いた時間はどう過ごしますか？'}, {'answer': '寝てる', 'certainty': 0.9410978555679321, 'question': 'query: 休日の過ごし方は？'}, {'answer': '寝る以外特にない', 'certainty': 0.9256762862205505, 'question': 'query: 趣味は？'}, {'answer': '特に不満はない', 'certainty': 0.9241939187049866, 'question': 'query: 今の生活で満足していますか？'}, {'answer': '最近はみはる', 'certainty': 0.9161971807479858, 'question': 'query: 誰と一緒に住んでいますか？'}, {'answer': '最近暖かくて寝心地がいい', 'certainty': 0.9148075580596924, 'question': 'query: 最近感動したことは？'}, {'answer': 'テレビって何？', 'certainty': 0.9138861298561096, 'question': 'query: テレビ番組はよく見ますか？'}, {'answer': 'ネムル', 'certainty': 0.9138029217720032, 'question': 'query: あなたの名前は？'}, {'answer': '寝ること', 'certainty': 0.9133186340332031, 'question': 'query: 好きなものは？'}]"
}
```

### 5.回答

Dify上の設定は以下となる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619695/a533d00e-9d1b-751e-e971-e957132602bb.png)


# 感想

Difyが登場したことにより、LLMやRAG関連のワークフローを構築するハードルが格段に下がった。
今回はある人物のパーソナリティーの情報を検索するようなRAGの仕組みを構築したが、classの定義を変更したりすることで、他の情報を検索するようなRAGを構築することができる。


# 追記（2024/0903）

ソースコード含めて、一部変数とするべきところを固定値にしていたため、「flask_select_class.py」のプログラムの修正版を記載しておきます。

修正前

```
results = high_certainty_result['data']['Get']['NemuruClass']
```

修正後

```
results = high_certainty_result['data']['Get'][target_class]
```

ソースコード全体

```
# -*- coding: utf-8 -*-
from flask import Flask, request, jsonify
import torch
import weaviate
from torch import Tensor
from transformers import AutoModel, AutoTokenizer
import torch.nn.functional as F

app = Flask(__name__)

model_name = "intfloat/multilingual-e5-large"
tokenizer = AutoTokenizer.from_pretrained(model_name)

# CUDAが利用可能かチェックし、利用可能であればデバイスをCUDAに設定
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")

# モデルをデバイスに移動
model = AutoModel.from_pretrained(model_name).to(device)

def average_pool(last_hidden_states: Tensor, attention_mask: Tensor) -> Tensor:
    last_hidden = last_hidden_states.masked_fill(~attention_mask[..., None].bool(), 0.0)
    return last_hidden.sum(dim=1) / attention_mask.sum(dim=1)[..., None]

# Weaviateクライアントの初期化
client = weaviate.Client("http://localhost:8080")

@app.route('/embedding', methods=['POST'])
def get_embedding():
    data = request.get_json()
    question_text = data.get('text', '')
    print(data)
    target_class = data.get('target_class', '')
    certainty = data.get('certainty', 0.9)
    record_limit = data.get('record_limit', 5)
    
    # テキストが提供されているか確認
    if not question_text:
        return jsonify({"error": "No question text provided"}), 400
    if not target_class:
        return jsonify({"error": "No target class provided"}), 400
    
    # テキストをエンベディング
    inputs = tokenizer(question_text, return_tensors="pt", padding=True, truncation=True, max_length=512).to(device)
    with torch.no_grad():
        outputs = model(**inputs)
    
    embeddings = average_pool(outputs.last_hidden_state, inputs['attention_mask'])
    embeddings = F.normalize(embeddings, p=2, dim=1)
    
    # エンベディングに最も近いテキストを検索
    high_certainty_query = {
        "vector": embeddings.tolist()[0],
        "certainty": certainty  # 確信度
    }
    
    high_certainty_result = client.query.get(target_class, ["question", "answer"]).with_near_vector(high_certainty_query).with_limit(record_limit).with_additional(['certainty']).do()
    results = high_certainty_result['data']['Get'][target_class]
    processed_results = []
    
    for result in results:
        question = result['question']
        answer = result['answer']
        certainty = result['_additional']['certainty']
        processed_results.append({
            'question': question,
            'answer': answer,
            'certainty': certainty
        })
    
    return processed_results

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0')
 ```

クラス名を「NemuruClass」で検索すれば、正常に検索できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619695/8ad2c912-da2c-cd5b-ff5e-649cf9ec6c91.png)