【AOAI】RAGパイプラインの構築から評価フェーズまでの実装を一挙解説!【Ragas】

こんにちは、サイオステクノロジーの佐藤 陽です。
今回もRAGの構築に関する記事を書いていきます!

これまでも何本かRAGに関して書いてきましたが、
今回はそれらの集大成として、PDFを外部情報とするRAGを実装し、Ragasで評価するところまで、ソースコードと合わせて一挙ご紹介していこうと思います。

これを読めば、今日からRAGが構築ができるような記事になってます!
ぜひ最後までご覧ください!

はじめに

今回一番伝えたいことは、「評価を回しながらRAGの開発を進めてください!!」 という事です。
RAGというと、どうしても回答を出す部分に注目が行きがちですが、評価の方も非常に大切です。

生成AIを利用していることもあり、RAGの回答内容は不安定であるため、人間が評価するのが難しいことがよく言われています。

更にRAGを構築する要素の設計は多岐にわたります。

  • プロンプト変更
  • チャンキング戦略
  • 検索方法
  • LLMのモデル
  • and more!

そのため、今回扱うようなRagasを利用し、定量的に評価をしながら開発を進めていくことが大切になります。
今回の記事を通じて、評価までを含んだシステムを構築し、よきRAG開発ライフを送っていただければ幸いです。

ポイント

今回のポイントとしては以下の通りです。
詳細に関してはそれぞれリンクしてある記事でも紹介してます。

それぞれの技術要素については、これまでにも解説してきましたが
いざシステム化するとうまく動かなかったり、つなぎ方が分からない点もあるかと思うので
今回は、実際にこれらを組み合わせたアプリケーションを構築していきたいと思います。

そのため本記事では各要素について深い解説を行いません。
本記事で読んでいて分からない点があれば、各記事にて詳細を確認ください!

題材

今回RAGに組み込むPDFファイルとしては、デジタル庁が提供するテキスト生成AI利活用におけるリスクへの対策ガイドブックを利用したいと思います。

環境

今回利用する技術としては以下のようなものです。
併せて利用したpythonライブラリのバージョンも記載しておきます。

※ragasが現状最新の0.1.10を使った場合、エラー発生したのでバージョン注意です。

  • python(3.1.23)
  • LangChain(0.2.9)
  • Azure OpenAI Service
  • Azure AI Document Intelligence(1.0.0b3)
  • Azure AI Search(11.6.0b3)
  • Ragas(0.1.9)

なお、Azure OpenAI Serviceにデプロイするモデルとしては

  • Chat(ex. gpt-4o)
  • Embeddings(ex. text-embedding-ada-002)

をデプロイします。
本記事執筆時に利用しているモデルが将来的に廃止になる可能性があるため、その場合は適宜モデルを変更してください。

システム構成図としては以下のような形となります。

ステップ

全体のステップとしては以下の流れです。

  1. DocumentIntelligenceを利用し、PDFを読み込んでMarkdownとして出力する
  2. LangChainを利用して、Markdownのファイルをセマンティックチャンキングする
  3. セマンティックチャンキングした情報をベクトル化してAI Searchに格納する
  4. RAG Pipelineを構築する
    1. ユーザーからの質問に基づきAI Searchからコンテキスト情報を取得する
    2. ユーザーからの質問に対してテンプレートを適用する
    3. テンプレートを適用したプロンプトをAzure OpenAI Serviceに対して投げかけ、回答を得る
  5. Ragasを利用して評価する
    1. Ragasを利用して評価用のテストセットを構築する
    2. 4.1(context)4.3(answer)5.1(question, ground_truth)で得られたパラメータを用いてRAGパイプラインを評価する

ではそれぞれのステップを、ソースコードと合わせてみていきたいと思います。

事前準備

今回のプロジェクト構成としては以下の通りです。

└── Project/
    ├── rag.ipynb    //ソースコード
    ├── .env    //環境変数用のファイル
    ├── ai_guidebook.pdf    //RAGに組み込む情報
    └── documents/
        └── splits    //チャンク化した情報を格納するフォルダ
            ├── split_0.txt
            └── split_1.txt

ライブラリ

必要なライブラリのインストールとインポートを行います。

! pip install python-dotenv langchain langchain-community langchain-openai langchainhub openai tiktoken azure-ai-documentintelligence azure-identity azure-search-documents==11.6.0b3 ragas==0.1.9 unstructured ipywidgets

from langchain_openai import AzureChatOpenAI
from langchain_community.document_loaders import AzureAIDocumentIntelligenceLoader
from langchain_openai import AzureOpenAIEmbeddings
from langchain.schema import StrOutputParser
from langchain.schema.runnable import RunnablePassthrough
from langchain.text_splitter import MarkdownHeaderTextSplitter
from langchain.vectorstores.azuresearch import AzureSearch

環境変数

次に、環境変数の読み込みを行います。

事前にAzure上に作成したリソースから、エンドポイント名やkeyを参照し.envファイルに記載しておいてください。

今回必要となるリソースとしては以下3つです。

  1. Azure OpenAI Service
  2. Azure AI Search
  3. Azure AI Document Intelligence

※keyに関してはGitHubなどのバージョン管理ツールにはpushしないよう注意しましょう。

import os
from dotenv import load_dotenv

load_dotenv()

os.environ["AZURE_OPENAI_ENDPOINT"] = os.getenv("AZURE_OPENAI_ENDPOINT")
os.environ["AZURE_OPENAI_API_KEY"] = os.getenv("AZURE_OPENAI_API_KEY")
doc_intelligence_endpoint = os.getenv("AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT")
doc_intelligence_key = os.getenv("AZURE_DOCUMENT_INTELLIGENCE_KEY")

envファイルとしては以下のような内容になります。

AZURE_OPENAI_ENDPOINT = "https://{resource-name}.openai.azure.com/"
AZURE_OPENAI_API_KEY = "{KEY}"
AZURE_SEARCH_ENDPOINT = "https:/{resource-name}.search.windows.net"
AZURE_SEARCH_ADMIN_KEY = "{KEY}"
AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT= "https://{resource-name}.cognitiveservices.azure.com/"
AZURE_DOCUMENT_INTELLIGENCE_KEY = "{KEY}"

PDFの読み込み

まず、AI Document Intelligenceを利用し、PDFから文章をMarkdownの形式で抽出します。

Markdownで出力するためのパラメータはDefaultでONになっているため特に設定する必要はありません。
ただapi_modelとしては、Markdown出力が可能なモデルであるprebuild-layoutを選択しましょう。

今回、RAGに組み込むPDF(ai_guidebook.pdf)は本プログラムと同じ階層に置いてあります。

loader = AzureAIDocumentIntelligenceLoader(file_path="ai_guidebook.pdf", api_key = doc_intelligence_key, api_endpoint = doc_intelligence_endpoint, api_model="prebuilt-layout")
docs = loader.load()

セマンティックチャンキング

出力したMarkdownをLangchainのMarkdownHeaderTextSplitter関数を利用してチャンキングします。

今回はHeaderレベルに合わせてチャンク化の方を行いました。
このチャンク化の粒度というものもRAGnお品質に関わってくる部分になります。

チャンキングした情報を、ローカルに作成しておいたdocuments/splitsというフォルダの中に保存します。

# Split the document into chunks base on markdown headers.
headers_to_split_on = [
    ("#", "Header 1"),
    ("##", "Header 2"),
    ("###", "Header 3"),
]
text_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)


docs_string = docs[0].page_content
splits = text_splitter.split_text(docs_string)


for i, split in enumerate(splits):
    with open(f"documents/splits/split_{i}.txt", "w") as f:
        f.write(split.page_content)

AI Searchへの格納

次に、分割した情報をベクトルストアであるAzure AI Searchに登録していきます。
ここでもLangchain経由で操作を行います。

まずはAI Searchにアクセスするためのインスタンスを作成します。
ベクトル化のため、Azure AI Searchのコンストラクタのパラメータとして、embeddingsモデルのLLMを指定します。
embeddingsモデルに関しては事前にAzure OpenAI Service上でデプロイしておいてください。

aoai_embeddings = AzureOpenAIEmbeddings(
    azure_deployment="text-embedding-ada-002",
    openai_api_version="2024-02-15-preview",  # e.g., "2023-12-01-preview"
)

vector_store_address: str = os.getenv("AZURE_SEARCH_ENDPOINT")
vector_store_password: str = os.getenv("AZURE_SEARCH_ADMIN_KEY")

index_name: str = "sios_sample_index"
vector_store: AzureSearch = AzureSearch(
    azure_search_endpoint=vector_store_address,
    azure_search_key=vector_store_password,
    index_name=index_name,
    embedding_function=aoai_embeddings.embed_query,
)

実行すると、AI Search上にインデックスが作成できていることが確認できます。

まだこのインデックスには何も情報が含まれていないため、先程チャンク化した情報を追加します。
add_documentsを誤って複数回実行すると、のちの関連情報の取得に影響出るため注意してください。

from langchain_community.document_loaders import DirectoryLoader
loader = DirectoryLoader("documents/splits")
documents = loader.load()

vector_store.add_documents(documents)

少し時間が経過するとデータの追加が確認できます。

RAGパイプラインの構築

実際にRAGのパイプラインを構築していきます。
LangChainの LCEL を利用し、チェーンを組み立てます。

Chainの構築に関してはこちらのブログで紹介しています。
今回組み立てるChainは2本です。

  1. Answerを得るためのChain
  2. Contextを得るためのChain

Answer用のChain

ユーザーからの質問に対する回答を得るためのChainです。
以下のようなChainを組みます

  1. ChatLLMに対して投げるプロンプトのためのテンプレートを構築
  2. questionに対して、ベクトルストアから関連情報を収集
  3. 関連情報と質問事項をテンプレートに組み込み、プロンプトを投げる
  4. 回答を出力する

Context用のChain

ユーザーからの質問に対する関連情報を得るためのChainです。
こちらのChainに関しては1ステップのみです。

  1. questionに対して、ベクトルストアから関連情報を収集

RAGを構築するためだけであればこのパイプラインは必須ではありません。
今回はRagasでの評価にて利用するため構築します。

from langchain_core.prompts import ChatPromptTemplate, HumanMessagePromptTemplate
from langchain_core.messages import SystemMessage

llm = AzureChatOpenAI(
    openai_api_version="2024-02-01",  # e.g., "2023-12-01-preview"
    azure_deployment="gpt-35-turbo-16k",
    temperature=0,
)

prompt = ChatPromptTemplate.from_messages(
    [SystemMessage(
         """質問に対して、関連情報を参照に回答してください。
         関連する情報を参照しても分からない場合は、「分かりません」と回答してください。"""
    ),
     HumanMessagePromptTemplate.from_template(
         """ 関連情報:{context}
           
         ## 質問:{question}
         ## 回答: """
    )
     ]
    )

retriever = vector_store.as_retriever(search_type="similarity")

# Documetを連結する
def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

# answerを得るためのchain
answer_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)

# contextを得るためのchain
context_chain = retriever

回答取得

ここでRAGのパイプラインが構築できたので、RAGから回答の方を取得したいと思います。
実行する内容としては、answerのchainに質問を与えてあげればOKです。

answer_chain.invoke("本資料の対象の読者は誰ですか?")
得られた回答としては以下の通りです。
'本資料の対象の読者は、テキスト生成AIのサービス開発者やサービス提供者です。'
これでRAGのパイプラインの構築は完了です。
では続いて評価のフェーズに映りたいと思います。

Ragasでの評価

評価の流れとしては以下のような感じです。

  1. Ragasでテストセット作成
    1. questionとground_truthを生成
  2. Ragasで生成されたquestionを入力としてRAGのパイプラインを実行
    1. answerとcontextを生成
  3. question, ground_truth, context, answerのパラメータをRagasに与えて評価を行う

テストセットの作成

Ragasでテストセット(評価に用いるQuestion,Ground_truth)を作成する場合は filenameのmetadataを保持している必要があります。

そのため、今回の場合は sourceのmetadata(ファイル名)をfilenameの値として設定します。

Each Document object contains a metadata dictionary, which can be used to store additional information about the document accessible via Document.metadata. Ensure that the metadata dictionary includes a key called filename, as it will be utilized in the generation process. The filename attribute in metadata is used to identify chunks belonging to the same document. For instance, pages belonging to the same research publication can be identified using the filename.

ref : Generate a Synthetic Test Set

from langchain_community.document_loaders import DirectoryLoader

loader = DirectoryLoader("documents/splits")
documents = loader.load()

for document in documents:
    document.metadata['filename'] = document.metadata['source']

次に、Ragasの機能でテストセットを作成します。
テストセットの作成にはLLMを利用するため、Azure OpenAI Service上にChatモデルとEmbeddingsモデルをデプロイしておきます。
デプロイしたモデルのapi versionや、モデル名を各種パラメータに設定します。

また今回は日本語でテストセットを作成するため、adaptのlanguageの設定をjapaneseとします。
その他、test_sizeやdistributionsの値は適宜調整してください。

from ragas.testset.generator import TestsetGenerator
from ragas.testset.evolutions import simple, reasoning, multi_context
from langchain_openai import AzureChatOpenAI, AzureOpenAIEmbeddings

# generator with openai models
generator_llm = AzureChatOpenAI(
    openai_api_version="2024-02-01",
    azure_deployment="gpt-35-turbo-16k",
)
critic_llm = AzureChatOpenAI(
    openai_api_version="2024-02-01",
    azure_deployment="gpt-35-turbo",
)
embeddings = AzureOpenAIEmbeddings(
    openai_api_version="2024-02-01",
    azure_deployment="text-embedding-ada-002"
)

generator = TestsetGenerator.from_langchain(
    generator_llm,
    critic_llm,
    embeddings
)

generator.adapt(
    language="japanese",
    evolutions=[simple, reasoning, multi_context]
)

# generate testset
testset = generator.generate_with_langchain_docs(documents, test_size=2, distributions={simple: 0.5, reasoning: 0.25, multi_context: 0.25})

テストセットの表示

どのようなテストセットが作成されたか見てみます。

testset.to_pandas()

もちろんこれらのテストセットは自分で用意してもらってもOKです。
ただ、Ragasで生成できるようにしておくと、膨大なテスト量をこなせたり、自動化したりしやすいのでよいですね。

評価項目のインポート

Ragasで評価する項目をimportし、あとで設定可能なようにListとして定義しておきます。

Ragasには多くの評価項目がありますが、全てを評価しようとするとLLMのトークン数を多く利用します。
その場合、リソースが枯渇したり、思わぬ金額が請求されたりするため注意してください。

今回は以下の4つのみを評価対象とします。

  • context_precision
  • faithfulness
  • context_recall
  • context_relevancy
from ragas.metrics import (
    context_precision,
    faithfulness,
    context_recall,
    context_relevancy
)

# list of metrics we're going to use
metrics = [
    context_precision,
    faithfulness,
    context_recall,
    context_relevancy
]

評価パラメータの整備

評価に使うための

  • question
  • ground_truth
  • context
  • answer

を用意していきます。

questionとground_truthに関しては、先程Ragasの機能で生成したtestsetの値を利用します。
この時、回答が生成されないケース(nan)があるため、”回答無し”という表記に変換します。

次に、先程構築した2つのchainを利用して、contextとanswerの内容を取得します。

df = testset.to_pandas()
df = df.fillna("回答なし") #nanは"回答なし"に変換する

def get_answer_contexts(question: str):
    answer = answer_chain.invoke(question)
    # langchainのDocumentからRagas評価時に使うテキストデータだけ取り出す。
    contexts = context_chain.invoke(question)
    contexts = [c.page_content for c in contexts]
    return {"answer": answer, "contexts": contexts}

results = [get_answer_contexts(s) for s in df["question"]]

評価パラメータのセット

作成した各種パラメータをdatasetsの中に代入します。

from datasets import Dataset

result_ds = Dataset.from_dict(
    {
        "question" : df["question"],
        "answer" : [r["answer"] for r in results],
        "contexts": [r["contexts"] for r in results],
        "ground_truth": df["ground_truth"]
    }
)

result_ds.to_pandas()

評価に利用するモデルの設定

評価に利用するモデルを用意します。
先程まで利用していたモデルと同一のものでも、別に用意してただいてもOKです。

from langchain_openai.chat_models import AzureChatOpenAI
from langchain_openai.embeddings import AzureOpenAIEmbeddings
from ragas import evaluate


chat_llm = AzureChatOpenAI(
    openai_api_version="2024-02-01",
    azure_deployment="gpt-35-turbo"
)
embeddings = AzureOpenAIEmbeddings(
    openai_api_version="2024-02-01",
    azure_deployment="text-embedding-ada-002"
)

評価の実施

evaluate関数で実際に評価を行います。

result = evaluate(
    result_ds, metrics=metrics, llm=chat_llm, embeddings=embeddings
)

結果の表示

RAGパイプライン全体の結果の表示

result

以下のような結果となりました。

{'context_precision': 0.9028, 'faithfulness': 0.5000, 'context_recall': 0.5000, 'context_relevancy': 0.1732}

次に、各質問に対する結果の表示をします。

 result.to_pandas()

まとめ

今回は、RAGのパイプライン構築から、Ragasでの評価まで一気に解説しました。

最初にも書きましたが、構築とともに評価を行っていることが今回の大きなポイントです。
「開発して、評価して、開発して、評価して…」と、いいループが回せるとよいかと思います。

是非今回の内容をベースに、RAGの構築の方に取り組んでみてください!
ではまた!

参考文献

 

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

1人がこの投稿は役に立ったと言っています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です