반응형
250x250
Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
Tags
- 도커
- Python
- aws
- chartjs
- openlens
- Flux
- Spring Error
- AWS CI/CD 구축하기
- Airflow
- VPN
- redis
- COALESCE
- java bigdecimal
- bootstrap
- codepipeline
- codedeploy
- Jenkins
- Kafka
- kubeflow
- codebuild
- Spring
- codedeploy error
- PostgreSQL
- JavaScript
- IntelliJ
- node
- aws cicd
- chart.js
- docker
- SQL
Archives
- Today
- Total
Small Asteroid Blog
Langfuse의 Span과 Generation의 관계 본문
728x90
Langfuse를 사용하다가 "왜 LLM 호출할 때마다 Generation이 자동으로 생성되지?" "Span과 Generation의 차이가 뭐지?" 같은 궁금증이 생겼습니다.
Langfuse 기본 개념 이해하기
Trace, Span, Generation의 관계
Trace (전체 LLM 애플리케이션 요청)
├── Span (입력 검증)
├── Span (비즈니스 로직)
│ └── Generation (LLM 호출)
└── Span (결과 후처리)
각각의 역할
- Trace: 전체 요청의 메타데이터 저장소 (user_id, session_id, 전체 비용 등)
- Span: 일반적인 작업 단위 (함수나 메서드 단위의 작업)
- Generation: LLM 호출에 특화된 Span (토큰, 비용, 모델 파라미터 등 자동 추적)
"LLM 호출하면 자동으로 Generation이 생성되나요?"
결론부터 말하면: 아니요!
일반 OpenAI 사용 시
import openai
# 이렇게 호출해도 Langfuse는 아무것도 모릅니다
response = openai.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": "안녕하세요"}]
)
# → Langfuse에 아무것도 기록되지 않음
Langfuse OpenAI 래퍼 사용 시
from langfuse.openai import OpenAI # 핵심은 여기!
client = OpenAI(api_key="your-key")
# 이제 자동으로 Generation이 생성됩니다
response = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": "안녕하세요"}]
)
# → 자동으로 Langfuse에 Generation 기록됨
핵심: langfuse.openai.OpenAI는 일반 OpenAI 클라이언트를 래핑하여 자동으로 Generation을 생성하는 특별한 클라이언트입니다.
실제 코드로 보는 Span과 Generation의 관계
실제 프로덕션 코드를 통해 왜 Span 밑에 Generation이 생성되는지 살펴보겠습니다:
class LLMCaller:
def __init__(self, service_name: ServiceName):
# 핵심: langfuse.openai 사용
self.client = OpenAI(api_key=api_key)
self.langfuse_client = get_langfuse_client(service_name)
def call(self, prompt_name: str, input_vars: dict, **kwargs):
# 1. 수동으로 Span 생성
with self.langfuse_client.start_as_current_span(
name="llm_business_logic",
input=input_vars
) as root_span:
# 2. Trace 메타데이터 설정
root_span.update_trace(
user_id=kwargs.get("user_id"),
session_id=kwargs.get("session_id"),
tags=kwargs.get("tags")
)
# 3. LLM 호출 - 여기서 자동으로 Generation 생성!
response = self.client.chat.completions.create(**params)
# ↑ langfuse.openai.OpenAI가 현재 활성 span을 감지하고
# 그 span의 child로 generation을 자동 생성
# 4. 결과 처리
result = self.process_response(response)
root_span.update_trace(output={"result": result})
return result
실행 흐름 상세 분석
# 사용자 호출
result = llm_caller.call(
prompt_name="product_analysis",
input_vars={"product": "iPhone"},
user_id="user123",
session_id="session456"
)
생성되는 구조
Trace: product_analysis
├── user_id: "user123" ← Span을 통해 설정
├── session_id: "session456" ← Span을 통해 설정
├── input: {"product": "iPhone"} ← Span에서 설정
└── output: {"result": "..."} ← Span에서 설정
└── Span: llm_business_logic (150ms)
├── 전체 비즈니스 로직 시간 추적
└── Generation: product_analysis (80ms) ← 자동 생성
├── model: "gpt-4"
├── prompt_tokens: 120
├── completion_tokens: 80
├── cost: 0.004
└── duration: 80ms
왜 Span을 추가로 만드는가?
"Generation만으로도 충분한데 왜 Span을 만들어?" 라는 의문이 들 수 있습니다.
1. Trace 메타데이터 설정
Langfuse에서는 Generation만으로는 Trace 레벨 정보를 설정할 수 없습니다
# ❌ Generation만으로는 이런 정보들을 설정할 수 없음
user_id = "user123"
session_id = "session456"
tags = ["ecommerce", "analysis"]
# ✅ Span을 통해야 Trace 정보 설정 가능
with span as root_span:
root_span.update_trace(
user_id=user_id,
session_id=session_id,
tags=tags
)
2. 전체 비즈니스 로직 추적
# Span이 감싸는 전체 작업
with span as root_span:
├── 1. 프롬프트 가져오기 (Langfuse에서)
├── 2. 파라미터 검증 및 빌드
├── 3. LLM 호출 ← Generation 자동 생성
├── 4. 응답 후처리 (JSON 파싱)
└── 5. 최종 결과 설정
Span 없이는 LLM 호출 시간만 알 수 있고, 전체 로직의 성능은 추적할 수 없습니다.
3. Context 관리
# 이 Context 내에서 일어나는 모든 Langfuse 작업은
with self.langfuse_client.start_as_current_span(...) as span:
# 이 span의 하위 요소가 됩니다
response = self.client.chat.completions.create(...)
# ↑ Generation이 span의 child가 되는 이유
Span 정보가 적어 보이는 이유
"Span은 Trace나 Generation 대비 정보가 별로 없는데?" 라는 의문이 들 수도 있습니다.
현재 코드에서의 Span 역할
# 현재: Span이 단순 wrapper 역할
Span {
name: "business_logic",
duration: 150ms,
status: "success"
// 실제 정보는 별로 없음...
}
# 대신 Trace와 Generation에 모든 정보가 집중
Trace { user_id, session_id, input, output, ... }
Generation { model, tokens, cost, ... }
더 복잡한 시나리오에서의 Span 활용
실제로는 이렇게 여러 단계로 나누어 사용할 때 Span이 빛을 발합니다:
def complex_product_analysis(self, product_data):
with self.langfuse_client.trace(name="product_analysis") as trace:
# 각 단계별로 의미있는 Span들
with trace.span(name="data_validation") as validation_span:
validated_data = self.validate(product_data)
validation_span.update(
input=product_data,
output=validated_data,
metadata={"validation_rules": 5}
)
with trace.span(name="market_research") as research_span:
market_data = self.get_market_data()
research_span.update(
output=market_data,
metadata={"api_calls": 3, "cache_hit": True}
)
with trace.span(name="llm_analysis") as llm_span:
# 여기서 Generation 자동 생성
analysis = self.llm_call(validated_data, market_data)
llm_span.update(metadata={"prompt_version": "v2.1"})
with trace.span(name="result_formatting") as format_span:
final_result = self.format_result(analysis)
format_span.update(
output=final_result,
metadata={"format_type": "json"}
)
결과
Trace: product_analysis (500ms, $0.02)
├── Span: data_validation (50ms)
│ ├── validation_rules: 5
│ └── input/output 정보
├── Span: market_research (200ms)
│ ├── api_calls: 3
│ ├── cache_hit: true
│ └── market_data 정보
├── Span: llm_analysis (200ms)
│ ├── prompt_version: "v2.1"
│ └── Generation: gpt4_call (150ms, $0.015)
│ ├── model: "gpt-4"
│ ├── tokens: 800
│ └── LLM 상세 정보
└── Span: result_formatting (50ms)
├── format_type: "json"
└── 포맷팅 결과
언제 무엇을 사용할까?
Generation을 사용하는 경우
- LLM 호출을 추적할 때
- 토큰 사용량과 비용을 자동으로 계산하고 싶을 때
- Langfuse의 LLM 특화 대시보드 기능을 활용하고 싶을 때
Span을 사용하는 경우
- 일반적인 비즈니스 로직을 추적할 때
- 여러 단계의 워크플로우를 구조화하고 싶을 때
- 커스텀 메타데이터가 필요할 때
권장 패턴
def process_user_request(user_input):
# 전체 요청을 trace로 감싸기
trace = langfuse.trace(name="user_request_processing")
# 각 단계별로 span 생성
with trace.span(name="input_validation") as span:
validated_input = validate(user_input)
span.update(output=validated_input)
with trace.span(name="business_logic") as span:
# LLM 호출은 자동으로 Generation 생성
result = llm_caller.call(prompt="analyze", input=validated_input)
span.update(metadata={"processing_steps": 3})
with trace.span(name="response_formatting") as span:
formatted_result = format_response(result)
span.update(output=formatted_result)
return formatted_result
자동화 옵션들
매번 수동으로 Generation을 만들기 번거롭다면 이런 방법들을 사용할 수 있습니다:
1. Langfuse OpenAI 통합
from langfuse.openai import openai
# 이후 일반 openai처럼 사용하면 자동 추적
response = openai.chat.completions.create(...)
2. 데코레이터 사용
from langfuse.decorators import observe
@observe()
def my_llm_function(prompt):
return openai.chat.completions.create(...)
3. LangChain 통합
from langfuse.callback import CallbackHandler
handler = CallbackHandler()
llm = ChatOpenAI(callbacks=[handler])
결론
Langfuse의 Span과 Generation은 처음에는 복잡해 보이지만, 각각의 역할을 이해하면 매우 강력한 도구입니다.
- Trace: 전체 요청의 메타데이터와 컨텍스트
- Span: 비즈니스 로직의 구조화된 추적
- Generation: LLM 호출의 자동화된 상세 추적
핵심은 langfuse.openai 래퍼를 사용하면 자동으로 Generation이 생성되고, 적절한 Span 구조를 만들면 전체 애플리케이션의 성능과 비용을 효과적으로 모니터링할 수 있다는 점입니다.
728x90
반응형
'AI' 카테고리의 다른 글
| Antigravity 개인적인 사용후기 (0) | 2025.11.27 |
|---|---|
| Serena MCP 사용해 토큰 절약하고 퀄리티 높이기 (0) | 2025.08.16 |
| 백엔드 개발자를 위한 Claude 꿀팁 (0) | 2025.07.20 |
| Claude CLI + Gemini CLI 조합으로 토큰 절약하기(GPT 대체 가능) (0) | 2025.07.13 |
| Langfuse v2 → v3 업그레이드 시 Trace Name 공란으로 기록됨 (0) | 2025.07.10 |