Langfuse auf einem VPS selbst hosten für LLM-Observability

11 Min. Lesezeit·Matthieu·llm-observabilityci-cddeepevaldocker-composelangfuse|

Langfuse v3 mit Docker Compose auf Ihrem eigenen VPS bereitstellen. LLM-Aufrufe tracen, Kosten verfolgen, automatisierte Evaluierungen mit DeepEval durchführen und Quality Gates in Ihre CI/CD-Pipeline integrieren.

Langfuse ist eine Open-Source-Plattform für LLM-Observability. Sie zeichnet jeden LLM-Aufruf Ihrer Anwendung auf: Latenz, Token-Verbrauch, Kosten und Prompt/Completion-Paare. Selbst gehostet mit Docker Compose bleiben alle Trace-Daten auf Ihrer Infrastruktur. Keine Abrechnung pro Event. Keine Daten, die Ihr Netzwerk verlassen.

Dieses Tutorial behandelt den gesamten Lebenszyklus: Langfuse v3 bereitstellen, TLS hinzufügen, Python- und TypeScript-Anwendungen instrumentieren und automatisierte Evaluierungspipelines mit DeepEval in CI/CD integrieren.

Was braucht Langfuse v3, um auf einem VPS zu laufen?

Langfuse v3 betreibt sechs Container: die Web-Oberfläche, einen asynchronen Worker, PostgreSQL für Metadaten, ClickHouse für Trace-Analysen, Redis für Queuing und MinIO für Blob-Speicher. Das ist eine wesentliche Änderung gegenüber v2, die nur PostgreSQL benötigte.

Komponente Zweck Standard-Port RAM-Grundlast
langfuse-web Web-Oberfläche und API 3000 ~512 MB
langfuse-worker Asynchrone Event-Verarbeitung 3030 ~512 MB
PostgreSQL 17 Transaktionale Metadaten 5432 ~256 MB
ClickHouse OLAP-Trace-Analysen 8123 (HTTP), 9000 (nativ) ~1 GB
Redis 7 Queue und Cache 6379 ~128 MB
MinIO Blob-/Medienspeicher 9000 (API), 9001 (Konsole) ~256 MB

Planen Sie mindestens 4 vCPU und 8 GB RAM ein. Ein Virtua Cloud VCS-8 (4 vCPU, 8 GB RAM, NVMe) bewältigt das problemlos. Beginnen Sie mit 100 GB Festplatte. ClickHouse wächst etwa um 1-2 GB pro Million Traces, abhängig von Prompt-/Completion-Größen.

Ressourcenbemessung nach Skalierung

Traces/Monat Festplattenwachstum/Monat Empfohlener VPS
< 100K ~500 MB 4 vCPU / 8 GB
100K - 1M 1-2 GB 4 vCPU / 8 GB
1M - 10M 10-20 GB 8 vCPU / 16 GB
> 10M 50+ GB Dediziert / Kubernetes

Voraussetzungen

Wie stelle ich Langfuse mit Docker Compose bereit?

Klonen Sie das offizielle Repository und verwenden Sie die mitgelieferte docker-compose.yml als Ausgangspunkt. Der entscheidende Schritt ist die Generierung echter Secrets statt der Platzhalter-Werte.

mkdir -p /opt/langfuse && cd /opt/langfuse

Erstellen Sie die Umgebungsdatei mit generierten Secrets:

cat > .env << 'ENVEOF'
# PostgreSQL
POSTGRES_USER=langfuse
POSTGRES_PASSWORD=REPLACE_PG
POSTGRES_DB=langfuse

# ClickHouse
CLICKHOUSE_USER=clickhouse
CLICKHOUSE_PASSWORD=REPLACE_CH

# MinIO
MINIO_ROOT_USER=minio
MINIO_ROOT_PASSWORD=REPLACE_MINIO

# Redis
REDIS_AUTH=REPLACE_REDIS

# Langfuse secrets
NEXTAUTH_SECRET=REPLACE_NEXTAUTH
SALT=REPLACE_SALT
ENCRYPTION_KEY=REPLACE_ENCRYPTION

# Langfuse config
NEXTAUTH_URL=https://langfuse.example.com
LANGFUSE_CSP_ENFORCE_HTTPS=true
KEEP_ALIVE_TIMEOUT=70
ENVEOF

Ersetzen Sie nun die Platzhalter durch echte Zufallswerte. Verwenden Sie openssl rand -hex statt -base64, da die Base64-Ausgabe die Zeichen /, + und = enthält, die PostgreSQL-Verbindungs-URLs beschädigen:

sed -i "s|REPLACE_PG|$(openssl rand -hex 32)|" .env
sed -i "s|REPLACE_CH|$(openssl rand -hex 32)|" .env
sed -i "s|REPLACE_MINIO|$(openssl rand -hex 32)|" .env
sed -i "s|REPLACE_REDIS|$(openssl rand -hex 32)|" .env
sed -i "s|REPLACE_NEXTAUTH|$(openssl rand -hex 32)|" .env
sed -i "s|REPLACE_SALT|$(openssl rand -hex 32)|" .env
sed -i "s|REPLACE_ENCRYPTION|$(openssl rand -hex 32)|" .env

Sperren Sie die Datei. Nur root sollte sie lesen können:

chmod 600 .env
ls -la .env
-rw------- 1 root root  715 Mar 19 10:00 .env

Wie löse ich den Port-9000-Konflikt zwischen MinIO und ClickHouse?

Sowohl MinIO als auch ClickHouse nutzen standardmäßig Port 9000. Die offizielle docker-compose.yml mappt den API-Port von MinIO bereits auf 9090 auf dem Host (9090:9000), wodurch der Konflikt vermieden wird. Wenn Sie eine eigene Compose-Datei schreiben, stellen Sie sicher, dass Sie einen der beiden Ports ummappen.

Die offizielle Compose-Datei bindet Infrastruktur-Ports auch nur an 127.0.0.1 (PostgreSQL, ClickHouse nativ, Redis, MinIO-Konsole), um externen Zugriff zu verhindern. Der einzige Port, der auf allen Schnittstellen exponiert wird, ist 3000 für die Web-Oberfläche, die wir hinter einen Reverse Proxy stellen.

Erstellen Sie die docker-compose.yml:

services:
  langfuse-web:
    image: docker.io/langfuse/langfuse:3
    ports:
      - "127.0.0.1:3000:3000"
    environment:
      - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
      - NEXTAUTH_URL=${NEXTAUTH_URL}
      - NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
      - SALT=${SALT}
      - ENCRYPTION_KEY=${ENCRYPTION_KEY}
      - CLICKHOUSE_MIGRATION_URL=clickhouse://clickhouse:9000
      - CLICKHOUSE_URL=http://clickhouse:8123
      - CLICKHOUSE_USER=${CLICKHOUSE_USER}
      - CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD}
      - CLICKHOUSE_CLUSTER_ENABLED=false
      - REDIS_HOST=redis
      - REDIS_PORT=6379
      - REDIS_AUTH=${REDIS_AUTH}
      - LANGFUSE_S3_EVENT_UPLOAD_BUCKET=langfuse
      - LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT=http://minio:9000
      - LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=${MINIO_ROOT_USER}
      - LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=${MINIO_ROOT_PASSWORD}
      - LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE=true
      - LANGFUSE_S3_EVENT_UPLOAD_REGION=auto
      - LANGFUSE_S3_MEDIA_UPLOAD_BUCKET=langfuse
      - LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT=http://minio:9000
      - LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID=${MINIO_ROOT_USER}
      - LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY=${MINIO_ROOT_PASSWORD}
      - LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE=true
      - LANGFUSE_S3_MEDIA_UPLOAD_REGION=auto
      - LANGFUSE_CSP_ENFORCE_HTTPS=${LANGFUSE_CSP_ENFORCE_HTTPS}
      - KEEP_ALIVE_TIMEOUT=${KEEP_ALIVE_TIMEOUT}
      - LANGFUSE_LOG_LEVEL=info
    depends_on:
      postgres:
        condition: service_healthy
      clickhouse:
        condition: service_healthy
      minio:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped

  langfuse-worker:
    image: docker.io/langfuse/langfuse-worker:3
    ports:
      - "127.0.0.1:3030:3030"
    environment:
      - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
      - NEXTAUTH_URL=${NEXTAUTH_URL}
      - SALT=${SALT}
      - ENCRYPTION_KEY=${ENCRYPTION_KEY}
      - CLICKHOUSE_MIGRATION_URL=clickhouse://clickhouse:9000
      - CLICKHOUSE_URL=http://clickhouse:8123
      - CLICKHOUSE_USER=${CLICKHOUSE_USER}
      - CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD}
      - CLICKHOUSE_CLUSTER_ENABLED=false
      - REDIS_HOST=redis
      - REDIS_PORT=6379
      - REDIS_AUTH=${REDIS_AUTH}
      - LANGFUSE_S3_EVENT_UPLOAD_BUCKET=langfuse
      - LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT=http://minio:9000
      - LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=${MINIO_ROOT_USER}
      - LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=${MINIO_ROOT_PASSWORD}
      - LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE=true
      - LANGFUSE_S3_EVENT_UPLOAD_REGION=auto
      - LANGFUSE_S3_MEDIA_UPLOAD_BUCKET=langfuse
      - LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT=http://minio:9000
      - LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID=${MINIO_ROOT_USER}
      - LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY=${MINIO_ROOT_PASSWORD}
      - LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE=true
      - LANGFUSE_S3_MEDIA_UPLOAD_REGION=auto
      - LANGFUSE_LOG_LEVEL=info
    depends_on:
      postgres:
        condition: service_healthy
      clickhouse:
        condition: service_healthy
      minio:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped

  postgres:
    image: docker.io/postgres:17
    ports:
      - "127.0.0.1:5432:5432"
    environment:
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=${POSTGRES_DB}
    volumes:
      - langfuse_postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
      interval: 3s
      timeout: 3s
      retries: 10
    restart: unless-stopped

  clickhouse:
    image: docker.io/clickhouse/clickhouse-server
    user: "101:101"
    ports:
      - "127.0.0.1:8123:8123"
      - "127.0.0.1:9000:9000"
    volumes:
      - langfuse_clickhouse_data:/var/lib/clickhouse
      - langfuse_clickhouse_logs:/var/log/clickhouse-server
    environment:
      - CLICKHOUSE_DB=default
      - CLICKHOUSE_USER=${CLICKHOUSE_USER}
      - CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD}
    healthcheck:
      test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1"]
      interval: 5s
      timeout: 5s
      retries: 10
    restart: unless-stopped

  minio:
    image: docker.io/minio/minio
    entrypoint: sh
    command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data'
    ports:
      - "127.0.0.1:9090:9000"
      - "127.0.0.1:9091:9001"
    environment:
      - MINIO_ROOT_USER=${MINIO_ROOT_USER}
      - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}
    volumes:
      - langfuse_minio_data:/data
    healthcheck:
      test: ["CMD", "mc", "ready", "local"]
      interval: 3s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  redis:
    image: docker.io/redis:7
    command: redis-server --requirepass ${REDIS_AUTH} --maxmemory-policy noeviction
    ports:
      - "127.0.0.1:6379:6379"
    volumes:
      - langfuse_redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_AUTH}", "ping"]
      interval: 3s
      timeout: 3s
      retries: 10
    restart: unless-stopped

volumes:
  langfuse_postgres_data:
  langfuse_clickhouse_data:
  langfuse_clickhouse_logs:
  langfuse_minio_data:
  langfuse_redis_data:

Einige Anmerkungen zu dieser Compose-Datei:

  • Der Web-Container bindet an 127.0.0.1:3000 statt 0.0.0.0:3000. Der gesamte Verkehr läuft über den Reverse Proxy.
  • CLICKHOUSE_MIGRATION_URL verwendet das native Protokoll (clickhouse://) auf Port 9000, während CLICKHOUSE_URL HTTP auf Port 8123 nutzt. Beide sind erforderlich. Fehlt CLICKHOUSE_MIGRATION_URL, stürzt der Web-Container beim Start ab.
  • Redis verwendet separate REDIS_HOST-, REDIS_PORT- und REDIS_AUTH-Variablen statt eines Connection Strings. Das ist das Format, das Langfuse v3 erwartet.
  • Der MinIO-Entrypoint erstellt das langfuse-Bucket-Verzeichnis beim ersten Start (mkdir -p /data/langfuse). Ohne das schlagen S3-Uploads fehl, bis der Bucket existiert.
  • ClickHouse läuft als user: "101:101", um dem internen clickhouse-Benutzer des Containers zu entsprechen.
  • Redis läuft mit --maxmemory-policy noeviction, um Datenverlust bei knappem Speicher zu verhindern. Langfuse nutzt Redis für seine Job-Queue, und das Verdrängen von Schlüsseln würde zu stillem Datenverlust führen.

Starten Sie den Stack:

docker compose up -d

Warten Sie etwa 2-3 Minuten, bis alle Container initialisiert sind. ClickHouse und PostgreSQL führen beim ersten Start Migrationen aus. Prüfen Sie den Status:

docker compose ps

Alle sechs Container sollten Up anzeigen. Die Infrastruktur-Container (postgres, clickhouse, minio, redis) zeigen (healthy). Die Container langfuse-web und langfuse-worker definieren keine Healthchecks und zeigen Up ohne Gesundheits-Tag. Wenn ein Container sich in einer Neustartschleife befindet, prüfen Sie seine Logs:

docker compose logs langfuse-web --tail 50

Rufen Sie den Health-Endpoint auf, um zu bestätigen, dass die API läuft und die Datenbankverbindung funktioniert:

curl -s http://localhost:3000/api/public/health?failIfDatabaseUnavailable=true | python3 -m json.tool
{
    "status": "OK",
    "version": "3.160.0"
}

Wie füge ich TLS zu einer selbst gehosteten Langfuse-Instanz hinzu?

Stellen Sie Caddy vor Langfuse. Caddy übernimmt die TLS-Zertifikatsbeschaffung von Let's Encrypt automatisch. Keine Certbot-Cronjobs, keine manuelle Erneuerung.

Installieren Sie Caddy:

apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
apt update && apt install -y caddy

Erstellen Sie das Caddyfile:

cat > /etc/caddy/Caddyfile << 'EOF'
langfuse.example.com {
    header -Server
    reverse_proxy 127.0.0.1:3000 {
        transport http {
            keepalive 75s
            keepalive_idle_conns 10
        }
    }
}
EOF

Der keepalive 75s-Wert liegt höher als Langfuses KEEP_ALIVE_TIMEOUT=70, um zu verhindern, dass der Reverse Proxy veraltete Verbindungen hält. Diese Diskrepanz ist die Hauptursache für die intermittierenden 502/504-Fehler, auf die viele Selbsthoster stoßen. Die header -Server-Direktive entfernt Versionsinformationen aus den Antworten.

systemctl enable --now caddy

enable sorgt dafür, dass Caddy Neustarts überlebt. --now startet es sofort.

systemctl status caddy
 caddy.service - Caddy
     Loaded: loaded (/usr/lib/systemd/system/caddy.service; enabled; preset: enabled)
     Active: active (running)

Testen Sie von Ihrem lokalen Rechner den TLS-Endpoint:

curl -I https://langfuse.example.com/api/public/health
HTTP/2 200
content-type: application/json

Authentifizierung und API-Schlüssel konfigurieren

Öffnen Sie https://langfuse.example.com in Ihrem Browser. Erstellen Sie das erste Benutzerkonto. Dieses wird zum Admin-Konto.

Nach der Anmeldung:

  1. Erstellen Sie ein neues Projekt (z. B. "production")
  2. Gehen Sie zu Settings > API Keys
  3. Klicken Sie auf Create API Key
  4. Speichern Sie den Public Key und den Secret Key. Sie benötigen beide, um Ihre Anwendungen zu instrumentieren.

Der Public Key identifiziert Ihr Projekt. Der Secret Key authentifiziert Schreibvorgänge. Behandeln Sie den Secret Key wie ein Datenbank-Passwort.

Wie instrumentiere ich eine Python-Anwendung, um Traces an Langfuse zu senden?

Das Langfuse Python SDK basiert auf OpenTelemetry. Der @observe()-Dekorator erstellt automatisch Traces und Spans für dekorierte Funktionen. Verschachtelte Aufrufe erzeugen verschachtelte Spans in der Langfuse-Oberfläche.

Installieren Sie das SDK:

pip install langfuse openai

Setzen Sie die Umgebungsvariablen, die auf Ihre selbst gehostete Instanz zeigen:

export LANGFUSE_PUBLIC_KEY="pk-lf-..."
export LANGFUSE_SECRET_KEY="sk-lf-..."
export LANGFUSE_HOST="https://langfuse.example.com"
export OPENAI_API_KEY="sk-..."

Hier ist eine instrumentierte Anwendung:

from langfuse import observe, propagate_attributes, get_client
from langfuse.openai import openai  # patched OpenAI client

@observe()
def retrieve_context(query: str) -> str:
    # Simulating a retrieval step
    return "Paris is the capital of France, with a population of 2.1 million."

@observe()
def answer_question(query: str) -> str:
    context = retrieve_context(query)

    response = openai.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": f"Answer based on this context: {context}"},
            {"role": "user", "content": query},
        ],
    )
    return response.choices[0].message.content

@observe()
def run_pipeline(query: str) -> str:
    with propagate_attributes(
        user_id="user_42",
        session_id="session_abc",
        tags=["production", "rag-pipeline"],
    ):
        return answer_question(query)

result = run_pipeline("What is the capital of France?")
print(result)

# Flush traces before exit in short-lived scripts
get_client().shutdown()

Das @observe() auf run_pipeline erstellt den Root-Trace. answer_question und retrieve_context werden zu verschachtelten Spans. Der gepatchte openai-Import (from langfuse.openai import openai) erfasst automatisch Modellname, Token-Anzahl, Latenz und Kosten als Generation-Span. Der propagate_attributes-Kontextmanager hängt Benutzer- und Session-Metadaten an alle verschachtelten Beobachtungen an.

Öffnen Sie nach dem Ausführen dieses Skripts das Langfuse-Dashboard. Der Trace Explorer zeigt den vollständigen Aufrufbaum mit Timing für jeden Span.

Wie instrumentiere ich eine TypeScript-Anwendung?

Installieren Sie das Langfuse TypeScript SDK und das OpenTelemetry Node SDK:

npm install @langfuse/tracing @langfuse/otel @opentelemetry/sdk-node

Setzen Sie dieselben Umgebungsvariablen:

export LANGFUSE_PUBLIC_KEY="pk-lf-..."
export LANGFUSE_SECRET_KEY="sk-lf-..."
export LANGFUSE_BASEURL="https://langfuse.example.com"
import {
  observe,
  startActiveObservation,
  propagateAttributes,
} from "@langfuse/tracing";

const fetchContext = observe(
  async (query: string) => {
    return "Paris is the capital of France.";
  },
  { name: "fetch-context", asType: "span" }
);

const callLLM = observe(
  async (query: string, context: string) => {
    // Replace with your actual LLM call
    return `Based on context: ${context}, the answer is Paris.`;
  },
  { name: "llm-call", asType: "generation" }
);

await startActiveObservation("rag-pipeline", async (root) => {
  await propagateAttributes(
    { userId: "user_42", sessionId: "session_abc" },
    async () => {
      const context = await fetchContext("Capital of France?");
      const answer = await callLLM("Capital of France?", context);
      root.update({ output: { answer } });
    }
  );
});

Rufen Sie forceFlush() auf dem Span-Processor auf, bevor Serverless-Funktionen oder Skripte beendet werden, um Datenverlust zu vermeiden.

Dashboard-Übersicht

Das Langfuse-Dashboard bietet vier zentrale Ansichten:

Trace Explorer zeigt individuelle Request-Traces. Klicken Sie auf einen Trace, um den vollständigen Span-Baum zu sehen: welche Funktionen liefen, wie lange jede dauerte und welche Prompts/Completions an das LLM gesendet wurden. Filtern Sie nach Benutzer-ID, Tags oder Zeitraum.

Kostenverfolgung schlüsselt die Ausgaben nach Modell auf. Sie sehen, welche Modelle die meisten Tokens verbrauchen und welche API-Aufrufe teuer sind. Nutzen Sie dies, um Prompts zu identifizieren, die Tokens für unnötig lange Completions verschwenden.

Latenz-Perzentile (p50, p90, p99) pro Endpoint oder Span-Name. Wenn Ihr p99 ansteigt, können Sie die langsamsten Traces analysieren, um den Engpass zu finden.

Token-Nutzungstrends über die Zeit. Achten Sie auf unerwartete Sprünge, die auf eine Prompt-Änderung oder einen Bug hindeuten, der längere Ausgaben erzeugt.

Wie richte ich automatisierte LLM-Evaluierung mit DeepEval und Langfuse ein?

DeepEval ist ein Open-Source-Framework zur LLM-Evaluierung, das Modellausgaben anhand von Metriken wie Halluzination, Treue und Relevanz bewertet. In Kombination mit Langfuse können Sie Produktions-Traces abrufen, Evaluierungen darauf ausführen und Bewertungen zurück an das Langfuse-Dashboard senden.

Installieren Sie DeepEval:

pip install deepeval

Evaluierungsmetriken

Metrik Was sie misst Wertebereich Wann verwenden
Hallucination Faktische Korrektheit gegen bereitgestellten Kontext 0-1 (1 = keine Halluzination) RAG-Pipelines
Faithfulness Übereinstimmung der Ausgabe mit dem Abrufkontext 0-1 (1 = treu) RAG-Pipelines
Answer Relevancy Ob die Antwort die Frage beantwortet 0-1 (1 = relevant) Jede LLM-Anwendung
G-Eval Benutzerdefinierte Kriterien via LLM-as-Judge 0-1 Individuelle Qualitätsprüfungen

Evaluierungsskript

Dieses Skript ruft aktuelle Traces von Langfuse ab, führt DeepEval-Metriken aus und sendet Bewertungen zurück:

import os
from langfuse import Langfuse
from deepeval.metrics import GEval, AnswerRelevancyMetric
from deepeval.test_case import LLMTestCase, LLMTestCaseParams

langfuse = Langfuse(
    public_key=os.environ["LANGFUSE_PUBLIC_KEY"],
    secret_key=os.environ["LANGFUSE_SECRET_KEY"],
    host=os.environ["LANGFUSE_HOST"],
)

# Fetch traces tagged for evaluation
traces = langfuse.api.trace.list(
    tags=["eval-candidate"],
    limit=50,
).data

# Define metrics
relevancy = AnswerRelevancyMetric(threshold=0.7)
correctness = GEval(
    name="Correctness",
    criteria="Determine whether the output is factually correct based on the context.",
    evaluation_params=[
        LLMTestCaseParams.ACTUAL_OUTPUT,
        LLMTestCaseParams.EXPECTED_OUTPUT,
    ],
)

for trace in traces:
    test_case = LLMTestCase(
        input=trace.input,
        actual_output=trace.output,
        retrieval_context=[trace.metadata.get("context", "")],
    )

    relevancy.measure(test_case)
    correctness.measure(test_case)

    # Push scores back to Langfuse
    langfuse.create_score(
        trace_id=trace.id,
        name="relevancy",
        value=relevancy.score,
        comment=relevancy.reason,
    )
    langfuse.create_score(
        trace_id=trace.id,
        name="correctness",
        value=correctness.score,
        comment=correctness.reason,
    )

langfuse.shutdown()
print(f"Evaluated {len(traces)} traces")

Nach der Ausführung erscheinen die Evaluierungsbewertungen im Langfuse-Dashboard neben den ursprünglichen Traces. Sie können Traces nach Bewertung filtern, um qualitativ minderwertige Ausgaben zu finden.

Wie führe ich LLM-Evaluierungen in einer CI/CD-Pipeline aus?

Integrieren Sie DeepEval in GitHub Actions, um Qualitätsregressionen abzufangen, bevor sie die Produktion erreichen. Der Workflow führt Ihre Evaluierungssuite bei jedem Pull Request gegen einen Testdatensatz aus.

Erstellen Sie .github/workflows/llm-eval.yml:

name: LLM Evaluation

on:
  pull_request:
    paths:
      - 'prompts/**'
      - 'src/llm/**'

jobs:
  evaluate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install dependencies
        run: pip install deepeval langfuse openai

      - name: Run evaluation suite
        env:
          LANGFUSE_PUBLIC_KEY: ${{ secrets.LANGFUSE_PUBLIC_KEY }}
          LANGFUSE_SECRET_KEY: ${{ secrets.LANGFUSE_SECRET_KEY }}
          LANGFUSE_HOST: ${{ secrets.LANGFUSE_HOST }}
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: deepeval test run tests/test_llm_quality.py

      - name: Upload results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: eval-results
          path: .deepeval/

Erstellen Sie die Testdatei tests/test_llm_quality.py:

import deepeval
from deepeval import assert_test
from deepeval.test_case import LLMTestCase
from deepeval.metrics import AnswerRelevancyMetric, FaithfulnessMetric
from deepeval.dataset import EvaluationDataset

# Load test cases from a JSON file or define inline
test_cases = [
    LLMTestCase(
        input="What is the capital of France?",
        actual_output=my_llm_function("What is the capital of France?"),
        retrieval_context=["France is a country in Western Europe. Its capital is Paris."],
    ),
    # Add more test cases covering your prompt changes
]

dataset = EvaluationDataset(test_cases=test_cases)

@deepeval.parametrize(dataset)
def test_answer_relevancy(test_case):
    metric = AnswerRelevancyMetric(threshold=0.7)
    assert_test(test_case, [metric])

@deepeval.parametrize(dataset)
def test_faithfulness(test_case):
    metric = FaithfulnessMetric(threshold=0.7)
    assert_test(test_case, [metric])

Der Workflow löst nur aus, wenn sich Prompt-Vorlagen oder LLM-Code ändern. Wenn eine Metrik unter den Schwellenwert fällt, schlägt der PR-Build fehl. Entwickler sehen in den GitHub Actions Logs, welcher Testfall fehlgeschlagen ist und um wie viel.

Wie sichere ich PostgreSQL und ClickHouse für Langfuse?

Selbst hosten ohne Backups ist fahrlässig. PostgreSQL enthält Benutzerkonten, Projekteinstellungen und API-Schlüssel. ClickHouse enthält alle Trace-Daten. MinIO enthält Medien-Uploads und Batch-Exporte.

PostgreSQL-Backup

docker compose exec postgres pg_dump -U langfuse langfuse | gzip > /opt/backups/langfuse-pg-$(date +%F).sql.gz

ClickHouse-Backup

Exportieren Sie Tabellen einzeln mit dem ClickHouse-Client:

docker compose exec clickhouse clickhouse-client \
  --user clickhouse \
  --password "$(grep CLICKHOUSE_PASSWORD /opt/langfuse/.env | cut -d= -f2)" \
  --query "SELECT * FROM traces FORMAT Native" > /opt/backups/traces-$(date +%F).native

ClickHouse unterstützt auch den Befehl BACKUP DATABASE für vollständige Backups, erfordert aber die Konfiguration eines allowed_disk in der ClickHouse-Server-Konfiguration. Für Docker-Deployments ist der obige Tabellen-Export einfacher.

MinIO-Backup

Synchronisieren Sie MinIO-Daten zu einem entfernten S3-kompatiblen Bucket oder einem lokalen Verzeichnis:

docker run --rm --network langfuse_default \
  minio/mc alias set local http://minio:9000 minio "$(grep MINIO_ROOT_PASSWORD /opt/langfuse/.env | cut -d= -f2)" && \
  mc mirror local/langfuse /opt/backups/minio-langfuse/

Automatisierung mit cron

cat > /etc/cron.d/langfuse-backup << 'EOF'
0 3 * * * root cd /opt/langfuse && docker compose exec -T postgres pg_dump -U langfuse langfuse | gzip > /opt/backups/langfuse-pg-$(date +\%F).sql.gz
0 4 * * * root find /opt/backups -name "langfuse-pg-*.sql.gz" -mtime +14 -delete
EOF
chmod 644 /etc/cron.d/langfuse-backup

Produktionshärtung

Update-Prozedur

Neue Images herunterladen und neu starten:

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

Langfuse führt Datenbankmigrationen beim Start automatisch aus. Prüfen Sie nach einem Update die Logs des Web-Containers:

docker compose logs langfuse-web --tail 20

Suchen Sie nach Ready in der Ausgabe. Wenn Migrationen fehlschlagen, startet der Container nicht. Setzen Sie auf die vorherige Version zurück, indem Sie den Version-Tag in docker-compose.yml fixieren (z. B. langfuse/langfuse:3.x.x).

Langfuse selbst überwachen

Der Health-Endpoint unter /api/public/health gibt 200 OK zurück, wenn die API funktioniert. Fügen Sie ?failIfDatabaseUnavailable=true für eine tiefere Prüfung inklusive Datenbankverbindung hinzu.

Überwachen Sie die Festplattennutzung des ClickHouse-Volumes. Es ist die am schnellsten wachsende Komponente:

docker system df -v | grep clickhouse

Überwachen Sie den Container-Speicherverbrauch mit:

docker stats --no-stream --format "table {{.Name}}\t{{.MemUsage}}\t{{.CPUPerc}}"

Firewall

Nur die Ports 22 (SSH), 80 und 443 sollten offen sein. Alle Datenbankports sind in der Compose-Datei an 127.0.0.1 gebunden, aber eine Firewall bietet zusätzliche Sicherheit Linux-VPS-Firewall mit UFW und nftables einrichten:

ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable

Langfuse vs LangSmith vs Opik

Feature Langfuse LangSmith Opik
Lizenz MIT (Open Source) Proprietär Apache 2.0
Self-Hosting Docker Compose / K8s Nur Enterprise-Lizenz Docker Compose / K8s
Trace-Speicher Ihre Infrastruktur LangChains Cloud Ihre Infrastruktur
Eval-Framework Extern (DeepEval, etc.) Integriert Integriert
OpenTelemetry Native Unterstützung Nein Teilweise

Langfuse und Opik sind die beiden brauchbaren Open-Source-Optionen für Self-Hosting. Langfuse hat eine größere Community und mehr Integrationen. LangSmith erfordert eine Enterprise-Lizenz für Self-Hosting. Weitere Informationen finden Sie in der Langfuse Self-Hosting-Dokumentation.

Fehlerbehebung

Container bleibt unhealthy: Prüfen Sie docker compose logs <service> --tail 100. Häufige Ursachen: falsches Passwort in .env (ClickHouse unterscheidet Groß-/Kleinschreibung bei Benutzernamen), oder MinIO kann beim ersten Start keine Buckets initialisieren. Starten Sie den Stack neu mit docker compose down && docker compose up -d.

Web-Container stürzt mit "invalid port number" ab: Ihr PostgreSQL-Passwort enthält Sonderzeichen (/, +, =), die die postgresql://-Verbindungs-URL beschädigen. Generieren Sie alle Passwörter neu mit openssl rand -hex 32 statt -base64, dann führen Sie docker compose down -v && docker compose up -d aus, um mit den neuen Zugangsdaten neu zu starten.

Web-Container stürzt mit "CLICKHOUSE_MIGRATION_URL is not configured" ab: Fügen Sie CLICKHOUSE_MIGRATION_URL=clickhouse://clickhouse:9000 zu den Environment-Sektionen von langfuse-web und langfuse-worker hinzu. Dies nutzt das native ClickHouse-Protokoll auf Port 9000, getrennt von der HTTP-API auf Port 8123.

502/504-Fehler über den Reverse Proxy: Setzen Sie KEEP_ALIVE_TIMEOUT im Langfuse-Web-Container auf einen Wert höher als den Idle-Timeout Ihres Reverse Proxy. Caddy verwendet standardmäßig 30s Keepalive. Wir setzen Langfuse auf 70s und Caddy auf 75s.

Worker-Shutdown dauert lange: Unter Last leert der Worker seine Queue vor dem Stoppen. Das kann bis zu einer Stunde dauern. Für einen schnelleren Shutdown skalieren Sie zuerst auf null: docker compose stop langfuse-worker, warten Sie, bis die Queue leer ist, dann fahren Sie fort.

ClickHouse-Festplatte füllt sich: Richten Sie Datenaufbewahrung ein. Langfuse bietet integrierte Aufbewahrungseinstellungen im Dashboard unter Settings > Data Retention. Konfigurieren Sie den Trace-TTL basierend auf Ihrer Speicherkapazität.

Logs: Alle Langfuse-Container schreiben nach stdout. Betrachten Sie sie mit:

journalctl -u docker -f
docker compose logs -f

AIOps auf einem VPS: KI-gesteuerte Serververwaltung mit Open-Source-Tools KI-Agenten auf einem VPS selbst hosten Eigenen MCP-Server auf einem VPS bauen und selbst hosten [-> docker-compose-multi-service-vps]