生成AIアプリの出力をRagasで評価して、LangfuseでGUI監視しよう！

先日、以下の記事でLLMアプリの評価ツール「Ragas」について紹介しました。

https://qiita.com/minorun365/items/2f4e238f8bbc6e393ba5

RagasはPythonライブラリであり、LLMアプリの出力を評価してスコアを算出してくれますが、そのままだと日々運用監視するのが大変です。

そこで、LLMアプリ監視ツールとして人気のあるLangfuseを使って、スコアをGUIで管理できるようにしてみましょう。


# Langfuseとは？

LLMアプリ専用の監視ツールです。
メトリクスやトレースの監視、プロンプト管理、出力の評価など多くのことができます。

https://langfuse.com/

この分野では、LangChain社が提供しているLangSmithという有名な製品がありますが、LangSmithはSaaS型での提供を中心としており、セルフホストするためにはEnterpriseプランへの加入が必要です。

そのため、誰でも手軽に利用できるOSSとして開発されているのが、今回紹介するLangfuseです。


# ちなみにRagasは最近アップデートされました

LLMアプリの出力を評価するPythonライブラリのRagasですが、先週10/15にv0.1系からv0.2系への大きめなアップデートが行われました。

https://docs.ragas.io/en/stable/howtos/migrations/migrate_from_v01_to_v02/

例えば、以下のような変更が含まれています。

- 新たな評価用メトリクスが多数追加：もともとRagasは名前のとおりRAGアプリの評価を主眼としたツールでしたが、新しいメトリクスではAIエージェントなど、最近のトレンドを含めた広いLLMアプリの評価に対応しています。
- メトリクス関連のコーディング方法の変更：v0.1系では評価の際、小文字で始まるメトリクス名（初期化済みクラス）をインポートして `evaluate()` 関数で利用していましたが、v0.2系ではメトリクスごとにLLMを指定する記法が推奨されています。

なお、v0.2系で非推奨となったコードはしばらく警告とともに利用できますが、v0.3系のリリース時に利用不可となるようです。（これはLangChainと同じですね）


# 実際に動かしてみよう！

### Langfuseをローカルで立ち上げる

Langfuseでは、以下のデプロイ方式がサポートされています。

- クラウド：マネージド版をSaaSとして利用
- セルフホスト：AWSなどのパブリッククラウドに自分でデプロイ
- ローカル：作業PC上にデプロイ（検証用）

https://langfuse.com/docs/deployment/local

今回は一番お手軽なローカルデプロイを試してみましょう。
最初に作業PCへDocker Desktopをインストールして起動します。

https://www.docker.com/ja-jp/

その後、ターミナルを起動して以下のコマンドを実行し、Langfuseのソースコードを作業PCへクローンして起動します。

```zsh: ターミナル
# GitHubからLangfuseのリポジトリをクローンする
git clone https://github.com/langfuse/langfuse.git

# クローンしたディレクトリへ移動する
cd langfuse

# Langfuseの本体サーバーのコンテナと、DB用のコンテナをまとめて起動する
docker compose up
```

ターミナルに `langfuse-server-1  |  ✓ Ready in XXXXms` といった表示がされたら無事に起動しています。ChromeなどのWebブラウザを立ち上げて、`localhost:3000` にHTTPでアクセスしてみましょう。

最初に自分用のユーザー、Organization（組織）、Projectを作成しましょう。名前は好きな文字列で大丈夫です。


### RAGアプリをLangfuseで監視してみる

シンプルなRAGアプリをLangChainで記述し、実行結果をLangfuseに表示してみましょう。

先ほどのLangfuseとは全く別の場所で大丈夫なので、以下のPythonファイルを作成してみます。

```py: rag-with-langfuse.py
# 必要なライブラリのインポート
import os
from dotenv import load_dotenv
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
from langchain_community.vectorstores import FAISS
from langchain_aws import ChatBedrockConverse, BedrockEmbeddings
from langfuse.callback import CallbackHandler

# Langfuseを初期化
load_dotenv()
langfuse_handler = CallbackHandler(
    secret_key=os.environ.get("LANGFUSE_SECRET_KEY"),
    public_key=os.environ.get("LANGFUSE_PUBLIC_KEY"),
    host=os.environ.get("LANGFUSE_HOST"),
)

# 質問を設定
question = "かぐたんって何？"
print("【質問】", question)

# LLMと埋め込みモデルを設定
llm = ChatBedrockConverse(model="anthropic.claude-3-5-sonnet-20240620-v1:0")
embeddings = BedrockEmbeddings(model_id="amazon.titan-embed-text-v2:0")

# ベクトルストアを作成
texts = [
    "情報1：KDDIアジャイル開発センター株式会社は、KAGという略称で親しまれています",
    "情報2：KAG社は、かぐたんというSlackアプリを開発しました",
]
vectorstore = FAISS.from_texts(texts=texts, embedding=embeddings)

# RAGチェーンを定義
retriever = vectorstore.as_retriever()
prompt = ChatPromptTemplate.from_template(
    "背景情報をもとに質問に回答してください。背景情報： {context} 質問： {question}"
)
chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)

# RAGチェーンを実行して回答を生成
answer = chain.invoke(
    question, 
    config={
        "callbacks": [langfuse_handler],
        "metadata": {"ground_truth": "かぐたんとは、KAG社が開発したSlack用チャットボットです"},
    }
)
print("【RAGアプリの回答】", answer)
```

回答生成や埋め込みに必要なモデルは、AWSのAmazon Bedrockを利用しています。
バージニア北部リージョンで「Claude 3.5 Sonnet」と「Titan Text Embeddings V2」を有効化しておいてください。

https://docs.aws.amazon.com/ja_jp/bedrock/latest/userguide/model-access-modify.html

また、ローカルPCへ上記AWSアカウントへの認証情報を設定しておいてください。

https://docs.aws.amazon.com/ja_jp/cli/v1/userguide/cli-authentication-short-term.html

今回のPythonコードでは、セキュリティリスクを軽減するため、Langfuseへの接続に必要なAPIキーを別ファイルに分離する前提としています。

Pythonファイルと同じ階層に、以下の `.env` ファイルを作成してください。LangfuseのAPIキーは、先ほどアクセスした管理画面の `Settings > API Keys` から新規に発行して記載しましょう、

```bash: .env
LANGFUSE_SECRET_KEY=sk-（以下略）
LANGFUSE_PUBLIC_KEY=pk-（以下略）
LANGFUSE_HOST=http://localhost:3000
```

その後、必要なPythonライブラリをインストールしてからアプリを実行してみます。

```zsh: ターミナル
# 必要なライブラリのインストール
pip install -U python-dotenv boto3 langchain langchain-aws langfuse ragas faiss-cpu nltk

# RAGアプリの実行
python rag-with-langfuse.py
```

※もし不足しているライブラリがあれば、追加で `pip install` してください。

正常に実行されると、以下のようにRAGアプリの実行結果が出力されます。

```zsh: ターミナル
【質問】 かぐたんって何？
【RAGアプリの回答】 かぐたんは、KAG社（KDDIアジャイル開発センター株式会社）が開発したSlackアプリです。

具体的な機能や用途については背景情報に詳細が記載されていませんが、Slackで使用するためのアプリケーションであることがわかります。Slackアプリは通常、業務効率化やコミュニケーション支援などの目的で開発されることが多いですが、かぐたんの具体的な機能については追加の情報が必要です。
```

その後、Langfuseの管理画面を覗いてみると、`Tracing > Traces` にいま実行した結果が記録されています。

![スクリーンショット 2024-10-22 1.06.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/23caa959-800a-6a02-fa95-c75f8b85ff8f.png)

これがLangfuseの基本的な使い方でした。
ここに、Ragasの評価結果も登録してみましょう。


### Ragas評価結果をLangfuseに送信する

前述のPythonコードにRagasの評価処理を追加したものが以下です。

```py: ragas-with-langfuse.py
# 必要なライブラリのインポート
import os, uuid, asyncio
from dotenv import load_dotenv
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
from langchain_community.vectorstores import FAISS
from langchain_aws import ChatBedrockConverse, BedrockEmbeddings
from langfuse import Langfuse
from langfuse.callback import CallbackHandler
from ragas.llms import LangchainLLMWrapper
from ragas.embeddings import LangchainEmbeddingsWrapper
from ragas.run_config import RunConfig
from ragas.dataset_schema import SingleTurnSample
from ragas.metrics.base import MetricWithLLM, MetricWithEmbeddings
from ragas.metrics import ResponseRelevancy, Faithfulness, LLMContextRecall, LLMContextPrecisionWithoutReference

# Langfuseを初期化
load_dotenv()
langfuse = Langfuse()
langfuse_handler = CallbackHandler(
    secret_key=os.environ.get("LANGFUSE_SECRET_KEY"),
    public_key=os.environ.get("LANGFUSE_PUBLIC_KEY"),
    host=os.environ.get("LANGFUSE_HOST"),
)
trace_id = str(uuid.uuid4())

# 質問を設定
question = "かぐたんって何？"
print("【質問】", question)

# LLMと埋め込みモデルを設定
llm = ChatBedrockConverse(model="anthropic.claude-3-5-sonnet-20240620-v1:0")
embeddings = BedrockEmbeddings(model_id="amazon.titan-embed-text-v2:0")

# ベクトルストアを作成
texts = [
    "情報1：KDDIアジャイル開発センター株式会社は、KAGという略称で親しまれています",
    "情報2：KAG社は、かぐたんというSlackアプリを開発しました",
]
vectorstore = FAISS.from_texts(texts=texts, embedding=embeddings)

# RAGチェーンを定義
retriever = vectorstore.as_retriever()
prompt = ChatPromptTemplate.from_template(
    "背景情報をもとに質問に回答してください。背景情報： {context} 質問： {question}"
)
chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)

# Ragas評価用のデータを定義
chunks = [doc.page_content for doc in retriever.invoke(question)]
ground_truth = "かぐたんとは、KAG社が開発したSlack用チャットボットです"

# RAGチェーンを実行して回答を生成
answer = chain.invoke(
    question, 
    config={
        "run_id": trace_id,
        "callbacks": [langfuse_handler],
        "metadata": {"ground_truth": ground_truth},
    }
)
print("【RAGアプリの回答】", answer)

# Ragasメトリクスを初期化
metrics = [
    Faithfulness(), # 忠実性：背景情報と一貫性のある回答ができているか
    ResponseRelevancy(), # 関連性：質問と関連した回答ができているか
    LLMContextRecall(), # 文脈精度：質問や正解に関連した背景情報を取得できているか
    LLMContextPrecisionWithoutReference(), # 文脈回収：回答に必要な背景情報をすべて取得できているか
]

# RagasメトリクスごとにLLMや埋め込みモデルを設定
for metric in metrics:
    if isinstance(metric, MetricWithLLM):
        metric.llm = LangchainLLMWrapper(llm)
    if isinstance(metric, MetricWithEmbeddings):
        metric.embeddings = LangchainEmbeddingsWrapper(embeddings)
    run_config = RunConfig()
    metric.init(run_config)

# Ragas評価を非同期関数として定義
async def score_with_ragas():
    scores = {}
    for metric in metrics:
        sample = SingleTurnSample(
            question=question, 
            user_input=question,
            contexts=chunks, 
            retrieved_contexts=chunks,
            reference=ground_truth,  
            answer=answer, 
            response=answer,
        )
        scores[metric.name] = await metric.single_turn_ascore(sample)
        print(f"【{metric.name}】のスコア：{scores[metric.name]}")

        # Langfuseに評価スコアを登録
        langfuse.score(name=metric.name, trace_id=trace_id, value=scores[metric.name])

# Ragas評価を非同期で実行
asyncio.run(score_with_ragas())
```

これも実行してみましょう。

```zsh: ターミナル
python ragas-with-langfuse.py
```

正常に動作すると、以下のように実行結果がターミナルに出力されます。

```zsh: ターミナル
/Users/minorun365/.pyenv/versions/3.12.7/lib/python3.12/site-packages/ragas/prompt/base.py:9: LangChainDeprecationWarning: As of langchain-core 0.3.0, LangChain uses pydantic v2 internally. The langchain_core.pydantic_v1 module was a compatibility shim for pydantic v1, and should no longer be used. Please update the code to import from Pydantic directly.

For example, replace imports like: `from langchain_core.pydantic_v1 import BaseModel`
with: `from pydantic import BaseModel`
or the v1 compatibility namespace if you are working in a code base that has not been fully upgraded to pydantic 2 yet.         from pydantic.v1 import BaseModel

  from ragas.llms.prompt import PromptValue
【質問】 かぐたんって何？
【RAGアプリの回答】 かぐたんは、KAG社（KDDIアジャイル開発センター株式会社）が開発したSlackアプリです。

具体的な機能や特徴については背景情報に詳細が記載されていませんが、Slackで使用するためのアプリケーションであることは分かります。Slackアプリは一般的に、コミュニケーションの効率化や業務プロセスの改善などを目的として開発されることが多いです。
【faithfulness】のスコア：0.7142857142857143
【answer_relevancy】のスコア：0.9465694385281352
【context_recall】のスコア：1.0
【llm_context_precision_without_reference】のスコア：0.9999999999
```

※なぜか `answer_relevancy` の処理が出力パーサーのエラーになることがあります。その場合は何度か再実行してみてください。

前半は、Ragas内部で使っているLangChainのバージョンが古いことに伴う警告のため、気にしないでOKです。
RAGアプリの回答のあとに、Ragasの代表的な4つのメトリクスを算出して表示しています。

これをLangfuseで再度確認してみましょう。

![スクリーンショット 2024-10-22 1.19.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/793b58a0-aba4-b91c-4ddf-9fac7e536d4c.png)

Ragasのスコアが、トレースに正しく付与されていますね 🎉