【LLMOps】あなたの生成AIアプリを監視＆評価しよう！ Langfuse入門ハンズオン

# これは何？

コミュニティ勉強会「Langfuse Night #2」で実施するハンズオンセッションの手順書です。

https://connpass.com/event/345731/

イベントに参加されていない方も、本手順をそのまま実施することでLangfuseに入門できますのでご活用ください。

:::note warn
実施にあたり、AWSアカウントへの課金（Bedrockの推論コスト＝数十円以内が目安）の発生が予想されます。金額含め、自己責任のうえ実施をお願いします。
:::

### Langfuseとは？

LLMアプリケーションの監視やデバッグに役立つ統合ツールです。無料で自分の環境にデプロイできるセルフホスト版と、SaaS型で使えるCloud版があります。

https://speakerdeck.com/minorun365/llmapuriworagasdeping-jia-site-langfusedeke-shi-hua-siyou

本ハンズオンではお手軽なCloud版を利用します。個人利用なら無料で使い始められます。


# 事前準備

### Langfuseアカウントの作成

公式サイト右上の「Sign Up」からCloud版にサインアップしましょう。

https://langfuse.com/

GoogleやGitHubのアカウントと連携すると楽ですね。
リージョンの選択がありますが、EU/USいずれでもOKです。

### AWSアカウントの作成

以下を参考にサインアップしましょう。クレジットカードが必要です。

https://aws.amazon.com/jp/register-flow/


# 1. 元となるLLMアプリを作成しよう

### Bedrockモデルの有効化

- AWSマネジメントコンソールにサインイン（ https://console.aws.amazon.com/ ）し、ホーム画面右上からリージョンを「オレゴン」に切り替えます。
- 検索ボックスから「Bedrock」を検索し、Bedrockのコンソールに移動します。
- 左サイドバー下部の「モデルアクセス」より、「すべてのモデルを有効にする」を選択してウィザードを進めます。
- 「ユースケースの詳細」では自分の会社名などをざっくり入力します。対象ユーザーは「社内の従業員」、ユースケースの説明は「ハンズオン」と記載して進めましょう。

![スクリーン ショット 2025-03-24 に 23.14.55 午後.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/3aa32ba0-5328-4a38-9ce9-c02fa7f78ab8.png)

- リクエストを送信すると、数分で生成AIモデルへアクセスできるようになります。待っている間に次の手順へ進みましょう。
- リクエスト送信直後、一部の古いモデルについてエラーが出ますが、今回利用しないため無視して問題ありません。

![スクリーン ショット 2025-03-24 に 23.18.48 午後.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/57150bcd-4340-424c-8206-147e904b761a.png)

:::note
本ハンズオンでは、AWSのオレゴンリージョンのみを利用します。Bedrockのモデル呼び出しレートリミットが緩いためです。
:::

### LLMアプリの実行

- 検索ボックスから「CloudShell」を検索し、CloudShellを全画面で開きます。「ようこそ」ポップアップは閉じてください。

![スクリーン ショット 2025-03-24 に 23.22.11 午後.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/a76fbf72-4ea7-45a8-bc3a-64116096681d.png)

- 作業PCのローカルでテキストエディタ（メモ帳など）を開き、以下のコードをコピペして「1_bedrock.py」という名前で保存します。

```py: 1_bedrock.py
import boto3

# Bedrockの推論APIを実行する関数
def invoke_bedrock(prompt):
	bedrock = boto3.client("bedrock-runtime")
	response = bedrock.converse(
		modelId="us.anthropic.claude-3-7-sonnet-20250219-v1:0",
		messages=[
			{
				"role": "user",
				"content": [{"text": prompt}]
			}
		]
	)
	output = response["output"]["message"]["content"][0]["text"]
	return output

# 上記の関数を呼び出し、結果を画面出力するメイン関数
def main(input):
	output = invoke_bedrock(input)
	print(output)
	return output

# メイン関数に入力を与えて呼び出す
main("Langfuseって何？")
```

:::note warn
最近のOSであれば大丈夫だと思いますが、テキストの保存フォーマットはShift_JISではなくUTF-8になるようにしてください。
:::

- CloudShell画面右上の「アクション > ファイルアップロード」から、上記ファイルをアップロードします。
- CloudShellのターミナルで `python 1_bedrock.py` というコマンドを実行して、アプリの出力結果が表示されることを確認しましょう。

:::note
もしアップロード後に内容を修正したい場合は、CloudShellで `rm 1_bedrock.py` を実行して当該ファイルを一度削除し、修正版のファイルを再度アップロードしてください。
:::


# 2. トレースを記録しよう

### Langfuse Cloudのセットアップ

- Langfuseの管理画面にアクセス（ https://cloud.langfuse.com ）し、サインインします。
- 「New Organization」をクリックして、好きな名前でOrganizationを作成します。（例：自分のニックネームなど）

![スクリーン ショット 2025-03-24 に 23.29.02 午後.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/0e2c457b-0e2f-44e9-b03e-dd0824467701.png)

- 「Organization Members」はデフォルトのまま「Next」を押して進みます。
- 「New Project」画面で、今回のハンズオン用のプロジェクトを `langfuse-night` という名前で作成します。
- 「Create API Key」ボタンを押して認証情報を表示させます。
- 作業PCでメモ帳などのテキストエディタを開き、以下のコマンドをコピペします。各行イコールの右辺に自分のLangfuse認証情報を上書きして、このメモは後でまた使えるように開いておきましょう。（機密情報のため、保存せずにハンズオン後削除することをお勧めします）

```bash: メモ帳
export LANGFUSE_SECRET_KEY=sk-XXXXXXXXXXXXXXXXXXXXXX
export LANGFUSE_PUBLIC_KEY=pk-XXXXXXXXXXXXXXXXXXXXXX
export LANGFUSE_HOST=https://XXXXXXX.langfuse.com
```

- 自分のLangfuse認証情報で上書きした上記コマンドを、CloudShellにコピペして実行します。（「複数行テキストの貼り付け」警告が出ますが、問題ないので貼り付け実行します。）すると環境変数が設定されて、そのターミナルのタブを閉じるまではLangfuseへの接続設定が記憶されます。


### 自分のアプリにトレースの計装を行う

- 作業PCのローカルでテキストエディタ（メモ帳など）を開き、以下のコードをコピペして「2_trace.py」という名前で保存します。

```py: 2_trace.py
import boto3
from langfuse.decorators import observe # 追加

@observe() # 追加
def invoke_bedrock(prompt):
	bedrock = boto3.client("bedrock-runtime")
	response = bedrock.converse(
		modelId="us.anthropic.claude-3-7-sonnet-20250219-v1:0",
		messages=[
			{
				"role": "user",
				"content": [{"text": prompt}]
			}
		]
	)
	output = response["output"]["message"]["content"][0]["text"]
	return output

@observe() # 追加
def main(input):
	output = invoke_bedrock(input)
	print(output)
	return output

main("Langfuseって何？")
```

:::note
元のコードに対して、Langfuseのモジュールをインポートして `@observe()` のデコレーションを追加しているだけです。簡単ですね！
:::

- CloudShell画面右上の「アクション > ファイルアップロード」から、上記ファイルをアップロードします。
- CloudShellで `pip install langfuse` を実行して、Python用のLangfuseライブラリをインストールします。
- CloudShellのターミナルで `python 2_trace.py` を実行して、アプリの出力結果が表示されることを確認しましょう。
- Langfuseのダッシュボードに移動し、左サイドバーの「Tracing > Traces」から実行結果のトレースを確認してみましょう。

![スクリーン ショット 2025-03-24 に 23.44.03 午後.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/0d166196-4da6-450b-b244-8222326b6404.png)


# 3. プロンプトを管理しよう

### Langfuseにプロンプトテンプレートを登録する

- Langfuseの左サイドバー「Prompts」をクリックし、「Create Prompt」を実行して以下のプロンプトを作成しましょう。
    - Name: `what-is`
    - Prompt: `{{name}} って何？`
    - 他はデフォルトのままでOK

![スクリーン ショット 2025-03-24 に 23.49.05 午後.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/c9b89c3d-fdbc-4c85-9945-5e40b60d4edc.png)


### 自分のアプリからLangfuseのプロンプトを呼び出す

- 作業PCのローカルでテキストエディタ（メモ帳など）を開き、以下のコードをコピペして「3_prompt.py」という名前で保存します。

```py: 3_prompt.py
import boto3
from langfuse.decorators import observe
from langfuse import Langfuse # 追加

# 追加：Langfuseからプロンプトを取得する関数
def get_prompt(word):
	langfuse = Langfuse()
	prompt = langfuse.get_prompt("what-is")
	compiled_prompt = prompt.compile(name=word)
	return compiled_prompt

@observe()
def invoke_bedrock(prompt):
	bedrock = boto3.client("bedrock-runtime")
	response = bedrock.converse(
		modelId="us.anthropic.claude-3-7-sonnet-20250219-v1:0",
		messages=[
			{
				"role": "user",
				"content": [{"text": prompt}]
			}
		]
	)
	output = response["output"]["message"]["content"][0]["text"]
	return output

@observe()
def main(input):
	prompt = get_prompt(input) # 追加
	output = invoke_bedrock(prompt)
	print(output)
	return output

main("Langfuse") # 引数を変更
```

:::note
元のコードに対して、「プロンプト取得関数」を追加しています。内容は、Langfuseインスタンスを作成して `get_prompt` メソッドを実行しているだけです。

受け取ったキーワードをプロンプトテンプレート内の変数に代入していることが分かると思います。
:::

- CloudShell画面右上の「アクション > ファイルアップロード」から、上記ファイルをアップロードします。
- CloudShellのターミナルで `python 3_prompt.py` を実行して、アプリの出力結果が表示されることを確認しましょう。
- ちゃんとプロンプトテンプレートが使われているか、Langfuseトレース内の `invoke_bedrock` 関数の入力を確認してみましょう。

![スクリーン ショット 2025-03-25 に 00.04.17 午前.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/f22bacf6-7ef9-44aa-b09c-ee34edeb98f0.png)


# 4. LLMアプリの生成結果を評価しよう

### IAMユーザーを作成し、アクセスキーを取得する

- AWSマネコンで「IAM」と検索し、IAMコンソールにアクセスします。
- 左サイドバーの「ユーザー」より、「ユーザーの作成」をクリックして以下のように設定します。（記載ないものはデフォルトでOK）
    - ユーザー名：`langfuse`
    - 許可のオプション：ポリシーを直接アタッチする
    - 許可ポリシー：「AmazonBedrockFullAccess」を検索してチェック

![スクリーン ショット 2025-03-25 に 00.21.50 午前.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/bfa262d9-412c-4949-a004-cf92d6abda30.png)

- 作成後、ユーザー名「langfuse」をクリックして「アクセスキーを作成」をクリックします。（記載ないものはデフォルトでOK）
    - ユースケース：サードパーティーサービス
    - 「上記のレコメンデーションを理解し、アクセスキーを作成します。」にチェック
    - 表示されたアクセスキーとシークレットアクセスキーを、このあとすぐ使います。この画面は一度閉じると再度開けないので、触らずに置いておいてください。


### Langfuseに評価器を設定する

- Langfuse左サイドバーから「Evaluation > LLM-as-a-Judge」を開き、「Create Evaluator」をクリックします。
- 「Select a template」から「Create new template」を選び、以下のように設定します。（記載ないものはデフォルトでOK）
    - Select a Langfuse managed template: Correctness
    - Model > 「+ Add new LLM API key」をクリック
        - Provider name: `Claude 3.5 Sonnet v2`
        - LLM adapter: bedrock
        - AWS Region: `us-west-2`
        - AWS Access Key ID: 前述のIAM画面からコピペ
        - AWS Secret Access Key: 前述のIAM画面からコピペ
        - Custom models: `us.anthropic.claude-3-5-sonnet-20241022-v2:0`（「+ Add custom model name」より追加）

:::note
Claudeモデルの初期クォータが厳しめのため、レートリミットに抵触しないようにアプリ本体が利用するモデルと微妙に変えています。
:::

![スクリーン ショット 2025-03-25 に 00.27.05 午前.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/df21ac35-4969-4421-992c-bd0f51f6e809.png)

- 上記すべて保存後、「Create evaluator」画面で「correctness」を選択します。

![スクリーン ショット 2025-03-25 に 00.31.39 午前.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/4e7a3b77-e117-42fe-8fa9-de5c6f0d147b.png)

- 「Variable mapping」セクション右側で以下のように設定して、Evaluatorを保存します。
    - {{query}}
        - Object: Span
        - Object Name: `invoke_bedrock`
        - Object Variable: Input
    - {{generation}}
        - Object: Trace
        - Object Variable: Output
    - {{ground_truth}}
        - Object: Span
        - Object Name: `get_truth`
        - Object Variable: Output


### 自分のアプリの出力を評価する

- 作業PCのローカルでテキストエディタ（メモ帳など）を開き、以下のコードをコピペして「4_eval.py」という名前で保存します。

```py: 4_eval.py
import boto3
from langfuse.decorators import observe
from langfuse import Langfuse

def get_prompt(word):
	langfuse = Langfuse()
	prompt = langfuse.get_prompt("what-is")
	compiled_prompt = prompt.compile(name=word)
	return compiled_prompt

# 追加：正解データを定義する関数
@observe()
def get_truth(word):
    truth = ""
    if word == "かぐたん":
        truth = "かぐたんは、KAG社が開発したSlackチャットボットです。"
    return truth

@observe()
def invoke_bedrock(prompt):
	bedrock = boto3.client("bedrock-runtime")
	response = bedrock.converse(
		modelId="us.anthropic.claude-3-7-sonnet-20250219-v1:0",
		messages=[
			{
				"role": "user",
				"content": [{"text": prompt}]
			}
		]
	)
	output = response["output"]["message"]["content"][0]["text"]
	return output

@observe()
def main(input):
	prompt = get_prompt(input)
	get_truth(input) # 追加
	output = invoke_bedrock(prompt)
	print(output)
	return output

main("かぐたん") # 入力ワードを変更
```

- CloudShell画面右上の「アクション > ファイルアップロード」から、上記ファイルをアップロードします。
- CloudShellのターミナルで `python 4_eval.py` を実行して、アプリの出力結果が表示されることを確認しましょう。
- Langfuse側で先ほど設定した評価器が、この生成結果を数秒遅れで評価してくれます。EvaluatorsのIDをクリックして、評価結果の点数とコメントを確認してみましょう。（コメントは見切れやすいので、ツールバーアイコンから行の幅を広げましょう）

![スクリーン ショット 2025-03-25 に 00.44.51 午前.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/cf4e7b8b-2c93-424b-be59-ba7d403d626e.png)

RAGなしで「かぐたん」の正しい説明ができるLLMは少ないはずなので、おそらくCorrectnessの点数は0点になったと思います。

https://aws.amazon.com/jp/blogs/news/aws-genai-usecases-kddi-agile-bedrock/

:::note
分かりづらいのですが、`get_truth` 関数は評価のカンペ用に正解データを取得しているだけで、（RAGのように）アプリ本体に正解を教えてあげているわけではありません。だからハルシネーションが発生しているのですね。
:::


# 5. データセットを管理しよう

前項では関数内に正解データをベタ書きしましたが、これをデータセットとしてLangfuseに登録して使ってみましょう。

- Langfuseの左側サイドバー「Datasets」より、「New Dataset」をクリックして以下のように設定します。（記載ないものはデフォルトでOK）
    - Name: `truth`

![スクリーン ショット 2025-03-25 に 00.51.32 午前.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/b7b69043-c05c-4c4e-a164-fe5df41942a0.png)

- 「New Item」より、以下のように設定します。（記載ないものはデフォルトでOK）
    - Input: `{"text": "かぐたん"}`
    - Expected output: `{"text": "かぐたんは、KAG社が開発したSlackチャットボットです。"}`


### 自分のアプリからデータセットを利用する

- 作業PCのローカルでテキストエディタ（メモ帳など）を開き、以下のコードをコピペして「5_dataset.py」という名前で保存します。

```py: 5_dataset.py
import boto3
from langfuse.decorators import observe
from langfuse import Langfuse

def get_prompt(word):
	langfuse = Langfuse()
	prompt = langfuse.get_prompt("what-is")
	compiled_prompt = prompt.compile(name=word)
	return compiled_prompt

# 更新：Langfuseから正解データを取得する関数
@observe()
def get_truth(word):
    langfuse = Langfuse()
    dataset = langfuse.get_dataset("truth")
    truth = ""
    for item in dataset.items:
        if item.input["text"] == word:
            truth = item.expected_output
    return truth

@observe()
def invoke_bedrock(prompt):
	bedrock = boto3.client("bedrock-runtime")
	response = bedrock.converse(
		modelId="us.anthropic.claude-3-7-sonnet-20250219-v1:0",
		messages=[
			{
				"role": "user",
				"content": [{"text": prompt}]
			}
		]
	)
	output = response["output"]["message"]["content"][0]["text"]
	return output

@observe()
def main(input):
	prompt = get_prompt(input)
	get_truth(input)
	output = invoke_bedrock(prompt)
	print(output)
	return output

main("かぐたん")
```

- CloudShell画面右上の「アクション > ファイルアップロード」から、上記ファイルをアップロードします。
- CloudShellのターミナルで `python 5_dataset.py` を実行して、アプリの出力結果が表示されることを確認しましょう。
- 正解データを正しくLangfuseから取得できているか、トレースから確認してみましょう。


# お片付け

- AWSアカウントはしばらく使わない場合、解約してしまいましょう。そのとき、登録したメールアドレスもAWSで二度と使えなくなってしまうため、捨てアドレスなどに設定変更してから解約することをお勧めします