Azure AI Studio Prompt flow によるプロンプトのテストと評価ハンズオン③ - RAG

Azure AI Studio の Prompt flow によるテストと評価のハンズオン第三回目では RAG の評価をしていきたいと思います。まずはシンプルなチャットフローから始めましょう。Azure AI Studio の「プロンプトフロー」メニューからプロンプトフローの「＋作成」ボタンをクリックして名前を入力して作成します。

![006.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/a1d2dbe5-e74f-98ed-3225-68b3b9c97633.png)

シンプルなチャットフローが表示されますので、すぐに右上の「コンピューティングセッションの開始」をクリックしてフローの実行を行う VM を稼働させます。状況によって数分かかる場合がありますので先に押しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/bf03f3d8-d1ad-d482-c8b2-8373459c7814.png)

コンピューティングセッションが実行中になったら、右上の「チャット」ボタンをクリックしてチャットペインを表示させます。ここでチャットフローのテストができます。

# モデルの指定
「chat」ノードでは、LLM の実行ができます。ここでプロンプトエンジニアリングの作業を実施します。まずは必須項目として、実行する LLM を指定しなければなりません。「接続」ドロップダウンをクリックすると、すでに設定で接続に追加しておいた Azure OpenAI リソースが表示されますので、この LLM ノードに使いたいモデルを選択します。[接続設定](https://learn.microsoft.com/azure/ai-studio/how-to/connections-add)が完了しているので、ポチポチと選んでいくだけで設定が完了します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/4a0c5020-ba35-7b4b-8c8e-d597fc0cb475.png)

設定が完了したら右上の「保存」ボタンをクリックします。


ここでデフォルトのプロンプトの状態で以下のテキストをチャットに打ち込んでみましょう。

`花ケ咲神社って何ですか？` 

これは [AI Challenge Day](https://ascii.jp/elem/000/004/210/4210325/) で使用した架空の世界遺産の名前なのでモデルが知る由もありません。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/66a29b26-89fe-b93a-7a88-8971937be7b7.png" width="300" />

モデルの知識を最大限使って回答しようとしていますが、当然欲しい回答にはなりませんし、ハルシネーション（幻覚）を起こしてしまう場合もあります。

# 1. プロンプトエンジニアリング
では、デフォルトのシステムプロンプトを改良して、回答に制約を加えたいと思います。例えば以下のようなプロンプトです。

```
あなたは日本の歴史の教師です。提供された出典からのみ回答してください。
答えに迷うようなことがあれば、「わかりません」と答えてください。
可能な限り各文の後に「（出典：引用）」の形で引用を加えてください。
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/40215dc8-5c7c-476d-2662-ea2f2286f0d6.png)

この状態で保存し、もう一度同じ質問を投げます。設定を変えたので、新しいセッションを作成（テキストボックスの左の💬+ボタン）して実行します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/a31772b8-4470-2659-a565-4547638cfac6.png" width="300" />

はい、ちゃんと「わかりません」と回答しましたね。

このようにしてシナリオに適したプロンプトへ改良していきます。

# 2. RAG 
ここからは、ユーザーの質問で一旦検索インデックスを検索し、その検索結果をプロンプトに挿入する一般的な RAG の構成を作成していきます。

## 2.1. インデックスの作成
検索インデックスは Azure AI Studio 上で簡単に作成できます。左メニューの「インデックス」から「新しいインデックス」をクリックします。そしてデータソースから「ファイルのアップロード」を選択して RAG を行いたい社内ドキュメント等のファイルをアップロードして「次へ」をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/f85c0595-3a41-5042-7afc-76d0e7308bdf.png)

デフォルトの検索インデックスとして Azure AI Search が指定されており、新たに作成するインデックス名を設定します。インデックス作成（チャンク化、ベクトル化等）を行うための VM を指定する必要があります。ここは有料となりますのでご注意ください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/32488871-423b-d5bc-c860-78cfdb023eb4.png)

ベクトル検索を実行したい場合は以下にチェックを入れ、`text-embedding-ada-002` モデルをデプロイ済みのリソースを選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/be01a9ce-3ab5-2b60-cdee-66daf099b274.png)

設定を確認して、「作成」ボタンをクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/f60e6f25-ac9e-9300-ba3a-766c14840812.png)

インデックス作成処理が開始されます。全行程が完了するのに大体 10 分程度掛かりました。「ジョブの詳細」をクリックすると、インデックス作成のジョブパイプラインを見ることができます。Azure Machine Learning のパイプラインですので、実行時間、そんなに早くないです。。。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/91376b6f-2ac5-a9d8-b149-63821370cb51.png)

全行程完了しましたら、ステータスが Completed となります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/4dbd8c52-573f-f756-0101-7b65c6c3f0ec.png)

参考として、Azure Portal から実際に Azure AI Search のリソースで作成されたインデックスを確認することができます。ドキュメントは 4 つのチャンクに分割されてインデックス化されたことが分かります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/832b7c34-277c-0cb8-bd09-12a6f70df837.png)

## 2.2. インデックスの読み込み
それでは先ほどのフローに戻り、作成したインデックスを読み込ませましょう。ツールメニューの「＋その他のツール」から「Index Lookup」ツールを選択します。名前には「AISearch」とでも入力しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/4ca4a53b-70be-9f66-31b9-bfb740d7fa8e.png)

- mlindex_content: インデックス設定
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/e238329e-ac99-ca80-58f8-032f7ac82a22.png)
- queries: `${inputs.question}`
- query_type: `Hybrid (vector + keyword)`
- top_k: `3`

各設定項目は上記のように設定します。
queries に `${inputs.question}` を設定すると、フローの inputs---AISearch 間がつながります。プロンプトフローはデータの流れを表します。これでユーザーからの入力値が AISearch 側へ渡るようになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/7297f4f5-da4e-238d-ddcb-d18353f38eab.png)

ではここで保存して、一度チャットに質問してみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/90d16eed-a028-7a90-5ccc-013f777d017f.png)

当然チャットの出力は「わかりません」のままですが、AISearch ノードの出力をご覧ください。Azure AI Search からの生の検索結果が表示されていることが分かりますね。ノード単体のテストとしてはこれで成功です。全文を確認したい場合は「完全な出力を表示する」ボタンをクリックします。

ただし、この生の出力結果（配列）をプロンプトに挿入するための加工・結合をしたいですよね。**top_k 分のテキストだけを取り出したい**。そのための後処理ノードを追加しましょう。

## 2.3. 検索結果を結合
Azure AI Search からの検索結果（JSON）の中身をループしてテキストだけを取り出して、結合した文字列を返す Python コードを以下のように貼り付けます。これはもうおまじないなのでコピペするだけで OK です。

```python
from typing import List
from promptflow import tool
from promptflow_vectordb.core.contracts import SearchResultEntity

@tool
def generate_prompt_context(search_result: List[dict]) -> str:
    def format_doc(doc: dict):
        return f"Content: {doc['Content']}"

    retrieved_docs = []
    for item in search_result:

        entity = SearchResultEntity.from_dict(item)
        content = entity.text or ""

        retrieved_docs.append({
            "Content": content
        })
    doc_string = "\n\n".join([format_doc(doc) for doc in retrieved_docs])
    return doc_string
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/74340c76-883d-0204-e7ae-4d747e6b6d8a.png)

引数、パラメータなどを編集した後は、「入力の検証と解析」ボタンをクリックする必要があります。**フローからの入力値を引数に紐づけるため、自動的にコード内の引数を抽出します**。コード内の `search_result` に AISearh ノードからの出力値である `${AISearch.output}` をセットすれば右のフローにあるように、AISearch---MergeResults 間のデータ接続がつながります。

`前のノードの出力値-->ノードの入力値`

という関係ですね。

ではここでフローを保存して、またチャットで同じ質問をしてみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/9da7de3b-e0ba-5a08-2ea5-9d6985312fbd.png)

ハイ今度は、検索結果 3 件分のテキストがすべて結合された文字列のみが返ってきたことが分かります。

これをプロンプトに挿入していきましょう。

## 2.4. 検索結果の挿入
最後に MergeResults からの結果を chat ノードに挿入するためのプロンプトを以下のように追加します。追加したら必ず「入力の検証と解析」をクリックし、`source` の入力値として `${MergeResults.output}` を指定します。

```jsx
# source: 
{{source}}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/668d9396-feca-9a80-4d13-118baf74b3a7.png)

すると `MergeResults---chat` 間のデータ接続がつながりました！

この状態でフローを保存し、チャットで質問してみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/a85f6aaa-108d-f92c-efd0-028b955d24de.png)


はい、成功ですね。ちゃんと出典にある回答をしてくれました。（内容はダミーテキストです）chat ノードの入力値には、それぞれの変数に渡ってきたテキストが格納されていることが確認できますね。

## 3. チャット履歴の考慮
デフォルトのプロンプトには以下のようなプロンプトが入っているのが分かります。Azure AI Studio は専用の変数 `chat_history` に過去のやり取りを蓄積しており、これをループしてプロンプトに展開することで一緒に LLM に投げることができます。

```jsx
{% for item in chat_history %}
# user:
{{item.inputs.question}}
# assistant:
{{item.outputs.answer}}
{% endfor %}
```

`そういえばあの神社ってなんでしたっけ？もう一度説明して`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/a6d77da9-eca0-a495-4e85-d7b5b65704b4.png)

chat ノードの入力値に過去の質問・回答が入ってきていることが分かりますね。

# 4. RAG 評価
## コンテキストを含む評価メトリクス
### Groundedness（接地性、根拠性）
回答がコンテキストに基づいているかどうかを 5 段階評価します。
必須項目：コンテキスト、回答
### Relevance（関連性）
コンテキストを考慮した上で、質問と回答との関連性を 5 段階評価します。
必須項目：コンテキスト、質問、回答

## 4.1. コンテキストを出力
デフォルトでは、最終出力として `answer` のみを出力しますが、評価のために `MergeResults` ノードからの出力も `context` として追加します。こうすることで評価の項目として使用することができるようになります。チャットの出力ラジオボタンを `answer` に指定しているので最終出力は `chat` ノードからのままとなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/1ad2c189-e472-1a8c-8a28-c48b608747e8.png)

また、簡単のために AISearch ノードからの検索結果の `top_k` を 1 件に変更しておきます。

## 4.2. 自動評価
ツールバーから評価→自動評価を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/1ea19924-0ed7-d5bb-3ea3-0676bac30a76.png)

「コンテキストを含む質問と回答」が選択されていることを確認し、次へ進みます。

## 4.3. CSV のアップロード
以下のようなシンプルな CSV ファイルを作成してアップロードします。`question` と `ground_truth` のみを記載しておき、`context` と `answer` はフローからの出力をマッピングするために空欄にしてあります。`chat_history` リストは今回使用しないので `[]` にしてあります。

```csvs
question,answer,ground_truth,context,chat_history
"花ケ咲神社の本殿が建造されたのは何年ですか？","","花ケ咲神社の本殿が建造されたのは794年です。","",[]
"花ケ咲神社の最寄り駅はどこ？","","花ケ咲神社の最寄駅は京都市営地下鉄烏丸線北山駅です。","",[]
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/e12e2944-3f53-7139-85bd-b747820f2609.png)

カラム名を合わせてあるので、自動的に `chat_history` と `question` がマッピングされます。

## 4.4. 評価メトリクス
今回は「根拠性」、「関連性」そして「GPT の類似性」にチェックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/83d7c5cb-f66c-9151-a4f1-914b574dd522.png)

`context` と `answer` にはフローからの出力がマッピングされていることを確認します。

- context: `${run.outputs.context}`・・・フローから
- answer: `${run.outputs.answer}`・・・フローから
- question: `${data.question}`・・・CSV から
- ground_truth: `${data.ground_truth}`・・・CSV から

そして評価に使用するモデルを選択します。今回は「gpt-4o」をセットします。準備ができたら次へ進んで内容を確認して「送信」をクリックします。

## 4.5. 評価結果
評価には 1~2 分掛かります。完了しましたら、メトリック ダッシュボードに評価結果のグラフと表が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/862294d1-9573-eb40-4959-850ec6c28910.png)

グラフには、評価メトリクスごとの平均スコアとヒストグラムが表示されます。表には CSV のレコードごとの結果が表示されます。わざと `top_k` を 1 件にしておいたので、検索結果に回答が含まれなかった場合が出てきますが、その場合は根拠性や関連性のメトリクスが 1 点となります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/f2e2ac51-16c8-2a18-47ae-d93bed5c6c9e.png)

レコード 2 は回答がコンテキストに存在したため、根拠性 5 点、関連性 5 点となっています。ただし、GPT 類似性では 3 点となっています。これは `ground_truth` と `answer` の類似性を判定しているためです。


# シリーズ
https://github.com/nohanaga/aistudio-handson