좋은 trace는 어떤 모습일까요?

좋은 trace는 어떤 모습일까요? (What does a good trace look like?)

Langfuse에 trace가 보이긴 하는데, 제대로 했는지 어떻게 알 수 있을까요? 확인하고 최적화할 수 있는 몇 가지가 있어요. trace 구조는 단지 보기 좋은 것만이 아니에요 — 많은 Langfuse 기능이 그 위에 구축돼요.

출처: 문서

본문

Langfuse에 trace가 나타나지만, 잘했는지 어떻게 알 수 있을까요? 살펴보고 최적화할 수 있는 몇 가지가 있어요.

trace 구조는 단지 보기 좋은 것만이 아니에요 — 많은 Langfuse 기능이 그 위에 구축돼요:

  • LLM-as-a-Judge 평가기는 observation을 이름과 유형으로 타겟팅하고, 그 입력과 출력을 읽어요.
  • 대시보드는 trace와 observation 이름으로 메트릭을 필터링·집계해요.
  • 데이터셋 experiment는 run 간 trace 입력과 출력을 비교해요.
  • tracing 테이블의 저장된 뷰는 이름과 속성을 참조해요.

잘 구조화된 trace는 오늘 디버깅을 더 빠르게 하고, 의미 있는 입력/출력을 가진 안정적인 이름은 애플리케이션이 진화해도 평가기·대시보드·experiment가 계속 작동하게 해줘요.

하나의 trace의 범위는 무엇인가요? (What's the scope of one trace?)

Langfuse의 데이터 모델은 세 가지 그룹화 레벨이 있어요: observations(개별 단계)는 trace_id로 trace에 그룹화되고, trace는 session_id로 session에 그룹화될 수 있어요.

trace는 애플리케이션에서 하나의 자족적(self-contained) 작업 단위를 나타내요. 전형적인 trace의 좋은 예시:

  • 한 번의 챗봇 턴 (사용자가 메시지를 보내고, 앱이 컨텍스트를 검색하고, LLM을 호출하고, 응답을 반환)
  • 한 번의 에이전트 실행 (에이전트가 작업을 받고, 추론하고, 도구를 호출하고, 결과를 생성)
  • 한 번의 파이프라인 실행 (문서가 들어오고, 청크되고, 임베딩되고, 저장됨)

이 중 여러 개가 순차적으로 발생하면(예: 다중 턴 대화, 최종 보고서에 합쳐지는 여러 에이전트 실행) sessions가 필요해요. 각 단계는 자체 trace이고, session이 그것들을 묶어줘요. 챗봇의 경우 턴마다 하나의 trace, 대화마다 하나의 session을 뜻해요 — 대화가 언제 끝날지 미리 알 수 없고, per-turn 모델은 trace를 작고 session 뷰에서 탐색하기 쉽게 유지해줘요.

trace는 Langfuse UI에서 trace 트리와 에이전트 그래프(agent graph)로 나타나요:

Trace tree

Agent graph

trace 트리 살펴보기 (Look at the trace tree)

trace를 클릭하면 trace 트리가 보여요. 확인할 수 있는 몇 가지:

올바른 단계가 표시되고 있나요? (Are the right steps showing up?)

LLM 호출, 도구 호출, 기타 중요한 단계가 트리에 표시되어야 해요. 올바른 observation 유형을 가져야 해요.

예를 들어:

  • LLM 호출은 generation으로 표시되어야 해요. generation은 비용, 토큰 사용량, 모델 정보를 담을 수 있어서 중요해요.
  • 도구 호출은 tool로 표시되어야 해요. 그러면 LLM-as-a-Judge 평가기를 만들 때 도구 호출 observation으로 필터링할 수 있어요.

프레임워크 통합은 보통 이런 유형을 자동으로 설정해요. 수동으로 계측한다면 Python의 as_type 파라미터나 JS/TS의 asType으로 설정할 수 있어요. 전체 목록은 observation types 문서를 참고하세요.

올바르게 중첩되어 있나요? (Is it nested correctly?)

도구 호출은 요청한 generation의 형제로, 그 단계를 조율하는 agent나 span observation 아래에 중첩되어야 해요. 그래야 도구 호출이 trace 루트에 매달려 있지 않고, 각 액션이 어느 단계에 속하는지 트리가 보여줘요.

프레임워크 통합은 보통 자동으로 올바르게 처리해요. 수동 계측의 경우 nesting observations를 참고하세요.

LLM 호출 집계에 주의하세요. 에이전트 루프에서 각 모델 호출에 대해 하나의 generation이 보여야 하고, 그 요청한 tool 호출과 번갈아 나타나야 해요. 전체 루프를 최종 출력만 기록하는 하나의 부모 generation으로 감싸는 것을 피하세요. 그러면:

  • 각 도구 결과 후 에이전트가 무엇을 결정했는지
  • 각 결정 뒤의 thinking
  • 어떤 도구 호출이 에이전트의 컨텍스트 창을 폭발시켰는지(비용 최적화에 유용) 를 볼 수 없게 돼요.

전체 루프를 감싸는 하나의 generation. 집계 비용과 최종 출력만 보여요. Agent loop collapsed into one generation

각 모델 호출이 자체 generation. 매 단계 후 thinking, 토큰, 비용을 볼 수 있어요. Agent loop with interleaved generations

thinking이 캡처되어 있나요? (Is thinking captured?)

추론 모델은 답하거나 도구를 호출하기 전에 thinking(추론이라고도 함)을 생성해요. 트리의 각 generation에 항상 thinking을 캡처해야 해요. thinking 데이터는 에이전트가 특정 도구/엔드포인트 등을 호출하는 특정 결정을 내린 이유를 디버깅하는 데 핵심이에요.

필요 없는 노이즈가 있나요? (Is there noise you don't need?)

트리의 모든 observation이 애플리케이션이 무엇을 했는지 이해하는 데 유용한 건 아니에요. HTTP observation, 데이터베이스 쿼리, 프레임워크 내부는 의미 있는 통찰 없이 지저분함만 더하는 경우가 많아요. 이런 observation이 trace 트리를 오염시키면 필터링해서 제거할 수 있어요.

Noisy observations in a trace tree

좋은 이름 고르기 (Choose good names)

observation·trace 이름은 여러 곳에서 사용돼요:

  • LLM-as-a-Judge 평가기를 설정할 때 이름으로 특정 observation을 타겟팅해요.
  • 대시보드에서 observation 이름으로 메트릭을 필터링·집계할 수 있어요.
  • tracing 테이블에서 이름은 각 단계가 무엇을 하는지 빠르게 식별하게 해줘요.

이름이 이런 모든 곳에서 참조되므로, 이름을 API처럼 다루세요: 이름이 바뀌면 옛 이름을 타겟팅하는 평가기·대시보드 쿼리·저장된 필터가 조용히 매칭을 중단해요. 이름을 신중히 고르고 안정적으로 유지할 것을 기대하세요.

능동 언어(active language)를 사용하세요. observation을 수행하는 액션에 따라 동사 먼저로 이름을 지으세요: classify-intent, retrieve-context, generate-response, summarize-results. 이렇게 하면 trace 트리가 애플리케이션이 한 일의 설명처럼 읽히고, 특정 단계 필터링이 쉬워져요.

동적 값을 이름에서 빼세요. process-order를 쓰지 process-order-8945generate-response-retry-2를 쓰지 마세요. 이름은 작업을 식별해야지 단일 실행을 식별해서는 안 돼요 — 그렇지 않으면 매 trace가 새 이름을 만들어 내고 그룹화·필터링·타겟팅이 불가능해져요. run별 값은 metadata에 넣으세요. (OpenTelemetry가 span 이름에 권장하는 것과 같은 low-cardinality 규칙이에요.)

observation을 사용된 AI 모델 이름(gpt-4o, claude-sonnet)으로 지으려 하지 마세요. 이름을 참조하는 모든 필터·평가기·대시보드는 모델을 바꾸는 순간 깨져요. 모델은 generation observation의 별도 속성이므로 그것을 사용하세요.

의미 있는 입력과 출력 고르기 (Choose meaningful input and output)

일반적으로 작업은 입력 및/또는 출력을 가져야 해요. observation에 둘 다 없다면, 정말 유용한지 아니면 버릴 수 있는지 스스로에게 물어보세요.

루트 observation이 가장 많은 주의를 받을 자격이 있어요: trace 레벨 입력·출력이 여기서 파생돼요. 그것들은 tracing 테이블에 표시되고, 평가기가 읽고, 데이터셋 experiment에서 run 간 비교돼요. 리뷰어가 한눈에 필요한 것으로 설정하세요 — 챗봇의 경우 입력은 사용자 메시지, 출력은 어시스턴트 응답 — 함수 인자의 원시 JSON 덩어리가 아니라. 디버깅에 원시 페이로드가 필요하면 metadata에 넣으세요.

가장 많이 보는 observation에 대해 추가로 신경 써서 설정하세요. tracing과 session 화면에 사전 필터링된 뷰를 만들게 될 거예요. 여기서 필터링하는 observation들이 가장 많이 보게 될 것들이에요. 이들에 대해 스스로에게 물어보세요: trace/session을 한눈에 빠르게 평가하려면 무엇을 봐야 하나요?

Tracing table with input and output

GENERATION observation의 전형적인 입력/출력:

  • 챗봇: 사용자 메시지(입력)와 어시스턴트 응답(출력).
  • RAG 파이프라인: 사용자 쿼리와 생성된 답변.
  • 분류 작업: 분류되는 텍스트와 예측된 라벨.

대부분의 입력/출력은 원시 JSON 덩어리가 아니라 읽을 수 있고 역할이 라벨된 대화로 렌더링될 수 있어요. 원시 JSON으로 표시된다면 포맷을 살펴보세요: 표준 OpenAI 형식의 메시지 목록(각각 role과 content)이어야 하고, 도구 호출은 assistant 메시지의 tool_calls 배열에 각 호출의 인자가 JSON 인코딩 문자열(예: "{\"location\": \"Paris\"}")로 주어질 때만 카드로 렌더링돼요.

입력·출력 필드가 의도치 않게 비어 보이면, trace의 입력·출력이 왜 비었는지 FAQ를 참고하세요.

유용한 속성 (Useful attributes)

Observations에는 사용 사례에 유용한 여러 속성이 있어요. 필터링, 점수화, 대시보드 제작을 더 멀리 끌어올려줘요.

컨텍스트를 위한 메타데이터 추가 (Add metadata for context)

Metadata는 각 observation의 유연한 키-값 저장소예요. 이름이나 입력/출력에 속하지 않는 유용한 컨텍스트가 여기 들어가요. 실제로 유용한 메타데이터 예시:

  • 평가 컨텍스트: Ground truth, 기대 동작, 또는 LLM-as-a-Judge 평가기가 필요하지만 실제 입력/출력의 일부가 아닌 컨텍스트. 평가기는 변수 매핑에서 메타데이터 필드를 참조할 수 있어요.
  • 요청 컨텍스트: 내부 요청 ID, 요청을 처리한 API 라우트나 앱 버전, 또는 활성 중이던 experiment 변형/피처 플래그. trace를 다른 시스템과 연관짓고 롤아웃별로 필터링하게 해줘요.
  • 검색 컨텍스트: RAG 단계의 경우 데이터 소스, 검색된 청크 수, 쿼리된 인덱스 같은 것. 검색 단계가 나쁜 결과를 반환한 이유를 디버깅할 때 유용해요.
  • 원시 페이로드: 입력/출력 필드를 지저분하게 만들지만 디버깅에 간혹 필요한 전체 요청/응답 객체.
  • 주석 컨텍스트: 수동 검토 시 메타데이터는 주석자가 더 나은 판단을 내리도록 추가 정보를 줘요.

Langfuse UI에서 메타데이터 키로 필터링할 수 있어요. 특정 특성을 가진 trace를 찾아야 할 때 유용해요.

generation에서 모델·토큰·비용 추적 (Track model, tokens, and cost on generations)

LLM 사용 비용을 모델·사용자·기능별로 이해하려면 generation observation에 세 가지가 필요해요:

  • 모델 이름: Langfuse가 모델 가격 테이블에서 가격을 조회하는 데 사용해요. 모델 이름이 일치하지 않으면 Langfuse가 비용을 자동 계산할 수 없어요.
  • 사용량 상세(Usage details): 입력 토큰, 출력 토큰, 선택적으로 캐시된 토큰. 대시보드의 토큰 사용량 뷰를 구동해요.
  • 비용 상세(Cost details, 선택): Langfuse의 자동 가격을 재정의하려면 — 예를 들어 커스텀 가격 계약이 있다면 — 비용을 명시적으로 전달할 수 있어요.

대부분의 통합은 이 모든 것을 자동으로 캡처해요. 수동 계측의 경우 token and cost tracking 문서를 참고하세요.

이 속성들을 Langfuse UI의 GENERATION observation에서 볼 수 있어요.

Generation attributes in Langfuse

비즈니스 레벨 차원에 태그 사용 (Use tags for business-level dimensions)

Tags는 비즈니스에 중요한 차원에 걸친 필터링과 메트릭 분석을 가능하게 해요. 좋은 태그는 "웹과 API 사용자 간 지연시간이 어떻게 다른가?" 같은 질문에 답해요.

태그의 한 속성은 불변이며 observation 생성 시점에 설정해야 한다는 것이에요. 그래서 사전에 아는 것(요청이 어디서 왔는지, 어떤 기능의 일부인지)에는 훌륭하지만, 나중에 배우는 것에는 적합하지 않아요.

LLM-as-a-Judge 평가 결과처럼 사후에 결정되는 것으로 trace에 라벨을 붙여야 한다면 scores를 사용하세요.

프롬프트를 trace에 연결 (Link prompts to traces)

Langfuse에서 프롬프트를 관리한다면 generation에 연결할 수 있어요. 주어진 trace에 어떤 프롬프트 버전이 사용됐는지 보고, 프롬프트 버전 간 메트릭이 어떻게 변하는지 추적할 수 있어요. 프롬프트를 반복하며 성능을 비교하고 싶을 때 유용해요.

환경 설정 (Set the environment)

프로덕션 대시보드와 평가를 테스트 trace가 오염시키지 않도록 environment 속성(production, staging, development)을 설정하세요.

사용자 ID로 사용자 추적 (Track users with user IDs)

user ID를 설정하면 trace가 특정 사용자와 연결되어 Langfuse의 per-user 뷰가 열려요. 이런 질문에 답하고 싶을 때 유용해요:

  • 어떤 사용자가 가장 많은 비용을 쓰나요?
  • 출력 품질이 사용자별로 어떻게 다른가요?
  • 특정 사용자의 사용 패턴은 어떤 모습인가요?

관련 trace를 session ID로 그룹화 (Group related traces with session IDs)

애플리케이션이 논리적으로 함께 속하는 여러 trace를 포함한다면 session으로 그룹화하세요. 전체 상호작용을 순서대로 볼 수 있는 session replay 뷰를 제공해요.

이렇게 하면:

  • 챗봇을 만들 때 (각 사용자 메시지가 새 trace를 만들지만 전체 대화는 하나의 session)
  • 최종 출력에 각각 기여하는 여러 에이전트가 있을 때 (예: 보고서를 만들기 위해 협력하는 다섯 에이전트)
  • 워크플로가 사이에 human-in-the-loop 단계를 두고 여러 요청에 걸칠 때

애플리케이션이 단일 요청/단일 응답이고 호출 간 연속성이 없다면 session이 필요 없을 거예요.

Sessions view in Langfuse

더 알아보기 (Learn more)