Самостоятельный хостинг Langfuse на VPS для наблюдаемости LLM
Разверните Langfuse v3 с Docker Compose на собственном VPS. Отслеживайте вызовы LLM, контролируйте расходы, запускайте автоматизированные оценки с DeepEval и встраивайте контроль качества в CI/CD-пайплайн.
Langfuse — это open-source платформа наблюдаемости для LLM. Она трассирует каждый вызов LLM в приложении: задержку, расход токенов, стоимость и пары prompt/completion. Самостоятельный хостинг через Docker Compose держит все данные трассировки на вашей инфраструктуре. Никакой тарификации за событие. Никаких данных, покидающих вашу сеть.
Этот туториал покрывает полный цикл: развёртывание Langfuse v3, настройка TLS, инструментирование Python- и TypeScript-приложений, построение автоматизированных пайплайнов оценки с DeepEval, интегрированных в CI/CD.
Что нужно Langfuse v3 для работы на VPS?
Langfuse v3 запускает шесть контейнеров: веб-интерфейс, асинхронный воркер, PostgreSQL для метаданных, ClickHouse для аналитики трассировок, Redis для очередей и MinIO для объектного хранилища. Это серьёзное изменение по сравнению с v2, которой требовался только PostgreSQL.
| Компонент | Назначение | Порт по умолчанию | Базовый RAM |
|---|---|---|---|
| langfuse-web | Веб-интерфейс и API | 3000 | ~512 МБ |
| langfuse-worker | Асинхронная обработка событий | 3030 | ~512 МБ |
| PostgreSQL 17 | Транзакционные метаданные | 5432 | ~256 МБ |
| ClickHouse | OLAP-аналитика трассировок | 8123 (HTTP), 9000 (нативный) | ~1 ГБ |
| Redis 7 | Очередь и кеш | 6379 | ~128 МБ |
| MinIO | Объектное/медиа-хранилище | 9000 (API), 9001 (консоль) | ~256 МБ |
Выделите минимум 4 vCPU и 8 ГБ RAM. Virtua Cloud VCS-8 (4 vCPU, 8 ГБ RAM, NVMe) справляется с этим без проблем. Начните со 100 ГБ диска. ClickHouse растёт примерно на 1-2 ГБ на миллион трассировок в зависимости от размера промптов/completions.
Планирование ресурсов по масштабу
| Трассировок/месяц | Рост диска/месяц | Рекомендуемый VPS |
|---|---|---|
| < 100K | ~500 МБ | 4 vCPU / 8 ГБ |
| 100K - 1M | 1-2 ГБ | 4 vCPU / 8 ГБ |
| 1M - 10M | 10-20 ГБ | 8 vCPU / 16 ГБ |
| > 10M | 50+ ГБ | Выделенный / Kubernetes |
Предварительные требования
- VPS на Debian 12 или Ubuntu 24.04 с минимум 4 vCPU и 8 ГБ RAM
- Установленные Docker и Docker Compose Docker в продакшене на VPS: что ломается и как это починить
- Доменное имя с A-записью, указывающей на IP вашего VPS (для TLS)
- SSH-доступ с аутентификацией по ключу Защита SSH на Linux VPS: полное руководство по настройке sshd_config
Как развернуть Langfuse с Docker Compose?
Склонируйте официальный репозиторий и используйте предоставленный docker-compose.yml как отправную точку. Ключевой шаг — сгенерировать настоящие секреты вместо значений-заглушек.
mkdir -p /opt/langfuse && cd /opt/langfuse
Создайте файл окружения с сгенерированными секретами:
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
Теперь замените заглушки настоящими случайными значениями. Используйте openssl rand -hex вместо -base64, потому что base64-вывод содержит символы /, + и =, которые ломают URL-строки подключения к 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
Заблокируйте файл. Только root должен иметь доступ на чтение:
chmod 600 .env
ls -la .env
-rw------- 1 root root 715 Mar 19 10:00 .env
Как решить конфликт порта 9000 между MinIO и ClickHouse?
И MinIO, и ClickHouse по умолчанию используют порт 9000. Официальный docker-compose.yml уже маппит API-порт MinIO на 9090 на хосте (9090:9000), избегая конфликта. Если вы пишете свой compose-файл, убедитесь, что перемаппили один из них.
Официальный compose-файл также привязывает инфраструктурные порты только к 127.0.0.1 (PostgreSQL, нативный ClickHouse, Redis, консоль MinIO), предотвращая внешний доступ. Единственный порт, открытый на всех интерфейсах — 3000 для веб-интерфейса, который мы поставим за обратный прокси.
Создайте 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:
Несколько важных моментов по этому compose-файлу:
- Веб-контейнер слушает на
127.0.0.1:3000вместо0.0.0.0:3000. Весь трафик идёт через обратный прокси. CLICKHOUSE_MIGRATION_URLиспользует нативный протокол (clickhouse://) на порту 9000, тогда какCLICKHOUSE_URLиспользует HTTP на порту 8123. Нужны оба. ЕслиCLICKHOUSE_MIGRATION_URLотсутствует, веб-контейнер падает при запуске.- Redis использует отдельные переменные
REDIS_HOST,REDIS_PORTиREDIS_AUTHвместо строки подключения. Такой формат ожидает Langfuse v3. - Entrypoint MinIO создаёт директорию бакета
langfuseпри первом запуске (mkdir -p /data/langfuse). Без этого S3-загрузки будут падать, пока бакет не появится. - ClickHouse запускается как
user: "101:101"для соответствия внутреннему пользователю clickhouse в контейнере. - Redis работает с
--maxmemory-policy noeviction, чтобы предотвратить потерю данных при нехватке памяти. Langfuse полагается на Redis для очереди задач, и вытеснение ключей привело бы к тихой потере данных.
Запустите стек:
docker compose up -d
Подождите около 2-3 минут, пока все контейнеры инициализируются. ClickHouse и PostgreSQL выполняют миграции при первом запуске. Проверьте статус:
docker compose ps
Все шесть контейнеров должны показывать Up. Инфраструктурные контейнеры (postgres, clickhouse, minio, redis) показывают (healthy). Контейнеры langfuse-web и langfuse-worker не определяют healthcheck, поэтому показывают Up без метки здоровья. Если контейнер перезапускается в цикле, проверьте его логи:
docker compose logs langfuse-web --tail 50
Обратитесь к эндпоинту здоровья, чтобы убедиться, что API работает и подключение к базе данных в порядке:
curl -s http://localhost:3000/api/public/health?failIfDatabaseUnavailable=true | python3 -m json.tool
{
"status": "OK",
"version": "3.160.0"
}
Как добавить TLS к самостоятельно размещённому экземпляру Langfuse?
Поставьте Caddy перед Langfuse. Caddy автоматически получает TLS-сертификаты от Let's Encrypt. Никаких cron-задач certbot, никакого ручного обновления.
Установите 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
Создайте 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
keepalive 75s установлен выше, чем KEEP_ALIVE_TIMEOUT=70 у Langfuse, чтобы обратный прокси не держал устаревшие соединения. Это несовпадение — основная причина периодических ошибок 502/504, с которыми сталкиваются многие при самостоятельном хостинге. Директива header -Server убирает информацию о версии из ответов.
systemctl enable --now caddy
enable позволяет Caddy переживать перезагрузки. --now запускает его немедленно.
systemctl status caddy
● caddy.service - Caddy
Loaded: loaded (/usr/lib/systemd/system/caddy.service; enabled; preset: enabled)
Active: active (running)
С локальной машины протестируйте TLS-эндпоинт:
curl -I https://langfuse.example.com/api/public/health
HTTP/2 200
content-type: application/json
Настройка аутентификации и API-ключей
Откройте https://langfuse.example.com в браузере. Создайте первый аккаунт. Он станет администраторским.
После входа:
- Создайте новый проект (например, "production")
- Перейдите в Settings > API Keys
- Нажмите Create API Key
- Сохраните Public Key и Secret Key. Оба нужны для инструментирования приложений.
Публичный ключ идентифицирует проект. Секретный ключ аутентифицирует запись. Обращайтесь с секретным ключом как с паролем от базы данных.
Как инструментировать Python-приложение для отправки трассировок в Langfuse?
Langfuse Python SDK построен на OpenTelemetry. Декоратор @observe() автоматически создаёт трассировки и спаны для декорированных функций. Вложенные вызовы порождают вложенные спаны в интерфейсе Langfuse.
Установите SDK:
pip install langfuse openai
Задайте переменные окружения, указывающие на ваш экземпляр:
export LANGFUSE_PUBLIC_KEY="pk-lf-..."
export LANGFUSE_SECRET_KEY="sk-lf-..."
export LANGFUSE_HOST="https://langfuse.example.com"
export OPENAI_API_KEY="sk-..."
Вот инструментированное приложение:
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()
@observe() на run_pipeline создаёт корневую трассировку. answer_question и retrieve_context становятся вложенными спанами. Пропатченный импорт openai (from langfuse.openai import openai) автоматически фиксирует имя модели, количество токенов, задержку и стоимость как спан генерации. Контекстный менеджер propagate_attributes прикрепляет метаданные пользователя и сессии ко всем вложенным наблюдениям.
После запуска скрипта откройте дашборд Langfuse. Trace Explorer покажет полное дерево вызовов с таймингами для каждого спана.
Как инструментировать TypeScript-приложение?
Установите Langfuse TypeScript SDK и OpenTelemetry Node SDK:
npm install @langfuse/tracing @langfuse/otel @opentelemetry/sdk-node
Задайте те же переменные окружения:
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 } });
}
);
});
Вызовите forceFlush() на span processor перед завершением serverless-функций или скриптов, чтобы избежать потери данных.
Обзор дашборда
Дашборд Langfuse предлагает четыре ключевых представления:
Trace Explorer показывает трассировки отдельных запросов. Кликните на трассировку, чтобы увидеть полное дерево спанов: какие функции выполнялись, сколько времени заняла каждая и какие именно промпты/completions были отправлены в LLM. Фильтруйте по ID пользователя, тегам или временному диапазону.
Отслеживание расходов разбивает затраты по моделям. Видно, какие модели потребляют больше всего токенов и какие API-вызовы дорогие. Используйте для выявления промптов, которые тратят токены на неоправданно длинные completions.
Перцентили задержки (p50, p90, p99) по эндпоинту или имени спана. Если ваш p99 подскочил, можно проанализировать самые медленные трассировки и найти узкое место.
Тренды использования токенов по времени. Следите за неожиданными скачками, которые указывают на изменение промпта или баг, генерирующий более длинные ответы.
Как настроить автоматизированную оценку LLM с DeepEval и Langfuse?
DeepEval — это open-source фреймворк для оценки LLM, который оценивает выходные данные моделей по метрикам: галлюцинация, верность контексту и релевантность. В связке с Langfuse можно получать продакшн-трассировки, запускать оценку и отправлять скоры обратно в дашборд Langfuse.
Установите DeepEval:
pip install deepeval
Метрики оценки
| Метрика | Что измеряет | Диапазон оценки | Когда использовать |
|---|---|---|---|
| Hallucination | Фактическая точность относительно предоставленного контекста | 0-1 (1 = без галлюцинаций) | RAG-пайплайны |
| Faithfulness | Соответствие выхода контексту извлечения | 0-1 (1 = верный) | RAG-пайплайны |
| Answer Relevancy | Отвечает ли ответ на вопрос | 0-1 (1 = релевантный) | Любое LLM-приложение |
| G-Eval | Пользовательские критерии через LLM-as-judge | 0-1 | Кастомные проверки качества |
Скрипт оценки
Этот скрипт получает последние трассировки из Langfuse, запускает метрики DeepEval и отправляет скоры обратно:
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")
После выполнения скоры оценки появятся в дашборде Langfuse рядом с исходными трассировками. Можно фильтровать трассировки по скору, чтобы найти некачественные выходы.
Как запускать оценки LLM в CI/CD-пайплайне?
Интегрируйте DeepEval в GitHub Actions, чтобы ловить регрессии качества до попадания в продакшн. Workflow запускает набор оценок на тестовом датасете при каждом pull request.
Создайте .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/
Создайте тестовый файл 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])
Workflow срабатывает только при изменении шаблонов промптов или LLM-кода. Если метрика падает ниже порога, сборка PR проваливается. Разработчики видят в логах GitHub Actions, какой тест-кейс упал и на сколько.
Как сделать бэкап PostgreSQL и ClickHouse для Langfuse?
Самостоятельный хостинг без бэкапов — безответственность. PostgreSQL хранит аккаунты, настройки проектов и API-ключи. ClickHouse хранит все данные трассировок. MinIO хранит загруженные медиафайлы и пакетные экспорты.
Бэкап PostgreSQL
docker compose exec postgres pg_dump -U langfuse langfuse | gzip > /opt/backups/langfuse-pg-$(date +%F).sql.gz
Бэкап ClickHouse
Экспортируйте таблицы по отдельности через клиент 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 также поддерживает команду BACKUP DATABASE для полных бэкапов, но она требует настройки allowed_disk в конфигурации сервера ClickHouse. Для Docker-развёртываний экспорт по таблицам выше проще.
Бэкап MinIO
Синхронизируйте данные MinIO в удалённый S3-совместимый бакет или локальную директорию:
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/
Автоматизация через 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
Подготовка к продакшну
Процедура обновления
Скачайте новые образы и перезапустите:
cd /opt/langfuse
docker compose pull
docker compose up -d
Langfuse автоматически запускает миграции базы данных при старте. Проверьте логи веб-контейнера после обновления:
docker compose logs langfuse-web --tail 20
Ищите Ready в выводе. Если миграции упали, контейнер не запустится. Откатитесь, зафиксировав предыдущий тег версии в docker-compose.yml (например, langfuse/langfuse:3.x.x).
Мониторинг самого Langfuse
Эндпоинт здоровья /api/public/health возвращает 200 OK, когда API работает. Добавьте ?failIfDatabaseUnavailable=true для более глубокой проверки с учётом подключения к базе данных.
Следите за использованием диска на томе ClickHouse. Это самый быстрорастущий компонент:
docker system df -v | grep clickhouse
Наблюдайте за памятью контейнеров:
docker stats --no-stream --format "table {{.Name}}\t{{.MemUsage}}\t{{.CPUPerc}}"
Файрвол
Только порты 22 (SSH), 80 и 443 должны быть открыты. Все порты баз данных привязаны к 127.0.0.1 в compose-файле, но файрвол добавляет эшелонированную защиту [-> 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
| Характеристика | Langfuse | LangSmith | Opik |
|---|---|---|---|
| Лицензия | MIT (open source) | Проприетарная | Apache 2.0 |
| Самостоятельный хостинг | Docker Compose / K8s | Только Enterprise-лицензия | Docker Compose / K8s |
| Хранение трассировок | Ваша инфраструктура | Облако LangChain | Ваша инфраструктура |
| Фреймворк оценки | Внешний (DeepEval и др.) | Встроенный | Встроенный |
| OpenTelemetry | Нативная поддержка | Нет | Частичная |
Langfuse и Opik — два жизнеспособных open-source варианта для самостоятельного хостинга. У Langfuse больше сообщество и больше интеграций. LangSmith требует Enterprise-лицензию для самостоятельного хостинга. Смотрите документацию по self-hosting Langfuse для актуальных вариантов развёртывания.
Устранение неполадок
Контейнер остаётся unhealthy: Проверьте docker compose logs <service> --tail 100. Типичные причины: неправильный пароль в .env (ClickHouse чувствителен к регистру в именах пользователей), или MinIO не может инициализировать бакеты при первом запуске. Перезапустите стек: docker compose down && docker compose up -d.
Веб-контейнер падает в цикле с "invalid port number": Пароль PostgreSQL содержит спецсимволы (/, +, =), которые ломают URL подключения postgresql://. Перегенерируйте все пароли через openssl rand -hex 32 вместо -base64, затем выполните docker compose down -v && docker compose up -d для сброса с новыми учётными данными.
Веб-контейнер падает с "CLICKHOUSE_MIGRATION_URL is not configured": Добавьте CLICKHOUSE_MIGRATION_URL=clickhouse://clickhouse:9000 в секции environment langfuse-web и langfuse-worker. Это использует нативный протокол ClickHouse на порту 9000, отдельно от HTTP API на порту 8123.
Ошибки 502/504 через обратный прокси: Установите KEEP_ALIVE_TIMEOUT в веб-контейнере Langfuse на значение выше idle timeout вашего обратного прокси. Caddy по умолчанию использует 30s keepalive. Мы устанавливаем Langfuse на 70s и Caddy на 75s.
Остановка воркера занимает много времени: Под нагрузкой воркер опустошает очередь перед остановкой. Это может занять до часа. Для более быстрой остановки сначала масштабируйте до нуля: docker compose stop langfuse-worker, подождите опустошения очереди, затем продолжайте.
Диск ClickHouse заполняется: Настройте ретеншн данных. Langfuse предоставляет встроенные настройки ретеншна в дашборде в разделе Settings > Data Retention. Настройте TTL трассировок исходя из ёмкости хранилища.
Логи: Все контейнеры Langfuse пишут в stdout. Смотрите их так:
journalctl -u docker -f
docker compose logs -f
AIOps на VPS: управление сервером с помощью ИИ и open-source инструментов Самостоятельный хостинг ИИ-агентов на VPS Создание и самостоятельный хостинг MCP-сервера на VPS [-> docker-compose-multi-service-vps]
Готовы попробовать?
Разверните свой сервер за секунды. Linux, Windows или FreeBSD. →