top of page

LangfuseのExperiments Compare ViewのBaseline機能を解説

  • Shogo Umeda
  • 1 時間前
  • 読了時間: 12分

はじめに


LLMアプリケーションの開発において、プロンプトの改善は避けて通れない作業です。しかし、プロンプトを変更するたびに、こんな不安を感じたことはありませんか?


  • このプロンプト変更、本当に改善になっているのか?

  • 一部のケースで良くなったけど、他のケースで悪化していないか?

  • 前のバージョンと比べて、どれくらい良くなったのか数字で示せない...


LangfuseのBaseline機能を使えば、変更前後の結果を定量的に比較し、改善した点と品質が下がった点を一目で把握できます。

本記事では、「京都の観光案内ボット」を題材に、感覚ではなくデータに基づいてプロンプトを改善する手順を紹介します。



本記事でわかること


  • LangfuseのExperiments機能の基本的な使い方

  • Baseline機能を使ったプロンプトの比較・評価手順

  • Compare Viewでのリグレッション検出方法


前提・想定対象読者


  • LLMアプリケーションの開発経験がある方

  • プロンプトの品質管理・改善に課題を感じている方

  • LangfuseのExperiments機能を使ったプロンプトの比較・評価手順を知りたい方


Langfuse Experimentsとは


Experiments機能の役割


LangfuseのExperiments機能は、プロンプトやモデル設定を変えたときに、出力が「良くなったか・悪くなったか」を、データセットを用いて定量的にテスト・比較できる機能です。具体的には以下のことが可能になります。


  • データセット管理 : テストケース(入力と期待される出力)を一元管理

  • 実験の実行 : 同じデータセットに対して、異なるプロンプトやモデルで実験を実行

  • 評価(スコアリング) : SDKの `evaluators` でスコアを付与、またはLangfuse上のEvaluator(LLM-as-a-Judge等)で評価を実行

  • 結果の比較 : 複数の実験結果を並べて比較


Baseline機能の登場(2025年11月リリース)


従来のExperiments機能でも複数の実験結果を比較できましたが、「どれが基準(Baseline)なのか」が明示されていませんでした。


Langfuse v3.125.0でBaseline機能が追加され、以下が可能になりました。


  1. 基準バージョンの明示 : 現在の本番環境や、比較の基準となる実験を「Baseline」として指定

  2. 差分の可視化 : Baseline と Candidate(比較対象)の差分を緑(改善)・赤(悪化)で色分け表示

  3. リグレッションの検出 : フィルター機能により、スコアが悪化した項目だけを抽出して確認


これにより、「新しいプロンプトは全体的には良いが、特定のケースで品質が低下している」といった状況を視覚的に素早く発見できるようになりました。


実務での活用シーン


Baseline機能は以下のようなシーンで特に有効です。


  • プロンプトの改善 : 新しいプロンプトが既存バージョンより優れているか検証

  • モデルの変更 : OpenAI GPTからGeminiへの移行など、モデル変更の影響を評価

  • 継続的な品質管理 : 定期的に実験を実行し、品質の推移をトラッキング

  • A/Bテストの事前検証 : 本番投入前に、複数のバリエーションを比較


次のセクションでは、プロンプトの変更によるリグレッションを検出するために、実際にコードを書いて実験を実行してみましょう。


実装



今回のシナリオ

「京都の観光案内ボット」を題材に、以下の2つのプロンプトバージョンを比較します。


  • プロンプトV1(Baseline) : 標準的で真面目なトーン

  • プロンプトV2(Candidate) : 親しみやすく絵文字を使うトーン


目的は、「親しみやすさを向上させつつ、正確性を維持できるか」を検証することです。


以下の流れで実装していきます。


Dataset(評価用データ)の作成
  ↓
Baseline実験(Prompt V1)
  ↓
Candidate実験(Prompt V2)
  ↓
Evaluator(accuracy, emoji_count)
  ↓
Compare View(差分・リグレッション可視化)
Tips: 本番運用では、Prompt Management機能 でプロンプトを管理するのがおすすめです(コードのデプロイなしで更新・バージョニングできます)。

ディレクトリ構成


今回のサンプルコードのディレクトリ構成は以下の通りです。

langfuse-experiments-demo/
├── src/
│   ├── data/
│   │   └── kyoto_tourism_dataset.json  # テストデータセット
│   ├── run_experiment.py               # 実験スクリプト
│   ├── upload_dataset.py               # データセットアップロード
│   └── .env                            # 環境変数
└── pyproject.toml                       # 依存関係定義

環境セットアップ


今回は uv を使ってPython環境を構築します。


UV環境設定ファイル作成


UVの環境設定ファイル`pyproject.toml` を作成します。

[project]
name = "langfuse-experiments-demo"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
    "langfuse>=3.0.0",
    "google-genai>=1.0.0",
    "python-dotenv>=1.0.0",
]

依存関係インストール


uvで依存関係をインストールします。

# uvのインストール(まだの場合)
curl -LsSf https://astral.sh/uv/install.sh | sh

# プロジェクトディレクトリに移動
cd langfuse-experiments-demo

# 仮想環境を作成し、依存関係をインストール
uv sync

環境変数設定


環境変数を設定します(`src/.env`ファイルを作成)。

# Langfuse設定
LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_SECRET_KEY=sk-lf-...
LANGFUSE_BASE_URL=https://cloud.langfuse.com

# Google Cloud / Vertex AI設定
GOOGLE_CLOUD_PROJECT=your-project-id
GOOGLE_CLOUD_LOCATION=asia-northeast1

Google Cloud / Vertex AI の認証


Vertex AI(Gemini)を呼び出すために、Google Cloud の Application Default Credentials(ADC)を実行します。

# Application Default Credentials を設定
gcloud auth application-default login

# プロジェクトを設定
gcloud config set project your-project-id

以上で環境構築完了です。


データセットの準備


テストケースを含むJSONファイルを作成します。以下のようなファイルを`src/data/kyoto_tourism_dataset.json` に保存します。

[
  {
    "input": "京都の有名な観光地を3つ教えてください。",
    "expected_output": "清水寺、金閣寺、伏見稲荷大社などが有名です。",
    "metadata": {
      "category": "general_spots",
      "difficulty": "easy"
    }
  },
  {
    "input": "京都で紅葉が綺麗な場所はどこですか?",
    "expected_output": "東福寺、永観堂、嵐山などが紅葉の名所として知られています。",
    "metadata": {
      "category": "seasonal",
      "difficulty": "medium"
    }
  }
]


データセットのアップロード用コード


Langfuseにデータセットをアップロードするスクリプトを作成します。以下のコードを `src/upload_dataset.py` に保存します。

コードが長くなるのでセクションに折り畳んでいます。コード内容はセクションを展開して確認してください。

src/upload_dataset.py

"""
Langfuseにデータセットを登録するスクリプト
"""

import json
import os
from dotenv import load_dotenv
from langfuse import get_client

# 環境変数を読み込み
load_dotenv()

# Langfuseクライアントの初期化
langfuse = get_client()

DATASET_NAME = "kyoto-tourism-qa"
DATASET_DESCRIPTION = "京都観光案内ボットの評価用データセット"


def upload_dataset():
    """Langfuseにデータセットをアップロード"""
    
    # ローカルデータセットの読み込み
    with open('data/kyoto_tourism_dataset.json', 'r', encoding='utf-8') as f:
        items = json.load(f)
    
    # データセットを作成(既に存在する場合はそのまま使用)
    try:
        langfuse.create_dataset(
            name=DATASET_NAME,
            description=DATASET_DESCRIPTION,
            metadata={"version": "1.0", "language": "ja"}
        )
        print(f" データセット '{DATASET_NAME}' を作成しました")
    except Exception as e:
        print(f" データセット '{DATASET_NAME}' は既に存在します")
    
    # データセットアイテムを登録
    for item in items:
        langfuse.create_dataset_item(
            dataset_name=DATASET_NAME,
            input=item["input"],
            expected_output=item["expected_output"],
            metadata=item.get("metadata", {})
        )
    
    print(f" {len(items)}件のアイテムを登録しました")


if __name__ == "__main__":
    upload_dataset()



実験スクリプトの実装


Langfuse Python SDK V3の `dataset.run_experiment()` を使って、Baseline実験とCandidate実験の両方を実行できるスクリプトを実装します。

以下のコードを `src/run_experiment.py` に保存します。 コードが長くなるのでセクションに折り畳んでいます。コード内容はセクションを展開して確認してください。

`src/run_experiment.py


"""
京都観光案内ボットの実験スクリプト
Baseline(V1)と Candidate(V2)の両方を実行可能

Usage:
  uv run python src/run_experiment.py baseline  # Baseline実験を実行
  uv run python src/run_experiment.py candidate # Candidate実験を実行
"""

import os
import sys
import json
import re
from dotenv import load_dotenv
from langfuse import get_client, Evaluation
from google import genai
from google.genai import types

# 環境変数を読み込み
load_dotenv()

# Langfuseクライアントの初期化
langfuse = get_client()

# Google Gen AI SDK クライアントの初期化
project_id = os.getenv("GOOGLE_CLOUD_PROJECT")
location = os.getenv("GOOGLE_CLOUD_LOCATION", "us-central1")
client = genai.Client(vertexai=True, project=project_id, location=location)

# モデル名
MODEL_NAME = "gemini-2.5-flash"

# データセット名
DATASET_NAME = "kyoto-tourism-qa"


# ================================================================
# プロンプト定義
# ================================================================

# プロンプトV1: 標準的で真面目なトーン(Baseline)
PROMPT_V1 = """あなたは京都の観光案内を専門とするアシスタントです。
正確で丁寧な情報提供を心がけてください。

ユーザーの質問: {question}

上記の質問に対して、正確かつ簡潔に回答してください。"""

# プロンプトV2: 親しみやすく絵文字を使うトーン(Candidate)
PROMPT_V2 = """あなたは京都の観光案内を専門とするアシスタントです。
正確で丁寧な情報提供を基本としつつ、親しみやすい雰囲気で案内してください。

回答のポイント:
- 正確性を最優先に、信頼できる情報を提供する
- 適度に絵文字を添えて、読みやすく親しみやすい印象にする(1〜2個/段落程度)
- 「です・ます」調で丁寧に、かつ堅すぎない自然な文体で

ユーザーの質問: {question}

上記の質問に対して、正確かつ親しみやすく回答してください。"""


# ================================================================
# タスク関数
# ================================================================

async def baseline_task(*, item, **kwargs):
    """Baseline実験のタスク関数(プロンプトV1を使用)"""
    question = item.input if hasattr(item, 'input') else item.get("input")
    prompt = PROMPT_V1.format(question=question)
    
    # generationとして記録(コスト計算のためにusage_detailsを設定)
    with langfuse.start_as_current_observation(
        as_type="generation",
        name="gemini-generation",
        model=MODEL_NAME,
        input=prompt
    ) as generation:
        response = await client.aio.models.generate_content(
            model=MODEL_NAME,
            contents=prompt,
            config=types.GenerateContentConfig(
                temperature=0.7,
                max_output_tokens=2048
            )
        )
        
        # usage_detailsを設定(コスト計算に必要)
        generation.update(
            output=response.text,
            usage_details={
                "input": response.usage_metadata.prompt_token_count,
                "output": response.usage_metadata.candidates_token_count,
            }
        )
    
    return response.text


async def candidate_task(*, item, **kwargs):
    """Candidate実験のタスク関数(プロンプトV2を使用)"""
    question = item.input if hasattr(item, 'input') else item.get("input")
    prompt = PROMPT_V2.format(question=question)
    
    # generationとして記録(コスト計算のためにusage_detailsを設定)
    with langfuse.start_as_current_observation(
        as_type="generation",
        name="gemini-generation",
        model=MODEL_NAME,
        input=prompt
    ) as generation:
        response = await client.aio.models.generate_content(
            model=MODEL_NAME,
            contents=prompt,
            config=types.GenerateContentConfig(
                temperature=0.7,
                max_output_tokens=2048
            )
        )
        
        # usage_detailsを設定(コスト計算に必要)
        generation.update(
            output=response.text,
            usage_details={
                "input": response.usage_metadata.prompt_token_count,
                "output": response.usage_metadata.candidates_token_count,
            }
        )
    
    return response.text


# ================================================================
# 評価関数(共通)
# ================================================================

async def accuracy_evaluator(*, output, expected_output, **kwargs):
    """正確性評価: LLMを使って意味的な正確性を評価(LLM as a Judge)"""
    if expected_output is None:
        return Evaluation(name="accuracy", value=0.0, comment="期待される出力がないため評価不可")
    
    judge_prompt = f"""あなたは回答の正確性を評価する審査員です。
以下の「期待される回答」と「実際の回答」を比較し、実際の回答が期待される内容を正確にカバーしているか評価してください。

【期待される回答】
{expected_output}

【実際の回答】
{output}

【評価基準】
- 1.0: 期待される内容を完全にカバーしている
- 0.7-0.9: 主要な内容はカバーしているが、一部欠けている
- 0.4-0.6: 部分的にカバーしているが、重要な情報が欠けている
- 0.1-0.3: ほとんどカバーできていない
- 0.0: 全く関係ない回答

以下のJSON形式のみで回答してください:
{{"score": 0.0〜1.0の数値, "reason": "評価理由を簡潔に"}}
"""
    
    try:
        response = await client.aio.models.generate_content(
            model=MODEL_NAME,
            contents=judge_prompt,
            config=types.GenerateContentConfig(
                temperature=0.0,
                max_output_tokens=2048
            )
        )
        
        response_text = response.text.strip()
        json_match = re.search(r'\{[^{}]*\}', response_text)
        result = json.loads(json_match.group()) if json_match else json.loads(response_text)
        
        score = max(0.0, min(1.0, float(result.get("score", 0.0))))
        reason = result.get("reason", "理由なし")
        
        return Evaluation(name="accuracy", value=score, comment=reason)
        
    except Exception as e:
        return Evaluation(name="accuracy", value=0.0, comment=f"評価エラー: {str(e)}")


def emoji_count_evaluator(*, output, **kwargs):
    """絵文字の使用数を評価"""
    emoji_count = sum(1 for char in output if ord(char) > 0x1F300) if output else 0
    return Evaluation(
        name="emoji_count",
        value=emoji_count,
        comment=f"絵文字を{emoji_count}個使用"
    )


# ================================================================
# 実験実行関数
# ================================================================

def run_baseline_experiment():
    """Baseline実験を実行"""
    dataset = langfuse.get_dataset(DATASET_NAME)
    
    result = dataset.run_experiment(
        name="Kyoto Tourism Bot - Prompt Comparison",
        run_name="baseline-v1",
        description="標準的で真面目なトーンのプロンプト(Baseline)",
        task=baseline_task,
        evaluators=[accuracy_evaluator, emoji_count_evaluator],
        metadata={"prompt_version": "v1", "model": MODEL_NAME}
    )
    
    print(result.format())
    langfuse.flush()
    return result


def run_candidate_experiment():
    """Candidate実験を実行"""
    dataset = langfuse.get_dataset(DATASET_NAME)
    
    result = dataset.run_experiment(
        name="Kyoto Tourism Bot - Prompt Comparison",
        run_name="candidate-v2",
        description="親しみやすく絵文字を使うトーンのプロンプト(Candidate)",
        task=candidate_task,
        evaluators=[accuracy_evaluator, emoji_count_evaluator],
        metadata={"prompt_version": "v2", "model": MODEL_NAME}
    )
    
    print(result.format())
    langfuse.flush()
    return result


# ================================================================
# メイン
# ================================================================

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python run_experiment.py [baseline|candidate]")
        sys.exit(1)
    
    experiment_type = sys.argv[1].lower()
    
    if experiment_type == "baseline":
        run_baseline_experiment()
    elif experiment_type == "candidate":
        run_candidate_experiment()
    else:
        print(f"Unknown experiment type: {experiment_type}")
        print("Usage: python run_experiment.py [baseline|candidate]")
        sys.exit(1)



実装のポイント


1. `dataset.run_experiment()` の活用


Langfuse Python SDK V3では、`dataset.run_experiment()` という高レベルメソッドが提供されています。以下のように、実験を実行することができます。


result = dataset.run_experiment(
    name="Experiment Name",        # 実験名
    run_name="baseline-v1",        # Run名
    task=baseline_task,            # 各項目に対して実行する関数
    evaluators=[ .. ],              # Item-level評価関数
    metadata={ .. }                 # メタデータ
)

2. 非同期タスクと評価関数


`run_experiment()` はタスク関数と評価関数の両方で非同期(`async def`)をサポートしています。LLM APIなどを呼び出す処理は非同期処理にすることで、並列実行時の効率が向上します。

# 非同期タスク関数
async def baseline_task(*, item, **kwargs):
    response = await client.aio.models.generate_content(
        model=MODEL_NAME,
        contents=prompt,
        config=types.GenerateContentConfig(...)
    )
    return response.text

# 非同期評価関数(LLM-as-a-Judge)
async def accuracy_evaluator(*, output, expected_output, **kwargs):
    response = await client.aio.models.generate_content(
        model=MODEL_NAME,
        contents=judge_prompt,
        config=types.GenerateContentConfig(...)
    )
    return Evaluation(name="accuracy", value=score, comment=reason)

3. 評価関数


`dataset.run_experiment()` の `evaluators` 引数には、評価関数をリストで指定します。

ここで指定する評価関数は、各データセット項目の出力を個別に評価する関数です。

今回の例では、正確性評価関数(`accuracy_evaluator`)と絵文字使用数評価関数(`emoji_count_evaluator`)を使用しています。


実験の実行


スクリプトを実行します。

# データセットをアップロード
uv run python src/upload_dataset.py

# Baseline実験を実行
uv run python src/run_experiment.py baseline

# Candidate実験を実行
uv run python src/run_experiment.py candidate


実行が完了したら、Langfuse UIにアクセスして結果を確認しましょう。次のセクションでは、Baseline機能を使った比較方法を詳しく解説します。


検証編 - Baseline機能で結果を比較する


Compare Viewへのアクセス


  1. Langfuseにログイン

  2. Datasets メニューから `kyoto-tourism-qa` をクリック

    Compare View へのアクセス

  3. `baseline-v1` と `candidate-v2` の両方にチェックを入れる

  4. Actions ボタン - Compare ボタンをクリック

Compare Viewへのアクセス


Baselineの設定


Compare View が開いたら、片方の実験を「Baseline」として指定します。


1. `baseline-v1` の表示横の`Set as Baseline`をクリック


Baseline の設定

これで、```candidate-v2` が Baseline との差分として表示されるようになります。



結果の見方


Delta(差分)表示


  • 緑色 : Baseline より改善された項目

  • 赤色 : Baseline より悪化した項目(リグレッション)

  • 灰色 : 変化なし

Langfuse Experiment Baseline 差分表示




今回の実験結果


筆者の実行結果では、一部の項目で `accuracy` スコアが下がっていることがわかりました。


Compare Viewでは、Datasetsの `Input`、`Expected Output`、バージョンごとの `Output` およびスコアを確認できます。

評価スコアのコメントアイコンにマウスオーバーすると、LLM-as-a-Judgeがスコアを付けた根拠を確認できます。

Score にマウスオーバー


また、出力の右下にマウスオーバーするとトレース確認用のアイコンが表示されます。クリックするとトレース画面がオーバーレイ表示され、詳細を確認できます。

Scoreコメントなどの詳細表示

Traceアイコンにマウスオーバー

Trace内容を表示

このように、評価結果のコメントやトレース詳細を確認しながら、リグレッションの有無等のプロンプト変更の影響を分析できます。


従来もCompare Viewで結果を横並びで見比べることはできましたが、Baseline機能により評価スコアやLatency、Costなどの差分が視覚的に把握しやすくなり、分析が容易になりました。


まとめ


プロンプトの改善は「感覚」ではなく「データ」で判断する時代です。Baseline機能を活用して、自信を持ってプロンプトの改善サイクルを促進しましょう。


Baseline機能のメリット


  1. 定量的な比較 : 「なんとなく良くなった」ではなく、数値で改善・悪化を判断

  2. リグレッションの早期発見 : 特定のケースでの品質低下を見逃さない

  3. 意思決定の根拠 : プロンプト変更の採用可否を、データに基づいて判断


実務での活用ポイント


  • 本番投入前の必須プロセス : プロンプト変更時は必ずBaseline比較を実施

  • 継続的な品質モニタリング : 定期的に実験を実行し、品質の推移を追跡

  • チーム内でのコミュニケーション : Langfuse UIを共有して、品質議論の土台に




参考リンク


コメント


bottom of page