なぜ「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 -.-> OTEL
Langfuse OTelエンドポイントへ向かう経路。自動計装ライブラリ(OpenInference/OpenLLMetry/LangChain/LlamaIndex/Vercel AI SDK)はOTel SDKを経由してOTLPで送信するだけで動く

Python 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とどう繋ぐかを掘り下げます。

理解度チェック

問題 0 / 50%
Q1

Langfuse が多言語対応のために選んだ基本戦略として正しいものはどれか?

キーボード: 1〜4 で選択、Enter で回答