본문 바로가기
AI

Langfuse 자체 호스팅으로 LLM 앱 관측하기 - 트레이스 계측, 토큰 비용 추적, 프롬프트 버전과 평가 대시보드

아주라·2026년 9월 6일·조회 3

LLM 애플리케이션을 운영에 올리고 나면 항상 같은 질문을 받는다. "이번 달 OpenAI 요금이 왜 이렇게 나왔나", "어제 그 답변이 왜 이상하게 나갔나", "프롬프트를 바꿨는데 품질이 좋아진 건 맞나". 저도 처음엔 로그를 grep으로 뒤지고 토큰 수를 손으로 더했다. 그러다 결국 LLM 전용 관측(observability) 도구를 붙이게 됐는데, SaaS로 데이터를 넘기기 껄끄러운 환경이 많아 자체 호스팅이 가능한 Langfuse를 자주 쓴다.

결론부터 말하자면, Langfuse는 Docker Compose로 몇 분이면 띄울 수 있고, Python이나 JS SDK로 함수 하나에 데코레이터를 붙이는 것만으로 트레이스(trace, 한 요청의 처리 과정을 단계별로 기록한 것)와 토큰, 비용, 지연이 대시보드에 쌓인다. 프롬프트를 코드에서 분리해 버전 관리하고, LLM-as-a-judge나 사람 평가로 품질 점수까지 한곳에 모을 수 있다. 아래에서 자체 호스팅 설치, 트레이스 계측, 비용 추적, 프롬프트 버전 관리, 평가 대시보드를 차례로 살펴본다.

1. Langfuse가 다루는 것과 핵심 용어

Langfuse는 오픈소스 LLM 관측 플랫폼이다. 일반 APM이 HTTP 요청과 DB 쿼리를 추적한다면, Langfuse는 LLM 호출에 특화해 프롬프트, 모델, 입력/출력 토큰 수, 비용(USD), 지연, 품질 점수를 기록한다.

먼저 용어 몇 개를 정리한다.

  • Trace: 사용자 요청 하나가 처리되는 전 과정. 예를 들어 RAG 요청이면 임베딩 조회, 문서 검색, LLM 호출이 한 트레이스 안에 묶인다.
  • Observation: 트레이스를 구성하는 단위. span(일반 작업 구간), generation(LLM 호출), event(순간 기록)로 나뉜다. 토큰과 비용은 generation에 붙는다.
  • Score: 트레이스나 관측에 매기는 품질 점수. 숫자, 범주형, 불리언, 텍스트가 가능하다.
  • Dataset: 평가에 쓰는 입력/기대출력 쌍의 모음. 프롬프트를 바꾼 뒤 회귀 테스트를 돌릴 때 쓴다.

2. Docker Compose로 자체 호스팅하기

가장 빠른 설치 방법은 공식 저장소의 Docker Compose다. v3부터 아키텍처가 바뀌어, 대량 트레이스 저장용 ClickHouse와 큐 처리를 위한 Redis, 오브젝트 저장용 MinIO가 함께 뜬다. 현재 문서 기준 서버 버전은 v4 계열이며, 버전 번호는 저장소 상태에 따라 다를 수 있다.

$ git clone https://github.com/langfuse/langfuse.git
$ cd langfuse
$ docker compose up -d

기동하면 다음 컨테이너가 올라온다. 2~3분 뒤 langfuse-web 컨테이너가 "Ready" 로그를 찍으면 준비가 끝난 것이다.

$ docker compose ps
NAME                 SERVICE      STATUS         PORTS
langfuse-web-1       langfuse-web running        0.0.0.0:3000->3000/tcp
langfuse-worker-1    langfuse-worker running
postgres-1           postgres     running (healthy)  5432/tcp
clickhouse-1         clickhouse   running (healthy)  8123/tcp, 9000/tcp
redis-1              redis        running (healthy)  6379/tcp
minio-1              minio        running (healthy)  0.0.0.0:9090->9090/tcp

여기서 한 번 걸린다. compose 파일에는 # CHANGEME 주석이 붙은 시크릿이 여러 개 있는데, 기본값 그대로 외부에 노출하면 안 된다. 최소한 아래 세 개는 반드시 새로 생성해 채운다.

  • NEXTAUTH_SECRET: 로그인 세션 쿠키 검증용
  • SALT: API 키 해시에 쓰는 솔트
  • ENCRYPTION_KEY: 민감 데이터 암호화용, 256비트(64자리 hex) 고정
# NEXTAUTH_SECRET, SALT (base64 32바이트)
$ openssl rand -base64 32

# ENCRYPTION_KEY (hex 32바이트 = 64자)
$ openssl rand -hex 32

외부에서 접근해야 하는 컨테이너는 langfuse-web(포트 3000)와 MinIO뿐이다. 나머지는 내부 네트워크에만 두는 것이 좋다. NEXTAUTH_URL은 실제 접속 주소로 맞춘다. 로컬이면 http://localhost:3000, 도메인을 붙였으면 https://langfuse.example.com이다.

브라우저로 3000 포트를 열어 계정과 프로젝트를 만들면, 프로젝트 설정에서 pk-lf-로 시작하는 퍼블릭 키와 sk-lf-로 시작하는 시크릿 키가 나온다. 이 두 개로 SDK를 붙인다.

참고로 이 compose 구성은 고가용성, 수평 확장, 백업 기능이 빠져 있다. 소규모 팀이나 사내 관측용으로는 충분하지만, 트래픽이 큰 프로덕션이면 공식 문서의 Kubernetes 배포를 검토한다.

3. Python SDK로 트레이스 계측하기

SDK부터 설치한다. Python SDK v3 기준이다.

$ pip install langfuse

인증 정보는 환경 변수로 넘긴다. 자체 호스팅이므로 LANGFUSE_HOST를 내가 띄운 주소로 지정하는 것이 핵심이다. 이 값을 빼먹으면 SDK가 클라우드(cloud.langfuse.com)로 데이터를 보내려다 조용히 실패한다.

export LANGFUSE_PUBLIC_KEY="pk-lf-..."
export LANGFUSE_SECRET_KEY="sk-lf-..."
export LANGFUSE_HOST="http://localhost:3000"

가장 간단한 계측은 OpenAI 드롭인 통합이다. import 경로만 langfuse.openai로 바꾸면 나머지 코드는 그대로 두어도 된다. 이 래퍼가 토큰 사용량과 비용을 자동으로 잡아 준다.

from langfuse.openai import openai

completion = openai.chat.completions.create(
    name="summarize-article",
    model="gpt-4o",
    messages=[{"role": "user", "content": "이 문서를 세 줄로 요약해줘"}],
    metadata={"user_id": "u-1041"},
)

여러 단계를 하나의 트레이스로 묶고 싶으면 @observe() 데코레이터를 쓴다. 함수의 인자를 입력, 반환값을 출력으로 잡고 실행 시간을 기록한다.

from langfuse import observe, get_client
from langfuse.openai import openai

langfuse = get_client()

@observe()
def answer(question: str) -> str:
    docs = retrieve(question)          # 검색 단계는 span으로 기록
    resp = openai.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": f"{docs}\n\nQ: {question}"}],
    )
    return resp.choices[0].message.content

answer("환불 규정 알려줘")
langfuse.flush()   # 짧게 끝나는 스크립트는 flush 필수

여기서 자주 놓치는 것이 langfuse.flush()다. SDK는 이벤트를 모아 비동기로 보내기 때문에, 배치 스크립트나 CLI처럼 금방 종료되는 프로그램은 flush를 호출하지 않으면 마지막 트레이스가 전송되기 전에 프로세스가 죽는다. 웹 서버처럼 계속 떠 있는 프로세스는 신경 쓰지 않아도 된다.

4. 토큰과 비용 추적

Langfuse는 generation 관측마다 두 값을 기록한다. usage details(사용 단위별 소비량, 예: input, output 토큰)와 cost details(단위별 USD 비용)다. 이 값이 채워지는 경로는 두 가지다.

  • Ingested(수집): LLM 응답에 담긴 실제 토큰/비용을 SDK나 통합이 그대로 보낸다.
  • Inferred(추론): Langfuse가 모델 정의와 토크나이저로 계산한다.

둘 다 있으면 수집된 값이 우선한다. Langfuse는 OpenAI, Anthropic, Google 모델의 가격표를 기본 내장한다. 자체 호스팅 모델이나 사내 모델을 쓰면 프로젝트 설정 UI나 Models API에서 사용 단위별 가격을 직접 등록해야 비용이 계산된다.

OpenAI 드롭인 통합은 토큰과 비용을 자동으로 잡지만, 직접 호출하는 SDK(예: Anthropic 클라이언트)는 값을 넘겨줘야 한다. 이럴 때 @observe(as_type="generation")update_current_generation()으로 수동 기록한다.

from langfuse import observe, get_client

langfuse = get_client()

@observe(as_type="generation")
def anthropic_completion(**kwargs):
    messages = kwargs.pop("messages", None)
    model = kwargs.pop("model", None)
    langfuse.update_current_generation(input=messages, model=model)

    response = anthropic_client.messages.create(
        model=model, messages=messages, **kwargs
    )

    langfuse.update_current_generation(
        usage_details={
            "input": response.usage.input_tokens,
            "output": response.usage.output_tokens,
        },
        cost_details={
            "input": 0.003,
            "output": 0.015,
        },
    )
    return response.content[0].text

대시보드에서는 모델별, 사용자별, 기간별로 토큰과 비용을 집계해 볼 수 있다. "이번 주 gpt-4o 비용이 어느 사용자에게서 나왔나" 같은 질문에 바로 답이 나온다. 실제 절감 폭은 환경마다 다르지만, 어떤 프롬프트가 토큰을 과하게 먹는지 눈으로 확인되면 손볼 지점을 잡기 훨씬 쉬워진다.

5. 프롬프트 버전 관리

프롬프트를 코드에 하드코딩하면 문구 한 줄 고칠 때마다 배포해야 한다. Langfuse 프롬프트 관리는 프롬프트를 중앙에 저장하고 런타임에 가져오게 해, 코드 배포 없이 프롬프트만 바꿀 수 있다.

프롬프트 생성은 create_prompt로 한다. 같은 이름으로 다시 만들면 자동으로 새 버전이 생긴다. labelsproduction 같은 라벨을 붙여 어느 버전을 배포할지 제어한다.

from langfuse import Langfuse

langfuse = Langfuse()

langfuse.create_prompt(
    name="movie-critic",
    type="text",
    prompt="As a {{criticlevel}} movie critic, do you like {{movie}}?",
    labels=["production"],
)

런타임에서는 get_prompt로 가져와 compile로 변수를 채운다. 라벨을 지정하지 않으면 production 버전을 가져온다. SDK가 클라이언트 측에 캐싱하므로 첫 조회 이후에는 지연이 거의 없고, Langfuse 서버가 잠시 죽어도 캐시로 버틴다.

from langfuse import get_client

langfuse = get_client()

prompt = langfuse.get_prompt("movie-critic")
compiled = prompt.compile(criticlevel="expert", movie="Dune 2")
# "As an expert movie critic, do you like Dune 2?"

여기서 한 가지 더 챙기면 좋다. 프롬프트를 트레이스의 generation에 연결해 두면, 대시보드에서 "이 프롬프트 버전이 실제로 어떤 출력과 비용을 냈는가"를 버전별로 비교할 수 있다. 채팅 프롬프트는 compile 결과가 메시지 리스트로 나와 그대로 LLM API에 넘길 수 있다.

6. 평가와 대시보드

관측이 붙었으면 다음은 품질 측정이다. Langfuse는 여러 평가 방식을 하나의 Score로 모은다.

  • LLM-as-a-Judge: 다른 LLM으로 트레이스를 자동 채점. 사실성, 어조, 완결성 같은 기준을 프롬프트로 정의한다.
  • Human annotation: Annotation Queue로 사람이 직접 점수를 매긴다.
  • Custom code evaluator: 정규식 일치, JSON 스키마 검증 같은 결정적 검사를 코드로 작성한다.
  • Dataset run: 입력/기대출력 데이터셋에 프롬프트를 돌려 회귀를 잡는다.

SDK로 점수를 직접 남길 수도 있다. 현재 트레이스에 점수를 붙이거나, 트레이스 ID를 지정해 나중에 붙인다.

from langfuse import get_client

langfuse = get_client()

# 실행 중인 트레이스에 바로 점수 남기기
langfuse.score_current_trace(
    name="user_feedback",
    value=1,                 # 숫자, 불리언, 범주형 모두 가능
    comment="답변이 정확했음",
)

# 나중에 trace_id로 지정해 점수 남기기
langfuse.create_score(
    trace_id="3f8b...c21",
    name="relevance",
    value=0.82,
)

이렇게 쌓인 점수는 대시보드에서 시간축으로 추세를 보여준다. 프롬프트 v3에서 v4로 올린 뒤 relevance 평균이 떨어졌는지, 특정 모델이 유독 낮은 점수를 받는지 한눈에 확인된다. 프로덕션 트레이스에 자동 채점을 걸어 두면, 품질 저하를 배포 직후가 아니라 실시간으로 잡을 수 있다.

7. 정리와 권장

정리하면, 데이터를 외부로 내보내기 어렵거나 비용을 통제하고 싶은 팀이라면 Langfuse 자체 호스팅이 실용적인 선택이다. Docker Compose로 띄우고, OpenAI 드롭인 통합이나 @observe로 계측을 붙이는 것만으로 토큰, 비용, 지연이 즉시 보인다.

도입 순서는 이렇게 잡길 권한다. 먼저 드롭인 통합이나 데코레이터로 트레이스와 비용부터 가시화한다. 그다음 자주 고치는 프롬프트를 코드에서 분리해 버전 관리로 옮긴다. 마지막으로 핵심 유스케이스에 데이터셋과 LLM-as-a-judge를 붙여 품질을 수치로 관리한다. 세 단계를 한 번에 하려 들면 지치니, 비용 가시화 하나만 먼저 붙여도 값어치를 한다.

자주 묻는 질문

Langfuse 자체 호스팅과 클라우드 버전의 차이는 무엇인가?

기능은 동일한 오픈소스 코어를 공유한다. 자체 호스팅은 트레이스 데이터가 내 인프라에만 저장되고 비용을 인프라 비용으로만 부담한다. 대신 PostgreSQL, ClickHouse, Redis, MinIO를 직접 운영하고 백업과 확장을 스스로 챙겨야 한다. 데이터 반출이 어려운 환경이면 자체 호스팅, 운영 부담을 줄이려면 클라우드가 맞다.

토큰과 비용이 대시보드에 안 잡히는데 왜 그런가?

가장 흔한 원인은 자동 수집이 안 되는 경로를 쓴 경우다. OpenAI 드롭인 통합이나 지원 통합은 토큰과 비용을 자동으로 잡지만, 저수준 SDK로 직접 호출하면 usage_details와 cost_details를 직접 넘겨야 한다. 또 사내 모델은 프로젝트 설정이나 Models API에 가격을 등록해야 비용이 계산된다.

짧게 끝나는 스크립트에서 트레이스가 일부만 올라온다.

SDK가 이벤트를 배치로 비동기 전송하기 때문이다. 스크립트나 CLI처럼 금방 종료되는 프로그램은 마지막에 langfuse.flush()를 호출해 남은 이벤트를 강제로 보내야 한다. 웹 서버처럼 상주하는 프로세스는 필요 없다.

자체 호스팅에서 반드시 바꿔야 하는 시크릿은 무엇인가?

compose 파일의 CHANGEME 주석이 붙은 값들, 특히 NEXTAUTH_SECRET, SALT, ENCRYPTION_KEY다. 앞 둘은 openssl rand -base64 32로, ENCRYPTION_KEY는 256비트 고정이라 openssl rand -hex 32로 생성한다. NEXTAUTH_URL은 실제 접속 주소로 맞춘다.

프롬프트를 바꿨을 때 품질이 좋아졌는지 어떻게 확인하나?

프롬프트를 버전 관리로 옮기고 트레이스의 generation에 연결한 뒤, 데이터셋 run이나 LLM-as-a-judge로 버전별 점수를 비교한다. Score 대시보드가 시간축으로 추세를 보여주므로 새 버전 배포 후 relevance나 사실성 평균이 어떻게 변했는지 확인할 수 있다.

관련 글

댓글 0

로그인 후 댓글을 남길 수 있습니다.

아직 댓글이 없습니다.