Self-hosting di Langfuse su un VPS per l'osservabilità LLM

12 min di lettura·Matthieu·llm-observabilityci-cddeepevaldocker-composelangfuse|

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

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:3000 invece di 0.0.0.0:3000. Tutto il traffico passa attraverso il reverse proxy.
  • CLICKHOUSE_MIGRATION_URL usa il protocollo nativo (clickhouse://) sulla porta 9000, mentre CLICKHOUSE_URL usa HTTP sulla porta 8123. Entrambi sono necessari. Se manca CLICKHOUSE_MIGRATION_URL, il container web va in crash all'avvio.
  • Redis usa variabili separate REDIS_HOST, REDIS_PORT e REDIS_AUTH invece di una stringa di connessione. Questo è il formato che Langfuse v3 si aspetta.
  • L'entrypoint di MinIO crea la directory del bucket langfuse al 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 noeviction per 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:

  1. Crea un nuovo progetto (es. "production")
  2. Vai su Settings > API Keys
  3. Clicca su Create API Key
  4. 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]