RAG評価ツールの "RAGAS" を使って、RAGパイプラインの性能を測定する

## はじめに
こんにちは、KDDIアジャイル開発センターのはしもと（仮名）です。

LLMで何かしたい勢のみなさま、検索拡張生成こと **RAG (Retrieval Augmented Generation)**、やってますか？
自社で持っているデータを使ってエンタープライズサーチを実現したい、それができればきっと無敵。そう考えて色々やろうとしているんじゃないでしょうか。私です。

RAGを使って意図した出力を得られるようにするには、十分なデータセットを準備したりパラメータを変更しながらチューニングをするなど、地道な作業が必要となります。

開発ライフサイクルにおける**評価・テスト**ステップで有効な、評価用フレームワーク **RAGAS** を使ってみましたので、本記事ではそれについてまとめます。

## RAGASとは
https://github.com/explodinggradients/ragas

https://docs.ragas.io/en/latest/

**RAGパイプラインを評価/テストするためのフレームワーク**です。

パイプラインを構築するためのツールは多く存在しますが、それらを評価し、パフォーマンスを具体的に測定することは重要であるにも関わらず困難です。

それを手助けし、測定を実現するためのツールがRAGASということですね。

### 類似ツール
**LangSmith(LangChain)** や **Prompt Flow(Azure OpenAI Service)** にも同様の機能が具備されています。
上記のツールは、いずれも RAGAS のスコープとする評価やテスト以外のステップでも適用でき、ライフサイクルにおけるより広範な部分をスコープとしています。

https://www.langchain.com/langsmith

https://learn.microsoft.com/en-us/azure/machine-learning/prompt-flow/overview-what-is-prompt-flow?view=azureml-api-2


### 評価指標

https://docs.ragas.io/en/latest/concepts/metrics/index.html#

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463957/eaa804c1-1ee6-aa77-22da-14d52b1b6f2e.png" width="400px">

RAGAS内で評価に使用される指標について抜粋して説明します。

基本的に、以下の情報を使って指標は算出されます。
- **与えられたプロンプト**
- **関連があるものとして取得されたコンテキスト**
- **生成された回答**
- **任意の質問に対する真の回答**

詳細は下記のとおりです。

| 指標 | 説明 | 例 |
| ---- | ---- | ---- |
| **Faithfulness（忠実度）** | 生成された回答が与えられたコンテキストに対して事実上一貫しているか | ・質問: アインシュタインはどこで、どこで生まれましたか?<br>・背景: アルバート・アインシュタイン (1879 年 3 月 14 日生まれ) はドイツ生まれの理論物理学者であり、史上最も偉大で最も影響力のある科学者の一人であると広く考えられています。<br>・高スコアとなるもの: アインシュタインは 1879 年 3 月 14 日にドイツで生まれました。<br>・低スコアとなるもの: アインシュタインは 1879 年 3 月 20 日にドイツで生まれました。|
| **Answer Relevancy（回答の関連性）** | 生成された回答が与えられたプロンプトにどれだけ関連しているか<br>不完全な回答や冗長な情報を含む回答には低いスコアとなる| ・質問: フランスはどこですか、そしてその首都はどこですか?<br>・高スコアとなるもの: フランスは西ヨーロッパにあり、パリはその首都です。<br>・低スコアとなるもの: フランスは西ヨーロッパにあります。|
| **Context Precision（コンテキストの精度）** | 取得されたコンテキスト内のすべての関連アイテムが高くランク付けされているか。||
| **Context Recall（コンテキストの再現性）** | 取得されたコンテキストが、真の回答とどれだけ一致しているか| ・質問: フランスはどこですか、そしてその首都はどこですか?<br>・真実: フランスは西ヨーロッパにあり、首都はパリです。<br>・高スコアとなるもの: 西ヨーロッパのフランスには、中世の都市、高山の村、地中海のビーチがあります。首都パリは、ファッションハウス、ルーブル美術館などの古典的な美術館、エッフェル塔などの記念碑で有名です。<br>・低スコアとなるもの: 西ヨーロッパのフランスには、中世の都市、高山の村、地中海のビーチがあります。この国はワインと洗練された料理でも有名です。ラスコーの古代の洞窟壁画、リヨンのローマ劇場、広大なベルサイユ宮殿は、その豊かな歴史を証明しています。|
| **Context Relevancy（コンテキストの関連性）** | 与えられたプロンプトと取得されたコンテキストの関連性|・質問: フランスの首都はどこですか?<br>・高スコアとなるもの: 西ヨーロッパのフランスには、中世の都市、高山の村、地中海のビーチが含まれています。首都パリは、ファッションハウス、ルーブル美術館などの古典的な美術館、エッフェル塔などの記念碑で有名です。<br>・低スコアとなるもの: 西ヨーロッパのフランスには、中世の都市、高山の村、地中海のビーチが含まれます。首都パリは、ファッションハウス、ルーブル美術館などの古典的な美術館、エッフェル塔などの記念碑で有名です。この国はワインと洗練された料理でも有名です。ラスコーの古代の洞窟壁画、リヨンのローマ劇場、広大なベルサイユ宮殿は、その豊かな歴史を証明しています。|

## 検証

:::note warn
RAGAS の内部では、ベクトル変換の際に OpenAI のモデルを使用します。
OpenAI の API を呼び出すための認証情報が必要になるのでご注意ください。
:::

### 検証用コード

検証に用いたコードは下記になります。

<details><summary>メイン実装</summary>

```python
import os

os.environ["OPENAI_API_KEY"] = "<YOUR API KEY HERE>"

```


```python
from langchain.document_loaders import TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter


loader = TextLoader("./ramen.txt")
documents = loader.load()
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=100, chunk_overlap=30
)
docs = text_splitter.split_documents(documents)

print(len(docs))
print(docs)

---
19
[Document(page_content='ラーメンは、中国から発祥し、日本で独自の進化を遂げた麺料理です。この料理は小麦粉から作られた細い麺と、鶏、豚、魚介など多様な材料から抽出されたスープ、そして様々な具材で構成されます。日本各地には地域ご', metadata={'source': './ramen.txt'}), Document(page_content='たスープ、そして様々な具材で構成されます。日本各地には地域ごとに異なるラーメンスタイルが存在し、札幌の味噌ラーメン、博多のとんこつラーメン、東京の醤油ラーメンなどが特に有名です。', metadata={'source': './ramen.txt'}), Document(page_content='ラーメンの特徴の一つは、トッピングの多様性にあります。チャーシュー（焼き豚）、メンマ（発酵させた竹の子）、味付け玉子、ネギ、海苔など、多種多様なトッピングが加えられることで、味わいや食感のバリエーシ', metadata={'source': './ramen.txt'}), Document(page_content='多様なトッピングが加えられることで、味わいや食感のバリエーションが豊かになります。', metadata={'source': './ramen.txt'}), Document(page_content='他の麺料理との比較では、麺の種類やスープ、具材に大きな違いが見られます。うどんは太くて柔らかい麺、そばはそば粉を使用した麺、パスタは小麦粉と卵で作られた麺で、それぞれ独自の食感と風味があります。また', metadata={'source': './ramen.txt'}), Document(page_content='と卵で作られた麺で、それぞれ独自の食感と風味があります。また、イタリアンのパスタはトマトベースやクリームベースのソースを使用し、具材には海鮮や野菜、肉を多用します。対して、ラーメンはスープの味が主体で', metadata={'source': './ramen.txt'}), Document(page_content='や野菜、肉を多用します。対して、ラーメンはスープの味が主体で、具材は比較的シンプルです。', metadata={'source': './ramen.txt'}), Document(page_content='ラーメンのスープは多種多様で、鶏ガラ、豚骨、魚介、野菜などの材料を長時間煮込んで作られます。これにより濃厚な味わいと深い旨味が生まれ、地域や店によって味噌、醤油、塩などの調味料を加えることで無限に近', metadata={'source': './ramen.txt'}), Document(page_content='や店によって味噌、醤油、塩などの調味料を加えることで無限に近いバリエーションが生まれます。', metadata={'source': './ramen.txt'}), Document(page_content='調理法の面でもラーメンは他の麺料理と異なります。イタリアンのパスタはアルデンテに茹で上げ、ソースと和えて提供されるのに対し、ラーメンは麺を茹でた後、熱いスープに直接入れて提供されます。これにより麺の', metadata={'source': './ramen.txt'}), Document(page_content='茹でた後、熱いスープに直接入れて提供されます。これにより麺の食感や味わいが大きく異なるのです。', metadata={'source': './ramen.txt'}), Document(page_content='ラーメンの文化的側面も見逃せません。日本では、ラーメンは手軽に食べられるファーストフードとしてだけでなく、地域の特色を反映した文化的アイデンティティを持つ料理としても認識されています。ラーメン店一つ', metadata={'source': './ramen.txt'}), Document(page_content='ンティティを持つ料理としても認識されています。ラーメン店一つをとっても、店主の哲学や技術が反映され、同じ種類のラーメンでも店によって全く異なる味わいを楽しむことができます。', metadata={'source': './ramen.txt'}), Document(page_content='さらに、ラーメンは国際的にも非常に人気が高く、世界各国で様々な形でアレンジされています。特にアメリカやヨーロッパでは、伝統的な日本のラーメンをベースにした新しいスタイルのラーメンが登場し、新たなファ', metadata={'source': './ramen.txt'}), Document(page_content='ンをベースにした新しいスタイルのラーメンが登場し、新たなファンを獲得しています。', metadata={'source': './ramen.txt'}), Document(page_content='日本国内においても、ラーメンは進化し続けており、最近ではヘルシー志向のラーメンや、ベジタリアン対応のラーメンなど、さまざまなニーズに応える形で展開されています。また、地域ごとの特色を活かした新しいタ', metadata={'source': './ramen.txt'}), Document(page_content='形で展開されています。また、地域ごとの特色を活かした新しいタイプのラーメンが登場することもあり、その多様性はさらに広がりを見せています。', metadata={'source': './ramen.txt'}), Document(page_content='総じて、ラーメンはその多様性、地域ごとの特色、豊富なトッピングから、単なる麺料理を超えた、日本の食文化を代表する存在として、国内外で愛され続けています。その豊かな味わいとバリエーションが、世界中の多', metadata={'source': './ramen.txt'}), Document(page_content='続けています。その豊かな味わいとバリエーションが、世界中の多くの人々を魅了しているのです。', metadata={'source': './ramen.txt'})]

```

```python
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import FAISS

oai_embeddings = OpenAIEmbeddings()

db = FAISS.from_documents(docs, oai_embeddings)

```


```python
from langchain.llms import Bedrock

llm = Bedrock(
    model_id="anthropic.claude-instant-v1",
    region_name="us-east-1",
    model_kwargs={"temperature": 0.0},
)
llm.predict("おはようございます")

---
' はい、おはようございます。日本語で挨拶ができてうれしいです。'
```

```python
questions = [
    "ラーメンはどこの国発祥ですか",
    "うどんの麺の特徴は？",
    "博多はどんなラーメンが多い？",
    "最近のラーメンの進化について教えてください",
]

answers = []
for question in questions:
    response = db.similarity_search(question, k=1)
    print(question)
    for res in response:
        print(res.page_content)
    print("-----")

---
    ラーメンはどこの国発祥ですか
    ラーメンは、中国から発祥し、日本で独自の進化を遂げた麺料理です。この料理は小麦粉から作られた細い麺と、鶏、豚、魚介など多様な材料から抽出されたスープ、そして様々な具材で構成されます。日本各地には地域ご
    -----
    うどんの麺の特徴は？
    他の麺料理との比較では、麺の種類やスープ、具材に大きな違いが見られます。うどんは太くて柔らかい麺、そばはそば粉を使用した麺、パスタは小麦粉と卵で作られた麺で、それぞれ独自の食感と風味があります。また
    -----
    博多はどんなラーメンが多い？
    たスープ、そして様々な具材で構成されます。日本各地には地域ごとに異なるラーメンスタイルが存在し、札幌の味噌ラーメン、博多のとんこつラーメン、東京の醤油ラーメンなどが特に有名です。
    -----
    最近のラーメンの進化について教えてください
    形で展開されています。また、地域ごとの特色を活かした新しいタイプのラーメンが登場することもあり、その多様性はさらに広がりを見せています。
    -----

```


```python
from langchain.chains import RetrievalQA

retriever = db.as_retriever(search_kwargs={"k": 1})
qa = RetrievalQA.from_chain_type(
    llm=llm, chain_type="stuff", retriever=retriever, verbose=True
)

```


```python
contexts = []
answers = []
for question in questions:
    res = db.similarity_search(question, k=1)
    contexts.append([r.page_content for r in res])
    answers.append(qa.run(question))

print(contexts)
print(answers)

--- 
    [1m> Entering new RetrievalQA chain...[0m
    
    [1m> Finished chain.[0m
    
    
    [1m> Entering new RetrievalQA chain...[0m
    
    [1m> Finished chain.[0m
    
    
    [1m> Entering new RetrievalQA chain...[0m
    
    [1m> Finished chain.[0m
    
    
    [1m> Entering new RetrievalQA chain...[0m
    
    [1m> Finished chain.[0m
    [['ラーメンは、中国から発祥し、日本で独自の進化を遂げた麺料理です。この料理は小麦粉から作られた細い麺と、鶏、豚、魚介など多様な材料から抽出されたスープ、そして様々な具材で構成されます。日本各地には地域ご'], ['他の麺料理との比較では、麺の種類やスープ、具材に大きな違いが見られます。うどんは太くて柔らかい麺、そばはそば粉を使用した麺、パスタは小麦粉と卵で作られた麺で、それぞれ独自の食感と風味があります。また'], ['たスープ、そして様々な具材で構成されます。日本各地には地域ごとに異なるラーメンスタイルが存在し、札幌の味噌ラーメン、博多のとんこつラーメン、東京の醤油ラーメンなどが特に有名です。'], ['形で展開されています。また、地域ごとの特色を活かした新しいタイプのラーメンが登場することもあり、その多様性はさらに広がりを見せています。']]
    [' ラーメンは中国から発祥し、日本で独自の進化を遂げた麺料理です。\n\nTherefore, the context provided indicates that ramen originated from China.', ' うどんの麺の特徴は太くて柔らかい、とあります。', ' とんこつラーメンが多いです。', ' この文脈からは、ラーメンが形で展開されているとか、地域ごとに新しいタイプのラーメンが登場することがある、と書かれています。最近のラーメンの進化についての具体的な内容はこの文脈からは分かりません。']

```


```python
from datasets import Dataset

ground_truths = [
    ["中国"],
    ["太くて柔らかい"],
    ["とんこつラーメン"],
    ["ヘルシー志向のものやベジタリアン対応、地域ごとの特色活かしたものの登場によって多様性が拡大している"],
]

ds = Dataset.from_dict(
    {
        "question": questions,
        "answer": answers,
        "contexts": contexts,
        "ground_truths": ground_truths,
    }
)

```


```python
for row in ds:
    print(f'question: {row["question"]}')
    print(f'contexts: {row["contexts"]}')
    print(f'answer: {row["answer"]}')
    print(f'ground_truths: {row["ground_truths"]}')
    print("-----")

---
    question: ラーメンはどこの国発祥ですか
    contexts: ['ラーメンは、中国から発祥し、日本で独自の進化を遂げた麺料理です。この料理は小麦粉から作られた細い麺と、鶏、豚、魚介など多様な材料から抽出されたスープ、そして様々な具材で構成されます。日本各地には地域ご']
    answer:  ラーメンは中国から発祥し、日本で独自の進化を遂げた麺料理です。
    
    Therefore, the context provided indicates that ramen originated from China.
    ground_truths: ['中国']
    -----
    question: うどんの麺の特徴は？
    contexts: ['他の麺料理との比較では、麺の種類やスープ、具材に大きな違いが見られます。うどんは太くて柔らかい麺、そばはそば粉を使用した麺、パスタは小麦粉と卵で作られた麺で、それぞれ独自の食感と風味があります。また']
    answer:  うどんの麺の特徴は太くて柔らかい、とあります。
    ground_truths: ['太くて柔らかい']
    -----
    question: 博多はどんなラーメンが多い？
    contexts: ['たスープ、そして様々な具材で構成されます。日本各地には地域ごとに異なるラーメンスタイルが存在し、札幌の味噌ラーメン、博多のとんこつラーメン、東京の醤油ラーメンなどが特に有名です。']
    answer:  とんこつラーメンが多いです。
    ground_truths: ['とんこつラーメン']
    -----
    question: 最近のラーメンの進化について教えてください
    contexts: ['形で展開されています。また、地域ごとの特色を活かした新しいタイプのラーメンが登場することもあり、その多様性はさらに広がりを見せています。']
    answer:  この文脈からは、ラーメンが形で展開されているとか、地域ごとに新しいタイプのラーメンが登場することがある、と書かれています。最近のラーメンの進化についての具体的な内容はこの文脈からは分かりません。
    ground_truths: ['ヘルシー志向のものやベジタリアン対応、地域ごとの特色活かしたものの登場によって多様性が拡大している']
    -----

```


```python
from ragas import evaluate


"""
評価の実行
- answer_relevancy
- context_precision
- faithfulness
- context_recall
"""

result = evaluate(ds)
print(result)

---
    evaluating with [answer_relevancy]


    100%|██████████| 1/1 [00:03<00:00,  3.82s/it]


    evaluating with [context_precision]


    100%|██████████| 1/1 [00:01<00:00,  1.01s/it]


    evaluating with [faithfulness]


    100%|██████████| 1/1 [00:08<00:00,  8.01s/it]


    evaluating with [context_recall]


    100%|██████████| 1/1 [00:03<00:00,  3.27s/it]


    {'answer_relevancy': 0.8139, 'context_precision': 1.0000, 'faithfulness': 1.0000, 'context_recall': 1.0000}

```


```python
fake_ground_truths = [
    ["アメリカ"],
    ["すごい"],
    ["塩ラーメン"],
    ["二足歩行する"],
]

fake_ds = Dataset.from_dict(
    {
        "question": questions,
        "answer": answers,
        "contexts": contexts,
        "ground_truths": fake_ground_truths,
    }
)

fake_result = evaluate(fake_ds)
print(fake_result)

---
    evaluating with [answer_relevancy]


    100%|██████████| 1/1 [00:04<00:00,  4.78s/it]


    evaluating with [context_precision]


    100%|██████████| 1/1 [00:00<00:00,  1.21it/s]


    evaluating with [faithfulness]


    100%|██████████| 1/1 [00:08<00:00,  8.39s/it]


    evaluating with [context_recall]


    100%|██████████| 1/1 [00:01<00:00,  1.65s/it]


    {'answer_relevancy': 0.8139, 'context_precision': 1.0000, 'faithfulness': 1.0000, 'context_recall': 0.2500}

```

</details>

<details><summary>検索対象ドキュメント</summary>

```ramen.txt
ラーメンは、中国から発祥し、日本で独自の進化を遂げた麺料理です。この料理は小麦粉から作られた細い麺と、鶏、豚、魚介など多様な材料から抽出されたスープ、そして様々な具材で構成されます。日本各地には地域ごとに異なるラーメンスタイルが存在し、札幌の味噌ラーメン、博多のとんこつラーメン、東京の醤油ラーメンなどが特に有名です。
ラーメンの特徴の一つは、トッピングの多様性にあります。チャーシュー（焼き豚）、メンマ（発酵させた竹の子）、味付け玉子、ネギ、海苔など、多種多様なトッピングが加えられることで、味わいや食感のバリエーションが豊かになります。
他の麺料理との比較では、麺の種類やスープ、具材に大きな違いが見られます。うどんは太くて柔らかい麺、そばはそば粉を使用した麺、パスタは小麦粉と卵で作られた麺で、それぞれ独自の食感と風味があります。また、イタリアンのパスタはトマトベースやクリームベースのソースを使用し、具材には海鮮や野菜、肉を多用します。対して、ラーメンはスープの味が主体で、具材は比較的シンプルです。
ラーメンのスープは多種多様で、鶏ガラ、豚骨、魚介、野菜などの材料を長時間煮込んで作られます。これにより濃厚な味わいと深い旨味が生まれ、地域や店によって味噌、醤油、塩などの調味料を加えることで無限に近いバリエーションが生まれます。
調理法の面でもラーメンは他の麺料理と異なります。イタリアンのパスタはアルデンテに茹で上げ、ソースと和えて提供されるのに対し、ラーメンは麺を茹でた後、熱いスープに直接入れて提供されます。これにより麺の食感や味わいが大きく異なるのです。
ラーメンの文化的側面も見逃せません。日本では、ラーメンは手軽に食べられるファーストフードとしてだけでなく、地域の特色を反映した文化的アイデンティティを持つ料理としても認識されています。ラーメン店一つをとっても、店主の哲学や技術が反映され、同じ種類のラーメンでも店によって全く異なる味わいを楽しむことができます。
さらに、ラーメンは国際的にも非常に人気が高く、世界各国で様々な形でアレンジされています。特にアメリカやヨーロッパでは、伝統的な日本のラーメンをベースにした新しいスタイルのラーメンが登場し、新たなファンを獲得しています。
日本国内においても、ラーメンは進化し続けており、最近ではヘルシー志向のラーメンや、ベジタリアン対応のラーメンなど、さまざまなニーズに応える形で展開されています。また、地域ごとの特色を活かした新しいタイプのラーメンが登場することもあり、その多様性はさらに広がりを見せています。
総じて、ラーメンはその多様性、地域ごとの特色、豊富なトッピングから、単なる麺料理を超えた、日本の食文化を代表する存在として、国内外で愛され続けています。その豊かな味わいとバリエーションが、世界中の多くの人々を魅了しているのです。
```
</details>

### 準備
#### ベクターストアへのドキュメント登録まで

取り込むドキュメント (`ramen.txt`) を細かく分割（チャンク化）し、それぞれに対して OpenAI の埋め込みモデルを使用してベクトル化していきます。
ベクトル化した情報は、Meta社が開発したベクトル検索ライブラリ **FAISS** を使用し、ベクターストアを作成します。

https://github.com/facebookresearch/faiss

https://python.langchain.com/docs/integrations/vectorstores/faiss

```python
from langchain.document_loaders import TextLoader
from langchain.embeddings import OpenAIEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.vectorstores import FAISS

loader = TextLoader("./ramen.txt")
documents = loader.load()
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=100, chunk_overlap=30
)
docs = text_splitter.split_documents(documents)

oai_embeddings = OpenAIEmbeddings()
db = FAISS.from_documents(docs, oai_embeddings)
```

`chunk_size`, `chunk_overlap` は適当です。
これらの値をいくつにするかもチューニングにおける検討対象になるでしょう。

#### ベクターストアからの関連情報の取得

ベクターストアに対して入力文をベクトル化して与えることで、ベクトル空間上での近傍探索を経て、入力文に類似のチャンクを取得することができます。

```python
questions = [
    "ラーメンはどこの国発祥ですか",
    "うどんの麺の特徴は？",
    "博多はどんなラーメンが多い？",
    "最近のラーメンの進化について教えてください",
]
answers = []
for question in questions:
    response = db.similarity_search(question, k=1)
    print(question)
    for res in response:
        print(res.page_content)
    print("-----")

>
ラーメンはどこの国発祥ですか
ラーメンは、中国から発祥し、日本で独自の進化を遂げた麺料理です。この料理は小麦粉から作られた細い麺と、鶏、豚、魚介など多様な材料から抽出されたスープ、そして様々な具材で構成されます。日本各地には地域ご
-----
うどんの麺の特徴は？
他の麺料理との比較では、麺の種類やスープ、具材に大きな違いが見られます。うどんは太くて柔らかい麺、そばはそば粉を使用した麺、パスタは小麦粉と卵で作られた麺で、それぞれ独自の食感と風味があります。また
-----
博多はどんなラーメンが多い？
たスープ、そして様々な具材で構成されます。日本各地には地域ごとに異なるラーメンスタイルが存在し、札幌の味噌ラーメン、博多のとんこつラーメン、東京の醤油ラーメンなどが特に有名です。
-----
最近のラーメンの進化について教えてください
形で展開されています。また、地域ごとの特色を活かした新しいタイプのラーメンが登場することもあり、その多様性はさらに広がりを見せています。
-----

```

入力文に対する回答が含まれていそうなチャンクを取得できていることが分かります。

#### RAGパイプラインの実装

取得した情報をLLMに渡し、最終的な応答を生成できるようにします。

LLM には Amazon Bedrock で使用可能な `anthropic.claude-instant-v1` を使用し、
`RetrievalQA`を用いて ユーザーの入力〜LLMによる応答の生成 までを行うパイプラインを作成します。

```python
from langchain.chains import RetrievalQA
from langchain.llms import Bedrock

llm = Bedrock(
    model_id="anthropic.claude-instant-v1",
    region_name="us-east-1",
    model_kwargs={"temperature": 0.0},
)

retriever = db.as_retriever(search_kwargs={"k": 1})
qa = RetrievalQA.from_chain_type(
    llm=llm, chain_type="stuff", retriever=retriever, verbose=True
)

```

#### RAGパイプラインを使用した問い合わせ

パイプラインに対して質問文を与えてみます。
`questions`に質問が入っているので、for文を使って実行します。

このとき、中間処理にて取得した関連情報を `contexts` に、最終的な出力を `answers` に格納していきます。
これらの情報は後ほど RAGAS による評価の際に必要になります。

```python
contexts = []
answers = []
for question in questions:
    res = db.similarity_search(question, k=1)
    contexts.append([r.page_content for r in res])
    answers.append(qa.run(question))

print(contexts)
print(answers)

> Finished chain.
[
    ['ラーメンは、中国から発祥し、日本で独自の進化を遂げた麺料理です。この料理は小麦粉から作られた細い麺と、鶏、豚、魚介など多様な材料から抽出されたスープ、そして様々な具材で構成されます。日本各地には地域ご'],
    ['他の麺料理との比較では、麺の種類やスープ、具材に大きな違いが見られます。うどんは太くて柔らかい麺、そばはそば粉を使用した麺、パスタは小麦粉と卵で作られた麺で、それぞれ独自の食感と風味があります。また'], 
    ['たスープ、そして様々な具材で構成されます。日本各地には地域ごとに異なるラーメンスタイルが存在し、札幌の味噌ラーメン、博多のとんこつラーメン、東京の醤油ラーメンなどが特に有名です。'], 
    ['形で展開されています。また、地域ごとの特色を活かした新しいタイプのラーメンが登場することもあり、その多様性はさらに広がりを見せています。']
]
[
    ' ラーメンは中国から発祥し、日本で独自の進化を遂げた麺料理です。\n\nTherefore, the context provided indicates that ramen originated from China.',
    ' うどんの麺の特徴は太くて柔らかい、とあります。',
    ' とんこつラーメンが多いです。',
    ' この文脈からは、ラーメンが形で展開されているとか、地域ごとに新しいタイプのラーメンが登場することがある、と書かれています。最近のラーメンの進化についての具体的な内容はこの文脈からは分かりません。'
]

```

一部出力が英語になっていたりしますが、与えられた情報の中から質問に対する答えをLLMが探し出し、その内容を踏まえた出力をしていることが分かります。

### RAGAS を使用した性能評価
ここからが本題となります。
入力と出力、中間処理でベクターストアから取得した情報に加え、入力に対する真の回答 `ground_truths` の4つを使用して、今回の実行結果を評価します。

#### 評価用データセットの作成
RAGAS で評価を実行するために、入力データを規定のフォーマットに変換します。
前述の `ground_truths` を加えた dict から `Dataset` 型のテストデータを作ります。

```python
from datasets import Dataset

ground_truths = [
    ["中国"],
    ["太くて柔らかい"],
    ["とんこつラーメン"],
    ["ヘルシー志向のものやベジタリアン対応、地域ごとの特色活かしたものの登場によって多様性が拡大している"],
]

ds = Dataset.from_dict(
    {
        "question": questions,
        "answer": answers,
        "contexts": contexts,
        "ground_truths": ground_truths,
    }
)

for row in ds:
    print(f'question: {row["question"]}')
    print(f'contexts: {row["contexts"]}')
    print(f'answer: {row["answer"]}')
    print(f'ground_truths: {row["ground_truths"]}')
    print("-----")

---
question: ラーメンはどこの国発祥ですか
contexts: ['ラーメンは、中国から発祥し、日本で独自の進化を遂げた麺料理です。この料理は小麦粉から作られた細い麺と、鶏、豚、魚介など多様な材料から抽出されたスープ、そして様々な具材で構成されます。日本各地には地域ご']
answer:  ラーメンは中国から発祥し、日本で独自の進化を遂げた麺料理です。

Therefore, the context provided indicates that ramen originated from China.
ground_truths: ['中国']
-----
question: うどんの麺の特徴は？
contexts: ['他の麺料理との比較では、麺の種類やスープ、具材に大きな違いが見られます。うどんは太くて柔らかい麺、そばはそば粉を使用した麺、パスタは小麦粉と卵で作られた麺で、それぞれ独自の食感と風味があります。また']
answer:  うどんの麺の特徴は太くて柔らかい、とあります。
ground_truths: ['太くて柔らかい']
-----
question: 博多はどんなラーメンが多い？
contexts: ['たスープ、そして様々な具材で構成されます。日本各地には地域ごとに異なるラーメンスタイルが存在し、札幌の味噌ラーメン、博多のとんこつラーメン、東京の醤油ラーメンなどが特に有名です。']
answer:  とんこつラーメンが多いです。
ground_truths: ['とんこつラーメン']
-----
question: 最近のラーメンの進化について教えてください
contexts: ['形で展開されています。また、地域ごとの特色を活かした新しいタイプのラーメンが登場することもあり、その多様性はさらに広がりを見せています。']
answer:  この文脈からは、ラーメンが形で展開されているとか、地域ごとに新しいタイプのラーメンが登場することがある、と書かれています。最近のラーメンの進化についての具体的な内容はこの文脈からは分かりません。
ground_truths: ['ヘルシー志向のものやベジタリアン対応、地域ごとの特色活かしたものの登場によって多様性が拡大している']


```

#### 評価の実行

評価には、`evaluate` 関数を使用します。

算出したい指標を個別に指定することも可能ですが、デフォルトで算出する指標は以下4つです。
- 忠実度 (`faithfulness`)
- 回答の関連性 (`answer_relevancy`)
- コンテキストの精度 (`context_precision`)
- コンテキストの再現性 (`context_recall`)

```python
from ragas import evaluate


"""
評価の実行
- answer_relevancy
- context_precision
- faithfulness
- context_recall
"""

result = evaluate(ds)
print(result)

---
{
    'answer_relevancy': 0.8139,
    'context_precision': 1.0000,
    'faithfulness': 1.0000,
    'context_recall': 1.0000
}
```

いずれの指標も高いスコアになりました。

`answer_relevancy`の値の理由ですが、この指標は `生成された回答が与えられたプロンプトにどれだけ関連しているか` を表現するものでした。
あくまで推測ですが、「`最近のラーメンの進化について教えてください`」の質問に対して、回答に「`地域ごとに新しいタイプのラーメンが登場することがある`」とはあるものの、元の文書にあった「ヘルシー志向」や「ベジタリアン」に関してや「**多様性が拡大している**」というところには達しておらず、結果として明確な回答ができなかったことが要因かと思われます。

このあたり、もう少し結果を詳細に見れるとよいのですが、記事作成時点では方法が分かっていません。

#### 評価データを一部変えてみる
さきほど評価に使用したデータを一部変更して、結果が変わることを確認しましょう。
ここでは、入力に対する真の回答として指定する `ground_truths` を変更します。

変更内容ですが、元の文書に含まれない内容、つまり誤った情報を使用します。
これらの内容はベクターストアから取得した情報にも含まれないため、パイプラインの出力が正解にたどり着く可能性はゼロに等しくなるはずです。

早速やってみましょう！

```python
fake_ground_truths = [
    ["アメリカ"],
    ["すごい"],
    ["塩ラーメン"],
    ["二足歩行する"],
]

fake_ds = Dataset.from_dict(
    {
        "question": questions,
        "answer": answers,
        "contexts": contexts,
        "ground_truths": fake_ground_truths,
    }
)

fake_result = evaluate(fake_ds)
print(fake_result)

---
{
    'answer_relevancy': 0.8484,
    'context_precision': 1.0000,
    'faithfulness': 1.0000,
    'context_recall': 0.0000
}
```

大きな変化として、`context_recall` のスコアが `1.0000` から `0.0000` になりました。
この指標は、「取得されたコンテキストが、真の回答とどれだけ一致しているか」を表すものでしたが、偽・真の回答 (`fake_ground_truths`) はコンテキストには含まれないので、この結果が妥当なものであると判断できます。

合わせて `answer_relevancy` のスコアも向上しています。
一瞬「おや？」と思いますが、この指標は **回答の正誤によるものではない** ため、あくまで **質問に対する答え方や振る舞い** が1回目よりも適切であると判断されたのでしょう。

## まとめ
RAGパイプラインの評価ツールである RAGAS を使用した評価と、評価対象データの内容が変更したのに伴い関連するスコアが上下することを確認しました。

チューニングの目的は、RAGパイプラインの出力パフォーマンスを最大化することですが、確立されたプラクティスは現状ありません。
これは、先日のOpenAI DevDay内のセッション (`A Survey of Techniques for Maximizing LLM Performance`[^1]) でも語られており、事前にベースラインを定義することと、小さな追加実装を加えた段階で評価を行い、ベースラインと常に比較しながら進めることが重要であると言われています。

実際、今回触れたチャンク分割時の設定以外にも、システムプロンプトの決定、Embeddingおよび応答の生成に使用するLLMの選定、ベクターストア/検索アルゴリズムの選定など、パフォーマンスに影響を与える要素は数多く存在します。

LLM・RAGを使用した開発事例やノウハウはまだまだ少ないですが、だからといって運任せで闇雲に頑張るのはつらすぎます。
ツールを活用しながらシステマチックに開発を行い、無敵になるべくして無敵になりましょう。

はしもと（仮名）でした。


## 参考
https://note.com/npaka/n/nc70d33764018

https://note.com/fukudawataru/n/n6730725b0dc0

https://qiita.com/RyeWiskey/items/c84556c0f3a164d52c2c

[^1]: https://www.youtube.com/watch?v=ahnGLM-RC1Y&ab_channel=OpenAI