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 呼び出し」か? - Yes →
generation(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.openaiがcreate()を横取りして自動記録 - 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