[{"content":"","date":"2026年4月9日","externalUrl":null,"permalink":"/","section":"GAO AI Blog","summary":"","title":"GAO AI Blog","type":"page"},{"content":"","date":"2026年4月9日","externalUrl":null,"permalink":"/tags/librechat/","section":"タグ","summary":"","title":"LibreChat","type":"tags"},{"content":" 1. はじめに # LibreChat は、OpenAI・Google Gemini・Anthropic など複数の LLM に対応したオープンソースのチャット UI です。セルフホストすることで、組織や個人の用途に合わせた AI チャット基盤を構築できます。\nLibreChat のデータベースには MongoDB が必要です。Google Cloud で MongoDB を使うには、GCE や GKE 上に Docker で構築するか、Google Cloud マーケットプレイスから MongoDB Atlas を契約する方法が一般的でした。前者はインスタンスの管理が必要になり、後者はフルマネージドですが別サービスとの契約・連携が必要です。\nそこで注目したのが、Firestore の MongoDB 互換モードです。Firestore の MongoDB 互換モードを利用すると、MongoDB を別途構築・運用することなく、フルマネージドなサーバーレス構成で LibreChat を動かせます。本記事では、この構成の検証結果と構築手順を紹介します。\n先に検証結果をお伝えします。\n結論 # 結論としては、基本的なチャット機能は正常に動作します。ただし、エージェントやプロンプトの一覧がリロード後に表示されなくなる、管理者ダッシュボードが使えないといった問題があります。これは Firestore MongoDB 互換が LibreChat の権限管理で内部的に利用する MongoDB ビット演算子クエリをサポートしておらず、権限チェックが失敗するためです。\n2. アーキテクチャ概要 # 今回構築した環境の全体像です。Cloud Run をアプリケーション基盤とし、データベースに Firestore MongoDB 互換を採用したサーバーレス構成になっています。\n構成図 # LibreChat構成図 GCP リソース一覧 # リソース 用途 Cloud Run (LibreChat) メインアプリケーション Firestore Enterprise MongoDB 互換モードのデータベース Vertex AI Gemini API によるチャット Secret Manager JWT シークレット、SA キー等の管理 Cloud Storage librechat.yaml の配信（GCS FUSE マウント） Artifact Registry ghcr.io リモートリポジトリ リポジトリ構成 # . ├── librechat.yaml # LibreChat 設定ファイル └── terraform/ ├── main.tf # 全リソース・変数定義 └── terraform.tfvars # 変数値（gitignore 推奨） 3. 前提条件 # 必要な環境 # GCP プロジェクト Terraform \u0026gt;= 1.5 Google Provider \u0026gt;= 7.19 Firestore の mongodb_compatible_data_access_mode 属性は v7.19.0 で追加されました。それ以前のバージョンではこの属性が認識されず、MongoDB 互換モードを Terraform から有効にできません。 gcloud CLI # 4. 構築手順 # librechat.yaml の設定 # LibreChat の設定ファイル librechat.yaml は、GCS バケットに配置し Cloud Run の GCS FUSE マウントで /app/config/librechat.yaml として読み込みます。\nこの例では、リモート MCP Server として Langfuse のドキュメント検索サーバーを streamable-http タイプで設定しています。\n# librechat.yaml version: 1.3.6 cache: true mcpServers: langfuse-docs: type: streamable-http url: \u0026#34;https://langfuse.com/api/mcp\u0026#34; timeout: 30000 initTimeout: 10000 registration: socialLogins: [] allowedDomains: [] Terraform コード # 全リソースを Terraform で IaC 管理しています。以下の内容を main.tf として保存します。各リソースの意図はコメントで説明しています。\nmain.tf の全コード # ================================================================= # 変数定義 # ======================+========================================== variable \u0026#34;project_id\u0026#34; { description = \u0026#34;GCP プロジェクト ID\u0026#34; type = string } variable \u0026#34;region\u0026#34; { description = \u0026#34;GCP リージョン\u0026#34; type = string default = \u0026#34;asia-northeast1\u0026#34; } variable \u0026#34;environment\u0026#34; { description = \u0026#34;環境名 (dev / stg / prod)\u0026#34; type = string default = \u0026#34;dev\u0026#34; } variable \u0026#34;firestore_database_name\u0026#34; { description = \u0026#34;Firestore データベース名\u0026#34; type = string default = \u0026#34;librechat\u0026#34; } variable \u0026#34;firestore_location\u0026#34; { description = \u0026#34;Firestore ロケーション\u0026#34; type = string default = \u0026#34;asia-northeast1\u0026#34; } variable \u0026#34;librechat_cpu\u0026#34; { description = \u0026#34;Cloud Run の vCPU 数\u0026#34; type = string default = \u0026#34;2\u0026#34; } variable \u0026#34;librechat_memory\u0026#34; { description = \u0026#34;Cloud Run のメモリ\u0026#34; type = string default = \u0026#34;1Gi\u0026#34; } # ================================================================= # プロバイダー設定 # ================================================================= terraform { required_version = \u0026#34;\u0026gt;= 1.5\u0026#34; required_providers { google = { source = \u0026#34;hashicorp/google\u0026#34; version = \u0026#34;\u0026gt;= 7.19\u0026#34; } } } provider \u0026#34;google\u0026#34; { project = var.project_id region = var.region } data \u0026#34;google_project\u0026#34; \u0026#34;current\u0026#34; { project_id = var.project_id } # ================================================================= # Firestore (MongoDB 互換モード) # ================================================================= # Enterprise エディション + mongodb_compatible_data_access_mode を有効化することで、 # MongoDB プロトコルでの接続が可能になる。 # 接続文字列の詳細は後述の「Firestore MongoDB 互換モードの解説」を参照。 resource \u0026#34;google_firestore_database\u0026#34; \u0026#34;librechat\u0026#34; { project = var.project_id name = var.firestore_database_name location_id = var.firestore_location type = \u0026#34;FIRESTORE_NATIVE\u0026#34; database_edition = \u0026#34;ENTERPRISE\u0026#34; mongodb_compatible_data_access_mode = \u0026#34;DATA_ACCESS_MODE_ENABLED\u0026#34; concurrency_mode = \u0026#34;PESSIMISTIC\u0026#34; delete_protection_state = \u0026#34;DELETE_PROTECTION_DISABLED\u0026#34; depends_on = [google_project_service.firestore] } resource \u0026#34;google_project_service\u0026#34; \u0026#34;firestore\u0026#34; { project = var.project_id service = \u0026#34;firestore.googleapis.com\u0026#34; disable_on_destroy = false } # ================================================================= # Artifact Registry # ================================================================= # Cloud Run は ghcr.io から直接 pull できないため、 # Artifact Registry にリモートリポジトリを作成して中継する。 resource \u0026#34;google_artifact_registry_repository\u0026#34; \u0026#34;ghcr_remote\u0026#34; { project = var.project_id location = var.region repository_id = \u0026#34;ghcr-remote\u0026#34; format = \u0026#34;DOCKER\u0026#34; mode = \u0026#34;REMOTE_REPOSITORY\u0026#34; # NOTE: custom_repository は deprecated。 # 最新の Provider では common_repository への移行が推奨されている。 remote_repository_config { docker_repository { custom_repository { uri = \u0026#34;https://ghcr.io\u0026#34; } } } depends_on = [google_project_service.artifactregistry] } resource \u0026#34;google_project_service\u0026#34; \u0026#34;artifactregistry\u0026#34; { project = var.project_id service = \u0026#34;artifactregistry.googleapis.com\u0026#34; disable_on_destroy = false } # ================================================================= # Secret Manager # ================================================================= # LibreChat に必要なシークレット（JWT、暗号化キー等）を管理。 # Terraform ではシークレットの「箱」のみ作成する。値は apply 後に手動で設定する。 # 例: echo -n \u0026#34;your-secret-value\u0026#34; | gcloud secrets versions add dev-librechat-jwt-secret --data-file=- # 各シークレットに対して上記コマンドを実行し、適切な値を設定すること。 locals { secrets = { jwt-secret = \u0026#34;LibreChat JWT シークレット\u0026#34; jwt-refresh-secret = \u0026#34;LibreChat JWT リフレッシュシークレット\u0026#34; creds-key = \u0026#34;LibreChat 暗号化キー\u0026#34; creds-iv = \u0026#34;LibreChat 暗号化 IV\u0026#34; } } resource \u0026#34;google_secret_manager_secret\u0026#34; \u0026#34;secrets\u0026#34; { for_each = local.secrets project = var.project_id secret_id = \u0026#34;${var.environment}-librechat-${each.key}\u0026#34; replication { auto {} } depends_on = [google_project_service.secretmanager] } # Vertex AI 用 SA キーを自動生成し Secret Manager に保存 resource \u0026#34;google_service_account_key\u0026#34; \u0026#34;librechat_vertex\u0026#34; { service_account_id = google_service_account.librechat.name } resource \u0026#34;google_secret_manager_secret\u0026#34; \u0026#34;vertex_sa_key\u0026#34; { project = var.project_id secret_id = \u0026#34;${var.environment}-librechat-vertex-sa-key\u0026#34; replication { auto {} } depends_on = [google_project_service.secretmanager] } resource \u0026#34;google_secret_manager_secret_version\u0026#34; \u0026#34;vertex_sa_key\u0026#34; { secret = google_secret_manager_secret.vertex_sa_key.id secret_data = google_service_account_key.librechat_vertex.private_key } resource \u0026#34;google_project_service\u0026#34; \u0026#34;secretmanager\u0026#34; { project = var.project_id service = \u0026#34;secretmanager.googleapis.com\u0026#34; disable_on_destroy = false } # ================================================================= # Cloud Storage - librechat.yaml の配信 # ================================================================= # GCS バケットに配置し、Cloud Run の GCS FUSE マウントで読み込む。 # librechat.yaml の詳細は後述の「librechat.yaml の設定」を参照。 resource \u0026#34;google_storage_bucket\u0026#34; \u0026#34;librechat_config\u0026#34; { project = var.project_id name = \u0026#34;${var.project_id}-librechat-config\u0026#34; location = var.region force_destroy = true uniform_bucket_level_access = true } resource \u0026#34;google_storage_bucket_object\u0026#34; \u0026#34;librechat_yaml\u0026#34; { name = \u0026#34;librechat.yaml\u0026#34; bucket = google_storage_bucket.librechat_config.name source = \u0026#34;${path.module}/../librechat.yaml\u0026#34; } # ================================================================= # IAM - サービスアカウントと権限 # ================================================================= resource \u0026#34;google_service_account\u0026#34; \u0026#34;librechat\u0026#34; { project = var.project_id account_id = \u0026#34;librechat-${var.environment}\u0026#34; display_name = \u0026#34;LibreChat Cloud Run SA (${var.environment})\u0026#34; } resource \u0026#34;google_project_iam_member\u0026#34; \u0026#34;librechat_firestore\u0026#34; { project = var.project_id role = \u0026#34;roles/datastore.user\u0026#34; member = \u0026#34;serviceAccount:${google_service_account.librechat.email}\u0026#34; } resource \u0026#34;google_project_iam_member\u0026#34; \u0026#34;librechat_secretmanager\u0026#34; { project = var.project_id role = \u0026#34;roles/secretmanager.secretAccessor\u0026#34; member = \u0026#34;serviceAccount:${google_service_account.librechat.email}\u0026#34; } resource \u0026#34;google_project_iam_member\u0026#34; \u0026#34;librechat_vertexai\u0026#34; { project = var.project_id role = \u0026#34;roles/aiplatform.user\u0026#34; member = \u0026#34;serviceAccount:${google_service_account.librechat.email}\u0026#34; } resource \u0026#34;google_project_iam_member\u0026#34; \u0026#34;librechat_artifact_reader\u0026#34; { project = var.project_id role = \u0026#34;roles/artifactregistry.reader\u0026#34; member = \u0026#34;serviceAccount:${google_service_account.librechat.email}\u0026#34; } resource \u0026#34;google_storage_bucket_iam_member\u0026#34; \u0026#34;librechat_config_reader\u0026#34; { bucket = google_storage_bucket.librechat_config.name role = \u0026#34;roles/storage.objectViewer\u0026#34; member = \u0026#34;serviceAccount:${google_service_account.librechat.email}\u0026#34; } # Cloud Run サービスエージェントにも Secret Manager へのアクセス権が必要。 # コンテナ起動時にシークレットを注入するのはサービスエージェントであり、 # アプリケーションの SA とは異なる。 resource \u0026#34;google_project_iam_member\u0026#34; \u0026#34;cloudrun_agent_secretmanager\u0026#34; { project = var.project_id role = \u0026#34;roles/secretmanager.secretAccessor\u0026#34; member = \u0026#34;serviceAccount:service-${data.google_project.current.number}@serverless-robot-prod.iam.gserviceaccount.com\u0026#34; } # ================================================================= # Cloud Run - LibreChat # ================================================================= resource \u0026#34;google_cloud_run_v2_service\u0026#34; \u0026#34;librechat\u0026#34; { project = var.project_id name = \u0026#34;librechat-${var.environment}\u0026#34; location = var.region ingress = \u0026#34;INGRESS_TRAFFIC_ALL\u0026#34; deletion_protection = false template { service_account = google_service_account.librechat.email scaling { min_instance_count = 0 max_instance_count = 2 } containers { # Artifact Registry のリモートリポジトリ経由で ghcr.io のイメージを pull image = \u0026#34;${var.region}-docker.pkg.dev/${var.project_id}/ghcr-remote/danny-avila/librechat:v0.8.3\u0026#34; name = \u0026#34;librechat\u0026#34; # PORT は Cloud Run の予約済み環境変数のため設定不可。 # container_port を指定すると Cloud Run が自動的に PORT=3080 を設定する。 ports { container_port = 3080 } # GCS FUSE: バケットをディレクトリとしてマウント volume_mounts { name = \u0026#34;librechat-config\u0026#34; mount_path = \u0026#34;/app/config\u0026#34; } resources { limits = { cpu = var.librechat_cpu memory = var.librechat_memory } cpu_idle = true startup_cpu_boost = true } # ---------- 環境変数 ---------- env { name = \u0026#34;HOST\u0026#34; value = \u0026#34;0.0.0.0\u0026#34; } env { name = \u0026#34;NODE_ENV\u0026#34; value = \u0026#34;production\u0026#34; } env { name = \u0026#34;NO_INDEX\u0026#34; value = \u0026#34;true\u0026#34; } env { name = \u0026#34;CONFIG_PATH\u0026#34; value = \u0026#34;/app/config/librechat.yaml\u0026#34; } # Firestore MongoDB 互換接続 # 接続文字列の詳細は後述の「Firestore MongoDB 互換モードの解説」を参照 env { name = \u0026#34;MONGO_URI\u0026#34; value = \u0026#34;mongodb://${google_firestore_database.librechat.uid}.${var.firestore_location}.firestore.goog:443/${var.firestore_database_name}?loadBalanced=true\u0026amp;tls=true\u0026amp;retryWrites=false\u0026amp;authMechanism=MONGODB-OIDC\u0026amp;authMechanismProperties=ENVIRONMENT:gcp,TOKEN_RESOURCE:FIRESTORE\u0026#34; } # Firestore 互換との接続安定化: # autoIndex を無効にし、接続直後のインデックス一斉作成を抑制 env { name = \u0026#34;MONGO_AUTO_INDEX\u0026#34; value = \u0026#34;false\u0026#34; } env { name = \u0026#34;MONGO_AUTO_CREATE\u0026#34; value = \u0026#34;false\u0026#34; } # mongoMeili 等の非同期エラーが uncaughtException になりクラッシュするのを防止 env { name = \u0026#34;CONTINUE_ON_UNCAUGHT_EXCEPTION\u0026#34; value = \u0026#34;true\u0026#34; } # Meilisearch 無効 (mongoMeili プラグインがクラッシュの原因のため) env { name = \u0026#34;SEARCH\u0026#34; value = \u0026#34;false\u0026#34; } # Vertex AI (Gemini) - SA キーで認証 env { name = \u0026#34;GOOGLE_SERVICE_KEY_FILE\u0026#34; value_source { secret_key_ref { secret = google_secret_manager_secret.vertex_sa_key.secret_id version = \u0026#34;latest\u0026#34; } } } # global を指定。リージョン指定だと一部モデルが利用不可 env { name = \u0026#34;GOOGLE_LOC\u0026#34; value = \u0026#34;global\u0026#34; } # Secret Manager からの参照 env { name = \u0026#34;JWT_SECRET\u0026#34; value_source { secret_key_ref { secret = google_secret_manager_secret.secrets[\u0026#34;jwt-secret\u0026#34;].secret_id version = \u0026#34;latest\u0026#34; } } } env { name = \u0026#34;JWT_REFRESH_SECRET\u0026#34; value_source { secret_key_ref { secret = google_secret_manager_secret.secrets[\u0026#34;jwt-refresh-secret\u0026#34;].secret_id version = \u0026#34;latest\u0026#34; } } } env { name = \u0026#34;CREDS_KEY\u0026#34; value_source { secret_key_ref { secret = google_secret_manager_secret.secrets[\u0026#34;creds-key\u0026#34;].secret_id version = \u0026#34;latest\u0026#34; } } } env { name = \u0026#34;CREDS_IV\u0026#34; value_source { secret_key_ref { secret = google_secret_manager_secret.secrets[\u0026#34;creds-iv\u0026#34;].secret_id version = \u0026#34;latest\u0026#34; } } } env { name = \u0026#34;ALLOW_REGISTRATION\u0026#34; value = \u0026#34;true\u0026#34; } env { name = \u0026#34;SESSION_EXPIRY\u0026#34; value = \u0026#34;900000\u0026#34; # 15 min } env { name = \u0026#34;REFRESH_TOKEN_EXPIRY\u0026#34; value = \u0026#34;604800000\u0026#34; # 7 days } startup_probe { http_get { path = \u0026#34;/health\u0026#34; } initial_delay_seconds = 10 period_seconds = 10 timeout_seconds = 5 failure_threshold = 30 } liveness_probe { http_get { path = \u0026#34;/health\u0026#34; } period_seconds = 30 } } volumes { name = \u0026#34;librechat-config\u0026#34; gcs { bucket = google_storage_bucket.librechat_config.name read_only = true } } } depends_on = [ google_project_service.run, google_project_service.aiplatform, google_secret_manager_secret.secrets, google_secret_manager_secret_version.vertex_sa_key, google_storage_bucket_object.librechat_yaml, ] } resource \u0026#34;google_project_service\u0026#34; \u0026#34;run\u0026#34; { project = var.project_id service = \u0026#34;run.googleapis.com\u0026#34; disable_on_destroy = false } resource \u0026#34;google_project_service\u0026#34; \u0026#34;aiplatform\u0026#34; { project = var.project_id service = \u0026#34;aiplatform.googleapis.com\u0026#34; disable_on_destroy = false } terraform.tfvars の設定例 # project_id = \u0026#34;your-gcp-project-id\u0026#34; region = \u0026#34;asia-northeast1\u0026#34; environment = \u0026#34;dev\u0026#34; firestore_database_name = \u0026#34;librechat\u0026#34; firestore_location = \u0026#34;asia-northeast1\u0026#34; Firestore MongoDB 互換モードの解説 # Firestore MongoDB 互換モードでは、通常の MongoDB と同じプロトコルで接続しますが、接続文字列のパラメータにいくつか注意点があります。\nmongodb://\u0026lt;UID\u0026gt;.\u0026lt;LOCATION\u0026gt;.firestore.goog:443/\u0026lt;DATABASE\u0026gt; ?loadBalanced=true \u0026amp;tls=true \u0026amp;retryWrites=false \u0026amp;authMechanism=MONGODB-OIDC \u0026amp;authMechanismProperties=ENVIRONMENT:gcp,TOKEN_RESOURCE:FIRESTORE ホスト名の \u0026lt;UID\u0026gt;は Firestore データベースごとに自動生成される一意の識別子です。Terraform では google_firestore_database の uid 属性から動的に組み立てています。\n各パラメータの意味は以下の通りです。\nパラメータ 値 理由 loadBalanced true 必須。Firestore はマネージドサービスのため、ユーザーからのリクエストは Google Cloud 内部のロードバランサーを経由してバックエンドに分散されます。一方、MongoDB ドライバーは通常、接続先に対して hello コマンドを送信し、レプリカセットやスタンドアロン等のサーバー構成を自動検出（トポロジー検出）します。ロードバランサーの背後ではこの検出が正しく動作しないため、loadBalanced=true を指定してトポロジー検出をスキップさせます。 retryWrites false 必須。MongoDB ドライバーはデフォルトで書き込み失敗時に自動リトライしますが、Firestore 互換はこの機能に対応していません。デフォルト（true）のままだとエラーになります。 authMechanism MONGODB-OIDC Cloud Run のサービスアカウントの ID トークンで自動認証されます。DB のユーザー名・パスワード管理は不要です。 tls true Firestore への接続には TLS が必須です。 接続の安定化 # LibreChat は内部で Mongoose（MongoDB の ODM ライブラリ）を使用しています。Mongoose には DB 接続直後にスキーマ定義に基づいてインデックスを自動作成する機能がありますが、Firestore MongoDB 互換はレート制限が厳しく、大量のインデックス作成要求で接続が切断されます。さらに Meilisearch 連携プラグイン（mongoMeili）が非同期エラーを投げ、Node.js の未捕捉例外としてプロセスが終了してしまいます。\nTerraform コード内では以下の環境変数でこれらを抑制しています。\nMONGO_AUTO_INDEX=false : インデックス自動作成を無効化 MONGO_AUTO_CREATE=false : コレクション自動作成を無効化 CONTINUE_ON_UNCAUGHT_EXCEPTION=true : 未捕捉例外でプロセスを終了させない SEARCH=false : Meilisearch プラグインを無効化 # 5. 動作確認と制約 # 機能ごとの動作可否 # 機能 動作 備考 ユーザー登録・ログイン OK Gemini (Vertex AI) チャット OK マルチユーザー会話分離 OK ユーザー間で会話は見えない 会話共有リンク OK MCP Server 利用 OK エージェント作成・利用 OK ただしリロードで一覧から消える（後述） 管理者ダッシュボード NG $bitsAllSet 非サポート エージェント一覧（リロード後） NG 権限チェック失敗で空になる プロンプト一覧（リロード後） NG 同上 $bitsAllSet 問題 # LibreChat v0.8では ACL（Access Control List）ベースのビットマスク権限を採用しています。aclentries コレクションに以下のようなドキュメントが保存されます。\n{ principalType: \u0026#34;user\u0026#34;, principalId: \u0026#34;user-id-xxx\u0026#34;, resourceType: \u0026#34;agent\u0026#34;, resourceId: \u0026#34;agent-id-xxx\u0026#34;, permBits: 7 // 0b111 = read(1) + use(2) + edit(4) } エージェントやプロンプトの一覧を取得する際、MongoDB の $bitsAllSet 演算子でビットマスクを照合します。\n// 「このユーザーが閲覧権限を持つエージェントの一覧」 AclEntry.find({ principalId: userId, resourceType: \u0026#34;agent\u0026#34;, permBits: { $bitsAllSet: 1 } // 閲覧ビットが立っているか }).distinct(\u0026#39;resourceId\u0026#39;) Firestore MongoDB 互換は、ビット演算クエリ演算子を公式にサポートしていません。Supported features ドキュメント の Bitwise operators セクションで、以下が全て「No」と明記されています。\n演算子 サポート $bitsAllSet No $bitsAnySet No $bitsAllClear No $bitsAnyClear No この結果、以下の流れでエージェント一覧が表示されなくなります。\nユーザーがエージェント一覧をリクエスト LibreChat の PermissionService が $bitsAllSet クエリを実行 Firestore が unknown operator $bitsAllSet エラーを返す PermissionService がエラーをキャッチし、アクセス可能リソース = 空配列で返す UI にはエージェントが0件として表示される データ自体は Firestore に保存されています。リロード時の権限チェッククエリが失敗するため、作成したエージェントやプロンプトが「消えた」ように見えます。\nなお、チャット（会話）の一覧取得は PermissionService を経由せず、単純に userId でフィルタするだけなので正常に動作します。\n6. まとめ # 問題なく動作する機能 # 以下の機能は Firestore MongoDB 互換 + Cloud Run の構成で問題なく利用できます。\nユーザー登録・ログイン チャット マルチユーザーの会話分離 会話共有リンク MCP Server 連携 これらの機能のみを使う場合、Firestore MongoDB 互換 + Cloud Run の構成は十分に実用的です。MongoDB を別途構築・運用する必要がなく、フルマネージドかつサーバーレスで LibreChat を動かせます。\n動作しない機能 # ただし、以下の機能は Firestore MongoDB 互換の $bitsAllSet 演算子未サポートにより動作しません。\n管理画面（/d/admin）: 何も表示されない エージェント一覧 : 作成できるがリロードすると消える プロンプト一覧 : 同上 エージェントやプロンプトの活用、管理者による権限制御が必要な場合は、この構成では対応できません。その場合は Cloud Run + MongoDB Atlas の構成や、GCE / GKE 上に Docker で MongoDB を構築する構成を検討してください。\n今後の展望 # Firestore MongoDB 互換は現在も機能拡充が進んでおり、Supported features のページは定期的に更新されています。ビット演算クエリ演算子（$bitsAllSet 等）がサポートされた際には、追加検証を実施し別途記事を更新したいと思います。\n7. 参考リンク # LibreChat GitHub Firestore MongoDB 互換ドキュメント Firestore MongoDB 互換 Supported features Firestore MongoDB 互換 動作の違い ","date":"2026年4月9日","externalUrl":null,"permalink":"/posts/librechat-%E3%82%92-cloud-run-%E3%81%A8-firestore-mongodb-%E4%BA%92%E6%8F%9B%E3%81%A7%E6%A7%8B%E7%AF%89%E3%81%99%E3%82%8B/","section":"Posts","summary":"1. はじめに # LibreChat は、OpenAI・Google Gemini・Anthropic など複数の LLM に対応したオープンソースのチャット UI です。セルフホストすることで、組織や個人の用途に合わせた AI チャット基盤を構築できます。\n","title":"LibreChat を Cloud Run と Firestore MongoDB 互換で構築する","type":"posts"},{"content":"","date":"2026年4月9日","externalUrl":null,"permalink":"/posts/","section":"Posts","summary":"","title":"Posts","type":"posts"},{"content":"","date":"2026年4月9日","externalUrl":null,"permalink":"/tags/","section":"タグ","summary":"","title":"タグ","type":"tags"},{"content":"","date":"2026年4月6日","externalUrl":null,"permalink":"/tags/langfuse/","section":"タグ","summary":"","title":"Langfuse","type":"tags"},{"content":"","date":"2026年4月6日","externalUrl":null,"permalink":"/tags/langfuse-v4/","section":"タグ","summary":"","title":"Langfuse V4","type":"tags"},{"content":" はじめに # Langfuse ライフ、いかがお過ごしですか。\n近いうちに Langfuse が v3 から v4 にアップデートされることを、すでにご存じでしょうか。現在、Langfuse の Web UI の左下に、v4 向けプレビュー体験のトグル「Fast (Preview)」（以前は「v4 Beta」）が表示されています。これをオンにすると、「Langfuseが速くなる」という旨が書かれた確認ダイアログが出ると思います。\nFast(Preview) トグル Fast(Preview) トグル 「Langfuse v4 になったら速度が上がるだけ」と認識している方も多いのではないでしょうか。本記事では、Langfuse v4 の更新がどのような考え方で進められているか、および変更の概要を思想レベルでまとめます。\n想定読者：Langfuse v3 の画面や 旧バージョンSDK を触ったことがあるが、v4 の概要をまだ掴めていない方。\n注意 本記事で触れる v4 / Fast (Preview) の話は、現時点では主に Langfuse Cloud のプレビュー体験を念頭に置いています。UI で新しい体験に切り替えられますが、すべての画面がすでに新データモデルへ移行済みというわけではありません。OSS / セルフホスト向けの移行パスは公式が作業中と明示しており、今後正式な案内が予定されています。\nまた、新しい UI でデータをほぼリアルタイムに近いタイミングで見るには、後述のPython SDK v4、JavaScript / TypeScript SDK v5、または OpenTelemetry で x-langfuse-ingestion-version: 4 を付けることが推奨されます。それ以外の場合、新 UI では最大おおよそ 10 分ほど表示が遅れることがあります。 Langfuse v4 に向けた、根本からの考え方 # ここから先で押さえておきたいのは、v4 は速くなるだけではない、という点です。どの単位をデータの主語にするか、どう保存し・どう問い合わせるかという前提を、いまのLLMの使われ方に合わせて揃え直しています。\nエージェントや複数ステップのパイプラインが一般化するにつれ、1 回のユーザー操作に紐づく Observation の数は桁違いに増えます。Trace の中に処理がたくさん入る世界では、「Trace を開いてから中を探す」だけでは運用もクエリも窮屈になりやすい、というのが背景にあります。\nクラウド上の製品体験は公式ドキュメント Langfuse Cloud: Fast Preview (v4) で説明されています。クライアント側では、Observation 中心にしたデータモデルに合わせて Python SDK v4 と JavaScript / TypeScript SDK v5 が同じ方向に更新されています。\n1. Observations-first: まず「処理単位」からたどれるようにする # v3 までの体験に慣れていると、一覧や探索の入口が Trace に寄りがちです。中身を開いて初めて、LLM 呼び出しやツール実行といったいま調べたい処理にたどり着く、という読み方になります。\nv4 の Observations-first は、この順序をひっくり返すイメージです。「どの Trace のどこかが遅いか」ではなく、「どの LLM 呼び出しが遅いか」「どのツールが失敗しているか」のように、日常の問いの起点を Observation に置きます。エージェント的なアプリでは 1 Trace にかなりの数の処理がぶら下がり得るので、最初から処理単位で絞り込めることが重要、という整理です。\n評価では、Trace 全体だけでなく Observation ごとに LLM-as-a-judge などを回せるようになる流れも、v4 とセットで押し上げられている話題のひとつです。Observation ごとの評価については弊社ブログ Langfuse の Observation レベル評価：「どのステップが悪いのか」をスコアで特定できるようになった をご覧ください。\n画面の使い方や保存データの詳細は、公式ドキュメント Explore Observations in v4 をご覧ください。\n2. データの置き方を変えて、画面や API の表示を速くする # 結論から言うと、v4 では一覧・ダッシュボード・公開 API などが速くなる方向です。「すでに速い」という前提ではなく、これから大きなデータ量でも待ち時間を抑えやすくする、という認識が正しいです。\nLangfuse 公式ブログの Simplifying Langfuse for Scale（2026-03-10） では、Observation を中心にしたテーブル構成に寄せることで、速度改善しているという説明が丁寧にされています。大規模プロジェクトでは、ダッシュボードの読み込みが桁違いに改善した、といった記述もあります。\n具体的には、Trace 用と Observation 用だった 2 つのテーブルを、1 つにまとめています。テーブルを分けたまま結び付ける「join」負担を減らします。\nLangfuse スキーマ定義の改善(出典: Simplifying Langfuse for Scale) Langfuse スキーマ定義の改善(出典: Simplifying Langfuse for Scale) テーブルをまとめることで、代表的なクエリが数秒〜数分かかっていたものが、1 秒前後まで短くなるイメージが示されています。\nLangfuse v4高速化のイメージ Langfuse v4 高速化のイメージ(出典: Simplifying Langfuse for Scale) この「速さ」は、単一画面の実装を直しただけ、というより次の二つがそろった結果として説明しやすいです。第一に、Trace と Observation を毎回結びつけ直さずに済む仕方を土台にすること。第二に、その土台のうえで一覧・ダッシュボード・公開 API・評価などが同じ前提で動くこと。こうすると Observation に直接問いを投げる経路が増え、待ち時間を抑えやすくなる、という読み方です。\n3. Langfuse API v2 がデフォルト API に # Langfuse の公開 API には、いま主に二つの世代があります。古い形が v1、新しい形が v2 とざっくり理解できます。v4 の流れでは、Observation・Scoreまわりでこの v2 がデフォルトの API になり、SDK からも v2 を普通に呼ぶ名前がメインに揃います。v1 に相当する呼び出しは legacy 側にまとめられます。名前やパスを変えたことが目的ではなく、「いま推奨されるのは v2」という分かりやすい入口に寄せている、というイメージです。\n例えば、Observations API v2 は、返す列の絞り込みやカーソル方式のページ送りなど、v1 より大量データでも負荷と待ち時間を抑えやすい問い合わせに作り替えられています。\n4. SDK も Langfuse v4 に合わせる # Langfuse サーバー側、つまり Langfuse の本体プログラムが Observation 中心のデータモデルに寄せられた以上、アプリ側から送る SDK 側も Trace を都度まとめて更新するだけの書き方では、保存されるデータとしっくりきません。そのため Python v4 と JS/TS v5 は、言語が違ってもLangfuse v4の方針に似た仕様に揃っています。\nTrace共通属性は「Trace だけ」ではなく「子にも載せる」 # user_id / session_id / metadata / tags などは、Trace 行にだけあるのではなく、配下のObservation にも伝わるのが前提です。Python では propagate_attributes() のようなコンテキストマネージャ、JS/TS では propagateAttributes() のようなコールバックでスコープを切る メソッド として表現されます。言語が違っても、「この範囲で作られる Observation に、Trace共通属性を載せる」という意味は共通です。\nTrace への一括更新から、役割を分ける # 以前の Trace をまとめて更新するメソッド（Python の update_current_trace()、JS/TS の updateActiveTrace() など）は、いろいろな種類の情報が一塊になっていました。v4 / v5 ではこれを分け、Trace共通属性は propagate、Trace 全体の入出力や公開状態は別のメソッドにします。\n新しいコードでの推奨は、「Trace の入出力に相当する情報」を、親を持たないルートの Observation の入出力として載せることです。通常、Trace にはそうしたルートがひとつあり、そこに載せた入出力が、画面や連携でいう Trace の入出力として扱われるイメージに寄せます。Trace 専用にまとめて入出力を書き込む API は、互換のため残っていますが、主に従来の Trace 単位の LLM-as-a-judge 向けで、新規はルート Observation を使う説明になっています。\nSpanの作成を「Observation」から開始にする # 今まではSpan と Generation という名前で別々のメソッドで増やす方針でした。これからは、Observation という共通の名前で開始し、observation typeを種別を引数で表す方向です（Python の start_observation / start_as_current_observation、JS/TS の startObservation / startActiveObservation など）。クライアント側でも主語が Observation だと分かる形にしている、という理解で十分です。\nOpenTelemetry 経由の span # OpenTelemetry 経由のspanについて、以前はほぼすべての span を送る前提に近い挙動があり、HTTP や DB などインフラ寄りの span が多いとトレースがノイズだらけになりがちでした。Python v4 / JS/TS v5 では、デフォルトで LLM / GenAI に近い span を中心に送るフィルタが入り、まずは見たいものが見える方向の初期設定になっています。OpenTelemetry経由の spanの制御については、別稿の A2A × ADKの\u0026quot;観測粒度\u0026quot;を設計する - Langfuse \u0026amp; Cloud Trace でトレース構造を可視化 - でも触れています。\nSDK の破壊的変更の一覧や置き換え手順は、次を参照してください。\nPython v3 → v4 JS/TS v4 → v5 おわりに # Langfuse v4 は、速度が上がるだけの話ではありません。Observation を中心にデータの持ち方と照会の前提を組み替え、そのうえで UI・公開 API・評価が同じ土台を共有する、という横断的な更新です。Trace の中に処理が密集する使われ方に合わせ、一覧や分析が Observation から直接たどれるようになる、という流れとセットで理解すると全体像がつかみやすいです。Langfuse v4 に合わせ、SDK も Observation 中心モデルにクライアントを合わせるバージョンアップがありました。\nさらに詳しく知りたい方は、次のWebページを参照してください。\nLangfuse Cloud: Fast Preview (v4) Simplifying Langfuse for Scale（ブログ） Python v3 → v4（SDK） JS/TS v4 → v5（SDK） Observation 中心のデータモデル ","date":"2026年4月6日","externalUrl":null,"permalink":"/posts/langfuse-v4%E3%81%AF%E3%81%AA%E3%81%AB%E3%81%8C%E5%A4%89%E3%82%8F%E3%82%8B-v3%E3%81%A8%E3%81%AE%E9%81%95%E3%81%84%E3%82%92%E3%81%96%E3%81%A3%E3%81%8F%E3%82%8A%E8%A7%A3%E8%AA%AC/","section":"Posts","summary":"はじめに # Langfuse ライフ、いかがお過ごしですか。\n近いうちに Langfuse が v3 から v4 にアップデートされることを、すでにご存じでしょうか。現在、Langfuse の Web UI の左下に、v4 向けプレビュー体験のトグル「Fast (Preview)」（以前は「v4 Beta」）が表示されています。これをオンにすると、「Langfuseが速くなる」という旨が書かれた確認ダイアログが出ると思います。\n","title":"Langfuse v4はなにが変わる？ v3との違いをざっくり解説","type":"posts"},{"content":" この記事のポイント # 前編では、Langfuse v3.158.0の \u0026ldquo;fulltext search\u0026rdquo; が実装上は部分一致検索であること、そして3層の検索アーキテクチャの全体像を解説しました。\n後編では、PR #12578 のソースコードを詳しく読み、以下の2点を明らかにします。\nプロンプト編集画面のTextモードとChatモードで検索の挙動が異なること、特にChatモードではカウンターとハイライトが食い違うケースがあること サーバーサイド検索（ClickHouse / PostgreSQL）を含めた、日本語利用時の具体的な制約（Limitation） → 前編はこちら：「Langfuse v3.158.0の\u0026quot;fulltext search\u0026quot;を読み解く — その実態は部分一致検索だった」（リンク ）\nPR #12578のコードリーディング # 検索のエントリポイント：MessageSearchProvider # 検索機能のアーキテクチャは、React ContextベースのMessageSearchProviderを中心に構成されています。\nPlayground画面（web/src/features/playground/page/index.tsx）では、全ウィンドウをMessageSearchProviderで包み、各ウィンドウのpageIdを渡しています。Prompt Management側（PromptChatMessages.tsx）も同じMessageSearchProviderを使いますが、ページは1つだけです。つまり共通コンポーネントが両方の画面で再利用されています。\nCmd+F（Mac）/ Ctrl+F（Windows）のキーボードショートカットは、context.tsxのuseEffectでキャプチャされ、controller.openSearch()を呼び出します。このときcaptureRootRefでスコープを限定しているため、ページ全体のブラウザ検索を奪わず、メッセージ編集エリア内だけで機能します。\n検索ロジックの核心：controller.tsのbuildMatches # 検索の実体はweb/src/components/ChatMessages/messageSearch/controller.tsにあります。buildMatches関数が全マッチを計算する部分です。\nfunction buildMatches(state: MessageSearchState) { const searchQuery = getCommittedQuery(state); if (!searchQuery) return []; const lowerQuery = searchQuery.toLocaleLowerCase(); const allMatches: MessageSearchMatch[] = []; for (const [pageIndex, pageId] of state.pageIds.entries()) { const pageMessages = state.pageMessagesById[pageId]; if (!pageMessages) continue; for (const [messageIndex, message] of pageMessages.entries()) { const text = getMessageSearchText(message); if (!text) continue; const lowerText = text.toLocaleLowerCase(); let from = lowerText.indexOf(lowerQuery); while (from !== -1) { // ... マッチオブジェクトを構築してallMatchesに追加 from = lowerText.indexOf(lowerQuery, from + Math.max(1, lowerQuery.length)); } } } return allMatches; } 非常にシンプルです。toLocaleLowerCase()でケースフォールディングした後、indexOf()で部分文字列を探しています。150msのデバウンス（入力が一定時間途切れるまで処理の実行を遅延させる仕組み）付きで、タイピング中に検索が走りすぎないよう制御されています。\nCodeMirrorとの連携と3つのマッチングシステム # マッチ結果の表示は、controllerとCodeMirrorがそれぞれ独立に処理しています。さらにCodeMirror内部にも2つの仕組みがあり、合計3つのマッチングシステムが同時に動作しています。\nシステム 役割 正規化処理 大文字小文字 1 controller buildMatches マッチカウンター（例: 1 / 3）とナビゲーション なし（toLocaleLowerCase + indexOf） 区別しない 2 @codemirror/search 検索ハイライト（黄色） NFKD + toLowerCase 区別しない 3 highlightSelectionMatches 選択テキストの類似箇所ハイライト（青色） NFKDのみ 区別する これらの正規化処理の違いが、後述するTextモード/Chatモードでの挙動差の原因になっています。\ncontrollerの****syncActiveMatchTarget() はアクティブなマッチに対応するメッセージ行までスクロールし、selectCodeMirrorRange()でCodeMirrorのselection（カーソル選択範囲）を設定します。\napplyCodeMirrorSearchQuery() は各エディタインスタンスに対して@codemirror/searchのsetSearchQueryエフェクトを発行します。ここでliteral: trueが設定されており、検索文字列中の.や*がメタ文字として解釈されず、入力そのままの文字列として扱われます。\n日本語での検索挙動：コードから読み解く # プロンプト編集画面には2つの検索がある # プロンプト編集画面にはTextモードとChatモードがあり、それぞれ異なる検索メカニズムが動いています。\nTextモード：PromptLinkingEditor → CodeMirrorEditorがそのまま使われ、enableSearchKeymapはデフォルトのtrueです。Cmd+Fを押すとCodeMirror組み込みの検索パネル（エディタ下端に表示）が開きます。これはPR #12578 のMessageSearchToolbarとは別の検索UIです Chatモード：PromptChatMessagesがMessageSearchProviderでラップされ、各メッセージのCodeMirrorEditorにはenableSearchKeymap={false}が設定されます。Cmd+Fを押すとPR #12578 MessageSearchToolbar（メッセージ一覧上部の検索バー）が開きます この2つは前述の3つのマッチングシステムのうち、どれが有効になるかが異なります。\nTextモード：CodeMirror組み込み検索のNFKD正規化 # Textモードでは、表中の「#2 @codemirror/search」が有効になります。\n@codemirror/search v6.6.0のソースコードを読むと、SearchCursorのコンストラクタで以下の処理が行われています。\n// 常に適用されるベース正規化 const basicNormalize = x =\u0026gt; x.normalize(\u0026#34;NFKD\u0026#34;); // SearchCursorのコンストラクタ内 this.normalize = normalize ? x =\u0026gt; normalize(basicNormalize(x)) : basicNormalize; caseSensitive: falseの場合、最終的な正規化関数はx =\u0026gt; x.normalize(\u0026quot;NFKD\u0026quot;).toLowerCase()になります。\nNFKD（Normalization Form Compatibility Decomposition） はUnicodeの互換分解を行う正規化形式で、全角英数字を半角英数字に分解します。たとえばＬ（U+FF2C、全角）はL（U+004C、半角）に分解されます。\nつまりTextモードでは、全角/半角が相互にヒットします。\nクエリ「Ｌａｎｇｆｕｓｅ」→ NFKD → Langfuse → toLowerCase → langfuse ドキュメント「Langfuse」→ NFKD → Langfuse → toLowerCase → langfuse → マッチする 実際にTextモードのプロンプト編集画面で確認したところ、全角英数字で検索しても半角英数字の文字列にヒットすることが確認できました。\nChatモード：カウンターとハイライトの食い違い # Chatモード（PR #12578 MessageSearchToolbar）では、表中の「#1 controller」と「#3 highlightSelectionMatches」が動作し、「#2 @codemirror/search」は無効化されています（panelがnullのためDecoration.noneを返す）。\nこの組み合わせにより、以下のような挙動が確認されました。全角 「Ｌａｎｇｆｕｓｅ」 で検索した結果です。\nドキュメント中のテキスト マッチカウンター（#1） ハイライト（#3） Ｌａｎｇｆｕｓｅ（全角） ヒットする 青色（選択色） Langfuse（半角・先頭大文字） ヒットしない 青色（選択色） langfuse（半角・全小文字） ヒットしない なし LANGFUSE（半角・全大文字） ヒットしない なし 実際にChat編集画面で検索を実行した画面 実際にChat編集画面で検索を実行した画面\nマッチカウンターは「1 / 1」（Ｌａｎｇｆｕｓｅのみ）。しかしLangfuseにも青色のハイライトが表示されています。\nこの食い違いの原因は、#1と#3の正規化処理の違いです。\n#1 controller：toLocaleLowerCase() + indexOf()。NFKD正規化なし。全角Ｌａｎｇｆｕｓｅと半角Langfuseはコードポイントが異なるためマッチしない #3 highlightSelectionMatches：controllerがＬａｎｇｆｕｓｅの位置にselectionを設定 → SearchCursorがNFKD正規化（toLowerCaseなし）で同じ文字列を探す → ＬａｎｇｆｕｓｅはNFKD後にLangfuseになるため、ドキュメント中のLangfuseとマッチ。ただし大文字小文字を区別するためlangfuseやLANGFUSEにはマッチしない Chatモードのcontroller側の日本語特性 # toLocaleLowerCase()はECMAScript仕様上、Unicode Case Foldingに基づいて大文字/小文字変換を行いますが、全角/半角の変換（全幅変換）は行いません。日本語のひらがな・カタカナ・漢字には大文字/小文字の区別がないため、この関数は実質的にパススルーになります。\nindexOf()によるマッチングはUTF-16コードユニット単位の部分文字列比較なので、日本語の部分一致検索自体は問題なく動作します。「プロンプト管理」で検索すれば「Langfuseのプロンプト管理機能は〜」にヒットします。\nClickHouse側（Traces / Observations）の日本語対応 # packages/shared/src/server/queries/clickhouse-sql/search.tsを読むと、サーバーサイドの検索は以下のSQLで実現されています。\ninput ILIKE \u0026#39;%クエリ文字列%\u0026#39; OR output ILIKE \u0026#39;%クエリ文字列%\u0026#39; ClickHouseのILIKEはLIKEの大文字小文字非区分版で、内部的にはバイト列に対するパターンマッチとして動作します。ClickHouseのスキーマを確認すると、input/outputカラムはNullable(String) CODEC(ZSTD(3))で定義されており、tokenbf_v1やngrambf_v1などのフルテキストインデックスは設定されていません。\nもし仮にtokenbf_v1（トークンベースのbloom filter）が使われていた場合、デフォルトのトークナイザは空白・句読点で分割するため、日本語のように分かち書きしない言語では検索精度に影響が出る可能性があります。しかし、現状のILIKE \u0026lsquo;%\u0026hellip;%\u0026lsquo;によるパターンマッチであれば、たとえ日本語文字列でも部分一致検索として機能します。\nトレードオフとして、先頭ワイルドカード付きのILIKEはインデックスが効かず、該当カラムの全行スキャンが発生します。大量のtracesがあるプロジェクトでは（データが日本語でもそうでなくても）パフォーマンスへの影響があり得ることには注意が必要です。\nPostgreSQL側（Prompt一覧）の日本語対応 # web/src/features/prompts/server/routers/promptRouter.tsを見ると、プロンプト本文検索は以下のPrisma SQLで行われています。\np.prompt::text ILIKE \u0026#39;%クエリ文字列%\u0026#39; promptカラムはJSON型で、::textでテキストにキャストした上でILIKEを適用しています。PostgreSQLのILIKEはロケール依存のケースフォールディングを行うため、日本語文字列に対しても部分一致検索が機能します。ClickHouseと同様、GINインデックスなどは設定されていないため全行スキャンとなりますが、Langfuseの使い方として「プロンプトを大量に登録する」ことはあまりmajorな使い方ではないと思われるため、この点が問題になる可能性はあまり高くないかもしれません。\nまとめ：現行の制約（Limitation）一覧 # Langfuseの検索を日本語で使う際に把握しておきたい制約を一覧にまとめます。\n検索の基本特性 # 「文字列を探す検索」である：ステミング（語形変化の吸収）、同義語展開、関連度スコアリングといった、狭義の全文検索が提供する機能はない 形態素レベルの検索はできない：「走る」で「走った」「走り」はヒットしない ひらがな/カタカナの同一視はされない：「ぷろんぷと」で「プロンプト」はヒットしない TextモードとChatモードの差異 # 全角/半角の扱いが異なる：Textモード（CodeMirror組み込み検索）ではNFKD正規化により全角/半角が相互にヒットするが、Chatモード（PR #12578のcontroller）ではヒットしない Chatモードではカウンターとハイライトが食い違うケースがある：controllerのマッチカウンターと、CodeMirrorのhighlightSelectionMatchesで正規化処理が異なるため サーバーサイド検索 # インデックスなしの全行スキャン：ClickHouse/PostgreSQLともにILIKEパターンマッチでインデックスが効かないため、データ量が増えた場合のパフォーマンスは注視が必要 API経由の検索はまだ非対応：GitHub Discussion #12373でPublic API経由の検索のリクエストが上がっており、近々対応される可能性はある 総括 # 今回のPR #12578は 、Playgroundという「手元で試す」場面に対して、シンプルかつ確実に動く検索を提供しています。日本語ユーザーとしては、今のところ「普通に使えてありがたい」というのが正直な感想ですが、今回の記事中で指摘した不整合は気になるため、Langfuseコミュニティにfeedbackしておこうと思います（＆feedbackが反映されたらまた記事として触れさせて頂くかもしれません！）。\n","date":"2026年4月3日","externalUrl":null,"permalink":"/posts/%E5%BE%8C%E7%B7%A8-%E3%82%BD%E3%83%BC%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89%E3%81%8B%E3%82%89%E8%AA%AD%E3%81%BF%E8%A7%A3%E3%81%8Flangfuse%E6%A4%9C%E7%B4%A2%E3%81%AE%E6%8C%99%E5%8B%95%E3%81%A8%E5%88%B6%E7%B4%84/","section":"Posts","summary":"この記事のポイント # 前編では、Langfuse v3.158.0の “fulltext search” が実装上は部分一致検索であること、そして3層の検索アーキテクチャの全体像を解説しました。\n","title":"【後編】ソースコードから読み解くLangfuse検索の挙動と制約","type":"posts"},{"content":" はじめに # Langfuse v3.158.0（2026年3月13日リリース）で、PlaygroundおよびPrompt Managementのチャットメッセージ編集画面にテキスト検索機能が追加されました。ChangelogやPRタイトルでは \u0026ldquo;fulltext search\u0026rdquo;（全文検索）と表現されています。\n日本語環境でLangfuseを使っている身としては、「全文検索」と聞くと気になるのが日本語の対応状況です。そこで今回、ソースコードを読んで実装の中身を調べてみました。\nこの前編では、まず新機能の概要とLangfuseの検索アーキテクチャの全体像を整理し、今回Langfuseに実装された検索機能が、技術的にはどういう仕組みなのかを明らかにします。後編では、ソースコードをさらに深く読み、日本語で使った場合の具体的な挙動や制約について掘り下げます。\n今回の新機能：Playground / Promptsのメッセージウィンドウ内検索 # 今回追加されたのは、Cmd+F（Mac）/ Ctrl+F（Windows）でPlaygroundやプロンプト編集画面内に検索バーが表示され、複数のメッセージウィンドウにまたがってテキストを検索できる機能です（PR #12578 by @nimarb）。マッチ箇所はハイライトされ、前後のマッチへの移動も可能です。プロンプトの編集・比較作業中に、特定のフレーズや変数名を素早く見つけたい場面で役立ちます。\n実際のプロンプト編集画面における検索の様子 実際のプロンプト編集画面における検索の様子\nLangfuseにはすでに2つの検索機能が存在していました。2025年5月に導入されたTraces / Observationsのinput/output検索（サーバーサイドでClickHouseに問い合わせる方式）と、2025年7月に導入されたPrompt Management一覧でのプロンプト名・本文検索（PostgreSQLに問い合わせる方式）です。今回のPR #12578は 、これらに続く3つ目の検索機能として、クライアントサイド（ブラウザ内）で完結する形で追加されました。\n\u0026ldquo;fulltext search\u0026quot;の中身を見てみよう # PRタイトルやChangelogではこの機能を \u0026ldquo;fulltext search\u0026rdquo; と表現していますが、この用語は文脈によって指す範囲がかなり広いです。ElasticsearchやPostgreSQLのtsvector/tsqueryのようにトークナイズと転置インデックスを伴う狭義の全文検索を指すこともあれば、単にテキスト本文を対象にした検索という広い意味で使われることもあります。\n実際にソースコードを読んでみると、Langfuseの実装はJavaScriptのindexOf()やSQLのILIKE \u0026lsquo;%\u0026hellip;%\u0026lsquo;による部分一致検索（substring search） でした。転置インデックスやトークナイザは使われていません。\nこの違いは、日本語での検索挙動を考える上で重要な意味を持ちます。英語のような言語では、本来はトークンベースの全文検索のほうがステミング（語形変化の吸収）や同義語展開などの豊かな検索体験を実現しやすいです。一方、日本語のように分かち書きしない言語ではトークナイズ自体が難しいという問題があります。Langfuseの実装は言語を理解する全文検索ではなく部分一致検索であるため、英語でも高度な検索機能は提供されないものの、日本語でも無難に動くという状況になっています。\n以降、技術的な区別を明確にするため、Langfuseの検索機能は実装に即して「部分一致検索」と表記します。\nLangfuseの検索アーキテクチャは3層構造 # 前述のとおり、Langfuseの検索機能は3つのレイヤーに分かれています。それぞれの仕組みをもう少し詳しく見てみましょう。\nレイヤー1：クライアントサイド検索（今回のPR #12578） # Playground / Prompt Managementのメッセージ編集画面で動作します。完全にブラウザ内で完結し、サーバーへのリクエストは発生しません。\nレイヤー2：ClickHouseによるTraces / Observations検索 # 2025年5月のLaunch Week 3で導入された、traces/observationsのinput/outputに対する検索です。v3で導入されたClickHouseバックエンドに対してILIKEクエリが発行されます。\nレイヤー3：PostgreSQLによるPrompt一覧検索 # 2025年7月に追加されたPrompt Management一覧のプロンプト本文検索です。こちらはPrisma経由でPostgreSQLにILIKEが発行されます。\nこの3つは見た目上は同じ「検索バー」ですが、裏側の仕組みはそれぞれ異なります。一方ですべてのレイヤーが「部分一致検索」というアプローチで共通しています。トークナイザやn-gramインデックスといった狭義の全文検索の仕組みは使われていません。\nコラム：全文検索と部分一致検索、そして日本語 「全文検索（Full-Text Search）」は技術的に厳密な意味では、文書をトークン（単語や部分文字列）に分割し、転置インデックスを構築した上で高速にキーワードを引く検索手法を指します。ElasticsearchやPostgreSQLのtsvector/tsqueryなどが代表的な実装です。対して、SQLのLIKE \u0026lsquo;%keyword%\u0026lsquo;やJavaScriptのindexOf()は、テキストを先頭から順に走査する部分一致検索です。字面として完全一致しているものしか検索できず、語形変化や同義語の吸収は行われません。\nこの区別が特に重要になるのが日本語です。狭義の全文検索で必須となるトークナイズは、英語のようにスペースで単語が区切られる言語では単純ですが、日本語は分かち書きをしないため、形態素解析（MeCab等）やn-gramといった特別な処理が必要になります。これらにはそれぞれ辞書依存性や偽陽性（「京都」で「東京都」がヒットする、いわゆる「京都東京都問題」）といった課題があります。\n部分一致検索はトークナイズを経由しないため、「日本語がうまくトークンに分割できず検索自体が機能しない」という問題は発生しません。ただし万能ではなく、偽陽性の問題はn-gramと同様に存在しますし、活用形の吸収や表記揺れの統合といった形態素レベルの正規化も行われません。\n前編のまとめ：現行の検索をどう理解して使うか # 現時点のLangfuseの検索は、「語を理解する検索」ではなく「文字列を探す検索」です。入力した文字列がそのまま含まれているかどうかを探す仕組みなので、日本語でも基本的な用途では問題なく使えます。\nただし、ソースコードを詳しく読むと、いくつかの気になる挙動も見えてきました。後編では、PR #12578 のコードリーディングを行い、TextモードとChatモードの検索挙動の違い、3つのマッチングシステムの不整合、サーバーサイド検索の日本語対応について掘り下げます。\n→ 後編はこちら：「ソースコードから読み解くLangfuse検索の挙動と制約」（リンク ）\n","date":"2026年4月3日","externalUrl":null,"permalink":"/posts/%E5%89%8D%E7%B7%A8-langfuse-v3-158-0%E3%81%AE-fulltext-search-%E3%82%92%E8%AA%AD%E3%81%BF%E8%A7%A3%E3%81%8F-%E3%81%9D%E3%81%AE%E5%AE%9F%E6%85%8B%E3%81%AF%E9%83%A8%E5%88%86%E4%B8%80%E8%87%B4%E6%A4%9C%E7%B4%A2%E3%81%A0%E3%81%A3%E3%81%9F/","section":"Posts","summary":"はじめに # Langfuse v3.158.0（2026年3月13日リリース）で、PlaygroundおよびPrompt Managementのチャットメッセージ編集画面にテキスト検索機能が追加されました。ChangelogやPRタイトルでは “fulltext search”（全文検索）と表現されています。\n","title":"【前編】Langfuse v3.158.0の\"fulltext search\"を読み解く — その実態は部分一致検索だった","type":"posts"},{"content":"","date":"2026年3月31日","externalUrl":null,"permalink":"/categories/langfuse/","section":"カテゴリ","summary":"","title":"Langfuse","type":"categories"},{"content":"","date":"2026年3月31日","externalUrl":null,"permalink":"/categories/langfuse-enterprise/","section":"カテゴリ","summary":"","title":"Langfuse Enterprise","type":"categories"},{"content":" 本記事でわかること # Langfuseにおける「管理者によるトレース閲覧の検知」というニッチだが重要な課題に対して、実機検証ベースで現状の選択肢を整理します。\nLangfuse環境で「管理者の会話ログ閲覧」をどう検知するか LANGFUSE_ADMIN_ACCESS_WEBHOOK の動作仕様（実機確認済み） この機能の限界と「本番で使えるか」の判断基準 対象読者 # Self-hosted Langfuseを複数チーム・複数顧客で共用している方 LLMアプリの運用でコンプライアンス・セキュリティを気にしている方 Langfuseの特権アクセス管理に興味がある方 課題：LLMアプリの会話ログは機密データである # LLMアプリを組織で運用していると、避けて通れない問題があります。\nユーザーの会話ログは、多くの場合、機密性の高いデータです。\nカスタマーサポートの問い合わせ内容、社内文書への質問、医療・法律領域のやりとり——これらはすべてLangfuseのTrace（トレース）として記録されます。プロンプトの改善やデバッグのために記録することは正しい運用ですが、「誰がそのトレースを見られるか」は別の問題です。\nSelf-hosted Langfuseを運用している場合、インスタンス全体の管理者は、すべての組織・プロジェクトのトレースに原理上アクセス可能です。\nこの権限は運用上の必要性から避けられませんが、同時に問題でもあります。\nusers.admin は、Org / Project の RBAC ロール（Owner・Admin・Member等）とは別の、インスタンス全体に作用する内部的な管理者フラグです。公式に「Server Admin」というロール名が定義されているわけではないため、本記事では便宜上このフラグを持つユーザーを \u0026ldquo;Server Admin\u0026rdquo; と呼びます。 こうした課題を踏まえ、Langfuseには Enterprise 向けの公式 Audit Logs があり、TraceやSessionを含む多くのリソースに対する作成・更新・削除といった変更系の操作が記録されます。ただし、記録対象はあくまで変更系アクションに限られており、管理者がトレースを「閲覧した」こと自体は記録されません。この「閲覧ログが標準では残らない」という隙間を埋める選択肢として、Langfuse v3.155.1 から追加された LANGFUSE_ADMIN_ACCESS_WEBHOOK を、Self-hosted環境で実機検証しました。\nAdmin Access Webhook とは # LANGFUSE_ADMIN_ACCESS_WEBHOOK は、環境変数に通知先URLを設定するだけで、Server AdminがLangfuse上の操作を行った際にPOSTリクエストを送信する仕組みです。\nLANGFUSE_ADMIN_ACCESS_WEBHOOK=https://your-endpoint.example.com/admin-webhook 設定はこれだけです。\nただし、この機能には重要な前提があります。このWebhookが対象とするのはServer Adminのみです。 通常のOrg Adminがプロジェクトにアクセスしても通知は届きません。\nLangfuseにおける「admin」の2種類 # 種類 役割 Server Admin 全組織・全プロジェクトにアクセス可能 Org Admin / Owner 組織内のロール（OWNER・ADMIN等）。通常の管理者権限 なお、Server Adminへの昇格方法についてもソースコードで調査しました。LANGFUSE_INIT_USER_* 環境変数・Instance Management API・tRPC・seedスクリプトのいずれにも admin=true をセットする手段は存在せず、DBを直接更新する（UPDATE users SET admin = true WHERE email = '...'）以外の方法は確認できませんでした。 この仕様からも、本機能が現時点ではSelf-hostedユーザー向けに整備されたものではない可能性を示唆しています。\n実際にどんな操作で通知が来るか # 実機で確認した結果をまとめます。\n発火するケース # AdminがUIからトレースページを開く（2件届く） AdminがURLを直打ちして他チームのプロジェクトにアクセス AdminがcurlでtRPCエンドポイントを叩く Adminが自分の担当プロジェクトを開く 発火しないケース # Adminがブラウザで他組織の設定ページを開こうとする（UIがクライアント側でブロック） 通常のOrg AdminやMemberが操作する 重要な点として、**「所属外リソースへのアクセスのみ通知する」ではありません。**発火条件は admin === true のユーザーがサーバーサイドの処理を通過した時点です。管理者が自分の担当プロジェクトを開いても通知が届きます。\nまた、トレース詳細ページを開くと通知が2件届きます（内部で2つのtRPCミドルウェアが並行呼び出しされるため）。\n通知の内容（ペイロード） # 届くJSONはシンプルです。\n{ \u0026#34;email\u0026#34;: \u0026#34;admin@example.com\u0026#34;, \u0026#34;timestamp\u0026#34;: \u0026#34;2026-03-04T00:17:03.152Z\u0026#34;, \u0026#34;project\u0026#34;: \u0026#34;cm00000000000000000000proj\u0026#34;, \u0026#34;org\u0026#34;: \u0026#34;cm00000000000000000000org0\u0026#34;, \u0026#34;region\u0026#34;: \u0026#34;self-hosted\u0026#34; } 実機確認：対象ページを開いた直後に届いたリクエスト フィールド 内容 備考 email 操作したAdminのメールアドレス 常に含まれる timestamp アクセス日時（ISO 8601） 常に含まれる project プロジェクトID orgレベルのアクセスでは null org 組織ID トレース・セッションアクセス時は null region 環境識別子 Self-hostedは \u0026quot;self-hosted\u0026quot; 固定 含まれないもの： traceId・sessionId・userId・操作の種別\n「何を見られたか」の特定はできません。「いつ、誰が、どのプロジェクトに触れたか」までです。\n使いどころと限界 # 現実的に使える場面 # ① リアルタイム監視\n管理者がトレースを閲覧したタイミングをリアルタイムで検知する。内部統制の「抑止」として機能します。\n② 監査ログの最低限の確保\nemail + timestamp + project/org の組み合わせをSIEMやログ基盤に転送するだけで、特権アクセスの証跡になります。\n③ APIレベルの操作の検知\nUIでは他組織のページを直接開けませんが、APIを直接叩くと通知が届きます。UIのガードをすり抜けた操作の検知に役立ちます。\n現状の制限 # 受信側で認証できない 通知に署名がない。エンドポイントURLが漏れると誰でも偽リクエストを送れる 「何を見たか」がわからない traceIdやsessionIdが含まれず、詳細な監査には不十分 ノイズが多い 管理者が自分の担当プロジェクトを開くだけでも通知が来る スケール環境で重複が出る Langfuse側に60秒以内の同一キー再送を抑制するdedup処理があるが、プロセスのメモリ上のみで動作する。複数インスタンスをまたいだ重複抑制が機能しない ドキュメント未記載 少なくとも2026年3月時点の公開ドキュメント上では、この環境変数の説明を見つけることができませんでした。 特に気になる点を一つ挙げると、**同じLangfuseのプロンプト向けWebhookにはHMAC-SHA256署名があるのに、セキュリティ監査用途であるこの機能には署名がありません。**設計の一貫性という観点では疑問が残ります。\nこの機能は誰のためのものか（仮説） # region フィールドが NEXT_PUBLIC_LANGFUSE_CLOUD_REGION を参照し、Self-hostedは \u0026quot;self-hosted\u0026quot; のフォールバック ドキュメント未記載・chore: 扱い・レビューなし当日マージ Server Admin自体、DB直接更新以外に設定方法がない（環境変数・API・UIのいずれにも手段が存在しないことをソースコードで確認） これらを総合すると、本機能はLangfuse Cloud内部チームが自社のAdmin操作を監視するために作ったツールである可能性が高く、現時点ではSelf-hosted向けの公式サポートは明確ではありません。\nただし、self-hosted という値が明示的にコーディングされている点は、Self-hosted向けの利用も意識していたとも解釈できます。\nまとめ # Langfuse環境で「管理者が会話ログを閲覧したことを記録する」というニーズ自体は本質的な課題です。そのために LANGFUSE_ADMIN_ACCESS_WEBHOOK を活用できる場面はあります。その際、Langfuse自体の監査ログ機能やアクセス制御と組み合わせ、補完的な用途として位置づけるのが適切です。\n一方で、認証ヘッダーの不在や詳細なペイロードの不足、公開ドキュメントでの言及がない点などを踏まえると、現時点では**「特定のユースケースを補完するための、発展途上の機能」**と捉えるのが自然でしょう。\n大規模な組織であればより厳密な監査ログ基盤が必要になりますが、小規模なSelf-hosted環境における内部統制の第一歩としては、このWebhookを活用する価値は十分にあります。現状の仕様（限界）を正しく把握した上で、補助的なセキュリティ策として導入を検討するのが現実的です。\n参考リンク # Langfuse GitHub — v3.155.1 Release Langfuse Self-Hosting ドキュメント Langfuse Prompt Webhooks（HMAC署名あり） ","date":"2026年3月31日","externalUrl":null,"permalink":"/posts/langfuse-admin-access-webhook/","section":"Posts","summary":"本記事でわかること # Langfuseにおける「管理者によるトレース閲覧の検知」というニッチだが重要な課題に対して、実機検証ベースで現状の選択肢を整理します。\n","title":"Langfuseで管理者アクセスを監視する：Admin Access Webhookの実態と使いどころ","type":"posts"},{"content":"","date":"2026年3月31日","externalUrl":null,"permalink":"/categories/llmops/","section":"カテゴリ","summary":"","title":"LLMOps","type":"categories"},{"content":"","date":"2026年3月31日","externalUrl":null,"permalink":"/tags/self-hosted/","section":"タグ","summary":"","title":"Self-Hosted","type":"tags"},{"content":"","date":"2026年3月31日","externalUrl":null,"permalink":"/tags/webhook/","section":"タグ","summary":"","title":"Webhook","type":"tags"},{"content":"","date":"2026年3月31日","externalUrl":null,"permalink":"/categories/","section":"カテゴリ","summary":"","title":"カテゴリ","type":"categories"},{"content":"","date":"2026年3月31日","externalUrl":null,"permalink":"/tags/%E3%82%BB%E3%82%AD%E3%83%A5%E3%83%AA%E3%83%86%E3%82%A3/","section":"タグ","summary":"","title":"セキュリティ","type":"tags"},{"content":"","date":"2026年3月31日","externalUrl":null,"permalink":"/tags/%E7%9B%A3%E6%9F%BB%E3%83%AD%E3%82%B0/","section":"タグ","summary":"","title":"監査ログ","type":"tags"},{"content":"こんにちは。ガオ株式会社の黒澤です。この記事では、Langfuseでトレースに非公開な画像を表示する場合に、Google Cloud Storage（以下、GCS）を用いた場合のアーキテクチャパターンについて、実装を踏まえてご紹介します。\n執筆時点の情報（2026年3月） 本記事は Langfuse v3.157.0 をセルフホストした環境での検証をもとにしています。将来のバージョンでよりシンプルな方法が提供される可能性があります。\n想定読者 # Langfuse を GCP 上でセルフホストしている 画像を入力とする LLM（Gemini、GPT-4o など）を使っており、入力画像をトレースで確認したい 画像は GCS のプライベートバケットに保存している Langfuse SaaS 版をお使いの方へ SaaS 版でも LangfuseMedia（Langfuse 管理ストレージ）や External Media URL は利用できます。ただし LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT などのサーバー側環境変数を変更できないため、「自前の GCS バケットを LangfuseMedia のアップロード先に指定する」構成はセルフホスティングが前提です。\n目的 # 画像を入力とする LLM を本番で使うとき、入力画像がトレースで確認できるかは LLMOps の基本です。モデルの挙動をデバッグするにも、品質評価をするにも、「そのとき何の画像を渡したか」を確認できることが重要です。\nしかし GCS のプライベートバケットに保存した画像を Langfuse で表示しようとすると、いくつかの注意点があります。本記事では、実際の検証で遭遇した注意点と、要件に応じた解決策を整理します。\n方式 A：External Media URL（自前でアップロード済みの場合） # アプリ側で GCS に画像をアップロードし、その URL を Langfuse に渡します。\nwith langfuse.start_as_current_observation( as_type=\u0026#34;generation\u0026#34;, name=\u0026#34;analyze-image\u0026#34;, input={\u0026#34;messages\u0026#34;: [{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: [ {\u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;この画像を分析してください\u0026#34;}, {\u0026#34;type\u0026#34;: \u0026#34;image_url\u0026#34;, \u0026#34;image_url\u0026#34;: {\u0026#34;url\u0026#34;: \u0026#34;https://...画像の URL...\u0026#34;}}, ]}]}, model=\u0026#34;gemini-2.5-pro\u0026#34;, ) as gen: result = call_vision_model(...) gen.update(output=result) Langfuse はこの URL をそのままブラウザに渡します。**自動インライン表示されず「Load Image」ボタンが表示されます。**クリックすると別タブで画像が開きます。\nLangfuse UI に「Load Image」ボタンが表示される なぜ自動表示されないのか？ 外部の信頼できない URL を自動で読み込むセキュリティリスクを避けるため、意図的にこの挙動になっています（langfuse/langfuse#5030）。 自動レンダリングを設定可能にする Feature Request（#5142）はありますが、現時点では未実装です。 注意点：ブラウザ認証なしの URL では 403 になる # storage.googleapis.comを渡すと、「Load Image」をクリックしても 403 になります。\n① UI に「Load Image」ボタンが表示される ② クリック → ブラウザが storage.googleapis.com に直接 GET ③ 403 \u0026#34;Anonymous caller does not have storage.objects.get access\u0026#34; 403 エラー：Anonymous caller does not have storage.objects.get access Google アカウントでログイン済みでも、storage.googleapis.com はブラウザのログインセッションを使いません。実は GCS のプライベートオブジェクトには認証の挙動が異なる 2 つの URL があります。\nブラウザ認証なし ブラウザ認証あり ドメイン storage.googleapis.com storage.cloud.google.com 認証 なし（IAM で許可されていないと 403） ブラウザの Google ログインセッション プライベートバケット ❌ 403 になる ✅ IAM 権限があれば表示される 解決策：ブラウザ認証ありの URL を使う # storage.googleapis.com の代わりに storage.cloud.google.com を使います。\n# ❌ これは 403 になる # url = \u0026#34;https://storage.googleapis.com/your-bucket/path/to/image.png\u0026#34; # ✅ これなら表示される url = \u0026#34;https://storage.cloud.google.com/your-bucket/path/to/image.png\u0026#34; 実装例：\nBUCKET_NAME = \u0026#34;your-gcs-bucket\u0026#34; def gcs_uri_to_authenticated_url(gcs_uri: str) -\u0026gt; str: \u0026#34;\u0026#34;\u0026#34;gs://bucket/key → https://storage.cloud.google.com/bucket/key\u0026#34;\u0026#34;\u0026#34; path = gcs_uri.removeprefix(\u0026#34;gs://\u0026#34;) return f\u0026#34;https://storage.cloud.google.com/{path}\u0026#34; authenticated_url = gcs_uri_to_authenticated_url(f\u0026#34;gs://{BUCKET_NAME}/uploads/image.png\u0026#34;) with langfuse.start_as_current_observation( as_type=\u0026#34;generation\u0026#34;, name=\u0026#34;analyze-image\u0026#34;, input={\u0026#34;messages\u0026#34;: [{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: [ {\u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;この画像を分析してください\u0026#34;}, {\u0026#34;type\u0026#34;: \u0026#34;image_url\u0026#34;, \u0026#34;image_url\u0026#34;: {\u0026#34;url\u0026#34;: authenticated_url}}, ]}]}, model=\u0026#34;gemini-2.5-pro\u0026#34;, ) as gen: gen.update(output={\u0026#34;result\u0026#34;: \u0026#34;...\u0026#34;}) 「Load Image」をクリックすると、ブラウザが Google アカウントのログインセッションを使って GCS にアクセスし、別タブで画像が表示されます。\n「Load Image」クリック後、別タブに画像が表示された 条件 # Langfuse ユーザーが Google アカウントでブラウザにログインしていること そのアカウントに GCS バケットの storage.objectViewer（または同等の権限） が付与されていること 社内チームで Langfuse を使っている場合、チームメンバーは通常 GCP プロジェクトへのアクセス権を持っているため、この条件を満たしているケースが多いです。\n制約 # 「Load Image」ボタンを 1 クリックする必要があります（自動インライン表示ではない） URL にバケット名やオブジェクトパスが含まれるため、URL 自体を隠したいケースには不向きです GCS の IAM をユーザーに付与できない場合（外部パートナーなど）は後述のプロキシ構成を検討してください 方式 B：LangfuseMedia（Langfuse にアップロードを任せる場合） # LangfuseMedia を使うと、SDK が自動でアップロードし、Langfuse UI では自動インライン表示されます。\nfrom langfuse.media import LangfuseMedia with open(\u0026#34;image.png\u0026#34;, \u0026#34;rb\u0026#34;) as f: media = LangfuseMedia(content_bytes=f.read(), content_type=\u0026#34;image/png\u0026#34;) with langfuse.start_as_current_observation( as_type=\u0026#34;generation\u0026#34;, name=\u0026#34;analyze-image\u0026#34;, input={\u0026#34;messages\u0026#34;: [{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: [ {\u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;この画像を分析してください\u0026#34;}, {\u0026#34;type\u0026#34;: \u0026#34;image_url\u0026#34;, \u0026#34;image_url\u0026#34;: {\u0026#34;url\u0026#34;: media}}, ]}]}, model=\u0026#34;gemini-2.5-pro\u0026#34;, ) as gen: result = call_vision_model(...) gen.update(output=result) LangfuseMedia によるインライン表示 設定 # Langfuse サーバー（docker-compose）に GCS の設定を追加します。\n# docker-compose.yml environment: LANGFUSE_S3_MEDIA_UPLOAD_BUCKET: your-gcs-bucket LANGFUSE_S3_MEDIA_UPLOAD_REGION: auto LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID: \u0026lt;GCS HMAC Access Key\u0026gt; LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY: \u0026lt;GCS HMAC Secret Key\u0026gt; LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: https://storage.googleapis.com # GCS の S3 互換エンドポイント LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE: \u0026#34;true\u0026#34; AWS_REQUEST_CHECKSUM_CALCULATION: when_required `LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT` について AWS SDK はデフォルトで AWS S3 に接続します。GCS を使う場合は `https://storage.googleapis.com` を明示的に指定する必要があります。 `AWS_REQUEST_CHECKSUM_CALCULATION` について Langfuse の AWS SDK v3 はデフォルトで `x-amz-checksum-sha256` ヘッダーを付与しますが、GCS の S3 互換 API はこれを認識せず 400 エラーになります。`when_required` に設定することで回避できます。 注意点：署名付き URL がブラウザに露出する # この構成では、Langfuse サーバーが 署名付き URL（presigned URL） を生成してブラウザに渡します。署名付き URL は有効期限内であれば認証なしでアクセスできます。\n署名付き URL で要件を満たせるかどうかは、扱うデータの性質や組織のポリシーといったセキュリティ要件によります。URL 露出が NG な場合は後述のプロキシ構成を検討してください。\n注意点：データ量に応じてコストが増加する # LangfuseMedia では SDK が画像を GCS に PUT するため、画像の枚数やサイズに応じて GCS のストレージ費用とオペレーション費用が発生します。方式 A（認証済み URL）は既に GCS にある画像を参照するだけなので、追加のストレージ費用はかかりません（GETオペレーション費用は発生しますが、10,000リクエストあたり数円程度です）。大量の画像を扱う場合はコストを考慮してください。\nURL 露出 NG / IAM を渡せない場合：Cloud Run プロキシ構成 # 以下のいずれかに該当する場合は、Cloud Run プロキシを経由させます。方式 A・B どちらにも対応できます。\n有効期限付きであっても URL が外部に漏れてほしくない Langfuse ユーザーに GCS の IAM 権限を付与できない（外部パートナー、委託先など） 画像へのアクセスを特定の IP アドレスに制限したい アーキテクチャ # ブラウザ → Cloud LB → Cloud Armor（IP制限） → Cloud Run プロキシ → GCS ▼ アーキテクチャ図\nポイント：\nGCS の URL はブラウザに一切渡らない Cloud ArmorでIP制限を行う 補足：IP制限ではなくIAMで制限を行いたい場合は、Cloud RunのIAM制限を行う。この場合は、Cloud LBおよびCloud Armorは不要。また、IAM制限とIP制限との併用も可能（Cloud RunのIAM制限は本記事では解説対象外です）。\nプロキシ（Cloud Run） # ソースコード\n# main.py import os from fastapi import FastAPI, HTTPException, Request from fastapi.responses import Response from google.cloud import storage app = FastAPI() client = storage.Client() ALLOWED_BUCKETS = set(os.environ.get(\u0026#34;ALLOWED_BUCKETS\u0026#34;, \u0026#34;\u0026#34;).split(\u0026#34;,\u0026#34;)) ALLOWED_PREFIX = os.environ.get(\u0026#34;ALLOWED_PREFIX\u0026#34;, \u0026#34;media/\u0026#34;) MAX_UPLOAD_SIZE = int(os.environ.get(\u0026#34;MAX_UPLOAD_SIZE\u0026#34;, 10 * 1024 * 1024)) # デフォルト 10MB def _validate(bucket_name: str, object_key: str): if bucket_name not in ALLOWED_BUCKETS: raise HTTPException(status_code=403, detail=\u0026#34;Bucket not allowed\u0026#34;) if not object_key.startswith(ALLOWED_PREFIX): raise HTTPException(status_code=403, detail=\u0026#34;Path not allowed\u0026#34;) @app.get(\u0026#34;/{bucket_name}/{object_key:path}\u0026#34;) def get_object(bucket_name: str, object_key: str): \u0026#34;\u0026#34;\u0026#34;ブラウザからの画像取得リクエストを GCS に転送する\u0026#34;\u0026#34;\u0026#34; _validate(bucket_name, object_key) bucket = client.bucket(bucket_name) blob = bucket.blob(object_key) if not blob.exists(): raise HTTPException(status_code=404, detail=\u0026#34;Not found\u0026#34;) content = blob.download_as_bytes() return Response(content=content, media_type=blob.content_type or \u0026#34;application/octet-stream\u0026#34;) @app.put(\u0026#34;/{bucket_name}/{object_key:path}\u0026#34;) async def put_object(bucket_name: str, object_key: str, request: Request): \u0026#34;\u0026#34;\u0026#34;LangfuseMedia からのアップロードリクエストを GCS に転送する\u0026#34;\u0026#34;\u0026#34; _validate(bucket_name, object_key) content_length = int(request.headers.get(\u0026#34;content-length\u0026#34;, 0)) if content_length \u0026gt; MAX_UPLOAD_SIZE: raise HTTPException(status_code=413, detail=\u0026#34;File too large\u0026#34;) content = await request.body() if len(content) \u0026gt; MAX_UPLOAD_SIZE: raise HTTPException(status_code=413, detail=\u0026#34;File too large\u0026#34;) content_type = request.headers.get(\u0026#34;content-type\u0026#34;, \u0026#34;application/octet-stream\u0026#34;) bucket = client.bucket(bucket_name) blob = bucket.blob(object_key) blob.upload_from_string(content, content_type=content_type) return Response(status_code=200) デプロイ用ファイル\n# requirements.txt fastapi uvicorn google-cloud-storage 補足：`--source` デプロイでは Buildpacks が `main.py` と `requirements.txt` を自動検出するため、Dockerfile は不要です。 デプロイ\n```bash export PROJECT_ID=your-project export BUCKET_NAME=your-gcs-bucket # サービスアカウント作成・権限付与 gcloud iam service-accounts create langfuse-media-proxy-sa \\ --project=\u0026#34;${PROJECT_ID}\u0026#34; gcloud storage buckets add-iam-policy-binding \u0026#34;gs://${BUCKET_NAME}\u0026#34; \\ --member=\u0026#34;serviceAccount:langfuse-media-proxy-sa@${PROJECT_ID}.iam.gserviceaccount.com\u0026#34; \\ --role=\u0026#34;roles/storage.objectAdmin\u0026#34; # デプロイ（ingress 制限 + デフォルト URL 無効化を同時に設定） gcloud run deploy langfuse-media-proxy \\ --source=. \\ --region=asia-northeast1 \\ --service-account=\u0026#34;langfuse-media-proxy-sa@${PROJECT_ID}.iam.gserviceaccount.com\u0026#34; \\ --allow-unauthenticated \\ --ingress=internal-and-cloud-load-balancing \\ --no-default-url \\ --set-env-vars=\u0026#34;ALLOWED_BUCKETS=${BUCKET_NAME}\u0026#34; \\ --port=8080 補足：アクセス制御について --allow-unauthenticated を指定していますが、Cloud Run 自体の認証ではなく Cloud Armor の IP 制限 + ingress 制限でアクセスを制御します。--ingress=internal-and-cloud-load-balancing により LB 経由のアクセスのみ許可され、--no-default-url で *.run.app URL を無効化するため、Cloud Armor をバイパスして直接アクセスすることはできません。\nGlobal LB + Cloud Armor\n# Serverless NEG gcloud compute network-endpoint-groups create langfuse-proxy-neg \\ --region=asia-northeast1 \\ --network-endpoint-type=serverless \\ --cloud-run-service=langfuse-media-proxy \\ --project=\u0026#34;${PROJECT_ID}\u0026#34; # バックエンドサービス gcloud compute backend-services create langfuse-proxy-backend \\ --global \\ --project=\u0026#34;${PROJECT_ID}\u0026#34; gcloud compute backend-services add-backend langfuse-proxy-backend \\ --global \\ --network-endpoint-group=langfuse-proxy-neg \\ --network-endpoint-group-region=asia-northeast1 \\ --project=\u0026#34;${PROJECT_ID}\u0026#34; # URL マップ・HTTPS プロキシ・転送ルール gcloud compute url-maps create langfuse-proxy-lb \\ --default-service=langfuse-proxy-backend \\ --project=\u0026#34;${PROJECT_ID}\u0026#34; gcloud compute target-https-proxies create langfuse-proxy-https \\ --url-map=langfuse-proxy-lb \\ --ssl-certificates=your-ssl-cert \\ --project=\u0026#34;${PROJECT_ID}\u0026#34; gcloud compute forwarding-rules create langfuse-proxy-rule \\ --target-https-proxy=langfuse-proxy-https \\ --ports=443 \\ --global \\ --project=\u0026#34;${PROJECT_ID}\u0026#34; # Cloud Armor セキュリティポリシー gcloud compute security-policies create langfuse-media-policy \\ --project=\u0026#34;${PROJECT_ID}\u0026#34; gcloud compute security-policies rules update 2147483647 \\ --security-policy=langfuse-media-policy \\ --action=deny-403 \\ --project=\u0026#34;${PROJECT_ID}\u0026#34; gcloud compute security-policies rules create 1000 \\ --security-policy=langfuse-media-policy \\ --expression=\u0026#34;inIpRange(origin.ip, \u0026#39;YOUR_IP/32\u0026#39;)\u0026#34; \\ --action=allow \\ --project=\u0026#34;${PROJECT_ID}\u0026#34; gcloud compute backend-services update langfuse-proxy-backend \\ --security-policy=langfuse-media-policy \\ --global \\ --project=\u0026#34;${PROJECT_ID}\u0026#34; GCS バケットの設定\ngcloud storage buckets create \u0026#34;gs://${BUCKET_NAME}\u0026#34; \\ --uniform-bucket-level-access \\ --pap \\ --project=\u0026#34;${PROJECT_ID}\u0026#34; gcloud storage buckets add-iam-policy-binding \u0026#34;gs://${BUCKET_NAME}\u0026#34; \\ --member=\u0026#34;serviceAccount:langfuse-media-proxy-sa@${PROJECT_ID}.iam.gserviceaccount.com\u0026#34; \\ --role=\u0026#34;roles/storage.objectAdmin\u0026#34; 補足：バケット側の IP フィルタリングは不要です。アクセス制御は Cloud Armor に一元化します。\n使い方 # 方式 A の場合： プロキシ URL を Langfuse に渡します。\nPROXY_BASE_URL = \u0026#34;https://your-proxy-domain.com\u0026#34; BUCKET_NAME = \u0026#34;your-gcs-bucket\u0026#34; def gcs_uri_to_proxy_url(gcs_uri: str) -\u0026gt; str: path = gcs_uri.removeprefix(\u0026#34;gs://\u0026#34;) return f\u0026#34;{PROXY_BASE_URL}/{path}\u0026#34; 方式 B の場合： LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT にプロキシの URL を指定します。\n```yaml # docker-compose.yml environment: LANGFUSE_S3_MEDIA_UPLOAD_BUCKET: your-gcs-bucket LANGFUSE_S3_MEDIA_UPLOAD_REGION: auto LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID: \u0026lt;GCS HMAC Access Key\u0026gt; LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY: \u0026lt;GCS HMAC Secret Key\u0026gt; LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: https://your-proxy-domain.com LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE: \u0026#34;true\u0026#34; LANGFUSE_S3_MEDIA_UPLOAD_PREFIX: media/ AWS_REQUEST_CHECKSUM_CALCULATION: when_required 補足：CORS について Langfuse UI とプロキシが異なるドメインの場合、クロスオリジンリクエストになります。Langfuse は現在 \u0026lt;img\u0026gt; タグで画像を読み込むため CORS ヘッダは不要ですが、将来 fetch ベースに変更された場合はプロキシ側で CORS ヘッダの設定が必要になる可能性があります。\n懸念点 # 認証済み URL はセキュリティ的に大丈夫か？ # この認証方式は GCS コンソールからオブジェクトをダウンロードする際にも使われている GCP の標準的な仕組みです。\n認証済み URL（storage.cloud.google.com）は認証なしではアクセスできません。URL を知っていても IAM 権限がなければ 403 になります。\nただし、Langfuse ユーザー全員に Google アカウントと GCS の IAM 権限が必要なため、外部パートナーや Google アカウントを持たないユーザーがいる場合はプロキシ構成を検討してください。\nIP制限はGCS の IP フィルタリングで代用できないか？そうすればLB＋Cloud Armorは不要では？ # 検証した限りでは、以下の点で運用が難しいと感じました。\nIPv4 だけでなく IPv6 も管理が必要（接続元がIPv6 対応の場合、ブラウザは IPv6 を優先する（Happy EyeballsによりIPv6を先に試す）） まとめ # 方式 A：認証済み URL 方式 B：LangfuseMedia 標準 プロキシ構成（A・B 共通） 画像の表示方法 「Load Image」ボタン 自動インライン A: Load Image / B: 自動インライン プロキシ / LB 不要 不要 必要 GCS URL のブラウザ露出 あり（認証付き） あり（署名付き URL） なし ユーザーに必要な権限 GCS IAM なし なし URL 単体でのアクセス 不可（IAM 認証が必要） 可（有効期限内） 不可（プロキシ経由） 実装コスト 低 低 中 主なランニングコスト 最小 GCS ストレージ・オペレーション（データ量に応じて増加） LB + Cloud Run + Cloud Armor 方式・構成ごとにメリット・デメリットがあることがわかりました。\nユースケースやコスト、セキュリティ要件を踏まえて、本記事が選定の一助となれば幸いです。\n補足：署名付き URL はメディア以外でも使われる 本記事ではメディア（画像）表示を扱いましたが、バッチエクスポート（CSV/JSON ダウンロード）でも同じ署名付き URL が生成されます。エクスポートデータには dataset_items 等の全件が含まれるため、URL 露出のポリシーが厳しい環境ではエクスポート機能についても同様の考慮が必要です。\n参考 # Langfuse: Multi-modality Langfuse: Self-hosting GCS: 認証済みブラウザ ダウンロード Cloud Armor: セキュリティポリシー Cloud Run: 上り（内向き）設定 ","date":"2026年3月26日","externalUrl":null,"permalink":"/posts/langfuse-gcs-private-bucket-trace-images/","section":"Posts","summary":"こんにちは。ガオ株式会社の黒澤です。この記事では、Langfuseでトレースに非公開な画像を表示する場合に、Google Cloud Storage（以下、GCS）を用いた場合のアーキテクチャパターンについて、実装を踏まえてご紹介します。\n","title":"Langfuse × GCS プライベートバケットで非公開画像をトレース表示する","type":"posts"},{"content":" はじめに # この記事では、オープンソースのチャットUI「LibreChat」をDocker Composeでセットアップし、既存のLangfuseへトレースを送信する機能を試します。\nLibreChatでLangfuseにトレースを送信するには # LibreChatでLangfuseにトレースを送信するには、これまでLiteLLMをプロキシとして挟む構成が必要でした。\nLibreChat → LiteLLM (プロキシ) → LLM Provider ↓ Langfuse LiteLLMはLangfuse連携をサポートしており、LLM呼び出しをトレースできますが、LibreChatとは別にLiteLLMの構築・管理が必要になるため、セットアップの複雑さが増すという課題がありました。\nLibreChat v0.8.1（2025年12月11日リリース）では、PR #10292 により、LiteLLMなしでLibreChatから直接Langfuseにトレースを送信できるようになりました。\nLibreChat → LLM Provider ↓ Langfuse（直接送信） これにより、LibreChatとLangfuseだけのシンプルな構成でオブザーバビリティを実現できます。\nこの記事で実現すること # この記事では、以下の内容を実践します。\nDocker ComposeでLibreChatを構築し、OpenAI APIとLangfuseに接続 基本的なトレースの確認 LibreChatでMCP Server利用時のトレースを確認 注意：この記事では、Langfuseは既に用意されている前提で進めます。Langfuseをセルフホストする方法については、Langfuse公式ドキュメント を参照してください。Langfuse Cloudを利用する場合は、https://cloud.langfuse.com でアカウントを作成してください。\nそれでは、環境構築から始めます。\n環境構築編 # LibreChatをDocker Composeでローカル環境に構築し、既存のLangfuseに接続します。\n前提条件 # 以下がインストールされていることを前提とします。\nDocker Desktopなど、Docker Composeが使える環境 以下何れかのLangfuseとAPIキー Langfuse Cloud セルフホストしたLangfuse LLMプロバイダーのAPIキー（OpenAI、Anthropic、Google Vertex AIなど） アーキテクチャ概要 # 今回構築する環境の構成は以下の通りです。\nアーキテクチャ図 各コンポーネントの役割\nLibreChat: チャットUIおよびLLM統合層 MongoDB: LibreChatのデータベース（ユーザー情報、会話履歴など） 外部Langfuse: トレース収集・可視化（Cloudまたはセルフホスト） LLM Provider API: OpenAI、Vertex AI、Anthropicなどのモデルプロバイダー セットアップ手順 # 1. ディレクトリ構成 # 以下のようなディレクトリ構成で環境構築します。\nlibrechat-langfuse-integrate/ ├── .env # 環境変数 ├── docker-compose.yml # コンテナ構成定義 └── librechat.yaml # LibreChatの設定ファイル 任意のディレクトリを作成し、以降の手順で各ファイルを配置していきます。\n2. 環境変数ファイル (.env) # .env ファイルを作成し、Langfuse接続情報とLLMプロバイダーのAPIキーを設定します。\n.env\n# ============================================ # LibreChat Server Settings # ============================================ PORT=3080 ALLOW_REGISTRATION=true # ============================================ # LLM Provider Settings # ============================================ # OpenAI API Key OPENAI_API_KEY=sk-proj-xxxxxxxxxxxxxxxxxxxxxxxx # ============================================ # Langfuse Settings (External Server) # ============================================ # Get these values from your Langfuse project settings LANGFUSE_SECRET_KEY=sk-lf-xxxxxxxxxxxxxxxxxxxxxxxx LANGFUSE_PUBLIC_KEY=pk-lf-xxxxxxxxxxxxxxxxxxxxxxxx # Langfuse Cloud の場合は https://cloud.langfuse.com # セルフホストの場合は https://your-langfuse-server.com LANGFUSE_BASE_URL=https://cloud.langfuse.com # ============================================ # LibreChat Auth Secrets # ============================================ # セキュリティ上、必ず変更してください # 生成: openssl rand -hex 16 CREDS_KEY=your-creds-key-change-this # 生成: openssl rand -hex 8 CREDS_IV=your-creds-iv-change-this # 生成: openssl rand -hex 32 JWT_SECRET=your-jwt-secret-change-this # 生成: openssl rand -hex 32 JWT_REFRESH_SECRET=your-jwt-refresh-secret-change-this 補足\nLangfuseとの接続に必要なのは LANGFUSE_SECRET_KEY、LANGFUSE_PUBLIC_KEY、LANGFUSE_BASE_URL の3つです。これらの値はLangfuseプロジェクトの Settings → API Keys から取得できます。 Auth Secrets（CREDS_KEY、CREDS_IV、JWT_SECRET、JWT_REFRESH_SECRET）は、各項目のコメントに記載した openssl rand コマンドで生成した値に置き換えてください。 3. Docker Compose設定ファイル (docker-compose.yml) # LibreChatとMongoDBのみを起動し、OpenAI APIを利用したシンプルな構成です。\nフル機能を含む公式のDocker Compose設定は以下公式GitHubリポジトリの設定ファイルを参照して下さい。\nhttps://github.com/danny-avila/LibreChat/blob/main/docker-compose.yml docker-compose.yml\nservices: # =========================================== # LibreChat Application Server # =========================================== api: container_name: LibreChat image: ghcr.io/danny-avila/librechat:v0.8.1 ports: - \u0026#34;${PORT}:${PORT}\u0026#34; depends_on: - mongodb restart: always extra_hosts: - \u0026#34;host.docker.internal:host-gateway\u0026#34; env_file: - .env environment: - HOST=0.0.0.0 - MONGO_URI=mongodb://mongodb:27017/LibreChat volumes: - ./librechat.yaml:/app/librechat.yaml:ro # =========================================== # MongoDB - LibreChatのデータストア # =========================================== mongodb: container_name: chat-mongodb image: mongo:8.0.17 restart: always volumes: - mongodb-data:/data/db command: mongod --noauth volumes: mongodb-data: 4. LibreChat設定ファイル (librechat.yaml) # LibreChatの動作をカスタマイズする設定ファイルです。以下の例はOpenAIモデルとエージェント機能を使用する設定です。\nlibrechat.yaml\n# Configuration version (required) version: 1.3.5 # Cache settings: Set to true to enable caching cache: true # Endpoints Configuration endpoints: # OpenAI Configuration openAI: apiKey: \u0026#34;${OPENAI_API_KEY}\u0026#34; models: default: - \u0026#34;gpt-5\u0026#34; - \u0026#34;gpt-5-mini\u0026#34; - \u0026#34;gpt-5-nano\u0026#34; titleConvo: true summarize: false titleModel: \u0026#34;gpt-5-nano\u0026#34; # Agents Configuration # Langfuse tracing is enabled for Agents agents: disableBuilder: false # Enable Agent Builder UI recursionLimit: 25 # Default steps an agent can take maxRecursionLimit: 50 # Maximum steps limit # MCP Server Configuration（後ほど検証編2で使用） mcpServers: {} registration: socialLogins: [] 補足\nmcpServers は後ほどMCP Server機能を検証する際に設定を追加します。 5. Docker ComposeでLibreChatを起動・動作確認 # 以下のコマンドでLibreChatを起動します。\n# LibreChatとMongoDBを起動 docker compose up -d # 起動状態を確認 docker compose ps 初回起動時は、イメージのダウンロードやデータベースの初期化が行われるため、数分かかる場合があります。\nサービスが正常に起動しているか、ログを確認します。\n# LibreChatコンテナのログを確認 docker compose logs -f api # 正常に起動していれば、以下のようなログが表示されます # LibreChat v0.8.1 # Server listening on port 3080 # Connected to MongoDB 起動を確認したら、以下の手順でLibreChatにアクセスします。\nhttp://localhost:3080 にアクセス 新規登録からアカウントを作成 LibreChatログイン画面 3. ログイン後、設定したLLM（OpenAI）を選択し、LLMに挨拶をし、LibreChatで正しくLLMが動作することを確認します。\nLLMチャット画面 6. Langfuse接続の確認 # LibreChatで簡単な会話を実行した後、Langfuse UIにアクセスしてトレースが記録されているか確認します。\nLangfuse UI アクセス先 # Langfuse Cloud : https://cloud.langfuse.com セルフホスト : セルフホストのLangfuseURL Tracing ページに移動し、LibreChatからのトレースが表示されていれば成功です。\nLangfuseトレース これで、LibreChatと外部Langfuseの統合環境が完成しました。次は、実際にトレースの詳細を確認します。\n検証編1 - 基本的なトレース確認 # 環境構築が完了したので、トレースが正しく送信されているか確認します。\n「5. Docker ComposeでLibreChatを起動・動作確認」での会話のトレース詳細を確認しましょう。\nトレースの確認 # 上記で実行した会話の内容についてトレースを深堀りしてみます。\nLangfuse UIの Tracing ページに移動します。\n確認できる情報 # 主に以下のような内容がトレースから確認できます。\nTrace ID（各会話に一意のIDが付与されています） Timestamp（トレースが記録された日時） User ID（LibreChat MongoDBのObjectIDと紐づく） Session ID （LibreChatのスレッドID : ConversationIDと紐づく） Input/Output（プロンプトとLLMの応答） Latency（リクエストにかかった時間） Tokens（入力トークン数と出力トークン数） Cost（トークン使用量から計算されたコスト） Langfuse トレース トレースの詳細を確認 # 特定のトレースをクリックすると、詳細情報が表示されます。\n1. 実行フロー # トレースは階層構造で表示され、以下のような流れと各処理にかかったコストが確認できます。\nまた、自動的にエージェントの処理フローがグラフとしても表示されます。\nトレースフロー 2. プロンプト詳細 # Input、Output セクションには、実際にLLMに送信されたプロンプトとLLMからの応答が表示されます。\nプロンプト詳細 3. パフォーマンスメトリクス # パフォーマンス関連のメトリクスとしては以下のようなものが確認できます。\nLatency（リクエストの開始から完了までの時間） Time to First Token (TTFT)（最初のトークンが生成されるまでの時間） パフォーマンスメトリクス これらの指標により、モデルのパフォーマンスを定量的に評価できます。\n複数の会話スレッドを確認 # LibreChatで新規チャットを開始すると、新しい Conversation ID が付与されます。Langfuse UIの Sessions ページで、Conversation ID ごとにグループ化された会話を確認できます。\nLangfuseのSessions これにより、ユーザーがどのような会話の流れを持っているかを追跡できます。\nコスト分析 # デフォルトで用意されている Langfuse Cost Dashboard ページにより、\n以下のようなコスト分析が可能です。\n総コスト（トークン使用量から計算された総コスト） モデル別コスト（どのモデルがどれだけコストを使っているか） ユーザー別コスト（どのユーザーがどれだけコストを使っているか） LangfuseのDashboard 検証1のまとめ # 基本的なトレース送信機能の検証により、以下のことが確認できました。\nLibreChatからLangfuseへのトレース送信が正常に動作 会話内容、パフォーマンスメトリクスが正確に記録される Sessions から ConversationIDごとの、会話を追跡できる トークン使用量とコストを可視化できる 次は、MCP Server機能を使用した場合のトレースを確認します。\n検証編2 - MCP Server利用時のトレース # LibreChatはMCP (Model Context Protocol) Serverをサポートしており、LLMにツール実行機能を追加できます。ここでは、MCP Serverを設定し、ツール呼び出し時のトレースがどのように記録されるかを確認します。\nMCP Serverの設定 # Langfuse Docs MCP Serverを設定する例を示します。\nlibrechat.yamlの更新 # librechat.yaml を編集し、mcpServers セクションに以下の設定を追加します。\nmcpServers: langfuse-docs: command: npx args: - \u0026#34;-y\u0026#34; - \u0026#34;mcp-remote\u0026#34; - \u0026#34;https://langfuse.com/api/mcp\u0026#34; metadata: name: \u0026#34;Langfuse Documentation\u0026#34; description: \u0026#34;Access to Langfuse official documentation via MCP\u0026#34; 追記後の librechat.yaml は下記セクションを参照ください。\nlibrechat.yaml\n# Configuration version (required) version: 1.3.5 # Cache settings: Set to true to enable caching cache: true # Endpoints Configuration endpoints: # OpenAI Configuration openAI: apiKey: \u0026#34;${OPENAI_API_KEY}\u0026#34; models: default: - \u0026#34;gpt-5\u0026#34; - \u0026#34;gpt-5-mini\u0026#34; - \u0026#34;gpt-5-nano\u0026#34; titleConvo: true summarize: false titleModel: \u0026#34;gpt-5-nano\u0026#34; # Agents Configuration # Langfuse tracing is enabled for Agents agents: disableBuilder: false # Enable Agent Builder UI recursionLimit: 25 # Default steps an agent can take maxRecursionLimit: 50 # Maximum steps limit # MCP Server Configuration mcpServers: langfuse-docs: command: npx args: - \u0026#34;-y\u0026#34; - \u0026#34;mcp-remote\u0026#34; - \u0026#34;https://langfuse.com/api/mcp\u0026#34; metadata: name: \u0026#34;Langfuse Documentation\u0026#34; description: \u0026#34;Access to Langfuse official documentation via MCP\u0026#34; registration: socialLogins: [] 設定の反映 # # LibreChatコンテナを再起動 docker compose restart api # ログを確認 docker compose logs -f api 起動ログに以下のようなメッセージが表示されれば、MCP Serverが正常に起動しています。\ninfo: MCP servers initialized successfully. Added 3 MCP tools. MCP Serverを使った会話 # LibreChat UIに戻り、新しい会話を開始します。MCP Serverが有効化されている場合、チャット欄に「MCP サーバー」という欄が表示され、「MCP サーバー」を押下すると langfuse-docs が選択できます。\nlangfuse-docs にチェックを入れた後、以下のような指示をLLMに依頼しましょう。\nチャット画面 すると、LLMは langfuse-docs の Tool : searchLangfuseDocs を利用して調査しその内容を下に回答を生成してくれます。（生成された回答は長いので一部省略）\nチャット画面 Langfuse UIでMCP Serverトレースを確認 # MCP Serverを使用した会話のトレースを確認します。\nToolの利用 # LLMがToolを利用した記録を確認できます。\nLangfuseトレース また、Tool の Input、Output から、呼び出し（クエリ内容）とそれに対する戻り値（クエリ結果）を確認できます。\n（Outputは階層が深いので一部省略。）\nLangfuseトレース 検証2のまとめ # MCP Server利用時のトレース検証により、以下のことが確認できました。\nMCP Serverのツール呼び出しが正確にトレースされる ツール呼び出しのパラメータと戻り値が記録される まとめ # LibreChat v0.8.1で追加されたLangfuse統合機能を、外部Langfuse接続で検証しました。\n良かった点 # セットアップが簡単（Langfuseの環境変数を3つ追加するだけで動作） コード変更なしで会話が自動的にトレースされる User ID、Conversation IDなどのメタデータが自動記録されLangfuse上でトレースがグルーピングされる MCP Serverのツール呼び出しも含めてトレースされる こんな人におすすめ # 既にLangfuseを使っていて、LibreChatの会話も一元管理したい方 企業内でLLMチャットUIを提供し、利用状況を可視化したい方 MCP Serverやエージェント機能のデバッグに詳細なトレース情報が必要な方 今後の期待 # 今回の検証では、トレースに記録されるObservation Typeが主に generation（LLM呼び出し）に限られていることも確認できました。\nLangfuseは generation 以外にも、agent、tool、など多彩なObservation Type をサポートしています。\n例えば、MCP Serverのツール呼び出しが tool 型として記録されるようになれば、Langfuse UI上でのフィルタリングや分析がより直感的になるはずです。\nLibreChatのLangfuse統合はv0.8.1で初めて導入された機能です。今後のバージョンアップでこれらのObservation Typeへの対応が進むことで、トレースの表現力がさらに向上し、より実践的なオブザーバビリティが実現できるのではないかと期待しています。\n参考リンク # LibreChat公式ドキュメント: https://www.librechat.ai/docs Langfuse公式ドキュメント: https://langfuse.com/docs Langfuse LibreChat統合ガイド: https://langfuse.com/integrations/other/librechat LibreChat PR #10292（Langfuse統合）: https://github.com/danny-avila/LibreChat/pull/10292 ","date":"2026年3月11日","externalUrl":null,"permalink":"/posts/librechat-langfuse-integration/","section":"Posts","summary":"はじめに # この記事では、オープンソースのチャットUI「LibreChat」をDocker Composeでセットアップし、既存のLangfuseへトレースを送信する機能を試します。\n","title":"LibreChatに統合されたLangfuseトレース送信機能を試す","type":"posts"},{"content":"過去、Langfuseでのマスキングについて触れてきましたが、これまではクライアントサイドで対応するしかありませんでした。しかし、ついに先日のリリース（v3.152.0 ）で、サーバーサイドでのマスキングが設定可能になりました。\n※注意：現時点ではEE（Enterprise Edition）ライセンス専用機能となっています。\n今回は公式ドキュメントを参考に、実際に設定してみました。\n基本設定 # 事前準備 # マスク処理を行う、Langfuseからのコールバック先が必要になります。\n今回は設定の挙動を確認したいため、公式の実装例 をそのまま利用し、Cloud Runにデプロイしました。\n設定 # 設定自体は、Langfuse Workerのコンテナに環境変数 LANGFUSE_INGESTION_MASKING_CALLBACK_URL を設定すれば利用できるようになります。\n※ 注意：Langfuse Worker コンテナにEEライセンスが適用されていない場合、コールバックURLを設定しても、そのURLへリクエストが送られることはありません。\n実際にEEライセンスが適用されていない状態でトレースを送信すると、手元の環境では以下のエラーログが出力されました。\nwarn: Ingestion masking callback URL is configured but enterprise license is not available. Masking will be disabled.\n実験 # サーバーサイドマスキングは、OpenTelemetryエンドポイント（/api/public/otel）経由のイベントに適用されます。Python SDK v3.x系はこれに対応しているため、SDKを利用した簡易的なトレース送信コードを作成し、テストしました。\nテストコード\n# ※環境に合わせてクライアントを初期化してください langfuse = get_client() with langfuse.start_as_current_observation( as_type=\u0026#34;span\u0026#34;, name=\u0026#34;test-trace\u0026#34;, input=\u0026#34;山田太郎様からの問い合わせ\u0026#34;, ) as root_span: trace_id = langfuse.get_current_trace_id() with langfuse.start_as_current_observation( as_type=\u0026#34;span\u0026#34;, name=\u0026#34;test-span\u0026#34;, input=\u0026#34;山田太郎です。登録しているメールアドレスは何ですか？\u0026#34;, ) as span: span.update( output=\u0026#34;山田太郎様のメールアドレスはtest-user@example.comです。\u0026#34;, metadata={ \u0026#34;user_email\u0026#34;: \u0026#34;test-user@example.com\u0026#34;, \u0026#34;support_phone\u0026#34;: \u0026#34;123-456-7890\u0026#34; }, ) root_span.update_trace( output=\u0026#34;問い合わせが完了しました。\u0026#34;, ) langfuse.flush() 検証１：公式の実装例 # まずは、サンプル実装に対してリクエストを送信した場合にどうなるか確認してみます。\n1階層目のトレース（test-trace）には、今回置換対象となる項目が含まれていないため、2階層目、3階層目のトレース情報を確認してみます。\n結果 # 2階層目\n3階層目\n置換対象となる、メールアドレス・電話番号が含まれている箇所について、Output、Metadata ともにマスキング（置換）されていることが確認出来ました。\n検証２：組織・プロジェクト単位でのマスク # mask_trace 関数内で x_langfuse_org_id や x_langfuse_project_id を利用することで、組織単位・プロジェクト単位で異なるマスク処理を適用することができます。\nここで、コールバック先に設定しているアプリケーションの mask_trace を以下の通り変更します。特定の組織のみ、マスク処理の異なるmask_pii2が適用されるようにしました。\ndef mask_pii2(data): print(f\u0026#34;mask_pii2: {data}\u0026#34;) if isinstance(data, str): data = f\u0026#34;[REDACTED] {data}\u0026#34; return data elif isinstance(data, dict): return {k: mask_pii2(v) for k, v in data.items()} elif isinstance(data, list): return [mask_pii2(item) for item in data] return data async def mask_trace( request: Request, x_langfuse_org_id: Optional[str] = Header(None), x_langfuse_project_id: Optional[str] = Header(None) ): body = await request.json() if x_langfuse_org_id == \u0026#34;xxxxxxxxxxxxxxxxxxxxxxx\u0026#34;: masked_body = mask_pii2(body) else: masked_body = mask_pii(body) return masked_body コード上の引数 x_langfuse_org_id、x_langfuse_project_id は、Langfuseから送られてくるHTTPヘッダー（x-langfuse-org-id、x-langfuse-project-id）から取得されています。そのため、ヘッダーが付与されていない際には値が取得できない可能性があります。なお、今回利用したPython SDKによる送信方法では、問題なく付与されていました。\n※ 上記コードのように条件分岐を行う場合、指定するのは name ではなく、id であることに注意して下さい\n結果 # 指定した組織には mask_pii2 が、それ以外の組織には mask_pii が適応されることを確認出来ました。\nしかし、mask_pii2は以下の通り、想定される形にはなりませんでした。\nサーバサイドで行う場合は、リクエストボディ全体が処理対象となります。そのため、今回のように再帰的に処理をかけると、トレースの構造を定義する resource.attributes などのキーまで書き換わってしまい、Langfuse側で正しくInput/Outputとして認識されなくなってしまいました。\nクライアントサイドで利用していた処理を移植したい場合は、メタデータを破壊しないよう、対象となるデータ部分だけを加工するなどの工夫が必要となりそうです。\n参考：同じmask_pii2をクライアントサイドの maskオプションに指定した場合 補足：Dataset Run への影響について # UIから Dataset Runを実行した場合にはマスク処理が適応されないことを確認しました。\nマスキングされることによるEvaluator への影響や、結果の目視がしにくいと言った問題は起こらなさそうです。\nまとめ # 実際に試してみた結果、細かい制御の手軽さという点では、クライアントサイドでマスク処理を行う方に分がある印象です。\nしかし、「いざという時のセーフティーネット」として活用できたり、既にOpenTelemetryエンドポイント経由で動作しているアプリケーションに対して、「アプリ側のコード改修無しでマスク処理を適用できる」など、サーバサイドならではのメリットも多く存在します。\nまた、今回は検証していませんが、環境変数 LANGFUSE_INGESTION_MASKING_PROPAGATED_HEADERSを設定することで、オリジナルのリクエストから任意のカスタムヘッダーをコールバック先へ伝播させることも可能なようです。これを活用すれば、要件に合わせてさらに柔軟なマスキング制御を実現していくこともできそうです。\n","date":"2026年3月2日","externalUrl":null,"permalink":"/posts/2026-03-02-langfuse%E3%81%AE%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%82%B5%E3%82%A4%E3%83%89%E3%83%9E%E3%82%B9%E3%82%AD%E3%83%B3%E3%82%B0%E3%82%92%E8%A9%A6%E3%81%97%E3%81%A6%E3%81%BF%E3%81%9F/","section":"Posts","summary":"過去、Langfuseでのマスキングについて触れてきましたが、これまではクライアントサイドで対応するしかありませんでした。しかし、ついに先日のリリース（v3.152.0 ）で、サーバーサイドでのマスキングが設定可能になりました。\n","title":"Langfuseのサーバーサイドマスキングを試してみた","type":"posts"},{"content":"","date":"2026年3月2日","externalUrl":null,"permalink":"/tags/pii/","section":"タグ","summary":"","title":"PII","type":"tags"},{"content":"","date":"2026年3月2日","externalUrl":null,"permalink":"/tags/%E3%83%97%E3%83%A9%E3%82%A4%E3%83%90%E3%82%B7%E3%83%BC/","section":"タグ","summary":"","title":"プライバシー","type":"tags"},{"content":"","date":"2026年3月2日","externalUrl":null,"permalink":"/tags/%E3%83%9E%E3%82%B9%E3%82%AD%E3%83%B3%E3%82%B0/","section":"タグ","summary":"","title":"マスキング","type":"tags"},{"content":"","date":"2026年2月28日","externalUrl":null,"permalink":"/tags/ai-gateway/","section":"タグ","summary":"","title":"AI Gateway","type":"tags"},{"content":"","date":"2026年2月28日","externalUrl":null,"permalink":"/tags/kong/","section":"タグ","summary":"","title":"Kong","type":"tags"},{"content":"","date":"2026年2月28日","externalUrl":null,"permalink":"/tags/opentelemetry/","section":"タグ","summary":"","title":"OpenTelemetry","type":"tags"},{"content":"","date":"2026年2月28日","externalUrl":null,"permalink":"/tags/%E3%82%AA%E3%83%96%E3%82%B6%E3%83%BC%E3%83%90%E3%83%93%E3%83%AA%E3%83%86%E3%82%A3/","section":"タグ","summary":"","title":"オブザーバビリティ","type":"tags"},{"content":"","date":"2026年2月28日","externalUrl":null,"permalink":"/tags/%E3%82%AC%E3%83%90%E3%83%8A%E3%83%B3%E3%82%B9/","section":"タグ","summary":"","title":"ガバナンス","type":"tags"},{"content":"LMアプリケーションの可観測性（オブザーバビリティ）を確保しようとする際、Langfuse SDK や OpenTelemetry SDK をアプリケーション側に組み込んで計装するのが一般的なアプローチですが、これは多少なりとも手間がかかることと、社内のエージェントを勝手に動かす人などが意図的に観測されないように対応しないこともありえるでしょう。\nしかしながら、LLM への呼び出しを LLM Gateway に集約することで、アプリケーション側での計装が不要になり、ガバナンスを高めることにも寄与します。\nそこでこの記事では、Pythonアプリ（Google ADKエージェント / google-genai SDK）からのLLM呼び出しを API gateway である Kong経由に切り替え、Kongの組み込みOpenTelemetr プラグインから Langfuse の OTLP/HTTP エンドポイントへ直接トレースを送信するまでの手順をまとめます。\n本記事の作成にあたりまして、Langfuse Night #3 の川村さんの登壇内容を参考にさせていただきました。ありがとうございます！ https://www.docswell.com/s/shukawam/Z2QJD9-langfuse-and-kong-gateway 検証環境 # コンポーネント バージョン 備考 Kong Gateway 3.10 OSS版を利用 Kong plugins bundled ai-proxy, opentelemetry を使用 google-adk 1.x google-genai SDK 1.x Langfuse Cloud OTLP/HTTP endpoint を使用 この記事での検証はネットワーク的にはローカルでの docker compose を前提としています。Kongとアプリは同一Dockerネットワーク内で通信し、Kong自体は外部公開しません。\n全体構成 # アーキテクチャの全体像は以下の通りです。\nUser (curl) ↓ MCP Server / ADK Agent (Python) ↓ genai SDK (base_url → Kong) Kong AI Gateway（Docker network 内部） ├── ai-proxy plugin → LLM リクエストを Vertex AI に送信 └── opentelemetry plugin → Langfuse OTLP/HTTP endpoint どのプラグインで Langfuse に送るか # Langfuse の Kong 連携ガイド では、kong-plugin-ai-tracing を導入してトレースする手順が「Recommended」として案内されています。\n一方で今回は、追加プラグインの導入・管理を増やしたくなかったので、Kong 組み込みの opentelemetry プラグインを使いました。Kong 側は OTLP/HTTP（Protobuf）で送信でき、バックエンド直送も Collector 経由も選べます。\n実装手順 # 細かい実装の手順やConfigはざっくりこちらにまとめました。ご参考までにどうぞ。\n1. Kong 設定 # kong.yaml\n_format_version: \u0026#34;3.0\u0026#34; services: - name: gemini-service # Service 定義上 url は必須だがダミー値。 # ai-proxy が upstream を置き換え、Vertex AI へ直接接続するため転送されない。 url: http://httpbin.konghq.com routes: - name: gemini-route paths: - /gemini plugins: - name: ai-proxy config: route_type: llm/v1/chat llm_format: gemini logging: log_statistics: true auth: gcp_use_service_account: true gcp_service_account_json: \u0026#39;${GCP_SERVICE_ACCOUNT_JSON}\u0026#39; # 本番環境では、サービスアカウント鍵（JSON）の配布・保管・ローテーションが # セキュリティ上の課題・運用負荷になりやすいため、サービスアカウントキーを # 使わない認証を推奨します。今回はあくまで検証用ということでご理解ください。 model: provider: gemini name: gemini-2.5-flash options: gemini: api_endpoint: us-central1-aiplatform.googleapis.com project_id: ${GCP_PROJECT_ID} location_id: ${GCP_LOCATION_ID} - name: opentelemetry config: traces_endpoint: ${LANGFUSE_HOST}/api/public/otel/v1/traces headers: Authorization: \u0026#34;Basic ${LANGFUSE_AUTH_BASE64}\u0026#34; 2. コード側でKongに向ける # コード記載例\nKONG_BASE_URL = os.getenv(\u0026#34;KONG_BASE_URL\u0026#34;, \u0026#34;http://kong:8000/gemini\u0026#34;) model = Gemini(model=\u0026#34;gemini-2.5-flash\u0026#34;, base_url=KONG_BASE_URL) client = genai.Client(http_options={\u0026#34;base_url\u0026#34;: KONG_BASE_URL}) 3. docker compose（最小構成） # docker-compose.yaml\nservices: kong: build: context: . dockerfile: Dockerfile.kong environment: KONG_DATABASE: \u0026#34;off\u0026#34; KONG_DECLARATIVE_CONFIG: /opt/kong/kong.yml KONG_PLUGINS: bundled KONG_PROXY_LISTEN: \u0026#34;0.0.0.0:8000\u0026#34; # 詰まりどころ（後述） KONG_NGINX_PROXY_CLIENT_BODY_BUFFER_SIZE: \u0026#34;8m\u0026#34; LANGFUSE_PUBLIC_KEY: ${LANGFUSE_PUBLIC_KEY} LANGFUSE_SECRET_KEY: ${LANGFUSE_SECRET_KEY} LANGFUSE_HOST: ${LANGFUSE_HOST:-https://cloud.langfuse.com} GCP_PROJECT_ID: ${GCP_PROJECT_ID} GCP_LOCATION_ID: ${GCP_LOCATION_ID} volumes: - ./credentials/service-account-key.json:/app/credentials/service-account-key.json:ro app: environment: - KONG_BASE_URL=http://kong:8000/gemini - GOOGLE_API_KEY=dummy depends_on: - kong 動作確認 # 1. Kong の起動確認 # docker compose exec kong kong health 2. Kong 経由で Gemini が応答するか # docker compose exec kong curl -s -X POST http://localhost:8000/gemini/v1/chat/completions \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{\u0026#34;messages\u0026#34;:[{\u0026#34;role\u0026#34;:\u0026#34;user\u0026#34;,\u0026#34;content\u0026#34;:\u0026#34;hello\u0026#34;}],\u0026#34;model\u0026#34;:\u0026#34;gemini-2.5-flash\u0026#34;}\u0026#39; \\ | jq .choices[0].message.content 3. Langfuse にトレースが届くか # Langfuse の Traces を開いて確認します。\n実行結果 (Traceの比較) # 実装できたら、Langfuseの画面で結果を確認してみます。\nすると、エージェント側で計装したもの(パターンA) と Kong経由で自動でLangfuseにTraceを飛ばすもの (パターンB) では微妙に確認できるものが異なることに気がつくと思います。\n両者とも基本的な要素は全てカバーされており、構造・トークン・モデル名・レイテンシーなどを確認することができますが、パターンB においては、Traceレベルの Input/Outputに情報が入らず、Promptとの紐付けなどもできません。一方でパターンAはエージェント側で計装しますので、もちろんどのようにでも表示可能です。\nパターンA. Kong を通さないもの (エージェント側で計装)\nトップレベルのTraceにもちゃんと Input/Output が入ってる パターンB. Kong経由 (エージェント側で実装なし)\n大体の情報は入ってるが、トップレベルのTraceには Input/Output は入らない おまけ: 実装時の注意メモ # 1. リクエストボディが大きいと ai-proxy が 400 を返す # エージェントがツールを使うと、2回目以降の LLM 呼び出しで会話履歴＋ツール結果が乗り、ボディが急に大きくなります。Nginx 側のデフォルトが小さいと、ボディがテンポラリに逃げたりして ai-proxy の処理が崩れ、次のエラーに当たりました。\n400 \u0026ldquo;request body doesn\u0026rsquo;t contain valid prompts\u0026rdquo; ツール不使用だと通るが、ツール使用だと落ちる（この分岐が地味に厄介） 対策：先ほどの compose ファイルにもあった通り、Nginx のバッファサイズを上げます。\nKONG_NGINX_PROXY_CLIENT_BODY_BUFFER_SIZE: \u0026#34;8m\u0026#34; 2. preserve 前提で組むと 3.11 以降で詰まる # preserve は 3.11.0.0 で deprecated です。将来削除される前提なので、早めに \u0026ldquo;新しい route_type\u0026rdquo;（今回なら llm/v1/chat）に寄せておくのが安全かと思われます。\nまとめ： # 今回は、Kong AI GatewayとLangfuseを用いて、アプリケーション側に計装SDKを組み込まずにLLMのオブザーバビリティを確保する手順をご紹介しました。\nゲートウェイ層にLLMの呼び出しとトレース送信の責務を集約することで、アプリケーション側のコードを汚さずに素早く観測を始められるのは、この構成の大きな強みです。\n","date":"2026年2月28日","externalUrl":null,"permalink":"/posts/2026-02-28-%E3%82%AC%E3%83%90%E3%83%8A%E3%83%B3%E3%82%B9%E3%82%92%E9%AB%98%E3%82%81%E3%82%8Bkong-ai-gateway-langfuse%E3%81%A7-%E3%82%A2%E3%83%97%E3%83%AA%E8%A8%88%E8%A3%85%E3%81%AA%E3%81%97-%E3%81%AEllm%E3%82%AA%E3%83%96%E3%82%B6%E3%83%BC%E3%83%90%E3%83%93%E3%83%AA%E3%83%86%E3%82%A3/","section":"Posts","summary":"LMアプリケーションの可観測性（オブザーバビリティ）を確保しようとする際、Langfuse SDK や OpenTelemetry SDK をアプリケーション側に組み込んで計装するのが一般的なアプローチですが、これは多少なりとも手間がかかることと、社内のエージェントを勝手に動かす人などが意図的に観測されないように対応しないこともありえるでしょう。\n","title":"ガバナンスを高めるKong AI Gateway + Langfuseで\"アプリ計装なし\"のLLMオブザーバビリティ","type":"posts"},{"content":"","date":"2026年2月27日","externalUrl":null,"permalink":"/tags/llmops/","section":"タグ","summary":"","title":"LLMOps","type":"tags"},{"content":"LLMアプリケーションの開発で、こんな経験はないでしょうか。\n「先週と同じ条件で実験したいのに、データセットを更新したから再現できない…」\n「評価データを改善したいけど、過去の結果と比較できなくなるのが怖い…」\n「チームメンバーがデータセットを変更して、自分の実験の前提が変わってしまった…」\n「実験後にデータを1件修正したら、あの実験で何を入力していたか確認できなくなった…」\n評価データセットを更新すると、過去の実験結果との比較が難しくなります。同じプロンプトで精度が変わったとき、それがモデル改善によるものか、データセットの変更によるものかを切り分けられなくなるのです。特にチーム開発では、誰かがデータセットを更新することで、実行中の実験の前提が変わってしまう可能性もあります。\nLangfuseのデータセットバージョニング機能は、この課題を解決します。この機能により、評価データの変更とモデルの変更を分離して管理し、実験の再現性を確保できるようになりました。\n本記事でわかること # データセットバージョニングが必要になる具体的な場面 Langfuseのタイムスタンプベースバージョニングの仕組み 実験の再現性確保・チーム開発での活用方法 なぜデータセットバージョニングが必要か # 「先週のデータセットで再度実験したいのに、もう戻せない」\n「精度が下がったけど、データが変わったせいなのか、モデルのせいなのか分からない」\n「他のメンバーがデータセットを更新して、自分の実験が意図しない影響を受けた」\nLLMアプリの評価では、評価データセットは「固定されたもの」ではなく、継続的に改善されるべきものです。エッジケースを発見したら追加し、不適切なテストケースを修正し、より実践的なシナリオを反映するよう更新していく。しかし、従来のデータセット管理では、更新のたびに過去のバージョンが上書きされてしまいます。これにより、以下のような問題が発生します：\n再現性の欠如: 実験結果のRun Itemを開いても「今の最新データ」が表示されるため、実験を実行した当時の入力が何だったか確認できない。実験後にデータを1件でも修正すると、「当時の入力で動いていたのか、修正後の入力で動いていたのか」が永久に不明になる 比較の困難: データセット更新前後で精度が変わったとき、データの変化によるものか、モデルの変化によるものか判断できない チーム開発での衝突: 複数人で開発していると、データセット更新が他のメンバーの実験に意図せず影響を与える LLMOpsの観点では、データのバージョン管理はモデルのバージョン管理と同じくらい重要です。MLOpsでは学習データのバージョン管理は一般的ですが、LLMアプリの評価データも同様に管理すべきなのです。\nLangfuseのデータセットバージョニング機能 # Langfuse v3.151.0 で、実験向けのデータセットバージョニング機能が強化されました。データセットアイテムの各更新が履歴として保持され、ISO 8601形式のタイムスタンプ（例: 2026-01-21T14:35:42Z）で特定の時点のデータセットを参照できます。\nこの仕組みにより、実験実行時に「どのバージョンのデータセットを使うか」を明示的に指定でき、実験結果には使用したバージョンが自動的に記録されます。後から実験結果を見たときに、「この実験は当時のどのデータで実行されたか」が正確に追跡できるのです。\n実現できること # データセットバージョニングにより、以下のような運用が可能になります。\n1. 実験の完全な再現性 # 「先週のプロンプト改善で精度が5%向上したはずなのに、今日試したら再現できない…」\nこんな経験はないでしょうか。バージョニング機能があれば、実験実行時のデータセットバージョンが自動記録されるため、数週間後でも当時と全く同じ条件で実験を再現できます。実験結果の画面には使用したバージョンが記録されており、クリックするだけで当時のデータ一覧に遷移できます。「あの実験はどのデータで動かしたっけ？」で悩む必要はありません。\n2. 安全なデータセット改善 # データセットを改善・修正しても、過去のバージョンは保持されます。「新しいケースを追加したら精度が下がった。元のデータセットで再確認したい」といった場合に、更新前のバージョンで再実験できるため、安心してデータセットを継続的に改善できます。\n3. データ変更とモデル変更の分離 # 同じプロンプトで2回実験を実行して結果が異なった場合、バージョンを確認することで原因を切り分けられます：\n同じバージョン → モデル側の問題（APIの非決定性など）\n異なるバージョン → データが変わったため\nこの切り分けができることで、デバッグや改善の方向性を正しく判断できます。CI/CDパイプラインに組み込む際も、バージョン指定によって特定の評価データセットに対する回帰テストが可能になります。\n4. チーム開発での安心感 # 実験を作成するとき、バージョンを指定して「先週時点のデータだけで実験する」という条件を固定できます。チームメンバーがその後ケースを追加・修正しても、自分の実験には影響しません。各メンバーが独立して作業しながら、必要に応じて最新版を取り込むという柔軟な運用ができます。\n使い方 # データセットバージョニングは、LangfuseのUIとSDK/APIの両方から利用できます。\nUIでの操作 # バージョン履歴の確認 # データセット詳細画面から、過去のバージョンを一覧で確認できます。各バージョンには「Copy version timestamp (UTC)」ボタンがあり、SDKやAPIで使用するタイムスタンプをそのままコピーできます。\nバージョン一覧から過去時点のデータセット状態を確認できる 実験実行時のバージョン指定 # 実験を作成する際、バージョン選択ドロップダウンからデータセットのバージョンを指定できます。デフォルトは最新版ですが、過去のバージョンを選択することも可能です。\n実験作成時に使用するデータセットのバージョンを指定できる 選択したバージョンで実験が実行され、使用したバージョンは実験結果のメタデータに自動記録されます。\n実験結果でのバージョン確認 # 実験Run詳細画面には、使用したデータセットのバージョンが表示されます。このバージョン日時はリンクになっており、クリックすると当時のデータセットアイテム一覧に遷移できます。\nRun詳細からワンクリックで当時のデータ一覧に遷移できる SDKでの使用 # SDKを使って実験を自動化している場合も、バージョニングを活用できます。`\nget_dataset()の version パラメータに日時オブジェクトを渡すことで、特定時点のデータセットを取得できます。\nfrom datetime import datetime, timezone from langfuse import get_client langfuse = get_client() # 特定バージョン（2026年1月21日時点）のデータセットを取得 version = datetime(2026, 1, 21, 14, 35, 42, tzinfo=timezone.utc) dataset = langfuse.get_dataset(name=\u0026#34;my-dataset\u0026#34;, version=version) # 取得したバージョンのアイテムで実験を実行 def my_task(*, item, **kwargs): return my_llm_function(item.input) result = dataset.run_experiment( name=\u0026#34;experiment-v1\u0026#34;, task=my_task, ) まとめ # LLMアプリの開発において、評価データのバージョン管理は実験の再現性と正確な分析のために不可欠です。Langfuseのデータセットバージョニング機能を使えば、データの変化とモデルの変化を分離して管理でき、チームでの並行開発も安心して進められます。\nデータセットを更新するたびに「過去の結果と比較できなくなる」不安から解放され、自信を持ってLLMアプリの品質を改善できるようになります。ぜひ実際の開発フローに取り入れてみてください。\n参考リンク # Langfuse Changelog - Dataset Versioning for Experiments ","date":"2026年2月27日","externalUrl":null,"permalink":"/posts/2026-02-27-llm%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E8%A9%95%E4%BE%A1%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3%E7%AE%A1%E7%90%86%E3%81%99%E3%82%8B-langfuse%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%82%BB%E3%83%83%E3%83%88%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%8B%E3%83%B3%E3%82%B0%E3%81%A7%E5%AE%9F%E9%A8%93%E3%81%AE%E5%86%8D%E7%8F%BE%E6%80%A7%E3%82%92%E7%A2%BA%E4%BF%9D%E3%81%99%E3%82%8B/","section":"Posts","summary":"LLMアプリケーションの開発で、こんな経験はないでしょうか。\n「先週と同じ条件で実験したいのに、データセットを更新したから再現できない…」\n「評価データを改善したいけど、過去の結果と比較できなくなるのが怖い…」\n","title":"LLMアプリの評価データをバージョン管理する - Langfuseのデータセットバージョニングで実験の再現性を確保する","type":"posts"},{"content":"","date":"2026年2月27日","externalUrl":null,"permalink":"/tags/llm%E8%A9%95%E4%BE%A1/","section":"タグ","summary":"","title":"LLM評価","type":"tags"},{"content":"","date":"2026年2月27日","externalUrl":null,"permalink":"/tags/%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3%E7%AE%A1%E7%90%86/","section":"タグ","summary":"","title":"バージョン管理","type":"tags"},{"content":"","date":"2026年2月27日","externalUrl":null,"permalink":"/tags/%E3%83%87%E3%83%BC%E3%82%BF%E3%82%BB%E3%83%83%E3%83%88/","section":"タグ","summary":"","title":"データセット","type":"tags"},{"content":"こんにちは。ガオ株式会社の黒澤です。\nLangfuse v3.153.0 で [PR #11861 ](https://github.com/langfuse/langfuse/pull/11861 ) がマージされ、LLM-as-a-Judge を Observation 単位で実行できるようになりました。本記事ではその背景と使い方をまとめます。\n課題：Trace 全体への評価では「どこが悪いか」がわからない # LLM アプリの評価で、こんな状況に陥ったことはありませんか。\nRAG アプリの LLM as a Judge スコアが下がった。でも、ドキュメント検索が悪いのか、回答生成が悪いのか、判断できない。\nこれは、従来の Langfuse の評価機能が Trace（エンドツーエンドのリクエスト全体） を評価単位としていたためです。\nBefore：Evaluator は Trace 全体にしか設定できなかった # Langfuse で LLM as a Judge Evaluator を作成すると、評価対象は Trace 全体の最終出力のみでした。\n▲ Run on に Live Tracesを選択すると、Trace全体の評価となる たとえば以下のような RAG パイプラインでは、retrieve（検索）と llm（生成）が別々の Observation として存在しますが、評価できるのは最終出力だけでした。\nTrace: ユーザーの質問 ├─ Span: retrieve ← ここは評価できなかった └─ Generation: llm ← ここの最終出力だけが評価対象 スコアが低くても「検索が悪いのか、生成が悪いのか」の切り分けは、ログを手で読むしかありませんでした。\nAfter：個々の Observation を評価ターゲットに指定できるようになった # 今回のアップデートで、Evaluator の設定画面に Observation をターゲットにするオプション が追加されました。\n▲ 例：retrieve 用。Target で Live Observations を選択し、Where で Type=SPAN, Name=retrieve を指定 ▲ 例：llm 用。Type=GENERATION, Name=llm でフィルタ。retrieve と llm で別々の Evaluator を設定できる ※ 以上は RAG パイプラインの一例。実際の Observation 名や Type はアプリの実装に合わせて設定する。\n検索と生成を Langfuse へ送信する簡単なサンプルソースです。ここでは例示として、実際の検索先や LLM は呼ばず、固定文言を Langfuse へ送信します。\nソースコード\n\u0026#34;\u0026#34;\u0026#34; Trace 構造: Trace: rag-pipeline ├─ Span: retrieve ← Context Relevance の評価対象 └─ Generation: llm ← Answer Relevance の評価対象 \u0026#34;\u0026#34;\u0026#34; import os try: from dotenv import load_dotenv load_dotenv() except ImportError: pass from langfuse import get_client, propagate_attributes def retrieve_documents(query: str) -\u0026gt; list[str]: \u0026#34;\u0026#34;\u0026#34;ドキュメント検索をシミュレート（モック）\u0026#34;\u0026#34;\u0026#34; mock_docs = [ \u0026#34;Python は 1991年に Guido van Rossum によって開発されました。\u0026#34;, \u0026#34;Langfuse は LLM アプリケーションの観測・評価プラットフォームです。\u0026#34;, ] return mock_docs[:2] def generate_answer(query: str, context: list[str]) -\u0026gt; str: \u0026#34;\u0026#34;\u0026#34;LLM による回答生成をシミュレート（モック）\u0026#34;\u0026#34;\u0026#34; context_text = \u0026#34;\\n\u0026#34;.join(context) return f\u0026#34;検索された文脈：{context_text[:50]}... に基づき、{query} への回答を生成します。\u0026#34; def run_rag_pipeline(query: str) -\u0026gt; str: langfuse = get_client() with langfuse.start_as_current_observation( as_type=\u0026#34;span\u0026#34;, name=\u0026#34;rag-pipeline\u0026#34;, input={\u0026#34;query\u0026#34;: query} ) as root_span: with propagate_attributes(tags=[\u0026#34;observation-eval-sample\u0026#34;]): with langfuse.start_as_current_observation( as_type=\u0026#34;span\u0026#34;, name=\u0026#34;retrieve\u0026#34;, input={\u0026#34;query\u0026#34;: query} ) as retrieve_span: documents = retrieve_documents(query) retrieve_span.update(output={\u0026#34;documents\u0026#34;: documents, \u0026#34;count\u0026#34;: len(documents)}) with langfuse.start_as_current_observation( as_type=\u0026#34;generation\u0026#34;, name=\u0026#34;llm\u0026#34;, model=\u0026#34;gpt-4o-mini\u0026#34;, input={\u0026#34;query\u0026#34;: query, \u0026#34;context\u0026#34;: documents}, ) as llm_gen: answer = generate_answer(query, documents) llm_gen.update(output={\u0026#34;answer\u0026#34;: answer}, usage_details={\u0026#34;input_tokens\u0026#34;: 50, \u0026#34;output_tokens\u0026#34;: 30}) root_span.update(output={\u0026#34;answer\u0026#34;: answer}) langfuse.flush() return answer if __name__ == \u0026#34;__main__\u0026#34;: answer = run_rag_pipeline(\u0026#34;Python はいつ開発されましたか？\u0026#34;) print(answer) これにより、先ほどの RAG パイプラインに対して次のような評価設計が可能になります。\n評価対象 Observation 評価軸 検出したいもの retrieve Span Context Relevance 検索結果がクエリに関連しているか llm Generation Answer Relevance 回答がユーザーの質問に答えているか Trace 詳細画面でも変化がわかる # 設定後、Trace の詳細画面を開くと、 各 Observation の行にもスコアが表示される ようになります。\n▲ 各 Observation 行にスコアが付いている。一目で「どのステップが悪いか」が把握できる 一目で「どのステップのスコアが低いか」が把握できます。たとえば：\nContext Relevance: 0.50（検索結果の精度が低い） Answer Relevance: 1.00（生成自体は質問に答えている） この場合、問題は検索ロジック側にあると判断できます。\nまとめ：評価の粒度がアーキテクチャの複雑さに追いついた # Before（Trace レベル） After（Observation レベル） 評価対象 最終出力のみ 各中間ステップも評価可能 問題の原因特定 最終出力のスコアは取れるが、複数ステップがある場合は「どのステップが悪いか」の切り分けが困難 各ステップのスコアで特定可能 向いている構成 シンプルな LLM 呼び出し RAG・エージェント・多段階チェーン アプリが複雑になるほど、「何かがおかしい」の検知だけでは不十分です。Observation レベルの評価は、その「どこがおかしいか」をデータとして取り出す手段です。\n","date":"2026年2月26日","externalUrl":null,"permalink":"/posts/2026-02-26-langfuse-%E3%81%AE-observation-%E3%83%AC%E3%83%99%E3%83%AB%E8%A9%95%E4%BE%A1-%E3%81%A9%E3%81%AE%E3%82%B9%E3%83%86%E3%83%83%E3%83%97%E3%81%8C%E6%82%AA%E3%81%84%E3%81%AE%E3%81%8B-%E3%82%92%E3%82%B9%E3%82%B3%E3%82%A2%E3%81%A7%E7%89%B9%E5%AE%9A%E3%81%A7%E3%81%8D%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB%E3%81%AA%E3%81%A3%E3%81%9F/","section":"Posts","summary":"こんにちは。ガオ株式会社の黒澤です。\nLangfuse v3.153.0 で [PR #11861 ](https://github.com/langfuse/langfuse/pull/11861 ) がマージされ、LLM-as-a-Judge を Observation 単位で実行できるようになりました。本記事ではその背景と使い方をまとめます。\n","title":"Langfuse の Observation レベル評価：「どのステップが悪いのか」をスコアで特定できるようになった","type":"posts"},{"content":"","date":"2026年2月26日","externalUrl":null,"permalink":"/tags/llm-as-a-judge/","section":"タグ","summary":"","title":"LLM-as-a-Judge","type":"tags"},{"content":"","date":"2026年2月26日","externalUrl":null,"permalink":"/tags/rag/","section":"タグ","summary":"","title":"RAG","type":"tags"},{"content":"","date":"2026年2月4日","externalUrl":null,"permalink":"/tags/agent-development-kit/","section":"タグ","summary":"","title":"Agent Development Kit","type":"tags"},{"content":"","date":"2026年2月4日","externalUrl":null,"permalink":"/tags/google-adk/","section":"タグ","summary":"","title":"Google ADK","type":"tags"},{"content":"Google ADK（Agent Development Kit）のトレースに Langfuse のプロンプト情報を紐付ける方法を解説します。これにより、プロンプトごとのコスト・レイテンシ分析や A/B テストが可能になります。\nなぜ紐付けが必要なのか # 紐付けができないと何が困るか # ・プロンプトごとのコスト・レイテンシを分析できない\n・A/B テストでプロンプトバージョンを比較できない\n・どのプロンプトが本番で使われているか追跡できない\nGoogleADKInstrumentor だけでは不十分 # GoogleADKInstrumentor を使えば、Google ADK のトレースを Langfuse に送信できます。 from openinference.instrumentation.google_adk import GoogleADKInstrumentorGoogleADKInstrumentor().instrument() しかし、これだけではプロンプト紐付けがされません。\nLangfuse ダッシュボード └── call_llm (GENERATION) └── promptName: null ← 紐づいていない 他のフレームワークとの違い # Langchain や OpenAI SDK では、Langfuse が公式にラッパーやコールバックを提供しており、簡単にプロンプト紐付けができます。\nフレームワーク プロンプト紐付け方法 LangChain Langfuse公式Callbackがある OpenAI SDK Langfuse公式ラッパーがある（langfuse_prompt引数） Google ADK OTel/OpenInference経由 → prompt属性の概念がない しかし、Google ADK は OpenTelemetry + OpenInference 経由でトレースを送信するため、Langfuse の標準的な方法では紐付けができません。この問題はGitHub Issue #7937 で議論されており、本記事ではその回避策を解説します。\n仕組み # ■ 解決のポイント # Langfuse がプロンプトを認識するには、LLM 呼び出しのスパンに以下の属性を設定する必要があります。\n・langfuse.prompt.name - プロンプト名\n・langfuse.prompt.version - プロンプトバージョン\n本記事では SpanProcessor と ContextVar を組み合わせて、call_llm スパンにこれらの属性を自動付与します。\n■ 全体の流れ # instruction 関数内でプロンプトを取得\n└─ ContextVar にプロンプト情報を保存\ncall_llm スパンが開始される\n└─ SpanProcessor.on_start() が呼ばれる\n└─ ContextVar からプロンプト情報を取得\n└─ スパンに属性を設定\nLangfuse がプロンプトリンクを認識\n└─ ダッシュボードで分析可能に\n■ なぜ SpanProcessor を使うのか # Google ADK は LLM 呼び出しを別スレッド（並行処理）で実行します。通常の OTel Context 伝播では、プロンプト情報を LLM 呼び出しに渡せません。\nSpanProcessor を使うと、スパン作成時に直接属性を設定できるため、この問題を回避できます。ContextVar はスレッドをまたいで値を保持できるので、組み合わせて使います。\n手順 # ■ 必要なパッケージ\npip install langfuse google-adk openinference-instrumentation-google-adk opentelemetry-sdk python-dotenv ■ 完全なコード例\n以下のコードをコピペで動作します。\nimport asyncio from contextvars import ContextVar from dotenv import load_dotenv from google.adk.agents import Agent from google.adk.runners import Runner from google.adk.sessions import InMemorySessionService from google.genai import types from langfuse import get_client from openinference.instrumentation.google_adk import GoogleADKInstrumentor from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider, SpanProcessor # ============================================================ # Langfuse プロンプト紐付け用コード # ============================================================ prompt_info_var = ContextVar(\u0026#34;prompt_info\u0026#34;, default=None) class LangfusePromptProcessor(SpanProcessor): \u0026#34;\u0026#34;\u0026#34;call_llm スパンにプロンプト情報を付与する\u0026#34;\u0026#34;\u0026#34; def on_start(self, span, parent_context=None): if hasattr(span, \u0026#34;name\u0026#34;) and span.name == \u0026#34;call_llm\u0026#34;: prompt_info = prompt_info_var.get() if prompt_info: span.set_attribute(\u0026#34;langfuse.prompt.name\u0026#34;, prompt_info[\u0026#34;name\u0026#34;]) span.set_attribute(\u0026#34;langfuse.prompt.version\u0026#34;, prompt_info[\u0026#34;version\u0026#34;]) def on_end(self, span): pass def shutdown(self): pass def force_flush(self, timeout_millis=30000): return True # ============================================================ # アプリケーションコード # ============================================================ async def main(): load_dotenv() langfuse = get_client() # 1. TracerProvider を作成し、SpanProcessor を登録 provider = TracerProvider() provider.add_span_processor(LangfusePromptProcessor()) trace.set_tracer_provider(provider) # 2. Google ADK のトレースを有効化 GoogleADKInstrumentor().instrument() # 3. instruction 関数を定義（ここでプロンプトを紐付け） def get_instruction(ctx): prompt = langfuse.get_prompt(\u0026#34;my_agent_instruction\u0026#34;) prompt_info_var.set({\u0026#34;name\u0026#34;: prompt.name, \u0026#34;version\u0026#34;: prompt.version}) return prompt.compile() # 4. Agent を作成 agent = Agent( name=\u0026#34;my_agent\u0026#34;, model=\u0026#34;gemini-2.5-flash\u0026#34;, instruction=get_instruction, # 関数を渡す tools=[], ) # 5. セッションを作成して実行 session_service = InMemorySessionService() await session_service.create_session( app_name=\u0026#34;my_app\u0026#34;, user_id=\u0026#34;user-1\u0026#34;, session_id=\u0026#34;session-1\u0026#34; ) runner = Runner(agent=agent, app_name=\u0026#34;my_app\u0026#34;, session_service=session_service) user_msg = types.Content(role=\u0026#34;user\u0026#34;, parts=[types.Part(text=\u0026#34;Hello\u0026#34;)]) for event in runner.run(user_id=\u0026#34;user-1\u0026#34;, session_id=\u0026#34;session-1\u0026#34;, new_message=user_msg): if event.is_final_response(): print(event.content.parts[0].text) # 6. トレースデータを送信 langfuse.flush() if __name__ == \u0026#34;__main__\u0026#34;: asyncio.run(main()) ■ 環境変数（.env）\nLANGFUSE_PUBLIC_KEY=pk-lf-xxx LANGFUSE_SECRET_KEY=sk-lf-xxx LANGFUSE_BASE_URL=https://xxx GOOGLE_API_KEY=xxx ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n注意点・よくある落とし穴 # TracerProvider の登録順序 SpanProcessor は GoogleADKInstrumentor().instrument() の「前に」登録する必要があります。\n【正しい順序】\nprovider = TracerProvider() provider.add_span_processor(LangfusePromptProcessor()) # 先に登録 trace.set_tracer_provider(provider) GoogleADKInstrumentor().instrument() # 後から計装 【動かない順序】\nGoogleADKInstrumentor().instrument() provider.add_span_processor(...) # 既に計装済みで反映されない instruction には関数を渡す プロンプトを動的に取得するには、instruction に「関数」を渡す必要があります。\n【正しい書き方】\ndef get_instruction(ctx): prompt = langfuse.get_prompt(\u0026#34;my_prompt\u0026#34;) prompt_info_var.set({\u0026#34;name\u0026#34;: prompt.name, \u0026#34;version\u0026#34;: prompt.version}) return prompt.compile() agent = Agent(instruction=get_instruction, ...) 【動かない書き方】\nagent = Agent(instruction=\u0026#34;You are a helpful assistant\u0026#34;, ...) プロンプトリンクは Generation 単位 Langfuse の仕様により、プロンプトリンクは Generation（LLM 呼び出し）スパンにのみ関連付けられます。トレース全体やエージェント実行単位への紐付けはできません。\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n結果 # 設定が成功すると、Langfuse ダッシュボードで以下が確認できます。\n・Generation スパンにプロンプトリンクが表示される\n・Prompt Metrics でコスト・レイテンシ分析が可能\n・プロンプトバージョンごとの比較ができる\nLangfuse ダッシュボード（設定後） └── call_llm (GENERATION) └── promptName: \u0026quot;my_agent_instruction\u0026quot; ← 紐づいた！ └── promptVersion: 1 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n参考リンク # Langfuse OpenTelemetry Docs Langfuse Prompt Management Google ADK OpenInference Google ADK この問題の議論Issues 公式のプロンプト紐付けドキュメント ","date":"2026年2月4日","externalUrl":null,"permalink":"/posts/2026-02-04-google-adk%E3%81%A7%E4%BD%9C%E3%81%A3%E3%81%9F%E3%82%A8%E3%83%BC%E3%82%B8%E3%82%A7%E3%83%B3%E3%83%88%E3%81%AB-langfuse%E3%81%AE%E3%83%88%E3%83%AC%E3%83%BC%E3%82%B9%E3%81%AB%E3%83%97%E3%83%AD%E3%83%B3%E3%83%97%E3%83%88%E3%82%92%E7%B4%90%E4%BB%98%E3%81%91%E3%82%8B%E6%96%B9%E6%B3%95/","section":"Posts","summary":"Google ADK（Agent Development Kit）のトレースに Langfuse のプロンプト情報を紐付ける方法を解説します。これにより、プロンプトごとのコスト・レイテンシ分析や A/B テストが可能になります。\n","title":"Google ADKで作ったエージェントに Langfuseのトレースにプロンプトを紐付ける方法","type":"posts"},{"content":"","date":"2026年2月4日","externalUrl":null,"permalink":"/tags/python/","section":"タグ","summary":"","title":"Python","type":"tags"},{"content":"","date":"2026年2月4日","externalUrl":null,"permalink":"/tags/%E3%83%97%E3%83%AD%E3%83%B3%E3%83%97%E3%83%88%E7%AE%A1%E7%90%86/","section":"タグ","summary":"","title":"プロンプト管理","type":"tags"},{"content":"","date":"2026年1月27日","externalUrl":null,"permalink":"/tags/chatml/","section":"タグ","summary":"","title":"ChatML","type":"tags"},{"content":"更新日：2月2日\n本記事では、LangfuseのTrace詳細画面で利用できる主要な特殊レンダリングパターンを解説します。これらのパターンを活用することで、トレース情報をより視覚的かつ構造的に表示できます。\n対象読者\nLangfuseでLLMアプリケーションのトレースを取得している開発者 Langfuseのトレースのアナリスト・手動評価者 はじめに # 複雑になるトレース構造 # 近年のLLMは多機能化・高性能化が進み、1つのトレースに様々な情報を詰め込むことが一般的になってきました。画像認識、音声処理、ツール呼び出し、推論過程など、単純なテキスト入出力だけでは済まないケースが増えています。\nしかし、情報量が増えれば増えるほど、本来可視化を目的としているはずのトレース画面が逆に見にくくなってしまう、という問題が発生します。大量のJSON文字列が羅列されるだけでは、重要な情報を見逃したり、デバッグに時間がかかったりします。\nそこで役立つのが、Langfuseが提供する特殊レンダリング機能です。適切なJSON構造を使うことで、同じデータをより整理された形で表示でき、開発効率が大きく向上します。\n特殊レンダリングが用意されている理由 # Langfuseのトレースは、OpenAI、Anthropic、Geminiといった主要なLLMプロバイダーのレスポンススキーマに対応しています。各プロバイダーが採用している独自のJSON構造を自動的に認識し、適切な形式で画面表示するため、開発者はプロバイダー固有のフォーマットをそのまま記録できます。\n以降では、実際にどのようなJSON構造が特殊なレンダリング対象となるのか、主要4パターンを見ていきます。\nパターン1: ChatML形式の会話履歴 # ChatML（Chat Markup Language）は、LLMとの会話履歴を構造化して記録する標準的な形式です。Langfuseは複数のChatML記述方法に対応しています。\nChatML形式のトレース表示 基本構造 # 配列の各要素がroleとcontentキーを持つオブジェクトであれば、自動的にChatML形式として認識されます。\n[ {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;こんにちは\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;こんにちは！何かお手伝いできることはありますか？\u0026#34;} ] 対応するroleと表示 # 画面上では、roleに応じて異なるラベルと背景色が適用されます。\nuser: \u0026ldquo;User\u0026quot;と表示 assistant: \u0026ldquo;Assistant\u0026quot;と表示（アクセント背景色で区別） system: \u0026ldquo;System\u0026quot;と表示（アクセント背景色で区別） tool: \u0026ldquo;Tool\u0026quot;と表示 function: \u0026ldquo;Function\u0026quot;と表示 developer: \u0026ldquo;Developer\u0026quot;と表示 カスタム表示名 # nameフィールドを追加すると、roleではなくそちらが優先表示されます。\n{ \u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;AI Agent Alpha\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;処理を開始します\u0026#34; } この場合、画面には\u0026quot;AI Agent Alpha\u0026quot;と表示されます。\nラッパー形式 # 以下のような構造にも対応しています。\n{ \u0026#34;messages\u0026#34;: [ {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;質問です\u0026#34;} ] } ネストされた配列形式も認識されます。\n[[{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;質問です\u0026#34;}]] パターン2: ツール定義と実行結果 # Function CallingやTool Useと呼ばれる機能を使う際、Langfuseはツールのスキーマ定義と実行ログを視覚的に表示します。\nツール定義と実行結果のトレース表示 ツールスキーマの定義 # メッセージにtoolsフィールドを追加すると、画面上部に「Tools」セクションが表示されます。\n{ \u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;東京の天気を調べて\u0026#34;, \u0026#34;tools\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;get_weather\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;指定した都市の天気情報を取得します\u0026#34;, \u0026#34;parameters\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;city\u0026#34;: {\u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;都市名\u0026#34;} }, \u0026#34;required\u0026#34;: [\u0026#34;city\u0026#34;] } } ] } ツールの呼び出し記録 # アシスタントがツールを呼び出した際の記録は、tool_callsフィールドで表現します。\n{ \u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: null, \u0026#34;tool_calls\u0026#34;: [ { \u0026#34;id\u0026#34;: \u0026#34;call_abc123\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;get_weather\u0026#34;, \u0026#34;arguments\u0026#34;: \u0026#34;{\\\u0026#34;city\\\u0026#34;: \\\u0026#34;東京\\\u0026#34;}\u0026#34; } ] } 画面上では、メッセージ内にTool Invocationカードが表示されます。\nツールからの返答 # ツール実行結果は、role: \u0026ldquo;tool\u0026quot;のメッセージとして記録します。\n{ \u0026#34;role\u0026#34;: \u0026#34;tool\u0026#34;, \u0026#34;tool_call_id\u0026#34;: \u0026#34;call_abc123\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;{\\\u0026#34;temperature\\\u0026#34;: 22, \\\u0026#34;condition\\\u0026#34;: \\\u0026#34;晴れ\\\u0026#34;}\u0026#34; } OpenAI形式のネスト構造 # OpenAIのChat Completions APIで使われるネスト構造にも対応しています。\n{ \u0026#34;tool_calls\u0026#34;: [ { \u0026#34;id\u0026#34;: \u0026#34;call_abc123\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;function\u0026#34;, \u0026#34;function\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;get_weather\u0026#34;, \u0026#34;arguments\u0026#34;: \u0026#34;{\\\u0026#34;city\\\u0026#34;: \\\u0026#34;東京\\\u0026#34;}\u0026#34; } } ] } パターン3: マルチモーダルコンテンツ # マルチモーダルファイルのアップロード方法や全般的な使い方については、こちら の記事で詳しく解説されていますので、そちらをご参照ください。\nここでは、特殊レンダリングに必要なJSON構造について解説します。\n基本構造 # マルチモーダルコンテンツとして認識されるには、contentが配列で、各要素にtypeキーが必要です。\n{ \u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: [ {\u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;この画像について教えて\u0026#34;}, {\u0026#34;type\u0026#34;: \u0026#34;image_url\u0026#34;, \u0026#34;image_url\u0026#34;: {\u0026#34;url\u0026#34;: \u0026#34;https://example.com/image.png\u0026#34;}} ] } 画像の表示（type: \u0026ldquo;image_url\u0026rdquo;） # 画像を表示するには、以下のような構造を使います。\n{ \u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: [ {\u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;この画像を説明して\u0026#34;}, { \u0026#34;type\u0026#34;: \u0026#34;image_url\u0026#34;, \u0026#34;image_url\u0026#34;: { \u0026#34;url\u0026#34;: \u0026#34;https://example.com/image.png\u0026#34;, } } ] } 対応しているURL形式\n形式 例 HTTP/HTTPS URL https://example.com/image.png Base64 Data URI data:image/png;base64,iVBORw0KGgo... Langfuseメディアトークン `@@@langfuseMedia:type=image/jpeg 対応している画像形式: PNG, JPEG, JPG, GIF, WebP\n画面上では、画像が実際に表示され、クリックでリサイズ可能です。\n画像のトレース表示 音声入力（type: \u0026ldquo;input_audio\u0026rdquo;） # 音声データを入力として記録する場合は、以下の構造を使います。\n{ \u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;input_audio\u0026#34;, \u0026#34;input_audio\u0026#34;: { \u0026#34;data\u0026#34;: \u0026#34;@@@langfuseMedia:type=audio/mp3|id=\u0026lt;uuid\u0026gt;|source=base64@@@\u0026#34; } } ] } 音声データは、Langfuseメディア参照形式で記録します。画面上ではオーディオプレーヤーが表示されます。\n音声出力（audioフィールド） # アシスタントが音声で返答した場合は、メッセージにaudioフィールドを追加します。\n{ \u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;音声でお答えします\u0026#34;, \u0026#34;audio\u0026#34;: { \u0026#34;data\u0026#34;: \u0026#34;@@@langfuseMedia:type=audio/mp3|id=\u0026lt;uuid\u0026gt;|source=base64@@@\u0026#34;, \u0026#34;transcript\u0026#34;: \u0026#34;これは音声の文字起こしです。\u0026#34; } } 画面には、文字起こしテキストとオーディオプレーヤーが両方表示されます。\n音声のトレース表示 その他のファイル（PDF等） # Langfuseメディアトークンを使うことで、PDFなど任意のファイルタイプも添付できます。\n@@@langfuseMedia:type=application/pdf|id=\u0026lt;uuid\u0026gt;|source=base64@@@ 画面上では、ファイルアイコンと表示リンクが表示されます。\nその他のファイルのトレース表示 パターン4: 推論過程（Thinking Block） # 一部のLLMは、最終的な回答を生成する前の推論過程を出力します。Langfuseはこれを折りたたみ可能な「Thinking」ブロックとして表示します。この特殊レンダリングはv3.148.0から搭載されています。\n標準形式（Anthropic形式） # { \u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;答えは4です。\u0026#34;, \u0026#34;thinking\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;thinking\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;これは基本的な算数問題です。2+2=4と計算しました。\u0026#34;, \u0026#34;summary\u0026#34;: \u0026#34;計算結果は4\u0026#34; } ] } summaryは省略可能です。\n推論過程のトレース表示 リダクテッド形式（暗号化された推論内容） # Anthropic APIでは、推論内容が暗号化されて返される場合があります。\n{ \u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;回答内容\u0026#34;, \u0026#34;redacted_thinking\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;redacted_thinking\u0026#34;, \u0026#34;data\u0026#34;: \u0026#34;\u0026lt;encrypted blob\u0026gt;\u0026#34; } ] } OpenAI Responses API形式（自動変換） # OpenAIの新しいResponses APIでは、推論が別メッセージとしてoutput配列内に記録されます。Langfuseはこれを自動的にthinkingフィールドへ変換します。\n{ \u0026#34;output\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;reasoning\u0026#34;, \u0026#34;content\u0026#34;: [{\u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;計算を行っています...\u0026#34;}], \u0026#34;summary\u0026#34;: [{\u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;結果は4\u0026#34;}] }, { \u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;4です。\u0026#34; } ] } Gemini形式（自動変換） # Gemini APIでは、thought: trueというフラグで推論部分を示します。\n{ \u0026#34;parts\u0026#34;: [ {\u0026#34;text\u0026#34;: \u0026#34;計算中...\u0026#34;, \u0026#34;thought\u0026#34;: true}, {\u0026#34;text\u0026#34;: \u0026#34;4です。\u0026#34;} ] } この形式も内部的にthinkingフィールドへ変換され、画面上で折りたたみ可能なブロックとして表示されます。\nよくある間違いとトラブルシューティング # 間違い1: roleの指定ミス # ChatML形式では、roleとcontentの両方が必要です。どちらか一方だけでは認識されません。\n{\u0026#34;content\u0026#34;: \u0026#34;こんにちは\u0026#34;} // 認識されない {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;こんにちは\u0026#34;} // 正しい 間違い2: MediaURIをどこかに入れれば自動レンダリングされると思い込む # Langfuseの公式ドキュメント を読むと、「MediaURIをtraceのどこかに入れれば、いい感じにレンダリングされる」という印象を受けるかもしれません。\nしかし、実際には指定されたJSON形式に従わないとレンダリングされません。例えば、以下のような単純な文字列として埋め込んでも、画像として表示されません。\n{ \u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;@@@langfuseMedia:type=image/jpeg|id=xxx|source=base64@@@\u0026#34; } 正しくないJSON形式のトレース 正しくは、マルチモーダルコンテンツの構造に従う必要があります。\n{ \u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: [ {\u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;画像の説明\u0026#34;}, { \u0026#34;type\u0026#34;: \u0026#34;image_url\u0026#34;, \u0026#34;image_url\u0026#34;: { \u0026#34;url\u0026#34;: \u0026#34;@@@langfuseMedia:type=image/jpeg|id=xxx|source=base64@@@\u0026#34; } } ] } 今後への期待 # これらの特殊レンダリング機能は非常に便利ですが、改善の余地もあります。\n特にマルチモーダルファイルの添付については、もっと柔軟な仕様にしてほしいというのが個人的な願いです。現状では、前述の通り厳密なJSON構造に従わないとレンダリングされませんが、MediaURIをinput、output、metadataのどこかに含めるだけで自動的に認識されるような仕組みになれば、開発者の負担が大きく軽減されるでしょう。\n公式ドキュメントの記述からは「どこに入れても大丈夫」という印象を受けますが、実際には構造化が必要です。この点が改善されることを期待しています。\nまとめ # LLMアプリケーションの開発において、トレース情報の可視化は非常に重要です。情報量が増えれば増えるほど、適切な構造化と表示方法が求められます。\n本記事で紹介したLangfuseの特殊レンダリングパターンを活用することで、以下のメリットが得られます。\n複雑なトレース情報が整理され、一目で理解できるようになる デバッグ時に重要な情報へ素早くアクセスできる チーム内でのトレース共有がスムーズになる 主要LLMプロバイダーのレスポンス形式をそのまま記録できる ただし、本記事で紹介したように、指定のJSON構造でないと特殊レンダリングされません。この点には注意してください。\n今後、Langfuseがさらに柔軟なレンダリング機能を提供してくれることを期待しつつ、今あるレンダリングの仕様を最大限活用して、より効率的なLLM開発を進めていきましょう。\n参考リンク # Langfuse公式ドキュメント - Improved ChatML rendering Changelog Langfuse公式ドキュメント - Multi Modality 弊社記事 - Langfuseのマルチモーダル対応:画像・音声ファイルのトレース添付機能がGAに ","date":"2026年1月27日","externalUrl":null,"permalink":"/posts/2026-01-27-langfuse-trace%E8%A9%B3%E7%B4%B0%E7%94%BB%E9%9D%A2%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8B%E7%89%B9%E6%AE%8A%E3%83%AC%E3%83%B3%E3%83%80%E3%83%AA%E3%83%B3%E3%82%B0%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3%E3%82%AC%E3%82%A4%E3%83%89/","section":"Posts","summary":"更新日：2月2日\n本記事では、LangfuseのTrace詳細画面で利用できる主要な特殊レンダリングパターンを解説します。これらのパターンを活用することで、トレース情報をより視覚的かつ構造的に表示できます。\n","title":"Langfuse Trace詳細画面における特殊レンダリングパターンガイド","type":"posts"},{"content":"","date":"2026年1月27日","externalUrl":null,"permalink":"/tags/openai/","section":"タグ","summary":"","title":"OpenAI","type":"tags"},{"content":"","date":"2026年1月27日","externalUrl":null,"permalink":"/tags/%E3%83%88%E3%83%AC%E3%83%BC%E3%82%B9/","section":"タグ","summary":"","title":"トレース","type":"tags"},{"content":"","date":"2026年1月27日","externalUrl":null,"permalink":"/tags/%E3%83%9E%E3%83%AB%E3%83%81%E3%83%A2%E3%83%BC%E3%83%80%E3%83%AB/","section":"タグ","summary":"","title":"マルチモーダル","type":"tags"},{"content":"Geminiの性能向上によりOCRは実用的になりましたが、高精度を目指すならプロンプト調整は必須です。しかし、調整のたびに画像と結果を目視で見比べるのは、手間がかかりミスも誘発します。\nそこで本記事では、Langfuseを使ってこの作業を自動化します。「評価」と聞くと難しそうですが、今回は複雑な指標や設定を使わず、かつ、チーム運用は一旦忘れ、まずは 「自分のPC上で、正解データと一致するか」 だけをチェックする気軽な構成を目指します。\nなお、プロンプト改善のサイクルを回すことが目的なので、今回は繰り返しテストに適したDataset Run 機能を利用します。\nDatasetの準備 # Dataset Runを実行するには、データセットの作成が必要です。\n※ 基本的な作成方法については、以前の記事（Langfuseデータセット構築ガイド：UI・CSV・SDKの徹底比較 ）にて紹介しています。\nしかし、ここで一つ問題が発生します。\n期待する結果の値（Expected Output）はテキストなので問題なく登録できますが、こちらの記事 でも言及している通り、 2026年1月現在、LangfuseのDataset Itemsはマルチモーダルコンテンツ（画像やPDFの直接保持）をサポートしていません。\nまた、Web UIから実行する場合のDataset Run機能も、同様にマルチモーダルには未対応です。\nそこで今回は、SDK を使ってDataset Runを実行するアプローチをとります。\n「スクリプトを書く必要がある」と聞くと手間に感じるかもしれませんが、Web上で完結させないことによるメリットもあります。\nスクリプトが動く環境に画像ファイルを置いておけば、Langfuseに画像実体をアップロードする必要や、Amazon S3 や Google Cloud Storageなどのストレージサービスを準備する必要も無く、LLMに投げる直前にローカルファイルを読みこむだけで済むため、構成がシンプルになります。\nデータセットの構造 # ファイルの実体はローカルに置きますが、「どのデータセット項目が、どの画像ファイルに対応するか」を紐付ける必要があります。\n今回は手軽さを優先し、ひとまず Datasetの input にはファイル名（パス）だけを記述することにします。\n例：\nこの形式なら、実ファイルを含める必要が無いため、CSVを使った一括インポートなど、好きな方法でDataset Itemsを登録できます。\n評価データとスクリプトの準備 # ディレクトリ構成イメージ # 今回は以下の画像ファイルを評価にかけます。\nディレクトリ構成は以下の通りです。\n├── ocr.py # 今回作成するスクリプト └── data/ # 画像置き場 ├── image_001.png ├── image_002.png └── ... 評価データ # 今回はテスト用に以下を準備しました。\nテスト用画像ファイル：Nano Banana Proを用いて、適当なレシート画像を作成 Dataset：テストデータ（csv）をLangfuseのWeb UIから「Import CSV」で一括登録 テスト用画像ファイル\nテストデータ（csv）\nExpected Output に shop_name、date、price をマッピングしています。\ninput shop_name date price image001.png DAILY STORE 2024/01/25 780 image002.png スーパーABC 2024/01/26 1098 image003.png COFFEE STAND 2024/01/27 1600 実際のデータセット（一部）\nスクリプトの準備 # 今回は手軽にやるため、処理内容・評価関数共に最低限の作りにします。\nLLMは Vertex AI（Gemini 2.5 Flash）を利用し、評価関数としては「店舗名の取得が正常に行えているか？」のチェックのみを実施します。\nまた、プロンプトの改善を容易にするため、プロンプトはコードへの記述ではなく、Langfuseで管理する形にしました。\nimport os from dotenv import load_dotenv from langfuse import Langfuse, Evaluation from google import genai import json # 設定（APIキー等は環境変数から読み込み） load_dotenv() langfuse = Langfuse() client = genai.Client( project=os.getenv(\u0026#34;GOOGLE_CLOUD_PROJECT\u0026#34;), location=os.getenv(\u0026#34;GOOGLE_CLOUD_REGION\u0026#34;, \u0026#34;asia-northeast1\u0026#34;), vertexai=True, http_options=genai.types.HttpOptions() ) target_file_path = \u0026#34;data/\u0026#34; prompt = langfuse.get_prompt(\u0026#34;ocr\u0026#34;) dataset = langfuse.get_dataset(name=\u0026#34;ocr_test\u0026#34;) # OCRタスクの定義 def ocr_test(item): # Datasetのinput（ファイル名）から実際のパスを生成 input_filename = item.input compiled_prompt = prompt.compile(input = item.input) # ローカルファイルをバイナリで読み込む file_path = os.path.join(target_file_path, input_filename) with open(file_path, \u0026#34;rb\u0026#34;) as f: file_data = f.read() # Geminiへ送信（画像データ + プロンプト） response = client.models.generate_content( model=\u0026#34;gemini-2.5-flash\u0026#34;, contents=[ genai.types.Part.from_bytes(data=file_data, mime_type=\u0026#39;image/png\u0026#39;), compiled_prompt, ], config=genai.types.GenerateContentConfig( response_mime_type=\u0026#34;application/json\u0026#34;, temperature=0.0, response_schema={ \u0026#34;type\u0026#34;: \u0026#39;ARRAY\u0026#39;, \u0026#34;items\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;OBJECT\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;shop_name\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#39;STRING\u0026#39;, \u0026#34;description\u0026#34;: \u0026#39;店舗名\u0026#39;, }, \u0026#34;date\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#39;STRING\u0026#39;, \u0026#34;description\u0026#34;: \u0026#39;日付\u0026#39;, }, \u0026#34;price\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#39;NUMBER\u0026#39;, \u0026#34;description\u0026#34;: \u0026#39;金額\u0026#39;, } }, \u0026#34;required\u0026#34;: [ \u0026#34;shop_name\u0026#34;, \u0026#34;date\u0026#34;, \u0026#34;price\u0026#34; ], }, } ), ) return response.text # 評価関数（単純一致） def simple_evaluator(*, input, output, expected_output=None, **kwargs): # 実務では output のチェックや、改行コードの削除や正規化（strip等）を推奨 response_data = json.loads(output) check = response_data[0].get(\u0026#34;shop_name\u0026#34;) == expected_output.get(\u0026#34;shop_name\u0026#34;) if check: comment = \u0026#34;Success\u0026#34; else: comment = \u0026#34;False\u0026#34; return Evaluation( name=\u0026#39;output_check\u0026#39;, value=check, metadata={ \u0026#34;expected\u0026#34;: expected_output, \u0026#34;actual\u0026#34;: output }, comment=comment, data_type=\u0026#34;BOOLEAN\u0026#34; ) # status: ACTIVEのみを取得 active_items = [item for item in dataset.items if item.status == \u0026#34;ACTIVE\u0026#34;] langfuse.run_experiment( name=f\u0026#34;{prompt.name}_{prompt.version}\u0026#34;, description=f\u0026#34;Dataset Run from SDK: {prompt.name}_{prompt.version}\u0026#34;, task=ocr_test, data=active_items, evaluators=[simple_evaluator], ) ※ dataset.run_experiment でも Dataset Run は実行できますが、データのフィルタリング（data 引数の指定）が出来ません。そのため、ARCHIVE済みアイテムも含む全件が対象となってしまいます。\n評価について # LLMによる採点（LLM-as-a-judge）を入れた方がそれらしく見えますが、LLMを使う以上コストが発生します。OCRのように正解が明確なタスク（完全一致または簡単なルールベースで比較ができる場合）であれば、LLMより単純な比較の方が精度が見込める場合も多いです。\n今回はスクリプトで評価まで完結させていますが、「あいまいな表現に対しても評価したい」など、別軸でLLM-as-a-judgeも適応させたい場合は、Langfuseの設定で後から追加することも可能です。\n実行結果 # スクリプトを実行すると、Langfuseの画面に結果が反映されます。\n今回は結果がBOOLEANとなる評価を設定しましたが、このようにEvaluatorで設定した項目に対して、True・Falseの件数が表示されます。\n詳細画面を確認すると、このように実際の値の比較や、どのデータが OK/NG だったかの確認も一目で出来ます。\n今回は項目が少なく、評価軸もシンプルなので、Output と Expected Output を見比べれば何が問題かを容易に読み取れます。\n実際の運用では、Metadataなどを活用するとよさそうです。\n上記画面では、結果に対してマウスオーバーで Evaluation にて設定したコメントやMetadataが表示されます。\n吹き出しにマウスオーバーした場合（comment=\\\u0026#34;Success\\\u0026#34;が表示） {...}部分にマウスオーバーした場合（metadataの中身が表示） さらに、過去の実行結果の比較も出来るため、「さっきのプロンプトの方が精度が良かったな…」といった振り返りも容易です。\n差分の比較方法については、LangfuseのExperiments Compare ViewのBaseline機能を解説 もあわせて確認頂くと、より詳細な比較が行えるかと思います。\nまとめ # 今回はとにかく「マルチモーダルな評価」を「とりあえず簡単にやってみる」ことに着目して実験しました。\nこの構成なら、ひとまずLLMが使える状態さえ準備出来れば、スクリプトを実行するだけで、OCRの精度評価をLangfuseに任せてしまえます。\nチーム開発への展望 # 今回は個人のPCで完結させましたが、もしチームで実行したい場合は Amazon S3や\nGoogle Cloud Storageなどのストレージサービスを利用するのがおすすめです。\nスクリプト上のローカルファイルからファイルを取得する処理を、各々のストレージサービスに対応した形に修正すれば、同じように実行できます。\n一例として、**Vertex AI（Gemini） ＋ Google Cloud Storage (GCS)**の構成であれば、スクリプト上でファイルを読み込む必要もありません。権限の設定は必要ですが、GCS上のファイル（gs://...）は直接Geminiに読み込ませることが可能となっています。\n（スクリプトの「ローカルファイルをバイナリで読み込む」ブロックの処理も不要となります。）\n","date":"2026年1月26日","externalUrl":null,"permalink":"/posts/2026-01-26-%E5%85%A5%E9%96%80%E7%B7%A8-langfuse%E3%81%A7%E7%94%BB%E5%83%8Focr%E3%81%AE%E7%B2%BE%E5%BA%A6%E6%A4%9C%E8%A8%BC%E3%82%92%E3%82%B7%E3%83%B3%E3%83%97%E3%83%AB%E3%81%AB%E5%A7%8B%E3%82%81%E3%82%8B%E6%96%B9%E6%B3%95/","section":"Posts","summary":"Geminiの性能向上によりOCRは実用的になりましたが、高精度を目指すならプロンプト調整は必須です。しかし、調整のたびに画像と結果を目視で見比べるのは、手間がかかりミスも誘発します。\n","title":"【入門編】Langfuseで画像OCRの精度検証をシンプルに始める方法","type":"posts"},{"content":"","date":"2026年1月26日","externalUrl":null,"permalink":"/tags/gemini/","section":"タグ","summary":"","title":"Gemini","type":"tags"},{"content":"","date":"2026年1月26日","externalUrl":null,"permalink":"/tags/ocr/","section":"タグ","summary":"","title":"OCR","type":"tags"},{"content":"","date":"2026年1月26日","externalUrl":null,"permalink":"/tags/vertex-ai/","section":"タグ","summary":"","title":"Vertex AI","type":"tags"},{"content":"本ブログでは、Langfuseを複数人で運用する環境下において、「プロンプトを誰でも変更されるのが不安」「うっかり本番用のラベルを動かしてしまった」というヒヤリハットや、「誰でも本番環境を変更できてしまう」というガバナンス上の課題を感じていたチームにとって、必須の機能をご説明します！\nカジュアルに本番プロンプトを変えられてしまう運用はリスクでしかない なぜこの機能が必要なのか？（利用シーン） # 複数人でLLMアプリを開発・運用していると、以下のようなリスクに直面します。この機能は、それらをシステム的に解決します。\n1. 「うっかり事故」の根絶 # 開発中、未検証のプロンプトに誤って production ラベルを付け替えてしまい、本番稼働中のアプリの挙動が突然変わってしまう。\n2. プロンプト版「デプロイ承認フロー」の確立 # 「プロンプトの作成・改善はメンバー全員で行いたいが、本番環境への反映（デプロイ）はリーダーや管理者の承認・操作に限定したい」。 ソフトウェア開発では当たり前のこの権限分離を、プロンプト管理においても実現できます。\n3. QA・検証環境の固定 # テスト中に、対象のプロンプトバージョンが誰かの手によって意図せず変更されるのを防ぎます。検証環境の再現性と安定性を担保するために役立ちます。\n機能の概要と仕組み # Protected Prompt Labels は、特定のラベル（例：production や staging）を文字通り「保護状態」にし、操作できるユーザー権限を制限する機能です。\n導入後の権限コントロール # 管理者が特定のラベルを「Protected（保護）」に設定すると、以下の挙動になります。\n一般メンバー（Member / Viewer） 保護されたラベルを操作（移動・削除・付与）できません。 これにより、現場レベルでの誤操作による本番影響リスクが物理的にゼロになります。 管理者（Admin / Owner） これまで通り、ラベルの操作が可能です。 「メンバーからの変更提案をレビューし、問題なければ管理者がラベルを付け替えて本番反映する」という運用が可能になります。 Langfuse のProtected Prompt Labels設定方法 # Settings -\u0026gt; Protected Prompt Labels で対象の Label を設定するだけです。既に作られているLabelを対象にすることができます。他に操作は必要ありません。\n選択したら Add をするだけで完了です。\n本機能を使った運用フロー例 # 開発・検証: メンバーは新しいプロンプトを作成し、dev や staging ラベルを用いて検証・改善を行う。 レビュー依頼: 検証が完了したら、管理者に本番反映を依頼する。 本番反映（デプロイ）: 管理者は内容をレビューし、問題がなければ production ラベル を新しいバージョンのプロンプトに付与する。 「プロンプトの変更（開発）」と「本番への適用（運用）」を明確に分離することは、複数人で開発をする際における安全なサービス運用の鍵となります。\nいかがでしたでしょうか？ 本機能は TeamプランやEnterpriseプランなどでご利用いただけるものになっていますが、複数人でLangfuseを利用する際には必須のものだと思います。本番環境のサービスに是非利用してみてください。\n","date":"2026年1月25日","externalUrl":null,"permalink":"/posts/2026-01-25-langfuse%E3%81%AE%E3%83%97%E3%83%AD%E3%83%B3%E3%83%97%E3%83%88%E5%A4%89%E6%9B%B4%E5%88%B6%E9%99%90-%E8%A7%A3%E8%AA%AC-protected-prompt-labels/","section":"Posts","summary":"本ブログでは、Langfuseを複数人で運用する環境下において、「プロンプトを誰でも変更されるのが不安」「うっかり本番用のラベルを動かしてしまった」というヒヤリハットや、「誰でも本番環境を変更できてしまう」というガバナンス上の課題を感じていたチームにとって、必須の機能をご説明します！\n","title":"Langfuseのプロンプト変更制限 解説(Protected Prompt Labels)","type":"posts"},{"content":"","date":"2026年1月25日","externalUrl":null,"permalink":"/tags/protected-prompt-labels/","section":"タグ","summary":"","title":"Protected Prompt Labels","type":"tags"},{"content":"","date":"2026年1月9日","externalUrl":null,"permalink":"/tags/experiments/","section":"タグ","summary":"","title":"Experiments","type":"tags"},{"content":" はじめに # LLMアプリケーションの開発において、プロンプトの改善は避けて通れない作業です。しかし、プロンプトを変更するたびに、こんな不安を感じたことはありませんか？\nこのプロンプト変更、本当に改善になっているのか？ 一部のケースで良くなったけど、他のケースで悪化していないか？ 前のバージョンと比べて、どれくらい良くなったのか数字で示せない\u0026hellip; LangfuseのBaseline機能を使えば、変更前後の結果を定量的に比較し、改善した点と品質が下がった点を一目で把握できます。\n本記事では、「京都の観光案内ボット」を題材に、感覚ではなくデータに基づいてプロンプトを改善する手順を紹介します。\n本記事でわかること # LangfuseのExperiments機能の基本的な使い方 Baseline機能を使ったプロンプトの比較・評価手順 Compare Viewでのリグレッション検出方法 前提・想定対象読者 # LLMアプリケーションの開発経験がある方 プロンプトの品質管理・改善に課題を感じている方 LangfuseのExperiments機能を使ったプロンプトの比較・評価手順を知りたい方 Langfuse Experimentsとは # Experiments機能の役割 # LangfuseのExperiments機能は、プロンプトやモデル設定を変えたときに、出力が「良くなったか・悪くなったか」を、データセットを用いて定量的にテスト・比較できる機能です。具体的には以下のことが可能になります。\nデータセット管理 : テストケース（入力と期待される出力）を一元管理 実験の実行 : 同じデータセットに対して、異なるプロンプトやモデルで実験を実行 評価（スコアリング） : SDKの evaluators でスコアを付与、またはLangfuse上のEvaluator（LLM-as-a-Judge等）で評価を実行 結果の比較 : 複数の実験結果を並べて比較 Baseline機能の登場（2025年11月リリース） # 従来のExperiments機能でも複数の実験結果を比較できましたが、「どれが基準（Baseline）なのか」が明示されていませんでした。\nLangfuse v3.125.0でBaseline機能が追加され、以下が可能になりました。\n基準バージョンの明示 : 現在の本番環境や、比較の基準となる実験を「Baseline」として指定 差分の可視化 : Baseline と Candidate（比較対象）の差分を緑（改善）・赤（悪化）で色分け表示 リグレッションの検出 : フィルター機能により、スコアが悪化した項目だけを抽出して確認 これにより、「新しいプロンプトは全体的には良いが、特定のケースで品質が低下している」といった状況を視覚的に素早く発見できるようになりました。\n実務での活用シーン # Baseline機能は以下のようなシーンで特に有効です。\nプロンプトの改善 : 新しいプロンプトが既存バージョンより優れているか検証 モデルの変更 : OpenAI GPTからGeminiへの移行など、モデル変更の影響を評価 継続的な品質管理 : 定期的に実験を実行し、品質の推移をトラッキング A/Bテストの事前検証 : 本番投入前に、複数のバリエーションを比較 次のセクションでは、プロンプトの変更によるリグレッションを検出するために、実際にコードを書いて実験を実行してみましょう。\n実装 # 今回のシナリオ # 「京都の観光案内ボット」を題材に、以下の2つのプロンプトバージョンを比較します。\nプロンプトV1（Baseline） : 標準的で真面目なトーン プロンプトV2（Candidate） : 親しみやすく絵文字を使うトーン 目的は、「親しみやすさを向上させつつ、正確性を維持できるか」を検証することです。\n以下の流れで実装していきます。\nDataset（評価用データ）の作成 ↓ Baseline実験（Prompt V1） ↓ Candidate実験（Prompt V2） ↓ Evaluator（accuracy, emoji_count） ↓ Compare View（差分・リグレッション可視化） **Tips:**本番運用では、Prompt Management機能 でプロンプトを管理するのがおすすめです（コードのデプロイなしで更新・バージョニングできます）。\nディレクトリ構成 # 今回のサンプルコードのディレクトリ構成は以下の通りです。\nlangfuse-experiments-demo/ ├── src/ │ ├── data/ │ │ └── kyoto_tourism_dataset.json # テストデータセット │ ├── run_experiment.py # 実験スクリプト │ ├── upload_dataset.py # データセットアップロード │ └── .env # 環境変数 └── pyproject.toml # 依存関係定義 環境セットアップ # 今回は uv を使ってPython環境を構築します。\nUV環境設定ファイル作成 # UVの環境設定ファイルpyproject.toml を作成します。\n[project] name = \u0026#34;langfuse-experiments-demo\u0026#34; version = \u0026#34;0.1.0\u0026#34; requires-python = \u0026#34;\u0026gt;=3.10\u0026#34; dependencies = [ \u0026#34;langfuse\u0026gt;=3.0.0\u0026#34;, \u0026#34;google-genai\u0026gt;=1.0.0\u0026#34;, \u0026#34;python-dotenv\u0026gt;=1.0.0\u0026#34;, ] 依存関係インストール # uvで依存関係をインストールします。\n# uvのインストール（まだの場合） curl -LsSf https://astral.sh/uv/install.sh | sh # プロジェクトディレクトリに移動 cd langfuse-experiments-demo # 仮想環境を作成し、依存関係をインストール uv sync 環境変数設定 # 環境変数を設定します（src/.envファイルを作成）。\n# Langfuse設定 LANGFUSE_PUBLIC_KEY=pk-lf-... LANGFUSE_SECRET_KEY=sk-lf-... LANGFUSE_BASE_URL=https://cloud.langfuse.com # Google Cloud / Vertex AI設定 GOOGLE_CLOUD_PROJECT=your-project-id GOOGLE_CLOUD_LOCATION=asia-northeast1 Google Cloud / Vertex AI の認証 # Vertex AI（Gemini）を呼び出すために、Google Cloud の Application Default Credentials（ADC）を実行します。\n# Application Default Credentials を設定 gcloud auth application-default login # プロジェクトを設定 gcloud config set project your-project-id 以上で環境構築完了です。\nデータセットの準備 # テストケースを含むJSONファイルを作成します。以下のようなファイルをsrc/data/kyoto_tourism_dataset.json に保存します。\n[ { \u0026#34;input\u0026#34;: \u0026#34;京都の有名な観光地を3つ教えてください。\u0026#34;, \u0026#34;expected_output\u0026#34;: \u0026#34;清水寺、金閣寺、伏見稲荷大社などが有名です。\u0026#34;, \u0026#34;metadata\u0026#34;: { \u0026#34;category\u0026#34;: \u0026#34;general_spots\u0026#34;, \u0026#34;difficulty\u0026#34;: \u0026#34;easy\u0026#34; } }, { \u0026#34;input\u0026#34;: \u0026#34;京都で紅葉が綺麗な場所はどこですか？\u0026#34;, \u0026#34;expected_output\u0026#34;: \u0026#34;東福寺、永観堂、嵐山などが紅葉の名所として知られています。\u0026#34;, \u0026#34;metadata\u0026#34;: { \u0026#34;category\u0026#34;: \u0026#34;seasonal\u0026#34;, \u0026#34;difficulty\u0026#34;: \u0026#34;medium\u0026#34; } } ] データセットのアップロード用コード # Langfuseにデータセットをアップロードするスクリプトを作成します。以下のコードを src/upload_dataset.py に保存します。\nコードが長くなるのでセクションに折り畳んでいます。コード内容はセクションを展開して確認してください。\nsrc/upload_dataset.py\n\u0026#34;\u0026#34;\u0026#34; Langfuseにデータセットを登録するスクリプト \u0026#34;\u0026#34;\u0026#34; import json import os from dotenv import load_dotenv from langfuse import get_client # 環境変数を読み込み load_dotenv() # Langfuseクライアントの初期化 langfuse = get_client() DATASET_NAME = \u0026#34;kyoto-tourism-qa\u0026#34; DATASET_DESCRIPTION = \u0026#34;京都観光案内ボットの評価用データセット\u0026#34; def upload_dataset(): \u0026#34;\u0026#34;\u0026#34;Langfuseにデータセットをアップロード\u0026#34;\u0026#34;\u0026#34; # ローカルデータセットの読み込み with open(\u0026#39;data/kyoto_tourism_dataset.json\u0026#39;, \u0026#39;r\u0026#39;, encoding=\u0026#39;utf-8\u0026#39;) as f: items = json.load(f) # データセットを作成（既に存在する場合はそのまま使用） try: langfuse.create_dataset( name=DATASET_NAME, description=DATASET_DESCRIPTION, metadata={\u0026#34;version\u0026#34;: \u0026#34;1.0\u0026#34;, \u0026#34;language\u0026#34;: \u0026#34;ja\u0026#34;} ) print(f\u0026#34; データセット \u0026#39;{DATASET_NAME}\u0026#39; を作成しました\u0026#34;) except Exception as e: print(f\u0026#34; データセット \u0026#39;{DATASET_NAME}\u0026#39; は既に存在します\u0026#34;) # データセットアイテムを登録 for item in items: langfuse.create_dataset_item( dataset_name=DATASET_NAME, input=item[\u0026#34;input\u0026#34;], expected_output=item[\u0026#34;expected_output\u0026#34;], metadata=item.get(\u0026#34;metadata\u0026#34;, {}) ) print(f\u0026#34; {len(items)}件のアイテムを登録しました\u0026#34;) if __name__ == \u0026#34;__main__\u0026#34;: upload_dataset() 実験スクリプトの実装 # Langfuse Python SDK V3の dataset.run_experiment() を使って、Baseline実験とCandidate実験の両方を実行できるスクリプトを実装します。\n以下のコードを src/run_experiment.py に保存します。 コードが長くなるのでセクションに折り畳んでいます。コード内容はセクションを展開して確認してください。\n`src/run_experiment.py\n\u0026#34;\u0026#34;\u0026#34; 京都観光案内ボットの実験スクリプト Baseline（V1）と Candidate（V2）の両方を実行可能 Usage: uv run python src/run_experiment.py baseline # Baseline実験を実行 uv run python src/run_experiment.py candidate # Candidate実験を実行 \u0026#34;\u0026#34;\u0026#34; import os import sys import json import re from dotenv import load_dotenv from langfuse import get_client, Evaluation from google import genai from google.genai import types # 環境変数を読み込み load_dotenv() # Langfuseクライアントの初期化 langfuse = get_client() # Google Gen AI SDK クライアントの初期化 project_id = os.getenv(\u0026#34;GOOGLE_CLOUD_PROJECT\u0026#34;) location = os.getenv(\u0026#34;GOOGLE_CLOUD_LOCATION\u0026#34;, \u0026#34;us-central1\u0026#34;) client = genai.Client(vertexai=True, project=project_id, location=location) # モデル名 MODEL_NAME = \u0026#34;gemini-2.5-flash\u0026#34; # データセット名 DATASET_NAME = \u0026#34;kyoto-tourism-qa\u0026#34; # ================================================================ # プロンプト定義 # ================================================================ # プロンプトV1: 標準的で真面目なトーン（Baseline） PROMPT_V1 = \u0026#34;\u0026#34;\u0026#34;あなたは京都の観光案内を専門とするアシスタントです。 正確で丁寧な情報提供を心がけてください。 ユーザーの質問: {question} 上記の質問に対して、正確かつ簡潔に回答してください。\u0026#34;\u0026#34;\u0026#34; # プロンプトV2: 親しみやすく絵文字を使うトーン（Candidate） PROMPT_V2 = \u0026#34;\u0026#34;\u0026#34;あなたは京都の観光案内を専門とするアシスタントです。 正確で丁寧な情報提供を基本としつつ、親しみやすい雰囲気で案内してください。 回答のポイント： - 正確性を最優先に、信頼できる情報を提供する - 適度に絵文字を添えて、読みやすく親しみやすい印象にする（1〜2個/段落程度） - 「です・ます」調で丁寧に、かつ堅すぎない自然な文体で ユーザーの質問: {question} 上記の質問に対して、正確かつ親しみやすく回答してください。\u0026#34;\u0026#34;\u0026#34; # ================================================================ # タスク関数 # ================================================================ async def baseline_task(*, item, **kwargs): \u0026#34;\u0026#34;\u0026#34;Baseline実験のタスク関数（プロンプトV1を使用）\u0026#34;\u0026#34;\u0026#34; question = item.input if hasattr(item, \u0026#39;input\u0026#39;) else item.get(\u0026#34;input\u0026#34;) prompt = PROMPT_V1.format(question=question) # generationとして記録（コスト計算のためにusage_detailsを設定） with langfuse.start_as_current_observation( as_type=\u0026#34;generation\u0026#34;, name=\u0026#34;gemini-generation\u0026#34;, model=MODEL_NAME, input=prompt ) as generation: response = await client.aio.models.generate_content( model=MODEL_NAME, contents=prompt, config=types.GenerateContentConfig( temperature=0.7, max_output_tokens=2048 ) ) # usage_detailsを設定（コスト計算に必要） generation.update( output=response.text, usage_details={ \u0026#34;input\u0026#34;: response.usage_metadata.prompt_token_count, \u0026#34;output\u0026#34;: response.usage_metadata.candidates_token_count, } ) return response.text async def candidate_task(*, item, **kwargs): \u0026#34;\u0026#34;\u0026#34;Candidate実験のタスク関数（プロンプトV2を使用）\u0026#34;\u0026#34;\u0026#34; question = item.input if hasattr(item, \u0026#39;input\u0026#39;) else item.get(\u0026#34;input\u0026#34;) prompt = PROMPT_V2.format(question=question) # generationとして記録（コスト計算のためにusage_detailsを設定） with langfuse.start_as_current_observation( as_type=\u0026#34;generation\u0026#34;, name=\u0026#34;gemini-generation\u0026#34;, model=MODEL_NAME, input=prompt ) as generation: response = await client.aio.models.generate_content( model=MODEL_NAME, contents=prompt, config=types.GenerateContentConfig( temperature=0.7, max_output_tokens=2048 ) ) # usage_detailsを設定（コスト計算に必要） generation.update( output=response.text, usage_details={ \u0026#34;input\u0026#34;: response.usage_metadata.prompt_token_count, \u0026#34;output\u0026#34;: response.usage_metadata.candidates_token_count, } ) return response.text # ================================================================ # 評価関数（共通） # ================================================================ async def accuracy_evaluator(*, output, expected_output, **kwargs): \u0026#34;\u0026#34;\u0026#34;正確性評価: LLMを使って意味的な正確性を評価（LLM as a Judge）\u0026#34;\u0026#34;\u0026#34; if expected_output is None: return Evaluation(name=\u0026#34;accuracy\u0026#34;, value=0.0, comment=\u0026#34;期待される出力がないため評価不可\u0026#34;) judge_prompt = f\u0026#34;\u0026#34;\u0026#34;あなたは回答の正確性を評価する審査員です。 以下の「期待される回答」と「実際の回答」を比較し、実際の回答が期待される内容を正確にカバーしているか評価してください。 【期待される回答】 {expected_output} 【実際の回答】 {output} 【評価基準】 - 1.0: 期待される内容を完全にカバーしている - 0.7-0.9: 主要な内容はカバーしているが、一部欠けている - 0.4-0.6: 部分的にカバーしているが、重要な情報が欠けている - 0.1-0.3: ほとんどカバーできていない - 0.0: 全く関係ない回答 以下のJSON形式のみで回答してください: {{\u0026#34;score\u0026#34;: 0.0〜1.0の数値, \u0026#34;reason\u0026#34;: \u0026#34;評価理由を簡潔に\u0026#34;}} \u0026#34;\u0026#34;\u0026#34; try: response = await client.aio.models.generate_content( model=MODEL_NAME, contents=judge_prompt, config=types.GenerateContentConfig( temperature=0.0, max_output_tokens=2048 ) ) response_text = response.text.strip() json_match = re.search(r\u0026#39;\\{[^{}]*\\}\u0026#39;, response_text) result = json.loads(json_match.group()) if json_match else json.loads(response_text) score = max(0.0, min(1.0, float(result.get(\u0026#34;score\u0026#34;, 0.0)))) reason = result.get(\u0026#34;reason\u0026#34;, \u0026#34;理由なし\u0026#34;) return Evaluation(name=\u0026#34;accuracy\u0026#34;, value=score, comment=reason) except Exception as e: return Evaluation(name=\u0026#34;accuracy\u0026#34;, value=0.0, comment=f\u0026#34;評価エラー: {str(e)}\u0026#34;) def emoji_count_evaluator(*, output, **kwargs): \u0026#34;\u0026#34;\u0026#34;絵文字の使用数を評価\u0026#34;\u0026#34;\u0026#34; emoji_count = sum(1 for char in output if ord(char) \u0026gt; 0x1F300) if output else 0 return Evaluation( name=\u0026#34;emoji_count\u0026#34;, value=emoji_count, comment=f\u0026#34;絵文字を{emoji_count}個使用\u0026#34; ) # ================================================================ # 実験実行関数 # ================================================================ def run_baseline_experiment(): \u0026#34;\u0026#34;\u0026#34;Baseline実験を実行\u0026#34;\u0026#34;\u0026#34; dataset = langfuse.get_dataset(DATASET_NAME) result = dataset.run_experiment( name=\u0026#34;Kyoto Tourism Bot - Prompt Comparison\u0026#34;, run_name=\u0026#34;baseline-v1\u0026#34;, description=\u0026#34;標準的で真面目なトーンのプロンプト（Baseline）\u0026#34;, task=baseline_task, evaluators=[accuracy_evaluator, emoji_count_evaluator], metadata={\u0026#34;prompt_version\u0026#34;: \u0026#34;v1\u0026#34;, \u0026#34;model\u0026#34;: MODEL_NAME} ) print(result.format()) langfuse.flush() return result def run_candidate_experiment(): \u0026#34;\u0026#34;\u0026#34;Candidate実験を実行\u0026#34;\u0026#34;\u0026#34; dataset = langfuse.get_dataset(DATASET_NAME) result = dataset.run_experiment( name=\u0026#34;Kyoto Tourism Bot - Prompt Comparison\u0026#34;, run_name=\u0026#34;candidate-v2\u0026#34;, description=\u0026#34;親しみやすく絵文字を使うトーンのプロンプト（Candidate）\u0026#34;, task=candidate_task, evaluators=[accuracy_evaluator, emoji_count_evaluator], metadata={\u0026#34;prompt_version\u0026#34;: \u0026#34;v2\u0026#34;, \u0026#34;model\u0026#34;: MODEL_NAME} ) print(result.format()) langfuse.flush() return result # ================================================================ # メイン # ================================================================ if __name__ == \u0026#34;__main__\u0026#34;: if len(sys.argv) \u0026lt; 2: print(\u0026#34;Usage: python run_experiment.py [baseline|candidate]\u0026#34;) sys.exit(1) experiment_type = sys.argv[1].lower() if experiment_type == \u0026#34;baseline\u0026#34;: run_baseline_experiment() elif experiment_type == \u0026#34;candidate\u0026#34;: run_candidate_experiment() else: print(f\u0026#34;Unknown experiment type: {experiment_type}\u0026#34;) print(\u0026#34;Usage: python run_experiment.py [baseline|candidate]\u0026#34;) sys.exit(1) 実装のポイント # 1. dataset.run_experiment() の活用 # Langfuse Python SDK V3では、dataset.run_experiment() という高レベルメソッドが提供されています。以下のように、実験を実行することができます。\nresult = dataset.run_experiment( name=\u0026#34;Experiment Name\u0026#34;, # 実験名 run_name=\u0026#34;baseline-v1\u0026#34;, # Run名 task=baseline_task, # 各項目に対して実行する関数 evaluators=[ .. ], # Item-level評価関数 metadata={ .. } # メタデータ ) 2. 非同期タスクと評価関数 # run_experiment() はタスク関数と評価関数の両方で非同期（async def）をサポートしています。LLM APIなどを呼び出す処理は非同期処理にすることで、並列実行時の効率が向上します。\n# 非同期タスク関数 async def baseline_task(*, item, **kwargs): response = await client.aio.models.generate_content( model=MODEL_NAME, contents=prompt, config=types.GenerateContentConfig(...) ) return response.text # 非同期評価関数（LLM-as-a-Judge） async def accuracy_evaluator(*, output, expected_output, **kwargs): response = await client.aio.models.generate_content( model=MODEL_NAME, contents=judge_prompt, config=types.GenerateContentConfig(...) ) return Evaluation(name=\u0026#34;accuracy\u0026#34;, value=score, comment=reason) 3. 評価関数 # dataset.run_experiment() の evaluators 引数には、評価関数をリストで指定します。\nここで指定する評価関数は、各データセット項目の出力を個別に評価する関数です。\n今回の例では、正確性評価関数（accuracy_evaluator）と絵文字使用数評価関数（emoji_count_evaluator）を使用しています。\n実験の実行 # スクリプトを実行します。\n# データセットをアップロード uv run python src/upload_dataset.py # Baseline実験を実行 uv run python src/run_experiment.py baseline # Candidate実験を実行 uv run python src/run_experiment.py candidate 実行が完了したら、Langfuse UIにアクセスして結果を確認しましょう。次のセクションでは、Baseline機能を使った比較方法を詳しく解説します。\n検証編 - Baseline機能で結果を比較する # Compare Viewへのアクセス # Langfuseにログイン Datasets メニューから kyoto-tourism-qa をクリック Compare View へのアクセス 3. baseline-v1 と candidate-v2 の両方にチェックを入れる 4. Actions ボタン - Compare ボタンをクリック\nCompare Viewへのアクセス Baselineの設定 # Compare View が開いたら、片方の実験を「Baseline」として指定します。\nbaseline-v1 の表示横のSet as Baselineをクリック Baseline の設定 これで、```candidate-v2` が Baseline との差分として表示されるようになります。\n結果の見方 # Delta（差分）表示 # 緑色 : Baseline より改善された項目 赤色 : Baseline より悪化した項目（リグレッション） 灰色 : 変化なし Langfuse Experiment Baseline 差分表示 # 今回の実験結果 # 筆者の実行結果では、一部の項目で accuracy スコアが下がっていることがわかりました。\nCompare Viewでは、Datasetsの Input、Expected Output、バージョンごとの Output およびスコアを確認できます。\n評価スコアのコメントアイコンにマウスオーバーすると、LLM-as-a-Judgeがスコアを付けた根拠を確認できます。\nScore にマウスオーバー また、出力の右下にマウスオーバーするとトレース確認用のアイコンが表示されます。クリックするとトレース画面がオーバーレイ表示され、詳細を確認できます。\nScoreコメントなどの詳細表示 Traceアイコンにマウスオーバー Trace内容を表示 このように、評価結果のコメントやトレース詳細を確認しながら、リグレッションの有無等のプロンプト変更の影響を分析できます。\n従来もCompare Viewで結果を横並びで見比べることはできましたが、Baseline機能により評価スコアやLatency、Costなどの差分が視覚的に把握しやすくなり、分析が容易になりました。\n# まとめ # プロンプトの改善は「感覚」ではなく「データ」で判断する時代です。Baseline機能を活用して、自信を持ってプロンプトの改善サイクルを促進しましょう。\nBaseline機能のメリット # 定量的な比較 : 「なんとなく良くなった」ではなく、数値で改善・悪化を判断 リグレッションの早期発見 : 特定のケースでの品質低下を見逃さない 意思決定の根拠 : プロンプト変更の採用可否を、データに基づいて判断 実務での活用ポイント # 本番投入前の必須プロセス : プロンプト変更時は必ずBaseline比較を実施 継続的な品質モニタリング : 定期的に実験を実行し、品質の推移を追跡 チーム内でのコミュニケーション : Langfuse UIを共有して、品質議論の土台に 参考リンク # Langfuse Experiments via SDK Langfuse Changelog - Compare View Baseline Support ","date":"2026年1月9日","externalUrl":null,"permalink":"/posts/2026-01-09-langfuse%E3%81%AEexperiments-compare-view%E3%81%AEbaseline%E6%A9%9F%E8%83%BD%E3%82%92%E8%A7%A3%E8%AA%AC/","section":"Posts","summary":"はじめに # LLMアプリケーションの開発において、プロンプトの改善は避けて通れない作業です。しかし、プロンプトを変更するたびに、こんな不安を感じたことはありませんか？\n","title":"LangfuseのExperiments Compare ViewのBaseline機能を解説","type":"posts"},{"content":"","date":"2025年12月17日","externalUrl":null,"permalink":"/tags/github/","section":"タグ","summary":"","title":"GitHub","type":"tags"},{"content":"LLM オブザーバビリティプラットフォーム「Langfuse」に機能を追加して、PR がマージされるまでの過程を紹介します。環境構築でハマったポイントや解決方法もまとめているので、日本語でのコントリビュートガイドとしてもお使いください。\nこの記事で得られること # Langfuse 開発環境のセットアップ手順 実際に遭遇したエラーと解決法 セルフホスト/クラウドモードの違いと切り替え方 PR 作成時のベストプラクティス コントリビュートの背景 # セルフホスト環境で Langfuse を使っていて、Vertex AI の認証に Application Default Credentials（ADC） を使いたかったのですが、その機能がありませんでした。AWS Bedrock には既に ADC 対応があったので、Vertex AI にも同様の機能を実装することにしました。\n成果: PR #11039 がv3.140.0でマージされました 🎉 （私の PR #10915 が取り込まれたもの）\nまずは公式ドキュメントを読もう # Langfuse には充実したコントリビュートガイド があります。必要なツール、セットアップ手順、コミット規約、テスト方法まで丁寧に書かれているので、まずはこれを一読することを強くおすすめします。\nこの記事は公式ドキュメントを補足するもので、「読んだけどハマった」ポイントを中心に書いています。\n必要なツール一覧 # 公式ドキュメントの内容を日本語で整理します。\nツール バージョン 備考 Node.js v24 推奨 v20 でも警告付きで動く。nvm 等でインストール pnpm 9.5.0+ npm/yarn は不可。corepack や npm 経由でインストール Docker 環境 最新 Docker Desktop、Rancher Desktop など。4 コンテナが起動 golang-migrate 最新 ClickHouse マイグレーション用 ClickHouse CLI 最新 デバッグ用（任意） インストール方法は環境によって異なるので、各ツールの公式ドキュメントを参照してください。\nDocker で起動するコンテナ # pnpm run dx を実行すると、以下の 4 つのコンテナが起動します。\nコンテナ 用途 ポート PostgreSQL メイン DB（OLTP） 5432 ClickHouse 分析 DB（OLAP） 8123, 9000 Redis キャッシュ、キュー 6379 MinIO S3 互換ストレージ 9090, 9091 環境構築でハマったこと # 1. pnpm not found # zsh: command not found: pnpm 原因: corepack が有効化されていない、または pnpm がインストールされていない\n解決方法:\n# 方法1: corepack を有効化 corepack enable # 方法2: npm でグローバルインストール npm install -g pnpm 2. golang-migrate がない # Error: migrate: command not found 原因: 公式ガイドに書いてあるが見落としがち\n解決方法:\nbrew install golang-migrate 📝 補足: golang-migrate は ClickHouse のマイグレーションに使われます。PostgreSQL は Prisma を使いますが、ClickHouse は Prisma がサポートしていないため、別のツールが必要です。\n3. ~/package-lock.json の罠 # Error: Cannot find module \u0026#39;@langfuse/shared\u0026#39; 原因: ホームディレクトリ（~）に古い package-lock.json があると、Node.js のモジュール解決がおかしくなることがある\n解決方法:\n# ホームディレクトリの package-lock.json を削除 rm ~/package-lock.json # node_modules もクリーン cd ~/langfuse pnpm run nuke # 全ての node_modules と build ファイルを削除 pnpm install 4. ポート 5432 競合 # Error: listen EADDRINUSE: address already in use :::5432 原因: 他のアプリケーション（Docker環境であるRancher Desktop）の PostgreSQL コンテナがポート 5432 を使用していた\n解決方法: .env ファイルでポートを変更\n# ポート番号を変更 POSTGRES_HOST_PORT=5433 # データベース URL も同じポートに DATABASE_URL=\u0026#34;postgresql://postgres:postgres@localhost:5433/postgres\u0026#34; DIRECT_URL=\u0026#34;postgresql://postgres:postgres@localhost:5433/postgres\u0026#34; 5. セルフホストとクラウドモード設定 # これが一番のハマりポイントでした！\n機能を実装したのに、UI に ADC のオプションが表示されない…\n原因 # 環境変数 NEXT_PUBLIC_LANGFUSE_CLOUD_REGION でアプリの動作モードが決まります。.env.dev.example には以下の設定があります。\nNEXT_PUBLIC_LANGFUSE_CLOUD_REGION=\u0026#34;DEV\u0026#34; この値が設定されていると、Boolean(\u0026ldquo;DEV\u0026rdquo;) は true になるため、クラウドモードとして動作します。\n// 判定ロジック（fetchLLMCompletion.ts） const isLangfuseCloud = Boolean(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION); const isSelfHosted = !isLangfuseCloud; クラウドモードでは、セルフホスト専用機能（ADC など）が UI に表示されません。\n解決方法 # .env でこの行をコメントアウトします。\n# コメントアウトする # NEXT_PUBLIC_LANGFUSE_CLOUD_REGION=\u0026#34;DEV\u0026#34; サーバーを再起動すると、ADC オプションが表示されるようになります。\nモードの違い # 機能 セルフホスト（未設定） クラウド（DEV/US/EU） ADC 認証 ✅ 使える ❌ 使えない レート制限 無効 有効 UI メッセージ \u0026ldquo;your database\u0026rdquo; \u0026ldquo;our servers\u0026rdquo; 見分け方 # ログイン画面を見れば、どちらのモードで動いているかすぐわかります。\nクラウドモード: 「Data Region」セレクターが表示される セルフホストモード: シンプルなログインフォームのみ 開発の始め方 # 1. Fork \u0026amp; Clone # git clone https://github.com/YOUR_NAME/langfuse.git cd langfuse 2. 依存関係のインストール # pnpm install 3. 環境変数の設定 # cp .env.dev.example .env .env を編集します。\nNEXT_PUBLIC_LANGFUSE_CLOUD_REGION をコメントアウト（セルフホスト機能をテストする場合） 必要ならポート番号を変更 4. 開発サーバーの起動 # # 初回（DB リセットあり、時間がかかる） pnpm run dx # 2回目以降 pnpm run dev 5. 動作確認 # http://localhost:3000 を開き、テストユーザーでログインします。\nEmail: demo@langfuse.com Password: password プロジェクト構造 # Langfuse は pnpm + Turborepo のモノレポ構成です。\nlangfuse/ ├── web/ # Next.js フロントエンド + API ├── worker/ # 非同期処理ワーカー ├── packages/ │ └── shared/ # 共有コード（スキーマ、ユーティリティ） ├── ee/ # Enterprise 機能 └── fern/ # OpenAPI 仕様 技術スタック # カテゴリ 技術 フレームワーク Next.js 14（Pages Router） API tRPC DB（OLTP） PostgreSQL + Prisma DB（OLAP） ClickHouse UI Tailwind CSS + shadcn/ui 認証 NextAuth.js PR 作成時に気をつけること # Conventional Commits を使う # feat(llm): add ADC support for Vertex AI fix(security): prevent projectId specification refactor(llm): rename useADC to shouldUseDefaultCredentials 提出前チェックリスト # # コード整形 pnpm format # Lint チェック pnpm run lint CI について # PR を作成すると、以下のチェックが自動実行されます。\nCLA assistant: 初回は Contributor License Agreement への署名が必要 depthfirst-app bot: AI による自動コードレビュー dosubot: 自動ラベル付け 全てパスすると、メンテナーによるレビューに進みます。\nPR の流れ（実体験） # 私の PR は以下の流れで進みました。\nPR 作成 → 3 つの自動チェックが走る CLA 署名 → CLA assistant が署名を要求 AI レビュー → depthfirst-app bot がレビュー（ここで弾かれた場合は修正 or コメントを残す） 人間レビュー → メンテナーから changes requested 修正 → フィードバック対応 \u0026mdash;\u0026mdash;\u0026ndash;ここからはメンテナーが対応\u0026mdash;\u0026mdash;- 6. マージ → 作業ブランチへマージ 7. 本番マージ → メンテナーが main へマージする PR を作成\nまずは3.のAI レビューのCIが通るまでが我々が行う作業です。\nここまで行けたら、メンテナーに知らせたりすると良いと思います！\nその他知っておくと良いこと # 今回の PR では使わなかったものもありますが、参考までに。\n大きな変更は Issue を先に立てる: 公式ガイドにも記載されています Discord で質問できる: 困ったことがあれば Discord で質問可能 good first issue から始める: 初コントリビュートなら good first issue ラベルがおすすめ まとめ # Langfuse へのコントリビュートは、充実した公式ドキュメントのおかげでスムーズに進められました。環境構築では特に NEXT_PUBLIC_LANGFUSE_CLOUD_REGION の設定に注意が必要です。\nレビューはとても丁寧で、変数名の改善やセキュリティの考慮点など建設的なフィードバックがもらえます。厳しく詰められるようなことはないので、安心して PR を出してみてください。\n日本からも OSS にどんどん貢献していきましょう！\n参考リンク # Langfuse GitHub Langfuse 公式ドキュメント コントリビュートガイド Discord PR #11039 ","date":"2025年12月17日","externalUrl":null,"permalink":"/posts/2025-12-17-langfuse-%E3%81%AB%E6%A9%9F%E8%83%BD%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%97%E3%81%A6%E3%83%9E%E3%83%BC%E3%82%B8%E3%81%95%E3%82%8C%E3%82%8B%E3%81%BE%E3%81%A7-%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AA%E3%83%93%E3%83%A5%E3%83%BC%E3%83%88%E3%82%AC%E3%82%A4%E3%83%89/","section":"Posts","summary":"LLM オブザーバビリティプラットフォーム「Langfuse」に機能を追加して、PR がマージされるまでの過程を紹介します。環境構築でハマったポイントや解決方法もまとめているので、日本語でのコントリビュートガイドとしてもお使いください。\n","title":"Langfuse に機能を追加してマージされるまで：日本語コントリビュートガイド","type":"posts"},{"content":"","date":"2025年12月17日","externalUrl":null,"permalink":"/tags/oss/","section":"タグ","summary":"","title":"OSS","type":"tags"},{"content":"","date":"2025年12月17日","externalUrl":null,"permalink":"/tags/%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AA%E3%83%93%E3%83%A5%E3%83%BC%E3%83%88/","section":"タグ","summary":"","title":"コントリビュート","type":"tags"},{"content":"","date":"2025年12月17日","externalUrl":null,"permalink":"/tags/%E9%96%8B%E7%99%BA%E7%92%B0%E5%A2%83/","section":"タグ","summary":"","title":"開発環境","type":"tags"},{"content":" Langfuse MCP Server Arrives as a Tool for Handling Langfuse Prompts in Dify # The release of the Langfuse MCP Server has opened a new avenue for accessing Langfuse\u0026rsquo;s prompt management capabilities from external tools. Following this release, some may wonder about the role of the \u0026ldquo;Dify Langfuse Plugin\u0026rdquo; developed by our company and which solution is better.\nThis article provides an in-depth comparison of the setup and features of the Langfuse MCP Server and the Dify Langfuse Plugin within Dify, offering the optimal choice for prompt management in your LLM development.\nNote: This comparison is focused solely on leveraging Langfuse Prompts within Dify workflows. The use of the MCP Server in other clients like Claude Code or Cursor is not covered here.\nNote: The information in this article is current as of December 2025.\nLangfuse MCP Server # The Langfuse MCP Server provides a unified HTTP API endpoint for retrieving and manipulating prompts managed in Langfuse from external applications like Dify. By utilizing this server, applications can directly integrate the benefits of Langfuse\u0026rsquo;s prompt management features, such as version control and A/B testing, into their workflows. Basic Authentication using project-scoped API keys ensures secure and efficient prompt lifecycle management.\nSetup in Dify # After creating your Langfuse API Key in advance, here are the steps to configure the MCP Server for use in your Dify workflow:\nClick [Tools] at the top of the Dify screen, and select [MCP] from the sidebar. Click [Add MCP Server (HTTP)] to open the new configuration screen. MCP server setting step 1 3. For Server URL, enter the URL corresponding to your Langfuse domain (e.g., https://cloud.langfuse.com/api/public/mcp or \u0026ldquo;{your_domain}/api/public/mcp\u0026rdquo;). 4. For [Name \u0026amp; Icon], enter an arbitrary name like langfuse-mcp and set an icon. 5. For [Server Identifier] , enter an arbitrary identifier. Use an example like {organization_name}_{project_name} to easily identify your Langfuse project. 6. Click [Add Header] in the Headers section. 7. For [HEADER NAME], enter Authorization, and for [HEADER VALUE], enter Basic {your-base64-token}.\nThe your-base64-token used here is generated by executing the following command with your Langfuse public and secret keys: echo -n \u0026#34;pk-lf-your-public-key:sk-lf-your-secret-key\u0026#34; | base64 MCP server setting step 2 Dify Langfuse Plugin # The Dify Langfuse Plugin is a custom plugin specifically developed for Dify workflows, allowing users to directly call, search, and update prompts managed in Langfuse. This plugin integrates Langfuse’s robust version control capabilities into Dify, facilitating easier tracking of prompt history and team sharing.\nUsage # Installation and authentication of the plugin follow these steps:\nClick [+ Install Plugin] in the top right corner of the Dify screen. Select [GitHub] as the installation source and paste the GitHub repository URL: https://github.com/gao-ai-com/dify-plugin-langfuse . After installation, enter the credentials from the plugin settings screen. The required authentication information is: \u0026ldquo;Langfuse Secret Key\u0026rdquo;, \u0026ldquo;Langfuse Public Key\u0026rdquo;, and \u0026ldquo;Langfuse Host\u0026rdquo;. Comparison of the Two Tools # These two integration methods exhibit clear differences in functionality, configuration flexibility, and detailed tool specifications.\nAvailable Tools # In addition to getting and listing prompts, the MCP server provides two creation tools, createTextPrompt and createChatPrompt, as well as updatePromptLabels, which updates the labels attached to existing prompt versions.\nIn contrast, the Plugin is limited to three tools: retrieval, search, and Text prompt update/creation. The updatePromptLabels tool found in the MCP Server was intentionally omitted from the Dify Langfuse Plugin\u0026rsquo;s design. Furthermore, the createChatPrompt tool is exclusive to the MCP Server, meaning the Plugin cannot manipulate Chat-type prompts.\nTool List Multiple Registrations # The Langfuse MCP Server identifies the project from which to retrieve prompts using the authentication information (API key). However, due to Dify\u0026rsquo;s specifications, multiple MCP servers cannot be registered using the same Server URL, restricting simultaneous operation across multiple Langfuse organizations or projects to one setting per project.\nError when registering multiple MCP servers Conversely, the Dify Langfuse Plugin offers high flexibility, allowing authentication settings to be configured per block using separate API keys. This enables simultaneous operation across multiple Langfuse projects.\nYou can change the API key Prompt Variable Substitution # There is a functional difference between the two when it comes to the dynamic utilization of prompt templates:\nLangfuse MCP Server: The getPrompt tool does not include dynamic variable substitution. After retrieving the prompt body, you must use a Dify Code Block or similar mechanism to manually perform variable transformation. Dify Langfuse Plugin: The variable substitution feature is built into the Get Prompt tool. By passing variables in JSON format, you receive the substituted prompt body immediately, simplifying your Dify workflow. Prompt variable substitution feature Which Tool Should You Use? # Use Case Recommended Choice Reason Want to create or update Chat-type prompts Langfuse MCP Server The createChatPrompt tool is provided. Want to manipulate prompt labels from Dify Langfuse MCP Server The updatePromptLabels tool is provided, allowing control over prompt promotion/demotion. Want to easily substitute prompt variables Dify Langfuse Plugin The variable substitution feature is built into the Get Prompt tool, simplifying the workflow. Want to handle multiple Langfuse projects/organizations simultaneously in Dify Dify Langfuse Plugin Authentication settings can be registered per block, allowing operation without switching projects. Summary # The Langfuse MCP Server integrates Langfuse\u0026rsquo;s official prompt operation API into Dify, excelling particularly in Chat prompt creation and strict label management. Meanwhile, the Dify Langfuse Plugin provides high flexibility tailored to specific Dify user needs, such as simplified workflows through variable substitution and concurrent operation across multiple projects. Choose the optimal tool based on your development structure and prompt usage.\nReference Links # Dify Langfuse Plugin: https://github.com/gao-ai-com/dify-plugin-langfuse Langfuse Official Documentation (MCP Server): https://langfuse.com/docs/api-and-data-platform/features/mcp-server GAO,Inc. is the only company that sells the Langfuse Enterprise plans to businesses in Japanese Yen and provides support and implementation assistance in Japanese.\nIf you are interested in Langfuse, please contact us at contact@gao-ai.com .\n","date":"2025年12月10日","externalUrl":null,"permalink":"/posts/2025-12-10-deep-dive-comparison-langfuse-mcp-server-vs-dify-langfuse-plugin-in-dify/","section":"Posts","summary":"Langfuse MCP Server Arrives as a Tool for Handling Langfuse Prompts in Dify # The release of the Langfuse MCP Server has opened a new avenue for accessing Langfuse’s prompt management capabilities from external tools. Following this release, some may wonder about the role of the “Dify Langfuse Plugin” developed by our company and which solution is better.\n","title":"Deep Dive Comparison: Langfuse MCP Server vs. Dify Langfuse Plugin in Dify","type":"posts"},{"content":"","date":"2025年12月10日","externalUrl":null,"permalink":"/categories/dify/","section":"カテゴリ","summary":"","title":"Dify","type":"categories"},{"content":"","date":"2025年12月10日","externalUrl":null,"permalink":"/tags/dify/","section":"タグ","summary":"","title":"Dify","type":"tags"},{"content":"","date":"2025年12月10日","externalUrl":null,"permalink":"/tags/mcp/","section":"タグ","summary":"","title":"MCP","type":"tags"},{"content":"","date":"2025年12月10日","externalUrl":null,"permalink":"/tags/prompt-management/","section":"タグ","summary":"","title":"Prompt Management","type":"tags"},{"content":" DifyでLangfuse Promptを扱うツールにLangfuse MCPサーバーが登場 # LangfuseからMCPサーバー がリリースされ、Langfuseのプロンプト管理機能を外部ツールから利用する新しい道が開かれました。このリリースを受け、弊社が提供する「Dify Langfuseプラグイン」との関係性や、どちらを選ぶべきかという疑問を持つ方もいるかもしれません。\n本記事では、DifyにおけるLangfuse MCPサーバーとLangfuseプラグインの導入方法、そして詳細な機能比較を通じて、LLM開発におけるプロンプト管理の最適な選択肢を提示します。\n※本記事は Difyワークフロー内でLangfuseプロンプトを活用するという観点に絞って比較しており、MCPサーバー自体が利用可能なClaude CodeやCursorなど他のMCPクライアントについては扱いません。\n※本記事の情報は2025年12月時点のものです。\nLangfuse MCPサーバー # Langfuse MCPサーバーは、Langfuseで管理されているプロンプトを、Difyなどの外部アプリケーションから統一されたHTTP API経由で取得・操作するためのエンドポイントを提供するものです。アプリケーションは、このサーバーを利用することで、プロンプトのバージョン管理やA/BテストといったLangfuseのプロンプト管理機能の恩恵を、ワークフローに直接組み込むことができます。LangfuseのプロジェクトごとにスコープされたAPIキーを用いたBasic認証により、安全かつ効率的なプロンプトのライフサイクル管理を実現します。\nDifyでの設定方法 # Langfuse API Keyを事前に作成した上で、DifyのワークフローでMCPサーバーを利用するための設定手順は以下の通りです。\nDifyの画面上部にある[ツール]をクリックし、サイドバーから[MCP]を選択します。 [MCP サーバー（HTTP）を追加]をクリックし、新しい設定画面を開きます。 MCPサーバー設定手順1 サーバーURLに、Langfuseのドメインに応じたURL（例：https://cloud.langfuse.com/api/public/mcp や {your_domain}/api/public/mcp\u0026quot;）を入力します。 [名前とアイコン]に、langfuse-mcpなど任意の名前を入力し、アイコンを設定します。 [サーバー識別子]に、任意の識別子を入力します。例として{組織名}_{プロジェクト名}など、Langfuseのプロジェクトを識別しやすい名前を設定します。 [ヘッダー]のセクションにある[ヘッダーを追加]をクリックします。 [ヘッダー名]にAuthorization 、[ヘッダーの値]にBasic {your-base64-token} を入力します。 ここで使用するyour-base64-token は、Langfuseの公開鍵と秘密鍵を用いて以下のコマンドを実行して発行します。 echo -n \u0026#34;pk-lf-your-public-key:sk-lf-your-secret-key\u0026#34; | base64 MCPサーバー設定手順2 Dify Langfuseプラグイン # Dify Langfuseプラグインは、Difyのワークフローに特化して開発されたカスタムプラグインで、LangfuseのプロンプトをDifyから直接呼び出し、検索、更新することを可能にします。このプラグインは、Langfuseが持つ堅牢なバージョン管理機能をDifyに統合し、プロンプトの変更履歴の追跡やチーム内での共有を容易にします。\n使用方法 # プラグインのインストールと認証は以下の手順で行います。\nDifyの画面右上の[+ プラグインをインストールする]をクリックします。 インストール元として[GitHub]を選択し、GitHubリポジトリのURL https://github.com/gao-ai-com/dify-plugin-langfuse を貼り付けてインストールします。 インストール後、プラグインの設定画面から認証情報を入力します。認証情報として「Langfuse 秘密鍵」「Langfuse 公開鍵」「Langfuse Host」を設定します。 2つのツールを比較してみた # この2つの連携方法は、機能、設定の柔軟性、そしてツールの詳細な仕様において、明確な違いがあります。\n提供されているツール # MCPサーバーは、プロンプトの取得・一覧表示に加え、createTextPrompt、createChatPromptといった作成系のツールが2種類と、既存のプロンプトのバージョンに付与されたラベルを更新するupdatePromptLabelsを提供します。\n一方、プラグインは取得、検索、Textプロンプトの更新・作成を行う3ツールに絞られています。MCPサーバーにあるupdatePromptLabelsツールは、Dify Langfuseプラグインの設計上不要と判断され、意図的に搭載されていません。また、createChatPromptツールはMCPサーバーにのみ存在し、プラグインではChatタイプのプロンプトを操作できません。\nツール一覧 複数登録 # Langfuse MCPサーバーは、認証情報（APIキー）でプロンプトを取得するプロジェクトを識別していますが、Difyの仕様により同じサーバーURLでは複数のMCPを登録できないため、同時に複数のLangfuse組織やプロジェクトのプロンプトを操作することはできず、1つの設定で1つのプロジェクトに限定されます。\n複数のMCPサーバーを登録したときのエラー 一方、Dify Langfuseプラグインは、認証設定をブロックごとに個別のAPIキーで登録できるため、複数のLangfuseプロジェクトを同時に運用したい場合に高い柔軟性を提供します。\nAPIキーの切り替え可能 プロンプト変数置換 # プロンプトテンプレートの動的利用において、両者には機能的な差があります。\nLangfuse MCPサーバー: getPromptツールには変数を動的に置換する機能は付いていません。プロンプト本文を取得した後、Difyのコード実行ブロックなどを利用して手動で変数変換処理を行う必要があります。 Dify Langfuseプラグイン: Get Promptツールに変数置換機能が組み込まれています。JSON形式で変数を渡すだけで、取得と同時に置換後のプロンプト本文を得られるため、Difyのワークフローをシンプルに保てます。 プロンプト変数置換機能 ユースケースごとの比較表 # ユースケース 推奨される選択肢 理由 Chatタイプのプロンプトを作成・更新したい Langfuse MCP Server createChatPromptツールが提供されている。 Difyからプロンプトのラベルを操作したい Langfuse MCP Server updatePromptLabelsツールが提供されており、プロンプトの昇格・降格などを制御できる。 プロンプト変数を簡単に置換したい Dify Langfuseプラグイン Get Promptツールに変数を置換する機能が内蔵されており、ワークフローをシンプルに保てる。 複数のLangfuseプロジェクト/組織をDifyで同時に扱いたい Dify Langfuseプラグイン 認証設定をブロックごとに登録でき、プロジェクトを切り替えずに運用できる。 まとめ # 結論として、Langfuse MCPサーバーはLangfuseの公式なプロンプト操作APIをDifyに統合し、特にChatプロンプトの作成やラベルの厳密な管理に優れています。一方、Dify Langfuseプラグインは、変数置換機能によるワークフローの簡略化や、複数プロジェクトの並行運用という、Difyユーザー特有のニーズに対して高い柔軟性を提供します。貴社の開発体制やプロンプトの利用形態に応じて、最適なツールを選択してください。\n参考リンク # Dify Langfuseプラグイン: https://github.com/gao-ai-com/dify-plugin-langfuse Langfuse公式ドキュメント: https://langfuse.com/docs/api-and-data-platform/features/mcp-server ","date":"2025年12月8日","externalUrl":null,"permalink":"/posts/2025-12-08-dify%E3%81%A7langfuse-mcp%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%81%A8langfuse%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E3%82%92%E5%BE%B9%E5%BA%95%E6%AF%94%E8%BC%83%E3%81%97%E3%81%A6%E3%81%BF%E3%81%9F/","section":"Posts","summary":"DifyでLangfuse Promptを扱うツールにLangfuse MCPサーバーが登場 # LangfuseからMCPサーバー がリリースされ、Langfuseのプロンプト管理機能を外部ツールから利用する新しい道が開かれました。このリリースを受け、弊社が提供する「Dify Langfuseプラグイン」との関係性や、どちらを選ぶべきかという疑問を持つ方もいるかもしれません。\n","title":"DifyでLangfuse MCPサーバーとLangfuseプラグインを徹底比較してみた","type":"posts"},{"content":"","date":"2025年12月5日","externalUrl":null,"permalink":"/tags/google-cloud/","section":"タグ","summary":"","title":"Google Cloud","type":"tags"},{"content":" はじめに # LangfuseはSelf-Hosted可能で、過去のブログでもご紹介したとおり、Google Cloud上にも簡単にLangfuse環境の構築が可能です。\nTerraform で実現する Langfuse on Google CloudLangfuse公式のTerraformモジュールを使ってGoogle Cloud上にLangfuse環境を構築する手順を解説。実際の構築時の留意点やポイントも合わせて紹介します。 2025-05-26 また、Google Cloud上でLangfuseを構築する場合、Identity-Aware Proxy（IAP）を利用すると、認証済みユーザーのみがLangfuseにアクセスできるようになり、Langfuseのセキュリティが強化されます。\nIdentity-Aware Proxy の概要 | Google Cloud Documentation IAPは非常に便利なサービスですが、IAPトークンの有効期限が約1時間という制約があるため、 長時間稼働するサーバー（RAGサーバー、チャットボットなど）では、サーバー起動時に取得したIAPトークンの有効期限が切れてしまうと、IAPの先にあるLangfuseにアクセスできなくなってしまいます。\n本記事では、このIAPトークンの有効期限を考慮し、LLMアプリケーションがLangfuseにアクセスする際のIAPトークン更新方法をご紹介します。\n本記事でわかること # IAP で保護された Langfuse に対して、LLM アプリケーションから安全にアクセスする方法 IAP トークンの有効期限（約 1 時間）をまたいで、トークンを自動更新する実装パターン Langfuse Python SDK v3 系以下通信に対して、それぞれ IAP トークンを付与する方法 httpx を使う通常 API（プロンプト管理 / スコア登録 等） requests.Session を使う OTEL Span Exporter（トレース送信） 前提・想定対象読者 # 前提及び想定対象読者は以下の通りです。\nGCP / IAP / Cloud Run がある程度わかる人向け Langfuse Python SDK v3.10.1 を前提 LangChain / Vertex AI Gemini を使う例ですが、他の LLM クライアントにも応用可能 IAP環境下でのLangfuseへのアクセス方法 # LangfuseのIAP環境下での構成 # LangfuseのIAP環境下での構成を下記に示します。\nLangfuse IAP構成 認証ヘッダーの使い分け # IAP環境下のLangfuseでは、2種類のHTTP認証ヘッダーを使い分けます。\nヘッダー 用途 値の形式 Authorization Langfuse API Key認証 Basic {base64(public_key:secret_key)} Proxy-Authorization IAPトークン Bearer {iap_token} 注意点として、IAPトークンを Authorization ヘッダーに設定すると、LangfuseのAPI Key認証が上書きされてしまいます。IAP認証には、Proxy-Authorization ヘッダーを利用してください。\nIAPトークン有効期限の問題 # 上記の通りIAPを通してLangfuseにアクセスするためには、クライアントはIAPトークンをHTTPヘッダーに含める必要があります。\nLangfuseクライアントでは、httpx_client オプションを利用して独自のHTTPクライアントを指定できます。 langfuse.comSetup of the Langfuse Python SDK - LangfuseSetup the Langfuse Python SDK for tracing your application and ingesting data into Langfuse. 以下のプログラム例では、まずIAPトークンを取得し、取得したトークンを Proxy-Authorization ヘッダーとして設定した httpx.Client を生成します。\n生成したクライアントをLangfuseクライアントの httpx_client パラメータに渡すことで、Langfuseの各種API呼び出し時にIAPトークンが自動的に付与されるようになります。\nこの実装をベースに動的にIAPトークンを更新する実装を考えてみましょう。\nimport httpx from google.oauth2 import id_token from google.auth.transport.requests import Request # IAPトークンを取得 iap_client_id = os.environ[\u0026#34;IAP_CLIENT_ID\u0026#34;] initial_token = id_token.fetch_id_token(Request(), iap_client_id) # httpx.Clientにヘッダーを設定 httpx_client = httpx.Client( headers={\u0026#34;Proxy-Authorization\u0026#34;: f\u0026#34;Bearer {initial_token}\u0026#34;} ) # Langfuseのhttpx_clientオプションに渡す langfuse = Langfuse( public_key=os.environ[\u0026#34;LANGFUSE_PUBLIC_KEY\u0026#34;], secret_key=os.environ[\u0026#34;LANGFUSE_SECRET_KEY\u0026#34;], host=os.environ[\u0026#34;LANGFUSE_HOST\u0026#34;], httpx_client=httpx_client, ) Langfuse SDK V3のHTTPクライアントの構造 # 先ず、Langfuseクライアントの構成と IAPトークン更新の解決策の概要を構成図で示します。\nLangfuse SDK V3では、内部的に2つの異なるHTTPクライアントが使用されています。\nクライアント種類 通信ライブラリ 用途例 本記事プログラム例でのIAPトークン更新の実装 Langfuseクライアント httpx.Client プロンプトAPIなどトレース送信以外のAPI呼び出し event_hooksで動的に更新 OTEL Span Exporter requests.Session トレース送信 exportメソッドのラップで動的に更新 IAPトークンの更新方法が異なることに注意してください。以下で詳しく解説します。\n1. Langfuseクライアント（httpx使用） # Langfuseクライアントは、プロンプト管理やスコア登録などのAPI呼び出しに使用されます。先に説明した通り、httpx_clientパラメータでhttpxのカスタムクライアントを注入できます。\nそのためhttpxのevent_hooks機能を使えば、リクエスト直前にIAPトークンを動的に更新できます。\n参考：HTTPX - Event Hooks 2. OTEL Span Exporter（requests.Session使用） # OTEL Span Exporterは、トレース送信に使用されます。\nLangfuseクライアントとは独立したHTTPクライアントを持っており、httpx_clientのevent_hooksは適用されません。\nそのため、OTEL Span Exporterのexportメソッドをラップして、エクスポート直前にIAPトークンを更新する必要があります。\n警告 本記事で記載している内容は公式にサポートされた方法ではなく、SDKの内部構造を利用した回避策のためLangfuse SDKのバージョンによって動作しない可能性があります。 Langfuse Python SDK V3.10.1でのみ動作することが確認されています。\n実装例 # 以上を踏まえて、IAP保護下のLangfuseと連携するための実装例を以下に示します。\n1. Google Cloud認証の準備 # IAP(OAuth)クライアントの作成 # 下記のドキュメントを参考にIAPで利用するOAuth（ IAP）クライアントを作成します。\nIAP 用カスタム OAuth クライアントを作成する | Identity-Aware Proxy | Google Cloud Documentation クライアント作成後に表示されるクライアントIDとクライアントシークレットは必要になるのでメモしてください。\nOAuthクライアント作成画面 また、クライアントの「承認済みのリダイレクトURI」に以下の設定をします。\nhttps://iap.googleapis.com/v1/oauth/clientIds/YOUR_CLIENT_ID:handleRedirect ※`YOUR_CLIENT_ID`は上記でメモしたクライアントIDに置き換えてください。 リダイレクト設定 サービスアカウントの作成 # 下記のドキュメントを参考にLLMアプリケーションが利用するサービスアカウントを作成します。 サービス アカウントを作成する | Identity and Access Management (IAM) | Google Cloud Documentationサービス アカウントを作成する方法。 サービスアカウントには以下のロールを割り当てる。\nIAP で保護されたウェブアプリ ユーザー : IAP認証配下のLangfuseサーバーにアクセスするのに必要 Vertex AI ユーザー：Vertex AIのGeminiを利用するのに必要 サービスアカウント作成画面 また、以下のドキュメントを参考にサービスアカウントキーJSONファイルとしてダウンロードします。\nダウンロードしたキーファイルはサンプルプログラムのルートディレクトリに保存してください。 サービス アカウント キーの作成と削除 | Identity and Access Management (IAM) | Google Cloud Documentationサービス アカウント キーの作成と削除の方法。 Langfuse側の環境変数設定 # IAP構成でLangfuseを運用する場合、Langfuse Webの環境変数にIAPクライアントIDとシークレットを設定しLangfuse Webをリスタートします。\n# IAP認証（Langfuse Web UIログイン用） AUTH_GOOGLE_CLIENT_ID=your-iap-client-id AUTH_GOOGLE_CLIENT_SECRET=your-iap-client-secret 2. 実装コード # コードの概要 # 実装は2つのファイルで構成されています。\nファイル 役割 iap_token_provider.py IAPトークンの動的更新を担当するユーティリティモジュール server.py LangfuseとLangChainを使用したサンプルアプリケーション ディレクトリ構成 # 以下の様なディレクトリ構成になっています。\n. ├── .env # 環境変数設定 ├── pyproject.toml # 依存関係（UV用） ├── service-account-key.json # サービスアカウントキー（※gitignore推奨） ├── iap_token_provider.py # IAPトークン管理モジュール └── server.py # サンプルアプリケーション 環境変数の設定 # .envファイルを作成し、以下の環境変数を設定します。\n# Langfuse設定 LANGFUSE_PUBLIC_KEY=pk-lf-... # Langfuse Project Settings → API Keysで発行・確認 LANGFUSE_SECRET_KEY=sk-lf-... # Langfuse Project Settings → API Keysで発行・確認 LANGFUSE_HOST=https://langfuse.your-domain.com # LangfuseのURL # IAP設定 IAP_CLIENT_ID=123456789-abcdefg.apps.googleusercontent.com # 先にメモしたクライアントID # サービスアカウント認証 GOOGLE_APPLICATION_CREDENTIALS=./service-account-key.json # サービスアカウントキーのパス # GCP設定 GCP_PROJECT_ID=your-project-id # Google CloudプロジェクトID GCP_LOCATION=asia-northeast1 # リージョン 仮想環境作成・依存関係のインストール # # 本記事では、高速なPythonパッケージマネージャー UVを使用して仮想環境を構成します。\n# UVのインストール（未インストールの場合） curl -LsSf https://astral.sh/uv/install.sh | sh # 仮想環境を作成 uv venv # 仮想環境を有効化 source .venv/bin/activate # プロジェクトの初期化 uv init # 依存関係のインストール（本記事執筆時のバージョン） uv add \u0026#34;langfuse\u0026gt;=3.10.1\u0026#34; \\ \u0026#34;langchain\u0026gt;=1.1.0\u0026#34; \\ \u0026#34;langchain-core\u0026gt;=1.1.0\u0026#34; \\ \u0026#34;langchain-google-vertexai\u0026gt;=3.1.0\u0026#34; \\ \u0026#34;httpx\u0026gt;=0.28.1\u0026#34; \\ \u0026#34;google-auth\u0026gt;=2.43.0\u0026#34; \\ \u0026#34;python-dotenv\u0026gt;=1.2.1\u0026#34; \\ \u0026#34;fastapi\u0026gt;=0.122.0\u0026#34; \\ \u0026#34;uvicorn\u0026gt;=0.38.0\u0026#34; 上記コマンドを実行すると、pyproject.tomlに依存関係が記録され、.venvディレクトリに仮想環境が作成されます。\niap_token_provider.py # iap_token_provider.pyでは、以下の3つの機能を提供します\nIAPTokenProvider：サービスアカウント認証でIAPトークンを取得・キャッシュ・更新 create_httpx_client_with_iap： event_hooksでリクエスト直前にトークンを更新するhttpxクライアントを作成 wrap_otel_exporter_with_iap_token： OTEL Exporterのexportメソッドをラップしてトークンを動的に更新 具体的なコードは下記の通りです。\niap_token_provider.py\nimport logging import os from typing import Optional import httpx from google.auth.transport.requests import Request as GoogleAuthRequest from google.oauth2 import service_account logger = logging.getLogger(__name__) class IAPTokenProvider: \u0026#34;\u0026#34;\u0026#34; Google Cloud IAP用のIDトークンを提供するクラス サービスアカウントの認証情報を使用してIAP用のIDトークンを取得します。 トークンは自動的にキャッシュされ、期限切れ時に自動更新されます。 \u0026#34;\u0026#34;\u0026#34; def __init__(self, client_id: str, service_account_file: Optional[str] = None): \u0026#34;\u0026#34;\u0026#34; Args: client_id: IAPで保護されたリソースのOAuth 2.0クライアントID （Google Cloud Consoleで確認可能） service_account_file: サービスアカウントキーファイルのパス 指定しない場合はGOOGLE_APPLICATION_CREDENTIALS環境変数を使用 \u0026#34;\u0026#34;\u0026#34; self.client_id = client_id self._request = GoogleAuthRequest() sa_file = service_account_file or os.environ.get(\u0026#34;GOOGLE_APPLICATION_CREDENTIALS\u0026#34;) if not sa_file: raise ValueError( \u0026#34;サービスアカウントキーファイルが指定されていません。\u0026#34; \u0026#34;service_account_file引数またはGOOGLE_APPLICATION_CREDENTIALS環境変数を設定してください。\u0026#34; ) # IAP用のIDトークンを取得するための認証情報を作成 self._credentials = service_account.IDTokenCredentials.from_service_account_file( sa_file, target_audience=client_id, ) def get_token(self) -\u0026gt; str: \u0026#34;\u0026#34;\u0026#34; 最新のIDトークンを取得する 認証情報が期限切れの場合は自動的に更新されます。 Returns: 有効なIDトークン文字列 \u0026#34;\u0026#34;\u0026#34; if not self._credentials.valid: self._credentials.refresh(self._request) return self._credentials.token def create_httpx_client_with_iap( client_id: str, service_account_file: Optional[str] = None, timeout: float = 30.0, ) -\u0026gt; httpx.Client: \u0026#34;\u0026#34;\u0026#34; IAP認証付きのhttpxクライアントを作成する event_hooksを使用して、リクエスト直前にトークンを動的に更新します。 これにより、長時間稼働するサーバーでもトークンの有効期限切れを防ぎます。 Args: client_id: IAPで保護されたリソースのOAuth 2.0クライアントID service_account_file: サービスアカウントキーファイルのパス timeout: リクエストタイムアウト（秒） Returns: IAP認証ヘッダー付きのhttpx.Client \u0026#34;\u0026#34;\u0026#34; token_provider = IAPTokenProvider(client_id, service_account_file) def update_iap_token(request: httpx.Request): \u0026#34;\u0026#34;\u0026#34;リクエスト直前にIAPトークンを動的に更新\u0026#34;\u0026#34;\u0026#34; token = token_provider.get_token() request.headers[\u0026#34;Proxy-Authorization\u0026#34;] = f\u0026#34;Bearer {token}\u0026#34; return httpx.Client( event_hooks={\u0026#34;request\u0026#34;: [update_iap_token]}, timeout=timeout, ) # ============================================================================= # OTEL Exporter ラップ機能 # ============================================================================= _otel_exporter_wrapped = False def wrap_otel_exporter_with_iap_token(iap_client_id: str) -\u0026gt; bool: \u0026#34;\u0026#34;\u0026#34; Langfuse SDK V3のOTEL Exporterをラップして、IAPトークンを動的に更新する この関数は、LangfuseResourceManagerの内部プロセッサーを探し、 OTLPSpanExporterのexportメソッドをラップします。 Args: iap_client_id: IAPで保護されたリソースのOAuth 2.0クライアントID Returns: ラップが成功した場合はTrue \u0026#34;\u0026#34;\u0026#34; global _otel_exporter_wrapped if _otel_exporter_wrapped: logger.debug(\u0026#34;OTEL Exporterは既にラップ済みです\u0026#34;) return True try: from langfuse._client.resource_manager import LangfuseResourceManager except ImportError as e: logger.warning(f\u0026#34;LangfuseResourceManagerをインポートできませんでした: {e}\u0026#34;) return False if not hasattr(LangfuseResourceManager, \u0026#39;_instances\u0026#39;): logger.warning(\u0026#34;LangfuseResourceManager._instancesが見つかりません\u0026#34;) return False instances = LangfuseResourceManager._instances if not instances: logger.warning(\u0026#34;LangfuseResourceManagerにインスタンスが登録されていません\u0026#34;) return False # 各インスタンスのプロセッサーをラップ for public_key, instance in instances.items(): _wrap_instance_processors(instance, iap_client_id) _otel_exporter_wrapped = True logger.info(\u0026#34;OTEL Exporterのラップが完了しました\u0026#34;) return True def _wrap_instance_processors(instance, iap_client_id: str) -\u0026gt; bool: \u0026#34;\u0026#34;\u0026#34;LangfuseResourceManagerインスタンスのプロセッサーをラップする\u0026#34;\u0026#34;\u0026#34; if not hasattr(instance, \u0026#39;_otel_tracer\u0026#39;): return False otel_tracer = instance._otel_tracer if not hasattr(otel_tracer, \u0026#39;span_processor\u0026#39;): return False span_processor = otel_tracer.span_processor if hasattr(span_processor, \u0026#39;_span_processors\u0026#39;): processors = list(span_processor._span_processors) for processor in processors: _wrap_processor_exporter(processor, iap_client_id) return True else: return _wrap_processor_exporter(span_processor, iap_client_id) def _wrap_processor_exporter(processor, iap_client_id: str) -\u0026gt; bool: \u0026#34;\u0026#34;\u0026#34;プロセッサーのExporterをラップする\u0026#34;\u0026#34;\u0026#34; if not hasattr(processor, \u0026#39;span_exporter\u0026#39;): return False exporter = processor.span_exporter original_export = exporter.export # IAPTokenProviderを使用してトークンをキャッシュ・自動更新 token_provider = IAPTokenProvider(iap_client_id) def export_with_iap_token(spans): \u0026#34;\u0026#34;\u0026#34;エクスポート前にIAPトークンを更新するラッパー\u0026#34;\u0026#34;\u0026#34; try: token = token_provider.get_token() if hasattr(exporter, \u0026#39;_session\u0026#39;): exporter._session.headers[\u0026#34;Proxy-Authorization\u0026#34;] = f\u0026#34;Bearer {token}\u0026#34; except Exception as e: logger.error(f\u0026#34;IAPトークンの更新に失敗しました: {e}\u0026#34;) return original_export(spans) exporter.export = export_with_iap_token return True server.py # server.py では、FastAPIを使用して、ユーザーからの質問に回答し、Langfuseにトレースを送信するサーバーを実装しています。\n以下の機能を提供します。\nユーザーからの質問についてVertexAI Geminiを使用して回答 システムプロンプトはLangfuseから取得して使用 Langfuseにトレースを送信 「2.」によりLangfuseクライアントのIAP認証を確認、「3.」によりOTEL Span Exporterの IAP認証を確認します。\n具体的なコードは下記の通りです。\nserver.py\nimport logging import os from typing import Optional from dotenv import load_dotenv load_dotenv() # .envファイルを読み込む from fastapi import FastAPI, Query from langfuse import Langfuse from langfuse.langchain import CallbackHandler from langchain_google_vertexai import ChatVertexAI from langchain_core.prompts import ChatPromptTemplate from iap_token_provider import ( create_httpx_client_with_iap, wrap_otel_exporter_with_iap_token, ) logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) app = FastAPI(title=\u0026#34;IAP Token Expiry Test Server\u0026#34;) _langfuse: Optional[Langfuse] = None def get_langfuse() -\u0026gt; Langfuse: \u0026#34;\u0026#34;\u0026#34;Langfuseクライアントのシングルトンを取得\u0026#34;\u0026#34;\u0026#34; global _langfuse if _langfuse is None: iap_client_id = os.environ[\u0026#34;IAP_CLIENT_ID\u0026#34;] # httpx_clientはプロンプト管理などのAPI呼び出しに使用 # event_hooksで動的にIAPトークンを更新 httpx_client = create_httpx_client_with_iap(iap_client_id) _langfuse = Langfuse( public_key=os.environ[\u0026#34;LANGFUSE_PUBLIC_KEY\u0026#34;], secret_key=os.environ[\u0026#34;LANGFUSE_SECRET_KEY\u0026#34;], host=os.environ[\u0026#34;LANGFUSE_HOST\u0026#34;], httpx_client=httpx_client, ) # OTEL Exporterをラップ（トレース送信用） # httpx_clientのevent_hooksはOTEL Exporterには適用されないため、 # 別途ラップが必要 wrap_otel_exporter_with_iap_token(iap_client_id) return _langfuse @app.get(\u0026#34;/qa\u0026#34;) async def qa(q: str = Query(default=\u0026#34;日本の首都はどこですか？\u0026#34;, description=\u0026#34;質問\u0026#34;)): \u0026#34;\u0026#34;\u0026#34;質問に回答し、Langfuseにトレースを送信する\u0026#34;\u0026#34;\u0026#34; langfuse = get_langfuse() langfuse_handler = CallbackHandler() # LangfuseからPromptを取得 try: langfuse_prompt = langfuse.get_prompt(\u0026#34;qa-assistant\u0026#34;) logger.info(f\u0026#34;Langfuseからプロンプトを取得しました: name={langfuse_prompt.name}, version={langfuse_prompt.version}\u0026#34;) except Exception as e: logger.error(f\u0026#34;Langfuseからプロンプトの取得に失敗しました: {e}\u0026#34;) raise # with_configでmetadataを設定してトレースとPromptを紐づける prompt = ChatPromptTemplate.from_messages( langfuse_prompt.get_langchain_prompt() ).with_config({ \u0026#34;metadata\u0026#34;: {\u0026#34;langfuse_prompt\u0026#34;: langfuse_prompt} }) # LLMを初期化 llm = ChatVertexAI( model=\u0026#34;gemini-2.5-flash\u0026#34;, project=os.environ[\u0026#34;GCP_PROJECT_ID\u0026#34;], location=os.environ[\u0026#34;GCP_LOCATION\u0026#34;], temperature=0, ) chain = prompt | llm # 質問に回答（LangChain invokeメソッドのcallbacksにLangfuseのCallbackHandlerを設定することで、トレースはLangfuseに送信される） response = chain.invoke( {\u0026#34;question\u0026#34;: q}, config={ \u0026#34;callbacks\u0026#34;: [langfuse_handler], \u0026#34;metadata\u0026#34;: { \u0026#34;langfuse_user_id\u0026#34;: \u0026#34;test-user\u0026#34;, \u0026#34;langfuse_tags\u0026#34;: [\u0026#34;iap-test\u0026#34;] } } ) # トレースをLangfuseに送信 try: langfuse.flush() logger.info(\u0026#34;Langfuseへトレースを送信しました\u0026#34;) except Exception as e: logger.error(f\u0026#34;Langfuseへのトレース送信に失敗しました: {e}\u0026#34;) return {\u0026#34;query\u0026#34;: q, \u0026#34;answer\u0026#34;: response.content} if __name__ == \u0026#34;__main__\u0026#34;: import uvicorn uvicorn.run(app, host=\u0026#34;0.0.0.0\u0026#34;, port=8080) 3. 動作確認 # 実際に動作確認します。\n# Langfuse にプロンプトを登録 # 以下のスクリーンショットを参考に、LangfuseのUIからサンプルプログラムが利用するプロンプトを登録します。\nプロンプト作成画面 サーバーを起動 # ターミナルから以下コマンドで server.pyを起動します。\nuv run python server.py 以下のような表示が出たら正常に起動しています。\nINFO: Started server process [xxxxx] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://0.0.0.0:8080 (Press CTRL+C to quit) サーバーにリクエスト # 別のターミナルから以下のコマンドでサーバーに対して問い合わせを開始します。\ncurl -G \u0026#34;http://localhost:8080/qa\u0026#34; --data-urlencode \u0026#34;q=Langfuseってなに？\u0026#34; 数秒後に以下のように結果が返ってくるはずです。\n{\u0026#34;query\u0026#34;:\u0026#34;Langfuseってなに？\u0026#34;,\u0026#34;answer\u0026#34;:\u0026#34;Langfuseは、**LLM（大規模言語モデル）アプリケーションのオブザーバビリティ（可観測性）と評価のためのオープンソースプラットフォーム**です。\\n\\n簡単に言うと、LLMを使ったアプリケーションを開発・運用する際に、以下のようなことを助けてくれるツールです。\\n\\n1. **トレース（実行履歴の記録）**:\\n * ユーザーからの入力がLLMにどのように渡され、どのようなプロンプトが生成され、どのような応答が返されたか、その過程を詳細に記録・可視化します。\\n * 複数のLLM呼び出しやツール利用を含む複雑なチェーンの動きも追跡できます。\\n2. **モニタリング**:\\n * アプリケーションのパフォーマンス（応答速度、エラー率など）やコスト（API利用料など）をリアルタイムで監視し、異常を検知します。\\n3. **評価**:\\n * LLMの出力品質を人間による評価や自動評価（評価モデルなど）で測定し、改善点を見つけ出します。\\n * 異なるプロンプトやモデルバージョンのA/Bテストにも利用できます。\\n4. **プロンプト管理**:\\n * 使用しているプロンプトのバージョン管理や、効果的なプロンプトの特定を支援します。\\n\\nこれにより、開発者はLLMアプリケーションのデバッグ、改善、最適化を効率的に行うことができます。\u0026#34;} また、サーバー側のターミナルには以下のようなログが出力されていることを確認してください。\nINFO:iap_token_provider:OTEL Exporterのラップが完了しました INFO:__main__:Langfuseからプロンプトを取得しました: name=qa-assistant, version=1 INFO:__main__:Langfuseへトレースを送信しました INFO: 127.0.0.1:49752 - \u0026#34;GET /qa?q=Langfuse%e3%81%a3%e3%81%a6%e3%81%aa%e3%81%ab%ef%bc%9f HTTP/1.1\u0026#34; 200 OK Langfuse でトレースを確認 # 次にLangfuse Web UIにアクセスし、トレースが送信されていること、上記の回答生成プロンプトにLangfuseからフェッチしたプロンプトが利用されていることを確認します。\n以下のようにトレースが飛んでいること、Prompt:qa-assistantが利用されたことが確認できます。\nトレース画面 まとめ # IAP保護下でLangfuseを運用するには、以下の2点がポイントです。\nトレース送信以外の機能（Langfuseクライアント）: httpxのevent_hooksでリクエスト直前にIAPトークンを動的に更新\nトレース送信（OTEL Exporter）: exportメソッドをラップして、エクスポート直前にIAPトークンを動的に更新\nこれで、長時間稼働するようなLLMアプリケーションでもIAPトークンを更新しつつ継続的にLangfuse APIを利用できます。\n","date":"2025年12月5日","externalUrl":null,"permalink":"/posts/2025-12-05-google-cloud-iap%E4%BF%9D%E8%AD%B7%E4%B8%8Blangfuse%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E5%8B%95%E7%9A%84%E6%9B%B4%E6%96%B0%E3%81%AE%E5%AE%9F%E8%A3%85%E3%82%AC%E3%82%A4%E3%83%89/","section":"Posts","summary":"はじめに # LangfuseはSelf-Hosted可能で、過去のブログでもご紹介したとおり、Google Cloud上にも簡単にLangfuse環境の構築が可能です。\n","title":"Google Cloud IAP保護下Langfuseトークン動的更新の実装ガイド","type":"posts"},{"content":"","date":"2025年12月5日","externalUrl":null,"permalink":"/tags/iap/","section":"タグ","summary":"","title":"IAP","type":"tags"},{"content":"","date":"2025年12月5日","externalUrl":null,"permalink":"/tags/%E3%82%BB%E3%83%AB%E3%83%95%E3%83%9B%E3%82%B9%E3%83%88/","section":"タグ","summary":"","title":"セルフホスト","type":"tags"},{"content":"","date":"2025年12月3日","externalUrl":null,"permalink":"/tags/langfuse-enterprise/","section":"タグ","summary":"","title":"Langfuse Enterprise","type":"tags"},{"content":" はじめに # Langfuseは、LLMアプリケーションの観測性、プロンプト管理、評価を一元管理できるオープンソースプラットフォームです。セルフホストでの運用が可能で、多くの企業が自社環境での導入を進めています。\nセルフホストには2つの選択肢があります\nOSS版（無料・MIT License）: すべてのコア機能が無制限で利用可能 Enterprise版（ライセンスキー必要）: OSS版の機能に加え、Enterpriseグレードのセキュリティ・管理機能を提供 本記事ではEnterprise版で追加される主要な機能と、その具体的なユースケースを詳しく解説します。「OSS版で十分なのか？」「Enterprise版が必要になるのはどんな時か？」という疑問に答えます。\nOSS版 vs Enterprise版 # 重要なポイント：コア機能は完全に同じ # まず理解すべき重要なポイントは、OSS版でもEnterprise版でも、Langfuseのコア機能に一切の制限がないということです。\n両バージョンとも以下が利用可能：\nトレーシング（エージェント対応） プロンプト管理 評価機能（データセット、実験、LLM-as-judge） Human Annotation マルチモーダル対応 無制限のプロジェクト・APIアクセス スケーラビリティの制限なし（Langfuse Cloudと同じインフラ） 組織レベルRBAC（Owner/Admin/Member/Viewer/None） Enterprise SSO（Google, AzureAD, GitHub） OSS版 vs Enterprise版 機能比較 # 機能 OSS版 Enterprise版 組織レベルRBAC ⭕️ ⭕️ Enterprise SSO（Okta、Authentik、Azure AD、Keycloakなど） ⭕ ⭕️ プロジェクトレベルRBAC ❌ ⭕️ 監査ログ ❌ ⭕️ 保護されたデプロイメントラベル ❌ ⭕️ データ保持ポリシー ❌ ⭕️ SCIM API（自動ユーザープロビジョニング） ❌ ⭕️ Organization Management API ❌ ⭕️ Organization Creators（組織作成者の制限） ❌ ⭕️ UIカスタマイズ ❌ ⭕️ 料金 無料 カスタム価格 重要：OSS版でも、トレーシング・プロンプト管理・評価・組織レベルのRBACやなどのすべてのコア機能が無制限で利用可能です。Enterprise版は、セキュリティ・コンプライアンス・大規模運用のための管理機能を追加します。\n詳細な機能比較は公式ページ をご覧ください。\nEnterprise機能の詳細解説 # 1. プロジェクトレベルRBAC # 何ができるか # OSS版でも組織レベルのRBACは利用可能ですが、Enterprise版ではプロジェクト単位で権限を細かく設定できます。組織の権限はNoneにして、特定プロジェクトだけAdmin権限を付与、といった柔軟に設定できます。 5つのロール（Owner, Admin, Member, Viewer, None）と30種類以上の詳細な権限スコープを組み合わせて、細かくアクセス制御できます。 Enterpriseの場合のorganization設定画面\nユースケース # マルチテナント運用 顧客ごとにプロジェクトを分けている場合、担当営業やCSは自分の顧客のプロジェクトだけにアクセスできるよう制限できます。「A社担当の田中さんはA社プロジェクトのみAdmin、他はNone」といった設定が可能です。 環境別の権限管理 開発環境は全員がMember権限で自由に触れるが、本番環境はマネージャー以上のみAdmin、他メンバーはViewerで閲覧のみ、という運用ができます。「見ていいけど触っちゃダメ」を仕組みで担保できます。 公式ドキュメント 2. 監査ログ（Audit Logs） # 何ができるか # システム内のすべてのアクティビティを詳細に記録します。誰が・いつ・何をしたかを正確に追跡でき、変更の場合は変更前後の完全な状態をJSON形式で保存します。UI上でフィルタリング・エクスポートが可能です。 Audit Logsの画面\nユースケース # 障害原因の特定 「先週からLLMの回答精度が落ちた」という報告があったとき、監査ログでプロンプトの変更履歴を確認し、どの変更が原因かを数分で特定できます。変更前後の内容も保存されているので、すぐにロールバック判断ができます。 コンプライアンス対応 SOC2やISMSの監査で「過去3ヶ月の操作履歴を提出してください」と求められても、監査ログをエクスポートするだけで対応完了。手動で履歴をまとめる作業が不要になります。 責任範囲の明確化 「このプロンプト、誰が最後に触った？」という確認がログ一発で解決。属人的な記憶に頼らず、事実ベースで会話できます。 公式ドキュメント 3. 保護されたデプロイメントラベル（Protected Deployment Labels） # 何ができるか # 特定のラベル（例：production）を保護状態にすることで、ViewerとMemberロールのユーザーはそのラベルを変更・削除できなくなります。保護されたラベルが付与されているプロンプトは、プロンプト自体の削除も防止されます。 Protected Deployment Labels設定画面\nユースケース # 本番事故の防止 productionラベルを保護することで、開発者は自由にプロンプトを作成・テストできますが、本番へのデプロイは管理者の承認が必須になります。「新人が誤って本番プロンプトを上書きしてしまった」という事故を仕組みで防げます。 リリースフローの強制 「開発→ステージング→本番」というフローを守らせたい場合、stagingとproductionを保護ラベルに設定。開発者が勝手に本番に直接デプロイすることを防ぎ、レビュープロセスを確実に通す運用ができます。 公式ドキュメント 4. データ保持ポリシー（Data Retention Policies） # 何ができるか # プロジェクト単位でデータ保持期間を設定できます（最小3日間）。設定期間を超えたトレース、観測、スコア、メディアアセットを毎晩自動削除します。 削除の判定基準：トレースはtimestamp、観測はstart_time、スコアはtimestamp、メディアアセット（画像・音声など）はcreated_atを基準にします。削除されたデータは復元できません。 Data Retention設定画面\nユースケース # 環境別のコスト最適化：開発環境は7日間、ステージングは30日間、本番は1年間、とプロジェクトごとに保持期間を設定。開発中の大量のテストデータでストレージコストが膨らむのを防ぎつつ、本番データは長期保存できます。 コンプライアンス要件への対応：GDPRや社内規定で「ユーザーデータは90日を超えて保持してはならない」という要件がある場合、ポリシーを設定すれば自動で削除されます。手動での削除作業や削除漏れのリスクがなくなります。 ストレージ容量の管理：セルフホスト環境でClickHouseの容量を抑えたい場合、古いトレースを自動削除することでディスク使用量を予測可能な範囲に収められます。 公式ドキュメント 5. SCIM APIによる自動ユーザープロビジョニング # 何ができるか # SCIM（System for Cross-domain Identity Management）プロトコルを使用して、Okta、Azure AD/Entra ID、Keycloak等のSCIM対応IdPとLangfuseを自動連携させます。IdPでユーザーを管理すれば、Langfuseへのアクセスも自動で設定・解除できます。 ユースケース # 入退社時の自動処理：入社時にOktaやAzure ADでアカウントを作成すれば、Langfuseへのアクセスも自動で付与。退職時にIdPで無効化すれば、Langfuseへのアクセスも即座に遮断されます。「退職者のアカウントが残っていた」というセキュリティリスクを排除できます。 IT部門の運用負荷削減：500名規模の組織でも、IT部門がLangfuseのユーザーを個別に追加・削除する作業が不要に。IdPを唯一の真実のソースとして、すべてのツールのアクセス管理を一元化できます。 公式ドキュメント 6. Organization Management API # 何ができるか # セルフホスト専用の機能です。管理者がAPI経由で組織をプログラマティックに作成・更新・削除できます。認証にはADMIN_API_KEY環境変数を設定し、APIリクエスト時にAuthorization: Bearer $ADMIN_API_KEYヘッダーを付与します。 ユースケース # 社内ワークフローとの連携 新規プロジェクト立ち上げ時に、Slackで申請→マネージャー承認→自動でLangfuse組織・プロジェクトが作成、という流れを構築できます。管理者がUIで手作業する必要がなくなります。 マルチテナントSaaSの運用 顧客ごとにLangfuse組織を作成する必要がある場合、顧客管理システムと連携して自動プロビジョニング。顧客が増えるたびに手動で設定する手間がなくなります。 一括管理・棚卸し 四半期ごとの棚卸しで「使われていない組織を整理したい」というとき、APIで一覧取得→利用状況確認→不要な組織を削除、という作業をスクリプト化できます。 公式ドキュメント 7. Organization Creators（組織作成者の制限） # 何ができるか # デフォルトでは全ユーザーが新しい組織を作成できますが、Enterprise版では特定のメールアドレスに制限できます。 ユースケース # 組織の乱立防止 デフォルトでは全ユーザーが組織を作成できるため、「気づいたら同じチームが3つの組織を作っていた」という事態が起こりえます。作成権限をIT管理者だけに制限することで、組織構造を統制できます。 ガバナンス強化 「組織を作るには申請が必要」というルールを、仕組みで強制できます。口頭でのルール周知だけでは守られないことも、システムで制限すれば確実です。 公式ドキュメント 8. UIカスタマイズ # 何ができるか # 環境変数を設定することで、ロゴ、ドキュメント・サポートリンク、LLMデフォルト設定、表示モジュールをカスタマイズできます。 UIカスタマイズ環境変数\nLANGFUSE_UI_LOGO_LIGHT_MODE_HREF=https://example.com/logo-light.png LANGFUSE_UI_DOCUMENTATION_HREF=https://wiki.example.com/langfuse LANGFUSE_UI_HIDDEN_PRODUCT_MODULES = tracing,datasets,prompt-management LANGFUSE_UI_HIDDEN_PRODUCT_MODULESを設定すると以下のようにトレースの画面をUIから削除したりとカスタマイズが可能です。\nユースケース # 社内ツールとしてのブランディング：自社ロゴを設定し、ドキュメントリンクを社内Wikiに変更することで、「Langfuse」ではなく「社内LLM管理ツール」として展開できます。ユーザーに外部サービス感を与えず、自然に使ってもらえます。 機能の絞り込み：非エンジニアのビジネスユーザーにはプロンプト管理だけを使わせたい場合、トレースやデータセット機能をUIから非表示にできます。「どこを触ればいいかわからない」という混乱を防ぎ、必要な機能だけを見せられます。 サポート導線の統一：ヘルプリンクを社内のサポートチャンネルやチケットシステムに変更することで、「困ったらここに聞く」という導線を明確にできます。 公式ドキュメント 導入方法 # Enterprise機能の有効化は、両方のLangfuseコンテナに以下の環境変数を追加するだけです LANGFUSE_EE_LICENSE_KEY=\u0026lt;your-license-key\u0026gt; ダウンタイムなしで有効化可能で、既存のデータやAPIキーに影響はありません。\n詳細（公式ドキュメント） どんな組織に向いているか # OSS版で十分なケース # チーム規模が20名未満 単一部署・単一チームでの利用 厳格なコンプライアンス要件がない 基本的なRBAC（組織レベル）で十分 重要：OSS版でもすべてのコア機能が無制限で利用可能です。Enterprise SSO（Google, AzureAD, GitHub）と組織レベルRBACも含まれています。\nEnterprise版が必要になるタイミング # 複数部署での利用が始まり、プロジェクト単位でのアクセス制御が必要になった コンプライアンス監査で操作履歴の提出を求められた 管理者が全組織・全プロジェクトを一元管理したい ユーザー管理の手動作業が運用負荷になってきた 本番環境のプロンプト誤変更のリスクが顕在化した データ保持期間の制御が必要になった まとめ # Langfuseのセルフホスト版は、OSS版でもすべてのコア機能が無制限で利用できます。Enterprise版は、セキュリティ、コンプライアンス、大規模運用のための管理機能を追加します。\n組織の規模や要件に応じて、最適なプランを選択してください。\nまた、企業向けサポートとして、ガオ株式会社を通じてEnterpriseプランを日本円で購入し、日本語でサポートを受けることが可能です。\nご興味ある方は、contact@gao-ai.com までご連絡ください。\n参考リンク：\nセルフホスト料金プラン: https://langfuse.com/pricing-self-host Enterpriseライセンスキー: https://langfuse.com/self-hosting/license-key 公式サポート: https://langfuse.com/support ","date":"2025年12月3日","externalUrl":null,"permalink":"/posts/2025-12-03-langfuse-%E3%82%BB%E3%83%AB%E3%83%95%E3%83%9B%E3%82%B9%E3%83%88-oss-vs-enterprise%E6%A9%9F%E8%83%BD%E6%AF%94%E8%BC%83/","section":"Posts","summary":"はじめに # Langfuseは、LLMアプリケーションの観測性、プロンプト管理、評価を一元管理できるオープンソースプラットフォームです。セルフホストでの運用が可能で、多くの企業が自社環境での導入を進めています。\n","title":"Langfuse セルフホスト｜OSS vs Enterprise機能比較","type":"posts"},{"content":" はじめに # LLMアプリケーション開発において、テキストだけでなく画像や音声などのマルチモーダルなデータを扱うケースが増えています。Langfuseは2024年8月に初めてマルチモーダルトレースのサポートを発表し、同年11月には画像、音声、PDFなどの添付ファイルにも対応する完全なマルチモーダルサポートを実現しました。\n当該機能は長らくpreviewとされていましたが、先日GAとなったようです（※中の人がSlackでそう言ってました）。\nそこで本記事では、Langfuseのマルチモーダル機能の概要、具体的な使い方、そして利用時の注意点について解説します。\nLangfuseマルチモーダル機能の概要 # たとえば画像ファイルを含んだトレースを送信した場合、LangfuseのWebUI上では以下のように表示されます。\nこのように、LLMを呼び出す際に画像データが含まれていた場合、テキストデータだけでなく画像データも同時に確認でき、改善活動の効率が大いに向上します。\n（※余談ですが、gpt-4oは寿司ネタにそこまで詳しくないのかもしれません。上記画像にタコにみえるネタはないようにみえるので…）\nまた、音声ファイルが添付されたトレースは以下のように表示されます。\nUI上に表示される再生ボタンを押すことで音声が再生できます。こちらも画像ほどではないにせよ、WebUIから直接LLMに送信した音声データの内容が確認できるため、LLMアプリケーションの挙動確認を効率よく行えるようになります。\n対応しているファイル形式 # Langfuseは以下のメディアファイル形式に対応しています:\n画像: PNG、JPG、WEBP 音声ファイル: MPEG、MP3、WAV その他の添付ファイル: PDF、プレーンテキスト これらのファイルは、トレースやObservationのinput、output、metadataフィールドに含めることができます。\n主な特徴 # 自動処理: Base64エンコードされたデータURIは、Langfuse SDKが自動的に検出し処理されます 外部URL対応: 外部URLで参照されるメディアファイルもUI上でインライン表示可能です 効率的な保存: メディアファイルはトレースデータと分離され、オブジェクトストレージに直接アップロードされます 重複排除: 同じファイルは自動的に重複排除され、参照IDのみが保存されます アーキテクチャ # Langfuseは、パフォーマンスと効率性を最適化するために以下のような仕組みを採用しています:\nメディアファイルはクライアント側でトレースデータから分離 AWS S3または互換性のあるオブジェクトストレージに直接アップロード トレース内にはmediaIdへの参照のみを保持 UI側でmediaIdを検出し、メディアファイルをインライン表示 使い方 # 1. Base64エンコードされたメディアの自動処理(最も簡単) # 最新バージョンのLangfuse SDKであれば、通常のトレース送信設定を実施しておくだけで、Base64エンコードされたメディアファイルが自動的にLangfuseにアップロードされます。\nOpenAI SDKとの連携例(画像の場合):\nfrom langfuse.openai import openai # Langfuse統合が有効化されたOpenAIクライアント client = openai.OpenAI() # 画像を含むリクエスト(Base64エンコードされた画像も自動処理) response = client.chat.completions.create( model=\u0026#34;gpt-4o\u0026#34;, messages=[ { \u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: [ {\u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;この画像には何が写っていますか?\u0026#34;}, { \u0026#34;type\u0026#34;: \u0026#34;image_url\u0026#34;, \u0026#34;image_url\u0026#34;: { \u0026#34;url\u0026#34;: \u0026#34;data:image/jpeg;base64,/9j/4AAQSkZJRg...\u0026#34; } } ] } ] ) SDK側で自動的に画像を抽出し、Langfuseのオブジェクトストレージにアップロード後、トレースに参照を記録します。\nまた、音声データの場合でもBase64エンコードして利用するぶんには同様に自動的に検出＆アップロードされます。\n# 2. 外部URLによる参照 # 外部URLで画像を参照する場合も、Langfuse UIで自動的にインライン表示されます:\nresponse = client.chat.completions.create( model=\u0026#34;gpt-4o\u0026#34;, messages=[ { \u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: [ {\u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;この画像を分析してください\u0026#34;}, { \u0026#34;type\u0026#34;: \u0026#34;image_url\u0026#34;, \u0026#34;image_url\u0026#34;: {\u0026#34;url\u0026#34;: \u0026#34;https://example.com/image.jpg\u0026#34;} } ] } ] ) この場合、メディアファイルはLangfuseのストレージにアップロードされず、元のURLからLangfuse WebUI上に直接表示されます。\n3. LangfuseMediaクラスを使ったカスタム制御 # より細かい制御が必要な場合や、Base64エンコードされていないメディアを扱う場合は、LangfuseMediaクラスを使用します:\nfrom langfuse import get_client, observe from langfuse.media import LangfuseMedia @observe() def process_document(): langfuse = get_client() # PDFファイルを読み込む with open(\u0026#34;document.pdf\u0026#34;, \u0026#34;rb\u0026#34;) as pdf_file: pdf_bytes = pdf_file.read() # LangfuseMediaでラップ pdf_media = LangfuseMedia( content_bytes=pdf_bytes, content_type=\u0026#34;application/pdf\u0026#34; ) # トレースのメタデータに追加 langfuse.update_current_trace( metadata={\u0026#34;document\u0026#34;: pdf_media} ) # または、入力や出力に含める langfuse.update_current_span( input={\u0026#34;document\u0026#34;: pdf_media} ) # セットアップと料金 # Langfuse Cloudを使用する場合 # Langfuse Cloudでは、マルチモーダル添付ファイルは現在無料で利用できます。ただし、将来的には大規模なマルチモーダルトレースに伴うストレージとコンピューティングコストを考慮した課金体系が導入される可能性があることに留意してください。\nセルフホスティングの場合 # セルフホスティング環境では、独自のオブジェクトストレージバケットを設定する必要があります:\nAWS S3または互換ストレージ(Google Cloud Storage、Azure Blob Storage、Minio等)を用意 環境変数LANGFUSE_S3_MEDIA_UPLOAD_*を設定 ストレージバケットは、SDK経由の直接アップロードとブラウザからのメディアアセット取得をサポートするため、公開解決可能なホスト名を持つ必要があります 設定の詳細はセルフホスティングドキュメント を参照してください。\n注意事項と制約 # 1. 現在サポートされていない機能 # Playgroundでの使用: マルチモーダルコンテンツはまだPlaygroundでサポートされていません Dataset Items: データセットアイテムでのマルチモーダルコンテンツもまだサポート対象外です 2. セキュリティと検証 # メディアアップロードには署名付きURL(presigned URL)が使用されます コンテンツ長、コンテンツタイプ、SHA256ハッシュによる検証が行われます ファイルの一意性は、プロジェクト、コンテンツタイプ、SHA256ハッシュによって判定されます 3. ストレージ容量の管理 # マルチモーダルデータは通常のトレースデータよりも大きなストレージを消費します。特にセルフホスティングの場合は、ストレージ容量とコストの管理に注意してください。\n4. トラブルシューティング # 画像が正しく表示されない場合:\nセルフホスト環境で画像がインラインではなくボタンとして表示される場合、LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE=trueの設定が必要な場合があります ストレージバケットの公開設定と署名付きURLの有効期限を確認してください Langfuseのバージョンがマルチモーダルサポートに対応しているか確認してください(v2.93.2以降推奨) まとめ # Langfuseのマルチモーダル対応により、テキストだけでなく画像、音声、文書ファイルなどを含むLLMアプリケーションの動作を包括的に観察できるようになりました。\n主なメリット:\nBase64エンコードされたメディアの自動処理により、開発者の手間を削減 外部URLとカスタムアップロードの両方に対応し、柔軟な実装が可能 効率的な重複排除とストレージ管理 既存のOpenAI SDK、LangChain、LlamaIndexなどとのシームレスな統合 マルチモーダルAIアプリケーションの開発・運用において、Langfuseは強力な観察性とデバッグ機能を提供します。まずは最新バージョンのSDKにアップグレードして、自動処理の恩恵を受けることから始めてみてください。\n参考リンク # Langfuse公式ドキュメント - マルチモーダル機能 マルチモーダルトレースの例(Jupyter Notebook) Langfuse Launch Week #2アナウンス セルフホスティングガイド ","date":"2025年12月2日","externalUrl":null,"permalink":"/posts/2025-12-02-langfuse%E3%81%AE%E3%83%9E%E3%83%AB%E3%83%81%E3%83%A2%E3%83%BC%E3%83%80%E3%83%AB%E5%AF%BE%E5%BF%9C-%E7%94%BB%E5%83%8F%E9%9F%B3%E5%A3%B0%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E3%83%88%E3%83%AC%E3%83%BC%E3%82%B9%E6%B7%BB%E4%BB%98%E6%A9%9F%E8%83%BD%E3%81%8Cga%E3%81%AB/","section":"Posts","summary":"はじめに # LLMアプリケーション開発において、テキストだけでなく画像や音声などのマルチモーダルなデータを扱うケースが増えています。Langfuseは2024年8月に初めてマルチモーダルトレースのサポートを発表し、同年11月には画像、音声、PDFなどの添付ファイルにも対応する完全なマルチモーダルサポートを実現しました。\n","title":"Langfuseのマルチモーダル対応:画像・音声ファイルのトレース添付機能がGAに","type":"posts"},{"content":"","date":"2025年11月30日","externalUrl":null,"permalink":"/tags/livekit/","section":"タグ","summary":"","title":"LiveKit","type":"tags"},{"content":"","date":"2025年11月30日","externalUrl":null,"permalink":"/tags/%E9%9F%B3%E5%A3%B0ai/","section":"タグ","summary":"","title":"音声AI","type":"tags"},{"content":" はじめに # LiveKit Agentsは、音声AIアプリケーションを構築するためのオープンソースフレームワークです。本記事では、Langfuseを使った観測可能性の実装と、その際に遭遇したトレース分離問題の解決方法を紹介します。\n想定読者\nOpenTelemetry、Langfuseの基礎知識がある方。 LiveKit Agentsで音声AIアプリケーションを構築している方。 LiveKitとは # LiveKitは、リアルタイム音声・映像通信のためのオープンソースプラットフォームです。WebRTCをベースにしており、音声AIエージェントをはじめとする様々なリアルタイムアプリケーションの構築に利用できます。\nLiveKitの主な特徴 # Room中心の設計 # LiveKitでは、Roomという仮想空間を中心とした設計になっています。\nRoom: 参加者が集まる仮想空間。会議室やチャットルームのようなイメージ。 Participant: Roomに参加するユーザーやエージェント。 Agent: プログラマブルなAI参加者。人間のようにRoomに参加し、音声で会話できる。 WebRTCによる低レイテンシ通信 # 従来のHTTP/WebSocketと比較して、WebRTCは音声・映像のリアルタイム通信に最適化されており、低レイテンシで高品質な通信が可能です。\n多様なクライアントSDK # ブラウザ、iOS、Android、Unityなど、主要なプラットフォームに対応したSDKが提供されており、幅広い環境で利用できます。\n詳細は公式ドキュメント をご覧ください。\nLiveKit Agentsを使うメリット # 1. 統一されたインターフェース # STT、LLM、TTSの各プロバイダーを統一されたAPIで扱えるため、プロバイダーの切り替えが容易です。\n2. 本番環境に対応した機能 # VAD（Voice Activity Detection）、Turn Detection、エラーハンドリングなど、実用的な機能が標準で提供されています。\n3. リアルタイム性の高さ # WebRTCベースの設計により、エンドツーエンドで低レイテンシな音声通信が実現できます。\n4. 柔軟なアーキテクチャ # STT+LLM+TTSの従来型パイプラインと、OpenAI Realtime APIなどのSpeech-to-Speechモデルの両方に対応しています。\n料金プラン # LiveKit CloudのFree Planでは、月間1,000分のAgent Sessionが無料で利用できます。これにより、開発段階やプロトタイプ作成において、コストを気にせず気軽に始められます。\n詳細は料金ページ をご確認ください。\n環境構築 # 今回は、LiveKit Agentsの公式リポジトリにあるサンプルコード（langfuse_trace.py） を使用します。このサンプルには、Langfuse統合の基本実装と、2つの異なるタイプのエージェント（STT+LLM+TTS構成とRealtime API構成）が含まれています。\nセットアップの流れは以下の通りです。\nLiveKit CloudでAPI Keyを取得。 必要な環境変数を設定。 サンプルコードをクローンして依存関係をインストール。 初回セットアップコマンドを実行。 それでは、具体的な手順を見ていきましょう。\nLiveKitアカウント作成とAPI Key取得 # まず、https://cloud.livekit.io/login にアクセスしてアカウントを作成します。アカウント作成後、Settings → API keys → Create key の順にクリックし、生成されたAPI KeyとSecretをコピーして保存します。\n必要な環境変数の設定 # プロジェクトルートに.envファイルを作成し、以下の環境変数を設定します。\n# Langfuse LANGFUSE_SECRET_KEY=sk-lf-** LANGFUSE_PUBLIC_KEY=pk-lf-** LANGFUSE_HOST=https://** # LiveKit LIVEKIT_URL=wss://**.livekit.cloud LIVEKIT_API_KEY=** LIVEKIT_API_SECRET=** # OpenAI OPENAI_API_KEY=sk-proj-** サンプルコードのセットアップ # 以下のコマンドでサンプルコードを取得し、環境をセットアップします。まず、リポジトリをクローンして該当ディレクトリに移動します。\ngit clone https://github.com/livekit/agents.git cd agents/examples/voice_agents 次に、仮想環境を作成してアクティベートします。\npython -m venv venv source venv/bin/activate 依存関係をインストールし、初回のみ必要なファイルをダウンロードします。\npip install -r requirements.txt python langfuse_trace.py download-files 使用バージョン\nPython 3.12.12 livekit-agents 1.3.2 livekit-plugins-openai 1.3.2 livekit-plugins-deepgram 1.3.2 livekit-plugins-silero 1.3.2 サンプルコードの実行 # 以下のコマンドでコンソールモードでエージェントを起動します。\npython langfuse_trace.py console このサンプルコードには、以下の機能が実装されています。\n実装されているエージェント # Kelly: Deepgram（STT）、GPT-4o-mini（LLM）、OpenAI TTS（TTS）を組み合わせた従来型パイプライン。 Alloy: OpenAI Realtime APIを使用したSpeech-to-Speechエージェント。 ツール # lookup_weather: 天気情報を取得するツール（仮想データを返す）。 エージェント交代機能 # KellyとAlloyは相互に交代可能です。Kellyに\u0026quot;transfer to Alloy\u0026quot;と話しかけるとAlloyに交代し、逆にAlloyに\u0026quot;transfer to Kelly\u0026quot;と話しかけるとKellyに戻ります。\nLiveKitを使ってみた感想 # 実際にLiveKit Agentsを使用して音声AIアプリケーションを構築してみた感想をいくつか紹介します。\nCLIが見やすく使いやすい # LiveKitのCLIは非常に見やすく設計されており、ログの確認やデバッグが容易でした。音声認識の結果やエージェントの応答がリアルタイムで表示されるため、開発体験が良好です。\nLiveKit CLI この画像では、STTによる音声認識結果、LLMやTTSのメトリクス（レイテンシ、トークン数など）、ツールの実行結果などが時系列で表示されているのが分かります。特に、EOU（End of Utterance）の検出やLLMのTime to First Token（TTFT）などの詳細なメトリクスが確認できる点が便利です。\n複数エージェントとの会話が簡単に実装できる # 今回のサンプルでは、KellyとAlloyという2つのエージェントを切り替えながら会話できました。エージェントの切り替えロジックがシンプルに実装されており、複雑な状態管理が不要な点が印象的でした。\nSTT+LLM+TTSのエージェントでも非常に速い # 当初、Realtime APIと比較してSTT+LLM+TTSパイプラインはレイテンシが高いのではないかと懸念していました。しかし、実際に使用してみると、体感的な遅延はほとんど感じられず、自然な会話が可能でした。\nSTT+LLM+TTSでも高速な理由 # LiveKit AgentsのSTT+LLM+TTSパイプラインが高速な理由は、以下のような最適化技術が組み込まれているためです。\n1. プリエンプティブ生成（Preemptive Generation） # preemptive_generation=True, ユーザーの発話が完全に終わる前に、部分的な転写結果に基づいて応答生成を開始します。これにより、ユーザーが話し終わった瞬間にエージェントが応答できます。\n2. ストリーミングTTS # tts=tts.StreamAdapter( tts=openai.TTS(), text_pacing=True, ), LLMがテキストを生成し次第、TTSが音声を順次送信します。全文生成を待たずに最初の音声が届くため、体感レイテンシが大幅に短縮されます。\n3. WebRTCによる低レイテンシ通信 # HTTP/WebSocketよりも低レイテンシなWebRTCプロトコルを使用しているため、ネットワーク遅延が最小限に抑えられます。\n4. 非同期並列処理 # STT、LLM、TTSが非同期で並列実行されるため、各処理の完了を待たずに次のステップに進めます。\n5. 最適化されたパイプライン # VAD（Voice Activity Detection）: 発話の開始/終了を正確に検出。 Turn Detection: 会話のターンを適切に判断。 インスタント接続: マイク入力をバッファリングして即座に処理開始。 実測値 # Langfuseのタイムライン表示で確認したところ、ユーザーが話し終わってからエージェントが話し始めるまでの時間は以下の通りでした。\nSTT+LLM+TTS: 約2.33秒 Realtime Model: 約0.65秒 数値で見ると差がありますが、体感的には両者とも自然な会話ができるレベルでした。STT+LLM+TTSでも十分に実用的な速度が出ていることが確認できました。\nトレースがバラバラになる問題を発見 # 期待していた構造 # Langfuseの公式統合ガイド では、以下のような階層化された単一のトレース構造が示されています。\nLangfuse公式HPに載っているトレース すべてのアクティビティが1つのトレースに階層化され、処理の流れが一目で把握できる状態を期待していました。\n実際の構造 # しかし、実際にサンプルコードを実行してみると、各アクティビティが独立したトレースとして記録されてしまいました。\nサンプルコード実行時のトレース一覧 この画像では、以下のように複数のトレースが別々に作成されています。\nagent_session start_agent_activity（エージェント入室） user_turn agent_turn drain_agent_activity（エージェント退室） 画像中の9:15:52付近を見ると、drain_agent_activityとstart_agent_activityが連続しています。これは、Kelly（STT+LLM+TTS）からAlloy（Realtime Model）へのエージェント交代を示しています。\nこの問題はGitHub Discussion でも報告されています。\nuser_turnとagent_turnの詳細 # トレースの詳細を見てみると、どちらのモデルでもuser_turnとagent_turnという基本構造は共通していました。しかし、内部の詳細度が大きく異なるようです。\nuser_turn（共通） # どちらのモデルでも、ユーザーの発話に関する情報が記録されます。転写テキスト、信頼度スコア、発話時間などが含まれます。\nサンプルコードのuser_turnトレース agent_turn（STT+LLM+TTS） # パイプラインの各ステップが詳細に記録されます。\nllm_node: LLMへのリクエスト。 llm_request: 実際のAPI呼び出し。 tts_node: TTSへのリクエスト。 tts_request: 実際の音声合成。 function_tool: ツール呼び出し（ツール使用時のみ）。 このように各ステップが可視化されるため、ボトルネックの特定や最適化がしやすくなります。\nサンプルコードのagent_turnトレース(STT+LLM+TTS) agent_turn（Realtime Model） # 使用しているOpenAI Realtime APIはSpeech-to-Speechモデルのため、内部処理が抽象化されています。STT、LLM、TTSといった明示的な分離がなく、シンプルな構造になっています。\nまた、Realtime ModelだけOutputがトレースに表示されました。\nサンプルコードのagent_turnトレース(Realtime Model) なぜトレースが分離するのか # LiveKit Agentsは内部で非同期処理を多用しています。各アクティビティ（user_turn、agent_turnなど）は別々の非同期タスクとして実行されますが、その際にOpenTelemetryのコンテキストが適切に伝播されないことがあります。\nOpenTelemetryでは、スパン作成時に「現在のコンテキスト」を参照して親子関係を構築します。しかし、非同期タスクが新しいコンテキストで実行されると、親スパンへのリンクが失われ、新しいトレースIDが生成されてしまいます。\nこの問題を解決するには、プログラム全体で共通のトレースIDを使用し、すべてのスパンがこのトレースIDを継承するように明示的に設定する必要があります。\n解決策とOpenTelemetryのコンテキスト管理 # 基本的なアプローチ # プログラム起動から終了までを1つのトレースとして扱うため、カスタムのトレースIDを生成し、すべてのスパンがこのトレースIDを継承するようにします。\n実装手順 # ステップ1: 必要なモジュールのインポート # まず、OpenTelemetryのコンテキスト管理に必要なモジュールをインポートします。\nfrom opentelemetry import trace from opentelemetry.trace import NonRecordingSpan, SpanContext, TraceFlags import hashlib ステップ2: グローバルトレースIDの生成 # プログラム起動時に、固定のトレースIDを生成します。プログラム起動時に1回だけ生成するようにグローバル変数としています。\n# プログラム起動時に固定のtrace_idを生成 # プログラム起動から終了までに使用するルームセッションで同じtrace_idを使用することで、 # すべてのアクティビティを1つのtraceにまとめる GLOBAL_TRACE_ID = int(hashlib.sha256(os.urandom(16)).hexdigest()[:32], 16) ステップ3: SpanContextの作成とコンテキスト設定 # entrypoint関数内で、カスタムトレースIDを使用したSpanContextを作成し、グローバルコンテキストとして設定します。\n@server.rtc_session() async def entrypoint(ctx: JobContext): # グローバルなtrace_idを使用 trace_id_int = GLOBAL_TRACE_ID # trace_idを設定するためのSpanContextを作成 span_context = SpanContext( trace_id=trace_id_int, span_id=int.from_bytes(os.urandom(8), \u0026#34;big\u0026#34;), # ランダムなspan_id is_remote=False, trace_flags=TraceFlags(TraceFlags.SAMPLED), ) # カスタムtrace_idでコンテキストを設定 non_recording_span = NonRecordingSpan(span_context) ctx_with_span = trace.set_span_in_context(non_recording_span) # コンテキストをグローバルに設定 # これにより、LiveKit Agentsが作成するすべてのスパンがこのコンテキストを継承 token = trace.context_api.attach(ctx_with_span) コードの解説\nNonRecordingSpan: 実際には記録されないスパン。コンテキスト伝播のためだけに使用します。 trace.set_span_in_context: スパンをコンテキストに設定します。 trace.context_api.attach: コンテキストをグローバルに設定し、後でデタッチするためのtokenを返します。 ステップ4: try-finallyでコンテキスト管理 # コンテキストを設定した後は、必ずデタッチする必要があります。try-finallyブロックを使用して、確実にクリーンアップを行います。\ntry: # set up the langfuse tracer（コンテキスト設定後に呼び出す） trace_provider = setup_langfuse( metadata={ \u0026#34;langfuse.session.id\u0026#34;: ctx.room.name, \u0026#34;room.name\u0026#34;: ctx.room.name, } ) async def flush_trace(): trace_provider.force_flush() ctx.add_shutdown_callback(flush_trace) session = AgentSession(vad=silero.VAD.load()) @session.on(\u0026#34;metrics_collected\u0026#34;) def _on_metrics_collected(ev: MetricsCollectedEvent): metrics.log_metrics(ev.metrics) await session.start(agent=Kelly(), room=ctx.room) finally: # コンテキストをデタッチ trace.context_api.detach(token) 重要なポイント\nsetup_langfuse()は必ずコンテキスト設定後に呼び出します。 finallyブロックで確実にデタッチし、コンテキストのリークを防ぎます。 修正後のトレース構造 # トレース一覧の変化 # 修正後は、すべてのアクティビティが1つのトレースに統合されました。\n修正後トレース一覧 Observation Levelが80となっており、多数のスパンが1つのトレースにまとまっていることが分かります。\nトレース詳細 # 修正後トレース詳細 トレースの内部構造を見ると、期待通りの階層構造になっています。\nタイムライン表示 # タイムライン表示では、時系列で処理の流れが可視化されます。\n修正後トレースのタイムライン1 修正後トレースのタイムライン2 エージェント交代のタイミングや、各ターンの実行順序、各ステップのレイテンシが視覚的に把握できます。\n改善された点 # すべてのアクティビティが1つのトレースに統合されました。 親子関係が正しく表現されるようになりました。 時系列での処理フローが追跡可能になりました。 ボトルネックの特定が容易になりました。 エージェント交代などの複雑なフローも明確に可視化されるようになりました。 まとめ # 今回、LiveKit Agentsを使用して音声AIアプリケーションを構築し、Langfuseによる観測可能性を実装しました。\n実際に使ってみて、CLIの見やすさや複数エージェント間の切り替えの容易さなど、開発体験の良さを実感しました。特に印象的だったのは、STT+LLM+TTSパイプラインの速度です。当初は遅いのではないかと懸念していましたが、プリエンプティブ生成やストリーミングTTSなどの最適化技術により、約2.33秒という実用的な速度を達成しており、体感的にも自然な会話が可能でした。\n一方で、Langfuse統合時にトレースが分離してしまう問題に遭遇しました。この問題は、OpenTelemetryのコンテキストが非同期処理で適切に伝播されないことが原因でした。\nカスタムトレースIDを生成し、グローバルコンテキストとして明示的に設定することで、すべてのアクティビティを1つのトレースにまとめることができました。修正後は、タイムライン表示で処理フローが可視化され、ボトルネックの特定も容易になりました。\n非同期処理を多用するアプリケーションでOpenTelemetryを使用する際は、コンテキストの明示的な管理が重要です。今回の経験が、同様の問題に直面している方の参考になれば幸いです。\n参考リンク # LiveKit Agents GitHub Langfuse統合ガイド Langfuse統合アナウンス OpenTelemetry公式ドキュメント LiveKit料金プラン 関連GitHub Discussion ","date":"2025年11月30日","externalUrl":null,"permalink":"/posts/2025-11-30-%E9%9F%B3%E5%A3%B0ai%E3%82%A8%E3%83%BC%E3%82%B8%E3%82%A7%E3%83%B3%E3%83%88livekit-langfuse%E9%80%A3%E6%90%BA-%E3%83%88%E3%83%AC%E3%83%BC%E3%82%B9%E5%88%86%E9%9B%A2%E5%95%8F%E9%A1%8C%E3%81%AE%E8%A7%A3%E6%B1%BA/","section":"Posts","summary":"はじめに # LiveKit Agentsは、音声AIアプリケーションを構築するためのオープンソースフレームワークです。本記事では、Langfuseを使った観測可能性の実装と、その際に遭遇したトレース分離問題の解決方法を紹介します。\n","title":"音声AIエージェントLiveKit × Langfuse連携 ~トレース分離問題の解決~","type":"posts"},{"content":"","date":"2025年11月13日","externalUrl":null,"permalink":"/tags/dataset/","section":"タグ","summary":"","title":"Dataset","type":"tags"},{"content":"先日、新規アプリケーションのプロンプトを検討するにあたり、トレースデータ（ログ）が存在しない状態からデータセットを作成する必要がありました。\nある程度のデータ量を用意したかったため、手動入力を避ける方法（SDK や CSV）を調査・検証しました。 本記事では、基本となる UI での登録手順と、今回試した一括登録の手順をそれぞれ整理し、使い勝手や特徴を比較した備忘録として残します。\n利用バージョン\nLangfuse: v3.127.0 OSS Python SDK: 3.9.0 全体の流れ\nDataset が利用できるまでに以下の手続きが必要です。\nデータセット（dataset）の作成 データセットアイテム（items）の作成 本記事は、公式ドキュメントのこちらの記事 を参考に実施しました。\n１．データセット（dataset）の作成 # UIを利用する方法 # Datasets へ遷移し、[+ New Dataset] をクリックすることで新規のデータセットが作成できます。\nName のみ指定し、[Create dataset] で作成完了です。\n特に複雑な操作も無く、直感的に作成できました。\nSDKを利用する方法 # 今回はPython SDKを利用したので、Python のサンプルコードとなります。\nこちらも既に アプリケーション内で Langfuse を利用したことがある方であれば、特に迷うことなく利用できるのではないかと思います。\nlangfuse.create_dataset( name=[データセット名], ) name をキーとした UPSERT が行われる仕様のようです。description や metadata を追加で指定したり、既に設定されている値を変更したりすると、データセットが更新されたことを UI 上で確認できました。\nただし、オプション未指定（または None ）の場合は更新されず、既に設定されているものがそのまま残る挙動を確認しました。\nまた、データセット名がキーとなっているため、データセット名自体の変更に SDK は利用できません。名称を変更したい場合は、UI から操作する必要があります。\n２，データセット（items）の作成 # UIを利用する方法 # 作成したデータセットに対し、UI または CSV でアイテムの追加が可能です。\n追加したいデータセットをクリックすると、デフォルトでは Runs タブが表示されるため、 Itemsタブに切り替えます。ここで UI での追加と、CSV の追加が行えます。\nUIで一つずつ追加する # [+New item] から追加します。\nJSON 形式で記述する必要がありますが、ひとまずは Input のみ指定すれば [Add to dataset] で追加できます。\nJSON 形式のハードルが高いことを除けば、こちらも複雑な操作は必要なく、概ね直感的な操作で作成出来ました。\nCSVを利用して一括で追加する # [Upload CSV] をクリックすると CSV のアップロード画面が表示されます。\nCSV を用いたアップロードでは、先の UI 同様、 Input, Expected Output, Metadata のみが登録可能です。なお、CSVアップロードによる一括での UPSERT はできないようです。\nCSV 登録の特徴として、「CSV のどの項目を各フィールドに割り当てるか」を UI 上でマッピング出来ることが挙げられます。この機能により、事前に アップロードフォーマットに合わせた CSV 形式への加工や、値をわざわざ JSON 形式に変換する必要が基本的にはない点は大きなメリットです。\n例として、以下のような CSV を 作成し、UIから取り込んでみました。\nid name num 1 apple 100g 2 egg 20 項目をすべて Input にマッピングします。\nすると、ヘッダ行と値が適切に設定された JSON として Input に入力されました。\n形式をあまり意識せずに登録できるため、既存データの CSVをとりあえず投げ込んでつくる、といった方法も取れます。個人的には、各種CSVアップロードはフォーマットの調整に時間がかかることが多いので、とても嬉しい機能でした。\nもちろん、データセットからダウンロードしたファイルもそのままアップできます。Input として、ダウンロードした CSV の Input を割り当てれば、{ input: {\u0026hellip;} } のような形式にならず、元のデータセットアイテムと同じ形式で登録されました。\nSDKを利用する方法 # データセット同様、特に難しい点はありませんでした。\nid が省略されている場合は INSERT、指定されている場合は UPSERT となります。\nlangfuse.create_dataset_item( id=[ID], dataset_name=[データセット名], input=[入力データの内容], ) id は省略可能ですが、登録時に指定しておいた方が明示的に管理できるため、SDK を利用して作成する場合は、指定しておく方が利便性が高いと思われます。\n省略した場合はUIからの登録同様、自動的にランダムな ID が付与されます。\n指定する際の注意点\n他のデータセットで既に利用している ID は利用できません。\n（異なる組織、またはプロジェクトであれば利用可能です）\nCSVからの一括更新が出来ないため、実際は ID を指定しての1つずつの処理にはなりますが、データを全てアーカイブしたい場合など、まとめて更新を行うことが想定される場合には SDK を利用すると良さそうです。\nまとめ # 今回、UI と SDK の両方を触ってみて、データセットアイテムにある程度のデータ量が必要な場合は以下のように使い分けるのが良さそうだと感じました。\nUI (CSV)\nデータの更新が不要な場合 CSV マッピング機能が優秀 時間をかけずにデータを投入したい 登録にあたり一切の開発が不要 SDK\nデータの更新が必要な場合 データ量が膨大な場合 CSV での処理に不安がある量の場合 手元のデータが複雑で、Input 用に何かしらの加工を行う必要がある場合 特に CSV アップロード時のマッピング機能は、データ前処理の手間が省けるので、今後は積極的に使っていきたいと思います。\n","date":"2025年11月13日","externalUrl":null,"permalink":"/posts/2025-11-13-langfuse%E3%83%87%E3%83%BC%E3%82%BF%E3%82%BB%E3%83%83%E3%83%88%E6%A7%8B%E7%AF%89%E3%82%AC%E3%82%A4%E3%83%89-uicsvsdk%E3%81%AE%E5%BE%B9%E5%BA%95%E6%AF%94%E8%BC%83/","section":"Posts","summary":"先日、新規アプリケーションのプロンプトを検討するにあたり、トレースデータ（ログ）が存在しない状態からデータセットを作成する必要がありました。\nある程度のデータ量を用意したかったため、手動入力を避ける方法（SDK や CSV）を調査・検証しました。 本記事では、基本となる UI での登録手順と、今回試した一括登録の手順をそれぞれ整理し、使い勝手や特徴を比較した備忘録として残します。\n","title":"Langfuseデータセット構築ガイド：UI・CSV・SDKの徹底比較","type":"posts"},{"content":"以前、Langfuse の mask オプションを利用する際のトレース保存方法について解説しました。（該当記事 ）\n当時、 mask オプションに設定した関数内でトレースを保存しようとすると、該当の関数が再帰的に呼び出されてしまう問題がありました。該当記事内では、グローバル変数を用いて制御しましたがあまりスマートな方法とは言えません。\nしかし、この課題をよりスマートに解決出来そうなアップデートが行われました。\n実際にどのように解決していけるか、試してみたいと思います。\n以前のコードと課題 # 以前のコードでは、masking_function が再帰的に呼び出されないよう、グローバル変数で制御していました。\nrequire_mask = True def masking_function(data: any, kwargs) -\u0026gt; any: global require_mask if require_mask and isinstance(data, str): try: require_mask = False # PII フィルター適用時のトレース保存 with langfuse.start_as_current_generation( name=\u0026#34;pii_filter_prompt\u0026#34;, prompt=[使用するプロンプト] ) as generation: # LLM呼び出し（マスキング処理） generation.update( output=[フィルター済みデータ], ) finally: require_mask = True return [フィルター済みデータ] langfuse = Langfuse( public_key=\u0026#34;**************\u0026#34;, secret_key=\u0026#34;****************\u0026#34;, host=\u0026#34;*****************\u0026#34;, mask=masking_function, ) # LLM呼び出しのトレース with langfuse.start_as_current_generation( name=\u0026#34;llm_called_trace\u0026#34;, input=[ユーザの入力], ) as generation: # LLM呼び出し（入力に対する回答取得用） generation.update( output=[LLMの回答], ) このアプローチでは、上記のような簡単なコードであればさほど難しくはありませんが、規模が大きくなるにつれ、コードや管理が複雑になってしまいます。\nLangfuse アップデート：Observation Typesの追加 # 2025年8月末頃のアップデートで、Observation Types が追加されました。（該当記事 ）\nこれまでも、 span や generation はありましたが、 guardrail や evaluator など、多くの種類が追加されました。\nこちらのアップデート記事を読んだ際に、Observation Types = \u0026ldquo;guardrail\u0026rdquo; としてトレースを保存すれば、Langfuse で良い感じに処理して再帰的に mask オプションの関数が呼ばれることは無くなるのでは？と期待しました。\nそこで、実際に以前の実験時に利用した、上記のコードを基に再起呼び出しを防ぐ対応が不要になるか試してみました。\n１．Observation Types = \u0026ldquo;guardrail\u0026rdquo; の利用 # 事前準備 # Observation Types の追加は Python SDK 3.3.1 以上でなければ対応していないため、利用するSDKをアップグレードします。\nまた、トレースの保存方法が start_as_current_generation から start_as_current_observation (as_type=\u0026ldquo;generation\u0026rdquo;) と少々変更されているため、合わせて調整します。\n# 念のための制御用 loop = 0 def masking_function(data: any, kwargs) -\u0026gt; any: global loop if loop \u0026gt; 5: return data if isinstance(data, str): loop += 1 # PII フィルター適用時のトレース保存 with langfuse.start_as_current_observation( as_type=\u0026#34;guardrail\u0026#34;, name=f\u0026#34;in masking function: {loop}\u0026#34;, prompt=[使用するプロンプト] ) as guardrail: # マスキング処理 sanitized_data = f\u0026#34;sanitized data:{data}\u0026#34; guardrail.update(output=sanitized_data) return sanitized_data else: return data # Langfuseの初期化 langfuse = Langfuse( public_key=\u0026#34;**************\u0026#34;, secret_key=\u0026#34;****************\u0026#34;, host=\u0026#34;*****************\u0026#34;, mask=masking_function, ) # LLM呼び出しのトレース with langfuse.start_as_current_observation( as_type=\u0026#34;generation\u0026#34;, name=\u0026#34;test_type_guardrails\u0026#34;, input=[ユーザの入力], ) as generation: # LLM呼び出し（入力に対する回答取得用） generation.update( output=[LLMの回答], ) 無限ループに陥らないよう、念のため複数回再帰的に呼び出されたタイミングで終了するようにしました。\nこちらで実行してみます。\n結果 # 残念ながら、 observation type = \u0026ldquo;guardrail\u0026rdquo; の指定だけでは、再帰的に実行されてしまう状態は変わらないようです。\n２．OpenTelemetry の利用 # Langfuseは内部でOpenTelemetryを利用しており、これを使うことでmasking_functionの呼び出し元のトレース情報を取得できることがわかりました 。\nトレース情報からObservationTypesを取得し、判断に利用すれば、グローバル変数を利用する形よりはいくらかスマートに対応できます。\n修正 # from opentelemetry import trace as otel_trace_api from langfuse._client.attributes import LangfuseOtelSpanAttributes // 中略 def masking_function(data: any, kwargs) -\u0026gt; any: current_observation_type = None try: current_span = otel_trace_api.get_current_span() if current_span and current_span != otel_trace_api.INVALID_SPAN: current_observation_type = current_span.attributes.get( LangfuseOtelSpanAttributes.OBSERVATION_TYPE, \u0026#34;unknown\u0026#34; ) except Exception as e: print(f\u0026#34;Error getting current observation info: {e}\u0026#34;) return data # guardrailの場合はマスキングを行わない if current_observation_type == \u0026#34;guardrail\u0026#34;: return data if isinstance(data, str): with langfuse.start_as_current_observation( as_type=\u0026#34;guardrail\u0026#34;, name=\u0026#34;in masking function\u0026#34;, prompt=[使用するプロンプト] ) as guardrail: # マスキング処理 sanitized_data = f\u0026#34;sanitized data:{data}\u0026#34; guardrail.update(output=sanitized_data) return sanitized_data else: return data opentelemetry.trace.get_current_span() を利用して、現在のトレースの情報を取得します。\n今回は、マスク用の関数内以外では as_type = \u0026ldquo;guardrail\u0026rdquo; の指定を利用しないこと、マスク用の関数内で別のObservation Typeを設定しないことを前提としています。そのため、単純に取得した observation type が guardrail の場合は、masking_function内での呼びだしとみなし、処理をせずに入力値をそのまま返す形にしています。\n結果 # 無事、Observation Typeを取得し、制御することに成功しました！\nこれにより、 mask オプションを利用する際も、グローバル変数を利用せずトレースの保存を制御できることが確認出来ました。\nまとめ # Observation Types の追加によって、mask関数の再帰呼び出しが自動的に回避されるといった変更はありませんでした。しかし、期待した方法とは異なりますが、Observation TypesとOpenTelemetryを組み合わせることで、これまでよりもはるかにスマートな方法で、トレース保存を制御できることが確認できました 。\n","date":"2025年10月28日","externalUrl":null,"permalink":"/posts/2025-10-28-observation-types-%E3%81%A7-mask-%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3%E5%86%85%E3%81%A7%E3%81%AE%E5%86%8D%E8%B5%B7%E5%91%BC%E3%81%B3%E5%87%BA%E3%81%97%E3%82%92%E5%9B%9E%E9%81%BF%E3%81%99%E3%82%8B/","section":"Posts","summary":"以前、Langfuse の mask オプションを利用する際のトレース保存方法について解説しました。（該当記事 ）\n当時、 mask オプションに設定した関数内でトレースを保存しようとすると、該当の関数が再帰的に呼び出されてしまう問題がありました。該当記事内では、グローバル変数を用いて制御しましたがあまりスマートな方法とは言えません。\n","title":"Observation Types で mask オプション内での再起呼び出しを回避する","type":"posts"},{"content":"","date":"2025年10月21日","externalUrl":null,"permalink":"/tags/a2a/","section":"タグ","summary":"","title":"A2A","type":"tags"},{"content":" はじめに # 2025年4月9日、GoogleがAgent2Agent（A2A）プロトコルを発表 してから半年以上が経過し、多くの開発者がマルチエージェントシステムの構築に取り組んでいます。\nA2Aは複雑なコンポーネント構成とエージェント間通信を持つため、処理フローをトレースとして可視化することがLLM Opsにおいて重要です。\n本記事では、LangfuseとCloud Traceを使用してA2A × ADKエージェントの挙動を観測し、実用的な分析のための観測粒度の最適化方法を解説します。\n実装環境 # 使用したライブラリ # 本記事で使用したライブラリとそのバージョンは以下の通りです。\nrequirements.txt\ngoogle-adk[a2a] google-genai langfuse python-dotenv opentelemetry-instrumentation-google-genai opentelemetry-exporter-gcp-logging opentelemetry-exporter-gcp-monitoring opentelemetry-exporter-otlp-proto-grpc opentelemetry-instrumentation-vertexai\u0026gt;=2.0b0 実際にインストールされたバージョン（pip list）\n- Python: 3.12.4 - google-genai: 1.39.1 - langfuse: 3.5.2 - opentelemetry-instrumentation-google-genai: 0.1.5 - opentelemetry-instrumentation-vertexai: 0.1.11 - python-dotenv: 1.1.1 エージェント構成 # 今回は、ADK Multi-tool Agent Sample をベースに、東京の天気と現在時刻を取得するシンプルなエージェントを構築しました。2つのツール（get_weatherとget_current_time）を持つエージェントです。\nfrom datetime import datetime from zoneinfo import ZoneInfo from google.adk.agents import Agent def get_weather(city: str) -\u0026gt; dict: \u0026#34;\u0026#34;\u0026#34;指定された都市の天気情報を取得\u0026#34;\u0026#34;\u0026#34; if city.lower() == \u0026#34;東京\u0026#34;: return { \u0026#34;status\u0026#34;: \u0026#34;success\u0026#34;, \u0026#34;report\u0026#34;: ( \u0026#34;東京の天気は晴れで、気温は25度です。\u0026#34; ), } else: return { \u0026#34;status\u0026#34;: \u0026#34;error\u0026#34;, \u0026#34;error_message\u0026#34;: f\u0026#34;\u0026#39;{city}\u0026#39;の天気情報は利用できません。\u0026#34;, } def get_current_time(city: str) -\u0026gt; dict: \u0026#34;\u0026#34;\u0026#34;指定された都市の現在時刻を取得\u0026#34;\u0026#34;\u0026#34; if city.lower() == \u0026#34;東京\u0026#34;: tz_identifier = \u0026#34;Asia/Tokyo\u0026#34; else: return { \u0026#34;status\u0026#34;: \u0026#34;error\u0026#34;, \u0026#34;error_message\u0026#34;: ( f\u0026#34;\u0026#39;{city}\u0026#39;のタイムゾーン情報は利用できません。\u0026#34; ), } tz = ZoneInfo(tz_identifier) now = datetime.now(tz) report = ( f\u0026#39;{city}の現在時刻は{now.strftime(\u0026#34;%Y-%m-%d %H:%M:%S %Z%z\u0026#34;)}です。\u0026#39; ) return {\u0026#34;status\u0026#34;: \u0026#34;success\u0026#34;, \u0026#34;report\u0026#34;: report} root_agent = Agent( name=\u0026#34;weather_time_agent\u0026#34;, model=\u0026#34;gemini-2.5-flash-lite\u0026#34;, description=( \u0026#34;東京の天気と現在時刻を取得するエージェントです。\u0026#34; ), instruction=( \u0026#34;東京の天気と現在時刻を取得するエージェントです。\u0026#34; ), tools=[get_weather, get_current_time], ) A2Aの実装パターン # A2Aには主に2つの実装パターンがあります。「Exposingパターン」と「Consumingパターン」です。今回は両方の方法でエージェントを実装し、トレースの挙動を確認しました。\n共通設定 # 両パターンで共通して使用するサーバー設定は以下の通りです。\nimport uvicorn from google.adk.agents import ADKAgentExecutor from a2a.server import A2AStarletteApplication, DefaultRequestHandler from a2a.storage import InMemoryTaskStore request_handler = DefaultRequestHandler( agent_executor=ADKAgentExecutor(agent=root_agent), task_store=InMemoryTaskStore(), ) server = A2AStarletteApplication( agent_card=public_agent_card, http_handler=request_handler, ) uvicorn.run(server.build(), host=\u0026#39;0.0.0.0\u0026#39;, port=9999) Exposingパターン # Exposingパターンは、自作のエージェントをA2Aプロトコルで公開する方式です。自社で開発したエージェントを外部に公開し、他のチームやシステムが利用できるエージェントサービスを提供する際に使用します。\nA2Aサーバーは、エージェントのメタデータを記述したAgent Cardを自動的に生成し、well-known URLで公開します。クライアントはこのAgent Cardを参照することで、エージェントの機能や入出力形式を理解できます。\nfrom google.adk.a2a import to_a2a # ADKエージェントをA2Aサーバーとして公開 a2a_app = to_a2a(root_agent, port=8001) 実装の詳細は、ADK公式ドキュメントのクイックスタート(Exposing) を参考にしました。\nConsumingパターン # Consumingパターンは、既存のA2Aエージェントをリモートから利用する方式です。外部ベンダーが提供するA2Aエージェントを活用する際に使用します。\nfrom google.adk.a2a import RemoteA2aAgent, AGENT_CARD_WELL_KNOWN_PATH from google.adk.agents import Agent weather_time_agent = RemoteA2aAgent( name=\u0026#34;weather_time_agent\u0026#34;, description=( \u0026#34;東京の天気と現在時刻を取得するエージェントです。\u0026#34; ), agent_card=f\u0026#34;http://localhost:8001/a2a/weather_time_agent{AGENT_CARD_WELL_KNOWN_PATH}\u0026#34;, ) root_agent = Agent( model=\u0026#34;gemini-2.5-flash-lite\u0026#34;, name=\u0026#34;root_agent\u0026#34;, instruction=( \u0026#34;日本語で回答して\u0026#34; ), sub_agents=[weather_time_agent], ) 実装の詳細は、ADK公式ドキュメントのクイックスタート(Consuming) を参考にしました。\nAgent Card # Agent Cardは、エージェントのメタデータを定義するJSONファイルで、Consumingパターンにおいて重要な役割を果たします。以下は今回使用したAgent Cardの例です。\n{ \u0026#34;capabilities\u0026#34;: {}, \u0026#34;defaultInputModes\u0026#34;: [\u0026#34;text/plain\u0026#34;], \u0026#34;defaultOutputModes\u0026#34;: [\u0026#34;text/plain\u0026#34;], \u0026#34;description\u0026#34;: \u0026#34;東京の天気と現在時刻を取得するエージェントです。\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;weather_time_agent\u0026#34;, \u0026#34;skills\u0026#34;: [ { \u0026#34;id\u0026#34;: \u0026#34;weather_time_checking\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;Weather and Time Checking\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;東京の天気と現在時刻を取得します。\u0026#34;, \u0026#34;tags\u0026#34;: [\u0026#34;weather\u0026#34;, \u0026#34;time\u0026#34;] } ], \u0026#34;url\u0026#34;: \u0026#34;http://localhost:8001/a2a/weather_time_agent\u0026#34;, \u0026#34;version\u0026#34;: \u0026#34;1.0.0\u0026#34; } Langfuseでのトレース取得 # Langfuseは、LLMアプリケーションのトレーシングと評価に特化したオープンソースツールです。OpenTelemetryと統合されており、A2A × ADKエージェントのトレースを簡単に取得できます。\n基本的な実装方法 # A2AやADKを使用する場合、Langfuseでトレースを取得する方法はシンプルで、Langfuse Clientを初期化するだけで、自動的にトレースが収集されます。※LANGFUSE_HOSTなどの環境変数はあらかじめ設定しておきます\nfrom langfuse import get_client langfuse = get_client() これだけで、A2AやADKが生成するすべてのspanが自動的にLangfuseに送信されます。明示的にspanの設定を記述する必要がないのは、OpenTelemetryのインストルメンテーションライブラリが自動的にspanを生成するためです。\nExposingパターンとConsumingパターンの違い # ExposingとConsuming両方のパターンでトレースを取得しましたが、構造や内容に差は見受けられませんでした。\nバッチ処理での結果 # まず、バッチ処理でのトレース結果を見てみましょう。\nLangfuseのTrace表示（バッチ） トレース構造の特徴 # バッチ処理では、1つのトレース内に大量のspanが生成されます。具体的には以下の構成になっています。\nA2A関連のspan: 約35個 ADK関連のspan: 約5個 合計: 約40個のspan これらのspanは階層的に配置され、エージェントの処理フローを詳細に記録しています。\nストリーミング処理での結果 # 次に、ストリーミング処理でのトレース結果を確認します。\nLangfuseのTrace表示（ストリーミング） トレース構造の特徴 # ストリーミング処理では、バッチ処理とは大きく異なるトレース構造が生成されます。\n複数の大きなトレース: それぞれがストリーミングレスポンスの断片に対応\n各トレースには約40個のspan（A2A: 35個 + ADK: 5個）が含まれる 約30個の独立した小さなトレース:\n各トレースのspan数は1~3個と少ない バッチ処理の各spanがトレースとして独立したような構造です Langfuse ストリーミング処理での小さなTrace バッチとの違い # spanの名前から判断すると、ストリーミング処理ではバッチ処理の各処理単位が独立したトレースになっていることがわかります。これは、ストリーミングの性質上、処理が分割されて実行されるためです。\nバッチ vs ストリーミングの数値比較\n項目 バッチ ストリーミング 大きなトレース数 1個 1個~ 大きなトレース1つあたりのspan数 約40個 約40個 小さな独立トレース数 0個 約30個 トレース内容の詳細分析 # トレースの内容を詳しく見ていくと、いくつかの興味深い特徴が見えてきます。\nEventQueue関連spanの多さ # トレースを観察すると、その大半をEventQueue関連のspanが占有していることがわかります。生成AIのリクエスト/レスポンス以外の処理で、膨大な数のspanが生成されています。\nこれらのspanは、A2AやADKの内部処理に関連しています。詳細な動作については、ライブラリの実装コードを参照する必要がありますが、イベント駆動型のアーキテクチャによる処理フローであることが考えられます。\nEventQueue.deque_event の連続発生 # 特に注目すべき点として、0.5秒継続するEventQueue.deque_eventspanが連続で発生しています。これは処理待ちやイベントキューの処理に起因すると推測されます。\n重要な注意点として、実際のエージェントのレスポンス速度は速い可能性があります。 しかし、イベント処理が具体的に何を行っているか不明なため、LangfuseのUIに表示されるLatencyの値は、その内部処理時間を含んで大きくなってしまいます。この現象が、パフォーマンス分析を複雑にする要因の一つです。\n複数の EventQueue.deque_event の処理で0.5秒費やされている様子 観測からの示唆 # A2A × ADKのトレース構造を観察した結果、A2A層は内部イベント処理が非常に詳細に出力されることが分かりました。これはA2Aの可視化が成功している証拠ですが、一方でLLM Ops的観点では情報過多となり、分析効率を下げる要因にもなります。\n具体的には:\nA2A関連のspanが34~35個と非常に多く、トレース全体の大部分を占める\nLLMの推論処理やツール実行といった、アプリケーションレベルで重要な情報が埋もれてしまう\nEventQueueの内部処理など、通常のデバッグでは不要な詳細情報が大量に含まれる\nそこで次に、「A2Aの内部動作を理解した上で、どこまで観測すべきか」という「観測粒度の設計」を紹介します。\nCloud Traceでのトレース取得 # Cloud Traceは、Google Cloudが提供する分散トレーシングサービスです。OpenTelemetryと統合されており、A2A × ADKエージェントのトレースをGoogle Cloudの観測可能性プラットフォームに送信できます。\n実装方法 # Cloud Traceを使用するには、OpenTelemetryの初期化とGoogle Cloud Observabilityの設定が必要です。Google Cloud 公式 ドキュメント に従って実装しました。\nimport google.auth import google.auth.transport.requests import grpc from opentelemetry import trace, logs, metrics from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opentelemetry.sdk._logs import LoggerProvider from opentelemetry.sdk._logs.export import BatchLogRecordProcessor from opentelemetry.exporter.cloud_logging import CloudLoggingExporter from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader from opentelemetry.exporter.cloud_monitoring import CloudMonitoringMetricsExporter from opentelemetry.sdk.resources import Resource from opentelemetry.semconv.resource import ResourceAttributes from opentelemetry.instrumentation.vertexai import VertexAIInstrumentor from opentelemetry.instrumentation.google_genai import GoogleGenAiSdkInstrumentor from opentelemetry.exporter.otlp.proto.grpc._auth import AuthMetadataPlugin SERVICE_NAME = ResourceAttributes.SERVICE_NAME # OpenTelemetry初期化 def initialize_opentelemetry() -\u0026gt; None: \u0026#34;\u0026#34;\u0026#34;OpenTelemetryの初期化とGoogle Cloud Observabilityの設定\u0026#34;\u0026#34;\u0026#34; credentials, project_id = google.auth.default() resource = Resource.create( attributes={ SERVICE_NAME: \u0026#34;multi-tool-agent\u0026#34;, # The project to send spans to \u0026#34;gcp.project_id\u0026#34;: project_id, } ) # Set up OTLP auth request = google.auth.transport.requests.Request() auth_metadata_plugin = AuthMetadataPlugin(credentials=credentials, request=request) channel_creds = grpc.composite_channel_credentials( grpc.ssl_channel_credentials(), grpc.metadata_call_credentials(auth_metadata_plugin), ) # Set up OpenTelemetry Python SDK tracer_provider = TracerProvider(resource=resource) tracer_provider.add_span_processor( BatchSpanProcessor( OTLPSpanExporter( credentials=channel_creds, endpoint=\u0026#34;https://telemetry.googleapis.com:443/v1/traces\u0026#34;, ) ) ) trace.set_tracer_provider(tracer_provider) logger_provider = LoggerProvider(resource=resource) logger_provider.add_log_record_processor( BatchLogRecordProcessor(CloudLoggingExporter()) ) logs.set_logger_provider(logger_provider) reader = PeriodicExportingMetricReader(CloudMonitoringMetricsExporter()) meter_provider = MeterProvider(metric_readers=[reader], resource=resource) metrics.set_meter_provider(meter_provider) # Load instrumentors # ADK uses Vertex AI and Google Gen AI SDKs. VertexAIInstrumentor().instrument() GoogleGenAiSdkInstrumentor().instrument() print(f\u0026#34;OpenTelemetryが初期化されました。Google Cloud Project ID: {project_id}\u0026#34;) # OpenTelemetryの初期化を実行 initialize_opentelemetry() この設定により、A2A × ADKエージェントのすべてのトレースがCloud Traceに送信されます。\n取得結果 # Cloud Traceで取得したトレースを確認してみましょう。\nCloud TraceのTrace表示 Cloud Traceで取得したトレース構造は、Langfuseで取得したものと一致しています:\nバッチ処理: 1つのトレースに大量のspan（39個） ストリーミング処理: 複数の大きなトレース + 多数の小さな独立トレース OpenTelemetryという共通の基盤を使用しているため、同じトレースデータが異なるUIで表示されているだけです。それぞれのツールの利点を活かして、目的に応じて使い分けることができます。\n観測粒度の最適化（フィルタリング） # ここまで見てきたように、A2A × ADKエージェントは非常に多くのspanを生成します。これは詳細な情報を得られる反面、重要なspanが埋もれてしまうという問題があります。\n課題 # 具体的な課題は以下の通りです:\nA2A関連のspan（34~35個）がトレースを複雑化 ADKの重要なspan（5個）が埋もれてしまう 分析やデバッグの効率が低下 例えば、LLMの推論処理だけを見たい場合、A2Aの内部処理に関する膨大なspanは不要です。このような場合、Langfuseの計測スコープフィルタリング機能が非常に有用です。\nLangfuseの計測スコープフィルタリング機能 # 計測スコープとは # 計測スコープ（Instrumentation Scope） は、インストルメンテーションライブラリがspanに付与するメタデータです。どのライブラリがspanを生成したかを識別するための情報で、Langfuse UIでは metadata.scope.name として表示されます。\n今回のA2A × ADKエージェントでは、主に以下の計測スコープが使用されています:\na2a-python-sdk: A2A関連のspan gcp.vertex.agent: ADK関連のspan この情報を活用することで、特定のライブラリが生成したspanをフィルタリングできます。\nフィルタリングの実装 # 基本的な設定方法 # Langfuseのクライアント初期化時に、blocked_instrumentation_scopesパラメータを指定することで、特定の計測スコープのspanをフィルタリングできます。\nfrom langfuse import Langfuse # フィルタリング機能使用時 langfuse = Langfuse( blocked_instrumentation_scopes=[\u0026#34;a2a-python-sdk\u0026#34;, \u0026#34;gcp.vertex.agent\u0026#34;] ) A2Aのみを削除のパターンの場合 # 最も実用的なパターンは、A2A関連のspanのみを削除する方法です。\nA2Aの内部処理はOpenTelemetryで詳細にトレースされますが、LLM推論やADKツール呼び出しの可視化を主目的とする場合は、A2Aの内部spanを除外した\u0026quot;要約的トレース\u0026quot;が実務上最適です。これはA2Aの動作を十分理解した上で、可視化の粒度を最適化するというアプローチです。\nlangfuse = Langfuse(blocked_instrumentation_scopes=[\u0026#34;a2a-python-sdk\u0026#34;]) a2a-python-sdkを除外したTrace 結果:\nADKの5つのspanのみが残り、シンプルなトレースになります LLMの推論処理や各ツールの実行状況が明確に可視化されます ただし、これらのspanが名前のないトレースの子として配置されます トレース一覧で区別しにくくなるという問題がありますが、LLMの処理部分が見えているため、多くの場合で有効な方法です フィルタリングの注意点 # Langfuse公式ドキュメントの計測スコープフィルタリングの警告 トレースツリーの破壊問題 # Langfuse公式ドキュメント には、以下の警告が記載されています:\n「ブロックされたライブラリとブロックされていないライブラリの範囲がネストされている場合、特定のライブラリをブロックするとトレースツリーの関係が壊れる可能性がある」\nこれは、A2A × ADKのようにライブラリのspanがネストされている場合に発生する問題です。親spanがフィルタリングされると、子spanが孤立してしまうのです。\n名前のないトレース問題 # 親spanがフィルタリングされたため、子spanが孤立し、名前のないトレースの子として配置されてしまいます。これにより、トレース一覧で区別しにくくなります。\n推奨されるフィルタリングパターン # 目的に応じて、以下のパターンから選択することをおすすめします:\nフィルタリングパターンと結果:\nパターン A2A span ADK span トレース構造 用途 フィルタなし ◯ ◯ 正常 全体把握・詳細デバッグ A2Aのみ削除 × ◯ 破壊 ※1 LLM処理分析（推奨） ADKのみ削除 ◯ × 正常 LLM処理が見えないため非推奨 両方削除 × × - ※2 手動トレース設定時（高度な用途） ※1 子spanが孤立し、「名前のないトレース」の子になる。\n※2 自動生成トレースが無効になる。\nパターン別の詳細 # 1. フィルタなし（デフォルト）\nすべてのspanを含めて全体像を把握したい場合 システム全体のフローを理解したい初期段階 詳細なデバッグが必要な場合 2. A2Aのみ削除（実用的）\nLLMの推論処理に焦点を当てたい場合 ツールの実行状況を明確に可視化したい場合 トレース構造は破壊され名前のないトレースになりますが、実用上は妥協可能な範囲 3. ADKのみ削除（非推奨）\nA2Aの内部処理を詳細に見たい場合 ただし、LLMの処理部分が見えなくなるため、このパターンはあまり採用する意味がありません 4. 両方削除（高度な用途）\n自動生成されるトレースを完全に無効化 トレースの手動設定\u0026amp;送信ができるため、細かくトレースの中身を設定したい場合は一番推奨できるパターン より細かい粒度でトレースをカスタマイズしたい上級者向け まとめ # トレーシングのポイント # A2A × ADKエージェントのトレーシングについて、以下の重要なポイントを確認しました:\nLangfuse/Cloud Trace両方でOpenTelemetry経由の自動トレース取得が可能\nLangfuse Clientの初期化だけで、簡単にトレースが収集される インストルメンテーションライブラリによる自動span生成が機能 バッチとストリーミングで大きく異なるトレース構造\nバッチ: 1つのトレースに約39個のspan ストリーミング: 複数の大きなトレース + 約30個の小さな独立トレース EventQueue関連のspanが大部分を占める\nトレースの大半がA2Aの内部処理に関連 EventQueue.deque_eventの連続発生がLatency表示値に影響（実際のレスポンス速度とは別） フィルタリング機能の活用 # Langfuseの計測スコープフィルタリング機能を効果的に活用するためのポイント:\n計測スコープによるフィルタリングで不要なspanを削除可能\nblocked_instrumentation_scopesパラメータを使用 a2a-python-sdkやgcp.vertex.agentなどの計測スコープを指定 トレース構造の破壊に注意が必要\nネストされたspanの親をフィルタリングすると、子spanが孤立 名前のないトレース問題が発生する可能性 目的に応じた適切なフィルタリング設定を選択\nLLM処理分析: A2Aのみ削除（最も実用的） 全体把握: フィルタなし 高度なカスタマイズ: 両方削除 + 手動トレース設定 今後の展望と注意事項 # A2A × ADKエージェントのトレーシングには、まだ解明されていない部分もあります:\nEventQueueの詳細な動作解明\nなぜこれほど多くのspanが生成されるのか 内部処理の最適化の可能性 ストリーミング時のspan分割メカニズムの理解\nどのようなロジックでトレースが分割されるのか パフォーマンスへの影響 より効率的なトレース分析手法の確立\nカスタムメトリクスの追加 トレースの可視化方法の改善 注意: A2Aのトレース構造は公式が今後変更する可能性があります。 本記事の内容は現時点（2025年10月）での実装に基づいており、将来的にライブラリのアップデートによってトレース構造やspan数が変わる可能性があることにご注意ください。\nA2A × ADKエージェントの観測可能性を高めることで、より効率的なシステム開発とデバッグが可能になります。本記事で紹介したトレーシングとフィルタリングの技術が、皆さんのエージェント開発の一助となれば幸いです。\n参考資料 # 本記事の実装にあたり、以下の公式ドキュメントを参考にしました:\nADK公式ドキュメント - クイックスタート(Exposing) ADK公式ドキュメント - クイックスタート(Consuming) ADK Multi-tool Agent Sample Cloud Trace - AI Agent ADK のインストルメンテーション Langfuse公式ドキュメント - 計測スコープのフィルタリング ","date":"2025年10月21日","externalUrl":null,"permalink":"/posts/2025-10-21-a2a-adk%E3%81%AE-%E8%A6%B3%E6%B8%AC%E7%B2%92%E5%BA%A6-%E3%82%92%E8%A8%AD%E8%A8%88%E3%81%99%E3%82%8B-langfuse-cloud-trace-%E3%81%A7%E3%83%88%E3%83%AC%E3%83%BC%E3%82%B9%E6%A7%8B%E9%80%A0%E3%82%92%E5%8F%AF%E8%A6%96%E5%8C%96/","section":"Posts","summary":"はじめに # 2025年4月9日、GoogleがAgent2Agent（A2A）プロトコルを発表 してから半年以上が経過し、多くの開発者がマルチエージェントシステムの構築に取り組んでいます。\n","title":"A2A × ADKの\"観測粒度\"を設計する - Langfuse \u0026 Cloud Trace でトレース構造を可視化 -","type":"posts"},{"content":"","date":"2025年10月21日","externalUrl":null,"permalink":"/tags/cloud-trace/","section":"タグ","summary":"","title":"Cloud Trace","type":"tags"},{"content":"","date":"2025年10月2日","externalUrl":null,"permalink":"/categories/genaiops/","section":"カテゴリ","summary":"","title":"GenAIOps","type":"categories"},{"content":"","date":"2025年10月2日","externalUrl":null,"permalink":"/tags/genaiops/","section":"タグ","summary":"","title":"GenAIOps","type":"tags"},{"content":"","date":"2025年10月2日","externalUrl":null,"permalink":"/tags/ragas/","section":"タグ","summary":"","title":"Ragas","type":"tags"},{"content":" はじめに # RAG（Retrieval-Augmented Generation）は、外部知識を参照してLLMの回答精度を向上させる強力な技術です。\nしかし、多くの開発者が「RAGを作ったはいいものの、その性能をどう客観的に評価すればいいのか分からない」という壁に直面しています。\n検索結果に不要な情報が混じっている気がする。 たまに事実と異なる回答（ハルシネーション）を生成してしまう。 改善したいが、検索と生成のどちらがボトルネックなのか切り分けられない。 この記事では、RAG評価で広く使われているオープンソースのライブラリRagasを用いて、これらの課題を解決する方法を解説します。\nRagasの主要な評価メトリクスを理解し、自作RAGシステムの課題を特定して改善サイクルを回すための、具体的なノウハウを掴んでいきましょう。\nなお、Ragasは標準メトリクス中心の評価フレームワークであり、ドメイン固有の要件（トーン、規制対応など）にはカスタム評価が必要になる場合があります（本稿末尾の「Ragasでカバーしきれない観点とカスタム評価」を参照してください）。\nRagasの評価フロー図 本稿の対象読者 # RAG/LLMアプリケーションの開発・評価・運用に携わる方 RAGの品質を定量的に測定し、継続的に改善したい方 なぜRAG評価は難しいのか？ # RAGシステムが期待通りに機能しない原因は、主に以下の3つのいずれかに分類できます。\nノイズ（Noise）: 質問と無関係な情報を検索してしまい、回答の精度が下がる。 回収漏れ（Missed Information）: 回答に必要な重要な情報を取り逃してしまい、不完全な回答になる。 幻覚（Hallucination）: 検索した情報から逸脱し、事実に基づかない回答を生成してしまう。 これらの原因を特定するには、RAGのパイプラインを「検索（Retrieval）」と「生成（Generation）」のフェーズに分解し、それぞれの品質を測るメトリクスが必要です。それを実現するのがRagasです。\nRagasとは # Ragasとは、LLMを利用したRAGパイプラインの健全性を測るための様々なメトリクスを提供します。\nここでは特に重要な5つのコアメトリクスを、「検索品質」と「生成品質」の2つのカテゴリに分けて解説します。\nなお、Ragasには本稿で扱っていないメトリクス（例: Context Entities Recall、Noise Sensitivity、マルチモーダル関連 など）も複数存在します。\n本稿では、RAGの実務で特に使用頻度が高いメトリクスに絞って解説しています。メトリクス一覧は公式の「Available Metrics」をご参照ください。\nhttps://docs.ragas.io/en/stable/concepts/metrics/available_metrics/ Ragasへの入力 # Ragasで検索、生成の品質を測るためには以下のようなデータを入力する必要があります。\n利用するメトリクスによって必須入力が異なります。\nquestion: ユーザーからの質問 answer: RAGシステムが生成した回答 contexts: 回答の根拠として検索されたコンテキスト ground_truth: 人間が用意した正解の回答 検索品質を測る # まずは、ユーザーの質問に対して適切な情報を集められているかを確認します。 各メトリクスの目的、必須入力、課題を発見できるか、また、各メトリクスの詳細について以下にまとめます。\nメトリクス 目的 必須入力 どんな課題を発見できるか？ Context Relevance 検索した文脈（コンテキスト）が、そもそも質問に関係あるか question, contexts 検索結果に全く無関係な情報が混じっていないか。基本的な検索アルゴリズムの適合性を確認。 Context Precision 検索した文脈の中に、回答に\u0026quot;本当に必要な\u0026quot;情報がどれだけあるか question, contexts, ground_truth 関連はあるが冗長な情報が多く、ノイズになっていないか。検索結果のS/N比を確認。 Context Recall 回答に必要な情報を、漏れなく検索できているか question, contexts, ground_truth 複雑な質問に対し、回答の根拠となる情報が不足していないか。情報の網羅性を確認。 ▼ 各指標の詳細 # Context Relevance ( コンテキスト の関連性)\n解釈: スコアが低い場合、検索エンジンがユーザーの質問意図を全く理解できていない可能性があります。 典型的な落とし穴: 曖昧なクエリに対して、無関係なドキュメントを大量に返してしまっているケースがあります。 Context Precision( コンテキスト の適合率)\n解釈: スコアが高いほど、検索結果のノイズが少ないことを意味します。低い場合は、検索範囲が広すぎる（拾いすぎ）可能性があります。 典型的な落とし穴: 検索でヒットしたドキュメントのチャンク（分割単位）が長すぎて、質問に関係ない部分までコンテキストに含まれてしまうケースがあります。 Context Recall ( コンテキスト の再現率)\n解釈: スコアが高いほど、必要な情報を漏れなく集められていることを意味します。低い場合は、検索アルゴリズムが見つけるべき情報を見つけられていません。 典型的な落とし穴: 専門用語の言い換えや同義語に対応できず、重要な情報を含むドキュメントを検索できていないケースがあります。 生成品質を測る # 次に、集めた情報に基づいて、LLMが質の高い回答を生成できているかを確認します。\nメトリクス 目的 必須入力 どんな課題を発見できるか？ Faithfulness 回答が、検索した文脈に忠実か（ハルシネーションの抑制） question, answer, contexts LLMが検索結果を無視して、勝手な情報を生成していないか。回答の信頼性を測る最重要指標。 Answer Relevancy 回答が、ユーザーの質問の意図に沿っているか question, answer 回答が冗長だったり、的外れな内容になったりしていないか。ユーザー体験に直結する指標。 ▼ 各指標の詳細 # Faithfulness (忠実度)\n解釈: 最も重要な指標の一つです。スコアが低い場合、たとえ流暢な回答でも、それは信頼できない「ハルシネーション」である可能性が高いです。 典型的な落とし穴: Context Recallが低くて情報が不足しているのに、LLMが無理に回答を生成しようとしてハルシネーションを起こす。原因は生成ではなく検索側にあることも多いです。 Answer Relevancy (回答の関連性)\n解釈: スコアが低い場合、ユーザーは「質問に答えてくれていない」と感じるでしょう。 典型的な落とし穴: 曖昧な質問に対して、LLMが安全策をとり、一般的で当たり障りのない回答を生成してしまうケースがあります。 【実践】Ragasを使ってみる # 1. データの用意 # まず、評価用のデータセットを準備します。最初は10〜30件程度の小さなセットで傾向を掴むのがおすすめですが、本サンプルコードでは2件のデータセットを作成します。 前述の通りRagasにはメトリクスごとに対して必須入力に合わせて、以下のようなデータセットを用意します。\nquestion: ユーザーからの質問 answer: RAGシステムが生成した回答 contexts: 回答の根拠として検索された文脈（文字列のリスト） ground_truth: （Context Recallの評価に必要）人間が用意した正解の回答 2. 実行コード例 # 事前に以下のコマンドで必要なライブラリをインストールし、OpenAIのAPIキーを設定しておきます。\npip install ragas==0.3.5 datasets==4.1.1 export OPENAI_API_KEY=your_api_key 本稿ではRagas 0.3.5を使用しています。バージョンによってAPIが異なる場合がありますので、ご注意ください。 コードの例は下記の通りです。このプログラムを実行すると評価用データセットに対するRagsの評価が出力されます。\nfrom datasets import Dataset from ragas import evaluate from ragas.metrics import ( ContextRelevance, context_precision, context_recall, faithfulness, answer_relevancy, ) # 評価用データセット（辞書形式） data_samples = { \u0026#39;question\u0026#39;: [ \u0026#39;製品Aの保証期間は？\u0026#39;, \u0026#39;ソフトウェアBの推奨OSは？\u0026#39; ], \u0026#39;answer\u0026#39;: [ \u0026#39;製品Aの保証期間は2年です。\u0026#39;, \u0026#39;Windows 11またはmacOS Sonomaが推奨されます。\u0026#39; ], \u0026#39;contexts\u0026#39; : [ [\u0026#39;製品Aの保証は購入日から2年間有効です。\u0026#39;, \u0026#39;配送は通常3営業日以内に行われます。\u0026#39;], [\u0026#39;ソフトウェアBはWindowsの場合はWindows 11で動作します。\u0026#39;, \u0026#39;macOSの場合はmacOS Sonomaです。\u0026#39;] ], \u0026#39;ground_truth\u0026#39;: [ \u0026#39;製品Aの保証期間は購入日から2年間です。\u0026#39;, \u0026#39;ソフトウェアBを利用するための推奨OSは、Windows 11またはmacOS Sonomaです。\u0026#39; ] } dataset = Dataset.from_dict(data_samples) # 評価の実行 result = evaluate( dataset, metrics=[ ContextRelevance(), context_precision, context_recall, faithfulness, answer_relevancy, ] ) # 結果をDataFrameで表示 df_result = result.to_pandas() print(df_result) 3. LangfuseでRagasスコアを記録する # Ragasで算出したスコアをLangfuseに記録することで、個々のリクエストのトレースデータと評価結果を紐づけ、継続的な改善サイクルを実現できます。\nLangfuseの公式ドキュメントでは、Ragas評価をLangfuseに連携する方法が紹介されています。\n公式ドキュメント：https://langfuse.com/docs/scores/model-based-evals/ragas 詳細な実装方法については上記の公式ドキュメントをご参照いただきたいですが、ここでは簡単に説明します。\n基本的な流れは以下の通りです：\nトレースの実行: RAGアプリケーションを実行し、その過程（検索、生成など）をLangfuseでトレース（追跡・記録）します。 評価の準備: Langfuseからトレースされたデータ（質問、コンテキスト、回答など）を取得し、Ragas評価フレームワークに渡します。 スコアの算出: Ragasを使い、忠実性（faithfulness）や回答の関連性（answer_relevancy）といったメトリクスに基づいて評価スコアを計算します。 結果の連携: 算出されたスコアを、対応するトレースに紐付ける形でLangfuseに送信し、ダッシュボード上で結果を可視化・分析します。 結果をどう読み解き、次の一手につなげるか？ # 評価スコアが出たら、次はその数値を元に改善アクションを考えます。\n以下によくある課題（症状）と、確認すべきメトリクス、そして具体的な対策をまとめました。\n課題（症状） 確認すべきメトリクス 主な対策案 回答に嘘や間違いが多い Faithfulness が低い ・まずContext Recallを確認。情報不足が原因の可能性。\n・根拠を明示させるプロンプトにする。\n・不明な場合は「分かりません」と答えるよう指示する。 無関係な情報が多く、回答が冗長 Context Precision が低い、Answer Relevancy が低い ・検索結果の再ランキング（Re-ranking）を導入する。\n・検索で取得するドキュメント数（k）を減らす。\n・ドキュメントのチャンクサイズを短くする。 回答が不十分・情報不足 Context Recall が低い ・検索で取得するドキュメント数（k）を増やす。\n・クエリ拡張（ユーザーの質問を複数のクエリに変換）を試す。\n・ベクトル検索とキーワード検索を組み合わせる。 質問と関係ない回答が返ってくる Context Relevance が低い ・埋め込みモデル（Embedding Model）の再検討。\n・ユーザーの質問の意図を明確にする前処理を追加する。 Ragasでカバーしきれない観点とカスタム評価 # Ragasは、RAGシステムの基本的な性能を標準的に評価するための強力なツールですが、実際のビジネス応用では、以下のような制約があります。\nRagasの制約\n事前に定義された評価メトリクスに基づく標準的な評価 ドメイン固有の要件（業界特有のルール、コンプライアンス要件など）への対応が限定的 評価基準の詳細な調整が困難 ブランドトーンや表現スタイルなど、定性的な評価軸への対応 このような場合には、オリジナルの評価基準を作成することが有効です。\n例えば、Langfuseを活用することで、以下のようなカスタム評価が可能になります：\n金融分野での規制要件への適合性評価 医療分野での専門用語の正確性評価 LangfuseとRagasの使い分けや、カスタム評価基準の作成方法については、リンク先の記事 ： Langfuse で LLM 評価を効率化！活用方法徹底解説 で詳しく解説されていますので、ぜひご参照ください。\nまとめ：Ragasで課題を可視化し、Langfuseで改善を加速させる # 本稿では、Ragasの主要メトリクスが、RAGシステムの課題をいかに的確に浮かび上がらせるかを解説しました。これは、データドリブンなRAG改善における、欠かすことのできない第一歩です。\nしかし、スコアを算出するだけでは、改善活動は始まりません。そのスコアを「いつ、誰が、何を改善した結果なのか」という文脈と共に記録し、チームで共有して初めて、評価は次のアクションに繋がります。\nそのための最適な基盤が Langfuse です。\nLangfuseにRagasの評価スコアを記録することで、個々のリクエストのトレースデータと評価結果が完全に紐づきます。\n例えば、「Context Precisionが低い」という課題が見つかった場合は、Langfuse上でその原因となった検索コンテキストを即座に確認し、次の改善策を立てられます。また、改善後のスコアも再び記録し、A/B比較を実施できます。\nこのように、Ragasが「What（何を改善すべきか）」を教え、Langfuseが「How（どう改善サイクルを回すか）」を支えることができます。\n推測に頼る評価から脱却し、RagasとLangfuseによる継続的な改善サイクルを、ぜひ今日から始めてみてください。\n","date":"2025年10月2日","externalUrl":null,"permalink":"/posts/2025-10-02-%E3%82%82%E3%81%86rag%E8%A9%95%E4%BE%A1%E3%81%A7%E8%BF%B7%E3%82%8F%E3%81%AA%E3%81%84-ragas%E6%9C%80%E6%96%B0%E3%83%A1%E3%83%88%E3%83%AA%E3%82%AF%E3%82%B9%E8%A7%A3%E8%AA%AC%E3%81%A8%E5%AE%9F%E8%B7%B5%E7%9A%84%E6%94%B9%E5%96%84%E3%82%AC%E3%82%A4%E3%83%89/","section":"Posts","summary":"はじめに # RAG（Retrieval-Augmented Generation）は、外部知識を参照してLLMの回答精度を向上させる強力な技術です。\n","title":"もうRAG評価で迷わない！Ragas最新メトリクス解説と実践的改善ガイド","type":"posts"},{"content":" AIアプリのセキュリティ問題について # AIを利用したアプリケーションが急速に普及する一方で、悪意あるプロンプトでAIをハッキングしようとする動きも出てきています。\nこれは、従来のSQLインジェクションのような攻撃と同様、またはそれ以上に深刻なリスクをもたらします。AIアプリケーションのセキュリティ対策の重要性が高まっている今、どのような対策を取ることが出来るか検討していきます。\nアプリケーションのセキュリティ対策とModel Armorの選択 # しかし対策と言っても、ハックの手法は日々進化しており、自力で全てに対応するのは困難です。また、アプリケーション本来の開発に専念したい開発者にとって、セキュリティ対策に多くの時間を割くのは悩ましい課題です。そこで、専門的なサービスを活用するのが最も効率的な解決策と言えます。\n数あるサービスの中でも、Model Armorは現在、Google Cloudサービスとの統合をPre-GA（一般提供開始前） で提供しており、これから本格的な活用が期待されます。Google Cloud Nextでも言及されており、Google Cloudのサービスとシームレスに統合できる点が大きな魅力です。\nこうした背景から、今回はModel Armorを実際に試してみることにしました。\nModel Armorの役割と機能 # Model Armorは、AIアプリケーションと大規模言語モデル（LLM）の間に位置し、悪意ある入力をブロックするゲートウェイのような役割を果たします。自前で様々な対策を検討するのに比べ、簡単な設定を行うだけで、プロンプトインジェクションや機密情報の流出といったリスクへのセキュリティを強化できるのが大きな利点です。\n特に、Google Cloudの主要なAIプラットフォームであるVertex AIとの連携が非常に容易なため、既存のサービスに手軽にセキュリティを加えたい開発者にとって、Model Armorは強力な選択肢となるでしょう。\n実際の使用例：フロア設定とテンプレートの使い分け # Model Armorには、プロジェクト全体に適用するフロア設定と、より柔軟に制御できるテンプレートという2つの適用方法があります。それぞれの使い方と違いを、実際に試した例を交えて解説します。\nフロア設定を使ってみる # まずは、最も手軽なフロア設定から試してみました。これは、特定のプロジェクト全体にModel Armorのポリシーを適用する設定です。 以下の公式ドキュメントを参考に設定を進めました。\n参考：Model Armor のフロア設定を構成する フロア設定 # 今回は、以下の構成で実験してみます。\n利用したコード # 今回の検証用に作成した、モデルにリクエストを送信する際の簡易的なコードです。\nfrom google import genai from google.genai import types import os from dotenv import load_dotenv load_dotenv() project_id = os.getenv(\u0026#34;GOOGLE_CLOUD_PROJECT\u0026#34;) client = genai.Client( vertexai = True, project = project_id, location = \u0026#39;us-central1\u0026#39;, http_options = types.HttpOptions() ) response = client.models.generate_content( model=\u0026#34;gemini-2.5-flash-lite\u0026#34;, contents=\u0026#34;プロンプト\u0026#34; ) 結果 # アプリケーションのレスポンスに検出結果は含まれておらず、これまでの結果とあまり違いは見られませんでしたが、Cloud Loggingには検出結果が出力されていました。\n該当のログは以下のフィルターで簡単に絞り込めます。\njsonPayload.@type=\u0026#34;type.googleapis.com/google.cloud.modelarmor.logging.v1.SanitizeOperationLogEntry\u0026#34; 確認したところ、今回の実験に利用したプロンプトは、特に問題が無いものとして認識されているようですが、何かしら検査が行われたことはログとして出力されています。\n機密データの保護 - 検出タイプを「高度」に変更し、DLPで作成した検出・匿名化テンプレートを設定してみました。\n以下の通り、ログ上では機密情報が自動的に[PERSON_NAME]のように匿名化されることを確認しました。\n（[PERSON_NAME] を検出・匿名化対象としているため、氏名部分が置換されている）\nPIIが検出される状態において、Vertex AI の種類を「違反を検出し、ブロックする」に変更するとどうなるか確認してみます。\n$python test.py xxxxxxxxxxx/google/genai/_common.py:474: UserWarning: MODEL_ARMOR is not a valid BlockedReason warnings.warn(f\u0026#34;{value} is not a valid {cls.__name__}\u0026#34;) SDK（google-genai：v1.33.0）を使用した場合、UserWarning が発生しました。Pre-GAのため、SDKがまだ対応されていないものと思われます。\nレスポンスからブロックされた理由を確認します。\n# print(response.prompt_feedback.block_reason) BlockedReason.MODEL_ARMOR # print(response.prompt_feedback.block_reason_message) Blocked by Model Armor Floor Setting: The prompt violated SDP/PII filters. ログとしては、「検査のみ」の場合とほぼ同内容になっているようです。\nブロックする場合はエラーハンドリングが必要にはなりますが、検査のみであれば、アプリケーションのコードには一切の変更を行うことなく導入できる点は、既存のアプリケーションにModel Armorを導入する上で非常に大きなメリットだと感じました。\nModel Armorのテンプレートを使ってみる # 次に、よりきめ細かな制御が可能なテンプレートを試してみました。\nテンプレートは、特定のAIリクエストに対して個別のセキュリティポリシーを適用したい場合に便利です。\n実際に利用できる場面としては、プロジェクト内で複数のAIアプリケーションを運用している場合に、それぞれのユースケースに合わせて異なるセキュリティポリシーを適用したい場合に特に有効です。\nテンプレートの作成は Model Armor templates console を参考に実施しました。\nフロア設定と概ね同じ内容ですが、ブロックを行うか、検査のみとするかが選べない点がフロア設定との大きな差異です。\nなお、テンプレートはフロア設定よりも厳しい制限でなければ保存できません。\n既にフロア設定が作成されている場合、フロア設定より緩い制限で保存しようとするとエラーが発生しました。\nアプリケーションへの組み込み # API のリファレンスを見る限りでは、generate_content にModel Armor のテンプレートを利用する設定を追加するだけでよさそうです。しかし、現時点（2025/09）ではSDKに該当の設定を追加することが出来ませんでした。\nそのため、こちらは Python から curl でリクエストを飛ばす形にして検証しました。\nテンプレートの適用は、通常のVertex AIへのリクエストに model_armor_config という設定を追加するだけです。\nただし、テンプレートを利用するには、事前にVertex AIのサービスアカウントにModel Armor ユーザーのロールを追加しておく必要があります。\n結果 # レスポンスとして、以下が返ってきました。フロア設定とあまり違いは無さそうですが、検出結果は即座にブロックとして返されました。\n{ \u0026#34;promptFeedback\u0026#34;: { \u0026#34;blockReason\u0026#34;: \u0026#34;MODEL_ARMOR\u0026#34;, \u0026#34;blockReasonMessage\u0026#34;: \u0026#34;The prompt violated SDP/PII filters.\u0026#34; }, \u0026#34;usageMetadata\u0026#34;: { \u0026#34;trafficType\u0026#34;: \u0026#34;ON_DEMAND\u0026#34; } \u0026#34;modelVersion\u0026#34;: \u0026#34;gemini-2.5-flash-lite\u0026#34;, # 一部省略 } Cloud Loggingを確認してみます。\nログの内容としても、フロア設定の場合とあまり大きな差は無さそうです。\nフロア設定が優先されて、テンプレートが適応されていない可能性も考えましたが、 resource.labels.template_id に利用したテンプレートのID（model-armor-test-template）が出力されていたため、問題なく適応されていると判断しています。\n\u0026#34;resource\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;modelarmor.googleapis.com/SanitizeOperation\u0026#34;, \u0026#34;labels\u0026#34;: { \u0026#34;location\u0026#34;: \u0026#34;[location]\u0026#34;, \u0026#34;resource_container\u0026#34;: \u0026#34;projects/[project_number]\u0026#34;, \u0026#34;template_id\u0026#34;: \u0026#34;model-armor-test-template\u0026#34; } }, なお、フロア設定が適応されている場合は以下の出力でした。\n\u0026#34;resource\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;modelarmor.googleapis.com/SanitizeOperation\u0026#34;, \u0026#34;labels\u0026#34;: { \u0026#34;template_id\u0026#34;: \u0026#34;FLOOR_SETTING--[n]\u0026#34; \u0026#34;resource_container\u0026#34;: \u0026#34;projects/[project_number]\u0026#34;, \u0026#34;location\u0026#34;: \u0026#34;global\u0026#34; } } まとめと今後の展望 # Model Armorのフロア設定は、既に運用中のAIアプリケーションにセキュリティ対策を導入する際に非常に手軽で強力な手段です。しかし、Pre-GAということもあり、SDKの修正が追い付いていない等、公開するアプリケーションで採用するには悩ましい点も少なくありません。\n今後の正式リリースやアップデートで、より柔軟な設定が可能になることを期待し、引き続き動向を注視していきたいと思います。\n","date":"2025年9月12日","externalUrl":null,"permalink":"/posts/2025-09-12-%E5%AE%9F%E8%B7%B5-model-armor%E3%81%A7vertex-ai%E3%81%AEai%E3%82%BB%E3%82%AD%E3%83%A5%E3%83%AA%E3%83%86%E3%82%A3%E3%82%92%E5%AE%9F%E8%A3%85%E3%81%99%E3%82%8B/","section":"Posts","summary":"AIアプリのセキュリティ問題について # AIを利用したアプリケーションが急速に普及する一方で、悪意あるプロンプトでAIをハッキングしようとする動きも出てきています。\n","title":"【実践】Model ArmorでVertex AIのAIセキュリティを実装する","type":"posts"},{"content":"","date":"2025年9月12日","externalUrl":null,"permalink":"/tags/ai-security/","section":"タグ","summary":"","title":"AI Security","type":"tags"},{"content":"","date":"2025年9月12日","externalUrl":null,"permalink":"/tags/model-armor/","section":"タグ","summary":"","title":"Model Armor","type":"tags"},{"content":"","date":"2025年9月7日","externalUrl":null,"permalink":"/tags/strands-agent/","section":"タグ","summary":"","title":"Strands Agent","type":"tags"},{"content":"更新日：2025年9月16日\nはじめに # 本記事では、Strands Agents とADK の二つのフレームワークを使用したシンプルなエージェント (両方ともLangfuse のMCPサーバーを使う) が、Langfuseを使ってどのように可視化されるのかをクイックに紹介する記事です。最近のアップデートで標準でグラフも出たりして便利です。タイトルが長すぎて、単にリフレーズしただけの冒頭文になってしまいました。\n利用するMCPサーバー # 今回はLangfuse のドキュメントを参照してくれるMCPサーバーを利用します。\n開発の時にMCPクライアント (Cursor など) に入れておくと開発が捗りますが、本題ではないので今回はこれ自体の説明は割愛します。詳細はこちらのリンクをご覧ください。\nhttps://langfuse.com/docs/docs-mcp # Strands Agents のサンプルコード # こちらはターミナル上で実行することを想定しているものです。Strands Agents はこのようなコードを大変シンプルに短く書けるのが非常に魅力的だと思います。\nサンプルコード\n#!/usr/bin/env -S uv run --script # /// script # requires-python = \u0026#34;\u0026gt;=3.10\u0026#34; # dependencies = [ # \u0026#34;strands-agents[openai]\u0026#34;, # \u0026#34;mcp\u0026#34;, # \u0026#34;langfuse\u0026#34;, # ] # /// import os, sys from strands import Agent from strands.models.openai import OpenAIModel from mcp.client.streamable_http import streamablehttp_client from [strands.tools](http://strands.tools).mcp.mcp_client import MCPClient from langfuse import observe, get_client SYSTEM_PROMPT = ( \u0026#34;You are a helpful assistant for Langfuse developers.\\n\u0026#34; \u0026#34;- For Langfuse questions, proactively use MCP tools from the docs server: \u0026#34; \u0026#34;call `searchLangfuseDocs` first, then `getLangfuseDocsPage` for details.\\n\u0026#34; \u0026#34;- Prefer up-to-date docs. Keep answers concise; include code when useful.\u0026#34; ) # モデルとMCPはプロセスで1回だけ初期化 model = OpenAIModel( client_args={\u0026#34;api_key\u0026#34;: os.environ[\u0026#34;OPENAI_API_KEY\u0026#34;]}, model_id=os.environ.get(\u0026#34;OPENAI_MODEL\u0026#34;, \u0026#34;gpt-4o-mini\u0026#34;), params={\u0026#34;temperature\u0026#34;: 0}, ) mcp = MCPClient(lambda: streamablehttp_client(\u0026#34;[https://langfuse.com/api/mcp](https://langfuse.com/api/mcp)\u0026#34;)) @observe(name=\u0026#34;agent-run\u0026#34;, as_type=\u0026#34;generation\u0026#34;) def run_agent(prompt: str) -\u0026gt; str: with mcp: # MCPセッション（with内で有効） agent = Agent(model=model, tools=mcp.list_tools_sync(), system_prompt=SYSTEM_PROMPT) res = agent(prompt) return getattr(res, \u0026#34;message\u0026#34;, str(res)) def main(): prompt = \u0026#34; \u0026#34;.join(sys.argv[1:]) or input(\u0026#34;\u0026gt; \u0026#34;) out = run_agent(prompt) print(out) get_client().flush() if **name** == \u0026#34;__main__\u0026#34;: main() そして特に何もせずとも Langfuse にはこんな感じでTraceとして可視化されます。\nOutputはTrace の中に Content として入っています。\nStrands Agents の Trace いい感じでグラフも自動生成されます。処理の部分をクリックすると、その詳細が表示されます。例えば、 execute_tool searchLangfuseDocsというツール実行をクリックすると、実際にはユーザーの質問 (Datasetの作り方を知りたい) がどんな query になってて (\u0026ldquo;Langfuse dataset creation\u0026rdquo;) 飛んで、どんな情報が取れて \u0026hellip; みたいな内容を確認して、デバックすることができます。\nADK (Agent Deployment Kit) のサンプルコード # こちらも同様にCLIで動作して、同じ動作をします。こちらは OpenAI ではなくて、VertexAI上のGeminiを利用しています。OpenAI を使おうとすると LiteLLMが必要ですが、本題ではないのでおとなしくGeminiを使います。2.5-flashは速くて安くて便利です。\nサンプルコード\n#!/usr/bin/env -S uv run --script # /// script # requires-python = \u0026#34;\u0026gt;=3.10\u0026#34; # dependencies = [ # \u0026#34;google-adk\u0026#34;, # \u0026#34;google-genai\u0026#34;, # \u0026#34;langfuse\u0026gt;=3\u0026#34;, # \u0026#34;mcp\u0026#34;, # ] # /// import os, sys, asyncio from typing import Optional, List, Literal from datetime import timedelta from google.adk.agents import Agent from google.adk.runners import InMemoryRunner from google.genai import types from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client from langfuse import observe, get_client SYSTEM_PROMPT = ( \u0026#34;You are a helpful assistant for Langfuse developers.\\n\u0026#34; \u0026#34;- For Langfuse questions, proactively use MCP tools from the docs server: \u0026#34; \u0026#34;call `lf_docs(mode=\\\u0026#34;search\\\u0026#34;, query=...)` first, then \u0026#34; \u0026#34;`lf_docs(mode=\\\u0026#34;get\\\u0026#34;, pathOrUrl=...)` for details.\\n\u0026#34; \u0026#34;- Prefer up-to-date docs. Keep answers concise; include code when useful.\u0026#34; ) MCP_URL = (os.getenv(\u0026#34;LF_MCP_URL\u0026#34;) or \u0026#34;[https://langfuse.com/api/mcp\u0026#34;).strip()](https://langfuse.com/api/mcp) async def *mcp*call(tool_name: str, **arguments) -\u0026gt; str: for k, v in list(arguments.items()): if isinstance(v, str): arguments[k] = v.strip().replace(\u0026#34;\\r\u0026#34;, \u0026#34;\u0026#34;) async with streamablehttp_client(url=MCP_URL) as (r, w, _): async with ClientSession(r, w) as session: await session.initialize() resp = await [session.call](http://session.call)_tool( tool_name, arguments=arguments, read_timeout_seconds=timedelta(seconds=60), ) texts: List[str] = [] for c in getattr(resp, \u0026#34;content\u0026#34;, []) or []: texts.append(getattr(c, \u0026#34;text\u0026#34;, \u0026#34;\u0026#34;) or \u0026#34;\u0026#34;) return \u0026#34;\\n\u0026#34;.join(t for t in texts if t).strip() async def lf_docs( mode: Literal[\u0026#34;search\u0026#34;, \u0026#34;get\u0026#34;], query: Optional[str] = None, pathOrUrl: Optional[str] = None, ) -\u0026gt; str: if mode == \u0026#34;search\u0026#34;: if not query: return \u0026#34;Error: \u0026#39;query\u0026#39; is required for mode=\u0026#39;search\u0026#39;.\u0026#34; return await *mcp*call(\u0026#34;searchLangfuseDocs\u0026#34;, query=query) # mode == \u0026#34;get\u0026#34; if not pathOrUrl: return \u0026#34;Error: \u0026#39;pathOrUrl\u0026#39; is required for mode=\u0026#39;get\u0026#39;.\u0026#34; return await *mcp*call(\u0026#34;getLangfuseDocsPage\u0026#34;, pathOrUrl=pathOrUrl) ROOT = Agent( name=\u0026#34;adk_mcp_cli_vertex\u0026#34;, model=os.getenv(\u0026#34;ADK_MODEL\u0026#34;, \u0026#34;gemini-2.5-flash\u0026#34;), description=\u0026#34;CLI agent using Langfuse Docs MCP tools (Vertex AI Gemini 2.5 Flash)\u0026#34;, instruction=SYSTEM_PROMPT, tools=[lf_docs], ) @observe(name=\u0026#34;adk-cli-turn\u0026#34;, as_type=\u0026#34;generation\u0026#34;) async def run_once(prompt: str) -\u0026gt; str: runner = InMemoryRunner(agent=ROOT, app_name=ROOT.name) session = await runner.session_service.create_session(app_name=ROOT.name, user_id=\u0026#34;cli-user\u0026#34;) user_msg = types.Content(role=\u0026#34;user\u0026#34;, parts=[types.Part(text=prompt)]) final = \u0026#34;(no answer)\u0026#34; async for event in [runner.run](http://runner.run)_async(user_id=\u0026#34;cli-user\u0026#34;, session_id=session.id, new_message=user_msg): if hasattr(event, \u0026#34;is_final_response\u0026#34;) and [event.is](http://event.is)_final_response(): if event.content and [event.content.parts](http://event.content.parts): final = [event.content.parts](http://event.content.parts)[0].text or final return final async def amain(): prompt = \u0026#34; \u0026#34;.join(sys.argv[1:]) or input(\u0026#34;\u0026gt; \u0026#34;) out = await run_once(prompt) print(out) get_client().flush() if **name** == \u0026#34;__main__\u0026#34;: [asyncio.run](http://asyncio.run)(amain()) こちらがTraceです。こちらも構造化されて、グラフ図も同様に出てきます。\nTraceの中のデータの入り方などは異なりますが、基本的には同じです。\nADK での Trace 同様に図も出してくれてわかりやすい まとめ # 今回のサンプルコードは @observe を利用して、Traceを作っていますがSpan の構成やUser, Session, 命名など細かく設定をすることも可能です。ぜひ色々と試して見てください（MCP サーバーの活用も便利だと思います！)。\n","date":"2025年9月7日","externalUrl":null,"permalink":"/posts/2025-09-07-strands-agents-%E3%81%A8-adk-%E3%81%A7%E3%83%AA%E3%83%A2%E3%83%BC%E3%83%88mcp%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%9Fagent%E3%82%92%E4%BD%9C%E3%82%8A-%E3%81%9D%E3%81%AE%E5%87%A6%E7%90%86%E3%82%92langfuse%E3%81%A7%E5%8F%AF%E8%A6%96%E5%8C%96%E3%81%99%E3%82%8B/","section":"Posts","summary":"更新日：2025年9月16日\nはじめに # 本記事では、Strands Agents とADK の二つのフレームワークを使用したシンプルなエージェント (両方ともLangfuse のMCPサーバーを使う) が、Langfuseを使ってどのように可視化されるのかをクイックに紹介する記事です。最近のアップデートで標準でグラフも出たりして便利です。タイトルが長すぎて、単にリフレーズしただけの冒頭文になってしまいました。\n","title":"Strands Agents と ADK でリモートMCPサーバーを使ったAgentを作り、その処理をLangfuseで可視化する","type":"posts"},{"content":"","date":"2025年9月3日","externalUrl":null,"permalink":"/tags/litellm/","section":"タグ","summary":"","title":"LiteLLM","type":"tags"},{"content":"","date":"2025年9月3日","externalUrl":null,"permalink":"/tags/terraform/","section":"タグ","summary":"","title":"Terraform","type":"tags"},{"content":" はじめに # 近年、LLMアプリケーションの開発において、複数のLLMプロバイダーを使い分ける必要性が高まっています。 OpenAIのGPT、AnthropicのClaude、GoogleのGeminiなど、それぞれ異なる特徴を持っており、それぞれの特徴に合わせてLLMを使い分けるケースも多くあります。 また、コスト最適化の観点から、簡単なタスクには安価なモデルを、複雑なタスクには高性能なモデルを使い分けることも重要です。\nしかし、従来のアプローチでは各LLMプロバイダーごとに異なるAPI仕様に対応する必要があり、APIキーの分散管理、使用量・コストの把握困難、アプリケーション側での複雑な実装といった課題がありました。\nLiteLLM Proxyは、複数のLLMプロバイダー利用に伴うAPI仕様の違いや管理の煩雑さといった課題を解決するLLM Gatewayソリューションです。\n統一されたAPIインターフェースを提供することで、開発者は一つのエンドポイントからOpenAIやGoogle Geminiなど様々なLLMプロバイダーへシームレスにアクセスできます。\nまた、自動的な負荷分散やフェイルオーバー機能により高い可用性を確保し、詳細な使用量・コストの監視機能によって効率的なリソース管理も実現します。\nさらに、LiteLLM ProxyはLangfuseとの連携によるトレース送信機能も標準で備えています。\n簡単な設定を行うだけで、各LLMリクエストのトレースログを自動的にLangfuseへ送信し、可観測性や運用分析を強化できます。\nLiteLLM Proxyの主要な特徴 # # LiteLLM Proxyは、複数のLLMプロバイダーを統一的に管理できるプロキシサーバーです。主な特徴は以下の通りです：\n複数プロバイダー統合: OpenAI、Anthropic、Google、Azure OpenAIなど、様々なLLMプロバイダーを統一APIで利用可能 コスト・使用量管理: リクエスト数、トークン使用量、コストの追跡と制限設定 APIキー管理: 複数のAPIキーを一元管理し、セキュアに配布 負荷分散・フェイルオーバー: 複数のエンドポイント間でのリクエスト分散と自動フェイルオーバー ログ・監視: 詳細なリクエストログとメトリクスの収集 Langufseへのログ送信も可能 本記事の全体像 # 本記事では以下について解説します。\nTerraformを使用して、Google Cloud上にLiteLLM Proxyを構築する手順を解説する LiteLLM Proxyを使用して複数のLLMプロバイダーへのアクセスを試す LiteLLM ProxyのWeb UIでの管理画面を確認する LiteLLM Proxyを利用した LLMプロバイダーアクセスのTrace情報がLangfuseへ送信されていることの確認する 構築する全体構成 # 以下の構成図に示すとおり、Google Cloud上にLiteLLM Proxy環境を構築します。\n警告 本構成は必要最小限の構成になっております。セキュリティ強度を高める場合はCloud Runサービスの前段に外部アプリケーションロードバランサーを構成し、IAPやCloud Armorを利用ください。\nLiteLLM Proxy構成図 この構成では以下の要素を組み合わせています：\nLiteLLM Proxy Cloud Run上でコンテナとして稼働する中核コンポーネント Secret Manager: APIキーの安全な管理 Cloud SQL: プロキシの設定データとメタデータの保存 ※以下のコンポーネントの構築・準備手順はこの記事では割愛します。\nLangfuse : LLMリクエストの可観測性とパフォーマンス監視 複数LLMプロバイダー: OpenAI、Googleへの統一アクセス 次のセクションから、実際の構築手順を解説していきます。\n※ 本記事ではTerraformのインストールやGoogle Cloud プロジェクトの作成等は割愛いたします。\nTerraformによるインフラ構築 # TerraformによりGoogle Cloud上にLiteLLM Proxy環境を構築します。\nTerraformファイル構成 # LiteLLM Proxy環境の構築に必要な主要ファイルを以下のように構成します\n. ├── main.tf # メインの設定ファイル（プロバイダー設定、リソース定義） ├── variables.tf # 変数定義（カスタマイズ可能な値） ├── outputs.tf # 出力値（デプロイ後に必要な情報） └── terraform.tfvars # 変数の実際の値（プロジェクト名や環境変数値を設定） 下記のとおり、main.tf,variables.tf,outputs.tf,terraform.tfvars ファイルを作成します。\nメインリソース（main.tf）\n# Googleプロバイダー、プロジェクト設定 terraform { required_version = \u0026#34;\u0026gt;= 0.14\u0026#34; required_providers { google = { source = \u0026#34;hashicorp/google\u0026#34; version = \u0026#34;~\u0026gt; 6.0\u0026#34; } random = { source = \u0026#34;hashicorp/random\u0026#34; version = \u0026#34;~\u0026gt; 3.1\u0026#34; } } } provider \u0026#34;google\u0026#34; { project = var.project_id region = var.region zone = var.zone } # 変数とランダム値生成（セキュリティ関連） # Random password for database resource \u0026#34;random_password\u0026#34; \u0026#34;db_password\u0026#34; { length = 16 special = false upper = true lower = true numeric = true } # Random master key for LiteLLM resource \u0026#34;random_password\u0026#34; \u0026#34;master_key\u0026#34; { length = 32 special = true upper = true lower = true numeric = true } # Random password for LiteLLM UI resource \u0026#34;random_password\u0026#34; \u0026#34;ui_password\u0026#34; { length = 16 special = true upper = true lower = true numeric = true } # Random salt key for LiteLLM resource \u0026#34;random_string\u0026#34; \u0026#34;salt_key\u0026#34; { length = 16 special = false upper = true lower = true numeric = true } # Random prefix for database name resource \u0026#34;random_id\u0026#34; \u0026#34;db_prefix\u0026#34; { byte_length = 4 } # Local values - 条件分岐により変数またはランダム値を使用 locals { db_password = var.db_password != null ? var.db_password : random_password.db_password.result master_key = var.master_key != null ? var.master_key : random_password.master_key.result ui_password = var.ui_password != null ? var.ui_password : random_password.ui_password.result salt_key = var.salt_key != null ? var.salt_key : random_string.salt_key.result vertex_project_id = var.vertex_project_id != null ? var.vertex_project_id : var.project_id } # Google Project Servicesの有効化 resource \u0026#34;google_project_service\u0026#34; \u0026#34;required_apis\u0026#34; { for_each = toset([ \u0026#34;compute.googleapis.com\u0026#34;, \u0026#34;run.googleapis.com\u0026#34;, \u0026#34;sqladmin.googleapis.com\u0026#34;, \u0026#34;secretmanager.googleapis.com\u0026#34;, \u0026#34;vpcaccess.googleapis.com\u0026#34;, \u0026#34;aiplatform.googleapis.com\u0026#34;, ]) project = var.project_id service = each.value disable_dependent_services = false disable_on_destroy = false } # ネットワーク（VPC/サブネット/プライベート接続） resource \u0026#34;google_compute_network\u0026#34; \u0026#34;litellm_vpc\u0026#34; { name = \u0026#34;litellm-vpc\u0026#34; auto_create_subnetworks = false } resource \u0026#34;google_compute_subnetwork\u0026#34; \u0026#34;cloudrun_egress\u0026#34; { name = \u0026#34;cloudrun-egress\u0026#34; ip_cidr_range = \u0026#34;10.0.2.0/24\u0026#34; network = google_compute_network.litellm_vpc.id region = var.region private_ip_google_access = true } resource \u0026#34;google_compute_global_address\u0026#34; \u0026#34;psa_subnet\u0026#34; { name = \u0026#34;psa-subnet\u0026#34; purpose = \u0026#34;VPC_PEERING\u0026#34; address_type = \u0026#34;INTERNAL\u0026#34; prefix_length = 16 network = google_compute_network.litellm_vpc.id } resource \u0026#34;google_service_networking_connection\u0026#34; \u0026#34;private_service_connection\u0026#34; { network = google_compute_network.litellm_vpc.id service = \u0026#34;servicenetworking.googleapis.com\u0026#34; reserved_peering_ranges = [google_compute_global_address.psa_subnet.name] } # Cloud SQL（PostgreSQLデータベース） resource \u0026#34;google_sql_database_instance\u0026#34; \u0026#34;litellm_db_01\u0026#34; { database_version = \u0026#34;POSTGRES_16\u0026#34; name = \u0026#34;litellm-db-${random_id.db_prefix.hex}-1\u0026#34; region = var.region project = var.project_id settings { activation_policy = \u0026#34;ALWAYS\u0026#34; availability_type = \u0026#34;ZONAL\u0026#34; disk_autoresize = false disk_size = 10 disk_type = \u0026#34;PD_SSD\u0026#34; edition = \u0026#34;ENTERPRISE\u0026#34; pricing_plan = \u0026#34;PER_USE\u0026#34; tier = \u0026#34;db-f1-micro\u0026#34; ip_configuration { ipv4_enabled = false private_network = google_compute_network.litellm_vpc.id enable_private_path_for_google_cloud_services = true } location_preference { zone = var.zone } } deletion_protection = false depends_on = [google_service_networking_connection.private_service_connection] } resource \u0026#34;google_sql_database\u0026#34; \u0026#34;litellm_db\u0026#34; { name = \u0026#34;litellm_db\u0026#34; instance = google_sql_database_instance.litellm_db_01.name project = var.project_id } resource \u0026#34;google_sql_user\u0026#34; \u0026#34;litellm_user\u0026#34; { name = \u0026#34;litellm_user\u0026#34; instance = google_sql_database_instance.litellm_db_01.name password = local.db_password project = var.project_id } # Secret Manager（機密情報管理） resource \u0026#34;google_secret_manager_secret\u0026#34; \u0026#34;litellm_master_key\u0026#34; { secret_id = \u0026#34;litellm-master-key\u0026#34; project = var.project_id replication { auto {} } } resource \u0026#34;google_secret_manager_secret_version\u0026#34; \u0026#34;litellm_master_key\u0026#34; { secret = google_secret_manager_secret.litellm_master_key.id secret_data = local.master_key } resource \u0026#34;google_secret_manager_secret\u0026#34; \u0026#34;litellm_database_url\u0026#34; { secret_id = \u0026#34;litellm-database-url\u0026#34; project = var.project_id replication { auto {} } } resource \u0026#34;google_secret_manager_secret_version\u0026#34; \u0026#34;litellm_database_url\u0026#34; { secret = google_secret_manager_secret.litellm_database_url.id secret_data = \u0026#34;postgresql://${google_sql_user.litellm_user.name}:${local.db_password}@${google_sql_database_instance.litellm_db_01.private_ip_address}:5432/${google_sql_database.litellm_db.name}\u0026#34; } resource \u0026#34;google_secret_manager_secret\u0026#34; \u0026#34;litellm_ui_username\u0026#34; { secret_id = \u0026#34;litellm-ui-username\u0026#34; project = var.project_id replication { auto {} } } resource \u0026#34;google_secret_manager_secret_version\u0026#34; \u0026#34;litellm_ui_username\u0026#34; { secret = google_secret_manager_secret.litellm_ui_username.id secret_data = var.ui_username } resource \u0026#34;google_secret_manager_secret\u0026#34; \u0026#34;litellm_ui_password\u0026#34; { secret_id = \u0026#34;litellm-ui-password\u0026#34; project = var.project_id replication { auto {} } } resource \u0026#34;google_secret_manager_secret_version\u0026#34; \u0026#34;litellm_ui_password\u0026#34; { secret = google_secret_manager_secret.litellm_ui_password.id secret_data = local.ui_password } resource \u0026#34;google_secret_manager_secret\u0026#34; \u0026#34;litellm_salt_key\u0026#34; { secret_id = \u0026#34;litellm-salt-key\u0026#34; project = var.project_id replication { auto {} } } resource \u0026#34;google_secret_manager_secret_version\u0026#34; \u0026#34;litellm_salt_key\u0026#34; { secret = google_secret_manager_secret.litellm_salt_key.id secret_data = local.salt_key } resource \u0026#34;google_secret_manager_secret\u0026#34; \u0026#34;litellm_proxy_admin_id\u0026#34; { secret_id = \u0026#34;litellm-proxy-admin-id\u0026#34; project = var.project_id replication { auto {} } } resource \u0026#34;google_secret_manager_secret_version\u0026#34; \u0026#34;litellm_proxy_admin_id\u0026#34; { secret = google_secret_manager_secret.litellm_proxy_admin_id.id secret_data = var.proxy_admin_id } resource \u0026#34;google_secret_manager_secret\u0026#34; \u0026#34;langfuse_public_key\u0026#34; { secret_id = \u0026#34;langfuse-public-key\u0026#34; project = var.project_id replication { auto {} } } resource \u0026#34;google_secret_manager_secret_version\u0026#34; \u0026#34;langfuse_public_key\u0026#34; { secret = google_secret_manager_secret.langfuse_public_key.id secret_data = var.langfuse_public_key } resource \u0026#34;google_secret_manager_secret\u0026#34; \u0026#34;langfuse_secret_key\u0026#34; { secret_id = \u0026#34;langfuse-secret-key\u0026#34; project = var.project_id replication { auto {} } } resource \u0026#34;google_secret_manager_secret_version\u0026#34; \u0026#34;langfuse_secret_key\u0026#34; { secret = google_secret_manager_secret.langfuse_secret_key.id secret_data = var.langfuse_secret_key } resource \u0026#34;google_secret_manager_secret\u0026#34; \u0026#34;litellm_config_yaml\u0026#34; { secret_id = \u0026#34;litellm-config-yaml\u0026#34; project = var.project_id replication { auto {} } } resource \u0026#34;google_secret_manager_secret_version\u0026#34; \u0026#34;litellm_config_yaml\u0026#34; { secret = google_secret_manager_secret.litellm_config_yaml.id secret_data = var.litellm_config_yaml } # OpenAI API Key Secret resource \u0026#34;google_secret_manager_secret\u0026#34; \u0026#34;openai_api_key\u0026#34; { secret_id = \u0026#34;openai-api-key\u0026#34; project = var.project_id replication { auto {} } } resource \u0026#34;google_secret_manager_secret_version\u0026#34; \u0026#34;openai_api_key\u0026#34; { secret = google_secret_manager_secret.openai_api_key.id secret_data = var.openai_api_key } # Cloud Run Service resource \u0026#34;google_cloud_run_v2_service\u0026#34; \u0026#34;litellm_proxy\u0026#34; { name = \u0026#34;litellm-proxy\u0026#34; location = var.region project = var.project_id ingress = \u0026#34;INGRESS_TRAFFIC_ALL\u0026#34; invoker_iam_disabled = true template { execution_environment = \u0026#34;EXECUTION_ENVIRONMENT_GEN2\u0026#34; dynamic \u0026#34;volumes\u0026#34; { for_each = var.litellm_config_yaml != null ? [1] : [] content { name = \u0026#34;litellm-config\u0026#34; secret { secret = google_secret_manager_secret.litellm_config_yaml.secret_id items { path = \u0026#34;litellm_config.yaml\u0026#34; version = \u0026#34;latest\u0026#34; } } } } containers { # LiteLLM Proxy image from Docker Hub image = \u0026#34;docker.io/litellm/litellm:${var.litellm_version}\u0026#34; name = \u0026#34;litellm-proxy\u0026#34; args = var.litellm_config_yaml != null ? [ \u0026#34;--config\u0026#34;, \u0026#34;/etc/litellm/litellm_config.yaml\u0026#34; ] : [] ports { container_port = 4000 name = \u0026#34;http1\u0026#34; } # Environment variables env { name = \u0026#34;LITELLM_LOG_LEVEL\u0026#34; value = \u0026#34;INFO\u0026#34; } env { name = \u0026#34;STORE_MODEL_IN_DB\u0026#34; value = \u0026#34;True\u0026#34; } env { name = \u0026#34;LANGFUSE_PUBLIC_KEY\u0026#34; value_source { secret_key_ref { secret = google_secret_manager_secret.langfuse_public_key.secret_id version = \u0026#34;latest\u0026#34; } } } env { name = \u0026#34;LANGFUSE_SECRET_KEY\u0026#34; value_source { secret_key_ref { secret = google_secret_manager_secret.langfuse_secret_key.secret_id version = \u0026#34;latest\u0026#34; } } } env { name = \u0026#34;LANGFUSE_HOST\u0026#34; value = var.langfuse_host } # Secret Manager references env { name = \u0026#34;DATABASE_URL\u0026#34; value_source { secret_key_ref { secret = google_secret_manager_secret.litellm_database_url.secret_id version = \u0026#34;latest\u0026#34; } } } env { name = \u0026#34;LITELLM_MASTER_KEY\u0026#34; value_source { secret_key_ref { secret = google_secret_manager_secret.litellm_master_key.secret_id version = \u0026#34;latest\u0026#34; } } } env { name = \u0026#34;UI_PASSWORD\u0026#34; value_source { secret_key_ref { secret = google_secret_manager_secret.litellm_ui_password.secret_id version = \u0026#34;latest\u0026#34; } } } env { name = \u0026#34;UI_USERNAME\u0026#34; value_source { secret_key_ref { secret = google_secret_manager_secret.litellm_ui_username.secret_id version = \u0026#34;latest\u0026#34; } } } env { name = \u0026#34;LITELLM_SALT_KEY\u0026#34; value_source { secret_key_ref { secret = google_secret_manager_secret.litellm_salt_key.secret_id version = \u0026#34;latest\u0026#34; } } } env { name = \u0026#34;PROXY_ADMIN_ID\u0026#34; value_source { secret_key_ref { secret = google_secret_manager_secret.litellm_proxy_admin_id.secret_id version = \u0026#34;latest\u0026#34; } } } # OpenAI API Key (conditional) dynamic \u0026#34;env\u0026#34; { for_each = var.openai_api_key != null ? [1] : [] content { name = \u0026#34;OPENAI_API_KEY\u0026#34; value_source { secret_key_ref { secret = google_secret_manager_secret.openai_api_key.secret_id version = \u0026#34;latest\u0026#34; } } } } # Resource limits resources { limits = { cpu = \u0026#34;1000m\u0026#34; memory = \u0026#34;1Gi\u0026#34; } startup_cpu_boost = true cpu_idle = true } dynamic \u0026#34;volume_mounts\u0026#34; { for_each = var.litellm_config_yaml != null ? [1] : [] content { name = \u0026#34;litellm-config\u0026#34; mount_path = \u0026#34;/etc/litellm\u0026#34; } } # Health check startup_probe { failure_threshold = 6 http_get { path = \u0026#34;/\u0026#34; port = 4000 } initial_delay_seconds = 60 period_seconds = 10 timeout_seconds = 10 } } # Scaling configuration scaling { max_instance_count = 1 min_instance_count = 0 } max_instance_request_concurrency = 80 service_account = google_service_account.lite_llm_proxy.email timeout = \u0026#34;300s\u0026#34; vpc_access { network_interfaces { network = google_compute_network.litellm_vpc.id subnetwork = google_compute_subnetwork.cloudrun_egress.id } egress = \u0026#34;PRIVATE_RANGES_ONLY\u0026#34; } } traffic { percent = 100 type = \u0026#34;TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST\u0026#34; } deletion_protection = false depends_on = [ google_project_service.required_apis, google_sql_user.litellm_user, google_service_account.lite_llm_proxy, google_project_iam_member.litellm_proxy_cloudsql_client, google_project_iam_member.litellm_proxy_secret_accessor, ] } # IAM for Cloud Run service # Cloud Run IAM for load balancer access resource \u0026#34;google_cloud_run_service_iam_member\u0026#34; \u0026#34;litellm_proxy_public\u0026#34; { service = google_cloud_run_v2_service.litellm_proxy.name location = google_cloud_run_v2_service.litellm_proxy.location project = google_cloud_run_v2_service.litellm_proxy.project role = \u0026#34;roles/run.invoker\u0026#34; member = \u0026#34;allUsers\u0026#34; } # Vertex AI permission for Cloud Run service account resource \u0026#34;google_project_iam_member\u0026#34; \u0026#34;litellm_proxy_vertex_user\u0026#34; { project = local.vertex_project_id role = \u0026#34;roles/aiplatform.user\u0026#34; member = \u0026#34;serviceAccount:${google_service_account.lite_llm_proxy.email}\u0026#34; } resource \u0026#34;google_service_account\u0026#34; \u0026#34;lite_llm_proxy\u0026#34; { account_id = \u0026#34;lite-llm-proxy\u0026#34; description = \u0026#34;LiteLLM Proxy Service Account\u0026#34; display_name = \u0026#34;LiteLLM Proxy\u0026#34; project = var.project_id } # IAM binding for service account resource \u0026#34;google_project_iam_member\u0026#34; \u0026#34;litellm_proxy_cloudsql_client\u0026#34; { project = var.project_id role = \u0026#34;roles/cloudsql.client\u0026#34; member = \u0026#34;serviceAccount:${google_service_account.lite_llm_proxy.email}\u0026#34; } resource \u0026#34;google_project_iam_member\u0026#34; \u0026#34;litellm_proxy_secret_accessor\u0026#34; { project = var.project_id role = \u0026#34;roles/secretmanager.secretAccessor\u0026#34; member = \u0026#34;serviceAccount:${google_service_account.lite_llm_proxy.email}\u0026#34; } # 変数定義（variables.tf）\nvariable \u0026#34;project_id\u0026#34; { description = \u0026#34;GCP Project ID\u0026#34; type = string } variable \u0026#34;region\u0026#34; { description = \u0026#34;GCP Region\u0026#34; type = string default = \u0026#34;asia-northeast1\u0026#34; } variable \u0026#34;zone\u0026#34; { description = \u0026#34;GCP Zone\u0026#34; type = string default = \u0026#34;asia-northeast1-a\u0026#34; } variable \u0026#34;db_password\u0026#34; { description = \u0026#34;Database password for LiteLLM user (optional)\u0026#34; type = string default = null sensitive = true } variable \u0026#34;master_key\u0026#34; { description = \u0026#34;LiteLLM master key (optional)\u0026#34; type = string default = null sensitive = true } variable \u0026#34;ui_username\u0026#34; { description = \u0026#34;UI username for LiteLLM admin\u0026#34; type = string default = \u0026#34;admin\u0026#34; } variable \u0026#34;ui_password\u0026#34; { description = \u0026#34;UI password for LiteLLM admin (optional)\u0026#34; type = string default = null sensitive = true } variable \u0026#34;salt_key\u0026#34; { description = \u0026#34;Salt key for LiteLLM (optional)\u0026#34; type = string default = null sensitive = true } variable \u0026#34;proxy_admin_id\u0026#34; { description = \u0026#34;Proxy admin ID\u0026#34; type = string default = \u0026#34;admin\u0026#34; } variable \u0026#34;langfuse_public_key\u0026#34; { description = \u0026#34;Langfuse public key\u0026#34; type = string default = null sensitive = true } variable \u0026#34;langfuse_secret_key\u0026#34; { description = \u0026#34;Langfuse secret key\u0026#34; type = string default = null sensitive = true } variable \u0026#34;langfuse_host\u0026#34; { description = \u0026#34;Langfuse host URL\u0026#34; type = string default = null } variable \u0026#34;vertex_project_id\u0026#34; { description = \u0026#34;Project ID where Vertex AI is used\u0026#34; type = string default = null } variable \u0026#34;litellm_config_yaml\u0026#34; { description = \u0026#34;Contents of litellm_config.yaml (optional)\u0026#34; type = string default = null sensitive = true } variable \u0026#34;litellm_version\u0026#34; { description = \u0026#34;LiteLLM container image version\u0026#34; type = string default = \u0026#34;v1.75.8-stable\u0026#34; } variable \u0026#34;openai_api_key\u0026#34; { description = \u0026#34;OpenAI API key (optional)\u0026#34; type = string default = null sensitive = true } 出力値定義（outputs.tf）\noutput \u0026#34;cloud_run_url\u0026#34; { description = \u0026#34;Cloud Run service URL\u0026#34; value = google_cloud_run_v2_service.litellm_proxy.uri } output \u0026#34;database_private_ip\u0026#34; { description = \u0026#34;Cloud SQL instance private IP\u0026#34; value = google_sql_database_instance.litellm_db_01.private_ip_address } output \u0026#34;database_connection_name\u0026#34; { description = \u0026#34;Cloud SQL instance connection name\u0026#34; value = google_sql_database_instance.litellm_db_01.connection_name } output \u0026#34;database_name\u0026#34; { description = \u0026#34;LiteLLM database name\u0026#34; value = google_sql_database.litellm_db.name } output \u0026#34;service_account_email\u0026#34; { description = \u0026#34;LiteLLM Proxy service account email\u0026#34; value = google_service_account.lite_llm_proxy.email } 変数入力ファイル（terraform.tfvars）\n# Google CloudのプロジェクトID、リージョン、ゾーンを入力してください project_id = \u0026#34;your-project-id\u0026#34; region = \u0026#34;asia-northeast1\u0026#34; zone = \u0026#34;asia-northeast1-a\u0026#34; # Langfuse settings（LLM可観測性プラットフォーム） # Langfuse のパブリックキーとシークレットキー、ホストURLを入力してください langfuse_public_key = \u0026#34;pk-lf-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\u0026#34; langfuse_secret_key = \u0026#34;sk-lf-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\u0026#34; langfuse_host = \u0026#34;https://your-langfuse-instance.com\u0026#34; # LiteLLM Container version litellm_version = \u0026#34;v1.75.8-stable\u0026#34; # OpenAI API Key # OpenAI を利用する場合はOpenAI API Keyを設定してください。 openai_api_key = \u0026#34;sk-proj-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u0026#34; # LiteLLM設定ファイル # LiteLLM設定は litellm_config.yaml に設定します。以下を参考に設定してください。 # Vertex AIでGeminiを利用する場合はproject_id,locationの設定、OpenAIを利用する場合はOpenAI API Keyの設定が必要な点を注意ください。 # サンプル設定を以下に示します。 litellm_config_yaml = \u0026lt;\u0026lt;EOT model_list: # Vertex AI Geminiモデル # Vertex AI を利用する場合はVertex AI Project ID,Locationを設定してください。 - model_name: \u0026#34;gemini-pro\u0026#34; litellm_params: model: \u0026#34;vertex_ai/gemini-1.5-pro-002\u0026#34; vertex_project: \u0026#34;your-project-id\u0026#34; vertex_location: \u0026#34;asia-northeast1\u0026#34; - model_name: \u0026#34;gemini-flash\u0026#34; litellm_params: model: \u0026#34;vertex_ai/gemini-1.5-flash-002\u0026#34; vertex_project: \u0026#34;your-project-id\u0026#34; vertex_location: \u0026#34;asia-northeast1\u0026#34; # OpenAIモデル（API Key設定時のみ） # OpenAI を利用する場合はOpenAI API Keyを設定してください。 - model_name: \u0026#34;gpt-4.1\u0026#34; litellm_params: model: \u0026#34;openai/gpt-4.1\u0026#34; api_key: os.environ/OPENAI_API_KEY - model_name: \u0026#34;gpt-4.1-mini\u0026#34; litellm_params: model: \u0026#34;openai/gpt-4.1-mini\u0026#34; api_key: os.environ/OPENAI_API_KEY litellm_settings: success_callback: [\u0026#34;langfuse\u0026#34;] failure_callback: [\u0026#34;langfuse\u0026#34;] EOT # Terraformの実行手順 # 下記の手順に従って、Terraformを実行LiteLLM Proxy環境を構築します。\n1. Terraform 初期化 # # 作業ディレクトリに移動 cd /path/to/your/terraform # Terraform初期化 terraform init 2. 実行計画の確認 # # 実行計画を確認 terraform plan 3. リソースの作成 # # リソース作成実行 terraform apply # 確認プロンプトで \u0026#34;yes\u0026#34; を入力 4. デプロイの確認 # # 出力値の確認 terraform output 次のセクションでは、デプロイされたLiteLLM ProxyのLLM Gateway機能を実際に試行してみます。\nLLM Gateway機能の試行 # LiteLLM Proxyがデプロイされたら、実際にLLM Gateway機能をテストしてみましょう。\n基本的な動作確認 # 1. ヘルスチェック # # Cloud RunのURL（outputsのcloud_run_urlで確認） CUSTOM_DOMAIN=$(terraform output -raw cloud_run_url) # ヘルスチェック curl -sS -X GET \u0026#34;${CUSTOM_DOMAIN}/\u0026#34; -o /dev/null -w \u0026#34;%{http_code}\\n\u0026#34; # 200 が返ってくれば正常 2. 利用可能なモデル一覧の取得 # # マスターキーを取得（実際の運用では環境変数に設定） MASTER_KEY=$(gcloud secrets versions access latest --secret=\u0026#34;litellm-master-key\u0026#34;) # モデル一覧を取得（ALB経由） curl -X GET \u0026#34;${CUSTOM_DOMAIN}/v1/models\u0026#34; \\ -H \u0026#34;Authorization: Bearer ${MASTER_KEY}\u0026#34; \\ -H \u0026#34;Content-Type: application/json\u0026#34; # モデル一覧が返ってくれば正常 OpenAI互換APIでのテスト # LiteLLM ProxyはOpenAI APIと互換性があるため、OpenAIクライアントと同様に使用できます。\n1. Chat Completions API（GPT-4.1-mini） # # GPT-4.1-miniを使用したチャット curl -X POST \u0026#34;${CUSTOM_DOMAIN}/v1/chat/completions\u0026#34; \\ -H \u0026#34;Authorization: Bearer ${MASTER_KEY}\u0026#34; \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{ \u0026#34;model\u0026#34;: \u0026#34;gpt-4.1-mini\u0026#34;, \u0026#34;messages\u0026#34;: [ { \u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;Hello! Please introduce yourself.\u0026#34; } ], \u0026#34;max_tokens\u0026#34;: 200, \u0026#34;temperature\u0026#34;: 0.7 }\u0026#39; 2. Chat Completions API（Google Gemini Flash） # # Gemini Flashを使用したチャット curl -X POST \u0026#34;${CUSTOM_DOMAIN}/v1/chat/completions\u0026#34; \\ -H \u0026#34;Authorization: Bearer ${MASTER_KEY}\u0026#34; \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{ \u0026#34;model\u0026#34;: \u0026#34;gemini-flash\u0026#34;, \u0026#34;messages\u0026#34;: [ { \u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;What are the advantages of using Google Cloud Run for hosting AI services?\u0026#34; } ], \u0026#34;max_tokens\u0026#34;: 200, \u0026#34;temperature\u0026#34;: 0.3 }\u0026#39; Web UIでの管理機能確認 # LiteLLM ProxyにはWeb UIでの管理画面が用意されており、ブラウザで管理できます。\n1. UIへのアクセス # 以下のコマンドでUIへのアクセス情報が確認できます。\n# UI認証情報を取得 UI_USERNAME=$(gcloud secrets versions access latest --secret=\u0026#34;litellm-ui-username\u0026#34;) UI_PASSWORD=$(gcloud secrets versions access latest --secret=\u0026#34;litellm-ui-password\u0026#34;) echo \u0026#34;UI URL: $(terraform output -raw cloud_run_url)/ui\u0026#34; echo \u0026#34;Username: ${UI_USERNAME}\u0026#34; echo \u0026#34;Password: ${UI_PASSWORD}\u0026#34; # 以下のように出力される UI URL: https://litellm-proxy-xxxxxxxx.a.run.app/ui Username: xxxxx Password: xxxxx 出力された情報をもとにWeb UIへログインします。\nLiteLLM ログイン画面 2. UIで確認できる機能 # LiteLLM のWeb UIからは以下のような情報が確認できます。\n詳しくは公式ドキュメントを参照ください。（LiteLLM公式ドキュメントリンク ）\nリクエスト統計: 各モデルの使用回数、成功/失敗率 コスト追跡 : プロバイダー別のコスト情報 使用量制限 : ユーザーやAPIキー別の制限設定 ログ表示 : リアルタイムのリクエストログ モデル管理 : 利用可能モデルの一覧と設定 LiteLLM Web UI画面 LangfuseでのTrace確認 # Langfuseでは、LiteLLM Proxyを利用した各種トレース情報が確認できます。\n本手順で設定したLangfuseホストにアクセスし、Traceを確認します。\nOpenAIのTrace確認 # 上記のCurlコマンドで確認したOpenAI/GTP へのアクセスのTraceを確認します。\nLangfuse - OpenAI Trace確認 GeminiのTrace確認 # 上記のCurlコマンドで確認したVertexAI/Gemini へのアクセスのTraceを確認します。\nLangfuse - Gemini Trace確認 まとめ # 構築の利点の再確認 # 今回のTerraformとGoogle Cloud Runを使用したLiteLLM Proxy構築により、以下の利点が実現できました\n1. 運用・管理面での利点 # コスト一元管理: 複数のLLMプロバイダーのコストを統合して追跡・制御 APIキー管理: Secret Managerによる安全な機密情報管理 統一API: OpenAI互換APIによる既存コードの再利用性 2. インフラ面での利点 # サーバーレス: インフラ管理不要で自動スケーリング 高可用性: Cloud Runのマネージドサービス利用 監視: Cloud Loggingによる包括的なログ管理 おわりに # 本記事では、TerraformとGoogle Cloud Runを用いて、Google Cloud上にLiteLLM Proxyを構築する手順を解説しました。\n構築したLiteLLM Proxyは、複数のLLMプロバイダーを一元的に管理し、コストや使用量を効率的に制御できる強力なLLMゲートウェイとして機能します。また、Cloud Runのサーバーレスな特性により、運用負荷を最小限に抑えつつ高い可用性を実現できます。\n本記事では触れませんでしたが、LiteLLM Proxyにはコスト管理やLLMプロバイダーのフォールバック機能など、LLMアプリケーションの開発・運用に役立つ機能が他にも備わっています。\nぜひ、LiteLLM Proxyを活用して、効率的で柔軟なLLMアプリケーション開発環境を構築してみてください！\n","date":"2025年9月3日","externalUrl":null,"permalink":"/posts/2025-09-03-terraform%E3%81%A7litellm-proxy%E3%82%92google-cloud%E4%B8%8A%E3%81%AB%E6%A7%8B%E7%AF%89%E3%81%99%E3%82%8B/","section":"Posts","summary":"はじめに # 近年、LLMアプリケーションの開発において、複数のLLMプロバイダーを使い分ける必要性が高まっています。 OpenAIのGPT、AnthropicのClaude、GoogleのGeminiなど、それぞれ異なる特徴を持っており、それぞれの特徴に合わせてLLMを使い分けるケースも多くあります。 また、コスト最適化の観点から、簡単なタスクには安価なモデルを、複雑なタスクには高性能なモデルを使い分けることも重要です。\n","title":"TerraformでLiteLLM ProxyをGoogle Cloud上に構築する","type":"posts"},{"content":"","date":"2025年9月2日","externalUrl":null,"permalink":"/tags/dify-langfuse-plug-in/","section":"タグ","summary":"","title":"Dify Langfuse Plug-In","type":"tags"},{"content":"更新日：2025年9月16日\nLLMアプリケーション開発プラットフォーム「Dify」と、LLMオブザーバビリティツール「Langfuse」を連携させるためのカスタムプラグイン「Dify Langfuse Plugin」が、v0.0.2にアップデートされました。\n今回のアップデートの目玉は、Langfuseからプロンプトを取得する「Get Prompt」ツールに、プロンプト内の変数を動的に置換する機能が追加された点です。これにより、Difyのワークフロー上で、より柔軟かつ再利用性の高いプロンプト管理が実現可能になります。\n本記事では、この新しいアップデート内容を中心に、Dify Langfuseプラグインの魅力と使い方を詳しくご紹介します。\nリリース情報はこちらからご確認いただけます。\nGitHub Release v0.0.2 Dify Langfuseプラグインとは？ # Dify Langfuseプラグインは、Difyのワークフロー内からLangfuseで管理しているプロンプトを直接呼び出したり、検索・更新したりするためのカスタムツールです。\nこのプラグインを使うことで、煩雑になりがちなプロンプトのバージョン管理やチームでの共有をLangfuseに集約し、Difyからは常に最適なプロンプトを呼び出してアプリケーションを構築、といった効率的な開発フローが実現できます。\nプラグインの基本的な機能や導入メリットについては、以下の記事で詳しく解説しています。\nDify のプロンプト管理を劇的に改善！Langfuse プラグインのご紹介DifyのプロンプトをLangfuseプラグインで管理する方法を紹介。バージョン管理・チーム共有・再利用を実現し、Dify開発時のプロンプト課題を解決します。 2025-05-22 【v0.0.2】Get Promptツールが変数置換に対応！ # 今回のアップデートで最も強力なのが、Get Promptツールの機能強化です。\n従来、Langfuseから取得できるのは静的なプロンプト本文のみでした。しかし、v0.0.2からは、プロンプト内に {{variable_name}} のような形式で変数を定義しておき、Dify側から値を渡して動的にプロンプトを生成できるようになりました。\n具体的な使い方 # 新しく追加されたツール変数 variables に、置換したい変数をJSON形式で指定します。\n例えば、Langfuseに以下のようなプロンプトが保存されているとします。\nこんにちは{{name}}さん、{{country}}へようこそ！ このプロンプトをGet Promptツールで取得する際に、variables パラメータに次のようなJSON文字列を渡します。\n{\u0026#34;name\u0026#34;: \u0026#34;太郎\u0026#34;, \u0026#34;country\u0026#34;: \u0026#34;日本\u0026#34;} すると、ツールからの出力 text は、変数が置換された以下の文字列になります。\nこんにちは太郎さん、日本へようこそ！ これにより、Difyのワークフロー内で得られた変数（ユーザーの入力など）を使って、Langfuseで一元管理されたプロンプトテンプレートを動的にカスタマイズすることが可能になります。\n変数置換前のプロンプトの例 変数置換後のプロンプトの例 アップデートされたGet Promptツールの詳細 # 以下は、Get Promptツールの入力パラメータと出力の詳細です。\n入力パラメータ # パラメータ名 説明 必須 デフォルト値 name Langfuseで管理されているプロンプトの一意な名前。 はい - label 取得したいプロンプトバージョンに付与されたラベル。version との同時指定不可。 いいえ production version 取得したいプロンプトの特定のバージョン番号。label との同時指定不可。 いいえ - variables プロンプト内の変数を置換するためのJSON文字列。形式：{\u0026ldquo;variable_name\u0026rdquo;: \u0026ldquo;value\u0026rdquo;} いいえ - 出力 # 変数が正しく置換されると、text には置換後のプロンプト本文が、json には処理の詳細を含むメタデータが返されます。\npromptキー はこれまで通り変数置換前のプロンプトが、processed_promptキー には変数置換後のプロンプトが入ります。\n出力例:\n{ \u0026#34;text\u0026#34;: \u0026#34;こんにちは太郎さん、日本へようこそ！\u0026#34;, \u0026#34;files\u0026#34;: [], \u0026#34;json\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;greeting_prompt\u0026#34;, \u0026#34;version\u0026#34;: 2, \u0026#34;prompt\u0026#34;: \u0026#34;こんにちは{{name}}さん、{{country}}へようこそ！\u0026#34;, \u0026#34;processed_prompt\u0026#34;: \u0026#34;こんにちは太郎さん、日本へようこそ！\u0026#34;, \u0026#34;variables_applied\u0026#34;: true, \u0026#34;labels\u0026#34;: [\u0026#34;production\u0026#34;, \u0026#34;latest\u0026#34;], ... } ] } json出力にprocessed_promptキーを追加 まとめ：より高度なLLMアプリケーション開発へ # 今回のDify Langfuseプラグインのアップデートにより、プロンプトの管理と活用の幅が大きく広がりました。\nぜひ、この機会にDify Langfuseプラグインをアップデート（または新規インストール）して、より効率的で高度なLLMアプリケーション開発にお役立てください。\nインストールと詳細はこちらのGitHubリポジトリのREADMEから\nhttps://github.com/gao-ai-com/dify-plugin-langfuse ","date":"2025年9月2日","externalUrl":null,"permalink":"/posts/2025-09-02-dify-langfuse%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E3%81%8Cv0-0-2%E3%81%AB%E3%82%A2%E3%83%83%E3%83%97%E3%83%87%E3%83%BC%E3%83%88-%E3%83%97%E3%83%AD%E3%83%B3%E3%83%97%E3%83%88%E3%81%AE%E5%A4%89%E6%95%B0%E7%BD%AE%E6%8F%9B%E3%81%AB%E5%AF%BE%E5%BF%9C/","section":"Posts","summary":"更新日：2025年9月16日\nLLMアプリケーション開発プラットフォーム「Dify」と、LLMオブザーバビリティツール「Langfuse」を連携させるためのカスタムプラグイン「Dify Langfuse Plugin」が、v0.0.2にアップデートされました。\n","title":"Dify Langfuseプラグインがv0.0.2にアップデート！プロンプトの変数置換に対応","type":"posts"},{"content":"こんにちは。ガオ株式会社の橘です。\n今回は、Langfuseに加わった新しい連携元「mcp-use」と、その連携方法や内容についてご紹介します。\n要約 # mcp-useにLangfuse連携機能が搭載された これにより、mcp-useで構築したLLM Agentから、Langfuseに簡単にtrace連携を行い、分析・運用ができるようになった 簡単な設定で、LLM Agentの動作ログがLangfuseに連携できる 連携されたtraceの例 mcp-useとは # mcp-useとは、mcp-use社が提供している、LLM agentの構築ライブラリです。たった数行のコード記述で、MCPサーバと接続してその機能を利用するLLM agentが構築できると謳われています。\n2025年8月現在では、Python版とTypeScript版が提供されています。\n詳しくは公式ページ やGithubのページ（Python版 、TypeScript版 ）をご参照ください。\nLangfuse連携の設定方法 # 以下の準備を行います。\nLangfuse側\ntraceを連携する連携先organizationsとprojectを用意する 既存のものを使うか、新規に作成するかは要件に合わせて選択 手順はこちらの記事 等を参照 APIKEYの発行を行い、「public key, secret key, hostnameの値」を控える 手順はこちらの記事 等を参照 mcp-use側\nmcp-useを使ったLLM Agentを構築する 我々は動作確認に、公式が公開 しているquickstartを利用 １画面に収まるPythonコードで、MCPサーバを活用する自律推論LLM Agentが構築できるのはなかなか凄い 以下の環境変数に、Langfuse側で発行した（＝控えておいた）値を設定する 環境変数名 設定する値 LANGFUSE_PUBLIC_KEY Langfuse側で発行したpublic key LANGFUSE_SECRET_KEY Langfuse側で発行したsecret key LANGFUSE_HOST Langfuseが稼働しているホスト名 基本的な準備は以上です。あとは、mcp-useが動作する環境からLangfuseが待ち受ける環境に対してHTTPS接続できる必要があるため、必要に応じてそのための設定（ファイアウォール設定変更等）を実施します（環境に大きく依存するため、本記事では割愛します）。\ntrace連携と結果確認 # 前述の準備をしてあれば、あとはmcp-useで作成したLLM Agentを動作させるだけで、自動的に（APIKEYを発行した organizations \u0026amp; project に）traceが連携されます。 これにより、LLM Agentがどのような入力に対し、どのMCPサーバを選択し、どんなやりとりをしたうえでユーザへの返答を構成したかが簡単にわかるようになります。また、時系列に沿ってtraceを追え、かつそれぞれの処理時間も可視化されるため、処理時間の長い応答のボトルネック調査なども容易に行うことができます。\nMCP tool選択を行った際のtrace例 まとめ # mcp-useがLangfuse連携に対応したことで、簡単にLLM Agentの挙動を確認できるようになりました。\nこれからLLM Agentを構築しようとしている方は、mcp-useとLangfuseの組み合わせも検討してみてはいかがでしょうか？\n備考 # LLM Agentの動作には、別途LLM APIを利用するためのAPIKEY等が必要です。詳細はmcp-useのドキュメントをご参照ください。 今回我々が検証したのはPython版のmcp-useのみで、TypeScript版mcp-useは未検証です。 mcp-useがLangfuse連携対応しているのは2025/08/19にリリースされたv1.3.10からなので、古いバージョンのmcp-useを使っている方は、まずmcp-useのアップデートを実施してください。 Langfuseのサイトでは「利用するための前提条件としてmcp-useのアカウントが必要」とありますが、2025/08/21現在ではmcp-useのアカウントを作らなくともtrace連携は問題なく動作しました。 ","date":"2025年8月21日","externalUrl":null,"permalink":"/posts/2025-08-21-langfuse%E3%81%AE%E6%96%B0%E3%81%97%E3%81%84%E9%80%A3%E6%90%BA%E5%85%83-mcp-use/","section":"Posts","summary":"こんにちは。ガオ株式会社の橘です。\n今回は、Langfuseに加わった新しい連携元「mcp-use」と、その連携方法や内容についてご紹介します。\n要約 # mcp-useにLangfuse連携機能が搭載された これにより、mcp-useで構築したLLM Agentから、Langfuseに簡単にtrace連携を行い、分析・運用ができるようになった 簡単な設定で、LLM Agentの動作ログがLangfuseに連携できる 連携されたtraceの例 ","title":"Langfuseの新しい連携元「mcp-use」","type":"posts"},{"content":"","date":"2025年8月21日","externalUrl":null,"permalink":"/tags/llm-agent/","section":"タグ","summary":"","title":"LLM Agent","type":"tags"},{"content":" はじめに # GoogleのVertex AI Geminiが提供するコンテキストキャッシュ機能は、大量のコンテキストを再利用することで、APIコストを大幅に削減できる強力なツールです。しかし、実際にどの程度のコスト削減効果があるのかを可視化するには、一手間加える必要があります。\n本記事では、オープンソースのLLM監視プラットフォームであるLangfuseとGeminiのコンテキストキャッシュを組み合わせ、キャッシュ効果を定量的に監視し、コストの内訳を詳細に把握する方法を解説します。\n必要なツール # Google Cloud Platform: Vertex AI API 有効化済み Langfuse: OpenSourceの LLM Engineering Platform Python 実行環境: uv または pip でパッケージ管理 実現できること # Before（従来） # キャッシュを利用しているものの、具体的な効果が不明確 コストの内訳（通常入力 vs キャッシュ）が見えない レイテンシ改善効果が分からない After（本手法） # 📊 Langfuse ダッシュボード表示例: ├── 入力トークン: 15 → $0.0000045 ├── キャッシュトークン: 1,189 → $0.0000891 ├── 出力トークン: 1,291 → $0.0032275 ├── 合計コスト: $0.0033211 └── レイテンシ: 自動測定 実装手順 # ステップ 1: Langfuse でモデル定義を設定 # ※ 今回は、gemini-2.5-flashを利用する前提で手順の説明をします。\nLangfuse ダッシュボードにログイン\nSettings \u0026gt; Modelsでgemini-2.5-flashを探し、右のCloneを選択\nモデルにキャッシュの価格を追加する 今回は、gemini-2.5-flashにcached_tokensとして$0.000000075(2025年8月時点の金額)と入力\nステップ 2: コンテキストキャッシュを作成 # ライブラリインストール google-generativeai langfuse python-dotenv 環境変数定義 (envファイル作成) ### .env LANGFUSE_PUBLIC_KEY=[パブリックキー] LANGFUSE_SECRET_KEY=[シークレットキー] LANGFUSE_HOST=[langfuseのホストURL] GOOGLE_CLOUD_PROJECT=[自身のGoogle Cloud Project ID] GOOGLE_CLOUD_LOCATION=global 大量のコンテキストデータを準備（例：長い文書、FAQ、マニュアル、PDF） 今回は長いテキストの文章をキャッシュに入れるようなサンプルになってます。 2. Vertex AI Clientでキャッシュを作成\n📖 公式ドキュメント: Context Cache Create - Google Cloud from google.genai.types import Content, CreateCachedContentConfig, Part content_cache = client.caches.create( model=\u0026#34;gemini-2.5-flash\u0026#34;, config=CreateCachedContentConfig( contents=[Content(role=\u0026#34;user\u0026#34;, parts=[Part.from_text(text=long_context)])], ttl=\u0026#34;3600s\u0026#34;, # 1時間有効 ), ) print(f\u0026#34;キャッシュID: {content_cache.name}\u0026#34;) キャッシュを作成するのには以下の制限があります。\n最小キャッシュサイズ: 1,024 トークン以上のコンテンツが必要（Gemini 2.5 Flash） 最大コンテンツサイズ: 10MB まで TTL 制限: 最小 1 分、最大制限なし 制限詳細: Google Cloud 公式制限表 コンテキストキャッシュ作成時のprint文で表示されたキャッシュID （例：projects/xxx/locations/global/cachedContents/123）を保存します。 次の呼び出しで使用するため、環境変数に入れておくと便利です。\n# .env GEMINI_CACHE_ID=projects/xxx/locations/global/cachedContents/123 ステップ 3: Langfuse 統合コードの実装 # 3-1. 使用量データの正確な取得 # Gemini APIでは、モデルを呼び出した後のレスポンス内にトークン使用数が含まれています。このレスポンスから得られたトークン数をLangfuseに渡す必要があります。\n# キャッシュIDを使用してGemini呼び出し cache_id = \u0026#34;projects/xxx/locations/global/cachedContents/123\u0026#34; # ステップ2で取得 response = client.models.generate_content( model=\u0026#34;gemini-2.5-flash\u0026#34;, contents=\u0026#34;あなたの質問をここに入力\u0026#34;, config=GenerateContentConfig(cached_content=cache_id) ) # APIレスポンスから直接取得 usage_metadata = response.usage_metadata cached_tokens = getattr(usage_metadata, \u0026#34;cached_content_token_count\u0026#34;, 0) input_tokens = getattr(usage_metadata, \u0026#34;prompt_token_count\u0026#34;, 0) - cached_tokens output_tokens = getattr(usage_metadata, \u0026#34;candidates_token_count\u0026#34;, 0) 補足 # キャッシュから供給されたトークン数は、呼び出し後の response.usage_metadata.cached_content_token_count に入ります GenerateContentConfig(cached_content=cache_id) を付けて呼び出す必要があります。 prompt_token_count は「プロンプト全体（キャッシュ分を含む）」のトークン数です。 通常課金される入力トークンは、全体からキャッシュ分を引いた数で算出をしています。 3-2. Langfuse トレース実装 # このサンプルで行っていること（流れ）\n@observe(\u0026hellip;, as_type=\u0026ldquo;generation\u0026rdquo;) で関数を計測対象にし、レイテンシを自動取得 GenerateContentConfig(cached_content=cache_id) を指定して Gemini を呼び出し レスポンス usage_metadata から cached_content_token_count などの使用量を取得 課金対象の入力トークンを prompt_token_count - cached_content_token_countで算出 update_current_generation に model/input/output/usage_details/metadata を渡して Langfuse に送信 モデル定義の単価に基づき、Langfuse 側でコストが自動計算・可視化 サンプルソースコード\nimport os from typing import Any from dotenv import load_dotenv from google import genai from google.genai.types import GenerateContentConfig, HttpOptions from langfuse import get_client, observe load_dotenv() def build_clients() -\u0026gt; tuple[genai.Client, Any]: _ = get_client() project_id = os.getenv(\u0026#34;GOOGLE_CLOUD_PROJECT\u0026#34;) location = os.getenv(\u0026#34;GOOGLE_CLOUD_LOCATION\u0026#34;) client = genai.Client( vertexai=True, project=project_id, location=location, http_options=HttpOptions(api_version=\u0026#34;v1\u0026#34;), ) return client, _ @observe(name=\u0026#34;gemini-cached-call\u0026#34;, as_type=\u0026#34;generation\u0026#34;) def call_gemini_with_cache( client: genai.Client, cache_id: str, query: str, model_name: str = \u0026#34;gemini-2.5-flash\u0026#34;, ) -\u0026gt; str: \u0026#34;\u0026#34;\u0026#34;Call Gemini with context cache and report usage to Langfuse.\u0026#34;\u0026#34;\u0026#34; response = client.models.generate_content( model=model_name, contents=query, config=GenerateContentConfig(cached_content=cache_id), ) usage = response.usage_metadata cached_tokens = getattr(usage, \u0026#34;cached_content_token_count\u0026#34;, 0) input_tokens = max(0, getattr(usage, \u0026#34;prompt_token_count\u0026#34;, 0) - cached_tokens) output_tokens = getattr(usage, \u0026#34;candidates_token_count\u0026#34;, 0) langfuse_client = get_client() langfuse_client.update_current_generation( model=model_name, input=query, output=(response.text or \u0026#34;\u0026#34;), usage_details={ \u0026#34;input\u0026#34;: input_tokens, \u0026#34;cached_tokens\u0026#34;: cached_tokens, \u0026#34;output\u0026#34;: output_tokens, }, metadata={ \u0026#34;cache_id\u0026#34;: cache_id, \u0026#34;provider\u0026#34;: \u0026#34;vertex-ai\u0026#34;, }, ) return response.text or \u0026#34;\u0026#34; def main() -\u0026gt; None: client, langfuse_client = build_clients() # Example placeholders for blog readers cache_id = os.getenv( \u0026#34;GEMINI_CACHE_ID\u0026#34;, \u0026#34;projects/[Project ID]/locations/global/cachedContents/[cache_number]\u0026#34;, ) query = \u0026#34;データサイエンスについて教えて\u0026#34; _ = call_gemini_with_cache(client, cache_id, query) # Ensure data is flushed to Langfuse backend in short-lived processes langfuse_client.flush() if __name__ == \u0026#34;__main__\u0026#34;: main() Langfuseダッシュボード可視化 # 以下の画像のように、キャッシュトークンが別途コストとして出力されています。\n命名の注意点\ninput, output という名前を入れた変数を設定すると、それぞれ Input cost, Output cost に分類されます。\n例：cached_input_tokensと命名し、モデル価格をセットすると以下のようにinput costとして描画されるようになります。 キャッシュコストを独立して表示したい場合は cached_tokens など別の名前を使用してください。 cached_tokens は Other cost として独立したカテゴリで表示されます。 まとめ # この手法により、以下を実現できます\n透明性: キャッシュ効果の可視化 自動化: 手動計算不要のコスト監視 継続性: 長期的なコスト最適化 スケール: 大規模システムでの運用 Gemini のコンテキストキャッシュと Langfuse を組み合わせることで、生成AIの「コスト」「レイテンシ」「再利用性」を定量的にコントロールできます。特に、長文コンテキストやPDF・動画等のマルチモーダルのデータの再利用が多いワークロードでは、入力コストの削減と応答時間の短縮が同時に見込め、AI アプリケーションの運用効率を大幅に向上させることができると思いますので、ぜひGemini のコンテキストキャッシュと Langfuseを利用して、生成AIアプリケーションの開発を進めていただけると嬉しいです。\n","date":"2025年8月17日","externalUrl":null,"permalink":"/posts/2025-08-17-gemini%E3%82%B3%E3%83%B3%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88%E3%82%AD%E3%83%A3%E3%83%83%E3%82%B7%E3%83%A5%E3%81%A8langfuse%E3%81%A7%E5%AE%9F%E7%8F%BE%E3%81%99%E3%82%8B%E3%82%B3%E3%82%B9%E3%83%88%E7%9B%A3%E8%A6%96/","section":"Posts","summary":"はじめに # GoogleのVertex AI Geminiが提供するコンテキストキャッシュ機能は、大量のコンテキストを再利用することで、APIコストを大幅に削減できる強力なツールです。しかし、実際にどの程度のコスト削減効果があるのかを可視化するには、一手間加える必要があります。\n","title":"GeminiコンテキストキャッシュとLangfuseで実現するコスト監視","type":"posts"},{"content":"本記事は こちらの記事 の続編 (アップデート版) となります。\n(改めて) n8nとは何か、プロンプト管理の課題 # n8nは「nodemation」の略称で、ドラッグ\u0026amp;ドロップ操作や各ノードの設定によってワークフローを作成できる自動化ツールです。300以上の組み込みノードを提供しており、Slack、Gmail、Notion、カレンダー、Webhookなど、様々なサービスとの連携が可能です。コードを書くことなく複雑な自動化フローを構築できる一方で、JavaScript や最近はPython を使用したコードの実行をフローに埋め込むことなどにも対応しています。\n同ツールを使用することで簡単に生成AIをワークフローに繋ぎ込むことができますが、\nプロンプト管理の機構が n8n には存在しておらず (筆者が観測する限り)、フローごとに作るなどの手間が発生してしまいます。\nLangfuseとの連携 # その課題に解決すべく前回の記事 では簡易的なコードで解決していましたが、記事を書いた直後に公式ノードがリリースされましたので、本ブログはそちらをご紹介します。\n前回記事を書いた1週間後に Langfuse CEO の Marc が公式版を作り始めた 前回の構成はこちらでしたが、 Code ノードを公式のノードに置き換えます。\n前回の記事の n8n フロー ゴールはこのような構成です。\nCode を Langfuse公式の Get a prompt に変更 まず、任意の場所で Add node -\u0026gt; Langfuseを検索 -\u0026gt; Install してください。\n右クリックなどして画面を表示。 検索欄に \u0026ldquo;Langfuse\u0026quot;と入れる 右上の \u0026ldquo;Install node\u0026rdquo; そうすると Langfuseノードがキャンパス上にできますので、クリックして中にキーを Credential と 取得する Prompt 名を指定しましょう。\nCredentialのところをクリックで新規作成 利用する時には Langfuseノードを前後のノードと接続するだけです。例えば AI Agentノードと接続すると、このような画面でプロンプトが見えておりますので、それをSystem promptなどに指定するだけです。（User promptももちろん必要です）\n左側のPromptから Drag \u0026amp; Drop で変数を入れるだけ テストでチャットしてみると、ちゃんとプロンプトを認識して動作してくれます。\nそして前回同様に Trace も正常に反映されています。\nプロンプトが反映された回答がきている Trace画面 いかがでしたでしょうか。今回のアップデートにより、さらに簡単に n8n からプロンプト管理ができるようになります。また前の記事のように簡単にTraceを取ることもできますので、n8n で生成AIを管理する方はぜひ試してしてみてください。\n","date":"2025年8月15日","externalUrl":null,"permalink":"/posts/2025-08-15-langfuse%E3%81%A7%E8%A7%A3%E6%B1%BA%E3%81%99%E3%82%8B-%E8%87%AA%E5%8B%95%E5%8C%96%E3%83%84%E3%83%BC%E3%83%AB-n8n-%E3%81%AE%E3%83%97%E3%83%AD%E3%83%B3%E3%83%97%E3%83%88%E8%AA%B2%E9%A1%8C/","section":"Posts","summary":"本記事は こちらの記事 の続編 (アップデート版) となります。\n(改めて) n8nとは何か、プロンプト管理の課題 # n8nは「nodemation」の略称で、ドラッグ\u0026ドロップ操作や各ノードの設定によってワークフローを作成できる自動化ツールです。300以上の組み込みノードを提供しており、Slack、Gmail、Notion、カレンダー、Webhookなど、様々なサービスとの連携が可能です。コードを書くことなく複雑な自動化フローを構築できる一方で、JavaScript や最近はPython を使用したコードの実行をフローに埋め込むことなどにも対応しています。\n","title":"Langfuseで解決する 自動化ツール n8n のプロンプト課題","type":"posts"},{"content":"","date":"2025年8月15日","externalUrl":null,"permalink":"/tags/n8n/","section":"タグ","summary":"","title":"N8n","type":"tags"},{"content":"","date":"2025年8月15日","externalUrl":null,"permalink":"/tags/%E3%83%97%E3%83%AD%E3%83%B3%E3%83%97%E3%83%88/","section":"タグ","summary":"","title":"プロンプト","type":"tags"},{"content":"これまで、LangfuseでのPIIマスキング手法として、llm-guard 、Guardrails for Amazon Bedrock 、そしてLLM（Gemini 2.5 Flash Lite） によるマスキング手法を検討してきました。\n今回は、Google Cloudの機密データ保護機能であるSensitive Data Protection（旧：Cloud Data Loss Prevention, Cloud DLP） の利用について検討します。\nSensitive Data Protection とは？ # Sensitive Data Protection （旧称：Cloud Data Loss Prevention、Cloud DLP）は、Google Cloudが提供する機密データ保護サービスです。データの検出、分類、匿名化といった強力な機能を提供し、個人情報（PII）をはじめとする機密情報の漏洩リスクを軽減します。\nこのサービスは、単なるパターンマッチングにとどまらず、機械学習や文脈分析を組み合わせることで、より高精度な機密データ検出を実現します。特に、クレジットカード番号、社会保障番号、メールアドレス、電話番号など、世界中の150種類以上の組み込みのInfoType（情報タイプ） 検出器をサポートしており、非常に広範な種類のPIIに対応できます。また、独自のビジネスニーズに合わせてカスタムInfoTypeを定義することも可能です。\nGuardrails for Amazon Bedrockと同様に、APIを通じてPIIマスキングを実行できます。今回の検証では、DLP APIを利用してマスキングを行います。\nGuardrails for Amazon Bedrock との違いは？ # これまでの検証で取り上げたGuardrails for Amazon Bedrockは、Sensitive Data Protection と目的が近いサービスです。どちらもLLMの入出力におけるPIIマスキングに利用できますが、いくつか重要な違いがあります。\n特徴 \\ ソリューション Sensitive Data Protection Guardrails for Amazon Bedrock 最適なユーザー Google Cloud環境でLLMアプリケーションを運用するユーザー AWS BedrockをLLMプラットフォームとして利用するユーザー 検出範囲と精度 汎用PII検出、150種類以上の高精度なInfoType（PIIに特化） Bedrockの入出力に特化、PIIを含む多様なフィルタタイプ 適用レイヤー アプリケーション層での柔軟な適用（API経由） Bedrock API呼び出し時（リアルタイム） リアルタイム性 API経由でリアルタイム処理が可能 リアルタイムで適用可能 Langfuse連携 API呼び出し結果をLangfuseへ記録可能 API呼び出し結果をLangfuseへ記録可能 その他特徴 高度な匿名化オプション、広範なPII検出器、コンテキストを考慮した検出 Bedrockでのコンテンツモデレーションの一元化、リアルタイムポリシー適用 Guardrails for Amazon BedrockはAmazon Bedrockと密接に統合されており、Bedrockを利用する際に特化したリアルタイム制御を提供します。一方、Sensitive Data ProtectionはGoogle Cloudの汎用的な機密データ保護サービスであり、より広範なPIIタイプに対応し、アプリケーション層で柔軟に組み込める点が特徴です。\n今回の検証では、PII検出の対象をサンプルコード内で設定しますが、Guardrailsと同様にコンソール上での設定も可能です。サービスとして運用する際は、コンソールで検出対象を追加・調整し、アプリケーションの修正やリリースなしでポリシーを更新できるテンプレートの利用を検討することをおすすめします。\nテスト方法 # 今回は以下の手順で、Sensitive Data ProtectionのPIIマスキング機能を検証していきます。\n事前準備 # まず、Sensitive Data Protection が提供するInfoType検出器（検出対象） を確認します。検出器のリファレンスはこちら にあります。過去の検証と同様に、今回はカスタム検出器は利用せず、組み込みのInfoType検出器のみを使用します。\n当初、すべてのInfoTypeを設定しようとしましたが、検出器の設定可能上限数（150個）を超過したため（総数216個）、カテゴリがPIIに該当するものに限定して設定することにしました。\n多数のInfoTypeを手動でリスト化するのは大変なので、以下のPythonコードでAPIからPII関連のInfoTypeを自動で取得します。\n# DLPクライアントを初期化（クォータプロジェクトを指定） dlp_client = dlp_v2.DlpServiceClient client_options={\u0026#34;quota_project_id\u0026#34;: project_id} ) def get_info_types(): info_types_response = dlp_client.list_info_types( request={ \u0026#39;parent\u0026#39;: f\u0026#34;projects/{project_id}\u0026#34;, \u0026#39;filter\u0026#39;: \u0026#39;supported_by=INSPECT\u0026#39; # 検査可能なInfoTypeに絞る } ) included_info_types = [] pii_list = [] for info_type in info_types_response.info_types: if info_type.name in included_info_types: continue # categoriesを反復してPIIを探す is_pii = False for category in info_type.categories: if category.type_category.name == \u0026#34;PII\u0026#34;: is_pii = True break; if is_pii: included_info_types.extend(info_type.specific_info_types) pii_list.append({ \u0026#39;name\u0026#39;: info_type.name, }) return pii_list 参考 # マスキング対象として期待する項目と、llm-guardのAnonymize機能、Guardrails for Amazon Bedrockの機密情報フィルター、そしてSensitive Data ProtectionのInfoType検出器で対応すると思われる項目を比較表にまとめました。\nPII llm-guard Guardrail for Amazon Bedrock Sensitive Data Protection 氏名 人名 NAME PERSON_NAME 生年月日 - - DATE_OF_BIRTH 住所 - ADDRESS GEOGRAPHIC_DATA 電話番号 電話番号 PHONE PHONE_NUMBER メールアドレス メールアドレス EMAIL EMAIL_ADDRESS 運転免許証番号 - DRIVER_ID DRIVERS_LICENSE_NUMBER パスポート番号 - US_PASSPORT_NUMBER PASSPORT 社会保障番号 米国社会保障番号（SSN） US_SOCIAL_SECURITY_NUMBER US_SOCIAL_SECURITY_NUMBER JAPAN_INDIVIDUAL_NUMBER（マイナンバー） （クレジットカード情報） カード番号 クレジットカード CREDIT_DEBIT_CARD_NUMBER CREDIT_CARD_DATA （クレジットカード情報） 有効期限 - CREDIT_DEBIT_CARD_EXPIRY CREDIT_CARD_DATA （クレジットカード情報） セキュリティコード - CREDIT_DEBIT_CARD_CVV CREDIT_CARD_DATA （銀行口座情報）銀行名 - - - （銀行口座情報）支店名 - - - （銀行口座情報） 口座番号 - US_BANK_ROUTING_NUMBER FINANCIAL_ID llm-guardと比べると検出項目は充実していますが、Guardrails for Amazon Bedrockでカバーしきれていなかった「生年月日」もSensitive Data Protectionでは検出できるようです。銀行口座情報の銀行名や支店名を除けば、期待されるほとんどのPII項目を網羅できる見込みです。\n今回の検証では、誤検知が発生しないかも確認するため、PIIカテゴリに属する85種類の検出器をすべて設定しました。\nアプリケーションコード # 過去の記事と同様に、簡単なPythonコードを用いてプロンプトの入力とLLMの回答を模擬的に生成し、それぞれをLangfuseに登録する形式でテストを行います。\nget_info_types関数とDLPクライアントの初期化部分を追記、masking_functionの中身をDLP APIを利用するように修正します。マスキング方法は、今回は検出されたInfoTypeの名前で置換する設定にします。\n今回は行いませんが、「*」のような特定の文字で置き換えたり、固定文言の指定、設定した文言のいずれかと置換、といった設定も可能です。\ndef masking_function(data: any, **kwargs) -\u0026gt; any: if isinstance(data, str) and len(data) \u0026gt; 0: # 利用可能なPIIのInfoTypeを取得 info_types_list = get_info_types() request = { \u0026#39;parent\u0026#39;: f\u0026#34;projects/{project_id}\u0026#34;, \u0026#39;inspect_config\u0026#39;: { \u0026#39;info_types\u0026#39;: info_types_list }, \u0026#39;deidentify_config\u0026#39;: { \u0026#39;info_type_transformations\u0026#39;: { \u0026#39;transformations\u0026#39;: [ { \u0026#39;primitive_transformation\u0026#39;: { \u0026#39;replace_with_info_type_config\u0026#39;: {} } } ] } }, \u0026#39;item\u0026#39;: {\u0026#39;value\u0026#39;: data} } # DLPを使用したPIIマスキング response = dlp_client.deidentify_content( request=request ) sanitized_data = response.item.value return sanitized_data else: return data テストデータ # 過去の記事と同様に、以下のダミーデータを流用してテストを行いました。\nダミーデータ\n私の個人情報は以下の通りです： 氏名：山田 太郎 Name: Taro Yamada 生年月日：1985年3月15日 Birthday: March 15, 1985 住所：東京都新宿区西新宿2-8-1 Address: 2-8-1 Nishi-Shinjuku, Shinjuku-ku, Tokyo, Japan 電話番号：03-1234-5678 / 090-9999-8888 Phone: +81-3-1234-5678 / +81-90-9999-8888 メールアドレス：taro.yamada@example.com Email: taro.yamada@example.com 運転免許証番号：123456789012 Driver\u0026#39;s License: DL-123456789012 パスポート番号：TK1234567 Passport Number: TK1234567 社会保障番号：987-65-4321 Social Security Number: 987-65-4321 クレジットカード情報： - カード番号：4111-2222-3333-4444 - 有効期限：12/25 - セキュリティコード：123 Credit Card Information: - Card Number: 4111-2222-3333-4444 - Expiry Date: 12/25 - Security Code: 123 銀行口座情報： - 銀行名：みずほ銀行 - 支店名：新宿支店 - 口座番号：1234567 Bank Account Information: - Bank Name: Mizuho Bank - Branch: Shinjuku Branch - Account Number: 1234567 検証結果 # 以下に、llm-guard（デフォルト設定）、Guardrails for Amazon Bedrock（apply_guardrail）、そして今回検証したSensitive Data Protectionそれぞれで処理した際のPII項目ごとの置換結果を比較します。\nllm-guard\n（デフォルト設定） Guardrail for Amazon Bedrock Sensitive Data Protection 氏名（日本語） [REDACTED_PERSON_1][REDACTED_PERSON_2][REDACTED_PERSON_3] {NAME} [PERSON_NAME] 氏名（英語） [REDACTED_PERSON_4] {NAME} [PERSON_NAME] 生年月日 - - [DATE_OF_BIRTH] 住所 - {ADDRESS} [PERSON_NAME][GEOGRAPHIC_DATA] 電話番号（日本） 03-1234-[REDACTED_PHONE_NUMBER_1] {PHONE} [PHONE_NUMBER] / [PHONE_NUMBER] 電話番号（海外） [REDACTED_PHONE_NUMBER_2] {PHONE} [PHONE_NUMBER] / [PHONE_NUMBER] メールアドレス [REDACTED_EMAIL_ADDRESS_1] {EMAIL} [EMAIL_ADDRESS] 運転免許証番号 - {DRIVER_ID} [DRIVERS_LICENSE_NUMBER]\nDL-[DRIVERS_LICENSE_NUMBER] パスポート番号 - {US_PASSPORT_NUMBER} パスポート番号：[DRIVERS_LICENSE_NUMBER]\nPassport Number: [PASSPORT] 社会保障番号 [REDACTED_US_SSN_RE_1] 987-65-4321\n{US_SOCIAL_SECURITY_NUMBER} 987-65-4321 クレジットカード（カード番号） [REDACTED_CREDIT_CARD_RE_1] {CREDIT_DEBIT_CARD_NUMBER} 4111-2222-3333-4444 クレジットカード（カード番号以外） - 有効期限：{CREDIT_DEBIT_CARD_EXPIRY}\nセキュリティコード：{CREDIT_DEBIT_CARD_CVV} 有効期限：12/25\nセキュリティコード：123 銀行口座情報 - 銀行名：みずほ銀行\n支店名：新宿支店\n口座番号：{US_BANK_ACCOUNT_NUMBER}\nBank Name: Mizuho Bank\nBranch: {ADDRESS} Branch\nAccount Number: {US_BANK_ACCOUNT_NUMBER} 銀行名：[PERSON_NAME]銀行\n支店名：[GEOGRAPHIC_DATA]\n口座番号：[FINANCIAL_ID]\nBank Name: Mizuho Bank\nBranch: Shinjuku Branch\nAccount Number: [FINANCIAL_ID] llm-guard（デフォルト設定）とGuardrails for Amazon Bedrock（apply_guardrail）の結果は、以前の記事で検証した際のデータを記載しています。Sensitive Data Protection の検証において、上記2回と明示的に差異がある項目の背景色を変更しています。\n今回の検証で目立つ点としては、期待していたクレジットカード情報や社会保障番号が検出されませんでした。また、いくつかの誤検出も見られます。DLPの検出器には、コンテキストの手がかりがある場合にのみ識別できるものもあるため、もう少し自然な文脈を与えたテキストであれば、結果は変わってくるかもしれません。\n結果詳細（誤検出・検出漏れ項目の抜粋）\n氏名 # 氏名：[PERSON_NAME]Name: [PERSON_NAME]\n検出自体は問題なさそうですが、元のテキストの改行が消えてしまっています。\n住所 # 住所：[PERSON_NAME][GEOGRAPHIC_DATA]\n何かが人名として誤検出されてしまっています。また、どこまでが人名として検出されているかは不明なため推測にはなりますが、おそらく日本語住所＋改行＋Address:＋英語住所がひとまとめに住所としてみなされているようです。\n運転免許証番号 # 運転免許証番号：[DRIVERS_LICENSE_NUMBER] Driver\u0026rsquo;s License: DL-[DRIVERS_LICENSE_NUMBER]\n誤検出というほどではありませんが、「DL-」というプレフィックスが残ってしまっています。\nパスポート番号 # パスポート番号：[DRIVERS_LICENSE_NUMBER] Passport Number: [PASSPORT]\n日本語版が運転免許証番号として誤検出されています。\n社会保障番号 # 社会保障番号：987-65-4321 Social Security Number: 987-65-4321\n期待とはことなり、PIIとして検出されませんでした。\nクレジットカード情報 # クレジットカード情報：\nカード番号：4111-2222-3333-4444 有効期限：12/25 セキュリティコード：123 Credit Card Information:\nCard Number: 4111-2222-3333-4444 Expiry Date: [CREDIT_CARD_EXPIRATION_DATE] Security Code: 123 英語版の有効期限のみが検出されました。\n銀行口座情報 # 銀行口座情報：\n銀行名：[PERSON_NAME]銀行 支店名：[GEOGRAPHIC_DATA] 口座番号：[FINANCIAL_ID] Bank Account Information:\nBank Name: Mizuho Bank Branch: Shinjuku Branch Account Number: [JAPAN_BANK_ACCOUNT] 日本語の銀行名が個人名、支店名が住所として誤検出されています。\nFINANCIAL_ID に JAPAN_BANK_ACCOUNT は含有されていますが、異なる検出器で置き換わっているのは気になります。\n検出の精度は十分とは言えませんが、Guardrailsやllm-guardでは検出できなかった一部の項目（例：生年月日）を捉えることができました。\nまとめ # 今回はGoogle CloudのSensitive Data Protectionを利用したPIIマスキング機能について検証しました。\nテストの結果、一部の項目で優れた検出能力を示しましたが、完全に網羅できているわけではなく、誤検出も見られました。今回の検証で検出できなかったPII項目の一部は、llm-guardのような別のフィルタリングツールでカバーできる可能性があります。そのため、Sensitive Data Protectionとllm-guardを併用することで、コストを抑えつつカバー範囲を広げられるかもしれません。\nまた、今回の検証は限定的な条件での確認となります。実際にアプリケーションで利用する際は、自然なテキスト中にPIIが含まれている場合や、全角文字が混在する場合など、日本語特有の表現が含まれている場合の挙動については、さらに詳細な検証が必要不可欠です。\n検討課題 # Sensitive Data Protectionの料金体系 も考慮すべき点です。料金は、検出・変換されたデータ量（バイト数）に基づいて発生します。月間1GBまでの利用は無料ですが、それを超えると1GBあたり2～3 USDの費用がかかるため、他のソリューションと比較して高額になる可能性があります。\n","date":"2025年7月29日","externalUrl":null,"permalink":"/posts/2025-07-29-langfuse%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8B%E5%80%8B%E4%BA%BA%E6%83%85%E5%A0%B1-pii-%E3%83%9E%E3%82%B9%E3%82%AD%E3%83%B3%E3%82%B0-sensitive-data-protection-%E3%81%AE%E6%B4%BB%E7%94%A8/","section":"Posts","summary":"これまで、LangfuseでのPIIマスキング手法として、llm-guard 、Guardrails for Amazon Bedrock 、そしてLLM（Gemini 2.5 Flash Lite） によるマスキング手法を検討してきました。\n今回は、Google Cloudの機密データ保護機能であるSensitive Data Protection（旧：Cloud Data Loss Prevention, Cloud DLP） の利用について検討します。\n","title":"Langfuseにおける個人情報（PII）マスキング：Sensitive Data Protection の活用","type":"posts"},{"content":"","date":"2025年7月29日","externalUrl":null,"permalink":"/tags/sensitive-data-protection/","section":"タグ","summary":"","title":"Sensitive Data Protection","type":"tags"},{"content":" PIIフィルター用LLMのトレース保存 # こちらの記事 で、Langfuse の PII フィルターに LLM を利用する方法について検証しました。\n実際の運用では、LLM呼び出しにかかる費用を正確に把握するため、PIIフィルターの実行を含めたトレース記録が求められることがよくあります。ここでは、Langfuse の mask オプションを利用する場合の、PIIフィルターLLMの呼び出しとメインのLLM呼び出しを同一トレース内で記録する方法と、その際の注意点について解説します。\nトレース記録の構成イメージ # require_mask = True def masking_function(data: any, **kwargs) -\u0026gt; any: global require_mask if require_mask and isinstance(data, str): try: require_mask = False # PII フィルター適用時のトレース保存 with langfuse.start_as_current_generation( name=\u0026#34;pii_filter_prompt\u0026#34;, prompt=[使用するプロンプト] ) as generation: # LLM呼び出し（マスキング処理） generation.update( output=[フィルター済みデータ], ) finally: require_mask = True return [フィルター済みデータ] langfuse = Langfuse( public_key=\u0026#34;****************\u0026#34;, secret_key=\u0026#34;****************\u0026#34;, host=\u0026#34;*****************\u0026#34;, mask=masking_function, ) # LLM呼び出しのトレース with langfuse.start_as_current_generation( name=\u0026#34;llm_called_trace\u0026#34;, input=[ユーザの入力], ) as generation: # LLM呼び出し（入力に対する回答取得用） generation.update( output=[LLMの回答], ) この構成では、langfuse.start_as_current_generation の input や generation.update の output に対して、自動的に masking_function が呼び出され、PIIがマスキングされた状態でトレースに保存されます。\n注意点：再帰呼び出し（無限ループ）への対策 mask オプションで設定した masking_function 内で、さらに Langfuse のトレース保存（例: langfuse.start_as_current_generation）を行おうとすると、無限ループに陥る可能性があります。これは、トレース保存処理自体が mask オプションの対象となり、masking_function が再帰的に呼び出されてしまうためです。 この問題を避けるためには、masking_function の内部でトレースを保存する際に、多重呼び出しを防止する何らかの制御が必要です。\n今回のサンプルコードでは、処理を簡潔にするため、グローバル変数 (require_mask) をフラグとして持たせ、masking_function 内部からの呼び出しに対してはフィルターを適用しないようにしています。 実際の運用では、mask オプションを利用せず明示的にマスキング用関数を呼び出す方法や、より堅牢な方法を検討することをお勧めします。\n検証結果 # 今回は、以下の条件でトレースの確認を行いました。\n入力値、LLMのプロンプト（PIIフィルター用）: こちらの記事 でも利用した、以下のダミーデータとプロンプトを利用 入力値\n私の個人情報は以下の通りです： 氏名：山田 太郎 Name: Taro Yamada 生年月日：1985年3月15日 Birthday: March 15, 1985 住所：東京都新宿区西新宿2-8-1 Address: 2-8-1 Nishi-Shinjuku, Shinjuku-ku, Tokyo, Japan 電話番号：03-1234-5678 / 090-9999-8888 Phone: +81-3-1234-5678 / +81-90-9999-8888 メールアドレス：taro.yamada@example.com Email: taro.yamada@example.com 運転免許証番号：123456789012 Driver\u0026#39;s License: DL-123456789012 パスポート番号：TK1234567 Passport Number: TK1234567 社会保障番号：987-65-4321 Social Security Number: 987-65-4321 クレジットカード情報： - カード番号：4111-2222-3333-4444 - 有効期限：12/25 - セキュリティコード：123 Credit Card Information: - Card Number: 4111-2222-3333-4444 - Expiry Date: 12/25 - Security Code: 123 銀行口座情報： - 銀行名：みずほ銀行 - 支店名：新宿支店 - 口座番号：1234567 Bank Account Information: - Bank Name: Mizuho Bank - Branch: Shinjuku Branch - Account Number: 1234567 プロンプト\n以下のテキストから、個人情報をマスクしてください。 # 条件 - マスク処理以外は行わない - テキストの前後に余分なテキストを追加しない - どのようなデータがマスクされたかわかるようにする（例：山田太郎 -\u0026gt; [氏名1]、000-0000-0000 -\u0026gt; [電話番号1]） - 同じ個人情報種別でも、異なる情報の場合は別物であるとわかるようにする（例：山田さんと佐藤さん -\u0026gt; [氏名1]さんと[氏名2]さん） # 置換対象の個人情報 - 氏名 - 生年月日 - 住所 - 電話番号 - メールアドレス - 運転免許証番号 - パスポート番号 - 社会保障番号 - クレジットカード情報(カード番号, 有効期限, セキュリティコード) - 銀行口座情報(銀行名, 支店名, 口座番号) # 対象のテキスト {{userMessage}} LLMのプロンプト（問い合わせ用）： 以下のテキスト中に「山」が何度出てくるか数えて\n#対象の文書\n[ 入力値 ]\n結果概要 # 問い合わせ用のLLMの下に、PIIフィルター用のLLM呼び出しがネストする形で保存できました。\nLangfuseのトレース上では、LLM呼び出し（問い合わせ1回、input、output のフィルター用各1回ずつ）計3回分のトークン数や費用が確認できます。\n# LLM呼び出し（llm_called_trace）: # このトレースでは、問い合わせの LLM 問い合わせにかかった費用と、一連の流れ（問い合わせ＋PIIフィルター）でかかった費用の両方が表示されます。\nこのLLM への問い合わせ結果を metadata の情報として登録したい場合、mask オプションが適用される作りになっているかを十分に確認する必要があります。上記のコード例では、文字列型以外の場合にマスキング処理が適用されないため、場合によっては metadata に個人情報が除去されていない状態で残ってしまう可能性があります。トレースの output としては個人情報が除去されていても、metadata には生データが残るリスクがあるため、input や output と metadata に保存する形式が異なっている場合は特に、保存内容には注意を払うことが必要です。\n冒頭の例のコードでLLM からの結果を metadata に保存（一部抜粋） LLM呼び出し（1つ目のpii_filter_prompt）:\nlangfuse.start_as_current_generation( name=\u0026#34;llm_called_trace\u0026#34;, input=[ユーザの入力], ) 上記で設定している、input に対して masking_function が呼び出された結果です。\nwith langfuse.start_as_current_generation( name=\u0026#34;pii_filter_prompt\u0026#34;, prompt=[使用するプロンプト] ) as generation: # LLM呼び出し（マスキング処理） generation.update( output=[フィルター済みデータ], ) 今回はPII フィルターに対するトレースを保存する際に、 PIIフィルターを適用しないよう制御しています。そのため、 個人情報が除去されていない生データを残さないよう、input は保存しないようにします。\nLangfuse で費用を表示したい場合には、モデルとトークン数の情報も必要になります。\nGeminiAPIの場合は、LLM の結果情報に含まれているため、 generation.update の際に model, usage_details もあわせて保存すると今回の例のような形になります。\nLLM呼び出し（2つ目のpii_filter_prompt）: # # LLM呼び出し（入力に対する回答取得用） generation.update( output=[LLMの回答], ) 上記で設定している、output に対して masking_function が呼び出された結果です。ここでも mask オプションが適用されます。\ninput に対するトレース保存と同様に、input には何も設定しないことで、誤って生データがトレースに保存されることを防ぎます。\nまとめ # この記事では、Langfuseのmaskオプションを活用し、PII（個人特定情報）フィルターにLLMを用いる際のトレース記録方法とその注意点について深掘りしました。LLM呼び出しにかかるコストを正確に把握するためには、PIIフィルターの実行を含む一連の処理を同一トレース内で記録することが重要です。\nLangfuseのmaskオプションは、inputやoutputデータがトレースに保存される際に自動的にマスキング関数を適用できる便利な機能です。しかし、オプションで指定した関数内部で再度Langfuseのトレース保存処理を行うと無限ループに陥る可能性があるため、実際に利用する際は適切かつ堅牢な処理となるよう注意が必要です。\nまた、metadataに個人情報を保存する際の注意点、PIIフィルターLLMのinputには生データを設定しないことの重要性についても解説しました。\nこれらの注意点を踏まえることで、PIIを適切に処理しつつ、Langfuseの強力なトレース機能を最大限に活用し、LLMアプリケーションの運用コストと安全性を両立させることができます。実際の運用では、本記事で紹介した内容を参考に、トレース設計を検討することをお勧めします。\n","date":"2025年7月16日","externalUrl":null,"permalink":"/posts/2025-07-16-langfuse%E3%81%A8llm%E3%82%92%E6%B4%BB%E7%94%A8%E3%81%97%E3%81%9Fpii%E3%83%9E%E3%82%B9%E3%82%AD%E3%83%B3%E3%82%B0-%E3%83%88%E3%83%AC%E3%83%BC%E3%82%B9%E4%BF%9D%E5%AD%98%E3%81%AE%E8%A9%B3%E7%B4%B0%E3%81%A8%E5%AE%9F%E8%B7%B5%E7%9A%84%E6%B3%A8%E6%84%8F%E7%82%B9/","section":"Posts","summary":"PIIフィルター用LLMのトレース保存 # こちらの記事 で、Langfuse の PII フィルターに LLM を利用する方法について検証しました。\n","title":"LangfuseとLLMを活用したPIIマスキング：トレース保存の詳細と実践的注意点","type":"posts"},{"content":" LangfuseにおけるPIIマスキング手法の検討 # 前回の記事 では、Guardrails for Amazon Bedrockを利用したPIIマスキングについて紹介しました。個人情報を列挙した形のテストデータにおいては精度が高く、大半の情報は除去できましたが、一部、フィルターが準備されていないなど、追加の対応が必要な項目が残っていました。\nまた、この機能はAmazon Bedrockに依存するため、AWS以外のクラウドを利用している方にとっては導入のハードルがやや高くなると考えます。\nそこで本記事では、PIIマスキングをLLM（大規模言語モデル）に任せるアプローチを検証します。\nLLMを活用する利点は、プロンプトの調整によって非エンジニアでも容易に設定変更できること、そして文脈を考慮した判断が可能になる点です。（例えば、芸能人の名前や公開されている会社の住所は公開情報としてマスキングしないなど）\nさらに、PIIフィルター用のプロンプトもあわせてLangfuseで管理することで、コードのデプロイを都度行う必要や、クラウドのコンソール画面を触ることなく、非エンジニアでもプロンプトの調整を容易に行えるようになります。\n今回のLLMモデルには、Gemini 2.5 Flash-Lite を採用します。\nGemini 2.5 Flash Liteとは？ # Googleが提供する最新の基盤モデルファミリーであり、低レイテンシのユースケース向けに最適化された、最もバランスの取れたGeminiモデルです。\nただし、現在パブリックプレビュー中のため、利用できる場面は限られています。\nなぜGemini 2.5 Flash Liteなのか # 処理速度が高速で、料金も入力トークン100万あたり、$0.1、出力は$0.4と低コストで提供されています。料金は、Gemini 2.5 Flash と比較すると、おおよそ3分の1以下の費用に抑えられています。\nPIIフィルターは呼び出し頻度が高くなるため、高速かつ低コストで利用できる点を高く評価し、今回採用しました。\nPIIマスキングのテスト方法 # 前回、前々回と同様のアプリケーションコードと、テストデータを用いて検証を行います。\nアプリケーションコード # 前回同様、masking_function の内部を調整します。\nclient = genai.Client(api_key=\u0026#34;GOOGLE_API_KEY\u0026#34;) def masking_function(data: any, **kwargs) -\u0026gt; any: if isinstance(data, str): # マスキング処理 response = client.models.generate_content( model=\u0026#39;models/gemini-2.5-flash-lite-preview-06-17\u0026#39;, contents=data ) sanitized_data = response.text return sanitized_data else: return data テストデータ # 前回、前々回でも利用した以下のダミーデータを、今回も同様に利用します。\nダミーデータ\n私の個人情報は以下の通りです：\n氏名：山田 太郎 Name: Taro Yamada\n生年月日：1985年3月15日 Birthday: March 15, 1985\n住所：東京都新宿区西新宿2-8-1 Address: 2-8-1 Nishi-Shinjuku, Shinjuku-ku, Tokyo, Japan\n電話番号：03-1234-5678 / 090-9999-8888 Phone: +81-3-1234-5678 / +81-90-9999-8888\nメールアドレス：taro.yamada@example.com Email:taro.yamada@example.com 運転免許証番号：123456789012 Driver\u0026rsquo;s License: DL-123456789012\nパスポート番号：TK1234567 Passport Number: TK1234567\n社会保障番号：987-65-4321 Social Security Number: 987-65-4321\nクレジットカード情報： - カード番号：4111-2222-3333-4444 - 有効期限：12/25 - セキュリティコード：123\nCredit Card Information: - Card Number: 4111-2222-3333-4444 - Expiry Date: 12/25 - Security Code: 123\n銀行口座情報： - 銀行名：みずほ銀行 - 支店名：新宿支店 - 口座番号：1234567\nBank Account Information: - Bank Name: Mizuho Bank - Branch: Shinjuku Branch - Accou****nt Number: 1234567\n検証結果１ # 上記の準備を行った上で、アプリケーションを実行します。\nトレースを確認したところ、以下の結果が登録されていました。\nご提示いただいた個人情報につきまして、私はAIアシスタントであり、あなたの個人情報を直接保存・管理することはできません。\nいただいた情報は、ご自身の確認のために役立つかもしれませんが、これらの情報は非常に機密性の高いものです。これらの情報を安易に他者に共有することは、悪用されるリスクを高めます。\n特に、クレジットカード情報や銀行口座情報は、絶対に第三者に教えないでください。\nもし、これらの情報を安全に管理したいのであれば、パスワードで保護された安全な場所（例えば、パスワードマネージャーなど）に保存することをお勧めします。\nもし、私にこれらの情報について何か特別な操作（例えば、どこかに登録するなど）を求めていらっしゃるのであれば、その意図を具体的に教えていただけますでしょうか？ただし、どのような場合でも、私のシステムに直接個人情報を入力したり保存したりすることはできませんので、その点ご了承ください。\nダミーデータをそのままLLMに投入したため、データに対する具体的な指示や解凍形式が適切に含まれていませんでした。\n比較を行う前に、この点を改善します。\nLLMフィルタリング用のプロンプト設計 # LLMから意図する回答を得るためには、プロンプトの改善が必要です。アプリケーションコードの修正を都度行うのは煩わしいため、PIIフィルター用のプロンプトも Langfuse で管理することにしました。\nアプリケーションコード（プロンプト管理版） # Langfuse で管理しているプロンプトを利用する形になるよう調整します。\n# Langfuse で管理しているプロンプトの取得 pii_prompt_template = langfuse.get_prompt(\u0026#34;pii_filter_prompt\u0026#34;, label=langfuse_config.prompt_label) client = genai.Client(api_key=\u0026#34;GOOGLE_API_KEY\u0026#34;) def masking_function(data: any, **kwargs) -\u0026gt; any: if isinstance(data, str): prompt_content = pii_prompt_template.compile(userMessage=data) # マスキング処理 response = client.models.generate_content( # モデル情報もプロンプトの config から取得 model=pii_prompt_template.config.get(\u0026#34;model\u0026#34;, \u0026#34;models/gemini-2.5-flash-lite-preview-06-17\u0026#34;), contents=prompt_content ) sanitized_data = response.text return sanitized_data else: return data プロンプト設定 # 先のコード修正により、 pii_filter_prompt という名称のプロンプトが利用可能になりました。Langfuse 側で該当の名前のプロンプトを作成し、調整を行います。\n今回の検証では、以下の点を満たす PII フィルター用のプロンプトを設定します。\n過去2回で対象とした項目を網羅できること どのようなデータがマスクされたかわかること 入力値以外の応答文等が入らないこと 3点目については、簡易的なプロンプトで個人情報がマスクされるかを試したところ、下記のように入力値にない応答文が含まれる結果となりました。\nこれでは、入力値からPII情報のみを抽出・マスキングするという本来の目的を達成できないため、この点をプロンプトの条件に追加しています。\n承知いたしました。以下のように個人情報をマスクしました。\n私の個人情報は以下の通りです：\n氏名：**** ****\nName: **** **** （後略）\nLangfuse 上でプロンプトの調整を行いつつ、最終的に、今回の検証では以下のプロンプトをPIIフィルター用として設定しました。\n以下のテキストから、個人情報をマスクしてください。 # 条件 - マスク処理以外は行わない - テキストの前後に余分なテキストを追加しない - どのようなデータがマスクされたかわかるようにする（例：山田太郎 -\u0026gt; [氏名1]、000-0000-0000 -\u0026gt; [電話番号1]） - 同じ個人情報種別でも、異なる情報の場合は別物であるとわかるようにする（例：山田さんと佐藤さん -\u0026gt; [氏名1]さんと[氏名2]さん） # 置換対象の個人情報 - 氏名 - 生年月日 - 住所 - 電話番号 - メールアドレス - 運転免許証番号 - パスポート番号 - 社会保障番号 - クレジットカード情報(カード番号, 有効期限, セキュリティコード) - 銀行口座情報(銀行名, 支店名, 口座番号) # 対象のテキスト {{userMessage}} こちらの状態で、PIIフィルターの検証を再度行います。\n検証結果２ # 前回の Guardrails for Amazon Bedrock、前々回のllm-guard を利用した際の結果と比較していきます。\nLLM利用\n（Gemini 2.5 Flash-Lite） llm-guard（デフォルト設定） Guardrail for Amazon Bedrock 氏名（日本語） [氏名1] [REDACTED_PERSON_1][REDACTED_PERSON_2][REDACTED_PERSON_3] {NAME} 氏名（英語） [氏名2] [REDACTED_PERSON_4] {NAME} 生年月日 [生年月日1] - - 住所 [住所1] - {ADDRESS} 電話番号（日本） [電話番号1] / [電話番号2] 03-1234-[REDACTED_PHONE_NUMBER_1] {PHONE} 電話番号（海外） [電話番号3] / [電話番号4] [REDACTED_PHONE_NUMBER_2] {PHONE} メールアドレス [メールアドレス1] [REDACTED_EMAIL_ADDRESS_1] {EMAIL} 運転免許証番号 [運転免許証番号1] - {DRIVER_ID} パスポート番号 [パスポート番号1] - {US_PASSPORT_NUMBER} 社会保障番号 [社会保障番号1] [REDACTED_US_SSN_RE_1] 987-65-4321 {US_SOCIAL_SECURITY_NUMBER} クレジットカード（カード番号） [クレジットカード番号1] [REDACTED_CREDIT_CARD_RE_1] {CREDIT_DEBIT_CARD_NUMBER} クレジットカード（カード番号以外） 有効期限：[クレジットカード有効期限1]\nセキュリティコード：[クレジットカードセキュリティコード1] - 有効期限：{CREDIT_DEBIT_CARD_EXPIRY}\nセキュリティコード：{CREDIT_DEBIT_CARD_CVV} 銀行口座情報 銀行名：[銀行名1]\n支店名：[銀行支店名1]\n口座番号：[銀行口座番号1]\nBank Name: [銀行名2]\nBranch: [銀行支店名2]\nAccount Number: [銀行口座番号2] - 銀行名：みずほ銀行\n支店名：新宿支店\n口座番号：{US_BANK_ACCOUNT_NUMBER}\nBank Name: Mizuho Bank\nBranch: {ADDRESS} Branch\nAccount Number: {US_BANK_ACCOUNT_NUMBER} llm-guard、Guardrails for Amazon Bedrock との比較において、いずれかと差異のある項目のみ背景色を変更しています。\n前回確認した、Guardrails においては、フィルターが対応している項目においてはおおむね問題なく検知できているという結果でしたが、今回の LLM を利用したフィルターについては、過去2回で検証した結果と比較して、検出漏れ・誤検出が少ない結果となりました。\n今回は LLM を利用するケースが優位に見える結果になりましたが、検証に利用しているダミーデータは自然な文脈が存在せず、個人情報が明示的に記載されているため、自然な文脈で利用した場合にはまた違う結果となる可能性があります。\nまた、今回提示しているアプリケーションコードでは、PIIフィルター処理そのものに対するトレース保存は行っていませんが、この処理に対してもトレースを保存することで、より詳細なコスト感を把握できるようになります。\nPIIフィルターのプロンプトに関しては、Langfuseの評価機能を活用することで、さらなる制度の工場が期待できます。\nまとめ # 本記事では、LLM（Gemini 2.5 Flash-Lite）を利用したPIIマスキングについて検証しました。Langfuseのトレース情報から個人情報を確実に除去するという目的のもと、PIIの除去については、概ね問題なく行える見込みです。\nただし、今回の検証は限定的な条件下での確認であるため、実際にアプリケーションで利用する際には、さらなる詳細な検証とプロンプトの改善が必要不可欠です。また、前回の記事でも触れましたが、LLMを利用するため、本実装のままではコスト面などから実際のアプリケーションで利用するには最適ではない可能性もあります。プロンプトや実装のアプローチについて、多角的に比較・検討することをお勧めします。\n","date":"2025年7月16日","externalUrl":null,"permalink":"/posts/2025-07-16-langfuse%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8B%E5%80%8B%E4%BA%BA%E6%83%85%E5%A0%B1-pii-%E3%83%9E%E3%82%B9%E3%82%AD%E3%83%B3%E3%82%B0-gemini-2-5-flash-lite%E3%81%A7%E5%AE%9F%E7%8F%BE%E3%81%99%E3%82%8Bllm%E3%83%99%E3%83%BC%E3%82%B9pii%E3%83%95%E3%82%A3%E3%83%AB%E3%82%BF%E3%83%BC/","section":"Posts","summary":"LangfuseにおけるPIIマスキング手法の検討 # 前回の記事 では、Guardrails for Amazon Bedrockを利用したPIIマスキングについて紹介しました。個人情報を列挙した形のテストデータにおいては精度が高く、大半の情報は除去できましたが、一部、フィルターが準備されていないなど、追加の対応が必要な項目が残っていました。\n","title":"Langfuseにおける個人情報（PII）マスキング：Gemini 2.5 Flash Liteで実現するLLMベースPIIフィルター","type":"posts"},{"content":"","date":"2025年7月16日","externalUrl":null,"permalink":"/tags/llm/","section":"タグ","summary":"","title":"LLM","type":"tags"},{"content":"","date":"2025年7月8日","externalUrl":null,"permalink":"/tags/amazon-bedrock/","section":"タグ","summary":"","title":"Amazon Bedrock","type":"tags"},{"content":"","date":"2025年7月8日","externalUrl":null,"permalink":"/tags/aws/","section":"タグ","summary":"","title":"AWS","type":"tags"},{"content":" LangfuseにおけるPIIマスキング手法の検討 # 前回の記事 では、llm-guardを用いたPIIマスキングについて検証しました。llm-guardは柔軟なモデル選択が可能である一方、日本語のPII検出にはまだ課題が残ることが分かりました。\nそこで、今回はその代替手段として、Guardrails for Amazon Bedrockの機密情報フィルター（Maskモード）の利用を検討します。\nGuardrails for Amazon Bedrock とは？ # Guardrails for Amazon Bedrock は、Amazon Bedrock 上に構築される生成 AI アプリケーションに対して、以下の主要なガードレール（保護機能）を提供します。これらのガードレールは、内部的にAmazon Bedrockの基盤モデル（FM）の推論能力を活用してコンテンツの評価やフィルタリングを行っています。ユーザーがガードレール内で直接LLMモデルを指定することはできませんが、高度なAIを活用したコンテンツモデレーションが実現されています。\nコンテンツフィルター: 憎悪、侮辱、性的、暴力、不法行為、プロンプト攻撃などのカテゴリに対してフィルタリング強度を調整できま 禁止トピック: 特定の話題に関するプロンプトや応答をブロックできます ワードフィルター: 指定した単語が含まれる入出力をブロックまたはマスキングできます 機密情報フィルター (Sensitive Information Filters): 氏名、メールアドレス、電話番号、クレジットカード番号などの PII を含む入出力を検出・ブロック・マスキングする機能です 特に「機密情報フィルター」は、事前に定義された PII タイプ（31種類以上）に対応しており、正規表現（RegEx）を使用してカスタムの機密情報を定義することも可能です。\nまた、PII の処理方法として、以下の2つのモードを選択できます。\nBLOCK: 機密情報が検出された場合、コンテンツ全体をブロックし、カスタムメッセージを返します MASK: コンテンツに含まれる機密情報をマスキング（編集）し、[NAME]、[EMAIL]のような識別子タグに置き換えます なぜGuardrails for Amazon Bedrockなのか？ # Guardrails for Amazon Bedrockには、基盤モデルを呼び出すことなく、ガードレールのルールのみを適用してコンテンツを評価・マスキングできる独立したAPI（bedrock_runtime.apply_guardrail）が存在します。このAPIを利用することで、LLMの推論プロセスとは分離して、Guardrailsが提供するPIIマスキング機能のみを検証・活用できます。\nこのapply_guardrailメソッドを活用し、Langfuseのトレースにおけるマスキング処理を実現します。\nPIIマスキングのテスト方法 # 今回は、以下の手順でGuardrails for Amazon Bedrockの機密情報フィルター（Maskモード）の動作を検証します。\n事前準備 # AWS上に有効なGuardrailsを作成する必要があります。今回は正規表現によるカスタム制約は検証せず、標準設定で利用可能な機密情報フィルターに焦点を当てます。\n事前定義されているPIIフィルターはこちら に記載があります。\n上記のページを参考に、コンソールまたはAPIから利用したい機密情報フィルターを設定していきます。今回は PII のマスキングを行うため、フィルターの動作は「マスク」を選択します。\n参考 前回マスキング対象として期待した項目と、llm-guardのAnonymize機能が対応しているとされる項目、Guardrails form Amazon Bedrockの機密情報フィルターで相応すると思われる項目を並べてみました。\nllm-guard と比較すると、Guardrails for Amazon Bedrock は標準で設定可能なPIIフィルターが充実していることが分かります。\nPII llm-guard Guardrail for Amazon Bedrock 氏名 人名 NAME 生年月日 - - 住所 - ADDRESS 電話番号 電話番号 PHONE メールアドレス メールアドレス EMAIL 運転免許証番号 - DRIVER_ID パスポート番号 - US_PASSPORT_NUMBER 社会保障番号 米国社会保障番号（SSN） US_SOCIAL_SECURITY_NUMBER （クレジットカード情報） カード番号 クレジットカード CREDIT_DEBIT_CARD_NUMBER （クレジットカード情報） 有効期限 - CREDIT_DEBIT_CARD_EXPIRY （クレジットカード情報） セキュリティコード - CREDIT_DEBIT_CARD_CVV （銀行口座情報）銀行名 - - （銀行口座情報）支店名 - - （銀行口座情報） 口座番号 - US_BANK_ROUTING_NUMBER Guardrailsを利用すれば、生年月日と銀行口座情報の銀行名、支店名を除き期待される項目は網羅できそうです。\n今回の検証では、誤検知が起こらないかを確認する意味合いも含め、上記に含まれない項目を含めた31種をフィルターとして設定しました。\nアプリケーションコード # 前回同様、簡単なコードを用いて、プロンプトの入力とLLMの回答を模擬的に生成し、それぞれをLangfuseに登録する形式でテストを行いました。\n前回のコードに対し、マスキング処理部分と初期化部分に手を加えます。今回はGuardrails for Amazon Bedrockのapply_guardrailメソッドを利用するように変更しています。\nbedrockRuntime = boto3.client( \u0026#39;bedrock-runtime\u0026#39;, region_name=\u0026#34;*********\u0026#34;, aws_access_key_id=\u0026#34;**********************\u0026#34;, aws_secret_access_key=\u0026#34;**********************\u0026#34; ) guardrail_id = \u0026#34;[あなたのガードレールのID]\u0026#34; guardrail_version = \u0026#34;DRAFT\u0026#34; # またはバージョン番号 def masking_function(data: any, **kwargs) -\u0026gt; any if isinstance(data, str): responce = bedrockRuntime.apply_guardrail( guardrailIdentifier=guardrail_id, guardrailVersion=guardrail_version, source=\u0026#34;INPUT\u0026#34;, # 今回はシンプルにするため、常に\u0026#34;INPUT\u0026#34;として評価する content=[ { \u0026#34;text\u0026#34;: { \u0026#34;text\u0026#34;: data } } ] ) # outputsからマスキング済みテキストを取得 if \u0026#39;outputs\u0026#39; in responce and len(responce[\u0026#39;outputs\u0026#39;]) \u0026gt; 0: sanitized_data = responce[\u0026#39;outputs\u0026#39;][0].get(\u0026#39;text\u0026#39;, data) return sanitized_data else: # outputsがない場合は元のデータを返す return data return data テストデータ # 前回利用した以下のダミーデータを、今回もそのまま利用します。\n私の個人情報は以下の通りです：\n氏名：山田 太郎 Name: Taro Yamada\n生年月日：1985年3月15日 Birthday: March 15, 198\n住所：東京都新宿区西新宿2-8-1 Address: 2-8-1 Nishi-Shinjuku, Shinjuku-ku, Tokyo, Japan\n電話番号：03-1234-5678 / 090-9999-8888 Phone: +81-3-1234-5678 / +81-90-9999-8888\nメールアドレス：taro.yamada@example.com Email: taro.yamada@example.com 運転免許証番号：123456789012 Driver\u0026rsquo;s License: DL-123456789012\nパスポート番号：TK1234567 Passport Number: TK1234567\n社会保障番号：987-65-4321 Social Security Number: 987-65-4321\nクレジットカード情報：\nカード番号：4111-2222-3333-4444 有効期限：12/25 セキュリティコード：123 Credit Card Information:\nCard Number: 4111-2222-3333-4444 Expiry Date: 12/25 Security Code: 123 銀行口座情報：\n銀行名：みずほ銀行 支店名：新宿支店 口座番号：1234567 Bank Account Information:\nBank Name: Mizuho Bank Branch: Shinjuku Branch Account Number: 1234567 検証結果 # 以下に、llm-guard（デフォルト設定）と Guardrails for Amazon Bedrock（apply_guardrail）それぞれで処理した際のPII項目ごとの置換結果を比較します。\nllm-guard（デフォルト設定） Guardrail for Amazon Bedrock 氏名（日本語） [REDACTED_PERSON_1][REDACTED_PERSON_2][REDACTED_PERSON_3] {NAME} 氏名（英語） [REDACTED_PERSON_4] {NAME} 生年月日 - - 住所 - {ADDRESS} 電話番号（日本） 03-1234-[REDACTED_PHONE_NUMBER_1] {PHONE} 電話番号（海外） [REDACTED_PHONE_NUMBER_2] {PHONE} メールアドレス [REDACTED_EMAIL_ADDRESS_1] {EMAIL} 運転免許証番号 - {DRIVER_ID} パスポート番号 - {US_PASSPORT_NUMBER} 社会保障番号 [REDACTED_US_SSN_RE_1] 987-65-4321\n{US_SOCIAL_SECURITY_NUMBER} クレジットカード（カード番号） [REDACTED_CREDIT_CARD_RE_1] {CREDIT_DEBIT_CARD_NUMBER} クレジットカード（カード番号以外） - 有効期限：{CREDIT_DEBIT_CARD_EXPIRY}\nセキュリティコード：{CREDIT_DEBIT_CARD_CVV} 銀行口座情報 - 銀行名：みずほ銀行\n支店名：新宿支店\n口座番号：{US_BANK_ACCOUNT_NUMBER}\nBank Name: Mizuho Bank\nBranch: {ADDRESS} Branch\nAccount Number: {US_BANK_ACCOUNT_NUMBER} llm-guardのマスキング結果は、前回記事のデフォルト設定時のものです。\n一部誤検出や期待通りに検出されていないものもありますが、概ね期待通りの結果が得られました。\n結果詳細（誤検出・検出漏れ項目の抜粋） # 社会保障番号：987-65-4321 Social Security Number: {US_SOCIAL_SECURITY_NUMBER}\n英語版は適切にマスキングされましたが、日本語版においては英語版と同様のフォーマットを渡していますが、マスキングされませんでした。\n本項目については、米国向けのフィルターとして準備されているため、日本語の文脈では対応できない可能性があります。\n銀行口座情報：\n銀行名：みずほ銀行 支店名：新宿支店 口座番号：{US_BANK_ACCOUNT_NUMBER} Bank Account Information:\nBank Name: Mizuho Bank Branch: {ADDRESS} Branch Account Number: {US_BANK_ACCOUNT_NUMBER}\n英語版の支店名が住所として誤検出されていました。\n今回は文脈を含まない情報でテストしましたが、実際の自然な文脈中で銀行支店名が出現した場合の挙動は別途検証が必要です。\n今回の検証では、口座番号は正常に検出されていますが、利用したフィルターは米国の銀行口座番号として準備されています。実際に利用する際は、日本の形式に適合するか、もう少し検証してみる必要はありそうです。\nまとめ # 今回はGuardrails for Amazon BedrockのPIIマスキング機能について検証しました。\nテストの結果、概ね Guardrails for Amazon Bedrock で PII の除去が出来ました。今回の検証では、限定的な条件での確認となるため、実際にアプリケーションで利用する際は、自然なテキスト中に含まれている場合や、全角文字が混在する場合など、日本語特有の表現が含まれている場合の挙動については、さらに詳細な検証が必要となります。\n一方、現状ではカバーしきれていない情報もいくつかは残っています。\nしかし、フィルターが準備されていない生年月日や、検知が上手くいかなかった社会保障番号（類似する情報であるマイナンバー）においては、ある程度フォーマットが定まっている情報になります。これらに対しては、正規表現フィルターを併用することで、PII除去の確実性を高められるでしょう。\n検討課題 # 今回の検証では Langfuse の mask オプションを使用して実装しましたが、運用を考慮すると、今回の実装が適切とは限りません。\nmask オプションは Langfuse にデータが渡されるたびに実行されるため、APIコール数が想定以上に増加し、コストに影響する可能性があります。実際、llm-guard のようなツールとは異なり、今回利用した機密情報フィルターについては、Bedrockの料金ページ に記載の通り、1,000テキストユニットあたり0.10 USD発生します。（2025年7月時点）\nまた、mask オプションを利用することで、エラーハンドリングが複雑になってしまう側面もあります。API エラーが発生した場合に Langfuse のトレース処理全体に影響を及ぼす懸念も無視できません。\nそのため、Langfuse のトレース保存前に、アプリケーションコード内で明示的に apply_guardrail を呼び出し、その結果を input や output として Langfuse に渡す方法など、様々な実装アプローチを比較・検討することをおすすめします。\n","date":"2025年7月8日","externalUrl":null,"permalink":"/posts/2025-07-08-langfuse%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8B%E5%80%8B%E4%BA%BA%E6%83%85%E5%A0%B1-pii-%E3%83%9E%E3%82%B9%E3%82%AD%E3%83%B3%E3%82%B0-guardrails-for-amazon-bedrock%E3%81%AE%E6%B4%BB%E7%94%A8/","section":"Posts","summary":"LangfuseにおけるPIIマスキング手法の検討 # 前回の記事 では、llm-guardを用いたPIIマスキングについて検証しました。llm-guardは柔軟なモデル選択が可能である一方、日本語のPII検出にはまだ課題が残ることが分かりました。\n","title":"Langfuseにおける個人情報（PII）マスキング：Guardrails for Amazon Bedrockの活用","type":"posts"},{"content":" n8nとは何か # n8nは「nodemation」の略称で、ドラッグ\u0026amp;ドロップ操作や各ノードの設定によってワークフローを作成できる自動化ツールです。300以上の組み込みノードを提供しており、Slack、Gmail、Notion、カレンダー、Webhookなど、様々なサービスとの連携が可能です。コードを書くことなく複雑な自動化フローを構築できる一方で、JavaScript や最近はPython を使用したコードの実行をフローに埋め込むことなどにも対応しています。\nDify との違いは何？ # 両方とも基本的にOSSベースのノーコードツールと位置付けてよいと思いますが、Difyは「生成AIアプリを作る」ツール、n8nは「生成AIを含むシステムを繋いで業務を処理する」ツールという違いがあると思います。特に実際のオフィスワークにおいては、生成AIをはじめとして JIRA やCRM, mail, Slack などと連携することが求められるケースも多く、n8n はそのようなケースで非常に有用であると考えられます。またコードを書くこともできるので、痒いところに手が届くところも人気の理由です。\nちなみに n8n は Dify よりも歴史が長く、その分で安定しているという意見もある一方で、現在利用者や人気という点ではほぼ互角に見えます (ただし日本ではローカライズが早かった Dify の方が採用が多いように思われます)\nGithub starsの推移、n8nの歴史は長いが スター数は近い n8n と Langfuse 連携の価値 # そんな n8n と Langfuse を連携させる理由の一つは、もちろんTraceを取得することによる処理の可視化です。レイテンシーやコスト、システムの中でどのような処理が行われているかを可視化することは運用上で大きな意味を持ちます。またLLM as a judge などの評価にも利用することも可能となります。\nもう一つの価値はプロンプト管理です。n8n の生成AIアプリケーションのノードの中にプロンプトをハードコードしても使い回しや世代管理などをすることができず、管理も煩雑になり、生産的ではありません。以降では、実際の設定方法について記載していきます。\nn8n サンプル構成概要 # 今回の説明には、サンプルとして下図のシンプルなフローを用います。\n[構成環境]\nVersion 1.99.1 OSS\nSelf-hosted on CloudRun + CloudSQL (PostgreSQL),\n[サンプルフロー]\nn8n フロー チャットでの入力を受け、このフローは以下の動作をします。\nCode ノードがプロンプトを Langfuse から取得 そのプロンプトを AI Agentノードの System prompt に入れ、チャットに入力された内容が user prompt として入れられる その情報が Langchain ノード (Langfuse LLM) に入り、そこから今回はVertexAIのModelを呼び出す (これは OpenAI でも Bedrock でも何でも同じです) ひとつずつコードと設定を見てみましょう。\nCodeノード コードサンプル const { Langfuse } = require(\u0026#34;langfuse\u0026#34;); const langfuseClient = new Langfuse(); return langfuseClient.getPrompt(\u0026#34;wording\u0026#34;).then(prompt =\u0026gt; { const variables = $input.item.json.variables || {}; const systemPrompt = prompt.compile(variables); return { json: { systemPrompt: systemPrompt, userMessage: $input.item.json.chatInput, langfusePrompt: prompt, promptVersion: prompt.version, promptName: \u0026#34;wording\u0026#34;, sessionId: $input.item.json.sessionId } }; }); まず langfuse を読み込み、その後に wording という 名前のプロンプトを langfuseから取って、それを SystemPrompt に渡しているだけです。その後、各変数をセットしています。ちなみに Langfuse の host などは環境変数に入れておくなどする必要があり、langfuseパッケージもコンテナの中にモジュールをインストールしておく必要がありますのでご注意ください。また今回は特に使っておりませんが、prompt.compile に変数を渡してあげることで、 Langfuse の中のプロンプトに変数を渡してあげることもできます。\nAI Agentノード このノードにはコードは書く必要はありません。Prompt の中に User messageを定義し、System Message として、1. Codeノードで取得した内容を反映させておきます。\n前のノードを実行するとこの画像の左側ペインに変数がでるので、それをドラッグ\u0026amp;ドロップして設定するのが簡単 Langchain ノード コードサンプル const { CallbackHandler } = require(\u0026#34;langfuse-langchain\u0026#34;); const model = await this.getInputConnectionData(\u0026#34;ai_languageModel\u0026#34;, 0); const inputData = $input.item.json; const langfuseHandler = new CallbackHandler({ sessionId: inputData.sessionId || `session_${Date.now()}`, userId: inputData.userId || \u0026#34;anonymous\u0026#34;, tags: [\u0026#34;n8n\u0026#34;, \u0026#34;ai-agent\u0026#34;, \u0026#34;production\u0026#34;] }); model.callbacks = model.callbacks || []; model.callbacks.push(langfuseHandler); return model; まず langfuse-langchain パッケージから CallbackHandler クラスをインポートし、次にLLMモデルを取得します。この例ではGoogle Vertex Chat model かモデル名と 0 (最初の接続) を指定してとってきます。とってきたjson からデータを展開し、sessionIDやUserIDを入れております。ついでに n8n などのタグも設定しています。\nそしてmodelに callback を追加して、model オブジェクトを AI Agent ノードに渡します。\n以上で、やるべきことは終わりです。\nLangfuse のTraceを見てみると以下のように記録されているはずです。Langchainを使ってるので、勝手にTokenなども取られています。\nプロンプトには \u0026ldquo;言葉遣いは荒々しい野武士のようにしてください\u0026rdquo; と指定しており、それをそのままSystem promptとして指定してある。 いかがでしたでしょうか。n8n は簡単に使えるツールですが、生成AIの利用もサポートされており、Langfuseとの組み合わせも実現可能です。ご興味あるかたは、ぜひ試してみてください。\n","date":"2025年6月27日","externalUrl":null,"permalink":"/posts/2025-06-27-%E8%87%AA%E5%8B%95%E5%8C%96%E3%83%84%E3%83%BC%E3%83%AB-n8n-%E3%81%A8-langfuse-%E3%81%AE%E9%80%A3%E6%90%BA/","section":"Posts","summary":"n8nとは何か # n8nは「nodemation」の略称で、ドラッグ\u0026ドロップ操作や各ノードの設定によってワークフローを作成できる自動化ツールです。300以上の組み込みノードを提供しており、Slack、Gmail、Notion、カレンダー、Webhookなど、様々なサービスとの連携が可能です。コードを書くことなく複雑な自動化フローを構築できる一方で、JavaScript や最近はPython を使用したコードの実行をフローに埋め込むことなどにも対応しています。\n","title":"自動化ツール n8n と Langfuse の連携","type":"posts"},{"content":" LangfuseにおけるPIIマスキングの必要性 # チャットボットのようなアプリケーションでは、ユーザーが意図せず個人情報（PII）を入力してしまう可能性があります。個人情報保護の観点から、これらの情報がLangfuseのトレースにそのまま出力されるのは望ましくありません。\nそこで、トレース上で個人情報をマスキングした状態で確認できるよう、どのような手段が考えられるか検証しました。\n個人情報（PII）の定義と具体例 # 具体的に個人情報（PII） に該当する項目は、一般的に以下のものが挙げられます。\n氏名 住所 電話番号 メールアドレス 顔写真 身分証番号（運転免許証、パスポートなど） 生年月日 社会保障番号 クレジットカード情報 銀行口座情報 今回はテキスト入力ベースのアプリケーションを想定しているため、顔写真のような画像データは検証対象外とします。\nLangfuseにおけるPIIマスキング手法の検討 # Langfuseの公式サイトで紹介されている Masking of Sensitive LLM Data を参考に、マスキング手法を検討します。\n氏名や住所は正規表現での対応が難しいため、今回は Example 2 で紹介されている llm-guard を試してみました。\n氏名や住所のような複雑な情報は正規表現での対応が難しいため、今回はExample 2で紹介されているllm-guardを試用しました。\n今回試用したllm-guardのAnonymize機能は、現在以下の個人情報（PII）の検出に対応しています。\nクレジットカード 人名 電話番号 URL メールアドレス IPアドレス UUID 米国社会保障番号（SSN） 暗号資産ウォレット番号 IBANコード 今回マスキング対象として期待している項目は以下の通りです。\n氏名 生年月日 住所 電話番号 メールアドレス 運転免許証番号 パスポート番号 社会保障番号 クレジットカード情報 カード番号 有効期限 セキュリティコード 銀行口座情報 銀行名 支店名 口座番号 PIIマスキングのテスト方法 # 簡単なコードを用いて、プロンプトの入力とLLMの回答を模擬的に生成し、それぞれをLangfuseに登録する形式でテストを行いました。\nアプリケーションコード # 今回の検証では、PythonアプリケーションからLangfuseにトレースを投入するケースを対象としています。マスキング処理は、Langfuseの初期化時にmaskオプションに対して、処理を行う関数を設定することで実現できます。\nvault = Vault() def create_anonymize_scanner(): scanner = Anonymize( vault=vault, ) return scanner def masking_function(data: any, **kwargs) -\u0026gt; any: if isinstance(data, str): scanner = create_anonymize_scanner() sanitized_data, is_valid, score = scanner.scan(data) return sanitized_data return data # テスト用の入力テキスト input_text = \u0026#34;\u0026#34;\u0026#34;[[ダミーデータを含むプロンプト]]\u0026#34;\u0026#34;\u0026#34; # Langfuseの初期化 langfuse = Langfuse( public_key=\u0026#34;xxxxxxxxxxxxxxxxxxxxxxxx\u0026#34;, secret_key=\u0026#34;xxxxxxxxxxxxxxxxxxxxxxxx\u0026#34;, host=\u0026#34;http://xxxxxxxxxxxxxxxx\u0026#34;, mask=masking_function, ) # LLM呼び出しのトレース with langfuse.start_as_current_generation( name=f\u0026#34;test_step\u0026#34;, input=input_text, ) as generation: # LLMからの実際の出力を格納 response_content = input_text # 現在はダミーデータを使用 generation.update(output=response_content) テストデータ # テスト用のプロンプトとLLMの出力結果として、Cursorを用いて上記の個人情報（PII）具体例を含むダミーデータを以下の通り生成しました。インプットとアウトプットの双方に個人情報が含まれるとトレース上での問題となるため、今回は同じテストデータを利用し、両方のデータにマスキングが適用されるかを確認します。\n私の個人情報は以下の通りです：\n氏名：山田 太郎 Name: Taro Yamada\n生年月日：1985年3月15日 Birthday: March 15, 1985\n住所：東京都新宿区西新宿2-8-1 Address: 2-8-1 Nishi-Shinjuku, Shinjuku-ku, Tokyo, Japan\n電話番号：03-1234-5678 / 090-9999-8888 Phone: +81-3-1234-5678 / +81-90-9999-8888\nメールアドレス：taro.yamada@example.com Email: taro.yamada@example.com 運転免許証番号：123456789012 Driver\u0026rsquo;s License: DL-123456789012\nパスポート番号：TK1234567 Passport Number: TK1234567\n社会保障番号：987-65-4321 Social Security Number: 987-65-4321\nクレジットカード情報：\nカード番号：4111-2222-3333-4444 有効期限：12/25 セキュリティコード：123 Credit Card Information:\nCard Number: 4111-2222-3333-4444 Expiry Date: 12/25 Security Code: 123 銀行口座情報：\n銀行名：みずほ銀行 支店名：新宿支店 口座番号：1234567 Bank Account Information:\nBank Name: Mizuho Bank Branch: Shinjuku Branch Account Number: 1234567 最低限の引数での動作確認 # 上記のコードを実行したところ、何らかの置換が行われていることが確認できました。\n検証結果：インプットとアウトプットの比較 # インプットの結果をpt1_input.txt、アウトプットの結果をpt1_output.txt として比較しました。\ninputとoutputのいずれに渡した場合でも、マスキング処理が適応されていることが確認できました。\n検証結果：各個人情報の検出状況 # プロンプトとして与えた各個人情報について、どのように処理されたかひとつずつ確認していきます。\n氏名：△ # 氏名：[REDACTED_PERSON_1][REDACTED_PERSON_2][REDACTED_PERSON_3] Name: [REDACTED_PERSON_4]\n検知・置換自体は行われていますが、日本語名が意図せず三分割されている点が課題です。\n生年月日：× # 住所：× # これらはllm-guardの現在の検出対象外のため、想定通りの結果です。\n電話番号：× # 電話番号：03-1234-[REDACTED_PHONE_NUMBER_1] Phone: [REDACTED_PHONE_NUMBER_2]\n2種類の電話番号を「/」区切りで記載していましたが、一つの電話番号とみなされているようです。いずれにしても、日本の電話番号形式への対応はまだ課題があります。\nメールアドレス：○ # 運転免許証番号：× # パスポート番号：× # 社会保障番号：○ # これらの項目については、当初の想定通りの検出結果となりました（運転免許証番号、パスポート番号は検出対象外のため×、メールアドレス、社会保障番号は検出対象のため○）。\nクレジットカード情報：△ # クレジットカード情報： カード番号：[REDACTED_CREDIT_CARD_RE_1] 有効期限：12/25 セキュリティコード：123\nカード番号は問題なく検出できましたが、有効期限やセキュリティコードはマスキングされませんでした。ドキュメント ではVisa、American Express、Diners Clubに対応とあるため、他のカード会社への対応状況も確認が必要です。\n銀行口座情報：× # こちらもllm-guardの現在の検出対象外のため、想定通りの結果です。\n各種設定や検出モデルの変更による検証 # オプションの設定や検出に利用するモデルを変更することにより、対応している項目に関しては精度が向上する可能性があると考え、合わせて確認してみました。\n日本語設定の試行 # Anonymizeの言語設定は標準では英語となっています。こちらを日本語に変更可能か試行しました。\nしかし、llm-guardが現在サポートしているのは英語（\u0026rsquo;en\u0026rsquo;）と中国語（\u0026lsquo;zh\u0026rsquo;）のみであることが確認されました。このため、日本語のPII検出において、言語設定によるアプローチは現状では利用できません。\nrecognizer_confの変更による比較検証 # Anonymizeでは、recognizer_confパラメータで検出モデルを指定できます。コードを以下のように変更し、llm_guard.input_scanners.anonymize_helpers で定義されているモデルを順に試行します。\nscanner = Anonymize( vault=vault, recognizer_conf=[[ ここの値を変更 ]] ) 定義されているモデルは以下の7種類になります。\nBERT_BASE_NER_CONF（dslim/bert-base-NER） BERT_LARGE_NER_CONF（dslim/bert-large-NER） BERT_ZH_NER_CONF（gyr66/bert-base-chinese-finetuned-ner） DISTILBERT_AI4PRIVACY_v2_CONF（Isotonic/distilbert_finetuned_ai4privacy_v2） DEBERTA_AI4PRIVACY_v2_CONF（Isotonic/deberta-v3-base_finetuned_ai4privacy_v2） MDEBERTA_AI4PRIVACY_v2_CONF（Isotonic/mdeberta-v3-base_finetuned_ai4privacy_v2） DEBERTA_LAKSHYAKH93_CONF（lakshyakh93/deberta_finetuned_pii） 各モデルにおいて、先に試行したデフォルト設定時の差分に重点を置いて確認していきます。\nデフォルト設定時にマスキングされなかったものについては、「-」で表示しています。\n（マスキングの数値のみが異なっている場合は、同様の検出が行われたと判断しています）\n氏名（日本語） 氏名（英語） 生年月日 住所 電話番号（日本） 電話番号（海外） メールアドレス 運転免許証番号 パスポート番号 社会保障番号 クレジットカード（カード番号） クレジットカード（カード番号以外） 銀行口座情報 デフォルト設定（参考） [REDACTED_PERSON_1][REDACTED_PERSON_2][REDACTED_PERSON_3] [REDACTED_PERSON_4] - - 03-1234-[REDACTED_PHONE_NUMBER_1] [REDACTED_PHONE_NUMBER_2] [REDACTED_EMAIL_ADDRESS_1] - - [REDACTED_US_SSN_RE_1] [REDACTED_CREDIT_CARD_RE_1] - - BERT_BASE_NER_CONF 山田 太郎 [REDACTED_PERSON_1][REDACTED_PERSON_2]mada - - 03-1234-5678 / 090-9999-8888 [REDACTED_PHONE_NUMBER_1] / [REDACTED_PHONE_NUMBER_2] [REDACTED_EMAIL_ADDRESS_1] - - [REDACTED_US_SSN_RE_1] [REDACTED_CREDIT_CARD_RE_1] - - BERT_LARGE_NER_CONF 山田 太郎 [REDACTED_PERSON_3] - - 03-1234-5678 / 090-9999-8888 [REDACTED_PHONE_NUMBER_1] / [REDACTED_PHONE_NUMBER_2] [REDACTED_EMAIL_ADDRESS_1] - - [REDACTED_US_SSN_RE_1] [REDACTED_CREDIT_CARD_RE_1] - - BERT_ZH_NER_CONF [REDACTED_PERSON_4] Taro Yamada - - 03-1234-5678 / 090-9999-8888 [REDACTED_PHONE_NUMBER_1] / [REDACTED_PHONE_NUMBER_2] [REDACTED_EMAIL_ADDRESS_1] - - [REDACTED_US_SSN_RE_1] [REDACTED_CREDIT_CARD_RE_1] - - DISTILBERT_AI4PRIVACY_v2_CONF 山田 太郎 Taro Yamada - - 03[REDACTED_PHONE_NUMBER_3] [REDACTED_PHONE_NUMBER_1] / [REDACTED_PHONE_NUMBER_2] [REDACTED_EMAIL_ADDRESS_1] 運転免許証番号：[REDACTED_PHONE_NUMBER_4]\nDriver's License: DL-[REDACTED_PHONE_NUMBER_5]12 - [REDACTED_US_SSN_RE_1] [REDACTED_CREDIT_CARD_RE_1] - - DEBERTA_AI4PRIVACY_v2_CONF [REDACTED_PERSON_5][REDACTED_PERSON_6][REDACTED_PERSON_7] [REDACTED_PERSON_3] - - 03-1234-[REDACTED_PHONE_NUMBER_6] [REDACTED_PHONE_NUMBER_7] [REDACTED_EMAIL_ADDRESS_1] - - [REDACTED_US_SSN_RE_1] [REDACTED_CREDIT_CARD_RE_1] - - MDEBERTA_AI4PRIVACY_v2_CONF [REDACTED_PERSON_8] 太郎 Taro [REDACTED_PERSON_9] - - [REDACTED_PHONE_NUMBER_8] / [REDACTED_PHONE_NUMBER_9] [REDACTED_PHONE_NUMBER_1] / [REDACTED_PHONE_NUMBER_2] [REDACTED_EMAIL_ADDRESS_1] - - [REDACTED_US_SSN_RE_1] [REDACTED_CREDIT_CARD_1] - - DEBERTA_LAKSHYAKH93_CONF 山田 太郎 [REDACTED_PERSON_1][REDACTED_PERSON_2][REDACTED_PERSON_3] [REDACTED_IBAN_CODE_1] [REDACTED_CRYPTO_2] 03-1234-5678 / 090-9999-8888 [REDACTED_PHONE_NUMBER_1] / [REDACTED_PHONE_NUMBER_2] [REDACTED_EMAIL_ADDRESS_1]\nEmail: [REDACTED_EMAIL_ADDRESS_2] - [REDACTED_IBAN_CODE_2]\nPassport Number: TK1234567 社会[REDACTED_IBAN_CODE_3]\nSocial Security Number: [REDACTED_US_SSN_RE_1] [REDACTED_CREDIT_CARD_RE_1] - [REDACTED_CRYPTO_3]- [REDACTED_CRYPTO_4] [REDACTED_IP_ADDRESS_2] 各モデルの比較検証結果\nBERT_BASE_NER_CONF\n氏名：山田 太郎 Name: [REDACTED_PERSON_1][REDACTED_PERSON_2]mada\n日本語氏名はマスキングされず、英語氏名は部分的にマスキングされる形になりました。\n電話番号：03-1234-5678 / 090-9999-8888 Phone: [REDACTED_PHONE_NUMBER_1] / [REDACTED_PHONE_NUMBER_2]\n電話番号も全てマスキングされない結果です。\nただし、海外向けフォーマットにおいては、適切に2つ分として判断されているようです。\nBERT_LARGE_NER_CONF\n氏名：山田 太郎 Name: [REDACTED_PERSON_3]\n日本語氏名はマスキングされませんでしたが、英語氏名は綺麗にマスキングされました。\n電話番号：03-1234-5678 / 090-9999-8888 Phone: [REDACTED_PHONE_NUMBER_1] / [REDACTED_PHONE_NUMBER_2]\n電話番号については、BERT_BASE_NER と同じ形になりました。\nBERT_ZH_NER_CONF\n氏名：[REDACTED_PERSON_4] Name: Taro Yamada\n日本語氏名はマスキングされましたが、英語氏名はマスキングされませんでした。\n中国語のNERモデルとなるため、漢字はうまく判別出来ているのかもしれません。\n実際に採用できるかは、名前がひらがなのケースも一度確認する必要がありそうです。\n電話番号：03-1234-5678 / 090-9999-8888 Phone: [REDACTED_PHONE_NUMBER_1] / [REDACTED_PHONE_NUMBER_2]\nこちらも電話番号については、BERT_BASE_NER と同じ形になりました。\nDISTILBERT_AI4PRIVACY_v2_CONF\n氏名：山田 太郎 Name: Taro Yamada\n日本語、英語共にマスキングされていません。\n電話番号：03[REDACTED_PHONE_NUMBER_5] Phone: [REDACTED_PHONE_NUMBER_1] / [REDACTED_PHONE_NUMBER_2]\n日本国内向けの電話番号はまだ適切に検知できていないようですが、こちらも海外向けフォーマットにおいては、適切に2つ分として判断されているようです。\n運転免許証番号：[REDACTED_PHONE_NUMBER_6] Driver\u0026rsquo;s License: DL-[REDACTED_PHONE_NUMBER_7]12\n電話番号として誤検出され、部分的にマスキングされています。\n英語では末尾2文字がマスキングされておらず、日本語と英語で検出範囲が異なっているのも気になる点です。\nDEBERTA_AI4PRIVACY_v2_CONF\n置換後の数値に差異はあるものの、デフォルトで指定されているモデルのため、同じ形でマスキングされていました。\nMDEBERTA_AI4PRIVACY_v2_CONF\n氏名：[REDACTED_PERSON_8] 太郎 Name: Taro [REDACTED_PERSON_9]\n日本語氏名、英語氏名共に姓のみマスキングされています。\n電話番号：[REDACTED_PHONE_NUMBER_8] / [REDACTED_PHONE_NUMBER_9] Phone: [REDACTED_PHONE_NUMBER_1] / [REDACTED_PHONE_NUMBER_2]\n電話番号に関しては、国内・海外向けフォーマットともにマスキングされています。\nDEBERTA_LAKSHYAKH93_CONF\n他のモデルに比べ、誤検知が多く発生してます。\nまず、1行目に記載している下記の文言ですが\n私の個人情報は以下の通りです\n以下の通り誤検知・置換されていました。\n私の個人情報[REDACTED_CRYPTO_1]下の通りです：\nその他、各項目についても以下の通り誤検出が多くなっていました。\n氏名：山田 太郎 Name: [REDACTED_PERSON_1][REDACTED_PERSON_2][REDACTED_PERSON_3]\n日本語氏名がマスキングされておらず、英語氏名も3分割で検知されています。\n[REDACTED_IBAN_CODE_1]\n生年月日がIBANコードとして誤検出されています。\n[REDACTED_CRYPTO_2]\n住所が暗号通貨のウォレット番号として誤検出されています。\n電話番号：03-1234-5678 / 090-9999-8888[REDACTED_IP_ADDRESS_1] [REDACTED_PHONE_NUMBER_1] / [REDACTED_PHONE_NUMBER_2]\n日本語の番号がマスキングされていません。\n海外向けフォーマットについては適切に検出されているように見えますが、関係のない「Phone：」がIPアドレスとして誤検出されています。\n[REDACTED_EMAIL_ADDRESS_1] Email: [REDACTED_EMAIL_ADDRESS_2]\n「メールアドレス：」部分もメールアドレスとして誤検出されています。\nDriver\u0026rsquo;s License: DL-123456789012[REDACTED_IBAN_CODE_2] Passport Number: TK1234567\n「パスポート番号：TK1234567」がIBANコードとして誤検出されています。\n社会[REDACTED_IBAN_CODE_3]\n社会保障番号が文言の途中からIBANコードとして誤検出されています。\n[REDACTED_CRYPTO_3]- [REDACTED_CRYPTO_4] [REDACTED_IP_ADDRESS_2]\n日本語の銀行口座情報が暗号通貨のウォレット番号、IPアドレスとして誤検知されています。\n# まとめ # 今回の検証では、llm-guardを用いたPIIマスキングの可能性を探りました。特に日本語の個人情報検出においては、現状では他の手法との併用や、より高精度なモデルの検証が必要となりそうです。\n","date":"2025年6月24日","externalUrl":null,"permalink":"/posts/2025-06-24-langfuse%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8B%E5%80%8B%E4%BA%BA%E6%83%85%E5%A0%B1-pii-%E3%81%AE%E3%83%9E%E3%82%B9%E3%82%AD%E3%83%B3%E3%82%B0/","section":"Posts","summary":"LangfuseにおけるPIIマスキングの必要性 # チャットボットのようなアプリケーションでは、ユーザーが意図せず個人情報（PII）を入力してしまう可能性があります。個人情報保護の観点から、これらの情報がLangfuseのトレースにそのまま出力されるのは望ましくありません。\n","title":"Langfuseにおける個人情報（PII）のマスキング","type":"posts"},{"content":" はじめに # 本記事では、この Langfuse 環境を AWS 上に構築する方法について解説します\n2025/05/22 に Langfuse の公式ドキュメントにおいて、AWS 向けの Terraform 構成が公開されました。この公式ドキュメントに記載された手順をベースとし、実際に環境を構築する際の具体的なステップや留意点、さらに実運用を見据えたポイントなどを、弊社の知見を交えながらご紹介します。\n本記事の Google Cloud バージョンはこちら 公式の AWS システム構成 # Langfuse 環境を AWS 上に Terraform で構築するにあたり、最も信頼できる情報源は Langfuse の公式ドキュメントです。公式で AWS 向けの Terraform 構成が提供開始され、これにより導入のハードルが大きく下がりました。\nまず、Langfuse 公式サイトの AWS 向けセルフホスティングガイド をご確認いただくことを強く推奨します。この公式ガイドには、Terraform で環境構築する手順が載っています。\n主要コンポーネントとAWS プロダクトのマッピング # 公式ドキュメントで推奨されている構成、および Langfuse の一般的なコンポーネントが AWS のどのプロダクトに対応するかを以下にまとめます。\nLangfuse コンポーネント AWS プロダクト 主な目的・役割 Langfuse Web, Worker Amazon Elastic Kubernetes Service (以下EKS) Langfuseサーバーのコンテナをホスティングします。 Redis Amazon ElastiCache キャッシュ・キューに使用します。 Postgres - OLTP Amazon Aurora PostgreSQL 認証情報などトランザクションデータを格納するストレージです。 ClickHouse - OLAP コンピュート: EKS ストレージ: Amazon EFS トレースなどを格納するストレージです。 Blob Storage Amazon S3 生のイベントやマルチモーダルファイルを保管します。 Terraformコードの提供形態 # Langfuse の GitHub リポジトリ には、これらの AWS リソースを効率的にプロビジョニングするための Terraform 設定ファイル群（HCL コード）が提供されています。ユーザーは提供されたコードをベースに、ドメインや構成オプションを指定することで、推奨構成を迅速にデプロイできます。\n構成図 # GitHub リポジトリにある構成図は以下のとおりです。この構成は本番環境を想定した最低限のコンポーネントになっていると考えます。\nシステム構成図　引用：langfuse-terraform-aws 費用の概算 # 最低限のランニングコストを試算しました。（リンク ）\n月額約 450$ となっています。\nTerraform での環境構築で知っておきたい Tips 集 # Langfuse の公式ドキュメントには、AWS 上に Terraform を用いて環境を構築するための手順が詳細に記載されています。基本的にはこのドキュメントに従って進めることで、迷うことなく環境をセットアップできるでしょう。\n本セクションでは、実際に構築する過程で気づいた点や、よりスムーズに進めるための Tips などをまとめてご紹介します。\n構築の大まかな流れ # 公式ドキュメントで提供されている Terraform コードを利用した構築は、主に以下のステップで進められます。\nAPI を有効化します。 設定 HCL ファイルを用意します。ドメインを指定します。 terraform init をして、先に DNS 設定だけ apply します。 全てのリソースを apply します。 所要時間: 全体のデプロイには、環境にもよりますが 20 分〜 60 分程度かかります。\n構築時の Tips と留意点 💡 # 実際に公式ドキュメントの手順に沿って構築を進める中で、いくつか留意しておくと良い点や、カスタマイズのヒントがありました。\n既知の問題について # 弊社で何回か試したときは、ステップ4で以下のエラーが出て apply 失敗しました。\nterraform apply ... ╷ │ Warning: Helm release \u0026#34;\u0026#34; was created but has a failed status. Use the `helm` command to investigate the error, correct it, then run Terraform again. │ │ with module.langfuse.helm_release.langfuse, │ on ../../langfuse.tf line 121, in resource \u0026#34;helm_release\u0026#34; \u0026#34;langfuse\u0026#34;: │ 121: resource \u0026#34;helm_release\u0026#34; \u0026#34;langfuse\u0026#34; { │ ╵ ╷ │ Error: context deadline exceeded │ │ with module.langfuse.helm_release.langfuse, │ on ../../langfuse.tf line 121, in resource \u0026#34;helm_release\u0026#34; \u0026#34;langfuse\u0026#34;: │ 121: resource \u0026#34;helm_release\u0026#34; \u0026#34;langfuse\u0026#34; { │ ╵ これは公式ドキュメントで既知の問題 として対処法が示されています。以下のコマンドを実行後に再び apply すれば成功します。\n# Connect your kubectl to the EKS cluster aws eks update-kubeconfig --name langfuse # Restart the CoreDNS and ClickHouse containers kubectl --namespace kube-system rollout restart deploy coredns kubectl --namespace langfuse delete pod langfuse-clickhouse-shard0-{0,1,2} langfuse-zookeeper-{0,1,2} 設定ファイルのテンプレート活用 # 公式ドキュメントには設定ファイルの記述例がありますが、リポジトリ内の examples/quickstart/quickstart.tf に類似の構成ファイルが含まれている場合があります。こちらを参考にしたり、コピーして自身の環境に合わせて修正したりすると、設定ファイル作成の手間を省けることがあります。\nAurora と ElastiCache のコスト最適化 # Terraform のデフォルト設定では、Aurora の容量や ElastiCache のインスタンス数が多い状態でプロビジョニングされる場合があり、特に検証用途では料金が高額になる可能性があります。開発・検証環境など、そこまで高い性能や障害耐性を求めない場合は、Langfuse モジュールの設定で以下のように調整することで、コストを大幅に抑えることができます。\nmodule \u0026#34;langfuse\u0026#34; { # ... 他の変数 ... postgres_max_capacity = 1.0 cache_instance_count = 1 } アクセスキー・リージョン未設定エラーへの対処 # terraform apply 実行時などに、「アクセスキーが設定されていない」や「リージョンが設定されていない」といったエラーメッセージが表示されることがあります。これは、AWS プロバイダが対象のプロジェクトやリージョンを認識できていない場合に発生します。以下の環境変数を設定するか、Terraform のプロバイダ設定で明示的に指定してください。\nexport AWS_ACCESS_KEY_ID=\u0026#34;YOUR_ACCESS_KEY\u0026#34; export AWS_SECRET_ACCESS_KEY=\u0026#34;YOUR_SECRET_KEY\u0026#34; export AWS_REGION=\u0026#34;YOUR_REGION\u0026#34; または、プロバイダブロックに記述します。\nprovider \u0026#34;aws\u0026#34; { access_key = \u0026#34;YOUR_ACCESS_KEY_ID\u0026#34; secret_key = \u0026#34;YOUR_SECRET_ACCESS_KEY\u0026#34; region = \u0026#34;YOUR_REGION\u0026#34; } ロードバランサー周りの設定 # Terraform で構築された環境でのロードバランサーは HTTP と HTTPS の2つのリスナーがあります。HTTP リスナーは HTTP から HTTPS へリダイレクトするためのものです。\nHTTPS リスナーからターゲットグループへアクセスし、ポート3000に設定して Langfuse Web へリクエストをルーティングします。\nまとめ # 本記事では、公式ドキュメントおよび Terraform コードを利用して、AWS 上に Langfuse を構築する手順と、その過程で得られた実践的な Tips をご紹介しました。\n公式に提供されている Terraform 構成を活用することで、EKS、Aurora といった AWS のマネージドサービス群を効率的かつ再現性高くプロビジョニングできることをご確認いただけたかと思います。特に、インフラ構成をコードで管理する Infrastructure as Code (IaC) のアプローチは、環境構築の迅速化だけでなく、構成変更の追跡やチーム内での共有を容易にし、属人化を防ぐ上でも大きなメリットがあります。\nLangfuse は活発に開発が続けられており、LLM アプリケーション開発の効率化と品質向上に寄与する機能が今後も追加されていくことが期待されます。本記事が、皆様の AWS 上での Langfuse 導入、そして LLM を活用したイノベーション推進のお役に立てれば幸いです。ぜひ、公式ドキュメントと合わせてご活用いただき、実際の開発プロジェクトでお試しください。\n","date":"2025年5月28日","externalUrl":null,"permalink":"/posts/2025-05-28-terraform-%E3%81%A7%E5%AE%9F%E7%8F%BE%E3%81%99%E3%82%8B-langfuse-on-aws/","section":"Posts","summary":"はじめに # 本記事では、この Langfuse 環境を AWS 上に構築する方法について解説します\n","title":"Terraform で実現する Langfuse on AWS","type":"posts"},{"content":" はじめに # 本記事では、この Langfuse 環境を Google Cloud 上に構築する方法について解説します\n2025/05/22 に Langfuse の公式ドキュメントにおいて、Google Cloud 向けの Terraform 構成が公開されました。この公式ドキュメントに記載された手順をベースとし、実際に環境を構築する際の具体的なステップや留意点、さらに実運用を見据えたポイントなどを、弊社の知見を交えながらご紹介します。\n本記事の AWS バージョンはこちら 公式の Google Cloud システム構成 # Langfuse 環境を Google Cloud 上に Terraform で構築するにあたり、最も信頼できる情報源は Langfuse の公式ドキュメントです。公式で Google Cloud 向けの Terraform 構成が提供開始され、これにより導入のハードルが大きく下がりました。\nまず、Langfuse 公式サイトの Google Cloud 向けセルフホスティングガイド をご確認いただくことを強く推奨します。この公式ガイドには、Terraform で環境構築する手順が載っています。\n主要コンポーネントとGoogle Cloudプロダクトのマッピング # 公式ドキュメントで推奨されている構成、および Langfuse の一般的なコンポーネントが Google Cloud のどのプロダクトに対応するかを以下にまとめます。\nLangfuse コンポーネント Google Cloud プロダクト 主な目的・役割 Langfuse Web, Worker Google Kubernetes Engine Langfuseサーバーのコンテナをホスティングします。 Redis Memorystore for Redis キャッシュ・キューに使用します。 Postgres - OLTP Cloud SQL for PostgreSQL 認証情報などトランザクションデータを格納するストレージです。 ClickHouse - OLAP Google Kubernetes Engine トレースなどを格納するストレージです。 Blob Storage Google Cloud Storage 生のイベントやマルチモーダルファイルを保管します。 Terraformコードの提供形態 # Langfuse の GitHub リポジトリ には、これらの Google Cloud リソースを効率的にプロビジョニングするための Terraform 設定ファイル群（HCL コード）が提供されています。ユーザーは提供されたコードをベースに、ドメインや構成オプションを指定することで、推奨構成を迅速にデプロイできます。\n想定される構成図 # これらのコンポーネントから想定される構成は以下のようになります。これは弊社が以前投稿した記事：Google CloudでLangfuse v3の構築手順（推奨設定/GKE） と同じ構成となっています。\nシステム構成図 本番運用を見据えたアーキテクチャ選択について # セルフホストする際のインストールパターンは多岐に渡り、使用する場面に合わせて選択するのが良いです。これらセルフホスティングのパターンについてはこちら の記事で解説しています。\n公式 Terraform のシステム構成では、Langfuse Web, Langfuse Workder, ClickHouse に Google Kubernetes Engine (GKE) を使用した構築を選択しています。この選択の理由と、他の主要なホスティングオプションとの比較を以下に示します。\nGoogle Kubernetes Engine (GKE) 強み: 本番グレードのオートスケーリング、詳細なネットワーク制御、高可用性を実現します。将来的な機能拡張やデータ増大にも柔軟に対応できる、最も堅牢な選択肢です。 考慮点: 高機能かつ柔軟である反面、運用コストは他の選択肢より高くなる傾向があり、Kubernetes の運用知識と管理スキルが求められます。 Cloud Run 特性: サーバーレスで迅速なデプロイが可能であり、運用負荷とコストを抑えたい場合に適しています。オートスケーリングの機能も備わっています。 GKE との比較: GKEほどの高度なカスタマイズ性や、複数ポート利用といった要件への対応は限定的です。 Compute Engine (GCE) 特性: docker compose コマンドにより簡単にデプロイ可能です。 GKE との比較: コンテナ化された Langfuse アプリケーションの運用（特にClickHouse のコンテナ）においては、GKE のようなオーケストレーション機能がないため、スケーリング、自己修復、ローリングアップデートといった運用効率で劣ります。 費用の概算 # 最低限のランニングコストを試算しました。（リンク ）\n月額約 570$ となっています。\nTerraform での環境構築で知っておきたい Tips 集 # Langfuse の公式ドキュメントには、Google Cloud 上に Terraform を用いて環境を構築するための手順が詳細に記載されています。基本的にはこのドキュメントに従って進めることで、迷うことなく環境をセットアップできるでしょう。\n本セクションでは、実際に構築する過程で気づいた点や、よりスムーズに進めるための Tips などをまとめてご紹介します。\n構築の大まかな流れ # 公式ドキュメントで提供されている Terraform コードを利用した構築は、主に以下のステップで進められます。\nAPI を有効化します。 設定 HCL ファイルを用意します。ドメインを指定します。 terraform init をして、先に DNS 設定と GKE クラスターだけ apply します。 全てのリソースを apply します。 所要時間: 全体のデプロイには、環境にもよりますが 20 分〜 60 分程度かかります。\n構築時の Tips と留意点 💡 # 実際に公式ドキュメントの手順に沿って構築を進める中で、いくつか留意しておくと良い点や、カスタマイズのヒントがありました。\n設定ファイルのテンプレート活用 # 公式ドキュメントには設定ファイルの記述例がありますが、リポジトリ内の examples/quickstart/quickstart.tf に類似の構成ファイルが含まれている場合があります。こちらを参考にしたり、コピーして自身の環境に合わせて修正したりすると、設定ファイル作成の手間を省けることがあります。\nリソースの削除保護について # デフォルト設定では、安全のために Cloud SQL インスタンスなどの主要リソースに削除保護 (deletion_protection = true ) が有効になっていることがあります。検証目的などで頻繁にリソースの作成・削除を行いたい場合は、Langfuse の Terraform モジュールで明示的に指定することで、Terraform による削除が可能になります。\nmodule \u0026#34;langfuse\u0026#34; { # ... 他の変数 ... deletion_protection = false } Cloud SQL インスタンスのコスト最適化 # Terraform のデフォルト設定では、Cloud SQL インスタンスが比較的高性能なマシンタイプ（db-perf-optimized-N-2 ）でプロビジョニングされる場合があり、特に検証用途ではオーバースペックで料金が高額になる可能性があります。開発・検証環境など、そこまで高い性能を求めない場合は、Langfuse モジュールの設定で以下のようにマシンタイプやアベイラビリティタイプを調整することで、コストを大幅に抑えることができます。\nmodule \u0026#34;langfuse\u0026#34; { # ... 他の変数 ... database_instance_tier = \u0026#34;db-f1-micro\u0026#34; database_instance_availability_type = \u0026#34;ZONAL\u0026#34; database_instance_edition = \u0026#34;ENTERPRISE\u0026#34; } Cloud DNS を別プロジェクトで管理している場合 # Langfuse の Terraform 構成に Cloud DNS ゾーンやレコード作成が含まれている場合で、DNS 管理を別の Google Cloud プロジェクトで行っているケースでは、dns.tf ファイル内の \u0026ldquo;Create Cloud DNS zone\u0026rdquo; と \u0026ldquo;Create DNS A record for the load balancer\u0026rdquo; の2つのDNS 関連のリソース定義をコメントアウト、または削除する必要があります。terraform apply が完了後、コンソールで Aレコードを編集して HTTPS の方の Load Balancer のフロントエンドの IP アドレスにします。\nGCS HMAC キー発行時の認証について # Google Cloud コンソールからはユーザーアカウントで HMAC キーを発行できますが、Terraform（サービスアカウント経由）でユーザーアカウントの HMAC キーを直接発行することはできません。サービスアカウント自身の HMAC キーを発行することは可能です。組織ポリシーでサービスアカウントから HMAC キーを発行を制限している場合は注意が必要です。\nプロジェクトID・リージョン未設定エラーへの対処 # terraform apply 実行時などに、「プロジェクトが設定されていない」や「リージョンが設定されていない」といったエラーメッセージが表示されることがあります。これは、Google Cloud プロバイダが対象のプロジェクトやリージョンを認識できていない場合に発生します。以下の環境変数を設定するか、Terraform のプロバイダ設定で明示的に指定してください。\nexport GOOGLE_PROJECT=\u0026#34;YOUR_PROJECT_ID\u0026#34; export GOOGLE_REGION=\u0026#34;YOUR_REGION\u0026#34; または、プロバイダブロックに記述します。\nprovider \u0026#34;google\u0026#34; { project = \u0026#34;YOUR_PROJECT_ID\u0026#34; region = \u0026#34;YOUR_REGION\u0026#34; } ロードバランサー周りの設定 # Terraform で構築された環境でのロードバランサーは HTTP ロードバランサーと HTTPS ロードバランサーの2つあります。HTTP ロードバランサーは HTTP から HTTPS へリダイレクトするためのものです。\nHTTPS ロードバランサーには2つのバックエンドがあります。これは、ポート3000でアクセスできるならポート指定するパスルール設定のようです。\nまとめ # 本記事では、公式ドキュメントおよび Terraform コードを利用して、Google Cloud 上に Langfuse を構築する手順と、その過程で得られた実践的な Tips をご紹介しました。\n公式に提供されている Terraform 構成を活用することで、GKE、Cloud SQL for PostgreSQL といった Google Cloud のマネージドサービス群を効率的かつ再現性高くプロビジョニングできることをご確認いただけたかと思います。特に、インフラ構成をコードで管理する Infrastructure as Code (IaC) のアプローチは、環境構築の迅速化だけでなく、構成変更の追跡やチーム内での共有を容易にし、属人化を防ぐ上でも大きなメリットがあります。\n本記事でご紹介した Tips、特に Cloud SQL インスタンスのサイジング調整などは、コストを意識した運用を行う上で重要なポイントです。これらを参考に、ご自身のユースケースや予算に合わせて環境を最適化してください。\nLangfuse は活発に開発が続けられており、LLM アプリケーション開発の効率化と品質向上に寄与する機能が今後も追加されていくことが期待されます。本記事が、皆様の Google Cloud 上での Langfuse 導入、そして LLM を活用したイノベーション推進のお役に立てれば幸いです。ぜひ、公式ドキュメントと合わせてご活用いただき、実際の開発プロジェクトでお試しください。\n","date":"2025年5月26日","externalUrl":null,"permalink":"/posts/2025-05-26-terraform-%E3%81%A7%E5%AE%9F%E7%8F%BE%E3%81%99%E3%82%8B-langfuse-on-google-cloud/","section":"Posts","summary":"はじめに # 本記事では、この Langfuse 環境を Google Cloud 上に構築する方法について解説します\n","title":"Terraform で実現する Langfuse on Google Cloud","type":"posts"},{"content":" 1. はじめに # 本記事では Dify の Langfuse プラグインをご紹介いたします。\nDify でアプリを開発する際、ワークフローに直接プロンプトを書き込んでいくと、「前のプロンプトの方が良かったけど消しちゃった」「チームで同じプロンプトをスムーズに共有したいけど…」といったお悩みが出てきませんか？\nDify 上でプロンプトが増えてくると、バージョン管理や再利用が難しくなりがちです。これが開発効率やアプリケーションの品質に響いてしまうことも。もし、プロンプトを Dify のワークフローから切り離し、専用のツールで一元的に、かつバージョン管理しながら集中的に扱えたら、こうした課題はずいぶん楽になるはずです。\nそこで注目したいのが、LLM アプリケーションのトレーシングやプロンプト管理に特化したオープンソースツール「Langfuse」です。\n実は、皆さんがお使いの Dify は、既に Langfuse の非常に強力な「トレース機能」との連携を標準でサポートしています！\nDify での Langfuse のトレース連携 この Dify と Langfuse のトレース連携の具体的な設定方法や活用事例については、Langfuseでの可視化 [Dify編 (前半) ] でも詳しく解説されていますので、ぜひそちらも併せてご覧いただき、皆さんのアプリケーション監視や改善にお役立てください。\nLangfuseでの可視化 [Dify編 (前半) ]DifyのLLMアプリケーションをLangfuseで可視化する方法を解説（前半）。トレースの見え方と各ステップの観測ポイントを紹介します。 2025-01-17 Langfuse はトレース機能だけでなくプロンプトのバージョン管理、変更履歴の追跡を効率的に行う機能もあります。しかし、Langfuse でプロンプトをしっかり管理していても、それを Dify のような LLM アプリケーション開発プラットフォームで利用するには、ひと手間必要でした。\nそこで今回、この課題を解決するために、Dify から Langfuse で管理されているプロンプトを直接利用したり検索したりできるカスタムプラグインを開発し、OSS として GitHub で公開しました。\n▶ 本プラグインの GitHub リポジトリはこちら このプラグインを使えば、Langfuse で厳密にバージョン管理されたプロンプトを、Dify のワークフローに簡単に組み込むことができます。\nこの記事では、私たちが開発したこの Dify 向け Langfuse プラグインが、どのように皆様のプロンプト管理を効率化し、アプリケーション開発をサポートできるのか、その機能や使い方をご紹介します。具体的には、「特定のプロンプトを取得するツール(Get Prompt)」と「プロンプトを検索するツール(Search Prompts)」、「プロンプトを更新するツール(Update Prompt)」の3つの主要な機能について解説していきます。\nプロンプト管理に課題を感じている方、そして Langfuse の強力な管理機能を Dify で活かしたいと考えている方に、この記事がお役に立てれば幸いです。\n2. 背景 # このプラグインがどのような課題を解決し、どんなメリットをもたらすのかをご理解いただくために、まずは Dify と Langfuse、そしてそれらを連携させる価値について簡単にご説明します。\nDify とは # Dify は、ノーコードまたはローコードで LLM を活用したアプリケーションを迅速に構築できるプラットフォームです。直感的な GUI を通じて、複雑なワークフローも比較的簡単に作成できるのが大きな魅力です。\nLangfuse とは # 次に Langfuse ですが、これはLLMアプリケーションの「オブザーバビリティ（可観測性）」を高めるための強力なオープンソースツールです。Langfuse の主な機能としては、アプリケーションの実行フローを詳細にトレース（追跡）してデバッグを容易にしたり、消費トークン数を管理したり、ユーザーの利用状況に関するデータを収集・分析して改善に繋げたりといったものが挙げられます。これにより、LLM アプリケーションが実際にどのように動作しているのか、どこに問題があるのかを把握しやすくなります。\nしかし、Langfuse の魅力はそれだけではありません。今回私たちが特に注目しているのは、Langfuse が提供する「プロンプト管理機能」です。 Langfuse を使えば、作成したプロンプトをバージョンごとに記録し、変更履歴を管理し、さらには異なるバージョンのプロンプトを比較・評価することも可能です。これにより、プロンプトを単なるテキストではなく、適切に管理・改善していくべき「資産」として扱えるようになります。\n記事：Langfuse によるプロンプト管理の魅力 Dify × Langfuse 連携の価値 # Dify の手軽なアプリケーション構築能力と、Langfuse の堅牢な LLM 運用機能を組み合わせることで、開発プロセス全体に以下のような戦略的な価値がもたらされます。\nプロンプト開発の独立性と迅速性の向上: プロンプトのライフサイクルを Dify のワークフローから分離し、Langfuse で一元管理することで、プロンプトの修正や実験がアプリケーション本体の変更・再デプロイを必要としません。これにより、迅速なイテレーションと改善が実現します。 プロンプト品質と再利用性の一貫した担保: Langfuse による厳密なバージョン管理は、過去の優れたプロンプトへの確実なロールバックを可能にし、チーム内やプロジェクト間でのプロンプト共有と再利用を効率化し、アプリケーション全体の品質維持に貢献します。 データ駆動型プロンプトエンジニアリングの推進: Langfuse のトレース機能や評価基盤と連携することで、各プロンプトのパフォーマンスを客観的に把握しやすくなります。これにより、データに基づいた継続的な改善サイクルを構築し、プロンプトの最適化を促進できます。 このように、Dify と Langfuse の連携は、単にツールを繋ぐだけでなく、LLM アプリケーション開発の質と効率を一段階引き上げるポテンシャルを秘めているのです。本プラグインは、その連携を具体的に実現するための一助となります。\n3. Langfuse プラグインの概要 # Langfuse プラグインは Dify におけるプロンプト管理を、Langfuse と連携して効率化するために開発されました。Difyのワークフローから Langfuse 上のプロンプトを手軽に扱えるようにすることが主な目的です。\nプラグインは、主に以下の3つのツールで構成されています。これらのツールの詳細については、GitHub の README-ja をご覧ください。\nGet Prompt ツール: Langfuse から特定のプロンプト（本文やメタデータ）を取得します。 Search Prompts ツール: Langfuse 内のプロンプトを様々な条件で検索します。 Update Prompts ツール: Langfuse の特定のプロンプトを新しいバージョンとして更新します。 なお、本プラグインを通じて Langfuse の機能を利用するためには、Langfuse の API キーが必須となります。 API キーの取得方法や Dify への具体的な設定手順については、次のセクションでご説明します。\n4. 導入方法と使い方 # このセクションでは、開発した Langfuse プラグインを Dify に導入し、実際にワークフローで活用するための手順と基本的な使い方についてご説明します。\nこのプラグインはオープンソースソフトウェアとして、以下の GitHub リポジトリで公開されています。\n🚀 Dify-Langfuse連携プラグイン GitHubリポジトリ 🚀\nhttps://github.com/gao-ai-com/dify-plugin-langfuse 準備: Langfuse でプロンプトの作成 # Langfuse でプロンプトを作成する手順は公式ドキュメント をご覧ください。\nプラグインのインストール # 本プラグインは、Dify のプラットフォーム機能を利用して、GitHub リポジトリから直接インストールすることができます。\nDify のプラグイン管理ページへ移動: Dify にログイン後、画面右上にある[プラグイン]からプラグイン管理ページを開き、[プラグインをインストールする]、[GitHub]を続けてクリックします。 リポジトリアドレスの入力: インストールするプラグインの GitHub リポジトリアドレスを入力するフィールドが表示されますので、ここに、https://github.com/gao-ai-com/dify-plugin-langfuse を入力します。 バージョンとパッケージファイルの選択・インストール: リポジトリが認識されると、利用可能なバージョン番号とパッケージを選択する画面に進みます。適切なものを選択し、指示に従ってインストールを完了してください。 必要な設定：Langfuse APIキーとエンドポイント # プラグインをインストール後、Langfuse と連携するためにはLangfuse API キーとエンドポイントを設定する必要があります。\nLangfuse の [Settings] から [API Keys] で API キーを発行します。 Langfuse で API キーを発行 2. Dify プラグインのページから Langfuse プラグインを選択し、[認証する]をクリックします「Langfuse 公開鍵」「Langfuse 秘密鍵」「Langfuse Host」を設定します。\nLangfuse の認証 基本的な使い方 # このLangfuse連携プラグインのツールをDifyのワークフローに組み込むことで、プロンプトの検索、取得、更新をシームレスに行うことができます。以下に基本的な利用フローの例を示します。\nSearch Prompts ツールで目的のプロンプトを探す\n特定のプロンプト名が不明な場合や、条件に合うプロンプトのリストから選びたい場合にこのツールを使用します。\nツールから Langfuse プラグインの Search prompts ツールを選択します。 入力パラメータを設定します。例えば、「customer_support_faq というラベルがついた最新のプロンプト」を探したい場合は、label フィールドに customer_support_faq を入力します。 このツールを実行すると、条件に一致するプロンプトのメタデータ（プロンプト名、バージョン情報など）がリスト形式で出力されます。 Get Prompt ツールでプロンプト本文を取得\n実際に使用するプロンプトの本文を取得します。\nツールから Langfuse プラグインの Get Prompt ツールを選択します。 取得するプロンプトの入力パラメータを設定します。 このツールを実行すると、指定されたプロンプトの本文が text 出力として、メタデータが json 出力として得られます。 LLMノードで取得したプロンプトを利用\nGet Prompt ツールで取得したプロンプト本文を、LLM ノードで使用します。\nLLM ノードを追加または選択します。 LLM ノードのプロンプト入力設定部分（例: システムプロンプトやユーザープロンプトのフィールド）に、Get Prompt ツールの出力を参照する変数を挿入します。 Get Prompt ツールの使用例 Update Prompt ツールでプロンプトを更新\n特定のプロンプト本文を更新します。\nツールから Langfuse プラグインの Update prompt ツールを選択します。 更新するプロンプトの入力パラメータを設定します。 このツールを実行すると、Langfuse に新しいプロンプトバージョンが作成され、指定したラベルやタグが設定されます。 ※Get Prompt ツールと併用する際にはタグの設定間違えのないよう注意してください。\nこれで、Langfuse で管理されたプロンプトを Dify のワークフロー内で動的に利用する基本的な流れが完成です。これにより、プロンプトの変更を Langfuse 側で行うだけで、Dify のワークフローは変更せずに最新のプロンプトを適用できるようになります。\n5. 活用シナリオ # この Langfuse 連携プラグイン、特に Get Prompt ツールをDifyのワークフローに組み込むことで、皆さんのアプリケーション開発はより効率的で、柔軟になります。いくつかの具体的な活用シナリオを見ていきましょう。\nシナリオ1：安全かつ迅速なプロンプトの改善サイクル # Dify で運用中のプロンプトを改善したいけれど、直接編集はリスクがあり、ワークフロー複製も手間になります。\nこのプラグインがあれば: Get Prompt ツールで取得取得したプロンプトを元に改善案を練り、その新しいプロンプトテキストを Update Prompt ツールを使って Langfuse の該当プロンプトの新しいバージョンとして登録します。その際、例えば staging や developer-test-v2 といったラベルを新しいバージョンに付与します。 その後、Dify のテスト用ワークフローで改めて Get Prompt ツールを使い、その新しいラベルを指定して改善版プロンプトを読み込み、効果を検証します。期待通りの結果が得られれば、Langfuse 側でそのバージョンに production ラベルを付け替えるか、あるいは本番用ワークフローが参照するラベルを更新します。 これにより、Dify 上で直接プロンプトを編集・テストし、その成果を即座に Langfuse のバージョン管理下に置き、安全に本番環境へ反映するという、よりダイレクトで迅速な改善サイクルが実現します。問題発生時のロールバックも Langfuse のバージョン履歴から容易に行えます。\nシナリオ2：A/Bテストによる最適なプロンプトの追求 # 複数のプロンプト案の効果を Dify で比較したいけれど、準備が大変だと感じていませんか。\nこのプラグインがあれば: 比較したい複数のプロンプト案を Dify 上で作成し、それぞれを Update Prompt ツールを使って Langfuse の同じプロンプト名に対して異なるラベル（例: test-A, test-B）を付与した新しいバージョンとして登録します。 その後、Dify 側で Get Prompt ツールを使い、リクエストに応じて呼び出す label を動的に切り替えて A/Bテストを実施します。それぞれの結果の品質を Langfuse のトレース機能やスコアリング機能（Langfuse 本体の機能）と組み合わせて分析すれば、実際の利用データに基づき最適なプロンプトを選定できます。 Dify の柔軟性と Langfuse の管理・評価機能を活かし、データドリブンなプロンプト改善が可能です。\nシナリオ3：チームでの効率的なプロンプト共同開発・運用 # チームでの Dify 開発時、プロンプト管理が属人化したり、最新版の共有が難しかったりしませんか。\nこのプラグインがあれば: Langfuse をチーム共通の「プロンプトリポジトリ」として活用します。プロンプトエンジニアが Langfuse でプロンプトのベースラインを作成・管理する一方で、Dify を利用する各開発メンバーも、日々の開発やテストの中で改善したプロンプトや新しいアイデアを Update Prompt ツールを使い、自身の名前やタスクID を含む一時的なラベルを付けて Langfuse に新しいバージョンとして気軽に登録・共有できます。 これにより、個々の改善案が埋もれることなく Langfuse に集約され、チームレビューを経て正式なバージョンやラベル（例: stable, beta）に昇格させることが可能になります。Search Prompts ツールでこれらの試行錯誤中のプロンプトも検索・発見できるため、チーム全体の知見共有と開発効率が大幅に向上します。\nこれらのシナリオはほんの一例です。皆さんのアイデア次第で、このプラグインは Dify での LLM アプリケーション開発をさらに強力にサポートしてくれるはずです。\n6. 制限事項と今後の展望 # この Dify 向け Langfuse 連携プラグインをより快適にご利用いただくために、現時点での主な制限事項と、今後の機能拡張に関するアイデアについて触れておきます。\n現時点での主な制限事項 # 本プラグインをご利用いただくにあたり、いくつかの制限事項がございます。主な点として、Get Prompt ツールと Update Prompt ツールは現状、Langfuse 上で text タイプとして保存されているプロンプトのみに対応しており、chat タイプは対象外です。また、Get Prompt ツールでラベルとバージョン番号の同時指定はできません。これは Langfuse API の仕様によるものです。\n今後の展望に関するアイデア # このプラグインをさらに便利にするためのアイデアとして、以下のような機能拡張が考えられます。これらは現時点での構想であり、将来的な実現をお約束するものではありませんが、可能性としてご紹介します。\nカスタムトレースを定義できるオリジナル LLM ノードの作成 Langfuse へのトレース情報をより詳細かつ柔軟に送信できる、このプラグイン専用のLLM 実行ノードがあれば、Dify のワークフロー内での処理と Langfuse 上のトレースデータとの連携を一層深め、より高度な分析やデバッグに役立つかもしれません。 chat タイプのプロンプトへの対応 Get Prompt ツールが chat タイプのプロンプトにも対応するようになれば、Langfuse で管理できるプロンプトの形式が広がり、より多様な LLM アプリケーションの構築に貢献できる可能性があります。 Langfuse データセットとの連携 Langfuse のデータセット機能を Dify から活用できるようになれば、例えば、Langfuse に登録された評価用データセットを Dify から参照し、異なるプロンプトバージョンでの出力をバッチテストするようなワークフローを構築するなど、プロンプトの品質評価や改善サイクルをさらに効率化できるかもしれません。 7. まとめ # 本記事では、Dify と Langfuse を連携させ、プロンプト管理を格段に向上させるカスタムプラグインをご紹介しました。このプラグインを利用することで、皆さんが普段お使いのDify の手軽なアプリケーション開発フローはそのままに、Langfuse が持つ堅牢なプロンプトのバージョン管理やトレーサビリティといった強力な機能の恩恵を受けることができます。これにより、プロンプト管理の煩雑さから解放され、品質向上、開発サイクルの迅速化、そしてチームでの効率的な共同作業が期待できます。\nDify での開発効率をさらに高めたい、あるいはプロンプト管理を本格的に導入したいとお考えの Dify ユーザーの皆様にとって、本プラグインはきっとお役に立てるはずです。\nこのプラグインはオープンソースソフトウェアとして GitHub で公開しています。ぜひ一度お試しいただき、その効果を実感していただければと思います。\n実際にプラグインを使ってみたご感想、改善点のご提案、バグのご報告、さらには機能追加に関するプルリクエストなど、皆様からの積極的なフィードバックやコントリビューションを心より歓迎いたします。コミュニティと共に、このプラグインをより実践的で強力なツールへと育てていければ幸いです。\nこの記事が、皆さんの Dify と Langfuse を活用したLLMアプリケーション開発の一助となることを願っています。\n","date":"2025年5月22日","externalUrl":null,"permalink":"/posts/2025-05-22-dify-%E3%81%AE%E3%83%97%E3%83%AD%E3%83%B3%E3%83%97%E3%83%88%E7%AE%A1%E7%90%86%E3%82%92%E5%8A%87%E7%9A%84%E3%81%AB%E6%94%B9%E5%96%84-langfuse-%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E3%81%AE%E3%81%94%E7%B4%B9%E4%BB%8B/","section":"Posts","summary":"1. はじめに # 本記事では Dify の Langfuse プラグインをご紹介いたします。\nDify でアプリを開発する際、ワークフローに直接プロンプトを書き込んでいくと、「前のプロンプトの方が良かったけど消しちゃった」「チームで同じプロンプトをスムーズに共有したいけど…」といったお悩みが出てきませんか？\n","title":"Dify のプロンプト管理を劇的に改善！Langfuse プラグインのご紹介","type":"posts"},{"content":" はじめに # 本記事では、Agent Development Kit (ADK) によって構築されたAIエージェントの挙動をLiteLLMを通じてLangfuseで可視化する方法について解説していきます。\nADKの基本的な内容やその評価についてはこちら Langfuse についての 説明についての記事はこちら 生成AIアプリケーションやAIエージェントを開発し、そのパフォーマンスを最大限に引き出す上で、内部処理の可視化は成功の鍵を握ると言っても過言ではありません。これは、ADKを用いた開発においても例外ではありません。\n可視化ツールとして Cloud Trace のような選択肢も考えられますが、本記事では、導入の容易さ、直感的な操作性、そして LLMOps 全体のトレーサビリティをオープンソースで実現できるLangfuseに焦点を当てます。\nただし、ADK から Langfuse へ直接Traceを連携させようとすると、現時点（2025年5月8日）では注意が必要です。例えば、OpenTelemetry 経由で Langfuse にトレースを送信した場合、全ての情報がメタデータとして一括りに扱われてしまい、詳細な分析やデバッグが困難になるなどの課題があります。\nそこで本記事では、この課題を解決し、ADKで開発するエージェントの情報を Langfuse 上で効果的に可視化するためのアプローチとして、ADK と Langfuse が共にネイティブサポートしている LiteLLM を利用する構成をご紹介します。この構成を採用することで、LLM とのやり取りを含む詳細なトレース情報を、構造化された形でスムーズに Langfuseへ連携させることが可能になります。構成はおおまかには以下のようになります。\nADK で作ったAgentと同じ環境にProxyとしてLiteLLMを起動 それでは、LiteLLM のセットアップから進めていきましょう。\nLiteLLM とは # LiteLLMは、OpenAI、Azure、VertexAI/Gemini、Anthropicなど、100種類以上の様々な大規模言語モデル（LLM）に対して、統一されたシンプルなインターフェースでアクセスできるようにするライブラリです。 これにより、開発者は特定のLLMプロバイダーにロックインされることなく、アプリケーションの要件に応じて最適なモデルを柔軟に選択・切り替えることが可能になります。 LiteLLM は、APIキー管理、呼び出しのフォーマット統一、そしてリクエストのロギングやコールバック, 同一コードでの切り替え, 帯域制限やコスト管理といった機能など提供し、LLM 運用において大きな助けとなります。LiteLLM には SDK として利用するモードと、Proxy として動作させるモードがありますが、今回はついでにProxy モードのセットアップ方法も紹介しながら構成を作っていきます。\nLiteLLM (Proxyモード) のセットアップ # セットアップは非常にシンプルです。公式の手順 がありますので、基本的にはこれに沿って進めるだけで立ち上げることが可能です。 litellm_config.yaml に model 定義などを入れるのですが、今回は以下のような設定をしています。\nmodel_list: - model_name: gpt-4o litellm_params: model: openai/gpt-4o api_key: \u0026#34;os.environ/OPEN_AI_API_KEY\u0026#34; - model_name: gemini-2.0-flash litellm_params: model: vertex_ai/gemini-2.0-flash vertex_project: \u0026#34;YOURPROJECTNAME\u0026#34; vertex_location: \u0026#34;us-central1\u0026#34; vertex_credentials: \u0026#34;os.environ/VERTEXAIFILE\u0026#34; - model_name: gemini-2.5-flash litellm_params: model: vertex_ai/gemini-2.5-flash-preview-04-17 vertex_project: \u0026#34;YOURPROJECTNAME\u0026#34; vertex_location: \u0026#34;us-central1\u0026#34; vertex_credentials: \u0026#34;os.environ/VERTEXAIFILE\u0026#34; general_settings: master_key: sk-1234 litellm_settings: drop_params: True success_callback: [\u0026#34;langfuse\u0026#34;] redact_user_api_key_info: true module list の中に、model_name として任意の名前をつけ、後ほどこの名前をプログラム内から利用することで、指定したモデルが使われるようになります。APIキーやクレデンシャルは、.env ファイルを作り、環境変数として読みこませておきます。\nそして設定最下部に success_callback: [\u0026ldquo;langfuse\u0026rdquo;] を入れることを忘れないようにしましょう。逆にいうとLangfuse のための設定はこの１行だけという簡単さです。\n続いて環境変数ファイル (.env)を、以下のような内容で作成します。\nexport LITELLM_MASTER_KEY=\u0026#34;sk-1234\u0026#34; export LITELLM_SALT_KEY=\u0026#34;sk-1234\u0026#34; export LANGFUSE_PUBLIC_KEY=\u0026#34;pk-lf-****\u0026#34; export LANGFUSE_SECRET_KEY=\u0026#34;sk-lf-****\u0026#34; export LANGFUSE_HOST=\u0026#34;YOURLANGFUSEHOSTURL\u0026#34; export OPEN_AI_API_KEY=\u0026#34;sk*****\u0026#34; export VERTEXAIFILE=\u0026#34;FILENAME\u0026#34; export OPENAI_API_BASE=http://localhost:4000/v1 続いて docker-compose.yml をちょっと編集します。volumes と command が最初はコメントアウトされているので、それをアンコメントして、VertexAI認証用のファイルを読み込ませています。(セキュリティ的にベストプラクティスではないですが、一時的な検証用ということで簡単な方法で実装しています) もちろん、たとえばOpenAI だけで良い場合は対応不要です。\nvolumes: - ./litellm_config.yaml:/app/litellm_config.yaml - /SOURCE.json:/app/vertexaifile.json command: - \u0026#34;--config=/app/litellm_config.yaml\u0026#34; docker compose up で Proxy を起動し、以下のように curl で疎通確認をしてみてください。結果をLangfuse内で Trace として確認できるはずです。\n% curl --location \u0026#39;http://0.0.0.0:4000/chat/completions\u0026#39; \\ --header \u0026#39;Content-Type: application/json\u0026#39; \\ --header \u0026#39;Authorization: Bearer sk-1234\u0026#39; \\ --data \u0026#39;{ \u0026#34;model\u0026#34;: \u0026#34;gpt-4o\u0026#34;, \u0026#34;messages\u0026#34;: [ { \u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;気の利いたアメリカンジョークをかましてくれ！\u0026#34; } ] }\u0026#39; すると、Langfuse側では以下のように Trace を確認することができます。\nジョークはつまらないですが、Trace は見えます ここまでで一旦準備は完了です。\nADK のコードを準備する # 基本的には全てこちらのコード を利用し、以下に示す点のみを編集するだけです。\nagent.py の冒頭で LiteLLM のモジュールをインポートします。\nfrom google.adk.models.lite_llm import LiteLlm from google.adk.agents import LlmAgent 続いて最下部を以下の通りに変更します。\nlite_model = LiteLlm(model=\u0026#34;openai/gemini-2.0-flash\u0026#34;) root_agent = LlmAgent( name=\u0026#34;weather_time_agent\u0026#34;, model= lite_model, description=( \u0026#34;Agent to answer questions about the time and weather in a city.\u0026#34; ), instruction=( \u0026#34;You are a helpful agent who can answer user questions about the time and weather in a city.\u0026#34; ), tools=[get_weather, get_current_time], ) 以上です！\nLiteLlm の中の model には litellm_config.yaml であらかじめ定義したmodel名を定義しますが、その際に指定した openai インターフェイスに則った LiteLLM Proxy のエンドポイントを指定するため、明示的に \u0026ldquo;openai/\u0026rdquo; と指定しています。Google が提供する LLM であるGemini にアクセスするのに openai/gemini という記載になることに違和感があるかもしれませんが、これにより OPENAI_API_BASE と .env に定義した先にリクエストが飛ぶようになるため、必須の設定となります。\nそして、実行するとこのように出力されます。(フォルダ名は任意に変えてください)\n% adk run adk_agent Log setup complete: /var/folders/ys/8vpjq9657dnb3dvq33k6q_hm0000gp/T/agents_log/agent.20250508_213004.log To access latest log: tail -F /var/folders/ys/8vpjq9657dnb3dvq33k6q_hm0000gp/T/agents_log/agent.latest.log Running agent weather_time_agent, type exit to exit. [user]: new york 21:30:14 - LiteLLM:INFO: utils.py:2827 - LiteLLM completion() model= gemini-2.0-flash; provider = openai 21:30:15 - LiteLLM:INFO: utils.py:2827 - LiteLLM completion() model= gemini-2.0-flash; provider = openai [weather_time_agent]: OK. The weather in New York is sunny with a temperature of 25 degrees Celsius (77 degrees Fahrenheit). The current time in new york is 2025-05-08 08:30:15 EDT-0400. ちなみに上記コマンドを入れた際、以下のようなエラーが出るかもしれません。\nlitellm.exceptions.AuthenticationError: litellm.AuthenticationError: AuthenticationError: OpenAIException - Authentication Error, Invalid proxy server token passed. key=******, not found in db. Create key via /key/generate call. エラーの原因は、LiteLLM Proxy が「渡されたトークン」を自身のデータベースで認識しておらず、認証に失敗しているためです。その際には以下のようなコマンドを実行して、keyを発行してみてください。\ncurl --location \u0026#39;http://0.0.0.0:4000/key/generate\u0026#39; \\ --header \u0026#39;Authorization: Bearer sk-master-あなたが決めたキー\u0026#39; \\ --header \u0026#39;Content-Type: application/json\u0026#39; \\ --data-raw \u0026#39;{ \u0026#34;models\u0026#34;: [\u0026#34;gpt-4o\u0026#34;], \u0026#34;metadata\u0026#34;: {\u0026#34;user\u0026#34;: \u0026#34;your-user-id\u0026#34;} }\u0026#39; そうすると key を含んだJSONが返却されますので、agent.py と同じディレクトリ下に置く .env に以下のような形式でKeyを追加しておいてください。\nexport OPEN_AI_API_KEY=\u0026#34;sk-***\u0026#34; なお locahost: 4000 でブラウザ経由でもアクセス可能ですので、LiteLLM は UI上から操作しても問題ないでしょう。\nそしてエラーがなくなれば、以下のような出力を Langfuse 上で確認できます。\nAgent の挙動を確認可能 いかがでしたでしょうか？\nLiteLLM を通じて、Trace が入ることを確認いただけたかと思います。ぜひお手元のADK 環境でも試してみてください！また別の機会に LiteLLM の機能の掘り下げた記事も公開していきたいと思います。Langfuse 可視化のみならず、LLMOps のサイクルをオールインワンで解決できることが強みです。ぜひこれを機会に活用してみてください。\n","date":"2025年5月8日","externalUrl":null,"permalink":"/posts/2025-05-08-adk-agent-development-kit-%E3%81%A7%E9%96%8B%E7%99%BA%E3%81%97%E3%81%9Fagent%E3%81%AE%E6%8C%99%E5%8B%95-%E3%82%92-langfuse%E3%81%A7%E5%8F%AF%E8%A6%96%E5%8C%96%E3%81%97%E3%82%88%E3%81%86/","section":"Posts","summary":"はじめに # 本記事では、Agent Development Kit (ADK) によって構築されたAIエージェントの挙動をLiteLLMを通じてLangfuseで可視化する方法について解説していきます。\n","title":"ADK (Agent Development Kit) で開発したAgentの挙動 を Langfuseで可視化しよう！","type":"posts"},{"content":" 1. はじめに: Langfuseとは何か？ # 生成AIアプリケーションを本番投入したものの、「何が悪いか分からないが生成AIアプリが思ったように動かない」「ちょっとプロンプトを変えるだけで、アプリ自体をもう一度リリース」「プロンプトやモデルを変えたら精度は上がような気がするが、どれくらい良くなったのかなどは感覚でしかない」「エージェントが暴走して 無限にAPIを叩き続けているが原因が分からない」「どのユーザーセッションで不具合が起きたか追えない」「そもそも役に立ってるのかも分からない」\n・・・・ このような悩みはありませんでしょうか？\nこうした 開発・運用・改善の断絶 は、プロダクトマネージャー, SRE, 生成AIの企画担当者 まで誰もが抱える悩みです。Langfuse はそれらの悩みを “Trace” という一本のバックボーン に集約し、以下のような役割の課題を解決します。\n開発者 … 処理の可視化、プロンプト管理、リリース前テストで開発工程を短縮 運用担当 … コスト, レイテンシ, 品質をモニタリングで異常を発見し、原因を特定 生成AI企画・業務担当者 … ROI や品質を判断して、改善に繋げる Langfuse は OSS をベースにして Self-hostedと SaaS という２種類の提供オプションを持ち、ロードマップは常に公開・更新されており透明性があり、OSS コアモデルがゆえに国内外で活発なコミュニティが形成されている、非常にユニークな「LLM エンジニアリング プラットフォーム」です。\n2. Langfuseが提供する主な機能 # 以下の表は、Langfuseが提供する主な機能をまとめたものです。\nこれらの機能が非常にわかりやすい GUI や API で提供されています。\n機能 目的 代表ユースケース Tracing レイテンシ・エラー・再試行・トークン・コストをまとめて記録 ボトルネック解析／コスト急騰の問題解決 Prompt Management バージョン管理・ラベル付け・保護 破壊的変更の防止、チーム共有、プロンプト変更とリリースの高速化 Evaluations ユーザーフィードバック＋LLM as a Judge ＋人手評価 生成AIの効果測定／A/B テスト Datasets 良質 Q\u0026amp;A を蓄積し学習ループを構築 継続学習・RAG 強化 Dashboards \u0026amp; Playground／Experiments KPI 分析＋GUI テスト＋並列実験 仮説検証を高速化し部門間共有 この画像は実際のTracing 画面のキャプチャです。画像左側のバーに実際の処理の流れとコンポーネントが表示されており、クリックするとその中身が右側に詳細が表示されます。\nTrace画面の例: 非常に見やすく、可視化や分析にすぐに役立つ 3. “Trace” を中心に循環するオペレーティングモデル # 最近では、生成AIアプリケーション市場の高まりに合わせて、Proprietary から OSS まで、さまざまな可視化ソリューションが登場してきました。しかしながら、単なる可視化ソリューションの導入は前述のような課題の一部のみを解決するものであり、LLMの品質を上げるためには可視化された情報を活用することこそが重要です。Langfuse はその点において、ひとつのプラットフォームとして、必要とされる一連の流れをカバーできるということに大きな強みがあります。参考までに、どのように Langfuse の各機能が Trace に紐づいているかをまとめたものが以下の表です。\nサイクル 役割 キーとなる Trace の利用法 Prompt Management プロンプトを一元管理し、Trace の入力/出力とプロンプトもリンク 使用されているプロンプトが、品質・コストへ与える影響をTraceごとに確認 Evaluations Trace ごとにユーザ評価/ 自動評価 / 人的評価を与える 評価をTraceに紐づけることで、問題の特定 Datasets テスト用などのデータを管理 Trace から直接データセットを作りし、問題があった Trace などをプロンプトやモデルの変更などで試験することで、改善サイクルを回すことができる Dashboards コスト、評価、利用状況などを一元管理 Trace から KPI を判断 このようにTrace さえ送れば 管理→評価→最適化 が芋づる式に回り、GenAIOps/LLMOps の“理想形”を最短で実装できることが、LLMエンジニアリングプラットフォームとされている所以とも言えるでしょう。\n例えば以下の画像は前出のTrace画像の一部ですが、トークンやコストだけではなく、この処理で使われたモデル名 \u0026ldquo;gpt-4o-mini \u0026quot; や プロンプト名 \u0026ldquo;qa-answer-withcontext-chat -v47\u0026rdquo; が紐づいていることが確認できます。またこのTraceを直接 dataset に入れることができる \u0026ldquo;Add to datasets\u0026rdquo; や 人的評価 (Human Annotation) をする \u0026ldquo;Annotate\u0026rdquo; もTraceから実施できることがわかります。\nこのように Langfuse は個々の機能を単に提供しているだけではなく、それらが Trace を軸にして全て結びついて設計がされているということに特徴を持ちます。\nまた今回は触れませんが、Langfuse外のツール 例えば異常を検知するガードレール機能やRAGASのような別の評価・スコアリングとも連携し、APIを通じてTraceにスコアをつけることなども可能であり、まさにプラットフォームとして幅広く利用することができます。\n4. 導入パターン # Langfuse では SaaS と Self-hosted の2パターンでの提供がされています。\nSaaS と同様の機能を Self-hosted で利用でき、かつ OSS から手軽に始めることができることも Langfuse の大きな特徴です。まずは無償のSaaS版や OSS で簡単に立ち上げ、その後に本格導入することができるため、企業への導入の敷居も非常に低いと言えるでしょう。\n方式 主に利用されているケース SaaS (Langfuse Cloud) 従量課金 (無償版あり)でも問題がない。個人プロジェクト、PoC やスタートアップ、メンテ不要で素早やく試したい。 Self-hosted (Docker Compose, Helm, Terraform, AWS CDK などによるインストール) 固定費用 (OSS版なら無償)が予算の都合が良い。セキュリティ要件として、自社内のNWに情報を保管する必要がある。検証用でローカル環境で手軽に試したい。 Self-hosted において、どのパターンでインストールするかについては、こちらの Blog も参考になさってみてください。 Docker compose などを使えば、すべての手順が数分で完了しますので、ぜひまずはとりあえずサクッとお試しください。\n5. ロードマップとコミュニティ ― “公開・参加型”の進化エコシステム # Langfuse は OSSをベースとして展開しており、その運営も非常にユーザーフレンドリーで透明性が高く、以下のような特徴を持ちます。\n公開されたロードマップ GitHub Discussions に専用スレッドを設け、公式ページは機能計画とステータスが随時更新されています。また採択・却下の理由まで閲覧可能で、ユーザーのニーズを汲み取りながらも、非常に透明性の高い運営がされています。\nロードマップ: https://langfuse.com/docs/roadmap 2. コミュニティサポート\nもし Bug と思われる挙動を発見した場合には、Github の Issue で報告をすることができます。有償サポートほどではないですが、開発者をはじめとしてコミュニティから多くのサポートを得られます。 3. フィードバックが反映\nIdea ボードで提案に投票→優先度が可視化されます。Issue/PR はコメント付きでレビューされ、採用までの過程がすべて残ります。 4. 定例イベント\nDiscord Community Session（隔週） と Town Hall（毎月） で最新リリースをライブ解説。アーカイブは YouTube に公開され、非参加者もキャッチアップが可能。 5. 日本発コミュニティ\n**「Langfuse Night」**Langfuse Night #1（2025/1/28、虎ノ門）と #2（2025/3/25）が 50〜60 名規模で開催されています。資料・動画を全公開。国内でも知見循環が急速に広がっています。​またAWS でのハンズオン なども開催され、関連エコシステムとの連携も発展しています。\nLangfuse の Github レポジトリはこちらです。ぜひご確認ください。 https://github.com/langfuse/langfuse まとめ # Langfuse は生成AIアプリケーション / エージェント の開発から運用までを一貫して支援するプラットフォームです。その導入は非常にシンプルで、Cloud版やSelf hosted のDocker版なら数分で利用を開始することができます。まずはTraceの可視化を見える化から 始めて、その後にプロンプト管理や評価などをそこに付け足していくことで、生成AIのビジネス効果が高めていくことが可能です。\nガオ株式会社はサポートや有償版のリセールなどを行なっている Langfuse の 日本/APAC唯一のパートナーです。不明点など、お気軽にご相談ください。\n","date":"2025年4月27日","externalUrl":null,"permalink":"/posts/2025-04-27-langfuse-%E5%85%A5%E9%96%80-%E3%81%9D%E3%81%97%E3%81%A6%E3%81%AA%E3%81%9C-langfuse%E3%81%8C%E6%94%AF%E6%8C%81%E3%82%92%E3%81%95%E3%82%8C%E3%81%A6%E3%81%84%E3%82%8B%E3%81%AE%E3%81%8B/","section":"Posts","summary":"1. はじめに: Langfuseとは何か？ # 生成AIアプリケーションを本番投入したものの、「何が悪いか分からないが生成AIアプリが思ったように動かない」「ちょっとプロンプトを変えるだけで、アプリ自体をもう一度リリース」「プロンプトやモデルを変えたら精度は上がような気がするが、どれくらい良くなったのかなどは感覚でしかない」「エージェントが暴走して 無限にAPIを叩き続けているが原因が分からない」「どのユーザーセッションで不具合が起きたか追えない」「そもそも役に立ってるのかも分からない」\n","title":"Langfuse 入門 、そしてなぜ Langfuseが支持をされているのか","type":"posts"},{"content":"","date":"2025年4月27日","externalUrl":null,"permalink":"/tags/%E7%94%9F%E6%88%90ai/","section":"タグ","summary":"","title":"生成AI","type":"tags"},{"content":"","date":"2025年4月27日","externalUrl":null,"permalink":"/tags/%E5%85%A5%E9%96%80/","section":"タグ","summary":"","title":"入門","type":"tags"},{"content":"最近話題の Google 製 AI エージェントフレームワーク「Agent Development Kit (ADK)」を触ってみました！ Gemini モデルとの連携がしやすく、柔軟なエージェント開発が可能とのことで、期待が高まります。エージェントが自律的にツールを使うのは凄いですが、ちゃんと意図通り動くか、修正で壊れないかを確認する「評価」も重要ですよね。\nそこで今回は、ADK のセットアップからエージェント作成、そしてそのエージェントを「評価」機能でテストするところまで、一通り試してみた記録をまとめます。\n公式ドキュメント : 事前準備の参考記事 ：（事前準備はこちらの記事を参考にしました） 1. 環境セットアップ # まずはローカル環境（Mac）でプロジェクトを準備します。パッケージマネージャーには uv を使用しました。\nBash\n# プロジェクトディレクトリ作成と移動 (Python 3.12.9 を指定) uv init -p 3.12.9 adk-work cd adk-work # ADK パッケージのインストール # (評価機能も使うため、[eval] 付きでインストールする) uv add \u0026#34;google-adk[eval]\u0026#34; google-adk[eval] とすることで、評価に必要な追加ライブラリ (pandasなど) も一緒にインストールされます。\n2. エージェントプロジェクトの作成 # 参考記事に沿って、天気 (get_weather) と時刻 (get_current_time) を取得するツールを持つエージェント (multi_tool_agent) を作成します。\nディレクトリ・ファイル構成:\nBash\nmkdir multi_tool_agent touch multi_tool_agent/{__init__.py,agent.py,.env} tree -a multi_tool_agent/ 実行結果：\nmulti_tool_agent/ ├── .env ├── __init__.py └── agent.py ファイルの内容:\nmulti_tool_agent/init.py: from . import agent multi_tool_agent/agent.pyのコードを記述します。 import datetime from google.adk.agents import Agent from zoneinfo import ZoneInfo def get_weather(city: str) -\u0026gt; dict: \u0026#34;\u0026#34;\u0026#34;特定の都市の現在の天気情報を取得する。 Args: city (str): 天気情報を取得したい都市名。英語で。 Returns: dict: ステータスと結果またはエラーメッセージ。 \u0026#34;\u0026#34;\u0026#34; if city.lower() == \u0026#34;new york\u0026#34;: return { \u0026#34;status\u0026#34;: \u0026#34;success\u0026#34;, \u0026#34;report\u0026#34;: (\u0026#34;ニューヨークの天気は晴れです。\u0026#34; \u0026#34;気温は25度（41°F）です。\u0026#34;), } else: return { \u0026#34;status\u0026#34;: \u0026#34;error\u0026#34;, \u0026#34;error_message\u0026#34;: f\u0026#34;{city}の天気情報は利用できません。\u0026#34;, } def get_current_time(city: str) -\u0026gt; dict: \u0026#34;\u0026#34;\u0026#34;特定の都市の現在時刻を取得する。 Args: city (str): 天気情報を取得したい都市名。英語で。 Returns: dict: ステータスと結果またはエラーメッセージ。 \u0026#34;\u0026#34;\u0026#34; if city.lower() == \u0026#34;new york\u0026#34;: tz_identifier = \u0026#34;America/New_York\u0026#34; else: return { \u0026#34;status\u0026#34;: \u0026#34;error\u0026#34;, \u0026#34;error_message\u0026#34;: (f\u0026#34;{city}のタイムゾーン情報は利用できません。\u0026#34;), } tz = ZoneInfo(tz_identifier) now = datetime.datetime.now(tz) report = f\u0026#39;{city}の現在時刻は {now.strftime(\u0026#34;%Y-%m-%d %H:%M:%S %Z%z\u0026#34;)}です。\u0026#39; return {\u0026#34;status\u0026#34;: \u0026#34;success\u0026#34;, \u0026#34;report\u0026#34;: report} root_agent = Agent( name=\u0026#34;weather_time_agent\u0026#34;, model=\u0026#34;gemini-2.0-flash-exp\u0026#34;, description=\u0026#34;任意の都市の時刻と天気に関する質問に答えるエージェントです。\u0026#34;, instruction=\u0026#34;私は指定された都市の時刻や天気に関する質問に答えることができます。\u0026#34;, tools=[get_weather, get_current_time], ) 3. モデル接続の設定 (.env ファイル) # LLM (Gemini) への接続情報を .env ファイルに記述します。Google AI Studio か Vertex AI かで設定内容が異なります。\nGoogle AI Studio: GOOGLE_GENAI_USE_VERTEXAI=\u0026#34;False\u0026#34; GOOGLE_API_KEY=\u0026#34;YOUR_API_KEY_HERE\u0026#34; Vertex AI: GOOGLE_CLOUD_PROJECT=\u0026#34;your-gcp-project-id\u0026#34; GOOGLE_CLOUD_LOCATION=\u0026#34;your-region\u0026#34; # 例: us-central1 GOOGLE_GENAI_USE_VERTEXAI=\u0026#34;True\u0026#34; (Vertex AI 利用時は事前の API 有効化、gcloud auth login が必要) 今回は Vertex AI を使用しました。\n4. エージェントを実行してみる (Web UIでの基本動作確認) # まずはエージェントがちゃんと動くか、Web UI で確認します。プロジェクトルート (adk-work) で以下を実行。\nBash\nuv run adk web ブラウザで http://localhost:8000 にアクセスし、エージェント multi_tool_agent を選択します。\nチャットで「ニューヨークの天気は？」などと尋ねると、エージェントがツールを使って応答してくれるはずです。右側のページで詳細な動作（LLM とのやり取り、ツール実行）も確認できます。これでエージェントが基本動作することは確認できました。\n5. エージェント評価の必要性 # 手動での動作確認も大事ですが、エージェントが複雑になったり、修正を繰り返したりすると、「毎回同じテストを手動でやるのは大変」「意図しないところで動作が変わっていないか心配」となります。ここで ADK の評価機能の出番です。\nADK ドキュメントによると、LLM エージェント評価では「最終出力」だけでなく、そこに至る「軌跡（ツールの使い方など）」も重要であり、これを自動でチェックできる仕組みが提供されています。\n6. 評価シナリオ (評価セット) の準備 # 評価のために、「お手本」となるシナリオと期待される動作を定義した評価セットファイル (.evalset.json) を用意します。ファイル名は任意（例: multi_tool_agent_test.evalset.json）です。Web UI で対話したセッションを保存して生成することも、手動で作成することも可能です。 以下のファイルでは、期待されるツール使用と参照応答 (reference) を定義しています。\n(ファイル名例: multi_tool_agent_test.evalset.json)\nJSON\n[ { \u0026#34;name\u0026#34;: \u0026#34;test\u0026#34;, \u0026#34;data\u0026#34;: [ { \u0026#34;query\u0026#34;: \u0026#34;おはよう！\u0026#34;, \u0026#34;expected_tool_use\u0026#34;: [], \u0026#34;reference\u0026#34;: \u0026#34;おはようございます！お手伝いできることはありますか？\u0026#34; }, { \u0026#34;query\u0026#34;: \u0026#34;ニューヨークの現在の時間は？\u0026#34;, \u0026#34;expected_tool_use\u0026#34;: [ { \u0026#34;tool_name\u0026#34;: \u0026#34;get_current_time\u0026#34;, \u0026#34;tool_input\u0026#34;: { \u0026#34;city\u0026#34;: \u0026#34;New York\u0026#34; } } ], \u0026#34;reference\u0026#34;: \u0026#34;ニューヨークの現在時刻は...です。...\u0026#34; }, { \u0026#34;query\u0026#34;: \u0026#34;ニューヨークの天気は？\u0026#34;, \u0026#34;expected_tool_use\u0026#34;: [ { \u0026#34;tool_name\u0026#34;: \u0026#34;get_weather\u0026#34;, \u0026#34;tool_input\u0026#34;: { \u0026#34;city\u0026#34;: \u0026#34;New York\u0026#34; } } ], \u0026#34;reference\u0026#34;: \u0026#34;ニューヨークの天気は晴れです。...\u0026#34; } ], \u0026#34;initial_session\u0026#34;: { \u0026#34;state\u0026#34;: {}, \u0026#34;app_name\u0026#34;: \u0026#34;multi_tool_agent\u0026#34;, \u0026#34;user_id\u0026#34;: \u0026#34;user\u0026#34; } } ] 7. 評価の基準 (ADK ドキュメントより) # ADK はエージェントの実際の動作と評価セットの期待値を比較する際、以下の基準（メトリクス）とデフォルトの閾値を使用します。\ntool_trajectory_avg_score: ツール使用履歴の一致度。デフォルト閾値 1.0 (完全一致)。 response_match_score: 最終応答と reference の類似度。デフォルト閾値 0.8 (ある程度似ていればOK)。 ちなみに、これらのデフォルト閾値は、必要であれば 以下のようなtest_config.json という設定ファイルを作成して各エージェントディレクトリ配下に置くことでカスタマイズすることも可能です。 { \u0026#34;criteria\u0026#34;: { \u0026#34;tool_trajectory_avg_score\u0026#34;: 1.0, \u0026#34;response_match_score\u0026#34;: 0.8 } } 8. Web UI で評価を実行！ # 準備ができたので、Web UI で評価を実行します。\n(もし adk web が停止していれば再起動) Web UI で multi_tool_agent を選択。 3. 右側の「評価 (Eval)」タブを開く。 4. 「評価セット (Eval Set)」ドロップダウンから、自分で準備した評価セットファイル（multi_tool_agent_test）を選択。\n5. 評価ケース test が表示されるのでチェックを入れて、「Run Evaluation」ボタンをクリック！\n実行後、結果が表示されます。\n今回は無事「PASS」しました！\nまた、ターミナルを見てみると詳細が出力されると思います。\n（※注意：応答一致 (response_match_score): 筆者が実験をしてみると、評価セットの reference に明らかに正しくない応答を設定し、さらに test_config.json で 値を明示的に指定しても、評価全体の結果は1となり「PASS」のままでした。）\nAgent Development Kit の評価まとめ # 今回は、ADK のセットアップからエージェント作成、そして評価機能を使ったテストまでを一通り体験しました。評価セットによる自動評価は、特にエージェントのツール利用手順（軌跡）が期待通りかを確認する上で有効だと感じました (tool_trajectory_avg_score は意図通り機能しているようです)。これにより、回帰テストの一部は自動化できそうです。 一方で、最終応答のテキスト内容 (reference) との比較 (response_match_score) については、今回試した範囲では、期待通りに最終的な評価結果 (PASS/FAIL) に影響を与えない場面がありました。 明らかに異なる応答を期待値としても、評価全体が PASS してしまうケースがあり、test_config.json で基準を明示的に設定しても結果は変わりませんでした。この挙動については、さらなる調査や ADK の今後のバージョンでの改善が必要かもしれません。 このように、現状では特に応答内容の自動評価に関しては注意が必要ですが、ツール利用の正確性を担保する目的など、部分的には ADK の評価機能は開発の助けになる可能性はあります。ADK の評価機能を利用する際は、現状の挙動をよく理解・検証した上で、目的に合わせて活用するのが良さそうです。皆さんもぜひ試してみてください！ ","date":"2025年4月11日","externalUrl":null,"permalink":"/posts/2025-04-11-agent-development-kit-adk-%E3%81%AE%E3%82%A8%E3%83%BC%E3%82%B8%E3%82%A7%E3%83%B3%E3%83%88%E8%A9%95%E4%BE%A1%E3%82%92%E8%A9%A6%E3%81%97%E3%81%A6%E3%81%BF%E3%81%9F/","section":"Posts","summary":"最近話題の Google 製 AI エージェントフレームワーク「Agent Development Kit (ADK)」を触ってみました！ Gemini モデルとの連携がしやすく、柔軟なエージェント開発が可能とのことで、期待が高まります。エージェントが自律的にツールを使うのは凄いですが、ちゃんと意図通り動くか、修正で壊れないかを確認する「評価」も重要ですよね。\n","title":"Agent Development Kit (ADK) のエージェント評価を試してみた！","type":"posts"},{"content":"更新日：2025年5月8日\n2025年4月9日にLangfuseのTownhall が開かれ、そこで直近のメジャーリリースと今後の予定について発表がされました。Langfuseのアップデートについてその速度と進化をシェアすべく、主な内容をまとめてみました！\nオリジナル動画はこちら Langfuse Q1 2025 アップデートまとめ # # 特にTrace におけるグラフ表示やJavaクライアントのサポート、またプロンプト管理などは待望の機能となっています！\nTrace のUIが刷新され大幅に改善:\nLangGraph などのエージェントフレームワークのグラフ表示、デバッグ情報の非表示オプション、統合されたタイムライン、トレースプレビュー機能の追加とキーボードナビゲーションが可能になりました。 https://langfuse.com/changelog/2025-02-14-trace-graph-view ビヨンビヨン動いて楽しいのと、どこで何が起きてるのか非常に追いやすい。 ネイティブ環境サポート (Environmentの設定) が追加:\n環境に基づいたトレースとプロンプトの分離とUIでの選択が可能になりました。\nOpenTelemetryエンドポイントが導入:\n様々なフレームワークや技術スタックとの互換性が大幅に向上しました。またJavaクライアントがリリースされ、JavaアプリケーションでのLangfuseの利用が可能になりました。\nプロンプト管理:\nUIの刷新、コードスニペットのコピー機能の改善、プロンプトの合成機能、保護されたプロンプトラベル (adminなどだけが変更できるラベル)、変数リンティング機能付きの新しいコードエディタ、コミットメッセージの追加、Langfuseプロンプト用のMCP（Model Context Protocol）サーバーのリリースが行われました。\nプレイグラウンド:\nツール呼び出しと構造化出力がサポートされました。\nセルフホスティング:\nバージョン1のHelm chartのリリースと、AWS用のTerraformリポジトリの公開が行われました。\nUIの改善:\nオンボーディング画面の追加とコマンドパレットによるナビゲーションの改善が行われました。\n評価機能:\n履歴データに対するElement Judge評価の実行、評価プロンプトへのデータ挿入をより具体的に指定するためのJSONパスのサポート、CSVファイルによるデータセットのアップロードが可能になりました。\nAPIの更新:\n新しいAPIリファレンスの公開、アノテーションキューとスコア構成用の新しいAPI、Python、JavaScript、Java用の型付きクライアントの追加が行われました。\nセキュリティ機能:\nUIおよびAPIによる単一またはバッチでのトレース削除、プロジェクトレベルでのデータ保持ポリシーの設定、Audit logの利用が可能になりました。\nLangfuse Q2 2025 ロードマップまとめ # Tracing:\nFull text検索の初期デモが近日公開予定であり、最優先事項の一つです。 Analytics:\nデータに基づいて独自のビューを作成できるカスタマイズ可能なダッシュボードと、アプリケーション内でのチャート表示やデータ集計に利用できるクエリAPIが近々提供されます。 OpenTelemetry:\nLangfuse SDKの内部をOpenTelemetryネイティブに移行する作業が進められています。 評価 (Evaluation):\nセッションレベルの評価の改善、コアな評価ビューの改善、データセット実験のアノテーションの簡略化が予定されています。 管理API (Admin API):\n組織とプロジェクトのプロビジョニングと管理を可能にするAdmin SCIM APIのドラフトがあります。 セルフホスティング:\nセルフホスティングの開始方法に関するドキュメントが来週再リリースされ、スケーリングに関するガイダンスも提供されます。 エージェント:\nエージェントグラフの汎用化、ツール呼び出しやツール検出に関する可視化オプションの追加、より意見の強いエージェント評価機能の構築が計画されています。 UI:\nテーブルのUI改善が予定されています。 プロンプト管理:\nプレースホルダーの導入、本番環境のトレースにおけるプロンプト変数の追跡、Elementによるプロンプトエンジニアリング支援、フォルダによるプロンプト整理機能、ABテスト機能、ツール呼び出しと構造化出力のネイティブサポートが計画されています。 データプラットフォーム:\nWebhookが追加され、Langfuse内の変更に基づいて外部アクションをトリガーできるようになります。また、アラート機能も検討されています。 プロンプト実験とプレイグラウンド:\nプロンプトプレイグラウンドへの分割ビューの追加、プロンプト実験へのツールと構造化出力のサポートの拡充が計画されています。 Langfuse Cloud:\nHIPAAコンプライアンスへの対応が検討されています。また、使用量に基づく請求アラート機能が追加される予定です。 設定可能なダッシュボード:\n最も要望の多かった機能の一つである設定可能なダッシュボードの早期ベータ版がLangfuse Cloudでリリースされました。 これらのアップデートは、有償版ユーザーとコミュニティからのフィードバックに基づいて優先順位付けされており、GitHub Discussionsでの議論から多くの機能が実装されています。\n気になる内容などあれば、お気軽にGithub あるいは GAO までお問い合わせください ！\n","date":"2025年4月10日","externalUrl":null,"permalink":"/posts/2025-04-10-langfuse-q1-%E3%82%A2%E3%83%83%E3%83%97%E3%83%87%E3%83%BC%E3%83%88%E3%81%A8-q2-%E3%83%AD%E3%83%BC%E3%83%89%E3%83%9E%E3%83%83%E3%83%97%E3%81%AE%E3%81%BE%E3%81%A8%E3%82%81/","section":"Posts","summary":"更新日：2025年5月8日\n2025年4月9日にLangfuseのTownhall が開かれ、そこで直近のメジャーリリースと今後の予定について発表がされました。Langfuseのアップデートについてその速度と進化をシェアすべく、主な内容をまとめてみました！\n","title":"Langfuse Q1 アップデートと Q2 ロードマップのまとめ","type":"posts"},{"content":"","date":"2025年4月10日","externalUrl":null,"permalink":"/tags/%E3%82%A2%E3%83%83%E3%83%97%E3%83%87%E3%83%BC%E3%83%88/","section":"タグ","summary":"","title":"アップデート","type":"tags"},{"content":"","date":"2025年4月10日","externalUrl":null,"permalink":"/tags/%E3%83%AD%E3%83%BC%E3%83%89%E3%83%9E%E3%83%83%E3%83%97/","section":"タグ","summary":"","title":"ロードマップ","type":"tags"},{"content":"更新日：2025年4月25日\nLLMOps とは？ # LLMOps（Large Language Model Operations）とは、大規模言語モデル（LLM）を利用した生成AIアプリケーションの開発から運用、改善までを一貫して管理するための考え方や仕組み（フレームワーク）です。多くの企業では、自社でモデルをゼロから構築するのではなく、OpenAI、Google、Anthropic などが提供する基盤モデルを活用し、プロンプト設計やファインチューニング（微調整）を通じて目的に合った生成AIアプリケーションを開発しています。LLMOpsは、こうした開発・運用プロセスを効率化し、品質管理やガバナンスを実現する上で重要な役割を果たします。\nLLMOps のメリットとユースケース # LLMOps は「便利ツール」ではなく、品質・コスト・リスクを同時に最適化する運用メソドロジーです。以下はその主なメリットの例です。\n開発スピードの加速 プロンプト管理、モデル、確立されたテスト手法により、開発効率の向上 (プロンプト管理に関しては こちら のブログを参照） 品質の継続的改善 LLM as a Judge、 ユーザーフィードバック、 Human Annotationなどをワンループで運用し、リリース後の精度向上を仕組み化\nコスト最適化 Trace単位でトークン数と請求額を可視化、高コスト箇所を改善\nガバナンスと監査対応 全入出力・モデルバージョン・評価結果を一元ログ化し、生成物の説明責任 (Explainability) を担保、有事の際におけるトレーサビリティの確保\nチーム横断コラボレーション データサイエンティスト／エンジニア／業務担当が 共通ダッシュボード で指標を共有し、改善案を合意形成しやすい\nスケールと未来対応力 新モデル登場やワークロード拡大時も、同じパイプラインにプラグインするだけで リスクなく横展開\nROI の可視化 Trace と KPI を結びつけ、「生成 AI がビジネス成果を生んでいるか」 を定量的に証明\nまたユースケースとしては、以下のようなものが想定されます。\n生成AI導入によるカスタマーサポート平均対応時間の削減\n生成AIの処理を可視化し、回答を継続的に評価、改善することで人件費削減 あわせて顧客の待ち時間も削減し、顧客満足度を向上 法務ドキュメントレビュー の時間削減\n一次評価を全て生成AIで実現しつつも、発生するエッジケースに対して、Trace から LLM as a Judge による誤りケースを検出し、専門家レビューによる改善 パーソナライズド広告コピー生成クリック率 の向上\n効果的なプロンプト をTraceのTagで判別し、A/Bテストを実施し、プロンプトを改善しつつ、効果的なものを瞬時に本番適用 LLMOps を導入せず当初の仮説だけでリリースした場合、こうした効果は期待できず、ビジネスにおける ROI も限定的なものとなってしまう恐れがあります。\nMLOps と LLMOps の違い # 従来のMLOps が一般的な機械学習モデルの開発と改善を対象とするのに対し、LLMOps はLLM を活用した生成AIアプリケーションやエージェント特有の課題に対応するためのものです。その主な特徴として運用プロセスにおいてモデルの出力に対するユーザーからのフィードバック収集や自動的な評価の実行結果などを改善に活用することが重視されます。これらの結果がプロンプトや自動評価自体の改善、モデル選定や評価データセットの構築などに活用されることになります (具体的なプロセスは後述）。\nLLMOps とGenAIOps の違いと関係性 # LLMOps と似た用語にGenAIOps（Generative AI Operations）があります。GenAIOps は、テキストだけでなく画像、音声、動画などの生成モデルを含む、あらゆる生成AIを対象とした、より広範な概念です。一方、LLMOps は生成AIの中でも特に「大規模言語モデル（LLM）」を利用した言語生成にフォーカスしています。つまり、LLMOps はGenAIOps の一部分（サブセット）と位置づけられ、特に言語生成に特化したプロセスが重視されます。\nLLMOps は GenAIOps の一部 生成AIアプリケーションのデプロイ、その後の課題 # IT部門やDX部門などにより開発・導入された生成AIアプリケーションが、通常のシステムと同様に最初から100% の品質でユーザニーズを満たすことはまずありません。そして、アプリケーションの使われ方、ユーザーフィードバックの獲得、定量的な評価などが何もない状況であれば、当然その状況は永遠に改善されません。結果として経営層がそのプロジェクトにROI を見出せなければ継続することが難しくなるかもしれません。\n生成AI は一見すると正しいように見える出力をする特性があり、出力結果はそのまま使えないが必ずしも全てが間違えているとも言い切れず、残念ながらユーザはモヤッとした不満を心に抱えることになります。その状況で仮にIT部門がアンケートを定期的にとったところで、低い解答率で曖昧な回答を受け取ることになり、具体的な改善に繋げることは難しいでしょう。\nこのような問題を解決するために必要な手法が LLMOps です。\nLLMOps に必要な構成要素 # 生成AIを利用したアプリケーションやエージェントの継続的な改善を実現させるためのLLMOps には以下のような構成要素が含まれます。\nTracing ユーザ入力とモデルの出力 (Trace)の取得および保管、および処理内容の可視化\nプロンプト管理 生成結果の品質向上のためのプロンプト修正、テスト、評価など\nユーザ フィードバックの取得 ユーザがどの処理に対して、どのような評価をしたのかを具体的に収集・管理\nHuman Annotation 業務の専門家から見て、適切な回答をしているのかどうかの評価\n自動評価 LLM as a Judge などを活用して、3や4の効率化・一次評価に利用\nDataset管理 ユーザの実際の入力などをデータセットとして管理し、テスト資材などのために管理\n利用傾向の把握と分析 コスト, プロンプト, モデル, ユーザ, アプリケーションなどの様々な観点で、どのような傾向があるのかを分析\n生成AIアプリケーションはこれらのプロセスを通じて継続的に改善し続けることで、初めてビジネスに価値を生み出すことが可能となります。続いて、この中から重要な要素であるTracingと評価 (ユーザーフィードバック, Human Annotation, 自動評価) に焦点をあてて、説明をしていきます。\nTracing - LLM アプリの“挙動”を可視化するバックボーン # Tracing (または Trace ) はユーザーのインプットと LLM のアウトプットをまとめ、どのモデルを使い・何を参照し・何秒かかり・いくらコストが発生し・どんな結果を返したかなどを時系列で残す仕組みです。このTraceを軸に評価 (Evaluation) やデバッグなどが行われることから、LLMOps におけるバックボーンと言えるでしょう。以下はTracing によって得られる一般的なパターンです。\nビジネス課題 Tracing が解決すること 例 品質管理 失敗した Generation をクリックし、入力→Retrieval→出力を時系列で確認 “Hallucination が起きたのは Retrieval が空集合だった”と即判明 コスト抑制 Trace 単位でトークン数と課金額を自動記録 月次レポートで高コスト プロンプトを可視化し、短縮を提案 SLA／レイテンシ Span 間のタイムスタンプでボトルネックを特定 「埋め込み検索が 600 ms → Pinecone にインデックス分割」で改善 A/B テスト 異なるプロンプトを与えたTrace毎に結果を比較 プロンプト v2 の CS 工数削減率が +12 % を確認 Tracing は LLM アプリの全イベントを “1 本のストーリー” に束ねることで、\n品質・コスト・パフォーマンスをワンクリックで照会 評価（LLM as a Judge、Human Annotation、User Feedback）との橋渡し プロンプトやパラメータが与える影響の確認 監査／セキュリティ要件への対応 と言ったLLMOps の道筋を作る重要な役割、最初の一歩の役割を持ちます。\nこれにより Tracing → 評価 (Evaluation) → 改善 のループが完成し、GenAIOps／LLMOps の継続的改善が“仕組み”として回り始めます。\n生成AI においては評価が鍵 # Tracing がされている前提で、LLMOps の構成要素の中で、特に重要なのは \u0026ldquo;評価\u0026rdquo; です。\n生成 AI アプリケーションは、モデルが生成したアウトプットが実際にユーザーの課題解決に役立っているかを常に確認し、改善し続けることでなければ ROI を証明できません。\n特に GenAIOps／LLMOps では、\nオンライン指標（ユーザー行動） オフライン指標（定義済みデータセットによるテスト） モデル／人による定性評価 これら3つを組み合わせ、素早くループを回すことが成功の分水嶺になります。\n1. ユーザーフィードバック：最も信頼できるオンライン評価 # 評価例 実装方法 オンライン 👍/👎、5 段階評価、自由コメント UI コンポーネントはできる限りワンクリックで完結させる オフライン クリック率、編集率、滞在時間 既存の分析基盤と Trace ID をひも付ける オフライン CS 工数削減、処理時間短縮 業務システム側で自動計測 → Langfuse の score API に送信 Langfuse では数値・カテゴリ・真偽値いずれのフィードバックも Trace 単位で保存、Exportすることが可能です。またダッシュボード上でスコア分布も即時確認できます。\n2. Human Annotation：ドメイン知識を注入する評価の“最後の砦” # Langfuse には以下の図のような極めて分かりやすい Human Annotation 専用の UI があり、次項で説明する自動評価でスコアが悪い Trace を Queue に入れ、Queueにいるそれらを簡単にドメインエキスパート (業務の専門家) が評価することができます。\n実際の INPUT と OUTPUT をみて、手動でカテゴリや点数などで専門家が判定（評価軸は独自設定が可能） 3. 自動評価──“LLM as a Judge” で 指標 を ビジネス価値 に変換する # # BLEU／Rouge のような機械翻訳系メトリクスは「表層的一致」を測るには有効ですが、\n本当にUX に寄与しているのか 業務 KPI（例：チケット処理時間、法令順守率）に直結しているのか という問いには答えてくれません。\nここで鍵になるのが LLM as a Judge――LLM 自身を “審査員 (Evaluator) ” として使い、独自プロンプト でタスク固有の評価軸を採点させる手法です。\nなぜ「独自プロンプト評価」が効くのか？ # 業務コンテキストを埋め込める 業界用語や必須の条文のチェックなど、業務独自の内容を反映可能\n評価基準の透明性 プロンプト の中に基準を明文化 することで、 ドメインエキスパートやステークホルダーも理解がしやすい\n定量化が難しい基準も自然言語で入れることができる ビジネスに相応しいか、似合っているか、多様か \u0026hellip; などの基準を作ることが難しい内容もプロンプトが可能（精緻化した方が効果は高くなることがあります）\nLangfuse は最初から評価用テンプレートも用意されており、そこにさらに独自の基準を日本語で追加することができます。こうした自然言語で半コード化された評価基盤 を「作る→回す→直す」のループに緊密に組み込み、Traceに紐づけることで、改善対象を明確にすることで、改善を具体化・高速化させることが可能になります。\n「メトリクスでは不十分な評価も、LLM 使うことで “自社専属の 一次QA チーム” を構築」\n── Langfuse はその最短ルートもあわせて提供します。\nまとめ｜LLMOpsはLLM活用の鍵 # LLMOps は、LLMをビジネスに導入し、その価値を継続的に生み出すために不可欠な運用手法です。特に、モデルプロバイダが提供する強力な基盤モデルの活用が一般的となった現在、LLMOps なしでの効果的な運用は困難と言えるでしょう。\nその中でも特に、評価が改善の鍵となり、Langfuse は世界中で使われている便利なUIや機能を最初から備えています。ご興味がある方は、あわせてご確認ください。\n","date":"2025年3月27日","externalUrl":null,"permalink":"/posts/2025-03-27-llmops%E3%81%A8%E3%81%AF-mlops%E3%81%A8%E3%81%AE%E9%81%95%E3%81%84%E3%82%84%E7%94%9F%E6%88%90ai%E3%81%AE%E8%A9%95%E4%BE%A1%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6%E8%A7%A3%E8%AA%AC/","section":"Posts","summary":"更新日：2025年4月25日\nLLMOps とは？ # LLMOps（Large Language Model Operations）とは、大規模言語モデル（LLM）を利用した生成AIアプリケーションの開発から運用、改善までを一貫して管理するための考え方や仕組み（フレームワーク）です。多くの企業では、自社でモデルをゼロから構築するのではなく、OpenAI、Google、Anthropic などが提供する基盤モデルを活用し、プロンプト設計やファインチューニング（微調整）を通じて目的に合った生成AIアプリケーションを開発しています。LLMOpsは、こうした開発・運用プロセスを効率化し、品質管理やガバナンスを実現する上で重要な役割を果たします。\n","title":"LLMOpsとは? MLOpsとの違いや生成AIの評価について解説","type":"posts"},{"content":"","date":"2025年3月27日","externalUrl":null,"permalink":"/tags/mlops/","section":"タグ","summary":"","title":"MLOps","type":"tags"},{"content":"Langfuse v3 になって、「Self-hosting は良さそうだけど、結構インストール面倒なんでしょう？」という声をよく聞きます。もちろん複雑な構成を作ることも可能ではありますが、一方でLangfuse には簡単なものもふくめて幾つかのインストール方法が用意されており、利用用途や条件に合わせた選択が可能です。\nこの記事では、各インストールオプションおよび公式ページのリンク、使い分けについて解説します。まず簡単なケースごとのフローチャートは以下のような形になります。当てはまりそうなオプションを中心にぜひご確認ください。\nインストールオプションのパターン 実際のインストールコマンドは、各オプションのリンクから公式をご参照ください。\nLangfuse のインストールオプション # 1. ローカルインストール # Docker と docker compose を利用したローカル環境でのセットアップ方法です。\n特筆すべきものはなく、非常にシンプルな手順で利用可能なもので、個人利用や簡単なテスト環境向きです。Mac やWindows でDocker をインストールしたら、docker compose upコマンド一つで起動します。特に悩むところもないです。\n使用するケース 手元のアプリ開発の可視化やテスト目的で手軽にLangfuse を試してみたい このLangfuse を本番環境に使う予定はない 2. Docker Composeを使ったインストール # 公式ドキュメントとしては区別されていますが、1と基本的には一緒です。ただしAWS/Google Cloud/Azure などで仮想マシン (EC2 など) をつかうことが想定されており、公式ページにはdockerパッケージの導入手順などについても記載してあります\n大規模なトラフィックがなく、可用性が許容されるのであれば本番環境にも適用可能です。また一部にマネージドサービスを使うといった方法で可用性も向上できます。\n使用するケース\n小〜中規模のチームでの本番やステージング環境を構築したい Trace などの欠損リスクがある程度は許容できる VM の構築や管理ができる人がいる インフラ構築をシンプルに管理したい 必要なリソース\n4Core 16GiB メモリなどが推奨 (それ以下でも立ち上がりはする) 100GiB ストレージ, 外部から接続できるNW設定 (Public IPなど) 本番利用に向けて、可用性を高める応用例\nObject storage はVMではなくS3 / blob /GCS などに変更 プロンプト取得の可用性を高めるため、Web server のコンテナとPostgreSQL だけをマネージドサービス利用する またdocker-compose.yml を修正することで、ディスクやデータベースの接続など各種設定を変更することもできます。langfuse ディレクトリ直下にありますので必要に応じてご確認ください。\n3. Dockerを使ったインストール # Docker composeではなくDocker コンテナを使ってカスタマイズした構成を作るパターンです。利点は複数のコンポーネントで構成されているLangfuse をクラウドサービス側のマネージドサービスを利用して、高い可用性を保つことができることなどにあります。\nPostgreSQL, Redis, Clickhouse をマネージドサービスで起動しておいて、docker コマンドの引数として指定しましょう。\n使用するケース\nKubernetes は使いたくないが、VM単体よりも可用性を高く保ちたい 一方でクラウドサービスのマネージドサービスを使って、手はかけたくない クラウドサービスのネットワークなどを構築・管理できる 改善したい可用性と構成例\nプロンプト取得における可用性を向上させたい\nWeb server のコンテナをECS やCloudRun で稼働 PostgreSQL をAurora や CloudSQL で稼働 Traceデータの可用性を高めたい\nObject Strage にS3, GCS などを活用 Clickhouse としてClickhouse Cloud などを使う、あるいはECSや GCE container optimized OS 上で動かす Async worker のコンテナをECS やCloud Run などで稼働 RedisにElasticashe や Memorystore などを活用する 最近ではTeraform, CDK, あるいはマニュアル などで比較的簡単 (あるいはほぼ自動) で構築できるようにリソースを公開しているものもあります。ご自身のケースに当てはまるかもご確認ください。\n参考リンク AWS tuboneさん (Blog ) リソース https://github.com/tubone24/langfuse-v3-terraform mazyu36さん (Blog ) リソース https://github.com/mazyu36/langfuse-with-aws-cdk Google Cloud クラウドエース株式会社 高木さん (Blog ) マニュアルでのインストール手順 4. Kubernetes Helmを使ったインストール # Kubernetes クラスタにHelm を使ってLangfuseを導入する方法です。可用性やスケーラビリティを確保しつつ、運用をKubernetesに任せたいケースに利用可能です。\n使用するケース 比較的大規模なチームやエンタープライズ環境 高い可用性とスケーラビリティを求める Kubernetesクラスタを運用することができる なお、Langfuse 公式で Helm Chart がリリースされており、特に理由がなければこれを使うことが最初の入口として良いと思います。その上で、さらに可用性を向上させたい場合には以下のオプションなどが有益です。\nTraceを格納するClickhouse自体の可用性や保守性などを高めたい\nコストや諸条件が合えばClickhouse Cloud など利用を検討する\nプロンプト取得における可用性を向上させたい\nPostgreSQL をAurora や CloudSQL などで稼働\n以下の記事では、Langfuse をKubernetes + マネージドサービスの構成でデプロイをした構成について言及しています。\n参考リンク Google Cloud GAO 遠矢 (Blog ) 本記事がインストール方法の決定における参考になれば幸いです。\nもし企業における導入サポートなどが必要でしたら、 お気軽にお問い合わせください。\nhttps://www.gao-ai.com/contact ","date":"2025年3月8日","externalUrl":null,"permalink":"/posts/2025-03-08-langfuse-%E3%81%AE-self-hosted-%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%81%AE%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3%E3%81%A8%E8%A7%A3%E8%AA%AC/","section":"Posts","summary":"Langfuse v3 になって、「Self-hosting は良さそうだけど、結構インストール面倒なんでしょう？」という声をよく聞きます。もちろん複雑な構成を作ることも可能ではありますが、一方でLangfuse には簡単なものもふくめて幾つかのインストール方法が用意されており、利用用途や条件に合わせた選択が可能です。\n","title":"Langfuse の Self-hosted インストールのパターンと解説","type":"posts"},{"content":"更新日：2025年5月3日\n本記事は、LLM（大規模言語モデル）アプリケーション開発プラットフォームであるLangfuse と LangSmith を比較するものです。両プラットフォームは、開発者が LLM を活用したアプリケーションを構築・運用することを支援しますが、その出自、焦点、実装において違いがあります。各セクションで、主要な基準で両者を比較し、対比を行なっていきます。なお内容は 2025年2月26日時点においての公開情報をもとに作成されております。\nまたセルフホスト版における費用比較は LangSmith 側の Web 上などに公開情報がないため、対象外となっています。\nTL;DR (2025年04月25日 修正) # 多くの相違点と類似点がありますが、最終的に論点は以下に点に収束します。\nぜひ皆様のユースケースに照らし合わせてご確認ください。\nOSS が持つガバナンスや今後のロードマップなどの透明性 (Langfuse) or 従来型のProprietary (LangSmith) という観点からの Pros/Cons の検討 自分の環境内に立てたいか (セキュリティポリシー的の観点) or SaaS 厳密なコスト計算シナリオ (ただし SaaS の場合は大きく変わらない) LangChain との緊密なインテグレーションをどの程度重要視するか 1. 基本概要 (Basic Overview) # Langfuse: # 開発元: Langfuse GmbH (Finto Technologies) 概要: オープンソースのLLMアプリケーションエンジニアリングプラットフォーム。 主な目的: チームがLLMアプリケーションを「共同でデバッグ、分析、反復」することを支援すること。開発ワークフローを加速するために、すべての機能が統合されています。 特徴: 特定のフレームワークに依存しない。 本番環境での利用を想定して構築。 オープンソースであるため、高い柔軟性とカスタマイズ性を持つ。 LangSmith: # 開発元: LangChainチーム 概要: プロプライエタリ（閉鎖的）なエンドツーエンドのプラットフォーム。 主な目的: LLMアプリケーションのライフサイクルのあらゆる段階に対応する、オールインワンの開発者プラットフォームを提供すること。 特徴: 特にLangChainユーザーを対象としているが、あらゆるLLMアプリケーションで利用可能。 綿密な監視と評価に重点を置き、開発者が信頼性の高いアプリケーションを迅速に提供できるよう支援。 SaaS (Software as a Service) として提供される (エンタープライズ向けのセルフホストオプションあり)。 LangChainエコシステムと緊密に統合されている。 要約: Langfuse (Langfuse GmbH) はオープンソースでフレームワーク非依存。LangSmith (LangChain) はLangChainユーザー向けに設計されたプロプライエタリなエンドツーエンドプラットフォームですが、あらゆるLLMアプリケーションで利用可能です。どちらも、LLMアプリケーションの開発/監視/運用の効率化を目指しています。\n2. 主要機能 (Main Features) # 両プラットフォームは、トレース、デバッグ、プロンプト管理、評価、フィードバック収集など、豊富な機能セットを提供します。以下の表では、主要な機能を比較し、実装や重点の違いを強調します。\n機能 Langfuse LangSmith トレース \u0026amp; デバッグ LLM可観測性: 完全な実行トレース (LLM呼び出し、ツール呼び出しなど) を完全なコンテキストとともにキャプチャ。ネストされたチェーン、並列呼び出し、マルチモーダルデータをサポート。複数ターンの会話のためのセッションビューと、レイテンシをデバッグするためのタイムラインビューを提供。開発者は、LangfuseのSDKまたは統合を介してコードを計測し、各ステップをログに記録。 可観測性 \u0026amp; トレース: LangChain/LangGraphアプリ用の組み込みトレース (自動計測) と、あらゆるアプリ用のAPI/SDKを提供。カスタムダッシュボードとアラートでトレースを分析可能。呼び出しシーケンスを完全に可視化し、エラーやボトルネックをリアルタイムで特定。トレースをリンクで共有して共同作業を可能にし、2つのトレース実行を並べて比較することも可能 (回帰デバッグ用)。 プロンプト管理 中央集約型プロンプト管理: プロンプトを共同で作成、編集、バージョン管理するためのコンソールを提供。プロンプトはコードから分離されており、新しいバージョンはアプリを再デプロイすることなくデプロイ可能。複数形式のプロンプト (テキストまたはチャット) をサポートし、バージョン管理とロールバック機能を備える。非開発者はUIを介してプロンプトを更新可能 (役割ベースのアクセス)。プロンプトの変更は経時的に追跡され、トレースにリンクされて影響を確認可能。また、Playground を提供し、プロンプトを単独で即座にテスト可能。 LangSmith Hub (プロンプトエンジニアリング): プロンプトを作成し、自動バージョン管理で反復するためのWeb UI。ユーザー (非エンジニアを含む) は、プロンプトテンプレートを作成し、入力変数を設定し、コメントや共同作業が可能。すべてのプロンプト編集は追跡され、ロールバックと比較が可能。統合された Prompt Playground により、デプロイ前にモデル (カスタムまたはOpenAI互換エンドポイントを含む) に対してプロンプトをテスト可能。LangSmithのプロンプトハブはチームの共同作業に重点を置いており、プロンプトはチームメンバー間で簡単に議論および共有可能。 評価 (自動化) LLM-as-a-Judge: 出力に対するLLMによる自動評価をサポート。Langfuseは、トレース (本番環境またはテストシナリオ) に対して「Evaluator」を実行し、スコアを生成可能。開発者は評価基準 (例: 正確性、関連性あるいは独自に基準) を定義し、モデルを使用して応答をスコアリング可能。これらのスコア (数値、ブール値、カテゴリ) は、トレースまたは特定のステップに付加。Langfuseは、カスタム評価指標をログに記録するためのAPI/SDKも提供。 AI支援評価: LangSmithは、関連性、正確性、有害性など、一般的な基準に対するカスタム評価器と 既製の評価器 の両方を提供。ユーザーは、LLMと提供されたプロンプトを使用して出力を自動的にスコアリングするか、事前に構築された評価モジュールを使用可能。LangSmithは 回帰テスト と オンライン評価 を重視しており、ライブ実行を継続的に評価し、品質の低下を検出可能。評価結果は各実行のメトリックとして保存され、品質がしきい値を下回った場合にアラートをトリガー可能。 フィードバック \u0026amp; アノテーション ユーザーフィードバック: Langfuseは、出力に関するエンドユーザーのフィードバックを収集し、トレースに付加することを可能にする。たとえば、ユーザーからのGood/Badなどを、Browser SDKまたはAPIを介して取り込むことが可能。すべてのフィードバックはトレースデータに付加されなり、低品質の結果を特定するために使用可能。 手動アノテーション: Langfuseには、内部レビュー担当者が出力にラベルを付けたり、手動でスコアリングしたりするための アノテーションキュー システムが含まれる。これは、トレースのサンプルに対してグラウンドトゥルースラベルを作成するのに役立つ。プラットフォームは、人間のアノテーターのワークフローをサポートし、自動スコアとともにその判断を追跡。 人間のフィードバック統合: LangSmithは、任意の実行に対して 「人間のフィードバックを重ね合わせる」 ことを可能にする。チームは、LangSmithアプリで アノテーションキュー を設定し、人間にモデル出力をレビューおよびラベル付けさせることが可能。これは、体系的な評価やファインチューニングデータの収集に役立つ。エンドユーザーのフィードバックもキャプチャ可能 (たとえば、アプリケーションがユーザーに応答を評価するように求める場合)。すべてのフィードバックはトレースとともにログに記録され、ダッシュボードで使用したり、メトリックを計算したりするために使用可能。LangSmithのUIはアノテーション用に統合されており、割り当てられたレビュー担当者はプラットフォーム内で直接ラベル付け可能。 データセット \u0026amp; テスト データセット \u0026amp; 実験: Langfuseでは、実際の例 (特に失敗例や重要なクエリ) からデータセットを構築可能。これらのデータセットは、テストスイートまたはベンチマークとして機能。これらのデータセットに対して プロンプト実験 またはチェーン実験を実行可能。たとえば、同じ入力セットに対して異なるプロンプトバージョンまたはモデル設定を並べて比較。プラットフォームは、各実験のメトリック (精度、コスト、レイテンシ、またはカスタムスコア) を表示し、最適化をガイド。これは、デプロイ前のテスト (新しいバージョンを出荷する前の回帰テスト) と、本番環境の外れ値を定期的にテストセットに追加することによる 継続的な改善 をサポート。 データセット \u0026amp; 評価スイート: LangSmithは、デバッグ実行またはアップロードされたデータから データセットを構築 するためのツールを提供。これらのデータセットは、体系的なテストのために、入力と期待される出力 (または参照出力) をグループ化。LLMベースの評価器またはカスタムテスト関数のいずれかを使用して、データセットに対して バッチ評価 を実行可能。プラットフォームは 回帰テスト をサポートしており、同じデータセットでアプリケーションのパフォーマンスをバージョン間で比較。たとえば、プロンプトまたはモデルを更新した後、保存したデータセットを再実行し、評価スコアが向上したか低下したかを確認可能。これにより、各反復で品質が確実に向上。 監視 \u0026amp; メトリクス ダッシュボード: Langfuseには、品質スコア、レイテンシ、LLM呼び出しのコストなどの主要なメトリックを監視するための組み込みダッシュボードが含まれる。トレースがストリーミングされると、これらの集計メトリックを経時的に追跡可能 (例: 平均応答時間、成功率、ユーザーフィードバックのスコア)。Langfuseはこれらの洞察をアプリ内で提供することに重点を置いているが、ドキュメントにはカスタムアラートルールについては明示的に言及されていない。(必要に応じて、APIを介してデータをエクスポートし、外部監視を行うことは可能。) コスト追跡は、ユーザーごとまたは全体的なAPI使用料を監視するためにサポート。タグ、ユーザーID、またはバージョンによるトレースの詳細なフィルタリングがUIでサポートされており、特定のシナリオを掘り下げることが可能。 ダッシュボード \u0026amp; アラート: LangSmithは監視に重点を置いている。ユーザーは、1秒あたりのリクエスト数、エラー率、レイテンシ、コストなどのメトリックを経時的に表示するためのカスタム ダッシュボード を作成可能。プラットフォームでは、アラート/自動化ルール を設定可能。たとえば、メトリックがしきい値を超えた場合や、評価スコアが低下した場合に通知。LangSmithは オンライン評価監視 もサポートしており、ライブトラフィック (AIまたはメトリックを使用) を継続的に評価し、リアルタイムで問題をフラグ付け可能。組み込みのフィルタリングと検索は、トレースデータを (モデル、時間枠、タグなどによって) 分割するのに役立ち、インターフェースは調査のために異常 (エラーまたはレイテンシのスパイク) を強調表示。 共同作業 チームコラボレーション: オープンソースツールであるLangfuseは、セルフホストして複数のチームメンバーで使用可能。組織向けの役割ベースのアクセス制御 (RBAC) をサポート (上位プランで利用可能)。それぞれのOrganizationやプロジェクトといった単位を活用しながら、1つのインスタンスで管理できる。コラボレーションは主に、Langfuseダッシュボード、プロンプト、トレース、データセットなどへのの共有アクセスを通じて行われる。 共有 \u0026amp; コラボレーション: LangSmithはチームワークを念頭に置いて構築。複数のユーザーと組織に対してきめ細かい権限 (Plus/Enterpriseプラン) を提供。特に、トレース共有 は機能の1つであり、開発者はトレースへの公開共有可能なリンクを生成し、同僚や利害関係者に送信可能。このリンクを使用すると、他のユーザーは透明性のために実行チェーン全体を表示可能。LangSmith Hub は、プロンプト設計におけるコラボレーションも可能にし、チームメンバーはプロンプトにコメントでき、非技術的な専門家は直接貢献可能。要約すると、LangSmithはすぐに使えるより多くのアプリ内コラボレーションツール (コメント、共有リンク) を提供する一方、Langfuseは複数ユーザーアクセスと外部ディスカッション (またはサードパーティ統合) に依存してコラボレーションを行う。 機能における主な違い: 両プラットフォームは、エンドツーエンドのLLM開発サイクル (プロンプト設計から監視まで) をカバーしています。Langfuseはオープンソースであるため、セルフホスト環境での柔軟性と統合を重視しており、可観測性 と 評価パイプライン を中心に強力なコアを備えています。LangSmithはマネージドサービスとして、LangChainとの容易な統合、既製の評価器、カスタムダッシュボード/アラート、組み込みのコラボレーション (共有とコメント) などの ユーザーエクスペリエンス 機能に特に重点を置いています。LangChainを頻繁に使用する場合 (計測のオーバーヘッドがない) はLangSmithが優位に立つ可能性があり、一方、Langfuseはフレームワーク非依存のカスタマイズ可能なソリューションが必要な場合や、セルフホストのオープンソースを好む場合に優れています。\n3. 統合性 (Integration) # LangfuseとLangSmithはどちらも、さまざまなツール、ライブラリ、APIと統合して使いやすさを向上させています。以下に、統合機能の比較を示します。\n言語SDK \u0026amp; API: # 両者とも、複数の言語の公式SDKを備えたAPIファーストのアプローチを提供。 Langfuse: Python と TypeScript/JavaScript SDK、および適切に文書化されたREST APIを提供。 LangSmith: 同様に Python と JS/TS SDK、およびトレースをログに記録するためのREST APIを提供。 つまり、どちらのプラットフォームも、APIを呼び出すかSDKメソッドを使用することで、あらゆる環境 (バックエンド、フロントエンドなど) で使用できます。両者とも開発者に優しく、カスタムワークフローやLangChain以外のアプリケーションに統合できます。 フレームワーク \u0026amp; ツール統合: # Langfuse: フレームワーク非依存 の設計。 幅広いコミュニティ統合を持つ。 LangChain, LlamaIndex, Haystack, OpenAI SDK, Vercel AI SDK, LiteLLM, Langflow, Flowiseなど、一般的なLLMフレームワークやツールをすぐにサポート。 これらの統合は、多くの場合、Langfuseにデータを自動的に送信する計測モジュールまたはコールバックとして提供される。 例: Langfuseには、チェーンをログに記録するためのLangChainコールバック統合があり、エンタープライズトレースシステムと統合するためのOpenTelemetryサポートさえある。 LangSmith: LangChainエコシステムと深く統合。 LangChain (または新しいLangGraphフレームワーク) を使用している場合、LangSmithは追加のコードをほとんど必要としない。環境変数とコールバックハンドラを設定するだけでトレースを有効にできる。 OpenTelemetry計測、Vercel AI SDK、カスタムLLM APIなど、他のシナリオとの統合に関するガイドも提供。 LangSmithは、LangChain以外のネイティブ統合はLangfuseよりも少ないが、SDKを介して任意のカスタムワークフローをサポート。 LangChainチームは、SDKまたはAPIを介してトレースをログに記録することで、「LangChainを使用しているかどうかに関係なく」LangSmithを使用できることを明示的に示している。 プラットフォーム/リージョンサポート: # Langfuse: # セルフホスト可能であるため、任意のクラウドまたはオンプレミス環境にデプロイでき、インフラストラクチャ (データベース、監視ツール) への統合は非常に柔軟。 Langfuse Cloud (ホスト型サービス) は現在、コンプライアンスのために米国またはEUリージョンでのデータホスティングを提供。 LangSmith:\n主にホスト型SaaS (LangChain Inc.がホスト) であり、サービスを使用する際にリージョンオプション (米国またはEU) を提供。 データレジデンシー要件がある企業向けに、LangSmithは エンタープライズセルフホスト オプションを提供 (下記の「デプロイメント」を参照)。 両プラットフォームともAPIを介してデータを公開しているため、必要に応じて、その出力 (トレース、メトリック) を他の分析ツールまたはBIツールと統合可能。 要約: Langfuse は幅広い統合フックを提供し、特定のライブラリに縛られないため、多様な技術スタックに適しています (複数のLLMOpsツールの統合もリストされています)。LangSmith はLangChainとシームレスに統合されており、LangChainユーザーには非常に摩擦の少ないセットアップを提供し、他のユーザーにはSDKを介した汎用的な統合もサポートします。実際には、すでにLangChainを使用している場合はLangSmithの方が簡単に組み込める可能性がありますが、LangfuseもLangChainはサポートしており簡単なインテグレーションが可能です。どちらもPython/JS SDKとREST APIを備えており、事実上すべてのプラットフォームまたは言語との統合が可能です。\n4. デプロイメント (Deployment Options) # このセクションでは、クラウドとセルフホストオプションを含め、各プラットフォームをデプロイまたは使用する方法を比較します。\nホスト型クラウドサービス:\n両方ともクラウドホスト型サービスを提供。 Langfuse Cloud: Langfuseチームによるフルマネージドサービス。サインアップすると、ダッシュボードとバックエンドがホストされる。インフラストラクチャを保守したくない場合に最適。 LangSmith: 本質的にマネージドクラウドプラットフォーム (smith.langchain.com経由でアクセス可能)。ほとんどのユーザーにとって、LangSmith はクラウドサービスとして使用することになる。 セルフホスティング:\n大きな違いはセルフホスティング。 Langfuse: オープンソースであるため、誰でも簡単にセルフホスト可能。 すべてのコアLangfuse機能はMITライセンスの下で利用可能。つまり、独自のサーバーまたはクラウドアカウントでプラットフォームを無料で実行でき、使用制限はない。 Langfuse社が Docker, Kubernetes, またはVMを介してインフラストラクチャにデプロイするためのガイドや公式のHelm, Terraform などをおおお提供。 個人情報保護や自社セキュリティ基準などの理由から、オンプレミスソリューションが必要なチームや、データを完全に制御したいチームに最適。 LangSmith: 一般公開のセルフホスティングは提供していない (クローズドソースプラットフォーム)。 ただし、エンタープライズ顧客 は、Enterpriseプランの一部としてLangSmithのセルフホスト/オンプレミスデプロイメントを取得可能。 このシナリオでは、LangChainチームは、Kubernetesクラスターで実行できるコンテナ化されたバージョンのLangSmithを提供し、データが環境内に留まることを保証。 このオプションは、(通常、厳格なデータ要件を持つ大企業向けの) 商用契約でのみ利用可能。 クラウド vs オンプレミス:\nLangfuseのオープンソース版はクラウド版と同じコアソフトウェアであるため、セルフホストユーザーは 完全な機能セット を利用できる (さらに開発も可能)。 Langfuseのセルフホスティングには機能的な損失はなく、実際、Langfuseは 「セルフホスティング時にはすべてのコア機能が無制限で利用可能」 と述べている。 LangSmithのセルフホスト (エンタープライズ) は、おそらくクラウドバージョンと同等の機能を備えている (環境にデプロイされるだけ) が、これは無料または小規模チーム向けではない。 LangSmithはクラウドに頻繁に更新をプッシュする可能性があることに注意。エンタープライズデプロイメントは、手配された更新を介してそれらを受け取る。 要約: Langfuseはデプロイメントにおいて最大限の柔軟性を提供します。クラウドを使用することも、(自分のクラウドまたはオンプレミスに) 自分でデプロイすることもでき、エアギャップ環境でも、コア機能のライセンス料はかかりません。LangSmithは主にクラウドサービスです。オンプレミスが必要な場合は、エンタープライズ契約を介してのみ可能です。オープンソースまたは自己管理ソリューションを優先する組織はLangfuseに傾く可能性があり、ターンキーSaaSを好み (サードパーティがデータをホストしても構わない) 組織はLangSmith も選択肢になりえます。\n5. 価格 (Pricing Plans) # Langfuse とLangSmith は異なる価格モデルを持っています。以下にクラウド版の費用の要約します。どちらも始めるための無料枠がありますが、Langfuse の無料枠はイベントによって使用量が制限され、LangSmith の無料枠はユーザーとトレースによって制限されます。そして有料プランでは、より高いクォータとサポートが導入されます。\nプラン Langfuse (Langfuse Cloud) LangSmith (LangChain) 無料枠 Hobby Plan – 無料. 特定の制限付きですべてのコア機能が含まれる。例: 月間最大 5万 イベント、30日間のデータ保持、最大2ユーザー。クレジットカードは不要。コミュニティサポート (Discord \u0026amp; GitHub経由) のみ。プロトタイプや趣味のプロジェクトに最適。 Developer Plan – 無料. シングル開発者向け。1ユーザー と月間最大 5,000 トレースを許可。5,000トレースを超えると、従量課金制 (追加の1,000トレースあたり0.50ドル)。すべての機能 (トレース、評価、プロンプト管理など) が含まれる。この無料枠は機能的には寛大だが、ボリュームとシート数に制限がある。 ミドルレベル Pro Plan – $59/月. 中程度のボリュームでの本番環境での使用に適している。Hobbyのすべてに加え、より高い制限が含まれる: 月間 10万 観測 (その後、追加の10万件あたり10ドル)、無制限のデータ保持、無制限のユーザー と評価器。サポートはメール/チャット経由。 Team Plan – $499/月. 大規模なチーム向けで、Proのすべてに加え、高度なセキュリティ機能が含まれる: シングルサインオン (SSO) 統合、きめ細かいRBAC、コンプライアンス (SOC2、ISO27001)。プライベートSlackチャネルを介した優先サポートも含まれる。ProとTeamはどちらも大規模な商用利用を許可し、Teamはエンタープライズのようなコントロールを追加する。 Plus Plan – $39/ユーザー/月. 小規模チーム向け。Developerのすべての機能をより高いクォータで含む: 月間 1万 トレース (プール)、最大10ユーザーシート。1万トレースを超える追加トレースは、1,000件あたり0.50ドル。このプランではチームコラボレーション (複数シート) が導入され、LangChainチームからの メールサポート が付属。レート制限は無料枠よりも高い。価格はユーザー数に応じてスケーリング (ユーザーごとの価格設定)。 エンタープライズ Enterprise Plan – カスタム価格. Teamのすべてに加え、エンタープライズグレードのサポートとカスタム条件が含まれる。特に、Enterpriseは稼働時間SLA、サポートSLA、専任サポートエンジニア、アーキテクチャレビューさえ提供。価格は交渉による (AWS Marketplace経由の請求などのオプションを提供)。 セルフホスティングコスト: Langfuseのコアソフトウェアはセルフホストが無料 (ライセンス料なし)。企業は無料でセルフホストし、必要に応じてエンタープライズサポートプランの料金のみを支払うことを選択できる。Langfuseは追加のサービスまたはサポートのためにセルフホストのPro/Enterpriseライセンスを提供しているが、すべての機能はセルフマネージドモードで無料で使用可能。 Enterprise Plan – カスタム価格. 大企業が必要とする組織全体の機能を追加。この階層にはPlusのすべてが含まれ、SSO統合 (Oktaなど)、エンタープライズレベルのSLAコミットメント、そして重要なことに セルフホストデプロイメント のオプションが追加される。エンタープライズ顧客は、データプライバシーのために独自のクラウド/KubernetesでプライベートLangSmithインスタンスを取得可能。価格は相談による。LangChainには特別な Startupsプログラム (初期段階のスタートアップ向けの割引価格) もあり、採用を促進している。これは階層そのものではなく、割引プログラムである。 クラウド版のコストに関する考慮事項:\nLangfuse の価格はイベントベースで月額固定であり、使用量が含まれるクォータ (例: 10万イベントで月額59ドル) に収まる場合は、より予測しやすい可能性があります。\n一方でLangSmithの価格はシートごとおよび使用量ベースであり、無料の割り当てを超えたトレースに対して課金されます。チームメンバーが多い場合やトレース量が多い場合は、コストが高くなる可能性がありますが、単独の開発者にとっては低コストのエントリが可能です。どちらもプラットフォームを試用するための無料枠があります。Langfuseの無料枠は、LangSmithの無料枠 (5,000トレース) よりもはるかに高いボリューム (5万イベント) を許可し、LangSmithの1ユーザーに対して2ユーザーを許可します。これは、即時のコストなしで小規模なコラボレーションを行う場合に有益です。ただし、LangSmithの無料枠には使用量ベースのオーバーフローが含まれています (5,000で打ち切られるのではなく、追加料金を支払うことができます)。一方、LangfuseのHobby枠は、常に5万件のイベントを超える場合はアップグレードが必要になります。要約すると、Langfuse は従来の階層型サブスクリプションモデル (明確な制限とアップグレードパス付き) を提供し、LangSmith はハイブリッドモデル (従量課金制の無料枠、その後チーム向けのユーザーごとのサブスクリプション) を使用します。エンタープライズニーズを持つ大規模組織は、カスタム価格設定のために両方を利用できます。Langfuseはライセンス料なしでセルフホストするオプションを提供し、インフラストラクチャが問題にならない場合はコストを削減できる可能性があります。\n6. サポートとコミュニティ (Support \u0026amp; Community) # ドキュメント:\n両プラットフォームとも十分に文書化されている。 Langfuse: ウェブサイトで包括的なドキュメント (ガイド、リファレンス、インタラクティブなデモ、ビデオウォークスルーなど) を提供。ドキュメントは複数の言語 (英語、日本語、韓国語、中国語) で利用可能であり、グローバルなコミュニティを反映。また日本語でのBlogやコミュニティのアウトプットなども数多く確認されている。 LangSmith: ドキュメントは、より広範なLangChainドキュメントサイトの一部。クイックスタート、ハウツーガイド、各SDKのAPIリファレンス、概念ガイドが含まれる。主に英語だが、LangChainのコミュニティはブログなどで非公式にコンテンツを翻訳している。公式のLangSmithドキュメントは英語のみ。 どちらのドキュメントも、統合手順、機能の使用法、ベストプラクティスを徹底的にカバー。LangSmithは新しい (2023年半ばに「LangChain Plus」として開始) ため、そのドキュメントは製品とともに急速に進化しており、一方もLangfuseの公式ドキュメントはAIによる性能も非常に高いと評価できる。 コミュニティとオープンソース:\nLangfuse: オープンソースであるため、GitHub (コアリポジトリはGitHubにある) に活発なユーザーと貢献者のコミュニティがある。問題や機能リクエストはそこで直接開くことができる。チームは、サポートとQ\u0026amp;AのためにDiscordコミュニティを介して関与。このオープンモデルは、より迅速なコミュニティ主導の改善と透明性を意味する。 LangSmith: それ自体はオープンソースではない (コードは公開されていない) が、LangChainの巨大なオープンソースコミュニティの恩恵を受けている。多くのLangChainユーザーは、LangChainのDiscordまたはフォーラムでLangSmithについて議論しており、LangChainのGitHubの問題はLangSmithの統合トピックをカバーする場合がある。さらに、(ブログ投稿やdev.toの記事で見られるように) サードパーティの比較やツールは、LangSmithがオープンな代替手段とどのように比較されるかについて、コミュニティ全体で関心が高まっていることを示している。 サポートチャネル:\nLangfuse: 階層型サポートを提供。 Hobbyユーザー: コミュニティサポート (Discord、GitHubディスカッション)。 有料プラン: Langfuseチームからの直接サポート (Proユーザーはメールまたはチャット、TeamユーザーはプライベートSlackチャネル、Enterpriseは専任サポートエンジニアとSLA)。 これにより、ミッションクリティカルなデプロイメントはタイムリーな支援を受けることができる。 また日本においてはガオ株式会社 (GAO,Inc) が日本語サポートと円建て請求書払いなどにも対応。 LangSmith: 無料のDeveloper階層: 直接サポートは含まれない (コミュニティチャネルを除く)。 Plusプラン: LangChainチームからの メールサポート が付属。 Enterprise顧客: 専用のサポート担当者、SLA、およびアカウントマネージャーがいる可能性が高い。(個別に要確認) さらに、LangChainはイベント (「Interrupt」カンファレンスなど) を主催し、教育コンテンツ (ブログ、LLMアプリケーションのテストに関する電子書籍) を共有しており、これらはLangSmithユーザーの間接的なサポートおよび学習リソースとして機能。 コミュニティの感情と採用:\nどちらのツールも、LLMアプリケーションの可観測性のための主要なソリューション。 Langfuse: 独立したオープンプロジェクトとして、「最も使用されているオープンソースLLMOpsプラットフォーム」を自称し、特にセルフホストツールを好むユーザーの間で急速にユーザーベースを拡大。 LangSmith: LangChainの人気に支えられ、すぐに大規模なユーザーベースを獲得 (LangChainのサイトではLangSmithに「25万人以上がサインアップ」と記載)。 要約: Langfuseは、開発者への直接チャネルを備えた強力なオープンソースコミュニティの雰囲気を提供します (CEOやCTOでさえDiscordで頻繁に関与しています)。そのドキュメントと国際化サポートは、世界中で親しみやすいものにしています。加えて、Langfuseは製品ロードマップを公開しており、ユーザーから度々要求を受けつけている点などにも特徴があります。\nLangfuse は直近のリリース や 今後のロードマップをオープンに公開している LangSmithはLangChainの名声の恩恵を受けています。LangChainのユーザーは、おそらくどこで助けを求めればよいかを知っており、LangChainチームのリソース (ブログ、ガイド) はサポートエクスペリエンスを向上させます。\nコミュニティ主導の開発と透明性を重視するユーザーにとって、Langfuseのモデルは魅力的です。マネージドソリューションとベンダーからの専門的なサポートを希望し、すでにLangChainエコシステムにいるユーザーにとって、LangSmithもまた同様に選択肢になりうるでしょう。\n結論 # LangfuseとLangSmithはどちらも、LLMアプリケーションのトレース、デバッグ、プロンプトの反復、および評価のための堅牢なソリューションを提供しますが、異なる哲学を持っています。\nLangfuse:\nオープンソースでセルフホストを念頭に置いているプラットフォーム であり、モデル/フレームワークに依存しない。 データ制御が最優先される環境や、カスタマイズが必要な環境で優れている。 その機能セット (トレース、プロンプト管理、評価、データセット、プレイグラウンド) は、柔軟性に重点を置いてライフサイクル全体をカバー。 チームは1つの機能 (例: トレースのみ) から始めて、徐々により多くを採用できる。 コストモデルはわかりやすく、無料枠のユーザーに対するコミュニティサポートも強力。 また日本においてはコミュニティの規模も成長しており、エンタープライズ向けにガオ株式会社 (GAO,Inc) が日本語サポートと円建て請求書払いなどにも対応。 LangSmith:\nプロプライエタリなマネージドプラットフォーム (オプションでエンタープライズ向けのオンプレミス) であり、特にLangChainユーザーにとって、利便性と緊密な統合を提供する。 最小限のセットアップ、豊富な監視およびコラボレーションツール、組み込みの評価フレームワークを備えたエンドツーエンドの開発者エクスペリエンスを提供。 LangChainスタックをすでに使用しているチームにとっては、すべてのLLMOpsニーズに対応する1つのまとまったインターフェースを提供することで、開発を加速する可能性がある。 価格は使用量とシートベースであり、チームの規模に応じてスケーリングできる。 どちらかを選択する際は、セルフホスト (自社クラウド環境やオンプレミスへの設置) の必要性、オープン性 vs 利便性、コスト構造、LangChainへの依存度などの要素を考慮してください。たとえば、LangChainで迅速にプロトタイプを作成するスタートアップは、LangSmithのプラグアンドプレイの性質と既製の評価ツールから価値を得る可能性があります。顧客データなどを自社内に置く必要がある企業や、オープンソースを好む企業は、Langfuseが良いオプションになるでしょう\nLangfuseとLangSmithはどちらも同じ主要な 機能領域 (トレース、プロンプトのバージョン管理、評価、監視、フィードバック収集) をカバーしていますが、提供方法 (オープン vs クローズド) と一部の 機能のニュアンス (例: LangSmithのカスタムダッシュボード/アラートと組み込みの評価モジュール vs Langfuseのより優れた統合拡張性とセルフホストの自由) が異なります。どちらを選択しても、本番環境でのLLMアプリケーションの動作に対する非常に必要な可視性と制御を得ることができますので、社内ポリシーなどに応じて適切なツールを選択することが肝要です。\n","date":"2025年2月26日","externalUrl":null,"permalink":"/posts/2025-02-26-langfuse-langsmith-%E6%AF%94%E8%BC%83%E3%83%AC%E3%83%9D%E3%83%BC%E3%83%88/","section":"Posts","summary":"更新日：2025年5月3日\n本記事は、LLM（大規模言語モデル）アプリケーション開発プラットフォームであるLangfuse と LangSmith を比較するものです。両プラットフォームは、開発者が LLM を活用したアプリケーションを構築・運用することを支援しますが、その出自、焦点、実装において違いがあります。各セクションで、主要な基準で両者を比較し、対比を行なっていきます。なお内容は 2025年2月26日時点においての公開情報をもとに作成されております。\n","title":"Langfuse / LangSmith 比較レポート","type":"posts"},{"content":"","date":"2025年2月26日","externalUrl":null,"permalink":"/tags/langsmith/","section":"タグ","summary":"","title":"LangSmith","type":"tags"},{"content":"","date":"2025年2月26日","externalUrl":null,"permalink":"/tags/%E6%AF%94%E8%BC%83/","section":"タグ","summary":"","title":"比較","type":"tags"},{"content":"更新日：2025年3月8日\n[前回の記事 ] では、「Langfuse」を活用したプロンプト管理の方法を具体的に解説しました。Prompt をハードコードすることなく、Diff 、 Commit コメント 、Duplicate (複製) 、タグ などの機能を、直感的にエンジニアから非エンジニアまで幅広く使うことができますという内容です。まだご覧になっていない方は、ぜひチェックしてみてください。\nさて今回は、Langfuseでのプロンプト開発と評価について触れていきたいと思います。\nLangfuseによるプロンプトの新規作成 # Langfuse にログインをしたら、左側のメニューバーから [Prompts] を選択し、右上の [+ New prompt] から新規作成を始めます。\n画面右側の + New prompt を選択 そして以下の新規作成ページに移りますので、必要項目を埋めるだけです。\n新規作成ページ 各項目の説明は以下の通りです。\nName は Prompt に対する任意の名前で、プログラム側から指定するもの Prompt に具体的に入れたいプロンプト内容を入力します。Text か Chat を選ぶことができ、Chat を指定すると、それぞれのrole (System, Assistant, Developer, User) を指定してメッセージを入れることが可能です。Prompt の中には {{variable}} の形式で変数を埋め込め、ユーザーの入力値などを受け取って処理するために使えます。 Config は前回のブログでご紹介した通り、JSONで任意の値を入れることができるパラメーターになっております。例えば以下のようにモデル名の指定などで使うことができます。 { \u0026#34;model\u0026#34;: \u0026#34;models/gemini-2.0-flash-001\u0026#34; } LabelsはデフォルトでProduction ラベルをつけるかどうかの選択です。Productionになっていると、プログラムコードでラベルを指定しない場合に選択されて使用されます。後で変更可能です。 Commit message は必須ではありませんが、Version1 以降はつけておくとチームメンバーなどに意図を伝えるために有益だと思います。 Playgroundの利用 # まず新規での作り方を記載しましたが、場合によっては0から考えるのが難しいことがあると思います。そんな時に使える Playground 機能をご紹介します\nPlayground 画面 左側のメニューから [Playground] を選択すると、Playground画面に遷移します。\n基本的な使い方としては、試してみたいPrompt を [Messages] に入れ、画面右側で定義した [Model] で、モデルの種類や、各種パラメータを設定するだけ* です。また変数を入れて、右下の [Variables] に値を入れます。\n例えば今回のPromptには\n以下の文章が LLM の動作原理を説明する文章として正しいかどうかを判定しなさい。 {{text_input}} という記載が含まれており、この変数 text_input の値として テキストボックスに\nLLM は、主に「Transformer」と呼ばれる深層学習 (ディープラーニング) のアーキテクチャを基盤としています。このモデルは、大量のテキストデータ (書籍、ウェブサイト、記事など) を読み込み、単語や文の間の関係性 (文脈、意味など) を学習します。また学習した知識を基に、与えられた入力 (プロンプト) に対して、最も適切と思われる出力 (文章、回答など) を生成します。 を入れます。そしてSubmit すると、 [Output] にLLM からの回答が入っています。\nこの内容が気に入れば、右上の [Save as prompt] を押すと先ほどの新規作成画面に遷移し、そのまま新規Prompt にすることができます。\n気に入らなければ好きなだけ修正するなり、初期化したければ [Reset playground] をするだけです。\n*Model自体の登録はこの画面ではなく、メニューの　[Settings] から あらかじめ設定しておく必要がありますのでご注意ください。Langfuse側からLLMモデルを利用しますので、課金されることにご注意ください。\nPrompt Experiment の利用 # 新規作成後、LLMアプリケーションの運用においてPromptの改善が必要な場面に出くわすと思います。Prompt Experiment 利用すると変更したプロンプトに対して、リリース前に過去のユーザーのTrace をDataset にするなどして挙動が改善するかを確かめることができます。\nPrompt Experiment 文字通りPrompt の実験として、対象Prompt とバージョンやモデルなどを指定し、\n利用するDataset を設定します。\nDatasetの指定 このDataset は0から作っておくもよいですし、Trace から直接Dataset に入れることで過去正常に動いたものがデグレしないか、不具合あったInputが改善されているかを実際に確かめていただくことができるようになります。また、LLM as a judge などと連携することで、自動で定量的な評価なども付加することが可能です。LLM as a judge については、こちらのブログ で詳しく紹介しておりますので、是非あわせてご覧ください。(当該ブログの \u0026ldquo;テスト実行と結果の可視化\u0026rdquo; に相当する部分が本項に相当します。)\nなお今回ご紹介したPlayground とExperiment はSelf-hosted のOSS 版には含まれません (SaaS であれば無償のHobbyプランで使えます) のでご注意ください。\nまとめ：Langfuseでプロンプト新規開発とテストを簡単に。 # いかがでしたでしょうか。Langfuseを活用し、Promptの新規作成 - 管理までを簡単に実施できることがご確認いただけたと思います。本記事が効率的なLLMアプリケーションの開発とオペレーションのお役に立てれば幸いです。\n本機能の具体的なユースケースや使い方のデモなどにご興味がある方は、ぜひこちらから お気軽にお問い合わせください。\n","date":"2025年2月22日","externalUrl":null,"permalink":"/posts/2025-02-22-langfuse%E3%81%AB%E3%82%88%E3%82%8B%E3%83%97%E3%83%AD%E3%83%B3%E3%83%97%E3%83%88%E7%AE%A1%E7%90%86-%E5%BE%8C%E5%8D%8A-%E3%83%97%E3%83%AD%E3%83%B3%E3%83%97%E3%83%88%E9%96%8B%E7%99%BA-%E5%AE%9F%E9%A8%93%E7%B7%A8/","section":"Posts","summary":"更新日：2025年3月8日\n[前回の記事 ] では、「Langfuse」を活用したプロンプト管理の方法を具体的に解説しました。Prompt をハードコードすることなく、Diff 、 Commit コメント 、Duplicate (複製) 、タグ などの機能を、直感的にエンジニアから非エンジニアまで幅広く使うことができますという内容です。まだご覧になっていない方は、ぜひチェックしてみてください。\n","title":"Langfuseによるプロンプト管理 (後半) - プロンプト開発＆実験編","type":"posts"},{"content":"更新日：2025年3月8日\n[前回の記事 ] では、プロンプト管理の重要性にくわえて、コード埋め込みやGit または データベースによる管理の課題について解説しました。今回は、それらの問題を解決すべく「Langfuse」を活用したプロンプト管理の方法を具体的に解説します。\nLangfuseとは？：LLMの開発と運用に特化したオープンプラットフォーム # Langfuseは、生成AIアプリケーションの開発や運用に特化したオープンソースのプラットフォームです。可視化やテストなど生成AIアプリケーションのライフサイクル全体を管理するものですが、今回は特にプロンプト管理に焦点を当ててご紹介をしていきます。\nLangfuseによるプロンプト管理の主な特徴は以下のとおりです。\n使いやすいUI: 直感的なインターフェースで、プロンプトの作成、編集、比較、テストが容易に行えます。 バージョン管理: 変更履歴を自動的に記録し、過去のバージョンとの比較やコピー、修正などが簡単に行えます。 詳細な分析: プロンプトのパフォーマンス（応答時間、コスト、品質スコアなど）を詳細に分析し、改善点を見つけやすくします。 柔軟な評価: LLM as a judge などによる自動評価指標に加えて、人間による評価 (Human Annotation) も組み合わせて、多角的にプロンプトを評価できます。 チームでの共有: プロンプトをチーム内で共有し、共同で作業できます。 実践！Langfuseによるプロンプト管理の具体的なステップ # 前提： Prompt はLangfuse から取ってくる # Langfuseを使うと、プロンプト管理はどのように変わるのでしょうか？\nまず前提としてアプリケーションコードに埋め込む形とは異なり、Prompt 本体はLangfuseに格納され、それをプログラム側から fetch する形となります。\nアプリケーションはLangfuse に格納されているPrompt を取りに行き、適切なものを入手します。なお本番運用した際に都度取得をする必要は必ずしも無いので、TTL を設定してキャッシュする運用が現実的だと思います (デフォルト60 sec、0で即時反映)。\n以下はPython のコード例です。極めてシンプルな実装が可能です。\nこの場合、wweという名前で管理されているPrompt をfetch してきます。\nfrom langfuse import Langfuse # Initialize Langfuse client langfuse = Langfuse() prompt = langfuse.get_prompt(\u0026#34;wwe\u0026#34;) TTLを設定する場合や、特定のラベルのPrompt を取得したい場合は以下のようになります。ラベルについては後述します。\n# TTL を 300に指定 prompt = langfuse.get_prompt(\u0026#34;wwe\u0026#34;, cache_ttl_seconds=300) # the-rock-is-cooking というラベルの　Promptを取得 prompt = langfuse.get_prompt(\u0026#34;wwe\u0026#34;, label=\u0026#34;the-rock-is-cooking\u0026#34;) ```python その他のオプションや記述例はLangfuse の公式ドキュメントに豊富な例が用意されていますので、参考にしてみてください。 URL: [https://langfuse.com/docs/prompts/get-started](https://langfuse.com/docs/prompts/get-started) 余談ですが、同サイトの右上にある Search 欄から、Ask AI (Cmd + k) で日本語で質問することも可能です。日本語でも精度が高いので、ぜひ使ってみてください。 ![](/images/89c202_c861392f4b4245daac7b005da93148f5~mv2.png) さてこれから、実際のプロンプト管理を見ていきましょう。 1. #### Prompt の作成とバージョン管理 LangfuseのUI上では、直感的にプロンプトを直接作成・編集できます。 変更を加えるたびに、自動的に新しいバージョンが作成され、変更履歴が記録されます。 以下は プロンプト管理画面のサンプルです。 ![](/images/89c202_5f25d52625d44feaac37f3e9ac7a90be~mv2.png) これは **toyidea** という名前のプロンプトで、右側で各バージョンを確認することができます。現在、Production に適用されているのは Version 2 のプロンプトです。 また、latest ラベルがついている Version 3 にはコメントがついており、\u0026#34;独自アイディアの追加プロンプト試行\u0026#34; とコメントがついています。併せて作業者もそれぞれ情報として付加されており、いつ誰が何のためにバージョンを変えたのかも非常に分かりやすくなっています。 ラベルについては最新のものには自動で latest がつきますが、この画像のように自分で任意の名前をつけ、前述のサンプルのように指定したものを fetch することもできます。 ![](/images/89c202_2f76775857d6485b9b39677aba26cf4d~mv2.png) Version 間での詳細を比較したい場合には、GUI 上でDiff をすることもできます。相違部分がハイライトされおり、一目で差異が分かります。 ![](/images/89c202_22942397d7144124ad3c89779a5abe22~mv2.png) またPrompt にはConfig として、以下のように任意の情報を持たせておくことができます。モデル名, Temperature などをPrompt と一緒に取得することで、管理を一元化し、結果の整合性を維持することが期待されます。 ![](/images/89c202_011907d722b84880bf0720c485d29a07~mv2.png) 例えばPrompt はLangfuse から持ってきても、コードの中で別のモデルを指定していると、テストしておいた結果と実際の結果は異なってしまいますが、管理をまとめることでそのような問題を防ぐことができ、コード自体もシンプルになります。 作成したプロンプト活用して、別のアプリケーションなどのために新たなプロンプトを作りたいこともあるでしょう。その際には、Duplicate 機能で任意のVersion だけ あるいはすべてのバージョンを含んでコピーを作ることができます。新たに Prompt を作る際に、どこからか Copy and Paste したり、新規で書く必要はありません。過去に作られている資産を使って作業を効率化することが可能なのです。 ### まとめ：Langfuseで簡単だけど効果的な プロンプト管理 他にも便利な機能がありますが、それらの紹介は別の機会に譲るとして、今回は一旦ここまでとしたいと思います。見ていただいた通り、Git でのオペレーションもSQL も不要かつ、非常に効果的な管理ができます。 本記事では、プロンプト管理の前提としてLangfuse で管理をする構成やどのように管理をされるのかという点について、いくつかの主要機能について説明をしていきました。 ぜひ参考にしていただき、プロンプトをハードコードする構成ではなく、LLM Ops を実現される一助になれば幸いです。 次回は Langfuse における実際のプロンプトの開発と評価について紹介します。 管理に加えて非常に有意義な機能を簡単に使うことができますので、ぜひご覧ください。 ","date":"2025年2月16日","externalUrl":null,"permalink":"/posts/2025-02-16-langfuse%E3%81%AB%E3%82%88%E3%82%8B%E3%83%97%E3%83%AD%E3%83%B3%E3%83%97%E3%83%88%E7%AE%A1%E7%90%86-%E5%89%8D%E5%8D%8A-%E5%9F%BA%E6%9C%AC-%E7%AE%A1%E7%90%86%E7%B7%A8/","section":"Posts","summary":"更新日：2025年3月8日\n[前回の記事 ] では、プロンプト管理の重要性にくわえて、コード埋め込みやGit または データベースによる管理の課題について解説しました。今回は、それらの問題を解決すべく「Langfuse」を活用したプロンプト管理の方法を具体的に解説します。\n","title":"Langfuseによるプロンプト管理 (前半) - 基本 \u0026 管理編","type":"posts"},{"content":"更新日：2025年3月26日\nはじめに：生成AIが抱える困難とプロンプト # 生成AIアプリケーションの開発は、従来のソフトウェア開発とは異なる難しさがあります。\nその一つが、生成AIの出力の不安定さです。そしてこの不安定さに大きく関わっているのが、プロンプトです。生成AIは、人間が与える指示、つまりプロンプトに基づいて動作しますが、プロンプトが適切でなければ、生成AIはその能力を十分に発揮できません。\nしかし、プロンプトの重要性は認識されつつも、その管理は後回しにされがちです。多くの開発現場では、プロンプトがコードの中に直接埋め込まれ、場当たり的に修正されているのが現状ではないでしょうか。(少なくとも、筆者は多くそのような現場を見聞きしています)\nLLMOps のプロンプト管理とは？：なぜ必要で、何が問題なのか # プロンプト管理とは、生成AIへの指示（プロンプト）を体系的に作成、テスト、改善、保存、共有するプロセス全体を指します。\nプロンプト管理の目的は、主に以下の4つです。\n品質向上: 生成AIの出力の品質を向上させ、安定させる。 一貫性確保: 同じプロンプトからは常に同じ品質の出力が得られるようにする。 効率化: プロンプトの作成、テスト、改善のサイクルを効率化する。 再利用性向上: 良いプロンプトをチーム内で共有し、再利用できるようにする。 これらの目的を達成するために、プロンプトを適切に管理する必要があります。\nしかし、現状では以下のような問題点があります。\n問題点1：コード埋め込みプロンプトの罠 # 開発時には、作業を優先させることに意識がいってしまい、プロンプトはプログラムコードの中に直接埋め込まれてしまいがちで、結果として以下のような問題を引き起こします。\n保守性の低下: プロンプト変更のたびにアプリケーションと一緒に テスト, デプロイのパイプラインなどを回すため、とにかく手間や時間ががかかる。 再利用性の低下: 他のアプリケーションやチームメンバーがプロンプトを再利用しにくい。 可読性の低下: コードとプロンプトが混在し、コード全体の読みやすさが下がる。 バージョン管理の困難: プロンプトの変更履歴が追跡しにくく、問題発生時の原因特定が難しい。 上記のうち特に保守性の低下は致命的であり、開発,修正,テストというサイクルに無駄な工数や待ち時間などが発生してしまいます。\nPrompt をコードに埋め込むとコードと同じ工程でリリースをすることになるが、その効果は特になく時間と手間だけが発生する 問題点2：Gitやデータベースでの管理の限界 # プロンプトをコードから分離するためには、Gitやデータベースで管理する方法も考えられます。それらの特徴を大まかにまとめるたものが以下の表です。\nプロンプト管理：データベース vs. Git\n項目 データベース Git 対象ユーザー システム開発者 システム開発者 (あるいはGit作業についてトレーニングを受けた方) 利用スキル要件 DB設計・運用、SQL、加えて何らかの管理用アプリケーションが必要になるケースがある Git操作（コミット、ブランチ、マージ等） データ形式 構造化データ（プロンプト、メタデータ） 非構造化データ（主にテキスト） バージョン管理 履歴記録可能（実装依存）、差分比較は困難 履歴記録・追跡が容易、過去バージョンへ復元可、差分比較も容易 共有・コラボ 共有可能（同時アクセス、排他制御はDBによる）、専門知識必要、リアルタイム共同編集は困難 共有容易（リモートリポジトリ）、プルリクエストでレビュー可、権限管理可、ただし非技術者には難解 テスト・評価 テスト結果保存は要開発、自動評価連携は困難、プロンプトと出力結果の紐付けについても開発が必要 テスト自動化はCI/CD連携でアプリケーションと一体で実施 (プロンプト単体の評価は不可能)、自動での評価機能なし、出力結果との紐付け困難 検索性・再利用性 属性検索可、自然言語特性の完全な表現は困難 Git機能だけでは不十分（別途プロンプトライブラリ等必要） この表からわかるように、Gitとデータベースはそれぞれプロンプト管理に活用できる側面があるものの、多くの課題が残ります。\nまたプロンプトは本来はシステム開発者ではなく、生成AIのアウトプットの良し悪しが分かる 業務エキスパート (ドメインエキスパート) によって修正、テスト、評価をするべきにもかかわらず、両者ともシステム開発者の手により管理されることになります。\nプロンプトがコードと一体化することにより、システム開発者はプロンプトの責任を持たざるを得ないが、正解が分からない悲劇 コードにプロンプトを取り込むことにより、生成AIのシステム開発者の管理スコープはプロンプトに及びます。なぜならば、変更できるのがシステム開発者だけだからです。\nしかし生成AIシステムの品質を追求するためには、組織のあるべき姿として、コード管理はシステム開発者が実施し、プロンプトは生成AIに出して欲しい期待値を知っている業務エキスパートの手に委ねるべきなのです。\nまとめ：プロンプト管理の課題を克服するために # 生成AIアプリケーションの品質は、プロンプトの品質に大きく左右されます。しかし、プロンプトはコードに埋め込まれたり、Gitやデータベースで十分に管理されていなかったりすることが多く、様々な問題を引き起こしています。\nこれらの問題を解決し、生成AIのポテンシャルを最大限に引き出すためには、プロンプト管理の仕組みをアプリケーション開発者以外でも対応できるよう整備し、プロンプト管理とテスト - 改善のプロセスを一元的に確立させることが不可欠です。\n[次回の記事 ] では、これらの課題を解決するための実例を「Langfuse」と、Langfuseを活用したプロンプト管理を通して詳しく解説します。\n","date":"2025年2月15日","externalUrl":null,"permalink":"/posts/2025-02-15-llmops-%E3%83%97%E3%83%AD%E3%83%B3%E3%83%97%E3%83%88%E7%AE%A1%E7%90%86%E3%81%AE%E8%AA%B2%E9%A1%8C/","section":"Posts","summary":"更新日：2025年3月26日\nはじめに：生成AIが抱える困難とプロンプト # 生成AIアプリケーションの開発は、従来のソフトウェア開発とは異なる難しさがあります。\n","title":"[LLMOps] プロンプト管理の課題","type":"posts"},{"content":"更新日：2025年4月10日\n1.初めに # 近年、AI 技術、特に大規模言語モデル（LLM）の進化は目覚ましく、様々な分野での活用が進んでいます。しかし、LLM をビジネスに適用する上で、その品質をどのように評価するかが大きな課題となっています。\nこれまでの LLM の評価は、主に「出力結果の目視確認」や「ユーザーフィードバック」に頼ってきました。この従来のアプローチには、以下のような課題があります\n評価基準の曖昧さ 何をもって良い評価とするのかが評価者によって異なり、客観的な評価が困難 個人の価値観や経験によって、評価結果にばらつきが生じます 評価の多面性 正確性、簡潔性、論理的構成など、複数の観点からの評価が必要で 多角的な視点での評価が必要となり、総合的な判断が難しくなります 実装の複雑さ テストデータの準備、評価項目の設定、結果の可視化など、多くの実装工程が必要 評価用データセットの作成やプロンプトの作成に多くの工数がかかります このような課題を解決し、効率的かつ効果的な LLM 評価を実現するために、注目されているのが Langfuse です。 Langfuse は、LLM システムの開発から運用までをサポートする包括的なプラットフォームです。本記事では、特に LLM の評価機能に焦点を当て、Langfuse がどのように LLM 開発を効率化するのか、具体的な活用方法を解説します。\n2. Langfuseによる評価機能 # Langfuse は、これらの課題を解決するために、以下の2つの主要な評価機能を備えています。※セルフホスティングの場合はPro/Enterprise版を利用する必要あり\nHuman Annotate: 人間の手によるラベル付けを可能にします。開発者以外のドメインエキスパートが評価する場合に特に有効です。 LLM as a Judge: LLM を評価者として活用し、評価用プロンプトに基づいて自動で評価を行います。これにより、評価の客観性と効率性を高めることができます。 Langfuse の評価機能を利用することで、評価軸を明確に定義し、客観的な評価を行うことが容易になります。また、評価結果は UI 上で可視化され、トレースと紐付けられるため、問題点を特定しやすく、効率的な改善サイクルを実現できます。\n3. RAGASとLangfuseの評価アプローチの違い # RAGシステムの評価フレームワークとして、RAGASは広く使用されています。RAGASは汎用的な評価フレームワークとして、検索精度や応答の関連性など、RAGシステムの基本的な性能を測定するために設計されています。しかし、実際のビジネス応用においては、以下のような制約があります\nRAGASの制約 # 事前に定義された評価指標に基づく標準的な評価 ドメイン固有の要件への対応が限定的 評価基準の詳細な調整が困難 これに対し、Langfuseは実務での活用に焦点を当てた評価プラットフォームとして、以下のような特徴があります\nLangfuseの特徴 # ドメイン固有の評価基準を柔軟に設定可能 評価プロンプトのカスタマイズと継続的な改善 具体的な評価例 金融分野での規制要件への適合性評価 医療分野での専門用語の正確性評価 このように、RAGASとLangfuseはそれぞれ異なる用途に適しています。RAGASはRAGシステムの基本性能を標準的に評価する際に有効で、Langfuseは実際のビジネス要件に応じた詳細な評価が必要な場合に適しています。\nRAGASとLangfuseの主な違い\nRAGAS Langfuse プロンプトを作成する必要性 必要なし 必要あり 各評価用のテンプレートはある 評価用プロンプトの可視化 Githubから確認 LangfuseのUI上で確認 評価のカスタマイズ性 ほぼなし 評価基準を作成・修正が容易に可能 実装方法 ソースコードで実装 UIで設定（別途実装も可能） 日本語での評価制度 （個人的印象） 不安定 安定するように評価用プロンプトの修正が可能 4. Langfuse を用いた評価の実践 # データセットの作成 # Langfuse では、トレースデータから評価用データセットを簡単に作成できます。UI 上で必要なトレースを選択し、「Add to dataset」ボタンをクリックするだけで、データセットとして利用できます。\nまた、データセット用のトレースをあらかじめ作成しておけば、より柔軟なデータセット作成が可能です。ユーザの入力だけ取得をして、データセットとして保管しておきたい場合などに有用です。\n方法としては以下のような、関数を1つ作成するだけです。\n例：（ユーザの入力だけほしい場合）\n@observe() async def trace_dataset(): langfuse_context.update_current_observation(input=ユーザの入力 ,output=\u0026#34;\u0026#34;) テストデータセットの選定と管理 # Langfuse では、作成したデータセットを「Queue」という仕組みで管理します。トレースを Annotation の Queue に入れ、ドメインエキスパートが Queue を仕分けることで、適切なテストデータを選定できます。これにより、評価に必要なデータだけを効率的に管理し、テストの精度を高めることができます。\nトレースからqueueに入れる画面 Human Annotateの画面（仕分け画面） LLM as a Judge の利用方法 # Langfuse の LLM as a Judge 機能を利用すると、データセットの選定も効率的に行えます。あらかじめ LLM as a Judge の設定をしておけば、トレースには自動でスコアが入力されるため、怪しい値をチェックすることで、より精度の高いデータセットを選定できます。\nLLM as a Judge設定画面 LLM as a Judge設定画面（トレースの設定） テスト実行と結果の可視化 # 選定したデータセットを用いて、Langfuse 上でテストを実行します。テストの際には、モデルやプロンプトを自由に選択でき、複数の評価基準を設定することも可能です。テスト結果は、ダッシュボードで可視化され、各データポイントの評価スコア、入出力、トレースなどの情報を一目で確認できます。これにより、システムの改善点を効率的に特定し、迅速な改善サイクルを回すことが可能です。\nプロンプト実験の設定画面 Langfuseを活用した評価サイクル # 以上の機能を用いることで以下のような評価・改善サイクルが実現できます。\nLLMの出力結果をトレースし、Langfuseで可視化 Annotation の Queueに入れて、ドメインエキスパートの人に出力結果を判断してもらう　\u0026amp; LLM as a Judgeで各評価項目に対してスコアを出力させる 2で悪い結果の原因追及をして、入力をデータセットに格納（オプションで期待する出力もあると良い） プロンプトを変更する プロンプト実験を行い、前回のプロンプトの出力との違いについてLangfuse上で結果を可視化し原因の追求 5. まとめ # Langfuse は、LLM システムの開発から運用までをカバーする包括的なプラットフォームです。特に評価機能においては、以下のような作業を UI 上で完結させることができます。\n評価の事前準備\n評価用プロンプトの作成 評価基準の定義 ドメイン固有の要件への対応 テストの効率化\nトレースからの自動データセット作成 Human AnnotateとLLM as a Judgeの併用 複数の評価軸でのテスト実行 結果の可視化と改善\n定量的なスコア表示 トレースを活用した原因追求 プロンプト実験による継続的な改善 現状では、Langfuseはトレースやモニタリング機能の利用がメインとなっていますが、本記事で紹介した評価機能を活用することで、LLM開発のライフサイクル全体を効率的に管理することが可能です。評価から改善までのサイクルを一つのプラットフォームで完結できる点は、開発効率を大きく向上させる要因となるでしょう。\nぜひ Langfuse を導入し、効率的なLLM開発の実現を目指してみてください。\n","date":"2025年2月5日","externalUrl":null,"permalink":"/posts/2025-02-05-langfuse-%E3%81%A7-llm-%E8%A9%95%E4%BE%A1%E3%82%92%E5%8A%B9%E7%8E%87%E5%8C%96-%E6%B4%BB%E7%94%A8%E6%96%B9%E6%B3%95%E5%BE%B9%E5%BA%95%E8%A7%A3%E8%AA%AC/","section":"Posts","summary":"更新日：2025年4月10日\n1.初めに # 近年、AI 技術、特に大規模言語モデル（LLM）の進化は目覚ましく、様々な分野での活用が進んでいます。しかし、LLM をビジネスに適用する上で、その品質をどのように評価するかが大きな課題となっています。\n","title":"Langfuse で LLM 評価を効率化！活用方法徹底解説","type":"posts"},{"content":"更新日：2025年5月23日\n本記事ではDify で作ったLLMアプリケーションをLangfuse で可視化してみた時に、処理はどう見えるのか、そしてどのように役に立つのかをご紹介します。\n＊このブログは前半 と後半に分かれており、後半パートなります。\nはじめに # 前半では、Dify で簡単なフローを作ったアプリケーションを使った場合のLangfuseにおけるTraceの可視化についてご紹介しました。\n今回はDify でRAG をつかったフローを構成した場合に、どのように詳細を確認できるかについてご紹介します。\nDify でのサンプルアプリの構成 # 今回のケースで用いたアプリは、以下の処理をします。\nユーザーが知りたい情報を入力 \u0026ldquo;知識取得\u0026rdquo; でRAG で情報参照 \u0026ldquo;テンプレート\u0026rdquo; で \u0026ldquo;知識取得\u0026rdquo; の出力変数から特定の情報を取得 \u0026ldquo;LLM\u0026rdquo; でRAG を利用した回答生成 4の結果を出力 Dify でのフロー 実行すると以下のようになります。\n入力Text として 「Dify でLangfuse の設定をする方法を教えて」と質問すると、前述の処理を経て、Result として設定方法をコメントしつつ参照したURLを返してくれるという仕様です。\ntext に知りたい情報の入力をすると、RAGで学習した内容から答えを返してくれる なお今回は、知識取得においてWEBでクロールした情報を登録しました。\nFirecrawl と Jina Reader のいずれからから選択することができますが、HTML から\nMarkDown を生成してくれて簡単にAPIキーを取得できるJina Reader を使用しています。\nJina Reader は何もせずともいきなりアクセスした瞬間にAPIキーが払い出されてるというアグレッシブなWebサイトになっており、公式サイト の日本語翻訳が怪しいですが性能の良さを感じました。多言語対応しており、有償版でもコストパフォーマンスも良さそうです (つい最近 v2 がリリースされており、Reader自体はツールというかSLMだそうです)。\nJina Readerの設定画面 またJina からは Rerank と Embedding のモデルも提供されており、今回はそれらをRAGの構成に利用しています。RAGにつかったソースは後述の通り英語で、クエリは日本語ですが高い品質があると感じました (ただ元データが英語なので、クエリも英語の方が結果の精度は高いかなとも思いました)。\nJina の各種モデル 今回はあくまでLangfuse での表示が主目的なので、とりあえずデフォルトの10サイトのクロールでLangfuse のBlog記事からデータを以下の通り取得しています。\n(余談ですが、特定のフォルダ以下をとるような処理はDify上では正常に動いていないと思われ、トップページなどのURLもクロールしてしまいました。)\nLangfuse での見え方 # ここからが本題のTrace の見え方です。Trace は前半のブログ にも記載がありますので、必要に応じて合わせてご確認ください\n前述のフローを動かした際、Trace の詳細は以下のようになります。\nTrace詳細 各Trace をクリックすると、Input/Output, Token, Metadataなどを を確認することができます。この辺りは基本的同じです。ただ今回はRAG の構成にしておりますので、新たに画面右側部分に\u0026quot; Knowledge-retrieval\u0026quot; があることを確認できると思います。\nフロー同様、Knowledge-retrieval が Start直後にある そしてOutput としてresult が 0, 1, 2 と返ってきており、その中を見ると参照されたドキュメントやScore などが返却されております。Score が高いほど一致率が高いものであり、0に比べて1 や 2 のSocre は低いことが確認できます。\nScoreなどを含む結果詳細 参考までにこのフローでの1 の結果は以下の通りです。Score 0.33 と0 の結果に比べてだいぶ低いということがわかると思います。これは2 になるともっと低いです。\n1 の詳細結果 そして今回は \u0026ldquo;テンプレート\u0026rdquo; ツール で、0 についてくる各種データもついでにとってきています。単に配列からデータをとってきているだけです。\n{{ result.0 }} Langfuse 側でテンプレートに対応するTrace を見るとこのように表示されています。\nresultの0 の値だけを正常に取得できています。\nTemplate-transform そしてLLMには 0の結果を含めて、処理を渡して個別に Metadata 中の Title のValueをアウトプットに含めるように指示しています。\nこの部分の実際の処理はLangfuse上で以下のように確認できます。\nSystem プロンプトで指示された内容がContext 含めて反映されていることが見てとれ、Title の Value にも取得した元のBlog記事が入っていることがわかります。\nそして最終的なアプリの結果は以下のようになっており、期待した通りの動作をしていることを確認することができます。\nこのようにLangfuse を活用することで、RAGのような処理が必要な際や特定の値をとってくる処理などをした際においても、Dify の各処理を確認しながら、効率的な開発・運用を進めることができます。今回は正常にデータが取れていましたが仮に期待した結果でない場合に、取得元のソースが違うのかなどのdebug にも大変役に立ちます。\nLangfuseでの可視化 [Dify編 (後半) ] まとめ # 本記事ではLangfuseのTrace機能を通じて、RAGなどを含むDifyの処理の可視化の基本的をご紹介しました。今後、Bedrock やVertexAI などのプラットフォームを使った場合の可視化についても適宜ご紹介をしていきたいと思います。\nガオ株式会社 は企業向けにLangfuse ProおよびEnterpriseプランを日本円で販売し、日本語でサポート提供・導入支援などを実施している唯一の企業です。\nLangfuseにご興味ある方は、contact@gao-ai.comまでご連絡ください。\n","date":"2025年1月20日","externalUrl":null,"permalink":"/posts/2025-01-20-langfuse%E3%81%A7%E3%81%AE%E5%8F%AF%E8%A6%96%E5%8C%96-dify%E7%B7%A8-%E5%BE%8C%E5%8D%8A/","section":"Posts","summary":"更新日：2025年5月23日\n本記事ではDify で作ったLLMアプリケーションをLangfuse で可視化してみた時に、処理はどう見えるのか、そしてどのように役に立つのかをご紹介します。\n","title":"Langfuseでの可視化 [Dify編 (後半) ]","type":"posts"},{"content":"","date":"2025年1月20日","externalUrl":null,"permalink":"/tags/%E5%8F%AF%E8%A6%96%E5%8C%96/","section":"タグ","summary":"","title":"可視化","type":"tags"},{"content":"更新日：2025年5月23日\n本記事ではDify で作ったLLMアプリケーションをLangfuse で可視化してみた時に、処理はどう見えるのか、そしてどのように役に立つのかをご紹介します。\n＊このブログは前半と後半 に分かれており、前半パートなります。\nはじめに # 近年のDifyユーザーの広がりとエンタープライズでの採用事例などから、Dify でも生成AIアプリケーションの可視化ニーズが高まっています。そこでこの記事では、Langfuse を使った可視化についてご紹介します。\nなおこの記事ではDify の設定には深く踏み込まず、あくまでLangfuse側でどのような情報が標準では反映されるのかにフォーカスしています。\nDify とLangfuse の設定の詳細手順については、Difyの公式ページ をご参照ください。\n手順といっても特に難しいことはなく、実際は各アプリケーション左側のメニューバー一番下の \u0026ldquo;監視\u0026rdquo; をクリックしていただければ、あとはキー情報などをいれるだけです。\n赤丸のところが \u0026ldquo;監視\u0026rdquo; です Dify でのサンプルアプリの構成 # まず今回のケースで用いたアプリは、以下の処理をします。\nユーザーがファイルアップロード \u0026ldquo;テキスト抽出ツール\u0026rdquo; でテキストを抽出 \u0026ldquo;LLM1\u0026rdquo; で 原文に忠実に翻訳 \u0026ldquo;LLM2\u0026rdquo; でそれをレビューして修正 \u0026ldquo;LLM3\u0026rdquo; で不要な文字は全て排除して、マークダウンに変更 5の結果を出力 Dify フロー図 抽出、翻訳、レビュー、整形を行っている 実行すると以下のようになります。\n特に何の変哲もないアプリケーションなので、詳細な処理は割愛します。\n複数段階の処理を経て、最終的にMarkDownで出力される ここまでが前置きで、ここからLangfuse側で状態を見ていきます。\nTraceとは # Dify側で \u0026ldquo;秘密キー\u0026rdquo; , \u0026ldquo;公開キー\u0026rdquo;, \u0026ldquo;HOST\u0026rdquo; を入れていることを確認し、Langfuse側で状況を見てみます。フローを実行してまもなく、Trace に新しいエントリがあることがわかります。\nTrace一覧 (見やすい表示項目は絞っていますが、コストや自動評価スコアなども表示可能です) 改めてご説明するとTrace の定義は以下のとおりです。詳細はこちら も確認ください。\nなんとなくでもご存知の方は以下の点線内は飛ばして先に進んでいただけます。\nTraces\nA Trace represents a single execution of a LLM feature. It is a container for all succeeding objects.\n訳:\nTrace はLLM機能の単一実行を表します。これはすべての後続オブジェクトのコンテナです\nObservations\nEach Trace can contain multiple Observations to record individual steps of an execution. There are different types of Observations:\nEvents are the basic building block. They are used to track discrete events in a Trace. Spans track time periods and include an end_time. Generations are a specific type of Spans which are used to record generations of an AI model. They contain additional metadata about the model, LLM token and cost tracking, and the prompt/completions are specifically rendered in the langfuse UI. 訳:\n各Traceは、実行の個々のステップを記録するための複数のObservations を含めることができます。Observations にはさまざまな種類があります。\nEvent は基本的な構成要素です。これらは、トレース内の個別のイベントを追跡するために使用されます。 Span は時間期間を追跡し、end_timeを含みます。 Generation は、スパンの特殊なタイプであり、AIモデルの世代を記録するために使用されます。これには、モデル、LLMトークン、コスト追跡に関する追加のメタデータが含まれ、プロンプト/補完は、特にlangfuse UI でレンダリングされます。 Traceの中の構成, Generation が処理のアウトプットに相当する 参照: https://langfuse.com/guides/cookbook/python_sdk_low_level#tracing Langfuse での見え方 # Traceの詳細 各Trace をクリックすると、このようにInput/Output を確認することができます。\nTrace名 workflow の下には、日時と所要時間とToken 数が見て取れます。Dify 側で出すInput/Out Token とLangfuse 側の取り扱いが原因で、ゼロになってますが合計のToken も確認することができます。\nまずこのTrace のInput とOutput を確認することが可能ですが、それに加えて下の方にはDify で付与したMetadata があることも確認できます。\nTraceのメタデータ そして右側のメニューを Tree から Timeline に切り替えると、どこの処理でどの程の時間がかかったかを視覚的に見ていただくことも可能です。\nまたフロー各処理のタイムライン ただ、ちょっと問題なのはDifyで3つあったLLM での処理と各アウトプットが全部 llm で名称が統一されてしまっており、Langfuse の画面だけで分からない (DifyのUIの仕様) 問題があります。これについては、各GENERATION をクリックして中身から確認が可能です。\nこの画面ではGENERATION の詳細で、Systemプロンプトなどの他にレイテンシーやToken、Model などの情報が確認できます。\nGENERATIONSの詳細 この画面の下の方では、Metadata が表示されておりDIfyのノード名やタイプなどの情報を\n確認することができます。 node_name として定義されている部分です。\nGENERATION の Metadata ちなみに結果がうまく出ないなど、本当に文章抽出されてる？など思った時にはSPANとして document-extractor を見ていただくと実際のOUTPUTなどの詳細を確認することができます。ここから更に Debug が可能です。\ndocument-extractor (Outputを確認できる) このようにLangfuse を活用することで、ブラックボックスになりがちなDify の各処理を確認しながら、効率的な開発・運用を進めることができます。\nLangfuseでの可視化 [Dify編 (前半) ] まとめ # 本記事ではLangfuseのTrace機能を通じて、Difyの処理の可視化の基本的をご紹介しました。次回はもう少し深掘りして、RAGを行った場合について触れていきます。\nガオ株式会社 は企業向けにLangfuse ProおよびEnterpriseプランを日本円で販売し、日本語でサポート提供・導入支援などを実施している唯一の企業です。\nLangfuseにご興味ある方は、contact@gao-ai.comまでご連絡ください。\n","date":"2025年1月17日","externalUrl":null,"permalink":"/posts/2025-01-17-langfuse%E3%81%A7%E3%81%AE%E5%8F%AF%E8%A6%96%E5%8C%96-dify%E7%B7%A8-%E5%89%8D%E5%8D%8A/","section":"Posts","summary":"更新日：2025年5月23日\n本記事ではDify で作ったLLMアプリケーションをLangfuse で可視化してみた時に、処理はどう見えるのか、そしてどのように役に立つのかをご紹介します。\n","title":"Langfuseでの可視化 [Dify編 (前半) ]","type":"posts"},{"content":"このBlog記事はガオ株式会社による Langfuse GmbH \u0026ldquo;From Zero to Scale: Langfuse\u0026rsquo;s Infrastructure Evolution\u0026rdquo; の日本語訳 後半となります。原文はこちら をご確認ください。\n前半記事はこちら 。\nビルディングブロック2: キャッシュと分離型のインフラストラクチャ # Clickhouseへのデータ取り込み問題を解決する一方で、プロンプト管理機能を利用するユーザーの状況も改善する必要がありました。Observerbility データ分析の結果、プロンプトAPIにはいくつかの問題があることが判明しました。具体的には、それらが大量の取り込みトラフィックがデータベースのIOPSやコンテナのCPUを枯渇させる可能性がありました。\n最初に、プロンプトを取得するためのPostgresデータベースへの問い合わせを回避しようと試みました。ここで、Redisをキャッシュとして利用することの強力さと、それがもたらす影響を実感しました。プロンプトが更新されるタイミングは把握しているため、古い結果を返すリスクなく効率的にキャッシュを無効化できます。\n次に、プロンプトや認証など、レイテンシーと可用性に敏感なインフラを専用のデプロイメントに分離しました。AWS ECSの特定のエンドポイントに対して、LoadBalancerルールと専用のTargetGroupを使用することで、取り込みトラフィックをアプリケーションの他の部分から隔離できます。さらに、専用ルートによって可観測性が向上するという追加のメリットも得られました。この構成はまだHelm Chartには含まれていませんが、必要であれば主要なクラウドプロバイダーやKubernetes全体でエミュレートできます。\nこれらの2つの変更により、プロンプトAPIのp99（99パーセンタイル）のレスポンスタイムを7秒から100ミリ秒に短縮することに成功しました。\n3つ目に、SDKを更新して、ゼロレイテンシのプロンプトフェッチングを使用するようにしました。以前にAPIからプロンプトのバージョンが取得されている場合は、そのバージョンを次のLLM呼び出しに使用し、LangfuseのAPIへの呼び出しはバックグラウンドで実行します。これにより、プロンプトがユーザーのアプリケーションのクリティカルパスから取り除かれました。\nビルディングブロック3：OLAPデータベース＋クエリ最適化 # Langfuse V3への移行における最後の課題は、読み取りクエリを最適化し、すべてのユーザーに対して一貫して低いレイテンシーを実現することでした。この取り組みを開始した時点で、すでにPostgresと並行してClickhouseにデータを書き込んでいました。これにより、Clickhouseからのデータの保存方法と取得方法を実験することができました。何度も「これでうまくいった」と思ったものの、そのたびにさらに多くのデータが到着しました。\n当初、過去1週間以内のデータに対するすべての呼び出し（単一レコードのルックアップを含む）はp99で1秒以内に完了し、1週間以上のデータについては4秒以内であれば許容できると考えていました。Datadogでの通常のトレーシングに加えて、PostgresとClickHouseの読み取りを同時に実行し、それらのレイテンシーの違いを測定するためのフレームワークを構築しました。一貫して改善された結果を確認することで、正しい方向に進んでいるという確信を得ました。\nまず最初に正しく行う必要があったのは、テーブルスキーマ、パーティショニング、およびデータの並び順キーでした。これらを変更するには、テーブル全体の書き換えが必要となり、つまり、すべての更新に数日かかる可能性がありました。他のClickhouseユーザーとの会話やClickhouseのドキュメントを参照し、早い段階でprojectIdと日付を並び順キーの最初の2つの列にすることにしました。さらに、フィルタリングによく使用される列にスキップインデックスを設定して、アイテムIDも追加しました。Postgresとは異なり、Clickhouseでは効率的な行ルックアップのためのB-Treeインデックスを保持できません。常にディスクレイアウトに基づいてデータを検索する必要があります\n次に最適化する必要があったのは結合処理 (Join) でした。Oberverbility データへの結合を行う際に、処理に数秒かかり、Clickhouseノードで使用可能なすべてのメモリを使い果たすことが頻繁に発生していました。Clickhouseは、結合の右側 (The right side of a join) を効果的にフィルタリングするのが苦手だということを学びました。そこで可能な限り結合を避け、Common Table Expressions (CTE) を使用して、できる限りフィルターを手動でプッシュダウンしました。場合によっては、単一のトレースやObserverbility データのルックアップで並び順キーをより有効に活用するために、タイムスタンプなどの追加情報をフロントエンドAPIの呼び出しに追加しました。\nこれらは私たちが想定していた課題でしたが、ここからはClickhouseの特異な性質について掘り下げていきます。まず、ReplacingMergeTree内でデータを重複排除する方法を見つける必要がありました。Clickhouseは最終的には行を重複排除しますが、挿入後数分から数時間以内に多くの重複が発生し、短期間のメトリクスやダッシュボードがほぼ使い物にならないことがわかりました。FINALキーワードを使用すると改善されましたが、クエリ実行中のリソース消費量が増加したり、スキップインデックスの最適化がまったく利用されなくなったりするという新たな問題が発生しました。書き込みタイムスタンプによる重複排除（order byとlimit by）、FINALキーワード、およびdistinct集計を組み合わせて、信頼性の高いメトリクス、テーブル、およびビューを作成しました。\nスキーマ設計に多くの時間を費やしたことが、Clickhouseへの移行において大きな成果をもたらしました。プロセス全体を通してわずかな調整しか必要なく、PostgresからClickhouseへの1回のデータインポートで済みました。残りの最適化については、特定のユースケースで何がうまくいくかを予測できなかったため、迅速な反復が功を奏しました。優れた可観測性と堅牢な実験フレームワークは、反復と新しい仮説の形成に迅速なフィードバックを提供しました。昔から言われているように、「変更が難しいことについては時間をかけて考え、できる限り迅速に動く」ということが重要です。\nこれらにより、以前は大規模プロジェクトでの外れ値によって悪化していたフロントエンドおよびバックエンドAPI呼び出しにおけるパフォーマンスが向上しました。\nV3 Architecture # まとめ：Langfuse v3のアーキテクチャに施した主な変更点は以下の通りです。\nWorker コンテナ：イベントを非同期に処理する S3/Blob store: ラージオブジェクトを保存するため Clickhouse: Trace, Observations, Scoreを保存する Redis/Valkey: Eventキューイングとデータキャッシュ V3構成\nV2構成 (参考)\n結論 (Conclusion) # Langfuse v3のリリースに非常に満足しています。Langfuse Cloud ユーザーからのポジティブなフィードバック、最初の1週間でのセルフホストユーザーの高い導入率、そしてわずかな問題点リストは、プロジェクト全体を通して多くの正しい決断を下したことを示しています。新しいアイデアを迅速にテストし、明確なデータに基づいた強力な仮説に導かれることで、大きな進歩を遂げることができました。\nしかし、プロジェクト中、しばしばスケジュールを見誤っていました。今後の取り組みについては、スコープを最大1か月に制限し、途中でより多くのマイルストーンを導入する予定です。さらに、Langfuseが急速に成長しているため、このプロジェクトの進行中、主要な担当者が継続的なインシデントやバグ修正の対応に手が取られることが多くありました。\n私たちはデータ駆動型の文化、本番データのサブセットで実験を行うためのシンプルな方法、そしてスタック全体にわたる優れた可観測性を維持します。これらの要素は、費やした各時間の価値を最大化し、セルフホストとクラウドベースの両方の顧客を満足させ続けるのにとって重要な意味を持つものとなります。\n原文：Langfuse GmbH \u0026ldquo;From Zero to Scale: Langfuse\u0026rsquo;s Infrastructure Evolution\u0026rdquo; 日本語：Langfuse 日本語サイト 本ブログにおけるお問い合わせは こちら まで。\n","date":"2025年1月9日","externalUrl":null,"permalink":"/posts/2025-01-09-%E5%BE%8C%E5%8D%8A-%E3%82%BC%E3%83%AD%E3%81%8B%E3%82%89%E3%82%B9%E3%82%B1%E3%83%BC%E3%83%AB%E3%81%B8-langfuse%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%95%E3%83%A9%E3%82%B9%E3%83%88%E3%83%A9%E3%82%AF%E3%83%81%E3%83%A3%E3%81%AE%E9%80%B2%E5%8C%96-%E5%92%8C%E8%A8%B3/","section":"Posts","summary":"このBlog記事はガオ株式会社による Langfuse GmbH “From Zero to Scale: Langfuse’s Infrastructure Evolution” の日本語訳 後半となります。原文はこちら をご確認ください。\n前半記事はこちら 。\n","title":"[後半] ゼロからスケールへ：Langfuseのインフラストラクチャの進化 (和訳)","type":"posts"},{"content":"","date":"2025年1月9日","externalUrl":null,"permalink":"/tags/%E3%82%A4%E3%83%B3%E3%83%95%E3%83%A9%E3%82%B9%E3%83%88%E3%83%A9%E3%82%AF%E3%83%81%E3%83%A3/","section":"タグ","summary":"","title":"インフラストラクチャ","type":"tags"},{"content":"更新日：2025年1月9日\nこのBlog記事はガオ株式会社による Langfuse GmbH \u0026ldquo;From Zero to Scale: Langfuse\u0026rsquo;s Infrastructure Evolution\u0026rdquo; の日本語訳 前半となります。原文はこちら をご確認ください。\nゼロからスケールへ：Langfuseの # インフラストラクチャの進化 # Langfuseのインフラストラクチャをシンプルなプロトタイプからスケーラブルな\nObservability プラットフォームへと進化させた過程を詳しくご紹介します。\nSteffen Schmitz Max Deichmann\nオープンソースのLLM Observerbility プラットフォームであるLangfuseは、Y Combinator 2023年Winter バッチから誕生しました。 私たちは、多くのLLMアプリケーションを自分たちで構築し、デモから本番環境への移行が難しいことを実感した後、バッチメイトの数名と緊密に協力し、LLM可視化プラットフォームのv0を迅速に開発しました。\n当初は、いくつかのコア機能に的を絞りました。SDKは非同期、Langfuseはトレースをベースとし、すべてのコンポーネントはオープンソースで簡単にセルフホスティングできるものでした。最初のバージョンは、NextJs、Vercel、Postgresで書かれていました。私たちは、この実験が1分あたり数万件のイベントを処理するまでに急速に進化するとは、夢にも思っていませんでした。\nLangfuseがすべてのユーザーに対してスケーリングできることを確実にするという点において、私たちの最近のV3リリースは重要なマイルストーンとなりました。私たちはすでにLangfuse Cloudでこれらの変更の多くを試験的に導入しており、v3リリースではオンライン評価、非同期/キューイング取り込み、キャッシュされたプロンプトなど、セルフホスティングユーザーにもそれらを利用できるようにしました。\n本記事では、Langfuseの開発中に直面したスケーリングの課題と、私たちの「仮説 - 実験 - フィードバック」のループがLangfuse v3の開発にどのように役立ったかについてご説明します。もし、私たちと一緒に同様の課題の解決に取り組みたいとお考えであれば、ベルリンで人材を募集しています！\nWhere it all started # 当初の私たちのアーキテクチャは、単一のコンテナとPostgresデータベースであり、運用とセルフホスティングは非常にシンプルでしたが、スケーリングが非常に困難な構成でした。\n私たちは、アーキテクチャを再考せざるを得ないような、いくつかの重要な課題に直面しました。最も重要な課題は次の通りです。\n課題 1: 耐障害性が高く、高スループットを取り込めるパイプラインの構築 # 目標: Ingestion API は、予測不可能な負荷パターン下でも、大量のイベントを受け入れ、一貫して低レイテンシを維持する Langfuse の可視化プラットフォームの中核は、SDK および API による効率的なイベントデータ収集に依存しています。 これらの SDK は、ユーザーのアプリケーションへのパフォーマンスへの影響を最小限に抑えるように設計されていますが、取り込みサーバーコンポーネントは、規模を拡大するにつれて、重大な課題に直面しました。\n**当初の課題：**2023年夏には、急激なトラフィックパターンにより、取り込みAPIのレスポンスタイムが最大50秒まで急上昇しました。\n**重要な要件：**Ingestion API は、SDKからのイベントの円滑なフラッシングを確保するために、常に低レイテンシを維持する必要があります。そうでないと、ユーザーのアプリケーションに悪影響が及ぶ可能性があります。\n課題は、大量のデータを処理することだけではありませんでした。予測不可能な負荷パターン下で信頼性を維持しながら、ユーザーのアプリケーションパフォーマンスへの影響を最小限に抑えることでした。この技術的なハードルは、トラフィックの急増をより適切に処理するための取り込みアーキテクチャを再考することを迫る、当社にとって最初の大きなスケーリングの課題となりました。\n課題 2：実稼働時のワークロードに合わせたプロンプトの最適化 # 目標： Prompt API は常に高い可用性とパフォーマンスを維持する Langfuse の重要な機能のひとつがプロンプト管理システムであり、ユーザーは UI を通じてプロンプトを定義し、SDK を通じて取得することができます。これにより、プロンプトを変更するためにアプリケーションを再デプロイする必要がなくなります。\nTrace は非同期かつノンブロッキングですが、プロンプトはLLMアプリケーションのクリティカルパスとなります。このため、一見単純な機能が複雑なパフォーマンス上の課題となりました。取り込みが集中する時間帯には、プロンプト取得の p95 レイテンシが7秒にまで上昇しました。この状況には、他の操作によるシステム負荷が重い場合でも、一貫した低レイテンシのパフォーマンスを維持できるアーキテクチャ上のソリューションが必要でした。\n課題 3：高速な分析読み取り（ダッシュボード、テーブルフィルター） # 目標： 大規模な観測データにも対応するダッシュボードとテーブルフィルター 当初のデータベースとしてPostgresを選択したことは、初期の段階ではうまくいきましたが、当社の最大顧客がシステムを通じてより多くのObserverbility データを送信するようになると、重大なパフォーマンスのボトルネックにぶつかりました。クエリを最適化しても、当社のダッシュボードとテーブルフィルターの操作は、企業ユーザーにとっては遅すぎました。LLMの分析データは多くの場合、大きなblobで構成されており、何百万行ものデータをスキャンする際には、行指向のストレージがディスク上で重荷となっていました。皮肉なことに、当社の分析機能を最も必要としているお客様が、最もパフォーマンスの低下を経験していました。この成長に伴う問題は、当初のアーキテクチャが迅速な開発には最適であったものの、企業規模の分析作業負荷に対応するには根本的な再考が必要であることを示していました。\n課題4：簡単なセルフホスティング # 目標： 簡単にセルフホスティングできるだけでなく、運用上の労力をほとんど必要とせずに拡張できること Langfuseをオープンソースプロジェクトとして構築することは、意図的な選択でした。私たちのビジョンはシンプルでした。誰もが簡単なdocker-compose upでLangfuseを利用し始めるべきであると同時に、Langfuseは同時に、毎分何百ものユーザーと何千ものLLMのやり取りがあるエンタープライズ規模の展開にも対応できなければなりません。このアプローチは、私たち自身が開発者として好むものを反映しています。私たちは、評価や展開が容易なソリューションを重視しています。\nしかし、実稼働環境に耐えうるオープンソースのObserverbility プラットフォームを構築するには、特有の課題があります。\n汎用性：当社のインフラは、開発者のLaptopからさまざまなクラウド事業者へのデプロイまで、多様な環境でシームレスに動作する必要があります。 オープンソースへの依存：当社はオープンソースコンポーネントのみを使用することで、無制限のセルフホスティング機能を確保することを約束しています。 ゼロタッチ操作：企業ユーザーは、メンテナンスとアップグレードの自動化を必要としています。手動操作ではエラーが発生しやすく、拡張性にも欠けます。 このシンプルさとエンタープライズ対応のバランスが、当社のアーキテクチャ上の決定を形作り、アクセスしやすく拡張性のあるソリューションの作成を後押ししました。\n新しい構成要素 # これらの課題に対処するために、私たちはスタックに複数のビルディングブロックを追加しました。本記事では、私たちがどのようにスタックを繰り返し改良していったかをご紹介します。\nビルディングブロック1：取り込みデータの非同期処理 # 同期処理から非同期処理へ\n私たちは当初、APIコールごとに多数のイベントを受信し、それらを繰り返し処理し、各イベントを個別に処理するIngestionパイプラインから始めました。処理中、まず同じIDを持つ履歴行を検索し、LLMコールのプロンプトと補完をトークン化し、コストを計算し、データベース内のイベントをupsertします。しかし私たちのテレメトリを調査したところ、2つの大きなボトルネックがあることが分かりました。PostgresのIOPSの枯渇と、長い文字列をトークン化する際のCPU消費です。これらはどちらも、当社のアプリケーションの稼働時間とレイテンシに影響を及ぼすリスクです。最悪の場合、当社の取り込みAPIでEvent が失われ、HTTP 500エラーが返されることになりました。\nソリューションを検討するにあたり、単にコンテナの数を増やすだけでは効果的ではないことに気づきました。個々のユーザーが大規模なバッチジョブを実行すると、取り込みトラフィックが大幅に急増することがよくあります。その結果、ユーザーからの API トラフィックは非常に予測が難しくなり、コンテナインスタンスではこうした急増に対応するのに十分な速さでスケールすることができません。そこで最終的に、すべての取り込みトラフィックを Redis のメッセージキューにルーティングすることにしました。Kafkaとは異なり、Redisは簡単に自己ホストでき、当社の要件を満たすように拡張できます。そして次に、別のLangfuseコンテナ (Worker) が非同期でこのデータをConsumeし、レート制限を適用して、当社のデータベースの負荷とコンテナのCPU使用率を低減します。この変更により、認証と本文の形式のみを確認する軽量な取り込みエンドポイントを作成しました。このWorkerコンテナは、トークン化やデータベースへの書き込みなどのより集中的なタスクを処理します。\nClickhouseから読み込まずにClickhouseに更新を書き込む # 私たちは、APIパフォーマンスを短期的に改善する必要があったため、上記のステップのみを行いました。しかし、作業はまだ終わっていませんでした。Worker コンテナがすべての処理を非同期で行っていたとしても、私たちの取り込みパイプラインのロジックを動作させるには、多くのPostgresのIOPSが引き続き必要でした。この問題についても、セルフホスティングユーザーから問い合わせがありました。同時に、私たちは読み取りクエリのAPIレイテンシを改善するという課題にも直面しており、最終的に、TraceデータをPostgresからClickhouseに移行することを決定しました。Clickhouseは、Observerbility分野において多くの新規参入者が使用しているOLAPデータベースで、Apacheライセンスが適用されており、当社の概念実証（PoC）で卓越したパフォーマンスが確認されていました。列指向のストレージレイアウトは、私たちが期待する分析クエリに適しており、大規模なバイナリ列を持つ単一行の検索でも高いパフォーマンスを発揮します。しかし、本番環境への導入は容易ではありませんでした\nLangfuseのSDKは、指定されたオブジェクトIDの更新をバックエンドに送信するように設計されています。Postgresにおける単一の行の読み取りと更新は高速かつ簡単ですが、ClickHouseにおけるすべての行の更新は非常にコストのかかる操作です。そこで私たちは更新を新しい挿入に変換し、ClickHouseのReplacingMergeTreeテーブルエンジンを使用して、最終的にバックグラウンドで行の重複を排除しています。\nつまり、常に行の最新の状態を取得し、更新を適用し、それをClickhouseに書き戻す必要があるということです。\n私たちはトラフィックを分析し、すべての更新の90%が10秒以内にデータベースに書き込まれることを認識しました。つまりこれは、同時実行性とデータの整合性に気を配らなければならないことを意味します。しかし、Clickhouse から行の最新の状態を取得するのは現実的ではありませんでした。Clickhouse は、クエリ結果を返す前に全てのデータが Clickhouse ノード間で同期されることを保証する、非常にコストの高い \u0026ldquo;select_sequential_consistency\u0026rdquo; 設定を使用した場合にのみ、書き込み後の読み取りの一貫性が保たれます。したがって、私たちの規模では、Clickhouse から既存のデータを読み取れる保証はありませんでした。また、同じIDに対する2つのイベントが並行して処理され、競合状態が発生する可能性もありました。\nそこでこの問題を緩和するために、承認された全てのイベントを Redis にキャッシュすることにしました。そして、Worker コンテナにイベントを送信し、Worker コンテナはオブジェクト ID に関連する全てのイベントを取得して、Clickhouse から読み込む必要なしに、確実に新しい Clickhouse の行を作成します。私たちは新しい取り込みパイプラインを実装し、Postgres 取り込みパイプラインと並行してイベントの処理を開始しました。\nしかし非常に大きなインスタンスを利用しても、AWS ElastiCache のネットワーク容量には限界があるという事実がすぐに判明しました。また、Redis のもう一つの欠点は、インメモリ型のアーキテクチャと、保存されたデータが一時的な性質 (ephemeral) であることです。S3 をイベントのPersistent ストレージとして導入することで、Redis には参照情報のみを保持できるようになりました。この変更により、エラーが発生した場合にイベントを再生することも可能になり、さらに驚くことに、Redis 用にイベントをシリアライズする処理が高コストだったため、Web コンテナの CPU 使用率が大幅に低下しました。Kafka の方が取り込みにはより適していたかもしれませんが、新しいマルチモーダル機能のために、Redis のキャッシュ機能と S3 を活用することで、コンポーネント数を少なく保つことを選択しました。これらの調整により、Clickhouse にデータを一貫して、かつ大規模に挿入することに成功しました。\n","date":"2025年1月6日","externalUrl":null,"permalink":"/posts/2025-01-06-%E5%89%8D%E5%8D%8A-%E3%82%BC%E3%83%AD%E3%81%8B%E3%82%89%E3%82%B9%E3%82%B1%E3%83%BC%E3%83%AB%E3%81%B8-langfuse%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%95%E3%83%A9%E3%82%B9%E3%83%88%E3%83%A9%E3%82%AF%E3%83%81%E3%83%A3%E3%81%AE%E9%80%B2%E5%8C%96-%E5%92%8C%E8%A8%B3/","section":"Posts","summary":"更新日：2025年1月9日\nこのBlog記事はガオ株式会社による Langfuse GmbH “From Zero to Scale: Langfuse’s Infrastructure Evolution” の日本語訳 前半となります。原文はこちら をご確認ください。\n","title":"[前半] ゼロからスケールへ：Langfuseのインフラストラクチャの進化 (和訳)","type":"posts"},{"content":"","date":"2024年12月16日","externalUrl":null,"permalink":"/tags/gke/","section":"タグ","summary":"","title":"GKE","type":"tags"},{"content":"更新日：2025年3月8日\nはじめに # GAOの遠矢です。普段はLLMアプリケーションの開発を主にしております。\nこのガイドでは、LangfuseというLLMOpsの基盤となるツールをGoogle Cloud上でセルフホスティングする手順を紹介します。Google Cloud上のKubernetes (GKE) でLangfuse v3を安全かつ推奨される構成でデプロイする手順を解説します。\nLangfuse v2からv3への主な変更点 # V3では以下のような重要な改善が行われています\nアーキテクチャの最適化\nキューに入れられたトレースの取り込み\nトレースはバッチ処理され、S3に直接保存 APIキーのキャッシュング\nRedisを活用した高速なAPI認証 プロンプトのキャッシュ（SDK・API）\nリードスルーキャッシュによる応答時間の改善 OLAPデータベース（ClickHouse）の採用\n分析処理の高速化 信頼性の向上\nS3/Blobストレージを活用したイベントの回復可能性 バックグラウンド移行のサポート マルチモーダルトレースのサポート コンポーネント構成\nアプリケーションコンテナ（Web・Worker） ストレージコンポーネント（Postgres, ClickHouse, Redis/Valkey, S3/Blob） オプショナルなLLM API/ゲートウェイ 注記 この手順はLangfuse v3のデプロイに焦点を当てています。v2のデプロイについては、v2のドキュメントを参照してください。\n注意事項 本番環境で使用する前に、セキュリティとパフォーマンスを考慮して構成を見直すことを強く推奨します。\nシステム要件と構成 # アーキテクチャ図 # 推奨構成 # GKE（コンテナアプリケーション）\n2つのCPUと4 GB の RAM 高可用性を実現するには、Langfuse Web コンテナのインスタンスを少なくとも 2 つ Cloud SQL(PostgreSQL)\n特に指定なし Redis\n1GB以上 ClickHouse:\n3レプリカ構成 Cloud Storage（S3）\n特に指定なし 注意事項 今回はClickhouseをGKE上に構築する場合の手順のため、GKEノード(GCE)のマシンスペックが低いとClickhouseを構築できない可能性があるためご注意ください。\n構築手順 # 本手順では、主にターミナルを利用して構築を行います。\n構築準備 # まず、Google Cloudプロジェクトの設定から始めます。\n以下のコマンドでプロジェクトを設定します。\ngcloud config set project [YOUR_PROJECT_ID] ネットワークインフラの構築 # セキュアな環境を構築するため、専用のVPCネットワークとサブネットを作成します。\nこれにより、リソース間の安全な通信が可能になります。\n# VPCネットワークの作成 gcloud compute networks create langfuse-network --subnet-mode=custom # サブネットの作成 gcloud compute networks subnets create langfuse-subnet \\ --network=langfuse-network \\ --region=asia-northeast1 \\ --range=10.0.0.0/20 # VPCピアリングの設定 gcloud compute addresses create google-managed-services-range \\ --global \\ --purpose=VPC_PEERING \\ --prefix-length=16 \\ --network=langfuse-network gcloud services vpc-peerings connect \\ --service=servicenetworking.googleapis.com \\ --ranges=google-managed-services-range \\ --network=langfuse-network # 内部通信用のファイアウォールルール gcloud compute firewall-rules create langfuse-allow-internal \\ --network=langfuse-network \\ --allow=tcp,udp,icmp \\ --source-ranges=10.0.0.0/20 Kubernetesクラスターの構築 # GKEクラスターを作成します。\n本番環境では可用性を考慮して、複数のノードを配置することを推奨します。\ngcloud container clusters create langfuse-cluster \\ --network=langfuse-network \\ --subnetwork=langfuse-subnet \\ --region=asia-northeast1 \\ --num-nodes=1~2 \\ --machine-type=e2-standard-2 \\ --enable-ip-alias # 認証設定 gcloud container clusters get-credentials langfuse-cluster --region=asia-northeast1 PostgreSQLの構築 # PostgreSQLデータベースをCloud SQLで構築します。本番環境では十分なリソースと冗長性を確保することが重要です。\nパスワード等は本来であれば、シークレット等で管理してください。\n# PostgreSQL作成 gcloud sql instances create langfuse-postgres \\ --database-version=POSTGRES_15 \\ --tier=db-g1-small \\ --region=asia-northeast1 \\ --root-password=[YOUR_ROOT_PASSWORD] \\ # 今回設定する箇所は特にないので任意で --database-flags=max_connections=100 \\ --network=langfuse-network \\ --no-assign-ip # データベース作成 gcloud sql databases create langfuse --instance=langfuse-postgres # ユーザの作成 gcloud sql users create langfuse --instance=langfuse-postgres --password=langfuse1234 また、プライベートIPはこの後Langfuse側で設定する必要があるのでメモしておいてください。\n# プライベートIPの取得 gcloud sql instances describe langfuse-postgres \\ --format=\u0026#34;get(ipAddresses[0].ipAddress) Redisの構築 # Redisをマネージドサービスである、Memorystore for Redisを利用して構築を行います。\nしかし、コスト等の兼ね合いがある場合マネージドサービスを利用せずコンテナ上で構築することも可能です。\n# Redisインスタンスの作成 gcloud redis instances create langfuse-redis \\ --size=1 \\ --region=asia-northeast1 \\ --redis-version=redis_7_0 \\ --network=langfuse-network \\ --enable-auth \\ --redis-config maxmemory-policy=noeviction Redisでは、認証用の文字列（REDIS_AUTH）、ホスト（REDIS_HOST）、ポート（REDIS_PORT）をこの後Langfuse側で設定する必要があるので、メモしておいてください\n# Redis認証文字列の取得 gcloud redis instances get-auth-string langfuse-redis --region=asia-northeast1 # Redisホストの取得 gcloud redis instances describe langfuse-redis \\ --region=asia-northeast1 \\ --format=\u0026#34;get(host)\u0026#34; # Redisポートの取得 gcloud redis instances describe langfuse-redis \\ --region=asia-northeast1 \\ --format=\u0026#34;get(port)\u0026#34; Cloud Storage (S3) の構築 # # バケットの作成 gcloud storage buckets create gs://$BUCKET_NAME \\ --location=asia-northeast1 HMAC キーの作成手順\nCloud Storage \u0026gt; 設定 \u0026gt; 相互運用性タブに移動\nサービスアカウント or ユーザーアカウントを選択しHMACキーを作成します。\nアクセスキーとシークレットを安全に保存（これらは後でLangfuseの設定で使用）\nClickHouseの構築 # 今回はGKE上にClickHouseをデプロイする手法で行います。\n# Helmのインストール curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash # Helmリポジトリの追加 helm repo add bitnami https://charts.bitnami.com/bitnami helm repo update 次に以下のclickhouse-values.yamlを作成します。\nshards: 1 replicaCount: 3 auth: username: default password: changeme resourcesPreset: large persistence: size: 100Gi shards: シャード数（この場合は1） replicaCount: レプリカ数（本番では3以上推奨） YAML作成後、Clickhouseのデプロイを行います。\nhelm install clickhouse bitnami/clickhouse \\ -f clickhouse-values.yaml \\ --namespace default その後以下のコマンドで、正常に起動することを確認します。\n# Podの状態確認 kubectl get pods | grep clickhouse # サービスの確認 kubectl get svc -l app.kubernetes.io/name=clickhouse # ログの確認 kubectl logs clickhouse-0 # デプロイされたリソース一覧の確認 kubectl get all -l app.kubernetes.io/name=clickhouse Kubernetes(helm)でのLangfuseのデプロイ # # Langfuseリポジトリのクローン git clone https://github.com/langfuse/langfuse-k8s.git cd langfuse-k8s/charts/langfuse git checkout lfe-1348-v3-chart リポジトリをクローン後、以下の内容でlangfuse-values.yamlを作成してください。\nまた、その際に変数等は適宜変更してください。\nlangfuse-values.yaml\nreplicaCount: 1 image: repository: langfuse/langfuse pullPolicy: Always tag: 3 imagePullSecrets: [] nameOverride: \u0026#34;\u0026#34; fullnameOverride: \u0026#34;\u0026#34; langfuse: port: 3000 nodeEnv: development next: healthcheckBasePath: \u0026#34;\u0026#34; nextauth: url: \u0026#34;http://localhost:3000\u0026#34; secret: \u0026#34;openssl rand -base64 32で作成\u0026#34; salt: \u0026#34;openssl rand -base64 32で作成\u0026#34; telemetryEnabled: true nextPublicSignUpDisabled: false enableExperimentalFeatures: true extraContainers: [] extraVolumes: [] extraInitContainers: [] extraVolumeMounts: [] web: replicas: 1 worker: replicas: 1 additionalEnv: - name: \u0026#34;LANGFUSE_LOG_FORMAT\u0026#34; value: \u0026#34;json\u0026#34; # Experimental v3 feature flags - name: \u0026#34;LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES\u0026#34; value: \u0026#34;true\u0026#34; - name: \u0026#34;ENCRYPTION_KEY\u0026#34; value: \u0026#34;openssl rand -hex 32で作成\u0026#34; - name: \u0026#34;LANGFUSE_ASYNC_INGESTION_PROCESSING\u0026#34; value: \u0026#34;true\u0026#34; - name: \u0026#34;LANGFUSE_ASYNC_CLICKHOUSE_INGESTION_PROCESSING\u0026#34; value: \u0026#34;true\u0026#34; - name: \u0026#34;LANGFUSE_READ_DASHBOARDS_FROM_CLICKHOUSE\u0026#34; value: \u0026#34;true\u0026#34; - name: \u0026#34;LANGFUSE_READ_FROM_POSTGRES_ONLY\u0026#34; value: \u0026#34;false\u0026#34; - name: \u0026#34;LANGFUSE_RETURN_FROM_CLICKHOUSE\u0026#34; value: \u0026#34;true\u0026#34; # Database connections - name: DATABASE_URL value: \u0026#34;postgresql://langfuse:langfuse1234@[DB_IP]:5432/langfuse\u0026#34; - name: \u0026#34;REDIS_HOST\u0026#34; value: \u0026#34;[REDIS_HOST]\u0026#34; - name: \u0026#34;REDIS_PORT\u0026#34; value: \u0026#34;[REDIS_PORT]\u0026#34; - name: \u0026#34;REDIS_AUTH\u0026#34; value: \u0026#34;[REDIS_AUTH]\u0026#34; - name: \u0026#34;CLICKHOUSE_URL\u0026#34; value: \u0026#34;http://clickhouse:8123\u0026#34; - name: \u0026#34;CLICKHOUSE_MIGRATION_URL\u0026#34; value: \u0026#34;clickhouse://clickhouse:9000\u0026#34; - name: \u0026#34;CLICKHOUSE_USER\u0026#34; value: \u0026#34;default\u0026#34; - name: \u0026#34;CLICKHOUSE_PASSWORD\u0026#34; value: \u0026#34;changeme\u0026#34; - name: CLICKHOUSE_CLUSTER_ENABLED value: \u0026#34;false\u0026#34; # Cloud Storage settings - name: \u0026#34;LANGFUSE_S3_EVENT_UPLOAD_ENABLED\u0026#34; value: \u0026#34;true\u0026#34; - name: \u0026#34;LANGFUSE_S3_EVENT_UPLOAD_BUCKET\u0026#34; value: \u0026#34;[BUCKET_NAME]\u0026#34; - name: \u0026#34;LANGFUSE_S3_EVENT_UPLOAD_REGION\u0026#34; value: \u0026#34;auto\u0026#34; - name: \u0026#34;LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID\u0026#34; value: \u0026#34;[HMAC_ACCESS_KEY]\u0026#34; - name: \u0026#34;LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY\u0026#34; value: \u0026#34;[HMAC_SECRET_ACCESS_KEY]\u0026#34; - name: \u0026#34;LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT\u0026#34; value: \u0026#34;https://storage.googleapis.com\u0026#34; - name: \u0026#34;LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE\u0026#34; value: \u0026#34;true\u0026#34; - name: \u0026#34;LANGFUSE_S3_EVENT_UPLOAD_PREFIX\u0026#34; value: \u0026#34;events/\u0026#34; serviceAccount: create: true annotations: {} name: \u0026#34;\u0026#34; podAnnotations: {} podSecurityContext: {} securityContext: {} service: enabled: true type: NodePort port: 3000 targetPort: 3000 ingress: enabled: false # ローカル環境では無効化 resources: {} autoscaling: enabled: false nodeSelector: {} tolerations: [] affinity: {} postgresql: deploy: false # 外部PostgreSQLを使用 auth: username: \u0026#34;langfuse\u0026#34; password: \u0026#34;langfuse1234\u0026#34; database: \u0026#34;langfuse\u0026#34; clickhouse: deploy: false # 外部Clickhouseを使用 valkey: deploy: false minio: deploy: false # 外部ストレージを使用 extraManifests: [] 主に変更する箇所は以下です。\nパラメータ 作成方法/説明 nextauth.secret openssl rand -base64 32で作成 salt openssl rand -base64 32で作成 ENCRYPTION_KEY openssl rand -hex 32で作成 [DB_IP] PostgreSQLデータベースのIPアドレス [REDIS_HOST] Redisサーバーのホスト名 [REDIS_PORT] Redisサーバーのポート番号 [REDIS_AUTH] Redisの認証パスワード [BUCKET_NAME] Cloud Storageのバケット名 [HMAC_ACCESS_KEY] Cloud StorageアクセスのHMACアクセスキー [HMAC_SECRET_ACCESS_KEY] Cloud StorageアクセスのHMACシークレットキー LANGFUSE_EE_LICENSE_KEY Langfuse Enterprise プランのライセンスキー（オプション）ない場合は特に追加の必要なし 以下のコマンドでhelmでLangfuseをlangfuse-values.yamlに沿って作成します。\n# Langfuseのデプロイ helm dependency update helm install langfuse . -f langfuse-values.yaml デプロイができたら、以下のコマンドでログとポッドの状態を確認します。\n# podの状態確認 kubectl get pods -l app.kubernetes.io/instance=langfuse # ログの確認 kubectl logs -f deployment/langfuse-web kubectl logs -f deployment/langfuse-worker どちらとも起動することを確認したら、ポートフォワーディングをして、localhost:3000でLangfuseのログイン画面が出力されることを確認します。\nポートフォワーディングのコマンド\n# ポートフォワーディングの設定 kubectl port-forward svc/langfuse-web 3000:3000 その後、トレースなどを行い動作に問題がないか確認したらデプロイ完了です。\n応用編 (外部IP付与 GCLB) # GoogleのGlobal Load Balancerを利用して、HTTPS通信を可能にします。\nまずは、Google マネージドのSSL証明書を作成します。\n以下のmanaged-cert.yamlを作成します。\napiVersion: networking.gke.io/v1 kind: ManagedCertificate metadata: name: langfuse-cert spec: domains: - YOUR_DOMAIN # 実際のドメインに変更 その後マネージド証明書の適用を行います。\n# マネージド証明書の適用 kubectl apply -f managed-cert.yaml 次に外部IPアドレスを予約しておきます\n# 静的外部IPアドレスの予約 gcloud compute addresses create langfuse-ip --global ここで予約したIPアドレスを DNS の A レコードとして設定してください。\n次に、langfuse-values.yamlを更新します。 主な変更点としては環境変数の更新・追加とIngressの追加を行います。\n# 主な更新箇所のみ抜粋 langfuse: nextauth: url: \u0026#34;https://YOUR_DOMAIN\u0026#34; # 外部アクセス用のドメインに更新 additionalEnv: - name: \u0026#34;LANGFUSE_CSP_ENFORCE_HTTPS\u0026#34; value: \u0026#34;true\u0026#34; # HTTPSを強制的に使用 # 新規追加のIngress設定 ingress: enabled: true annotations: kubernetes.io/ingress.global-static-ip-name: langfuse-ip networking.gke.io/managed-certificates: langfuse-cert kubernetes.io/ingress.class: \u0026#34;gce\u0026#34; kubernetes.io/ingress.allow-http: \u0026#34;false\u0026#34; hosts: - host: YOUR_DOMAIN paths: - path: / pathType: Prefix Langfuseの更新\n# Langfuseの更新 helm upgrade langfuse . -f langfuse-values.yaml 確認コマンド\n# 証明書の状態確認 kubectl describe managedcertificate # AtiveになったらOK # ロードバランサーの確認 kubectl get ingress # HOST名が設定したドメイン名、アドレスが langfuse-ipの値になってたらOK # GUIでGCLBが作成できるかどうかも確認 以上の設定が完了したら、https://YOUR_DOMAINにアクセスして、LangfuseUIにログインできたら成功です。 トレースなどができるか確認してみてください。\nできない場合は helm unistallなどをしてください\nまた、以下の画面はenterpriseEditionのライセンスキーの有効化を行なっているため、左上がv3.2.0 EEとなっています。\nEEとなっており、Playground等の機能も画面に出力されている状態になっております。\nクリーンアップ # 必要に応じて、以下の手順でリソースを削除します\n# Langfuseの削除 helm uninstall langfuse helm uninstall clickhouse # GKEクラスターの削除 gcloud container clusters delete langfuse-cluster \\ --region=asia-northeast1 \\ --async # Cloud SQLインスタンスの削除 gcloud sql instances delete langfuse-postgres # Redisインスタンスの削除 gcloud redis instances delete langfuse-redis \\ --region=asia-northeast1 # Cloud Storageバケットの削除 gsutil rm -r gs://$BUCKET_NAME # ファイアウォールルールの削除 gcloud compute firewall-rules delete langfuse-allow-internal # VPCピアリング接続の削除 gcloud services vpc-peerings disconnect \\ --service=servicenetworking.googleapis.com \\ --network=langfuse-network # 予約したIPレンジの削除 gcloud compute addresses delete google-managed-services-range \\ --global # サブネットの削除 gcloud compute networks subnets delete langfuse-subnet \\ --region=asia-northeast1 # VPCネットワークの削除 gcloud compute networks delete langfuse-network トラブルシューティング # Redis接続エラーについて\n\u0026ldquo;Redis connection error: ERR unknown command \u0026lsquo;client\u0026rsquo;\u0026ldquo;という警告は、Google CloudのMemoryStoreの制限によるものです。 この警告は表示されても、Redisとの接続自体は正常に行われています。 証明書のプロビジョニング\nマネージド証明書のプロビジョニングには数分から数十分かかる場合があります。 kubectl describe managedcertificate で状態を確認できます。 注意事項 # すべてのパスワードとシークレットは適切に管理してください。 本番環境ではより強力なセキュリティ設定と、より多くのリソースを割り当てることを検討してください。 環境変数の値は実際の値に置き換えてください。 Langfuseの公式ドキュメントも参照し、最新の情報を確認してください。 最後に # このガイドが、Google Cloud上でのLangfuse v3の安全なデプロイに役立つことを願っています。\nまた企業向けサポートとして、ガオ株式会社 を通じてLangfuse ProおよびEnterpriseプランを日本円で購入し、日本語でサポートを受けることが可能です。\nご興味ある方は、contact@gao-ai.comまでご連絡ください。\n","date":"2024年12月16日","externalUrl":null,"permalink":"/posts/2024-12-16-google-cloud%E3%81%A7langfuse-v3%E3%81%AE%E6%A7%8B%E7%AF%89%E6%89%8B%E9%A0%86-%E6%8E%A8%E5%A5%A8%E8%A8%AD%E5%AE%9A-gke/","section":"Posts","summary":"更新日：2025年3月8日\nはじめに # GAOの遠矢です。普段はLLMアプリケーションの開発を主にしております。\n","title":"Google CloudでLangfuse v3の構築手順（推奨設定/GKE）","type":"posts"}]