Auto-héberger Langfuse sur un VPS pour l'observabilité LLM
Déployez Langfuse v3 avec Docker Compose sur votre propre VPS. Tracez les appels LLM, suivez les coûts, lancez des évaluations automatisées avec DeepEval et intégrez des portes qualité dans votre pipeline CI/CD.
Langfuse est une plateforme open source d'observabilité LLM. Elle trace chaque appel LLM de votre application : latence, consommation de tokens, coût et paires prompt/complétion. L'auto-héberger avec Docker Compose garde toutes les données de trace sur votre infrastructure. Pas de facturation à l'événement. Pas de données qui quittent votre réseau.
Ce tutoriel couvre le cycle complet : déployer Langfuse v3, ajouter TLS, instrumenter des applications Python et TypeScript, et construire des pipelines d'évaluation automatisés avec DeepEval intégrés au CI/CD.
De quoi Langfuse v3 a-t-il besoin pour tourner sur un VPS ?
Langfuse v3 fait tourner six conteneurs : l'interface web, un worker asynchrone, PostgreSQL pour les métadonnées, ClickHouse pour l'analytique des traces, Redis pour la file d'attente et MinIO pour le stockage d'objets. C'est un changement important par rapport à la v2, qui n'avait besoin que de PostgreSQL.
| Composant | Rôle | Port par défaut | RAM de base |
|---|---|---|---|
| langfuse-web | Interface web et API | 3000 | ~512 Mo |
| langfuse-worker | Traitement asynchrone des événements | 3030 | ~512 Mo |
| PostgreSQL 17 | Métadonnées transactionnelles | 5432 | ~256 Mo |
| ClickHouse | Analytique OLAP des traces | 8123 (HTTP), 9000 (natif) | ~1 Go |
| Redis 7 | File d'attente et cache | 6379 | ~128 Mo |
| MinIO | Stockage d'objets/médias | 9000 (API), 9001 (console) | ~256 Mo |
Prévoyez au minimum 4 vCPU et 8 Go de RAM. Un Virtua Cloud VCS-8 (4 vCPU, 8 Go RAM, NVMe) gère ça confortablement. Commencez avec 100 Go de disque. ClickHouse grossit d'environ 1 à 2 Go par million de traces selon la taille des prompts/complétions.
Dimensionnement par volume
| Traces/mois | Croissance disque/mois | VPS recommandé |
|---|---|---|
| < 100K | ~500 Mo | 4 vCPU / 8 Go |
| 100K - 1M | 1-2 Go | 4 vCPU / 8 Go |
| 1M - 10M | 10-20 Go | 8 vCPU / 16 Go |
| > 10M | 50+ Go | Dédié / Kubernetes |
Prérequis
- Un VPS sous Debian 12 ou Ubuntu 24.04 avec au moins 4 vCPU et 8 Go de RAM
- Docker et Docker Compose installés Docker en production sur un VPS : ce qui casse et comment corriger
- Un nom de domaine avec un enregistrement A pointant vers l'IP de votre VPS (pour TLS)
- Un accès SSH avec authentification par clé Sécuriser SSH sur un VPS Linux : guide sshd_config
Comment déployer Langfuse avec Docker Compose ?
Clonez le dépôt officiel et utilisez le docker-compose.yml fourni comme point de départ. L'étape clé consiste à générer de vrais secrets au lieu d'utiliser les valeurs par défaut.
mkdir -p /opt/langfuse && cd /opt/langfuse
Créez le fichier d'environnement avec des secrets générés :
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
Remplacez les valeurs temporaires par de vrais aléatoires. Utilisez openssl rand -hex plutôt que -base64 car la sortie base64 contient des caractères /, + et = qui cassent les URL de connexion PostgreSQL :
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
Verrouillez le fichier. Seul root doit pouvoir le lire :
chmod 600 .env
ls -la .env
-rw------- 1 root root 715 Mar 19 10:00 .env
Comment résoudre le conflit de port 9000 entre MinIO et ClickHouse ?
MinIO et ClickHouse utilisent tous les deux le port 9000 par défaut. Le docker-compose.yml officiel mappe déjà le port API de MinIO sur 9090 côté hôte (9090:9000), ce qui évite le conflit. Si vous écrivez un fichier compose personnalisé, pensez à remapper l'un des deux.
Le fichier compose officiel lie aussi les ports d'infrastructure à 127.0.0.1 uniquement (PostgreSQL, ClickHouse natif, Redis, console MinIO), empêchant tout accès externe. Le seul port exposé sur toutes les interfaces est le 3000 pour l'interface web, que nous allons placer derrière un reverse proxy.
Créez le 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:
Quelques points à noter dans ce fichier compose :
- Le conteneur web écoute sur
127.0.0.1:3000au lieu de0.0.0.0:3000. Tout le trafic passe par le reverse proxy. CLICKHOUSE_MIGRATION_URLutilise le protocole natif (clickhouse://) sur le port 9000, tandis queCLICKHOUSE_URLutilise HTTP sur le port 8123. Les deux sont nécessaires. SiCLICKHOUSE_MIGRATION_URLmanque, le conteneur web plante au démarrage.- Redis utilise des variables séparées
REDIS_HOST,REDIS_PORTetREDIS_AUTHau lieu d'une chaîne de connexion. C'est le format attendu par Langfuse v3. - L'entrypoint de MinIO crée le répertoire du bucket
langfuseau premier démarrage (mkdir -p /data/langfuse). Sans ça, les uploads S3 échouent tant que le bucket n'existe pas. - ClickHouse tourne avec
user: "101:101"pour correspondre à l'utilisateur clickhouse interne au conteneur. - Redis tourne avec
--maxmemory-policy noevictionpour éviter la perte de données quand la mémoire est limitée. Langfuse s'appuie sur Redis pour sa file de travaux ; évincer des clés causerait une perte de données silencieuse.
Démarrez la pile :
docker compose up -d
Attendez environ 2-3 minutes que tous les conteneurs s'initialisent. ClickHouse et PostgreSQL exécutent les migrations au premier démarrage. Vérifiez l'état :
docker compose ps
Les six conteneurs doivent afficher Up. Les conteneurs d'infrastructure (postgres, clickhouse, minio, redis) affichent (healthy). Les conteneurs langfuse-web et langfuse-worker n'ont pas de healthcheck défini et affichent Up sans indicateur de santé. Si un conteneur redémarre en boucle, consultez ses logs :
docker compose logs langfuse-web --tail 50
Interrogez le point de santé pour confirmer que l'API est opérationnelle et que la connexion à la base de données fonctionne :
curl -s http://localhost:3000/api/public/health?failIfDatabaseUnavailable=true | python3 -m json.tool
{
"status": "OK",
"version": "3.160.0"
}
Comment ajouter TLS à une instance Langfuse auto-hébergée ?
Placez Caddy devant Langfuse. Caddy gère automatiquement l'obtention des certificats TLS auprès de Let's Encrypt. Pas de tâche cron certbot, pas de renouvellement manuel.
Installez 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
Créez le 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
Le keepalive 75s est réglé plus haut que le KEEP_ALIVE_TIMEOUT=70 de Langfuse pour éviter que le reverse proxy garde des connexions périmées. Ce décalage est la cause principale des erreurs 502/504 intermittentes que rencontrent beaucoup d'auto-hébergeurs. La directive header -Server supprime les informations de version des réponses.
systemctl enable --now caddy
enable fait survivre Caddy aux redémarrages. --now le démarre immédiatement.
systemctl status caddy
● caddy.service - Caddy
Loaded: loaded (/usr/lib/systemd/system/caddy.service; enabled; preset: enabled)
Active: active (running)
Depuis votre machine locale, testez le point d'accès TLS :
curl -I https://langfuse.example.com/api/public/health
HTTP/2 200
content-type: application/json
Configurer l'authentification et les clés API
Ouvrez https://langfuse.example.com dans votre navigateur. Créez le premier compte utilisateur. Il devient le compte administrateur.
Après connexion :
- Créez un nouveau projet (par exemple « production »)
- Allez dans Settings > API Keys
- Cliquez sur Create API Key
- Sauvegardez la Public Key et la Secret Key. Vous avez besoin des deux pour instrumenter vos applications.
La clé publique identifie votre projet. La clé secrète authentifie les écritures. Traitez la clé secrète comme un mot de passe de base de données.
Comment instrumenter une application Python pour envoyer des traces à Langfuse ?
Le SDK Python de Langfuse repose sur OpenTelemetry. Le décorateur @observe() crée automatiquement des traces et des spans pour les fonctions décorées. Les appels imbriqués produisent des spans imbriqués dans l'interface Langfuse.
Installez le SDK :
pip install langfuse openai
Définissez les variables d'environnement pointant vers votre instance auto-hébergée :
export LANGFUSE_PUBLIC_KEY="pk-lf-..."
export LANGFUSE_SECRET_KEY="sk-lf-..."
export LANGFUSE_HOST="https://langfuse.example.com"
export OPENAI_API_KEY="sk-..."
Voici une application tracée :
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()
Le @observe() sur run_pipeline crée la trace racine. answer_question et retrieve_context deviennent des spans imbriqués. L'import patché d'openai (from langfuse.openai import openai) capture automatiquement le nom du modèle, le nombre de tokens, la latence et le coût sous forme de span de génération. Le gestionnaire de contexte propagate_attributes attache les métadonnées utilisateur et session à toutes les observations imbriquées.
Après avoir exécuté ce script, ouvrez le tableau de bord Langfuse. Le Trace Explorer affiche l'arbre d'appels complet avec le timing de chaque span.
Comment instrumenter une application TypeScript ?
Installez le SDK TypeScript de Langfuse et le SDK Node OpenTelemetry :
npm install @langfuse/tracing @langfuse/otel @opentelemetry/sdk-node
Définissez les mêmes variables d'environnement :
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 } });
}
);
});
Appelez forceFlush() sur le span processor avant la sortie des fonctions serverless ou des scripts pour éviter la perte de données.
Tour d'horizon du tableau de bord
Le tableau de bord Langfuse offre quatre vues principales :
Trace Explorer affiche les traces de requêtes individuelles. Cliquez sur une trace pour voir l'arbre complet des spans : quelles fonctions ont tourné, combien de temps chacune a pris et les prompts/complétions exacts envoyés au LLM. Filtrez par identifiant utilisateur, tags ou plage temporelle.
Suivi des coûts ventile les dépenses par modèle. Vous voyez quels modèles consomment le plus de tokens et quels appels API sont coûteux. Utilisez-le pour repérer les prompts qui gaspillent des tokens sur des complétions inutilement longues.
Percentiles de latence (p50, p90, p99) par endpoint ou nom de span. Si votre p99 grimpe, vous pouvez explorer les traces les plus lentes pour trouver le goulot d'étranglement.
Tendances d'utilisation des tokens dans le temps. Surveillez les sauts inattendus qui indiquent un changement de prompt ou un bug générant des sorties plus longues.
Comment mettre en place une évaluation LLM automatisée avec DeepEval et Langfuse ?
DeepEval est un framework open source d'évaluation LLM qui note les sorties de modèles sur des métriques comme l'hallucination, la fidélité et la pertinence. Combiné à Langfuse, vous pouvez récupérer les traces de production, lancer des évaluations dessus et renvoyer les scores au tableau de bord Langfuse.
Installez DeepEval :
pip install deepeval
Métriques d'évaluation
| Métrique | Ce qu'elle mesure | Plage de score | Quand l'utiliser |
|---|---|---|---|
| Hallucination | Exactitude factuelle par rapport au contexte fourni | 0-1 (1 = pas d'hallucination) | Pipelines RAG |
| Faithfulness | Alignement de la sortie avec le contexte de récupération | 0-1 (1 = fidèle) | Pipelines RAG |
| Answer Relevancy | La réponse répond-elle à la question | 0-1 (1 = pertinent) | Toute application LLM |
| G-Eval | Critères personnalisés via LLM-as-judge | 0-1 | Contrôles qualité personnalisés |
Script d'évaluation
Ce script récupère les traces récentes de Langfuse, exécute les métriques DeepEval et renvoie les scores :
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")
Après exécution, les scores d'évaluation apparaissent dans le tableau de bord Langfuse aux côtés des traces originales. Vous pouvez filtrer les traces par score pour trouver les sorties de mauvaise qualité.
Comment lancer des évaluations LLM dans un pipeline CI/CD ?
Intégrez DeepEval dans GitHub Actions pour détecter les régressions de qualité avant qu'elles n'atteignent la production. Le workflow exécute votre suite d'évaluation sur un jeu de données de test à chaque pull request.
Créez .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/
Créez le fichier de test 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])
Le workflow ne se déclenche que quand les templates de prompts ou le code LLM changent. Si une métrique passe sous le seuil, le build de la PR échoue. Les développeurs voient quel cas de test a échoué et de combien dans les logs GitHub Actions.
Comment sauvegarder PostgreSQL et ClickHouse pour Langfuse ?
Auto-héberger sans sauvegardes est un risque. PostgreSQL contient les comptes utilisateurs, les paramètres de projet et les clés API. ClickHouse contient toutes les données de trace. MinIO contient les uploads médias et les exports par lots.
Sauvegarde PostgreSQL
docker compose exec postgres pg_dump -U langfuse langfuse | gzip > /opt/backups/langfuse-pg-$(date +%F).sql.gz
Sauvegarde ClickHouse
Exportez les tables individuellement avec le client ClickHouse :
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 supporte aussi la commande BACKUP DATABASE pour les sauvegardes complètes, mais elle nécessite de configurer un allowed_disk dans la configuration du serveur ClickHouse. Pour les déploiements Docker, l'export par table ci-dessus est plus simple.
Sauvegarde MinIO
Synchronisez les données MinIO vers un bucket S3 distant ou un répertoire local :
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/
Automatiser avec 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
Durcissement pour la production
Procédure de mise à jour
Téléchargez les nouvelles images et redémarrez :
cd /opt/langfuse
docker compose pull
docker compose up -d
Langfuse exécute les migrations de base de données automatiquement au démarrage. Consultez les logs du conteneur web après une mise à jour :
docker compose logs langfuse-web --tail 20
Cherchez Ready dans la sortie. Si les migrations échouent, le conteneur ne démarrera pas. Revenez en arrière en fixant le tag de version précédent dans docker-compose.yml (par exemple langfuse/langfuse:3.x.x).
Surveiller Langfuse lui-même
Le point de santé à /api/public/health renvoie 200 OK quand l'API est fonctionnelle. Ajoutez ?failIfDatabaseUnavailable=true pour une vérification plus profonde incluant la connectivité à la base de données.
Surveillez l'utilisation disque du volume ClickHouse. C'est le composant qui grossit le plus vite :
docker system df -v | grep clickhouse
Surveillez la mémoire des conteneurs avec :
docker stats --no-stream --format "table {{.Name}}\t{{.MemUsage}}\t{{.CPUPerc}}"
Pare-feu
Seuls les ports 22 (SSH), 80 et 443 doivent être ouverts. Tous les ports de bases de données sont liés à 127.0.0.1 dans le fichier compose, mais un pare-feu ajoute une couche de défense Comment configurer un pare-feu Linux avec UFW et nftables sur un VPS :
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
| Fonctionnalité | Langfuse | LangSmith | Opik |
|---|---|---|---|
| Licence | MIT (open source) | Propriétaire | Apache 2.0 |
| Auto-hébergement | Docker Compose / K8s | Licence Enterprise uniquement | Docker Compose / K8s |
| Stockage des traces | Votre infrastructure | Cloud LangChain | Votre infrastructure |
| Framework d'évaluation | Externe (DeepEval, etc.) | Intégré | Intégré |
| OpenTelemetry | Support natif | Non | Partiel |
Langfuse et Opik sont les deux options open source viables pour l'auto-hébergement. Langfuse a une communauté plus large et plus d'intégrations. LangSmith nécessite une licence Enterprise pour l'auto-hébergement. Consultez la documentation d'auto-hébergement de Langfuse pour les dernières options de déploiement.
Dépannage
Le conteneur reste unhealthy : Vérifiez docker compose logs <service> --tail 100. Causes fréquentes : mauvais mot de passe dans .env (ClickHouse est sensible à la casse sur les noms d'utilisateur), ou MinIO qui échoue à initialiser les buckets au premier démarrage. Redémarrez la pile avec docker compose down && docker compose up -d.
Le conteneur web plante en boucle avec "invalid port number" : Votre mot de passe PostgreSQL contient des caractères spéciaux (/, +, =) qui cassent l'URL de connexion postgresql://. Regénérez tous les mots de passe avec openssl rand -hex 32 au lieu de -base64, puis lancez docker compose down -v && docker compose up -d pour réinitialiser avec les nouveaux identifiants.
Le conteneur web plante avec "CLICKHOUSE_MIGRATION_URL is not configured" : Ajoutez CLICKHOUSE_MIGRATION_URL=clickhouse://clickhouse:9000 aux sections environment de langfuse-web et langfuse-worker. Cela utilise le protocole natif ClickHouse sur le port 9000, distinct de l'API HTTP sur le port 8123.
Erreurs 502/504 à travers le reverse proxy : Réglez KEEP_ALIVE_TIMEOUT dans le conteneur web Langfuse à une valeur supérieure au timeout d'inactivité de votre reverse proxy. Caddy utilise un keepalive de 30s par défaut. Nous avons réglé Langfuse à 70s et Caddy à 75s.
L'arrêt du worker prend longtemps : Sous charge, le worker vide sa file d'attente avant de s'arrêter. Cela peut prendre jusqu'à une heure. Pour un arrêt plus rapide, réduisez d'abord à zéro : docker compose stop langfuse-worker, attendez que la file se vide, puis continuez.
Le disque ClickHouse se remplit : Configurez la rétention des données. Langfuse fournit des paramètres de rétention intégrés dans le tableau de bord sous Settings > Data Retention. Configurez le TTL des traces selon votre capacité de stockage.
Logs : Tous les conteneurs Langfuse écrivent sur stdout. Consultez-les avec :
journalctl -u docker -f
docker compose logs -f
AIOps sur un VPS : gestion de serveur par IA avec des outils open source Héberger des agents IA sur un VPS Construire et auto-héberger un serveur MCP sur un VPS [-> docker-compose-multi-service-vps]