Langfuse auf einem VPS selbst hosten für LLM-Observability
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
- Ein VPS mit Debian 12 oder Ubuntu 24.04 mit mindestens 4 vCPU und 8 GB RAM
- Docker und Docker Compose installiert Docker in Produktion auf einem VPS: Was schiefgeht und wie Sie es beheben
- Ein Domainname mit einem A-Record, der auf die IP Ihres VPS zeigt (für TLS)
- SSH-Zugang mit schlüsselbasierter Authentifizierung SSH-Härtung auf einem Linux-VPS: sshd_config Sicherheitsanleitung
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:3000statt0.0.0.0:3000. Der gesamte Verkehr läuft über den Reverse Proxy. CLICKHOUSE_MIGRATION_URLverwendet das native Protokoll (clickhouse://) auf Port 9000, währendCLICKHOUSE_URLHTTP auf Port 8123 nutzt. Beide sind erforderlich. FehltCLICKHOUSE_MIGRATION_URL, stürzt der Web-Container beim Start ab.- Redis verwendet separate
REDIS_HOST-,REDIS_PORT- undREDIS_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:
- Erstellen Sie ein neues Projekt (z. B. "production")
- Gehen Sie zu Settings > API Keys
- Klicken Sie auf Create API Key
- 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]
Bereit, es selbst auszuprobieren?
Stellen Sie Ihren eigenen Server in Sekunden bereit. Linux, Windows oder FreeBSD. →