Self-hosting di Langfuse su un VPS per l'osservabilità LLM
Distribuisci Langfuse v3 con Docker Compose sul tuo VPS. Traccia le chiamate LLM, monitora i costi, esegui valutazioni automatizzate con DeepEval e integra quality gate nella tua pipeline CI/CD.
Langfuse è una piattaforma open source per l'osservabilità LLM. Traccia ogni chiamata LLM della tua applicazione: latenza, utilizzo dei token, costi e coppie prompt/completion. Il self-hosting con Docker Compose mantiene tutti i dati di tracciamento sulla tua infrastruttura. Nessuna fatturazione per evento. Nessun dato che lascia la tua rete.
Questo tutorial copre l'intero ciclo di vita: distribuire Langfuse v3, aggiungere TLS, instrumentare applicazioni Python e TypeScript, e costruire pipeline di valutazione automatizzate con DeepEval integrate nel CI/CD.
Di cosa ha bisogno Langfuse v3 per funzionare su un VPS?
Langfuse v3 esegue sei container: l'interfaccia web, un worker asincrono, PostgreSQL per i metadati, ClickHouse per l'analisi delle tracce, Redis per le code e MinIO per lo storage di oggetti. Questo è un cambiamento significativo rispetto alla v2, che richiedeva solo PostgreSQL.
| Componente | Scopo | Porta predefinita | RAM base |
|---|---|---|---|
| langfuse-web | Interfaccia web e API | 3000 | ~512 MB |
| langfuse-worker | Elaborazione asincrona degli eventi | 3030 | ~512 MB |
| PostgreSQL 17 | Metadati transazionali | 5432 | ~256 MB |
| ClickHouse | Analisi OLAP delle tracce | 8123 (HTTP), 9000 (nativo) | ~1 GB |
| Redis 7 | Coda e cache | 6379 | ~128 MB |
| MinIO | Storage di oggetti/media | 9000 (API), 9001 (console) | ~256 MB |
Prevedi almeno 4 vCPU e 8 GB di RAM. Un Virtua Cloud VCS-8 (4 vCPU, 8 GB RAM, NVMe) gestisce tutto comodamente. Parti con 100 GB di disco. ClickHouse cresce di circa 1-2 GB per milione di tracce a seconda delle dimensioni dei prompt/completion.
Dimensionamento per scala
| Tracce/mese | Crescita disco/mese | VPS consigliato |
|---|---|---|
| < 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 | Dedicato / Kubernetes |
Prerequisiti
- Un VPS con Debian 12 o Ubuntu 24.04 con almeno 4 vCPU e 8 GB di RAM
- Docker e Docker Compose installati Docker in produzione su un VPS: cosa si rompe e come risolvere
- Un nome di dominio con un record A che punta all'IP del tuo VPS (per TLS)
- Accesso SSH con autenticazione a chiave Hardening SSH su VPS Linux: guida completa a sshd_config
Come distribuisco Langfuse con Docker Compose?
Clona il repository ufficiale e usa il docker-compose.yml fornito come punto di partenza. Il passaggio chiave è generare veri secret invece di usare i valori segnaposto.
mkdir -p /opt/langfuse && cd /opt/langfuse
Crea il file di ambiente con i secret generati:
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
Ora sostituisci i segnaposto con valori casuali reali. Usa openssl rand -hex invece di -base64 perché l'output base64 contiene i caratteri /, + e = che rompono gli URL di connessione 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
Blocca il file. Solo root dovrebbe poterlo leggere:
chmod 600 .env
ls -la .env
-rw------- 1 root root 715 Mar 19 10:00 .env
Come risolvo il conflitto della porta 9000 tra MinIO e ClickHouse?
Sia MinIO che ClickHouse usano la porta 9000 come predefinita. Il docker-compose.yml ufficiale mappa già la porta API di MinIO sulla 9090 dell'host (9090:9000), evitando il conflitto. Se scrivi un file compose personalizzato, assicurati di rimappare una delle due.
Il file compose ufficiale lega anche le porte dell'infrastruttura solo a 127.0.0.1 (PostgreSQL, ClickHouse nativo, Redis, console MinIO), impedendo l'accesso esterno. L'unica porta esposta su tutte le interfacce è la 3000 per l'interfaccia web, che metteremo dietro un reverse proxy.
Crea il 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:
Alcuni punti da notare in questo file compose:
- Il container web ascolta su
127.0.0.1:3000invece di0.0.0.0:3000. Tutto il traffico passa attraverso il reverse proxy. CLICKHOUSE_MIGRATION_URLusa il protocollo nativo (clickhouse://) sulla porta 9000, mentreCLICKHOUSE_URLusa HTTP sulla porta 8123. Entrambi sono necessari. Se mancaCLICKHOUSE_MIGRATION_URL, il container web va in crash all'avvio.- Redis usa variabili separate
REDIS_HOST,REDIS_PORTeREDIS_AUTHinvece di una stringa di connessione. Questo è il formato che Langfuse v3 si aspetta. - L'entrypoint di MinIO crea la directory del bucket
langfuseal primo avvio (mkdir -p /data/langfuse). Senza questo, gli upload S3 falliscono finché il bucket non esiste. - ClickHouse gira come
user: "101:101"per corrispondere all'utente clickhouse interno al container. - Redis gira con
--maxmemory-policy noevictionper prevenire la perdita di dati quando la memoria scarseggia. Langfuse si affida a Redis per la sua coda di lavori, e l'espulsione delle chiavi causerebbe perdita di dati silenziosa.
Avvia lo stack:
docker compose up -d
Attendi circa 2-3 minuti per l'inizializzazione di tutti i container. ClickHouse e PostgreSQL eseguono le migrazioni al primo avvio. Controlla lo stato:
docker compose ps
Tutti e sei i container dovrebbero mostrare Up. I container dell'infrastruttura (postgres, clickhouse, minio, redis) mostrano (healthy). I container langfuse-web e langfuse-worker non definiscono healthcheck, quindi mostrano Up senza tag di salute. Se un container si riavvia in loop, controlla i suoi log:
docker compose logs langfuse-web --tail 50
Interroga l'endpoint di salute per confermare che l'API è attiva e la connessione al database funziona:
curl -s http://localhost:3000/api/public/health?failIfDatabaseUnavailable=true | python3 -m json.tool
{
"status": "OK",
"version": "3.160.0"
}
Come aggiungo TLS a un'istanza Langfuse self-hosted?
Metti Caddy davanti a Langfuse. Caddy gestisce automaticamente il provisioning dei certificati TLS da Let's Encrypt. Niente cronjob certbot, niente rinnovo manuale.
Installa 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
Crea il 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
Il keepalive 75s è impostato più alto del KEEP_ALIVE_TIMEOUT=70 di Langfuse per evitare che il reverse proxy mantenga connessioni obsolete. Questa discrepanza è la causa principale degli errori 502/504 intermittenti che molti self-hoster incontrano. La direttiva header -Server rimuove le informazioni di versione dalle risposte.
systemctl enable --now caddy
enable fa sopravvivere Caddy ai riavvii. --now lo avvia immediatamente.
systemctl status caddy
● caddy.service - Caddy
Loaded: loaded (/usr/lib/systemd/system/caddy.service; enabled; preset: enabled)
Active: active (running)
Dalla tua macchina locale, testa l'endpoint TLS:
curl -I https://langfuse.example.com/api/public/health
HTTP/2 200
content-type: application/json
Configurare autenticazione e chiavi API
Apri https://langfuse.example.com nel browser. Crea il primo account utente. Diventa l'account amministratore.
Dopo l'accesso:
- Crea un nuovo progetto (es. "production")
- Vai su Settings > API Keys
- Clicca su Create API Key
- Salva la Public Key e la Secret Key. Ti servono entrambe per instrumentare le tue applicazioni.
La chiave pubblica identifica il tuo progetto. La chiave segreta autentica le scritture. Tratta la chiave segreta come una password del database.
Come instrumento un'applicazione Python per inviare tracce a Langfuse?
Il SDK Python di Langfuse è costruito su OpenTelemetry. Il decoratore @observe() crea automaticamente tracce e span per le funzioni decorate. Le chiamate annidate producono span annidati nell'interfaccia di Langfuse.
Installa l'SDK:
pip install langfuse openai
Imposta le variabili d'ambiente che puntano alla tua istanza self-hosted:
export LANGFUSE_PUBLIC_KEY="pk-lf-..."
export LANGFUSE_SECRET_KEY="sk-lf-..."
export LANGFUSE_HOST="https://langfuse.example.com"
export OPENAI_API_KEY="sk-..."
Ecco un'applicazione instrumentata:
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()
Il @observe() su run_pipeline crea la traccia radice. answer_question e retrieve_context diventano span annidati. L'import patchato di openai (from langfuse.openai import openai) cattura automaticamente nome del modello, conteggio token, latenza e costo come span di generazione. Il context manager propagate_attributes allega i metadati utente e sessione a tutte le osservazioni annidate.
Dopo aver eseguito questo script, apri la dashboard di Langfuse. Il Trace Explorer mostra l'albero completo delle chiamate con il timing di ogni span.
Come instrumento un'applicazione TypeScript?
Installa l'SDK TypeScript di Langfuse e l'SDK Node di OpenTelemetry:
npm install @langfuse/tracing @langfuse/otel @opentelemetry/sdk-node
Imposta le stesse variabili d'ambiente:
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 } });
}
);
});
Chiama forceFlush() sullo span processor prima che le funzioni serverless o gli script terminino per prevenire la perdita di dati.
Panoramica della dashboard
La dashboard di Langfuse offre quattro viste principali:
Trace Explorer mostra le tracce delle singole richieste. Clicca su una traccia per vedere l'albero completo degli span: quali funzioni sono state eseguite, quanto tempo ha impiegato ciascuna e i prompt/completion esatti inviati all'LLM. Filtra per ID utente, tag o intervallo temporale.
Monitoraggio dei costi suddivide le spese per modello. Vedi quali modelli consumano più token e quali chiamate API sono costose. Usalo per identificare i prompt che sprecano token in completion inutilmente lunghe.
Percentili di latenza (p50, p90, p99) per endpoint o nome dello span. Se il tuo p99 schizza, puoi analizzare le tracce più lente per trovare il collo di bottiglia.
Tendenze di utilizzo dei token nel tempo. Osserva i salti inattesi che indicano un cambiamento di prompt o un bug che genera output più lunghi.
Come configuro la valutazione LLM automatizzata con DeepEval e Langfuse?
DeepEval è un framework open source per la valutazione LLM che assegna punteggi agli output dei modelli su metriche come allucinazione, fedeltà e rilevanza. Combinato con Langfuse, puoi recuperare le tracce di produzione, eseguire valutazioni su di esse e inviare i punteggi alla dashboard di Langfuse.
Installa DeepEval:
pip install deepeval
Metriche di valutazione
| Metrica | Cosa misura | Intervallo punteggio | Quando usarla |
|---|---|---|---|
| Hallucination | Accuratezza fattuale rispetto al contesto fornito | 0-1 (1 = nessuna allucinazione) | Pipeline RAG |
| Faithfulness | Se l'output è allineato al contesto di recupero | 0-1 (1 = fedele) | Pipeline RAG |
| Answer Relevancy | Se la risposta risponde alla domanda | 0-1 (1 = rilevante) | Qualsiasi app LLM |
| G-Eval | Criteri personalizzati via LLM-as-judge | 0-1 | Controlli qualità personalizzati |
Script di valutazione
Questo script recupera le tracce recenti da Langfuse, esegue le metriche DeepEval e invia i punteggi:
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")
Dopo l'esecuzione, i punteggi di valutazione appaiono nella dashboard di Langfuse accanto alle tracce originali. Puoi filtrare le tracce per punteggio per trovare gli output di bassa qualità.
Come eseguo valutazioni LLM in una pipeline CI/CD?
Integra DeepEval in GitHub Actions per intercettare le regressioni di qualità prima che raggiungano la produzione. Il workflow esegue la tua suite di valutazione su un dataset di test ad ogni pull request.
Crea .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/
Crea il file di 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])
Il workflow si attiva solo quando cambiano i template dei prompt o il codice LLM. Se una metrica scende sotto la soglia, la build della PR fallisce. Gli sviluppatori vedono nei log di GitHub Actions quale caso di test è fallito e di quanto.
Come faccio il backup di PostgreSQL e ClickHouse per Langfuse?
Il self-hosting senza backup è un rischio. PostgreSQL contiene gli account utente, le impostazioni del progetto e le chiavi API. ClickHouse contiene tutti i dati delle tracce. MinIO contiene gli upload dei media e gli export batch.
Backup di PostgreSQL
docker compose exec postgres pg_dump -U langfuse langfuse | gzip > /opt/backups/langfuse-pg-$(date +%F).sql.gz
Backup di ClickHouse
Esporta le tabelle individualmente usando il 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 supporta anche il comando BACKUP DATABASE per backup completi, ma richiede la configurazione di un allowed_disk nella configurazione del server ClickHouse. Per i deployment Docker, l'esportazione per tabella qui sopra è più semplice.
Backup di MinIO
Sincronizza i dati MinIO verso un bucket remoto compatibile S3 o una directory locale:
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/
Automatizzare con 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
Hardening per la produzione
Procedura di aggiornamento
Scarica le nuove immagini e riavvia:
cd /opt/langfuse
docker compose pull
docker compose up -d
Langfuse esegue le migrazioni del database automaticamente all'avvio. Controlla i log del container web dopo un aggiornamento:
docker compose logs langfuse-web --tail 20
Cerca Ready nell'output. Se le migrazioni falliscono, il container non si avvierà. Torna alla versione precedente fissando il tag nel docker-compose.yml (es. langfuse/langfuse:3.x.x).
Monitorare Langfuse stesso
L'endpoint di salute a /api/public/health restituisce 200 OK quando l'API è funzionante. Aggiungi ?failIfDatabaseUnavailable=true per un controllo più approfondito che include la connettività al database.
Monitora l'uso del disco del volume ClickHouse. È il componente che cresce più velocemente:
docker system df -v | grep clickhouse
Monitora la memoria dei container con:
docker stats --no-stream --format "table {{.Name}}\t{{.MemUsage}}\t{{.CPUPerc}}"
Firewall
Solo le porte 22 (SSH), 80 e 443 devono essere aperte. Tutte le porte dei database sono legate a 127.0.0.1 nel file compose, ma un firewall aggiunge difesa in profondità [-> linux-firewall-ufw-nftables-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
| Caratteristica | Langfuse | LangSmith | Opik |
|---|---|---|---|
| Licenza | MIT (open source) | Proprietaria | Apache 2.0 |
| Self-hosting | Docker Compose / K8s | Solo licenza Enterprise | Docker Compose / K8s |
| Storage delle tracce | La tua infrastruttura | Cloud LangChain | La tua infrastruttura |
| Framework di valutazione | Esterno (DeepEval, ecc.) | Integrato | Integrato |
| OpenTelemetry | Supporto nativo | No | Parziale |
Langfuse e Opik sono le due opzioni open source praticabili per il self-hosting. Langfuse ha una community più ampia e più integrazioni. LangSmith richiede una licenza Enterprise per il self-hosting. Consulta la documentazione di self-hosting di Langfuse per le ultime opzioni di deployment.
Risoluzione dei problemi
Il container resta unhealthy: Controlla docker compose logs <service> --tail 100. Cause comuni: password sbagliata in .env (ClickHouse distingue maiuscole e minuscole nei nomi utente), o MinIO che non riesce a inizializzare i bucket al primo avvio. Riavvia lo stack con docker compose down && docker compose up -d.
Il container web va in crash loop con "invalid port number": La tua password PostgreSQL contiene caratteri speciali (/, +, =) che rompono l'URL di connessione postgresql://. Rigenera tutte le password usando openssl rand -hex 32 invece di -base64, poi esegui docker compose down -v && docker compose up -d per resettare con le nuove credenziali.
Il container web va in crash con "CLICKHOUSE_MIGRATION_URL is not configured": Aggiungi CLICKHOUSE_MIGRATION_URL=clickhouse://clickhouse:9000 alle sezioni environment di langfuse-web e langfuse-worker. Questo usa il protocollo nativo ClickHouse sulla porta 9000, separato dall'API HTTP sulla porta 8123.
Errori 502/504 attraverso il reverse proxy: Imposta KEEP_ALIVE_TIMEOUT nel container web di Langfuse a un valore superiore al timeout di inattività del tuo reverse proxy. Caddy usa 30s di keepalive come predefinito. Noi impostiamo Langfuse a 70s e Caddy a 75s.
Lo shutdown del worker impiega molto: Sotto carico, il worker svuota la sua coda prima di fermarsi. Questo può richiedere fino a un'ora. Per uno shutdown più veloce, scala prima a zero: docker compose stop langfuse-worker, aspetta che la coda si svuoti, poi procedi.
Il disco di ClickHouse si riempie: Configura la data retention. Langfuse fornisce impostazioni di retention integrate nella dashboard sotto Settings > Data Retention. Configura il TTL delle tracce in base alla tua capacità di storage.
Log: Tutti i container Langfuse scrivono su stdout. Consultali con:
journalctl -u docker -f
docker compose logs -f
AIOps su un VPS: gestione dei server con IA e strumenti open source Self-hosting di agenti IA su un VPS Creare e ospitare un server MCP personalizzato su un VPS [-> docker-compose-multi-service-vps]