🎨

プロンプトエンジニアリングを全員参加型に!Langfuseで実現するノーコードLLM改善

に公開

はじめに

こんにちは!satto workspaceでプロダクトエンジニアをしている ryohei oyama(@ryohei_oyama)です。

LLMアプリケーションの品質向上において、プロンプトエンジニアリングは最も重要な要素の1つです。

しかし、多くの開発現場では以下のような課題に直面しています:

本記事では、これらの課題をLangfuseを使って解決する方法を、実際に動くコード具体的な設定手順を交えて解説します。

🚀 Langfuseとは?何が解決できるのか

Langfuseは、LLMアプリケーションの観測・評価・プロンプト管理を一元化できるオープンソースプラットフォームです。

最大の特徴は、プロンプトの管理・編集・デプロイをすべてWebインターフェースで完結できることです。

📊 主要機能

機能 説明 メリット
プロンプトバージョン管理 Gitのような履歴管理 過去バージョンへの即座のロールバック
A/Bテスト 本番環境での安全な実験 データドリブンな意思決定
パフォーマンス分析 レスポンス時間とコストの可視化 コスト最適化と品質向上の両立
非技術者向けUI エンジニア不要でプロンプト編集 チーム全体での改善サイクル

📝 Langfuseの基本概念

🔄 プロンプトの外部管理化

import { Langfuse } from 'langfuse';

const langfuse = new Langfuse();

// バージョン指定でプロンプト取得
const prompt = await langfuse.prompt.get("user_chat_prompt", { 
  version: 2  // "latest"を指定すると最新版を取得
});

// 変数の注入({{ user_input }} 形式で定義)
const formattedPrompt = prompt.compile({ 
  user_input: "商品について質問があります" 
});

🎯 バージョン管理の特徴

機能 説明 利点
自動バージョニング 編集するたびに新バージョンが自動作成 変更履歴を完全に追跡可能
ロールバック機能 問題発生時に前のバージョンへ即座に復旧 リスクの最小化
差分表示 変更箇所がハイライトされ一目瞭然 レビューと承認が簡単

プロンプトバージョン管理画面

🧪 A/Bテストの実装方法

バリエーション分岐の実装

A/Bテスト実装コード。
import { LangfuseClient } from "@langfuse/client";
import { 
  updateActiveTrace, 
  updateActiveObservation 
} from "@langfuse/tracing";

// Langfuseクライアントの初期化
const langfuse = new LangfuseClient({
  secretKey: process.env.LANGFUSE_SECRET_KEY,
  publicKey: process.env.LANGFUSE_PUBLIC_KEY,
  baseUrl: process.env.LANGFUSE_BASE_URL,
});

export async function POST(req: Request) {
  const { messages, chatId }: { 
    messages: UIMessage[], 
    chatId: string 
  } = await req.json();
  
  // 最新のメッセージを取得
  const inputText = messages[messages.length - 1].parts
    .find((part) => part.type === "text")?.text;
  
  // A/Bテスト用の分岐
  const variant: 'A' | 'B' = Math.random() < 0.5 ? "A" : "B";
  const version = variant === "A" ? 1 : 2;
  
  // Langfuseからプロンプトを取得
  const prompt = await langfuse.prompt.get("user_chat_prompt", {
    version: version
  });
  
  // アクティブトレースを更新
  updateActiveTrace({
    name: "chat-completion",
    sessionId: chatId,
    userId: "user-123", // 実際のユーザーIDを使用
    input: inputText,
    metadata: { 
      ab_test_variant: variant,
      prompt_version: version 
    }
  });
  
  console.log(\`Using prompt variant: \${variant}, version: \${version}\`);
}

ユーザー満足度の記録

フィードバック記録コード。
interface FeedbackData {
  traceId: string;
  satisfaction: number;
  source: string;
}

// ユーザーの満足度を記録
function recordUserFeedback(data: FeedbackData): void {
  langfuse.score({
    traceId: data.traceId,
    name: "user_satisfaction",
    value: data.satisfaction,  // 1-5のスケール
    metadata: { feedback_source: data.source }
  });
}

🚀 実装ステップガイド

Step 1️⃣ 環境構築

🐳 Docker Composeでローカル環境

git clone https://github.com/langfuse/langfuse.git
cd langfuse
docker compose up -d

☁️ SaaS版を利用

langfuse.com でアカウント作成(無料プランあり)

Step 2️⃣ プロンプト作成

1️⃣ 管理画面にアクセス
http://localhost:3000 を開く。

2️⃣ 新規プロンプトを作成
PromptsCreate New をクリック。

3️⃣ 初期バージョンを登録
名前とプロンプト内容を設定。

プロンプト作成画面

Step 3️⃣ バックエンド実装

Next.js API Routeの例

app/api/chat/route.ts のコード。
// app/api/chat/route.ts
import { google } from "@ai-sdk/google";
import { createVertex } from "@ai-sdk/google-vertex";
import { LangfuseClient } from "@langfuse/client";
import {
  convertToModelMessages,
  stepCountIs,
  streamText,
  type UIMessage,
} from "ai";
import { langfuseSpanProcessor } from "@/instrumentation";
import { after } from "next/server";
import {
  observe,
  updateActiveObservation,
  updateActiveTrace,
} from "@langfuse/tracing";
import { trace } from "@opentelemetry/api";

// Vertex AIの設定
const vertex = createVertex({
  location: "global",
  project: process.env.GOOGLE_VERTEX_PROJECT,
  baseURL: `https://aiplatform.googleapis.com/v1/projects/${process.env.GOOGLE_VERTEX_PROJECT}/locations/global/publishers/google`,
  googleAuthOptions: {
    credentials: {
      client_email: process.env.GOOGLE_VERTEX_CLIENT_EMAIL,
      private_key: process.env.GOOGLE_VERTEX_PRIVATE_KEY,
    },
  },
});

// Langfuseクライアントの初期化
const langfuse = new LangfuseClient({
  secretKey: process.env.LANGFUSE_SECRET_KEY,
  publicKey: process.env.LANGFUSE_PUBLIC_KEY,
  baseUrl: process.env.LANGFUSE_BASE_URL,
});

// ハンドラー関数
const handler = async (req: Request) => {
  const { messages, chatId }: { messages: UIMessage[], chatId: string } = await req.json();

  // 入力テキストを取得(parts配列から)
  const inputText = messages[messages.length - 1].parts
    .find((part) => part.type === "text")?.text;
   
  // Observation(観測)を更新
  updateActiveObservation({
    input: inputText,
  });
   
  // トレースを更新
  updateActiveTrace({
    name: "my-ai-sdk-trace",
    sessionId: chatId,
    userId: "123",
    input: inputText,
  });

  // A/Bテスト用のバリアントを選択
  const variant: 'A' | 'B' = Math.random() < 0.5 ? "A" : "B";
  const version = variant === "A" ? 1 : 2;

  // Langfuseからプロンプトを取得
  const prompt = await langfuse.prompt.get("user_chat_prompt", {
    version: version
  });

  console.log(`Using prompt variant: ${variant}, version: ${version}`);

  // ストリーミングレスポンスを生成
  const result = streamText({
    model: vertex("gemini-2.5-flash"),
    messages: convertToModelMessages(messages),
    tools: {
      url_context: google.tools.urlContext({}),
    },
    stopWhen: stepCountIs(5),
    system: prompt.compile({}),
    experimental_telemetry: {
      isEnabled: true,
      metadata: {
        langfusePrompt: prompt.toJSON(),
      }
    },
    onFinish: async (result) => {
      // 成功時の観測とトレースを更新(contentプロパティを使用)
      updateActiveObservation({
        output: result.content,
      });
      updateActiveTrace({
        output: result.content,
      });
 
      // スパンを手動で終了
      trace.getActiveSpan()?.end();
    },
    onError: async (error) => {
      // エラー時の観測とトレースを更新
      updateActiveObservation({
        output: error,
        level: "ERROR"
      });
      updateActiveTrace({
        output: error,
      });
 
      // スパンを手動で終了
      trace.getActiveSpan()?.end();
    },
  });

  // レスポンス後にLangfuseにフラッシュ
  after(async () => await langfuseSpanProcessor.forceFlush());

  return result.toUIMessageStreamResponse();
}

// observeでラップしたハンドラーをエクスポート
export const POST = observe(handler, {
  name: "handle-chat-message",
  endOnExit: false, // ストリーム終了後に観測を終了
});

Step 4️⃣ フロントエンド実装

React + Vercel AI SDKの例

app/page.tsx のコード。
// app/page.tsx
"use client";

import { useChat } from "@ai-sdk/react";
import { useState } from "react";
import { PromptInput, PromptInputTextarea } from "@/components/ai-elements/prompt-input";
import { Conversation, ConversationContent } from "@/components/ai-elements/conversation";
import { Message, MessageContent } from "@/components/ai-elements/message";
import { Response } from "@/components/ai-elements/response";

export default function ChatPage() {
  const [input, setInput] = useState("");
  const [chatId] = useState(() => `chat-${Date.now()}`);
  
  const { messages, sendMessage, status } = useChat({
    api: "/api/chat",
    body: {
      chatId, // セッションIDを送信(A/Bテスト追跡用)
    },
  });

  const handleSubmit = (message: { text: string }) => {
    if (!message.text) return;
    
    sendMessage({
      text: message.text,
    });
    setInput("");
  };

  return (
    <div className="max-w-4xl mx-auto p-6 h-screen">
      <div className="flex flex-col h-full">
        {/* メッセージ表示エリア */}
        <Conversation className="h-full">
          <ConversationContent>
            {messages.map((message) => (
              <div key={message.id}>
                {message.parts.map((part, i) => {
                  // テキストパートのみを表示
                  if (part.type === "text") {
                    return (
                      <Message key={`${message.id}-${i}`} from={message.role}>
                        <MessageContent>
                          <Response>{part.text}</Response>
                        </MessageContent>
                      </Message>
                    );
                  }
                  return null;
                })}
              </div>
            ))}
            {status === "submitted" && (
              <div className="animate-pulse">入力中...</div>
            )}
          </ConversationContent>
        </Conversation>

        {/* 入力エリア */}
        <PromptInput
          onSubmit={handleSubmit}
          className="mt-4"
        >
          <PromptInputTextarea
            onChange={(e) => setInput(e.target.value)}
            value={input}
            placeholder="メッセージを入力..."
          />
        </PromptInput>
      </div>
    </div>
  );
}

Step 5️⃣ 環境変数設定

.env.local ファイルを作成:

# Langfuse設定
LANGFUSE_SECRET_KEY=your-secret-key
LANGFUSE_PUBLIC_KEY=your-public-key
LANGFUSE_BASE_URL=http://localhost:3000

# Google Vertex AI設定(任意のLLMプロバイダーに変更可)
GOOGLE_VERTEX_PROJECT=your-project-id
GOOGLE_VERTEX_CLIENT_EMAIL=service-account@example.com
GOOGLE_VERTEX_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n..."

📈 実運用での活用

チャットUIの実装例

実際のチャット画面では、ユーザーは通常通りメッセージを送信します。バックグラウンドでは自動的にA/Bテストが実行され、ランダムに選ばれたプロンプトバージョンが使用されます。

チャット画面のスクリーンショット
ユーザーには通常どおりのチャット体験が提供される

プロンプト編集画面

Langfuseの管理画面では、非技術者でも直感的にプロンプトを編集できます。各バージョンの編集履歴も自動的に保存されます。

Langfuseプロンプト編集画面
プロンプト編集画面 - バージョン管理と変数設定が可能

A/Bテスト結果の分析

トレース情報

各会話セッションの詳細なトレース情報を確認できます。どのバリアント(A/B)が使用されたか、レスポンス時間、トークン使用量などが記録されています。

Langfuseトレース画面
トレース一覧 - 各セッションの詳細情報を確認

メトリクス比較

Langfuseのプロンプトメトリクス画面では、各プロンプトバージョンのパフォーマンスを詳細に比較できます。

Langfuseプロンプトメトリクス
プロンプトメトリクス画面 - バージョン別のパフォーマンス指標

📊 主要な分析指標

  • Version:プロンプトのバージョン番号(1、2など)
  • Labels:バージョンのラベル(production、latestなど)
  • Median latency:レスポンス時間の中央値(例:1.329秒 vs 1.493秒)
  • Median input/output tokens:入出力トークンの中央値
  • Median cost:使用コストの中央値($0.000011 vs $0.000009)
  • Generations count:実行回数(10回 vs 4回)
  • Trace/Generation Scores:評価スコア
  • Last used:最終使用日時

この例では、バージョン2(production、latest)とバージョン1を比較できます。バージョン2はレスポンス時間が若干速く(1.329秒 vs 1.493秒)、実行回数も多くなっています(10回 vs 4回)。

👩‍💼 非技術者でもできる!運用ガイド

🔄 プロンプト改善のPDCAサイクル

サイクルの詳細:

  1. 分析:Langfuse Dashboardで現在の性能指標を確認
  2. 編集:管理画面でプロンプトを修正
  3. テスト:新バージョンでA/Bテストを開始
  4. 評価:数日後に結果を比較
  5. 採用:優れたバージョンを本番環境に適用

💡 運用のベストプラクティス

✅ Do(推奨事項)

  • 小さな改善を積み重ねる(一度に大きく変えない)
  • 仮説を立てる(「これで応答時間が短くなるはず」)
  • 変更理由を記録する(後で振り返れるように)

❌ Don't(避けるべき事項)

  • 本番環境でいきなり大きく変更する
  • テストなしで完全に切り替える
  • 過去のバージョンを削除する(履歴は貴重な資産)

⚠️ 運用時の3つの注意点

1️⃣ データ量に注意する

A/Bテストは最低100件以上のサンプルを集めてから判断しましょう。

2️⃣ 段階的に展開する

新プロンプトは 10% → 30% → 50% → 100% と徐々に利用率を上げましょう。

3️⃣ 定期的に整理する

月に1回は使用されていない古いバージョンをアーカイブしましょう。

🔧 補足実装例

フィードバック収集API

フィードバックAPIエンドポイント。
interface FeedbackRequest {
  sessionId: string;
  rating: number;
  comment?: string;
  category: 'helpful' | 'accurate' | 'relevant' | 'overall';
}

// app/api/feedback/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { Langfuse } from 'langfuse';

const langfuse = new Langfuse({
  secretKey: process.env.LANGFUSE_SECRET_KEY!,
  publicKey: process.env.LANGFUSE_PUBLIC_KEY!,
  baseUrl: process.env.LANGFUSE_BASE_URL || "http://localhost:3000"
});

export async function POST(req: NextRequest) {
  try {
    const feedback: FeedbackRequest = await req.json();
    
    // フィードバックをLangfuseに記録
     langfuse.score.create({
      traceId: feedback.sessionId,  // chatIdがトレースIDとして使用される
      observationId: feedback.messageId,  // 特定のメッセージに紐付け
      name: `user_${feedback.category}`,
      value: feedback.rating,  // 0 or 1
      dataType: "BOOLEAN",
      comment: feedback.rating === 0 ? "Not helpful" : "Helpful"
    });
    
    return NextResponse.json({ success: true, message: "フィードバックを記録しました" });
    
  } catch (error) {
    console.error('Feedback API error:', error);
    return NextResponse.json(
      { error: "フィードバックの記録に失敗しました" },
      { status: 500 }
    );
  }
}

🎉 導入効果

開発チームへの効果

開発効率の向上

  • プロンプト修正にデプロイ不要
  • TypeScriptによる型安全性
  • Next.jsエコシステムとの統合

品質管理の改善

  • 完全なバージョン管理
  • 本番環境での安全な実験
  • データドリブンな意思決定

ビジネスチームへの効果

自律的な運用

  • 非技術者による直接編集
  • リアルタイムでの効果測定
  • 迅速なロールバック

継続的改善

  • A/Bテストによる最適化
  • ユーザーフィードバックの活用
  • コスト効率の可視化

🎯 まとめ

Langfuseは、LLMアプリケーションのプロンプト管理を民主化する強力なツールです。

🔄 導入前と導入後の変化

📊 最大の価値

1️⃣ 開発速度の劇的な向上

  • プロンプト修正のリードタイムが数日→数分に短縮
  • エンジニアリソースを本質的な開発に集中できる

2️⃣ データに基づく改善サイクル

  • A/Bテストで効果を定量的に測定
  • 勘や経験ではなく、実データに基づく意思決定

3️⃣ チーム全体でのオーナーシップ

  • CS、PM、マーケティングチームも改善に参加
  • 顧客に最も近い人が直接改善できる

🎯 Langfuseが特に効果的な5つのケース

こんなチームにおすすめ:

プロンプトの微調整が頻繁 → デプロイ不要で即座に反映。
非技術者からの改善要望が多い → GUIで誰でも編集可能。
A/Bテストを実施したい → コード変更不要でテスト実行。
バージョン管理が複雑 → Gitのような履歴管理で解決。
コスト最適化をしたい → 詳細なメトリクス分析が可能。

参考文献

ソフトバンク株式会社_satto開発チーム

Discussion