Langfuse の Observation レベル評価:「どのステップが悪いのか」をスコアで特定できるようになった
- 智之 黒澤
- 2 日前
- 読了時間: 4分
こんにちは。ガオ株式会社の黒澤です。
Langfuse v3.153.0 で [PR #11861](https://github.com/langfuse/langfuse/pull/11861) がマージされ、LLM-as-a-Judge を Observation 単位で実行できるようになりました。本記事ではその背景と使い方をまとめます。
課題:Trace 全体への評価では「どこが悪いか」がわからない
LLM アプリの評価で、こんな状況に陥ったことはありませんか。
RAG アプリの LLM as a Judge スコアが下がった。でも、ドキュメント検索が悪いのか、回答生成が悪いのか、判断できない。
これは、従来の Langfuse の評価機能が Trace(エンドツーエンドのリクエスト全体) を評価単位としていたためです。
Before:Evaluator は Trace 全体にしか設定できなかった
Langfuse で LLM as a Judge Evaluator を作成すると、評価対象は Trace 全体の最終出力のみでした。

▲ Run on に Live Tracesを選択すると、Trace全体の評価となる
たとえば以下のような RAG パイプラインでは、`retrieve`(検索)と `llm`(生成)が別々の Observation として存在しますが、評価できるのは最終出力だけでした。
Trace: ユーザーの質問
├─ Span: retrieve ← ここは評価できなかった
└─ Generation: llm ← ここの最終出力だけが評価対象スコアが低くても「検索が悪いのか、生成が悪いのか」の切り分けは、ログを手で読むしかありませんでした。
After:個々の Observation を評価ターゲットに指定できるようになった
今回のアップデートで、Evaluator の設定画面に Observation をターゲットにするオプション が追加されました。

▲ 例:retrieve 用。Target で Live Observations を選択し、Where で Type=SPAN, Name=retrieve を指定

▲ 例:llm 用。Type=GENERATION, Name=llm でフィルタ。retrieve と llm で別々の Evaluator を設定できる
※ 以上は RAG パイプラインの一例。実際の Observation 名や Type はアプリの実装に合わせて設定する。
検索と生成を Langfuse へ送信する簡単なサンプルソースです。ここでは例示として、実際の検索先や LLM は呼ばず、固定文言を Langfuse へ送信します。
ソースコード
"""
Trace 構造:
Trace: rag-pipeline
├─ Span: retrieve ← Context Relevance の評価対象
└─ Generation: llm ← Answer Relevance の評価対象
"""
import os
try:
from dotenv import load_dotenv
load_dotenv()
except ImportError:
pass
from langfuse import get_client, propagate_attributes
def retrieve_documents(query: str) -> list[str]:
"""ドキュメント検索をシミュレート(モック)"""
mock_docs = [
"Python は 1991年に Guido van Rossum によって開発されました。",
"Langfuse は LLM アプリケーションの観測・評価プラットフォームです。",
]
return mock_docs[:2]
def generate_answer(query: str, context: list[str]) -> str:
"""LLM による回答生成をシミュレート(モック)"""
context_text = "\n".join(context)
return f"検索された文脈:{context_text[:50]}... に基づき、{query} への回答を生成します。"
def run_rag_pipeline(query: str) -> str:
langfuse = get_client()
with langfuse.start_as_current_observation(
as_type="span", name="rag-pipeline", input={"query": query}
) as root_span:
with propagate_attributes(tags=["observation-eval-sample"]):
with langfuse.start_as_current_observation(
as_type="span", name="retrieve", input={"query": query}
) as retrieve_span:
documents = retrieve_documents(query)
retrieve_span.update(output={"documents": documents, "count": len(documents)})
with langfuse.start_as_current_observation(
as_type="generation", name="llm",
model="gpt-4o-mini",
input={"query": query, "context": documents},
) as llm_gen:
answer = generate_answer(query, documents)
llm_gen.update(output={"answer": answer}, usage_details={"input_tokens": 50, "output_tokens": 30})
root_span.update(output={"answer": answer})
langfuse.flush()
return answer
if __name__ == "__main__":
answer = run_rag_pipeline("Python はいつ開発されましたか?")
print(answer)これにより、先ほどの RAG パイプラインに対して次のような評価設計が可能になります。
評価対象 Observation | 評価軸 | 検出したいもの |
retrieve Span | Context Relevance | 検索結果がクエリに関連しているか |
llm Generation | Answer Relevance | 回答がユーザーの質問に答えているか |
Trace 詳細画面でも変化がわかる
設定後、Trace の詳細画面を開くと、 各 Observation の行にもスコアが表示される ようになります。

▲ 各 Observation 行にスコアが付いている。一目で「どのステップが悪いか」が把握できる
一目で「どのステップのスコアが低いか」が把握できます。たとえば:
Context Relevance: 0.50(検索結果の精度が低い)
Answer Relevance: 1.00(生成自体は質問に答えている)
この場合、問題は検索ロジック側にあると判断できます。
まとめ:評価の粒度がアーキテクチャの複雑さに追いついた
Before(Trace レベル) | After(Observation レベル) | |
評価対象 | 最終出力のみ | 各中間ステップも評価可能 |
問題の原因特定 | 最終出力のスコアは取れるが、複数ステップがある場合は「どのステップが悪いか」の切り分けが困難 | 各ステップのスコアで特定可能 |
向いている構成 | シンプルな LLM 呼び出し | RAG・エージェント・多段階チェーン |
アプリが複雑になるほど、「何かがおかしい」の検知だけでは不十分です。Observation レベルの評価は、その「どこがおかしいか」をデータとして取り出す手段です。



コメント