[Langfuse 3] Prompts UI에서 버전과 라벨 관리하기

심대용·4일 전

Langfuse 공부 3 — 프롬프트 버전과 라벨 관리

이 글에서 다룰 주제

  • Prompts 화면에서 프롬프트 본문·변수·Config를 함께 읽기
  • Version과 Label을 구분하고 필요한 버전을 명시적으로 가져오기
  • 로컬 Ollama 호출을 프롬프트 버전과 연결해 추적하기

주요 단어 · Prompt · Text · Variable · Config · Version · Label · Generation


답변 형식을 바꾸자 같은 질문의 결과가 달라졌다. 응답만 저장하면 어떤 지시문으로 만든 답인지 찾기 어렵다. 본문·설정·버전을 함께 남겨 변경 전후를 비교해 보자.

실습 환경: Langfuse 서버 4.50.0(Docker), Ollama 0.35.1과 qwen3:8b(macOS), Python SDK langfuse==4.16.0, openai==3.24.0. 서버와 SDK의 버전 번호는 다르다.

설치 편의 환경과 Trace·Session·Score 편을 이어 사용한다. 추론은 호스트의 Python SDK에서 실행하고 UI로 결과를 읽는다. Playground의 Ollama 연결은 이번에 설정하지 않는다.

Prompt 기능을 강조한 Langfuse 학습지도

점선은 학습 순서다. Prompt를 가져오는 동작과 Generation에 사용 버전을 연결하는 동작을 구분한다.

1. Prompts 목록 읽기

Prompt는 모델에 보낼 지시문과 관련 설정을 관리하는 객체다. 이번 실습에서는 ui-study/tutor-answer라는 이름 아래 응답 형식이 다른 두 버전을 만들었다.

왼쪽 Prompts 메뉴에서 ui-study 폴더를 열고 tutor-answer를 선택한다. 실습 목록에는 Versions 2, Type text, Number of Observations (7d) 6이 표시됐다. 마지막 값은 최근 7일간 연결된 관측 수이므로 프롬프트 버전 개수와 구분한다.

실습 프롬프트를 찾는 Prompts 목록

같은 이름 아래 버전을 쌓아 이력을 비교한다. 이름 자체를 바꾸면 서로 다른 프롬프트가 된다.

초기 두 버전은 SDK로 생성했다. 다음은 각 필드의 실습 값이다.

항목실습 값읽는 기준
Nameui-study/tutor-answer앱에서 같은 프롬프트를 찾는 이름
Typetext컴파일 결과를 하나의 문자열로 받음
Variable{{question}}실행할 때 질문으로 치환할 자리
Config모델명·생성 설정 JSON본문과 함께 버전 관리할 설정
Labelstudy-baseline, study-structured특정 버전을 선택하는 이름표

Text는 문자열, Chat은 역할을 가진 메시지 목록을 다룬다. 생성 후 Type을 바꿀 수 없으므로 먼저 구분한다. 이번에는 Text를 컴파일한 뒤 user 메시지 한 개에 넣는다. 공식 시작 안내

2. 본문과 Config 확인

먼저 v1의 내용을 읽어 보자.

너는 LLM 도구를 설명하는 튜터다. 다음 질문에 한국어 한 문장으로 답해.
질문: {{question}}

{{question}}는 Langfuse 템플릿의 변수 표기다. prompt.compile(question="Ollama의 주된 역할은 무엇인가?")로 질문을 넣는다. compile()은 문자열을 완성하며, 모델 호출은 별도다. 변수 공식 문서

Versions 화면에서 선택한 v2의 본문과 라벨

Versions 화면 왼쪽에서 v1·v2를 고른다. 캡처는 v2를 선택한 상태다. 오른쪽에는 Prompt, Config, Linked Generations, Use Prompt 탭이 있다. Prompt에서 본문과 변수 question을 읽고 Config에서 생성 설정을 확인한다. 헤더의 study-ui는 다음 절에서 추가한 학습용 라벨이다.

이번 두 버전은 동일한 Config를 사용했다.

{
  "model": "qwen3:8b",
  "temperature": 0,
  "max_tokens": 256,
  "reasoning_effort": "none"
}

Config는 본문과 함께 버전 관리하는 JSON 객체다. 설정을 실제 호출에 적용하는 것은 앱의 책임이다. UI에서 온도를 바꿔도 코드가 고정값을 보내면 실행은 그대로다. 이번에는 prompt.config를 Ollama 호출에 전달했다. Config 공식 문서

temperature: 0만으로 항상 동일 응답이 보장되지는 않는다. max_tokens는 출력 상한이며 실제 사용량은 Generation에서 읽는다.

3. Version과 Label 구분

Version은 저장된 프롬프트의 특정 개정판을 가리킨다. Label은 애플리케이션이 선택할 버전에 붙이는 이름표다.

이번 실습의 두 기준은 다음과 같다.

Version학습용 Label요청한 답변 형식
1study-baseline한국어 한 문장
2study-structured답·근거·한계, 총 세 문장 이내

버전 번호는 저장된 내용을 고정해서 선택한다. 라벨은 코드의 이름을 유지한 채 가리키는 버전을 옮길 수 있다. 라벨을 바꿔도 과거 Generation은 당시 연결한 버전으로 읽어야 한다.

SDK에서는 둘 중 하나를 명시한다.

# 저장된 특정 버전 선택
prompt = lf.get_prompt(
    "ui-study/tutor-answer", version=1,
    cache_ttl_seconds=0,
)

# 라벨이 현재 가리키는 버전 선택
prompt = lf.get_prompt(
    "ui-study/tutor-answer", label="study-structured",
    cache_ttl_seconds=0,
)

cache_ttl_seconds=0은 학습 중 변경을 즉시 확인하기 위한 값이다. 서비스 전체에 캐시 해제를 권하는 것은 아니다. version과 label은 함께 넣지 않는다.

latest는 최근 생성 버전을 가리킨다. get_prompt(name)의 기본 선택은 production이므로 인자를 생략했다고 최신 버전을 가져오는 것은 아니다. 요청한 라벨이 없으면 다른 라벨로 자동 대체하지 않는다. 학습 라벨 이름을 명시하자. 버전 관리 공식 문서

화면에서 학습용 라벨 study-ui를 v2에 추가해 보았다.

  1. Versions에서 v2를 선택하고 헤더의 Add prompt label 버튼을 연다.
  2. Custom labels의 Search or create label...에 study-ui를 입력한 뒤 Create a new label: study-ui를 선택한다.
  3. 기존 study-structured와 새 study-ui가 선택된 상태에서 Save를 누른다.

v2에 학습용 study-ui 라벨을 선택한 실제 설정 화면

위 캡처는 저장 직전의 라벨 선택 상태다. 저장 후 v2 헤더에 latest, study-structured, study-ui 세 라벨이 표시되는 것을 확인했다. 같은 v2에 이름표를 더한 것이므로 새 버전이 생기는 작업은 아니다. production은 이번 실습에서 선택하지 않았다.

4. 변경 내용 비교

v2에서는 질문을 유지하면서 지시문을 다음과 같이 바꿨다.

너는 LLM 도구를 설명하는 튜터다. 다음 질문에 짧고 정확한 한국어로 답해.
답: 핵심 설명 한 문장
근거: 역할이나 관계를 설명하는 한 문장
한계: 이 설명만으로 단정할 수 없는 점 한 문장
모르면 모른다고 말하고 제품의 역할을 혼동하지 마. 전체 3문장 이내.
질문: {{question}}

v1과 v2의 프롬프트 변경 내용을 비교하는 화면

v2가 선택된 상태에서 왼쪽 v1 행의 Compare with selected prompt 아이콘을 누르면 Changes v1 → v2 모달이 열린다. Content의 왼쪽은 v1, 오른쪽은 v2다. 추가·삭제 표시로 지시문 차이를 읽을 수 있다. 아래 Config는 No changes로 표시돼 두 버전의 설정이 같음을 확인했다.

비교할 때는 유지한 조건도 본다. 이번에는 이름·변수·모델·온도·출력 제한을 고정하고 지시문만 바꿨다. 모델까지 동시에 바꾸면 결과 차이의 원인을 구분하기 어렵다.

실제 v2는 “모든 span이 반드시 trace에 포함되지 않을 수 있다”라는 부정확한 문장을 추가했다. Langfuse의 Span은 Trace 안의 개별 작업이다. 형식을 자세히 지정하는 것과 사실 정확성은 별개다. 품질 비교는 다음 편에서 다룬다.

5. 로컬 모델과 연결

5.1 가져오기와 실행

두 버전×세 문항을 실행한 prompt_experiment.py는 버전 번호를 고정해 실행하고, 학습 라벨이 같은 버전을 가리키는지 별도로 검증했다. 아래는 그 호출 구조를 라벨 선택 방식으로 바꾼 설명용 예제다. 이 축약본의 독립 실행은 별도로 검증하지 않았다. 파일 옆 .env에 로컬 프로젝트 키를 둔다.

LANGFUSE_BASE_URL=http://localhost:3300
LANGFUSE_PUBLIC_KEY=내_로컬_프로젝트의_public_key
LANGFUSE_SECRET_KEY=내_로컬_프로젝트의_secret_key

Ollama의 OpenAI 호환 주소는 http://127.0.0.1:11434/v1이다.

import os
from pathlib import Path

from dotenv import load_dotenv
from langfuse import Langfuse
from openai import OpenAI

load_dotenv(Path(__file__).with_name(".env"), override=True)
lf = Langfuse(
    public_key=os.environ["LANGFUSE_PUBLIC_KEY"],
    secret_key=os.environ["LANGFUSE_SECRET_KEY"],
    base_url=os.environ["LANGFUSE_BASE_URL"],
)
ollama = OpenAI(
    base_url="http://127.0.0.1:11434/v1",
    api_key="ollama", timeout=180, max_retries=0,
)

try:
    prompt = lf.get_prompt(
        "ui-study/tutor-answer",
        label="study-structured",
        cache_ttl_seconds=0,
    )
    messages = [{
        "role": "user",
        "content": prompt.compile(
            question="Ollama의 주된 역할은 무엇인가?"
        ),
    }]

    with lf.start_as_current_observation(
        name="tutor-answer",
        as_type="generation",
        model=prompt.config["model"],
        prompt=prompt,
        input=messages,
    ) as generation:
        response = ollama.chat.completions.create(
            messages=messages, **prompt.config
        )
        answer = response.choices[0].message.content
        generation.update(
            output=answer,
            usage_details={
                "input": response.usage.prompt_tokens,
                "output": response.usage.completion_tokens,
                "total": response.usage.total_tokens,
            },
        )
    print(answer)
finally:
    lf.flush()
    lf.shutdown()
    ollama.close()

get_prompt()로 버전을 읽고, compile()으로 질문을 넣으며, chat.completions.create()로 추론한다. 결과와 토큰은 Generation에 기록한다.

prompt=prompt가 버전 연결의 핵심이다. 입력 문자열만 기록하는 것과 달리, 객체를 Generation에 넘기면 사용한 버전으로 이동하며 결과를 묶어 읽을 수 있다. Trace 연결 문서

5.2 Generation 확인

v2의 Linked Generations에서 확인한 실제 실행 세 건

v2에서 Linked Generations 탭을 연다. 처음에는 No results였지만 Environment 필터에 sdk-experiment를 포함하자 실제 Generation 세 건이 보였다. 표시된 지연은 약 1.86초, 2.34초, 2.22초였다. 이 실험의 관측이 저장된 환경과 화면 필터가 일치해야 한다.

캡처는 v2에 연결된 실행 목록이다. 개별 입출력을 펼친 화면과는 구분한다. 프롬프트를 저장한 뒤 이 목록에 실행이 연결되는 것까지 확인해야 사용 버전을 추적할 수 있다. 아래 토큰·응답 값은 동일한 실제 호출의 저장 결과에서 가져왔다.

“Ollama의 주된 역할은 무엇인가?”를 물은 각 버전 한 번의 관측값이다. 평균이나 벤치마크가 아니다.

항목v1v2
입력 토큰58131
출력 토큰2894
전체 토큰86225
모델 호출 시간0.775초2.343초

v1은 “Ollama는 로컬에서 대규모 언어 모델(LLM)을 실행하고 관리하는 도구입니다.”라고 답했다. v2는 답·근거·한계로 응답하며 토큰이 증가했다. 이 관측만으로 속도나 품질을 일반화하지 않는다.

연관 Generation이 없다면 조회 기간·버전, 코드의 prompt=prompt, 프로젝트 키, flush()를 확인한다. 프롬프트를 가져오기만 하고 Generation을 기록하지 않았다면 연관 실행은 없다.

5.3 Playground와 실행 위치

Python은 macOS 호스트의 127.0.0.1:11434에 접근한다. Langfuse는 Docker에 있으므로 컨테이너 안의 127.0.0.1은 컨테이너 자신을 가리킨다.

SDK 호출의 성공이 Playground 연결까지 뜻하지는 않는다. 이번에는 서버 측 모델 연결을 설정하지 않았으며, SDK 추론과 UI 조회 경로를 재현한다.

6. 직접 확인할 과제

  1. version=1과 label="study-baseline"으로 가져온 결과의 prompt.version이 같은지 확인한다.
  2. 질문 문자열을 바꾸고 compile() 결과를 출력해, 버전은 그대로인 채 변수만 치환되는지 확인한다.
  3. v1·v2의 같은 질문 결과에서 형식 준수와 내용 정확성을 각각 판단한다.
  4. 다음 실험에는 프롬프트 본문·Config·데이터셋 중 무엇을 고정할지 먼저 적는다.

Version으로 실행 조건을 식별하고 Label로 다음 호출의 선택을 제어한다. Generation 연결까지 확인하면 수정이 실제 결과에 미친 영향을 추적할 수 있다.

다음 편에서는 두 프롬프트를 같은 세 문항으로 실행한 Experiment를 비교하며 점수를 읽는다.


실습·문서 확인: 2026-10-04~05(KST). Langfuse 로컬 서버 4.50.0 기준. 그림의 Langfuse 로고는 공식 브랜드 자산을 제품 식별 목적으로 사용했다.

profile
어제보다 더 성장하는 나

0개의 댓글