手っ取り早くRAGする

余計なクラウド環境などを使わずに、とりあえずローカルから`Python`と`OpenAIのAPI`だけでRAGする。

# そもそもRAGとはなんだったか

LLMは何でも知っている訳ではない。例えば特定の会社の規則などの、社外秘情報は知らない。
なので、まずはそれらを検索できるデータベースなど使って、ユーザーの`質問`から、`質問に関する情報`を取得する。
そして`質問`と`質問に関する情報`を一緒にLLMに渡してやることで、与えた情報に基づいて回答を生成させる。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184660/d02dc8ad-9ce5-43b8-881b-773a1c89c0d7.png" width=80%>

# 必要なライブラリを入れる
``` sh
pip install openai
pip install numpy
```

# データベースを作成する

データベースから検索したい文章について、ベクトルを生成する。
検索させたい単位毎にベクトルを生成する。

ここでは、コード中の配列に設定した文字列データを入力として、`embed.json`という名称のファイルに、ベクトル化したデータを書き出す。これをデータベースとする。

``` python
from openai import OpenAI
import json

OPEN_AI_API_KEY = 'xxxx'

data = {
    '日本国憲法 第1章 天皇 第1条': {
        'body': '天皇は、日本国の象徴であり日本国民統合の象徴であつて、この地位は、主権の存する日本国民の総意に基く。'
    },
    '日本国憲法 第1章 天皇 第2条': {
        'body': '皇位は、世襲のものであつて、国会の議決した皇室典範の定めるところにより、これを継承する。'
    }
}

openai_client = OpenAI(api_key=OPEN_AI_API_KEY)

# ベクトルを保存する配列
index = []

# 全データに対して繰り返し
for title, item in data.items():
    body = item['body']
    input = title + '\n' + body

    # ベクトル化
    response = openai_client.embeddings.create(
        model="text-embedding-3-small",
        input=input
    )
    embedding = response.data[0].embedding

    # ベクトルを配列に追加
    index.append({
        'title': title,
        'body': body,
        'embedding': embedding
    })

# ファイルに書き出し
with open('./embed.json', 'w') as f:
    json.dump(index, f)
```

# ユーザーの質問からデータベースを検索する

ユーザーの質問もベクトル化する。
そして`embed.json`から、質問ベクトルに近いベクトルを検索する。

ベクトルの近さは`コサイン類似度`によって計算する。

``` python
import json
from openai import OpenAI
from numpy import dot
from numpy.linalg import norm

OPEN_AI_API_KEY = 'xxxx'

openai_client = OpenAI(api_key=OPEN_AI_API_KEY)

# 準備したベクトルデータベースを読み込み
with open('./embed.json') as f:
    index = json.load(f)

# コサイン類似度を計算する関数    
def cosine_similarity(lhs, rhs):
    return dot(lhs, rhs) / (norm(lhs) * norm(rhs))

# 準備したベクトルデータベースから、近いものを検索する関数
def search(vec, k=3):
    results = map(
        lambda i: {
            'title': i['title'],
            'body': i['body'],
            'similarity': cosine_similarity(i['embedding'], vec)
        },
        index
    )

    # 類似度の大きいもの順にソート
    results = sorted(results, key=lambda i: i['similarity'], reverse=True)

    # 類似度の大きいものからk件を返す
    return results[:k]


# --------------------------------


# LLMへの質問
input = '皇位は世襲ですか？'

# 質問をベクトル化
response = openai_client.embeddings.create(
    model='text-embedding-3-small',
    input=input
)
vec = response.data[0].embedding

# 近いデータを検索
data = search(vec)

print(data)
```

## 実行結果
```
[{'title': '日本国憲法 第1章 天皇 第2条', 'body': '皇位は、世襲のものであつて、国会の議決した皇室典範の定めるところにより、これを継承する。', 'similarity': np.float64(0.5653466695377146)}, {'title': '日本国憲法 第1章 天皇 第1条', 'body': '天皇は、日本国の象徴であり日本国民統合の象徴であつて、この地位は、主権の存する日本国民の総意に基く。', 'similarity': np.float64(0.37133550037315366)}]
```

# プロンプトを生成する

検索したデータから、プロンプトを準備する。
データからLLMに渡す文字列を作成するだけ。

``` python
# プロンプトのテンプレート
prompt_template = '''
ユーザーの質問に対する回答を作成してください。

回答に必要であれば、以下のinformationを参照してください。
informationに示す文章には、0から1の範囲の数値が、ユーザーの発言に対する類似度として付与されています。類似度は1が最もユーザーの発言に近いという意味です。

# information

{information}
'''

# プロンプトに埋め込むテキストを検索したデータから作成
information = '\n\n'.join(map(lambda e: f"=====\n類似度 {e['similarity']}\n\n{e['body']}", data))

# プロンプト生成
prompt = prompt_template.format(
    information=information,
).strip()

print(prompt)
```

## 実行結果
```
ユーザーの質問に対する回答を作成してください。

回答に必要であれば、以下のinformationを参照してください。
informationに示す文章には、0から1の範囲の数値が、ユーザーの発言に対する類似度として付与されています。類似度は1が最もユーザーの発言に近いという意味です。

# information

=====
類似度 0.5653466695377146

皇位は、世襲のものであつて、国会の議決した皇室典範の定めるところにより、これを継承する。

=====
類似度 0.37133550037315366

天皇は、日本国の象徴であり日本国民統合の象徴であつて、この地位は、主権の存する日本国民の総意に基く。
```

# LLMに回答を生成させる

準備したプロンプトとユーザーの質問をLLMに渡して、回答を生成させる。

``` python
response = openai_client.chat.completions.create(
    messages=[
        {
            "role": "system",
            "content": prompt, # プロンプトを渡す
        },
        {
            "role": "user",
            "content": input, # 質問を渡す
        }
    ],
    model="gpt-4o",
    n=1,
    temperature=1.0,
    frequency_penalty=0.5,
)
content = response.choices[0].message.content

print(content)
```

## 実行結果
```
はい、皇位は世襲であり、国会の議決した皇室典範の定めるところにより継承されます。
```

# おまけ
`Python`と`AWS`を使用して、`Slack`からのチャットによる問い合わせに`RAG`で回答するボットの、サンプルコードを作成しました。

よろしければ、ご自由にご利用ください🤗

- https://github.com/sfjwr/ragbot_slack