コンテンツにスキップ

Langfuse 用語集 — 写経で詰まったところ

ex01〜06 を写経しながら「言葉が混乱する」ポイントを1枚にまとめた用語集。 ロードマップ本体は ../../langfuse/STUDY_NOTES.md、 サンプルの並びは README.md を参照。


1. 一番混乱する4語:trace / span / generation / observation

この4つは入れ子の階層で、粒度が違うだけ。まず全体像:

Trace(トレース)= 1リクエストまるごと。一番外側の箱
└── Observation(オブザベーション)= トレースの中の「1ノード」の総称
    ├── Span(スパン)       … ふつうの処理ノード(検索・検証・前後処理)
    └── Generation(ジェネレーション)… LLM 呼び出し専用のノード
用語 何を指すか 具体例(ex04) 作り方
Trace 1リクエスト全体。木のルート answer_question 1本 一番外の @observe()
Observation trace 内の任意のノードの総称(span も generation も含む) retrieve / generate / validate すべて — (span/generation の上位概念)
Span LLM ではない処理ノード retrieve(検索), validate(検証) @observe()
Generation 1回の LLM 呼び出しを表すノード generate(gpt-4o-mini を1回叩く) @observe(as_type="generation") または ドロップイン

覚え方: 「observation = span ∪ generation」。Langfuse のダッシュボードで Observation と書いてあったら「span も generation も全部ひっくるめた話」、Generation と書いてあったら「LLM 呼び出しだけに絞った話」。

span にするか generation にするかの判定基準(ここで毎回迷う)

そのノード自身が「1回の LLM 呼び出し」か? - Yesgeneration(model・トークン・コストが紐づく葉ノード) - No(複数処理をまとめる箱)span

answer_question は「検索 + LLM + 検証」をまとめるなので span が正解。 generation にすると「model は何? トークンは? コストは?」が答えられず破綻する。 generation にするのは一番内側で実際に API を1回叩く葉ノードだけ、親は全部 span。

answer_question  ← span   (箱。中の合計時間を持つ)
├── retrieve     ← span
├── generate     ← generation  ★ここだけ。唯一の「1回のLLM呼び出し」
└── validate     ← span

2. コード側の用語(デコレータ・API)

用語 役割 出てくる回
@observe() 関数を span として記録。引数→Input、戻り値→Output を自動キャプチャ ex01〜
@observe(as_type="generation") その span をLLM 呼び出しとして扱う。model/トークン/コスト集計の対象になる ex02
update_current_generation(model=, usage_details=) 「いま実行中の generation」に model とトークン数を後付け。トークン数 × モデル単価 = コスト自動計算 ex02
from langfuse.openai import OpenAI OpenAI SDK の薄いラッパ(ドロップイン)。import 1行で generation 化を全自動にする。update_current_generation 不要 ex03
langfuse.flush() キューに溜まったデータを送り切るまで待つ。短命スクリプトで必須(→ §5) 全回
score トレースに評価値(👍👎・数値)を付ける。score_current_trace(内側から)/ create_score(外側から trace_id 指定) ex05
propagate_attributes(user_id=, session_id=, tags=, trace_name=) trace に検索用メタデータを付ける。Sessions / Users 画面で探せるようになる。コンテキストマネージャwith で囲む)で、中の span 全部に伝播する。langfuse import propagate_attributes で取る(クライアントのメソッドではない)。v3 の update_current_trace から変更(→ §7) ex06
usage_details {"input": 入力トークン, "output": 出力トークン}。金額ではなくトークン数を渡す(単価は Langfuse 側が持っている) ex02
from langfuse.langchain import CallbackHandler LangChain/LangGraph 連携の計装オブジェクト。config={"callbacks":[CallbackHandler()]} を invoke に挿すと、その実行中の LLM 呼び出し・ノードが自動で1トレースになる。import 先は v3/v4 共通で langfuse.langchain ex07, ex08
config={"callbacks":[...]} LangChain の invoke に渡す設定。callbacks に CallbackHandler を入れる = 「この invoke の内側を計装する」宣言。LCEL チェインもグラフも挿す型は同じ ex07, ex08

自動キャプチャの2系統(思想は同じ「ラッパで横取り」)

  • OpenAI ドロップイン(ex03): langfuse.openaicreate() を横取りして自動記録
  • LangChain CallbackHandler(ex07/ex08): config={"callbacks":[CallbackHandler()]} を渡すと LangChain/LangGraph のイベントを受けて自動記録
  • ⚠️ handler が見えるのは invoke()内側だけ。invoke の外でやる自前の前後処理は見えない → 全体を1トレースにしたいなら @observe() で外側を包む(ex07 後半 explain_with_outer_trace がその対比デモ)
  • LCEL チェイン(ex07)もグラフ(ex08)も挿す型は同じ config の callbacks。違うのは中身が直線か状態機械かだけ
  • グラフ(ex08)ではノード = observation。LLM を呼ぶノードは generation、決定論ノードは span として木に出る(§1 の「observation = span ∪ generation」がノードにそのまま対応)

CallbackHandler / config callbacks / @observe 外包み の三者関係(ex07 で詰まる所)

何を 役割 単独で見える範囲
CallbackHandler() 計装オブジェクト本体 (これ単体では何もしない。config に挿して初めて効く)
config={"callbacks":[handler]} handler を invoke に登録する口 invoke の内側だけ(チェイン/グラフの中の LLM 呼び出し・ノード)
@observe() 外包み invoke を呼ぶ関数全体を span 化する箱 invoke の前後処理まで含めた関数全体

判定基準: 「トレースに前処理・後処理も入れたいか?」 - チェイン/グラフの中身だけでよい → handler だけ(config に挿す) - 前処理(入力整形)・後処理(出力加工)も同じ1トレースにしたい → @observe() で外包み + handler。外包みが箱を作り、handler が拾った generation をその子にぶら下げる


3. ダッシュボードの3パネル(粒度違いの同じ統計)

左から右へ「ズームイン」していく関係:

パネル 集計対象 用途
Trace latency percentiles トレース全体(前処理+LLM+後処理 込み) どのアプリ機能が遅いか
Generation latency percentiles LLM 呼び出しだけ抜き出し(= 外部 API の往復時間) LLM 自体のブレを見る
Observation latency percentiles 全ノード(span も generation も)を個別に その機能の中のどの段が犯人か

読む順番:左で「どの機能が遅い」→ 中で「LLM のブレ」→ 右で「中のどの段が犯人」

  • -(ハイフン)= データなし or 速すぎて計測されない(retrieve/validate はミリ秒未満で -
  • Generation パネルに出るのは as_type="generation" を付けたものだけ(型を宣言したから抜き出せる)

4. パーセンタイル(p50 / p90 / p95 / p99)

実行した全回数の所要時間を速い順に並べ、その%の位置の値を取ったもの。特定の1本を追跡しているのではなく、集団からの集計値

指標 意味 用途
p50 中央値。半分はこれより速い 「ふつう何秒か」(典型的な体験)
p90 / p95 上位 10% / 5% の境目 多くのユーザーの上限体感
p99 上位1%、ほぼ最悪ケース タイムアウト・SLA 設計の基準(最悪の体験)

なぜ平均(mean)ではなく p50 か: LLM レイテンシは「ふつう速いが、たまに激遅」の偏った分布。 [2s,2s,2s,2s,20s] だと 平均=5.6s(誤解を招く)/ p50=2s(正しい典型)/ p99=20s(最悪を別に把握)。 → 「典型」と「最悪」を分けて見たいからパーセンタイルを使う。

  • p50=p99 で全部同じ値 = 実行回数が少なく、ブレがまだ見えていないだけ(データ点1つだと1回のブレが支配する)
  • 値が割れてくる(例 p50 2.46s → p99 4.31s)= ロングテール。外部 API が出力長やプロバイダ混雑でブレている証拠

Generation latency = 自分のコードではなく外部 API の時間

client.chat.completions.create() を叩いて応答が返り切るまでの往復時間。 ブレる要因は自分の制御外(出力トークン数 / プロバイダ混雑 / ネットワーク)。 → p99 が突出してもコードのバグではないことが多い。対策はモデル変更・ストリーミング・タイムアウト/リトライ・冗長化。


5. flush の正体(よくある誤解)

誤解: 「flush しないと送信されない」 正確: 送信は常にバックグラウンドで行われている。flush() は「溜まっているものを今すぐ送り切るまで待つ」命令。

SDK はデータを即送信せず、メモリのキューに溜めてバックグラウンドスレッドがバッチ送信する(毎回 HTTP を叩くとアプリが遅くなるため。OTel の BatchSpanProcessor と同じ発想)。 問題はすぐ終わるスクリプトで、送信前にプロセスが死ぬとデータごと消える。だから末尾に flush() で exit を待たせる。

実行形態 flush
短命スクリプト・サーバーレス(Lambda) 必須(終了前に送り切る)
常駐アプリ(FastAPI/Chainlit) 不要(プロセスが生き続ける。毎回呼ぶと逆に遅い)

「観測データを失う」方向にしか壊れないので、短命プロセスでは付けておくのが安全側。


6. クイック早見表(迷ったらここ)

困りごと 見る/使うもの
全体が遅い、どの機能か Trace latency percentiles
LLM 自体が遅いのか知りたい Generation latency percentiles
機能の中のどの段が犯人か Observation latency percentiles(右パネル)or トレースの木
LLM のコスト・トークンを見たい generation 化(as_type or ドロップイン)が前提 → Generations タブ
検索が空振りしてないか トレースの retrieve ノードの Output
ユーザー/セッションでトレースを探す with propagate_attributes(user_id=, session_id=): → Sessions/Users
複数トレースを1会話にまとめたい session_id を揃える → Sessions 画面で1本に連なる
機能・実験を分類して絞りたい tags=[...] → Traces 一覧でタグフィルタ
LangChain/LangGraph を自動トレースしたい config={"callbacks":[CallbackHandler()]} を invoke に挿す(ex07/ex08)
invoke の前後処理もトレースに入れたい @observe() で外側を包む + handler(ex07 後半)
スクリプトでトレースが届かない 末尾に langfuse.flush()

7. 教材コードとインストール版(SDK v4.7.1)の API 差分(写経でハマった所)

写経元の教材は Langfuse SDK v3 前提で書かれており、ローカルにインストールされているのは v4.7.1。 v4 で「span も generation も observation に統合」「trace 属性は伝播コンテキストで設定」という2つの大きな変更が入ったため、ex05 と ex06 はそのままでは動かず、現行版に直して実行した。

教材(v3)の書き方 現行 v4.7.1 で動く書き方 何が変わったか
ex05 langfuse.start_as_current_span(name=...) langfuse.start_as_current_observation(name=..., as_type="span") span 専用メソッドが廃止。observation に統合され as_type で種別指定(用語集 §1 の「observation = span ∪ generation」が API にも反映された形)
ex06 langfuse.update_current_trace(user_id=, session_id=, tags=) from langfuse import propagate_attributes して with propagate_attributes(trace_name=, user_id=, session_id=, tags=, metadata=): で囲む trace 属性の「後付け1回」メソッドが廃止。コンテキストマネージャになり、with の中で作る span 全部に属性が伝播する方式へ

ハマりポイント2点

  • propagate_attributesクライアントのメソッドではなく独立関数langfuse.propagate_attributes(...)AttributeError になる。from langfuse import propagate_attributes で取る
  • propagate_attributes は「できるだけ早く呼ぶ」。後から呼ぶと、それ以前に作られた span には user_id 等が乗らず、集計(per-user コスト等)から漏れる。だから chat_turn の本体の先頭で with を開く
  • name= ではなく trace_name=(メタデータの伝播関数なので命名が違う)

教訓: 教材コードを写経するときは、まず uv run python -c "import langfuse; print(langfuse.__version__)" で実バージョンを確認し、API が無いと言われたら dir(langfuse.Langfuse)dir(langfuse)(パッケージ直下)でメソッド名を引く。v3→v4 のような世代差は「メソッド名の変更」だけでなく「メソッド→コンテキストマネージャ」のような形そのものの変更を含む。


作成: 2026-06-12 / 最終更新: 2026-06-12