APC 技術ブログ

株式会社エーピーコミュニケーションズの技術ブログです。

株式会社 エーピーコミュニケーションズの技術ブログです。

さくらのAI EngineをLangfuseで可視化してみた

こんにちは。クラウド事業部の遠見です。

  • 本記事は、「さくらのAI Engine」をOSSのLLM ObservabilityツールであるLangfuseで可視化する検証を実施したエンジニアが、内容をできる限り客観的に共有することを目的に作成しました。
  • 本記事内の見解は執筆者個人のものであり、所属組織を代表するものではありません。

前回の記事では、さくらのAI EngineとDatadogのLLM Observabilityを組み合わせた構成を検証しました。

techblog.ap-com.co.jp

今回は、OSSのLLM ObservabilityツールであるLangfuseをさくらのクラウド上にセルフホストし、同じ構成を再現してみました。
「完全にデータを自分のインフラ内で完結させたい」という構成を意識しています。


目次


はじめに

SaaSとOSSの使い分け

前回の記事でDatadogのLLM Observabilityを試した際、「ここまで手軽に可視化できるのか」という驚きがありました。
一方で、実際の運用を考えると、以下のようなことも考えられます。

懸念 詳細
トレースデータの外部送信 プロンプトや応答内容に機密情報が含まれる可能性があるため、送信前後にデータのマスキング処理や、特定のタグ・属性の除外設定を検討する必要がある
コストへの影響 外部SaaSでは取り込み量に応じた従量課金となるため、トレース量が増加するとコストも比例して増大する

外部SaaSを使用せず完全に自社インフラで完結させるには、自己ホスト型トレーシング基盤の構築が必要となります。
こうしたニーズに応えるのが、OSSのLLM Observabilityツールです。

今回はその代表格である「Langfuse」を選びました。

Langfuseとは

Langfuseは、OSSのLLM Observabilityプラットフォームです。
MITライセンスで公開されており、セルフホストで完全に自社インフラ内に閉じた運用が可能です。

主な機能は以下の通りです。

項目 内容
Traces LLMの入出力・レイテンシ・トークン数の記録
Sessions 会話単位のセッション管理
Evaluations モデル出力の品質評価
Prompts プロンプトのバージョン管理

外部SaaSと比較したとき、最大の差別化ポイントは「データが自分のサーバーから出ない」という点です。


今回の構成とゴール

本検証では、前回の構成で利用したチャットアプリを再利用し、Langfuseで挙動を可視化することを目指します。

分類 項目 内容 / バージョン
構成概要 実行環境 ローカルPC
LLM基盤 さくらのAI Engine
監視基盤 Langfuse v3 (さくらのクラウドでセルフホスト)
構成管理 Terraform
ローカル OS Windows 11 Home 25H2
PowerShell 7.5.5
Python 3.12.4
uv 0.11.7
Langfuse SDK 2.60.10
クラウド Terraform 1.14.9 (GitHub Codespaces)
サーバーOS Ubuntu 22.04
Langfuse v3 (3.157.0)

構成図


事前準備

さくらのAI Engineのアカウント作成・トークン取得

前回記事と同様の手順です。

techblog.ap-com.co.jp

さくらのクラウドAPIキー取得

Terraformがサーバーやディスクをさくらのクラウドに作成するために必要な権限を取得します。

[さくらのクラウド ホーム] > [APIキー] > [APIキーの作成]をクリックします。

[APIキーの作成]で以下を入力します。
さくらのクラウドでTerraformを動かすには、リソースの「作成」や「削除」ができる権限が必要です。

項目 設定内容 詳細・用途
APIキー種別 リソース操作APIキー Terraformでの構築に必要
APIキー名 任意 任意の値を入力
アクセスレベル 作成・削除 インフラの構築・破棄を行うために必要
サービスへのアクセス権 すべて未チェック デフォルト設定のままでOK

入力が確認できたら、[作成]をクリックします。

[APIキーが作成されました]ダイアログが表示されたら、CSVをダウンロードするか、以下の2つをメモします。

  • アクセストークン
  • アクセストークンシークレット

オブジェクトストレージのアクセスキー作成

オブジェクトストレージのアクセスキーは、リソース操作APIキーとは別の管理画面から発行が必要です。

[さくらのクラウド ホーム] > [トップ] > [オブジェクトストレージ]をクリックします。

利用したいリージョンを選択して、[OK]をクリックします。

[アクセスキー]ダイアログが表示されたら、以下の2つをメモします。

  • アクセスキーID
  • シークレットアクセスキー

オブジェクトストレージのバケット作成

Terraformのステート保存用とLangfuseのイベント保存用に、それぞれ別のバケットを作成します。

バケット 用途 命名例
Terraformステート保存用 Terraform tfstate の保管 tfstate-sakura-langfuse
Langfuseイベント保存用 Langfuse v3 必須のBlob Store langfuse-events-yourname

[オブジェクトストレージ] > [バケット] > [バケットを追加]をクリックします。

[バケットを追加]ダイアログで、任意の名前(例:tfstate-sakura-langfuse)を入力し、[追加]をクリックします。

バケットが作成されます。

同様の手順で、もう一つのバケットを作成します。
2つのバケットが作成できればOKです。


LangfuseをTerraformで構築する

今回は、GitHub Codespacesを使ってTerraformでインフラを構築します。

まずGitHubにリポジトリを作成し、Terraformのコードを配置します。
コードの構成は以下の通りです。

sakura-langfuse
├─ main.tf
├─ variables.tf
├─ terraform.tfvars
├─ outputs.tf
└─ scripts/
  └─ setup.sh

コードは参考までに記載します。
他の環境で動作するかは検証していないため、あらかじめご了承ください。

【重要】terraform.tfvarsには機密情報が含まれます。
GitHubなどで管理する場合は、誤って公開リポジトリにプッシュしないよう、必ず.gitignoreに追加されていることを確認してください。

main.tf

terraform {
  required_providers {
    sakuracloud = {
      source  = "sacloud/sakuracloud"
      version = "~> 2.0"
    }
  }

  backend "s3" {
    bucket   = "tfstate-sakura-langfuse"
    key      = "langfuse/terraform.tfstate"
    region   = "jp-north-1"
    endpoints = { s3 = "https://s3.isk01.sakurastorage.jp" }

    skip_credentials_validation = true
    skip_region_validation      = true
    skip_requesting_account_id  = true
    skip_metadata_api_check     = true
    use_path_style              = true
  }
}

provider "sakuracloud" {
  zone = "is1c"
}

data "sakuracloud_archive" "ubuntu" {
  os_type = "ubuntu2204"
}

resource "sakuracloud_disk" "langfuse_disk" {
  name              = "${var.server_name}-disk"
  source_archive_id = data.sakuracloud_archive.ubuntu.id
  plan              = "ssd"
  size              = 20
}

resource "sakuracloud_packet_filter" "filter" {
  name = "langfuse-filter"

  expression {
    protocol         = "tcp"
    destination_port = "80"
    source_network   = var.my_ip
    allow            = true
    description      = "Allow HTTP"
  }

  expression {
    protocol         = "tcp"
    destination_port = "32768-61000"
    allow            = true
    description      = "Allow outbound return packets"
  }

  expression {
    protocol         = "udp"
    destination_port = "32768-61000"
    allow            = true
    description      = "Allow outbound return packets UDP"
  }

  expression {
    protocol    = "ip"
    allow       = false
    description = "Deny ALL"
  }
}

resource "sakuracloud_server" "langfuse_server" {
  name   = var.server_name
  core   = 2
  memory = 8  # v3はClickHouse・Redis追加のため8GBを推奨
  disks  = [sakuracloud_disk.langfuse_disk.id]

  network_interface {
    upstream         = "shared"
    packet_filter_id = sakuracloud_packet_filter.filter.id
  }

  disk_edit_parameter {
    note {
      id = sakuracloud_note.langfuse_init.id
    }
  }
}

resource "sakuracloud_note" "langfuse_init" {
  name    = "langfuse-deployment"
  content = templatefile("${path.module}/scripts/setup.sh", {
    db_password          = var.db_password,
    langfuse_secret      = var.langfuse_secret,
    salt                 = var.salt,
    encryption_key       = var.encryption_key,
    clickhouse_password  = var.clickhouse_password,
    redis_password       = var.redis_password,
    s3_access_key_id     = var.s3_access_key_id,
    s3_secret_access_key = var.s3_secret_access_key,
    s3_bucket_events     = var.s3_bucket_events,
    s3_endpoint          = var.s3_endpoint,
  })
}

variables.tf

variable "server_name" {
  default = "langfuse-server"
}

variable "db_password" {
  type      = string
  sensitive = true
}

variable "langfuse_secret" {
  type      = string
  sensitive = true
}

variable "salt" {
  type      = string
  sensitive = true
}

variable "my_ip" {
  type      = string
  sensitive = true
}

variable "encryption_key" {
  description = "32バイトのhex文字列。生成: openssl rand -hex 32"
  type        = string
  sensitive   = true
}

variable "clickhouse_password" {
  type      = string
  sensitive = true
}

variable "redis_password" {
  type      = string
  sensitive = true
}

variable "s3_access_key_id" {
  description = "さくらのオブジェクトストレージ アクセスキー"
  type        = string
  sensitive   = true
}

variable "s3_secret_access_key" {
  description = "さくらのオブジェクトストレージ シークレットキー"
  type        = string
  sensitive   = true
}

variable "s3_bucket_events" {
  description = "Langfuseイベント保存用バケット名"
  type        = string
}

variable "s3_endpoint" {
  description = "さくらのオブジェクトストレージ エンドポイント"
  type        = string
  default     = "https://s3.isk01.sakurastorage.jp"
}

terraform.tfvars

db_password     = ""  # 任意のパスワード
langfuse_secret = ""  # openssl rand -base64 32
salt            = ""  # openssl rand -base64 32
my_ip           = ""  # curl ifconfig.me で確認

encryption_key       = ""  # openssl rand -hex 32
clickhouse_password  = ""  # 任意のパスワード
redis_password       = ""  # 任意のパスワード

# さくらのオブジェクトストレージで発行
s3_access_key_id     = ""  # 取得したアクセスキーID
s3_secret_access_key = ""  # 取得したシークレットアクセスキー 
s3_bucket_events     = ""  # 作成したバケット名
# s3_endpoint はデフォルト値あり(石狩リージョンの場合は設定不要)

outputs.tf

output "langfuse_url" {
  value       = "http://${sakuracloud_server.langfuse_server.ip_address}"
}

scripts/setup.sh

#!/bin/bash
set -euo pipefail

curl -fsSL https://get.docker.com | sh

SERVER_IP=$(curl -s ifconfig.me)

mkdir -p /opt/langfuse
cat <<EOF > /opt/langfuse/docker-compose.yml
services:
  postgres:
    container_name: langfuse-postgres
    image: postgres:16
    restart: always
    environment:
      POSTGRES_USER: langfuse
      POSTGRES_PASSWORD: ${db_password}
      POSTGRES_DB: langfuse
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U langfuse"]
      interval: 5s
      timeout: 5s
      retries: 10

  clickhouse:
    container_name: langfuse-clickhouse
    image: clickhouse/clickhouse-server:24.3
    user: "101:101"
    restart: always
    environment:
      CLICKHOUSE_DB: default
      CLICKHOUSE_USER: clickhouse
      CLICKHOUSE_PASSWORD: ${clickhouse_password}
    volumes:
      - clickhouse_data:/var/lib/clickhouse
      - clickhouse_logs:/var/log/clickhouse-server
    healthcheck:
      test: ["CMD-SHELL", "clickhouse-client --user clickhouse --password ${clickhouse_password} --query 'SELECT 1'"]
      interval: 5s
      timeout: 5s
      retries: 10

  redis:
    container_name: langfuse-redis
    image: redis:7-alpine
    restart: always
    command: redis-server --requirepass ${redis_password}
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${redis_password}", "ping"]
      interval: 5s
      timeout: 5s
      retries: 10

  langfuse-web:
    container_name: langfuse-web
    image: langfuse/langfuse:3
    restart: always
    depends_on:
      postgres:
        condition: service_healthy
      clickhouse:
        condition: service_healthy
      redis:
        condition: service_healthy
    ports:
      - "80:3000"
    environment:
      DATABASE_URL: postgresql://langfuse:${db_password}@postgres:5432/langfuse
      NEXTAUTH_URL: http://$SERVER_IP
      NEXTAUTH_SECRET: ${langfuse_secret}
      SALT: ${salt}
      ENCRYPTION_KEY: ${encryption_key}
      CLICKHOUSE_URL: http://clickhouse:8123
      CLICKHOUSE_USER: clickhouse
      CLICKHOUSE_PASSWORD: ${clickhouse_password}
      CLICKHOUSE_MIGRATION_URL: clickhouse://clickhouse:9000
      CLICKHOUSE_CLUSTER_ENABLED: "false"
      REDIS_HOST: redis
      REDIS_PORT: "6379"
      REDIS_AUTH: ${redis_password}
      LANGFUSE_S3_EVENT_UPLOAD_BUCKET: ${s3_bucket_events}
      LANGFUSE_S3_EVENT_UPLOAD_REGION: jp-north-1
      LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: ${s3_access_key_id}
      LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: ${s3_secret_access_key}
      LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: ${s3_endpoint}
      LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: "true"
      TELEMETRY_ENABLED: "true"

  langfuse-worker:
    container_name: langfuse-worker
    image: langfuse/langfuse-worker:3
    restart: always
    depends_on:
      postgres:
        condition: service_healthy
      clickhouse:
        condition: service_healthy
      redis:
        condition: service_healthy
    environment:
      DATABASE_URL: postgresql://langfuse:${db_password}@postgres:5432/langfuse
      SALT: ${salt}
      ENCRYPTION_KEY: ${encryption_key}
      CLICKHOUSE_URL: http://clickhouse:8123
      CLICKHOUSE_USER: clickhouse
      CLICKHOUSE_PASSWORD: ${clickhouse_password}
      CLICKHOUSE_CLUSTER_ENABLED: "false"
      REDIS_HOST: redis
      REDIS_PORT: "6379"
      REDIS_AUTH: ${redis_password}
      LANGFUSE_S3_EVENT_UPLOAD_BUCKET: ${s3_bucket_events}
      LANGFUSE_S3_EVENT_UPLOAD_REGION: jp-north-1
      LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: ${s3_access_key_id}
      LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: ${s3_secret_access_key}
      LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: ${s3_endpoint}
      LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: "true"

volumes:
  postgres_data:
  clickhouse_data:
  clickhouse_logs:
  redis_data:
EOF

cd /opt/langfuse && docker compose up -d

setup.shは、サーバー起動時に自動実行されるセットアップスクリプトです。
DockerとDocker Composeをインストールし、Langfuse v3の全コンポーネント(PostgreSQL、ClickHouse、Redis、langfuse-web、langfuse-worker)を起動します。

ポイントは CLICKHOUSE_CLUSTER_ENABLED: "false" の設定です。
デフォルトではClickHouseがクラスターモード(ReplicatedMergeTree)で接続しようとしますが、シングルノード構成では明示的にfalseを指定することで通常のMergeTreeが使われます。
この設定がないとマイグレーション時にエラーになります。

Terraform関連のコード配置は以上です。

ここからはGitHub Codespacesの環境設定になりますが、初期状態ではTerraformが入っていなかったため、.devcontainer/devcontainer.jsonを作成して、Terraformをインストールしました。

{
  "name": "Terraform Environment",
  "features": {
    "ghcr.io/devcontainers/features/terraform:1": {}
  }
}

最終的なディレクトリ構成は以下の通りです。

sakura-langfuse
├─ .devcontainer/
│ └─ devcontainer.json
├─ main.tf
├─ variables.tf
├─ terraform.tfvars
├─ outputs.tf
└─ scripts/
  └─ setup.sh

コードの配置が完了したら、Codespacesを起動します。

起動後、以下のコマンドでTerraformがインストールされているか確認します。

terraform --version

バージョンが表示されればOKです。

Langfuse v3のアーキテクチャについて

Langfuse v3の検証を行う前に、初めはv2で検証を行っていました。
v2は単一コンテナ+PostgreSQLのシンプルな構成でした。
ただ、v2はサポートが終了しているため、v3での検証に切り替えました。

langfuse.com

...Langfuse v2 receives security updates until end of Q1 2025.

v3では、大幅にアーキテクチャが刷新されています。

コンポーネント v2 v3
Webコンテナ
Workerコンテナ ✅(新規追加)
PostgreSQL
ClickHouse ✅(新規追加)
Redis ✅(新規追加)
S3/Blob Store ✅(新規追加)

v3では、トレースのイベントがまずS3に書き込まれ、WorkerコンテナがキューからピックアップしてClickHouseに格納するという「非同期パイプライン」になっています。
これにより、大量のトレースデータを高スループットで処理できます。

実際にバケット(例:langfuse-events-tomi)の中身を覗いてみると、以下のようにトレースやオブザベーションのデータがファイルとして格納されている様子が分かります。

デプロイ実行

terraform init
terraform plan

terraform planの結果

@user ➜ /workspaces/sakura-langfuse (main) $ terraform plan
data.sakuracloud_archive.ubuntu: Reading...
data.sakuracloud_archive.ubuntu: Read complete after 1s [id=113702234083]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # sakuracloud_disk.langfuse_disk will be created
  + resource "sakuracloud_disk" "langfuse_disk" {
      + connector            = "virtio"
      + encryption_algorithm = "none"
      + id                   = (known after apply)
      + name                 = "langfuse-server-disk"
      + plan                 = "ssd"
      + server_id            = (known after apply)
      + size                 = 20
      + source_archive_id    = "113702234083"
      + zone                 = (known after apply)
    }

  # sakuracloud_note.langfuse_init will be created
  + resource "sakuracloud_note" "langfuse_init" {
      + class       = "shell"
      + content     = (sensitive value)
      + description = (known after apply)
      + id          = (known after apply)
      + name        = "langfuse-deployment"
    }

  # sakuracloud_packet_filter.filter will be created
  + resource "sakuracloud_packet_filter" "filter" {
      + id   = (known after apply)
      + name = "langfuse-filter"
      + zone = (known after apply)

      + expression {
          + allow            = true
          + description      = "Allow HTTP"
          + destination_port = "80"
          + protocol         = "tcp"
          + source_network   = (sensitive value)
        }
      + expression {
          + allow            = true
          + description      = "Allow outbound return packets"
          + destination_port = "32768-61000"
          + protocol         = "tcp"
        }
      + expression {
          + allow            = true
          + description      = "Allow outbound return packets UDP"
          + destination_port = "32768-61000"
          + protocol         = "udp"
        }
      + expression {
          + allow       = false
          + description = "Deny ALL"
          + protocol    = "ip"
        }
    }

  # sakuracloud_server.langfuse_server will be created
  + resource "sakuracloud_server" "langfuse_server" {
      + commitment        = "standard"
      + core              = 2
      + cpu_model         = (known after apply)
      + disks             = (known after apply)
      + dns_servers       = (known after apply)
      + gateway           = (known after apply)
      + gpu_model         = (known after apply)
      + hostname          = (known after apply)
      + id                = (known after apply)
      + interface_driver  = "virtio"
      + ip_address        = (known after apply)
      + memory            = 8
      + name              = "langfuse-server"
      + netmask           = (known after apply)
      + network_address   = (known after apply)
      + private_host_name = (known after apply)
      + zone              = (known after apply)

      + disk_edit_parameter {
          + note {
              + id = (known after apply)
            }
        }

      + network_interface {
          + mac_address      = (known after apply)
          + packet_filter_id = (known after apply)
          + upstream         = "shared"
          + user_ip_address  = (known after apply)
        }
    }

Plan: 4 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + langfuse_url = (known after apply)

───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.

terraform apply

terraform apply完了後、サーバーのIPアドレスが出力されます。

Outputs:
langfuse_url = "http://xxx.xxx.xxx.xxx"

サーバー起動後、DockerコンテナのHealthCheckが通るまで1〜2分ほどかかります。

[さくらのクラウド] > [サーバー] > [該当のサーバーにチェック] > [詳細]から進捗を確認できます。

[コンソール]からプロンプト画面をクリックすると、プロンプト入力が可能になります。
以下の画面では、まだ起動が進行中のため待ちます。

Ubuntuのログイン画面が出たら、ubuntuと入力してEnterキーを押すことで、サーバーにログインできます。

コンテナの起動確認をしてみます。

sudo docker ps

全てのコンテナが Up 状態であればOKです。

これで、Codespaces の環境を削除してもインフラの状態を維持できるようになりました。

【補足】オブジェクトストレージに保存されたtfstate
terraform apply実行後、さくらのオブジェクトストレージを確認すると、設定通りtfstate-sakura-langfuseバケット内にlangfuse/terraform.tfstate が作成されていることが確認できます。


Langfuseの初期設定

terraform apply完了の画面に戻り、OutputされたURLにアクセスします。

Langfuseの画面が表示されます。

初回は誰もユーザーがいないため、Sign upします。

必要な情報を入力し、[Sign up]をクリックします。

サインアップ後、以下の順序で初期設定を行います。

1. Organization作成

[Organization] > [New Organization]をクリックします。

[Organization name]に任意の値を入れて、[Create]をクリックします。

[Organization Members]には、現在は初めに作ったユーザー以外はいないため、そのまま[Next]をクリックします。

2. Project作成

[Project name]に任意の値を入れて、[Create]をクリックします。

3. APIキー発行

[Project Settings] > [Create new API keys]をクリックします。

[Create API Keys]ダイアログで、[Note]に任意の値を入れて、[Create new keys]をクリックします。

表示された以下の3つの値をメモします。

  • Public Key(pk-lf-...
  • Secret Key(sk-lf-...
  • Host URL(http://サーバーのIPアドレス


本番検証のWebアプリ作成

作業ディレクトリに移動し、uvでプロジェクトを初期化します。

cd sakura-langfuse-chat
uv init 

必要なパッケージを追加します。
Langfuse SDKはv2系を使います。

uv add fastapi uvicorn openai python-dotenv
uv add "langfuse>=2.60.0,<3.0.0"
パッケージ 役割 今回の用途
fastapi WebAPIフレームワーク /api/chat エンドポイントを作る
uvicorn ASGIサーバー FastAPIを実際に動かすサーバー
openai OpenAI SDK さくらのAI Engine(OpenAI互換)への接続
langfuse Langfuse トレースSDK LLMのレイテンシ・トークン・エラーをLangfuseに送信
python-dotenv 環境変数管理 .env ファイルからAPIキーを読み込む

ファイル構成は以下の通りです。

sakura-langfuse-chat/
├─ .env
├─ .env.example
├─ app.py
├─ main.py
├─ pyproject.toml
├─ static/
│ └─ index.html
└─ uv.lock

【重要】.envには機密情報が含まれます。
GitHubなどで管理する場合は、誤って公開リポジトリにプッシュしないよう、必ず.gitignoreに追加されていることを確認してください。

環境変数の設定

.env.exampleをコピーして.envを作成します。

copy .env.example .env

.envに以下を設定します。

# さくらのAI Engine
SAKURA_API_KEY=さくらのAI Engineのアカウントトークン

# Langfuse(セルフホスト)
LANGFUSE_PUBLIC_KEY=pk-lf-xxxxxxxx
LANGFUSE_SECRET_KEY=sk-lf-xxxxxxxx
LANGFUSE_HOST=http://サーバーのIPアドレス

Langfuseはセルフホストサーバーのホスト名を指定するだけです。

バックエンド(app.py)のポイント

Langfuseクライアントの初期化

from langfuse import Langfuse

langfuse = Langfuse(
    public_key=os.getenv("LANGFUSE_PUBLIC_KEY"),
    secret_key=os.getenv("LANGFUSE_SECRET_KEY"),
    host=os.getenv("LANGFUSE_HOST"),
)

ホスト名を指定するだけで動作します。

トレースとGenerationの記録

# トレース(会話セッション単位)
trace = langfuse.trace(
    name="sakura-chat",
    session_id=req.session_id,
    input=req.message,
)

# Generation(LLM呼び出し単位)
generation = trace.generation(
    name="sakura-llm-call",
    model=req.model,
    input=[{"role": "user", "content": req.message}],
)

LangfuseはTrace/Generationオブジェクトを明示的に扱うスタイルです。

LLM呼び出し後の記録

generation.end(
    output=answer,
    usage={
        "input": response.usage.prompt_tokens,
        "output": response.usage.completion_tokens,
        "total": response.usage.total_tokens,
    },
    metadata={
        "latency_ms": latency_ms,
        "model_provider": "sakura-ai-engine",
    },
)
trace.update(output=answer)

トークン数・レイテンシ・モデル名が全てLangfuseに記録されます。

サーバー起動・動作確認

uv run uvicorn app:app --host 0.0.0.0 --port 8000 --reload

ブラウザで http://localhost:8000 にアクセスし、チャット画面が表示されればOKです。
チャット画面のUIは前回と同じものを使用しています。


【検証】チャットUIから各モデルの出力を確認する

3つのモデルで「LLM Observabilityとは?」と質問します。
前回と同様なので、実際の動作については以下をご参照ください。

techblog.ap-com.co.jp


【検証】LangfuseダッシュボードでTracesを確認する

ブラウザでhttp://サーバーIPにアクセスし、作成したプロジェクトを選択します。

Tracesの確認

[Tracing] > [Traces]を開くと、送信されたトレースの一覧が表示されます。
各トレースをクリックすると以下のような詳細が確認できます。

項目 記録内容
Input / Output プロンプトと応答内容
Model 使用したモデル名(gpt-oss-120bなど)
Usage 各種トークン数(Input / Output / Total)
Latency レイテンシ(ms単位)

[Tracing] > [Observations]では、LLM呼び出し単位の一覧が確認できます。
モデル名でフィルタリングすることで、モデルごとのトークン消費量・レイテンシを比較できます。

モデル 平均レイテンシ トークン消費傾向
preview/Phi-4-mini-instruct-cpu 約5分 少ない(途中で打ち切り)
gpt-oss-120b 約20秒 多い(長文出力)
llm-jp-3.1-8x13b-instruct4 約4秒 中程度(簡潔な出力)

各トレースをクリックすると、詳細が確認できます。


【検証】コスト登録(さくらのAI Engineの料金をLangfuseに反映する)

Langfuseでは、カスタムモデルのUnit Priceを手動登録することで、トークン消費量からコストを推計できます。

ただし、LangfuseのModel DefinitionはUSD建て固定のため、円建てのさくらのAI Engine料金をドル換算して登録する必要があります。

今回の換算式

Langfuse入力値 = 円料金 ÷ 150(円→ドル) ÷ 10(10Kトークン→1Kトークン)

以下、さくらのAI Engineの公式料金をもとにしています。

www.sakura.ad.jp

換算した入力値は以下の通りです。

gpt-oss-120b / llm-jp-3.1-8x13b-instruct4

項目 Langfuse入力値(per 1K tokens)
Input 0.15円/10,000トークン 0.000001
Output 0.75円/10,000トークン 0.000005

preview/Phi-4-mini-instruct-cpu

項目 Langfuse入力値(per 1K tokens)
Input 0.01円/10,000トークン 0.0000000667
Output 0.03円/10,000トークン 0.000000200

登録手順

[Settings] > [Models] > [Add Model]をクリックします。

[Create Model]で以下を入力し、[Submit]をクリックします。

項目 設定値(例:gpt-oss-120b)
Model Name gpt-oss-120b
Match Pattern (?i)^(gpt-oss-120b)$
Input price per 1k tokens 0.000001
Output price per 1k tokens 0.000005
Tokenizer None

同様に llm-jp-3.1-8x13b-instruct4preview/Phi-4-mini-instruct-cpu も登録します。

設定後に新しいトレースを送ると、コストが反映されていることが確認できます。


DatadogとLangfuseの比較

今回の検証を通じて感じた両者の特徴をまとめます。

観点 Datadog LLM Observability Langfuse(セルフホスト)
セットアップの手間 低い(APIキーのみ) 高い(サーバー構築が必要)
データの所在 Datadogのクラウド(AP1なら国内) 完全に自社インフラ内
コスト トレース量に応じた従量課金 インフラコストのみ
可視化の豊富さ 高い(既製のダッシュボードが充実) 中程度(カスタマイズ性は高い)
導入の判断軸 既存のDatadog監視と統合したい場合 データを外部に出せない場合・OSSで完結させたい場合

セットアップの手間やメンテナンスコストは、Langfuseの方が確実に大きいです。
しかし、カスタマイズ性やデータの所在などのコントロールの部分は柔軟な対応が可能です。


まとめ

さくらのAI EngineとLangfuseを組み合わせることで、トレースデータを含む全てのデータをさくらのクラウド内で完結させるLLM Observability基盤を構築できました。

Datadogとの最大の差は「データの所在」です。
プロンプトや応答内容には機密情報が含まれる場合があり、それを外部に送ることへのハードルが高い現場は少なくありません。
そのような場合、Langfuseのセルフホスト構成は有力な選択肢になります。

一方で、セットアップやメンテナンスの手間、ダッシュボードの完成度ではDatadogに軍配が上がります。
既存のDatadog監視基盤がある場合はDatadog、データを完全に自社管理したい場合はLangfuse、という使い分けが現実的な判断軸になると思います。

さくらのAI Engine + OSSツールという組み合わせで、「国内完結のAI監視基盤」を低コストで構築できるか

今回のこの検証が、皆さんのLLM運用基盤選定の参考になれば幸いです。


お知らせ

現在、弊社はさくらインターネットとパートナー契約を締結しています。 www.ap-com.co.jp

また、Datadog社ともパートナーシップを結んでおり、その知見を活かした運用支援に力を入れています。
www.ap-com.co.jp

一緒に働いていただける仲間も募集中です!
今年もまだまだ組織規模拡大中なので、ご興味持っていただけましたらぜひお声がけください。 www.ap-com.co.jp

本記事の投稿者: s_tomi