Langfuse WorkshopClickHouse Workshops

02 トレーシング

これはトレースステップの白紙状態です — checkpoint/01-base-app と同じコードですが、Langfuse 配線がまだありません。Langfuse パッケージは既に package.json に含まれています — npm inst...

ワークショップマテリアルは公開 langfuse/langfuse-workshop リポジトリで保守されています。実行可能なアプリ、チェックポイントブランチ、ローカルセットアップについてはリポジトリを使用してください。

このMarkdownファイルを表示

開始ポイント

git checkout checkpoint/02-tracing

これはトレースステップの白紙状態です — checkpoint/01-base-app と同じコードですが、Langfuse 配線がまだありません。Langfuse パッケージは既に package.json に含まれています — してない場合は npm install を実行します。.env に OPENAI_API_KEY とLangfuseキーが含まれていることを確認します。

トレースが必要な理由

トレースは、エージェントが実行するすべてのステップを記録します — すべてのモデル呼び出し、すべてのツール呼び出し、入った入力と返ってきた出力 — それが発生した順序で。これにより、エージェントはブラックボックスから、後で検査できるものに変わり、答えが間違っていると、推測する代わりに正確に間違ったステップをポイントできます。

より大きな視点の動機を知りたい場合は、Langfuse Academy レッスンのトレース を参照してください。技術的な詳細 (SDK オプション、OpenTelemetry 内部、スパン属性) を知りたい場合、トレース ドキュメント が詳しく説明しています。

ゴール

お父さんが「Bluetooth をオンにするにはどうすればいいですか?」と尋ねると、エージェントはOpenAIに一度ヒットするだけではありません。舞台裏では、OpenAI に何をするかを尋ね、get_support_context を呼び出してお父さんのiPhoneセットアップを取得し、OpenAI に再度尋ね、Bluetooth ステップの search_help_library を呼び出し、次に番号付きの答えを生成するためにOpenAI に3回目の質問をします。今日はそのどれも見えません。

このチャプターの目標は、Langfuse の各ステップを可視化することです — 1つのチャットターンが1つのネストされたトレースになり、エージェント実行、OpenAI世代、および2つのツール呼び出しがすべて順序で記録されます。

Specs のステップバイステッププロセス

エージェントの構造をミラーリングする3つのステップでトレースを構築します:

  1. 最初のトレース — OpenAI世代自体をログします。
  2. ネストされたトレース — 世代をターンごとに1つのエージェント実行の下にグループ化します。
  3. ツール呼び出しの記録 — 各ツール呼び出しを独自の観察にします。

ステップ1 — 最初のトレース

入力と出力が何であるか、および費用、トークン、時間がどのくらい費やされたかを確認するために、OpenAI呼び出し自体の可視性が必要です。2つの変更で十分です。

src/server/index.ts

ファイルの上部の近くでLangfuse スパンプロセッサを開始します:

import { NodeSDK } from "@opentelemetry/sdk-node";
import { LangfuseSpanProcessor } from "@langfuse/otel";

new NodeSDK({ spanProcessors: [new LangfuseSpanProcessor()] }).start();

プロセッサは、LANGFUSE_PUBLIC_KEY、LANGFUSE_SECRET_KEY、LANGFUSE_BASE_URL をNode プロセス環境から読み取ります。このワークショップでは、サーバーはLangfuse SDK が起動する前にリポジトリ .env をロードするため、エクスポートされたシェル値に依存する代わりに .env を編集します。

脇注:最後のトレースが npm run dev を停止または再起動するときに表示されることがある場合は、index.ts に戻り、上記の1行を名前付き langfuseSpanProcessor と sdk 変数に変えて、shutdown() がそれらをフラッシュできるようにします:

const langfuseSpanProcessor = new LangfuseSpanProcessor();
const sdk = new NodeSDK({ spanProcessors: [langfuseSpanProcessor] });
sdk.start();

async function shutdown() {
  server.close();
  await langfuseSpanProcessor.forceFlush();
  await sdk.shutdown();
}

トレースモデルを理解する必要はありませんが、ローカル開発で「最後のトレースはどこに行ったのか?」という混乱した瞬間を避けています。

src/server/support-agent.ts

インポートを追加します:

import { observeOpenAI } from "@langfuse/openai";

そして OpenAI クライアントを作成する場所でラップしてください。runSupportConversation でこの行を見つけます:

const openai = new OpenAI({ apiKey: env.openaiApiKey });

これに変更します:

const openai = observeOpenAI(new OpenAI({ apiKey: env.openaiApiKey }));

これが全体的な差分です。ファクトリ関数なし、独立した生のクライアントなし — observeOpenAI はOpenAI クライアントをコール サイトでインラインでラップし、openai.chat.completions.create(...) 以下は各呼び出しのトレースを出力します。

確認: npm run dev、1つの質問を尋ねて、Langfuse を更新します — OpenAI呼び出しごとに1つの世代でプロンプト、応答、トークン、レイテンシが表示されます。各世代はまだ独立したトップレベルトレースです。次に修正します。

ステップ1後のLangfuse トレースビュー — 各チャットターンは独立したopenai-chat-completion世代として表示されます。

ステップ2 — ネストされたトレース

世代をコンテキストに入れるために、ターンごとに1つのエージェント実行の下にグループ化します。src/server/support-agent.ts の3つの編集 — 関数本体の変更なし。

1. インポートを追加します:

import { observe } from "@langfuse/tracing";

2. 既存の関数を降格させます。 見つけます:

export async function runSupportConversation(request: ChatRequest): Promise<ChatResponse> {

export をドロップして名前を変更します:

async function runSupportConversationInner(request: ChatRequest): Promise<ChatResponse> {

本体はそのままです。

3. ファイルの下部にラップされたエクスポートを追加します:

export const runSupportConversation = observe(runSupportConversationInner, {
  name: "dad-it-support-chat-turn",
  asType: "agent"
});

index.ts は依然として runSupportConversation を同じ方法でインポートします。observe(...) は自動的に関数引数をトレース入力として、戻り値をトレース出力としてキャプチャします。

確認: 1つのチャットターンが、OpenAI世代がネストされた1つの dad-it-support-chat-turn 観察として表示されるようになりました。

ステップ2後のトレースツリー — OpenAI 世代が子として含まれる1つの dad-it-support-chat-turn エージェントルート。

ステップ3 — ツール呼び出しの記録

OpenAI 世代は既に tool_calls 出力でツール呼び出しに言及していますが、実際のツール実行の観察はありません — 入力が何であり、何が出たかを確認する方法はありません。各ツールに同じ observe(...) パターンを適用できます。

src/server/tools.ts

executeTool の上にインポートを追加し、2つの観察されたヘルパーを追加し、次に 既存の executeTool を以下のバージョンに置き換えて、スイッチがインラインでの作業の代わりにラップされたヘルパーを呼び出すようにします。TOOL_DEFINITIONS はそのままです。

import { observe } from "@langfuse/tracing";

const getSupportContextTool = observe(
  async () => {
    const context = getSupportContext();

    return {
      ok: true,
      context: {
        id: context.id,
        label: context.label,
        devices: context.devices,
        deviceSummary: context.deviceSummary,
        responseStyle: context.responseStyle,
        scopeHighlights: context.scopeHighlights,
        notableApps: context.notableApps
      }
    };
  },
  { name: "get_support_context", asType: "tool" }
);

const searchHelpLibraryTool = observe(
  async (input: { question: string }) => {
    const guides = searchGuides(input.question);

    return {
      ok: true,
      results: guides.map((guide) => ({
        id: guide.id,
        title: guide.title,
        summary: guide.summary,
        steps: guide.steps,
        caution: guide.caution ?? null
      }))
    };
  },
  { name: "search_help_library", asType: "tool" }
);

export async function executeTool(name: string, input: Record<string, unknown>): Promise<ToolResult> {
  switch (name) {
    case "get_support_context":
      return getSupportContextTool();

    case "search_help_library":
      return searchHelpLibraryTool({ question: String(input.question ?? "") });

    default:
      return { ok: false, error: `Unsupported tool: ${name}` };
  }
}

npm run dev が Multiple exports with the same name "executeTool" で停止する場合、元の executeTool は依然としてファイルのさらに下にあります。それを削除して上のバージョンのみを保持します。

ステップ3後の完全なトレース — dad-it-support-chat-turn (エージェント) と get_support_context + search_help_library ツール観察が兄弟として下にあり、OpenAI世代があります。

完了したことを確認する方法

  • 1つのユーザーターンがLangfuseで1つのトレースを作成します。
  • ルート観察: dad-it-support-chat-turn (タイプ agent)。
  • observeOpenAI(...) からのチャイルド世代、プロンプト、応答、トークン、レイテンシがあります。
  • チャイルドツール観察: get_support_context、search_help_library。
  • ルート入力はチャットリクエスト、ルート出力はチャット応答です。

ラップアップ

同じパターン、異なる観察タイプ、同じ概念: observe(fn, { asType }) は関数をラップし、与えられた名前と型のスパンを出力します。observeOpenAI(client) はOpenAI SDKの特殊版です。

Langfuseのベストプラクティスに沿った豊かなトレースを追加するより直接的な方法は、Langfuseスキル(/langfuse) です。推奨パターンを、各ラップを手動でロールしずに適用します。このチュートリアルが存在するのは、スキルが何をしているかを理解するためです。

observeOpenAI 自体は、公式OpenAI SDK をラップします — 舞台裏では、Langfuse OpenAI JS用の自動計装 と同じです。別のSDK (Anthropic、Vercel AI SDK、独自のHTTPクライアント) を使用している場合、Langfuse 統合カタログ には同等のラッパーまたは自動計装ガイドがあります。

付録/ボーナスセクション — ユーザーとセッションID

上記のチュートリアルは、クリーンな親 → 世代 → ツール形状でトレースを取得します。ほとんどのチームが次に望むのは、ユーザーとセッションでトレースをスライスすることです — 「このユーザーがエージェントで持っているすべてのターン」または「昨朝からのフルマルチターンセッション」を取得できるようにします。 シンプルさの理由で、ライブトレース化チュートリアルではこのステップをスキップしますが、トレース化後のチェックポイントに含まれているため、後のチャプターは別のコード ステップなしでセッション/ユーザービューを使用できます。Langfuse ドキュメントで Sessions と Users に関する情報を参照してください。

要するに:

import { propagateAttributes } from "@langfuse/tracing";

return propagateAttributes(
  {
    userId: request.userId ?? `workshop-${context.id}`,
    sessionId: request.sessionId,
    tags: ["langfuse-workshop", "dad-it-support"]
  },
  async () => {
    // ...the same tool-calling loop...
  }
);

propagateAttributes(...) ブロック内のすべて — observeOpenAI によって出力されるすべてのチャイルドスパンを含む — は自動的に userId、sessionId、およびタグを取得します。Langfuse Users ビュー、Sessions ビュー、およびタグフィルタはすべて、属性が存在するとすぐにライト アップします。

最終状態

この完成したトレース化アプリは 03-prompt-management と 04-monitoring の開始ポイントです。

このページの内容

Track your progress?

Optional. We email a link to confirm your address; progress records once you open it.

Please use your work email address, not a personal one.

Progress tracking also requires accepting the current Terms of Service in Privacy settings.

JA