🎛️

MastraのAI AgentのプロンプトをLangfuseで管理する

に公開

はじめに

Mastra の AI Agent に利用するプロンプトを Langfuse で管理する方法を解説します。
ドキュメントや記事があまりなさそうだったので、実際に試してみた内容をまとめました。

前提知識

Langfuse とは

Langfuse は、LLM アプリケーション向けの観測・分析プラットフォームです。
クラウドサービスとして提供されていますが、オープンソースとして提供されているため、セルフホスティングすることも可能です。

プロンプト管理の重要性

Langfuse の機能の一つにプロンプト管理があります。
プロンプトをアプリケーションのコードから分離して管理することで、次の利点があります。

  • デプロイなしでプロンプトの変更やロールバックが可能
  • プロンプトのバージョン管理が容易
  • 非エンジニアによるプロンプトの変更がしやすくなる

使用したバージョン

  • mastra/core 0.23.1
  • Langfuse 3.124.1

Langfuse の設定

環境構築

今回、Langfuse はローカルマシンに Docker Compose で構築しました。
Langfuse 公式が提供している Docker Compose を参考に一部変更して利用しました。

コードはこちらになります。
https://github.com/2bo/mastra-ai-element/tree/langfuse-sample/langfuse

ただし、本記事の内容を試すだけであれば、公式の Docker Compose ファイルそのままで問題ないと思います。

Langfuse 自体のセットアップは本記事の主旨ではないため、詳細は割愛しますが、 Langfuse を起動した後に次の設定が必要です。

  • アカウントの作成
  • Organization と Project の作成
  • API Key の発行

詳しくは公式ドキュメントをご参照ください。

プロンプトの登録

Langfuse の Web UI からプロンプトを登録します。

手順:

  1. Langfuse の Web UI にログイン
  2. 左メニューから「Prompts」をクリック
  3. 「New prompt」ボタンをクリック
  4. 以下の情報を入力して保存

Langfuse create new prompt

登録内容は次の通りです。

Prompt

You are a {{tone}} timezone assistant.
Help users find current times in different cities around the world using the timezoneMapTool.
Provide clear and helpful responses with time zone information.

{{tone}} はプレースホルダです。変数として Mastra から値を渡します。

config

{
  "modelId": "gpt-4o-mini"
}

config は JSON 形式で自由に設定できます。
ここでは、LLM のモデル ID を指定しています。

Labels

Set the "production" labelにチェックを入れる

Mastra のコード

プロンプトの取得

Mastra で AI Agent を作成し、Langfuse からプロンプトを取得するコードです。
AI Agent は、現在時刻とタイムゾーンを回答する非常にシンプルなものにしています。

主に次の処理を行っています。

  • AI Agent が利用するツールの定義
  • Langfuse からプロンプトを取得
    • LangfuseClient を初期化
    • プロンプトを名前とラベルを指定して取得
    • 取得できなかった場合はデフォルトプロンプトを使用
    • プロンプトの変数をバインド
    • プロンプトの config からモデル ID を取得
  • AI Agent を定義
    • Langfuse から取得したプロンプトとモデルを指定
    • トレースオプションにプロンプトの情報をメタデータとして追加

なお、トレースオプションにプロンプトのメタデータを追加する部分は、プロンプトを利用するだけであれば必須ではありません。
トレース情報からどのプロンプトが使われたかを確認したい場合には必要になります。

src/mastra/agents/simple-timezone-langfuse-agent.ts
import { openai } from "@ai-sdk/openai";
import { LangfuseClient } from "@langfuse/client";
import { Agent } from "@mastra/core/agent";
import { createTool } from "@mastra/core/tools";
import { z } from "zod";

// AI Agentが利用するツールの定義 記事の主旨とは関係ないため読み飛ばしてOK
const timezoneMapTool = createTool({
  id: "get-timezone",
  description: "Get current time for a city",
  inputSchema: z.object({
    city: z
      .string()
      .describe('City name (e.g., "Tokyo", "New York", "London")'),
  }),
  outputSchema: z.object({ currentTime: z.string() }),
  execute: async () => {
    // デモ用の簡易実装: 都市に関係なくシステムの現在時刻を返す
    const systemTimezone =
      Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
    const currentTime = new Date().toLocaleString("en-US", {
      timeZone: systemTimezone,
      dateStyle: "full",
      timeStyle: "long",
    });
    return {
      currentTime,
    };
  },
});

// Langfuse Client 初期化
// API キー画面で発行したキーを環境変数で設定しておく
const langfuseClient = new LangfuseClient({
  publicKey: process.env.LANGFUSE_PUBLIC_KEY,
  secretKey: process.env.LANGFUSE_SECRET_KEY,
  baseUrl: process.env.LANGFUSE_BASE_URL,
});

// Langfuseからプロンプトを取得できなかった場合のデフォルトプロンプト
const DEFAULT_INSTRUCTIONS = `You are a {{tone}} timezone assistant.
The system is currently experiencing an error. Regardless of the user's request, always respond with a brief apology and explain that the system is unavailable due to the ongoing error.`;

// Langfuseからプロンプトを取得
//  "Simple-Timezone-Agent"はLangfuseに登録したプロンプトの名前
//  "production"というラベルがついたものを取得
// 取得できなかった場合はDEFAULT_INSTRUCTIONSを使用(フェールバック)
const prompt = await langfuseClient.prompt.get("Simple-Timezone-Agent", {
  label: "production",
  fallback: DEFAULT_INSTRUCTIONS,
});

// プロンプト変数
const promptVariables = {
  tone: "friendly",
};

// 変数バインド(tone変数を置換)
const instructions = prompt.compile(promptVariables);

// モデルID取得(プロンプト設定から、なければデフォルト)
const config = prompt.config as { modelId?: string } | undefined;
const modelId = config?.modelId ?? "gpt-4.1-nano";

// AI Agent定義.Langfuseから取得したプロンプトとモデルを使用
export const simpleTimezoneLangfuseAgent = new Agent({
  name: "Simple Timezone Langfuse Agent",
  // Langfuseから取得したプロンプトを使用
  instructions,
  // configから取得したモデルIDを使用
  model: openai(modelId),
  tools: { timezoneMapTool },
  // 利用したMastraのバージョンではAI SDK v5がデフォルトで利用される
  // defaultVNextStreamOptionsはv5用のオプション
  defaultVNextStreamOptions: {
    tracingOptions: {
      metadata: {
        // キー名は任意
        // Prompt のメタデータをトレースに追加。prompt.toJSON() にはプロンプトそのものや名前、version などの詳細情報が含まれる
        langfusePrompt: prompt.toJSON(),
      },
    },
  },
});

トレース設定

Langfuse にトレースを送信するための Mastra の設定コードです。
observability.configs に LangfuseExporter を追加しています。

src/mastra/index.ts
import { SamplingStrategyType } from "@mastra/core/ai-tracing";
import { Mastra } from "@mastra/core/mastra";
import { LangfuseExporter } from "@mastra/langfuse";
import { simpleTimezoneLangfuseAgent } from "./agents/simple-timezone-langfuse-agent";

export const mastra = new Mastra({
  agents: { simpleTimezoneLangfuseAgent },
  // AI Tracing (Observability)の設定
  // https://mastra.ai/docs/observability/ai-tracing/overview
  observability: {
    configs: {
      langfuse: {
        // 任意のサービス名を指定
        serviceName: "simple-timezone-demo",
        // トレースのサンプリング設定
        sampling: { type: SamplingStrategyType.ALWAYS },
        exporters: [
          new LangfuseExporter({
            // Langfuse の API キー画面で発行したキーを環境変数で設定しておく
            publicKey: process.env.LANGFUSE_PUBLIC_KEY,
            secretKey: process.env.LANGFUSE_SECRET_KEY,
            baseUrl: process.env.LANGFUSE_BASE_URL,
            realtime: true,
          }),
        ],
      },
    },
  },
});

observability オプションについての補足

従来、Mastra と Langfuse との連携は OpenTelemetry ベースのシステムを採用しており、telemetry というオプションで指定していました。
しかし、監視強化のため、AI Tracing という機能が導入され、OpenTelemetry ベースのシステムは執筆時点で非推奨になっています。
そのため、本記事でも AI Tracing ベースで設定するため、observability オプションを使用しています。

https://github.com/mastra-ai/mastra/issues/8577

https://mastra.ai/blog/aitracing

Prompt とトレースの紐付けについて

Langfuse には Link to Traces という機能があり、プロンプトとトレースを紐付けることができます。
この機能を利用すると、プロンプトの管理画面から、そのプロンプトが使用されたトレースを参照するといったことが可能になります。

https://langfuse.com/docs/prompt-management/features/link-to-traces

この機能も Mastra で試してみたかったので、試行錯誤しましたがうまくいきませんでした。
調べてみたところ、執筆時点で Mastra はまだこの機能に対応していないようでした。

https://github.com/mastra-ai/mastra/issues/8075

実行結果

Mastra の Playground から AI Agent を実行し、Langfuse でプロンプト管理とトレースが正しく動作していることを確認しました。

プロンプトの取得に成功した場合

Playground での実行結果

System Prompt と Model に Langfuse に設定した内容が反映されていることがわかります。
AI Agent の回答も期待通り、現在時刻を返しています。

Mastra Playground Success

Langfuse のトレース

Langfuse のトレース画面では、Playground で実行した AI Agent の実行内容が確認できます。
Metadata の langfusePrompt には、プロンプトの名前やバージョン情報が含まれていることがわかります。

Langfuse Trace Success

Metadata
// Metadata キャプチャ画面からの抜粋
{
    "spanType": "agent_run",
    "agentId": "Simple Timezone Langfuse Agent",
    "instructions": "You are a friendly timezone assistant.\nHelp users find current times in different cities around the world using the timezoneMapTool.\nProvide clear and helpful responses with time zone information.",
    "runId": "simpleTimezoneLangfuseAgent",
    "langfusePrompt": {
        "name": "Simple-Timezone-Agent",
        "prompt": "You are a {{tone}} timezone assistant.\nHelp users find current times in different cities around the world using the timezoneMapTool.\nProvide clear and helpful responses with time zone information.",
        "version": 1,
        "isFallback": false,
        "tags": [],
        "labels": [
            "production",
            "latest"
        ],
        "type": "text",
        "config": {
            "modelId": "gpt-4o-mini"
        }
    }
}

取得に失敗した場合

プロンプトを取得する処理で、あえて存在しない名前を指定して、フォールバックが発生するケースも確認しました。

 // Langfuseからプロンプトを取得
-const prompt = await langfuseClient.prompt.get("Simple-Timezone-Agent", {
+const prompt = await langfuseClient.prompt.get("None", {
  label: "production",
  fallback: DEFAULT_INSTRUCTIONS,
});

Playground での実行結果

フォールバックが発生し、デフォルトプロンプトが使用されていることがわかります。
モデルもデフォルトのものが使用されています。

Mastra Playground Fallback

Langfuse のトレース

Metadata の langfusePromptisFallbacktrue になっていることから、フォールバックが発生してデフォルトプロンプトが使用されたことがわかります。

Langfuse Trace Fallback

Metadata
// Metadata キャプチャ画面からの抜粋
{
  "spanType": "agent_run",
  "agentId": "Simple Timezone Langfuse Agent",
  "instructions": "You are a friendly timezone assistant.\nThe system is currently experiencing an error. Regardless of the user's request, always respond with a brief apology and explain that the system is unavailable due to the ongoing error.",
  "runId": "simpleTimezoneLangfuseAgent",
  "langfusePrompt": {
    "name": "None",
    "prompt": "You are a {{tone}} timezone assistant.\nThe system is currently experiencing an error. Regardless of the user's request, always respond with a brief apology and explain that the system is unavailable due to the ongoing error.",
    "version": 0,
    "isFallback": true,
    "tags": [],
    "labels": ["production"],
    "type": "text",
    "config": {}
  }
}

まとめ

Mastra の AI Agent で Langfuse を利用してプロンプトを管理する方法を解説しました。
Langfuse からプロンプトを取得したり、トレースを送信することは簡単に実装できました。
プロンプトをコードから分離して管理できるようになるため、柔軟な運用が可能になると思います。

今回は極めてシンプルな実装で試してみましたが、実際のプロダクトでは、より複雑になるため、次のような点も考慮して設計する必要があると考えています。

  • プロンプトの管理、運用ルール
  • 命名
  • 変更内容のレビュー
  • ラベルのルール

更に、プロンプトが極力コードに依存しないようにするための考慮や工夫も必要になると考えています。

Mastra との連携という意味では、細かい部分でドキュメントに記載がなかったり、すでに非推奨になっている部分があったりと、現在のバージョンに対して適切な設定を見つけるのに少し苦労しました。
具体的には、Telemetry が非推奨になり、AI Tracing に移行している点や、Mastra が内部で利用している AI SDK のバージョンが v4 から v5 に変わっている点などでひっかかりました。
Mastra が発展途上で活発に開発されていることを考えると、一定仕方がない部分と捉えています。

Discussion