なぜ「10言語SDKを自前で書かない」のか
前章でLangfuseサーバ側のアーキテクチャを見ました。本章は逆サイドのクライアント — SDKと自動計装の話です。 LLMアプリはPython/TypeScriptが中心ですが、Go・Rust・Java・Ruby・PHP・C#...と言語は10を超えます。 LangfuseはこのSDKの多言語展開問題に対し、OpenTelemetryへの完全準拠 という解を選びました。
OTel互換エンドポイント
Langfuseは /api/public/otel/v1/traces というOTLP HTTPエンドポイントを公開しています。
OTel SDKが生成する OTLP/HTTP Span をそのまま受け取り、サーバ側で Trace / Observation / Score に変換します。
graph LR
APP[アプリコード] --> OTEL[OpenTelemetry SDK<br/>(任意の言語)]
OTEL -->|OTLP/HTTP| LFOTEL[Langfuse OTLP Endpoint]
LFOTEL --> WORKER[Worker]
WORKER --> CH[(ClickHouse)]
subgraph auto instrumentation[自動計装]
OI[OpenInference]
OL[OpenLLMetry]
LC[LangChain Callback]
LI[LlamaIndex Handler]
VA[Vercel AI SDK telemetry]
end
auto instrumentation -.-> OTELPython SDK v3 — @observe と OTel統合
Python SDK v3(2025年リリース)は、v2の独自バッチャーからOTel SpanProcessorベースへ全面リライトされました。
ユーザーが書くコードは @observe() デコレータひとつです。
from langfuse import observe, get_client
langfuse = get_client()
@observe(name="rag-query")
def rag_query(question: str) -> str:
docs = retrieve(question) # ここも @observe で計装可
answer = call_llm(question, docs) # 自動計装 or 手動
langfuse.update_current_trace(
user_id="u_42",
session_id="s_abc",
tags=["prod", "ja"],
)
return answer @observe はバックエンドで OTel Span を開始・終了し、Langfuse独自属性(model / usage / cost / prompt など)をSpan属性に載せます。
送信はOTel BatchSpanProcessorを流用し、v2時代に必要だった「明示的なflush」問題が大幅に改善されました。
LLMコールの計装 — @observe(as_type="generation")
from openai import OpenAI
from langfuse import observe
client = OpenAI()
@observe(as_type="generation")
def call_llm(question: str, docs: list[str]) -> str:
completion = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "Answer from context."},
{"role": "user", "content": f"Q: {question}\nContext: {docs}"},
],
)
return completion.choices[0].message.content as_type="generation" にすると、Observation種別が Generation になり、model / prompt_tokens / completion_tokens / cost が一級属性として扱われます。
OpenAI/Anthropic/Gemini/Bedrockなどメジャープロバイダのレスポンス形式は自動パースされ、手動で詰める必要はありません。
TypeScript SDK v4 — observe() / startActiveObservation()
TS SDK v4(2025年後半リリース)も同様にOTelネイティブ化されました。 Next.js / Node / Deno / Bun / Edge Runtime のいずれでも動作し、Vercel AI SDK の experimental_telemetry と直結できます。
import { observe, startActiveObservation, updateActiveTrace } from "@langfuse/tracing";
export const ragQuery = observe(
{ name: "rag-query" },
async (question: string) => {
const docs = await startActiveObservation("retrieve", async () => {
return retrieve(question);
});
const answer = await callLlm(question, docs); // Vercel AI SDK の generateText を計装済み
updateActiveTrace({ userId: "u_42", sessionId: "s_abc", tags: ["prod", "ja"] });
return answer;
}
); Vercel AI SDK との統合
Vercel AI SDK は experimental_telemetry オプションでOTel Spanを出力します。
Langfuseのセットアップはグローバルに NodeTracerProvider + LangfuseSpanProcessor を登録するだけです。
import { registerOTel } from "@vercel/otel";
import { LangfuseSpanProcessor } from "@langfuse/otel";
registerOTel({
serviceName: "my-app",
spanProcessors: [new LangfuseSpanProcessor()],
});
// 以降、Vercel AI SDK 呼び出しが自動で Langfuse に送られる
import { generateText } from "ai";
await generateText({
model: "openai/gpt-4o-mini",
prompt: "hello",
experimental_telemetry: { isEnabled: true, functionId: "greet" },
}); LangChain / LlamaIndex 連携
| フレームワーク | 接続方法 | 記録される情報 |
|---|---|---|
| LangChain (Python/JS) | CallbackHandler を instantiate して config に渡す。または OTel + OpenInference 経由 | Chainのステップ、LLMコール、Tool呼び出し、Retriever結果をネストされたObservationとして記録 |
| LlamaIndex | GlobalHandlerに set_global_handler("langfuse")。OTel + OpenLLMetry でも可 | QueryPipelineの各ステップ、Retriever / Synthesizer / Reranker がSpanとして記録 |
| OpenInference (Arize) | LlamaIndex/LangChain/Groq/OpenAI/Bedrockの自動計装。Langfuse OTel Endpointに向ける | フレームワーク内部の詳細なSpan。Langfuseは OpenInference Semantic Conventions を尊重 |
| OpenLLMetry (Traceloop) | OpenAI/Anthropic/Cohere/Pineconeなどの低レベル計装 | SDK呼び出し単位のGeneration。細粒度観測に有利 |
| CrewAI / AutoGen / Haystack | 公式連携またはOpenInference経由 | エージェント思考の各ステップを階層構造で記録 |
PIIマスキングとサンプリング
本番運用ではPII(個人情報)の扱いが必須です。Langfuse SDKは SpanProcessor 層で「送信前に中身を書き換える」仕組みを提供します。
from langfuse import Langfuse
import re
EMAIL_RE = re.compile(r"[\w.-]+@[\w.-]+")
def mask(data):
if isinstance(data, str):
return EMAIL_RE.sub("[REDACTED_EMAIL]", data)
if isinstance(data, dict):
return {k: mask(v) for k, v in data.items()}
return data
langfuse = Langfuse(mask=mask) # input/output/metadata すべてに適用 | 機能 | 目的 | SDK設定 |
|---|---|---|
| Masking | PII/Secretsを送信前に書き換え | mask=callable を SDK に渡す |
| Sampling | 大量Traceを間引いてコスト抑制 | sample_rate=0.1 など。OTel Samplerも利用可 |
| Environment分離 | prod / staging / dev を分ける | environment フィールド または タグで区別 |
| Release tag | デプロイ単位で比較 | release="v1.2.3" を SDK init で渡す |
| Do not send | 特定Spanを完全除外 | SpanProcessor前段でフィルタ、または update_current_observation(metadata={"skip": True}) |
他言語対応 — 素のOTelで接続
公式SDKは Python/TS ですが、Go/Java/Ruby/C#などでも OTLP HTTPエンドポイントに送信する設定 をすれば動きます。
// 環境変数だけでLangfuseに接続(どの言語でも)
// OTEL_EXPORTER_OTLP_ENDPOINT=https://cloud.langfuse.com/api/public/otel
// OTEL_EXPORTER_OTLP_HEADERS=Authorization=Basic <base64(pk:sk)>
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(otlptracehttp.NewClient()),
) まとめ
次章はもう一つの柱 Prompt Management。バージョン管理 / A/B / CI/CDとどう繋ぐかを掘り下げます。
理解度チェック
Langfuse が多言語対応のために選んだ基本戦略として正しいものはどれか?
キーボード: 1〜4 で選択、Enter で回答