본문으로 건너뛰기

"Evaluation" 태그 — 2개 게시물

평가, 실험, 벤치마크 경험

모든 태그 보기

MLflow 기술 검토(RAG 성능 실험 테스트베드 기능)

· 약 10분
김태한
AI Research Engineer, Brain Crew

TL;DR

MLflow는 전통적인 ML과 LLM 애플리케이션의 전체 생명주기를 관리할 수 있는 오픈소스 통합 플랫폼입니다. LangGraph 기반 RAG Agent 개발 시 Tracking으로 하이퍼파라미터와 메트릭을 관리하고, Tracing으로 복잡한 LLM 호출 흐름을 추적하며, Evaluation으로 LLM-as-a-Judge를 포함한 다양한 평가 지표를 자동화할 수 있습니다. OpenTelemetry 표준 준수로 기존 옵저버빌리티 도구와 쉽게 연동 가능하며, 완전 오픈소스로 온프레미스/클라우드 자체 호스팅이 가능합니다.

Key Takeaways

  • 계층적 실험 관리: Experiment > Run 구조로 RAG 시스템의 chunk_size, top_k, temperature 등 다양한 설정 조합을 체계적으로 추적하고 MLflow UI에서 시각적으로 비교할 수 있어, 최적 구성 탐색이 효율적입니다.

  • OpenTelemetry 표준 기반 Tracing: MLflow Tracing은 OpenTelemetry 호환으로 Grafana, Datadog, New Relic 등 기존 옵저버빌리티 도구와 즉시 연동 가능하며, Span 계층 구조로 복잡한 LLM 체인의 입출력과 토큰 사용량을 단계별로 추적할 수 있습니다.

  • LLM-as-a-Judge 평가 자동화: faithfulness, answer_relevance 등 주관적 품질 지표를 LLM으로 자동 평가하고, make_genai_metric()으로 커스텀 평가 기준을 프롬프트로 정의하여 평가 프로세스를 표준화할 수 있습니다.

  • 프롬프트 버전 관리: 프롬프트를 실험 변수 및 아티팩트로 저장하여 변경 사항을 추적하고, 여러 프롬프트 변형의 성능을 정량적으로 비교해 A/B 테스트를 수행할 수 있습니다.

  • 오픈소스 vs 상용 플랫폼 선택: MLflow는 Apache 2.0 오픈소스로 인프라 비용만 발생하고 전통적 ML과 LLM을 통합 관리 가능한 반면, LangSmith는 LangChain 전용 SaaS로 즉시 사용 가능하지만 벤더 종속성이 있습니다. 프로젝트 요구사항(자체 호스팅 필요성, 기존 도구 통합, 비용 구조)에 따라 선택해야 합니다.

상세 내용

배경: LLM 애플리케이션의 실험 관리 문제

LLM 기반 RAG Agent 개발은 전통적인 ML 모델과 다른 어려움이 있습니다. 문서 청크 크기, 임베딩 모델, 검색 개수(top_k), LLM temperature, 프롬프트 템플릿 등 수많은 하이퍼파라미터 조합을 실험해야 하며, LLM 출력의 비결정성과 주관성으로 인해 품질 평가가 어렵습니다. 또한 여러 컴포넌트(Retriever → LLM → Reranker)가 연결된 복잡한 파이프라인에서 병목 구간을 찾기 위해 각 단계의 실행 흐름을 추적해야 합니다.

MLflow는 이러한 문제를 해결하기 위해 전통적인 ML과 LLM 애플리케이션을 모두 지원하는 통합 플랫폼으로, Tracking, Tracing, Evaluation 기능을 제공합니다.

1. MLflow Tracking: 실험 파라미터와 메트릭 관리

핵심 구조

MLflow는 Experiment(실험 그룹) > Run(단일 실행) 계층 구조로 실험을 조직화합니다. 각 Run은 고유 ID로 식별되며 다음을 기록합니다:

  • Parameters: 모델 설정값 (불변)
  • Metrics: 정량적 성능 지표 (시간에 따라 변화 가능)
  • Artifacts: 파일 형태의 결과물 (모델, 설정 파일, 생성된 답변 등)
  • Tags: 메타데이터 (환경, 버전 등)

RAG 시스템 적용 사례

import mlflow

mlflow.set_experiment("rag-optimization")

with mlflow.start_run(run_name="chunk-512-topk-5"):
# 파라미터 기록
mlflow.log_param("chunk_size", 512)
mlflow.log_param("chunk_overlap", 50)
mlflow.log_param("top_k", 5)
mlflow.log_param("embedding_model", "text-embedding-3-small")
mlflow.log_param("llm_model", "gpt-4")
mlflow.log_param("temperature", 0.7)

# RAG 파이프라인 실행
result = rag_pipeline.run(query)

# 메트릭 기록
mlflow.log_metric("response_time", result.latency)
mlflow.log_metric("num_retrieved_docs", len(result.docs))
mlflow.log_metric("total_tokens", result.token_count)

# 아티팩트 저장
mlflow.log_text(result.answer, "generated_answer.txt")
mlflow.log_dict(result.config, "config.json")

의사결정 포인트: Parameters는 실험 시작 시 결정되는 불변 값으로, Metrics는 실행 중/후에 측정되는 성능 지표로 구분해야 합니다. 프롬프트 템플릿은 아티팩트로 저장하면 버전별 변경 사항을 명확히 추적할 수 있습니다.

UI 활용

MLflow UI에서 여러 Run의 메트릭을 테이블 또는 차트로 비교할 수 있습니다. 예를 들어 chunk_size와 response_time의 상관관계를 scatter plot으로 시각화하거나, top_k 값에 따른 답변 품질 변화를 확인할 수 있습니다. Backend 서버로 배포하면 REST API로 프로그래밍 방식 접근도 가능합니다.

2. MLflow Tracing: LLM 호출 흐름 추적

핵심 개념: Trace와 Span

MLflow Tracing은 OpenTelemetry 표준을 따르며 다음 구조를 사용합니다:

  • Trace: 전체 요청/세션의 실행 흐름
  • Span: Trace를 구성하는 개별 작업 단위 (시작/종료 시간, 입출력, 메타데이터 포함)
  • Span은 Parent-Child 계층 구조로 실행 순서를 표현

자동 Tracing vs 수동 Tracing

자동 Tracing (Autolog):

import mlflow

# LangChain 자동 추적 활성화
mlflow.langchain.autolog()

# 이후 LangChain 호출은 자동으로 Trace 생성
chain = create_retrieval_chain(retriever, llm)
result = chain.invoke({"input": "What is RAG?"})

자동 추적은 다음을 자동 수집합니다:

  • 각 컴포넌트(Retriever, LLM, Chain)별 Span
  • 입출력 데이터
  • 토큰 사용량 (LLM 호출 시)
  • 실행 시간

수동 Tracing (Decorator):

import mlflow

@mlflow.trace(name="document_preprocessing", span_type="PROCESSING")
def preprocess_documents(docs):
"""문서 전처리 로직"""
mlflow.set_span_attribute("num_docs", len(docs))
processed = [clean_text(doc) for doc in docs]
return processed

@mlflow.trace(name="rerank_documents", span_type="RETRIEVAL")
def rerank(query, docs):
"""검색 결과 재정렬"""
scores = compute_relevance_scores(query, docs)
mlflow.set_span_attribute("rerank_model", "cross-encoder-v2")
return sorted(zip(docs, scores), key=lambda x: x[1], reverse=True)

의사결정 근거: 자동 추적은 LangChain/LangGraph 기본 동작을 추적할 때 편리하지만, 비즈니스 로직이나 커스텀 컴포넌트는 수동 데코레이터로 명시적으로 Span을 추가해야 합니다. 특히 문서 전처리, 필터링, 재정렬 등 RAG 파이프라인의 중간 단계를 추적하려면 수동 Tracing이 필수입니다.

Trace 시각화

MLflow UI의 Traces 탭에서 다음을 확인할 수 있습니다:

  • Tree View: Span 간 계층 구조와 실행 순서
  • Timeline View: 각 Span의 시작/종료 시간과 병렬 실행 여부
  • Details Panel: Span별 입출력 데이터, 토큰 사용량, 커스텀 속성
  • Error Tracking: 예외 발생 Span 및 스택 트레이스

OpenTelemetry 통합

MLflow Tracing은 OpenTelemetry 표준을 준수하므로 기존 관찰성 도구와 쉽게 연동됩니다:

from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

# OpenTelemetry 설정
trace.set_tracer_provider(TracerProvider())
otlp_exporter = OTLPSpanExporter(endpoint="http://grafana-tempo:4317")
trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(otlp_exporter))

# MLflow Tracing은 자동으로 OpenTelemetry와 통합됨
mlflow.langchain.autolog()

이를 통해 Grafana, Datadog, New Relic 등에서 MLflow Trace를 시각화하고, 마이크로서비스 환경에서 분산 추적이 가능합니다.

3. MLflow Evaluation: LLM 출력 품질 평가

평가 워크플로우

MLflow Evaluation은 다음 단계로 구성됩니다:

  1. 평가 데이터셋 준비 (Pandas DataFrame)
  2. 모델 또는 예측 함수 정의
  3. 평가 지표(Scorer) 선택
  4. mlflow.evaluate() 실행
  5. 결과 분석 (MLflow UI 또는 API)

평가 데이터셋 구조

import pandas as pd

eval_data = pd.DataFrame({
"inputs": [
"What is the capital of France?",
"Explain quantum computing",
"How does photosynthesis work?"
],
"context": [
"France is a country in Europe. Paris is its capital.",
"Quantum computing uses quantum bits...",
"Plants use sunlight to produce energy..."
],
"ground_truth": [
"Paris",
"A computing paradigm using quantum mechanics",
"Process where plants convert light to chemical energy"
]
})

필수 컬럼:

  • inputs: 모델 입력
  • context: (선택) RAG의 검색 문서 등 추가 정보
  • ground_truth 또는 targets: (선택) 정답 레이블

Built-in Evaluators 활용

import mlflow

# LLM 출력 품질 평가
results = mlflow.evaluate(
model="openai:/gpt-4", # 또는 커스텀 함수
data=eval_data,
model_type="question-answering",
evaluators="default", # 기본 메트릭 세트
extra_metrics=[
mlflow.metrics.toxicity(),
mlflow.metrics.flesch_kincaid_grade_level(),
mlflow.metrics.answer_similarity(),
mlflow.metrics.faithfulness(model="openai:/gpt-4-turbo"),
mlflow.metrics.answer_relevance(model="openai:/gpt-4-turbo")
]
)

print(results.metrics)

주요 LLM 메트릭:

  • toxicity: 유해성 점수 (0~1, 낮을수록 좋음)
  • flesch_kincaid_grade_level: 가독성 (미국 학년 수준)
  • answer_similarity: 임베딩 기반 답변-정답 유사도
  • faithfulness: 답변이 제공된 context에 충실한지 (LLM-as-Judge)
  • answer_relevance: 답변이 질문과 관련 있는지 (LLM-as-Judge)

LLM-as-a-Judge 동작 원리

faithfulness, answer_relevance 같은 메트릭은 LLM을 판단자로 사용합니다:

  1. 평가용 프롬프트 생성 (질문 + 답변 + context)
  2. Judge LLM에 전송 (예: GPT-4)
  3. LLM이 평가 기준에 따라 점수 반환 (1~5 척도 등)
  4. 점수를 정규화하여 저장

의사결정 포인트: Judge 모델은 평가 대상 모델보다 강력해야 신뢰성이 높습니다. 예를 들어 GPT-3.5로 생성한 답변을 GPT-4로 평가하거나, 오픈소스 LLM 출력을 Claude Opus로 평가하는 방식이 권장됩니다.

Custom Evaluators 작성

함수 기반 메트릭:

from mlflow.metrics import make_metric

def contains_keyword(eval_df, builtin_metrics):
"""답변에 특정 키워드가 포함되어 있는지 체크"""
keywords = ["quantum", "photosynthesis", "capital"]
scores = []
for _, row in eval_df.iterrows():
answer = row["outputs"].lower()
score = 1.0 if any(kw in answer for kw in keywords) else 0.0
scores.append(score)
return scores

keyword_metric = make_metric(
eval_fn=contains_keyword,
greater_is_better=True,
name="keyword_presence"
)

mlflow.evaluate(
model=rag_model,
data=eval_data,
extra_metrics=[keyword_metric]
)

LLM-as-Judge 커스텀 메트릭:

from mlflow.metrics.genai import make_genai_metric

custom_faithfulness = make_genai_metric(
name="custom_faithfulness",
definition="답변이 제공된 문서에만 기반하고 외부 지식을 사용하지 않는지 평가",
grading_prompt="""
다음 기준으로 답변을 평가하세요:
- 5점: 모든 정보가 문서에서 직접 추출됨
- 4점: 대부분 문서 기반이지만 약간의 추론 포함
- 3점: 문서와 외부 지식이 혼합됨
- 2점: 주로 외부 지식 사용
- 1점: 문서와 무관한 정보

질문: {inputs}
문서: {context}
답변: {outputs}

점수만 반환하세요 (1~5).
""",
grading_context_columns=["context"],
examples=[
{
"inputs": "What is the capital?",
"outputs": "Paris is the capital.",
"context": "Paris is the capital of France.",
"score": 5,
"justification": "답변이 문서에서 직접 추출됨"
}
],
model="openai:/gpt-4-turbo",
parameters={"temperature": 0.0}
)

mlflow.evaluate(
model=rag_model,
data=eval_data,
extra_metrics=[custom_faithfulness]
)

의사결정 근거: 프로젝트별 도메인 특화 평가 기준(예: 의료 분야의 전문 용어 정확성, 금융 분야의 수치 정확성)은 커스텀 메트릭으로 정의해야 합니다. 프롬프트에 Few-shot 예시를 포함하면 Judge LLM의 평가 일관성이 크게 향상됩니다.

평가 결과 활용

# 평가 결과 출력
print(results.metrics)
# {'toxicity/v1/mean': 0.02, 'faithfulness/v1/mean': 4.5, ...}

# 행별 상세 결과
results_df = results.tables["eval_results_table"]
print(results_df[["inputs", "outputs", "faithfulness/v1/score"]])

# MLflow UI에서 시각화
# - 메트릭별 분포 히스토그램
# - 실패 사례 필터링
# - 여러 모델 간 평가 결과 비교

4. 추가 고급 기능

프롬프트 버전 관리

import mlflow

prompt_template_v1 = """
You are a helpful assistant. Answer the question based on the context.

Context: {context}
Question: {question}
Answer:
"""

with mlflow.start_run():
mlflow.log_param("prompt_version", "v1")
mlflow.log_text(prompt_template_v1, "prompt_template.txt")

# 프롬프트 성능 평가
results = evaluate_prompt(prompt_template_v1, test_data)
mlflow.log_metrics(results)

프롬프트를 파라미터와 아티팩트로 저장하여 변경 사항을 추적하고, 여러 버전의 성능을 비교할 수 있습니다.

Multi-Turn 대화 추적

import mlflow

session_id = "user-123-session-456"

with mlflow.start_run(run_name=f"conversation-{session_id}") as run:
mlflow.set_tag("session_id", session_id)

conversation_history = []
for turn, user_input in enumerate(user_inputs):
with mlflow.start_span(name=f"turn_{turn}") as span:
response = chatbot.reply(user_input, conversation_history)
conversation_history.append({"user": user_input, "bot": response})

mlflow.log_metric(f"turn_{turn}_latency", response.latency)

# 전체 대화 저장
mlflow.log_dict({"conversation": conversation_history}, "full_conversation.json")

session_id 태그로 대화를 그룹화하고, 턴별 Span으로 각 응답을 추적합니다.

OpenTelemetry 분산 추적

마이크로서비스 환경에서 여러 서비스에 걸친 요청을 추적:

# Service A (Gateway)
import mlflow
from opentelemetry import trace

tracer = trace.get_tracer(__name__)

with tracer.start_as_current_span("gateway_request"):
with mlflow.start_span(name="preprocess_input"):
cleaned_input = preprocess(user_query)

# Service B 호출 (Trace context 자동 전파)
response = requests.post("http://rag-service/query", json={"input": cleaned_input})

OpenTelemetry context propagation으로 Service A와 Service B의 Span이 하나의 Trace로 연결됩니다.

5. LangSmith와의 비교

MLflow 선택이 유리한 경우:

  • 전통적인 ML 모델과 LLM을 동일 플랫폼에서 관리하고 싶을 때
  • 온프레미스 또는 프라이빗 클라우드에서 자체 호스팅이 필요할 때
  • 기존 옵저버빌리티 스택(Grafana, Prometheus)과 통합하고 싶을 때
  • 오픈소스 라이선스로 비용 절감이 중요할 때
  • Scikit-learn, PyTorch, TensorFlow 등 다양한 프레임워크를 함께 사용할 때

LangSmith 선택이 유리한 경우:

  • LangChain/LangGraph를 주요 프레임워크로 사용하고, 즉시 사용 가능한 SaaS가 필요할 때
  • Playground 기능으로 프롬프트를 즉시 테스트하고 수정하고 싶을 때
  • 사용자 피드백(thumbs up/down)을 손쉽게 수집하고 관리하고 싶을 때
  • 인프라 운영 리소스가 부족하고 관리형 서비스를 선호할 때

의사결정 체크리스트:

항목MLflowLangSmith
라이선스오픈소스 (Apache 2.0)상용 (Free tier 제한적)
호스팅자체 호스팅 필요클라우드 SaaS
프레임워크 지원범용 (Scikit-learn, PyTorch, LangChain 등)LangChain 전용
통합성OpenTelemetry 표준LangChain 에코시스템
UI/UX범용 실험 관리 UILangChain 특화 대화형 UI
비용인프라 비용만사용량 기반 과금

하이브리드 접근: 일부 팀은 개발 단계에서 LangSmith로 빠른 프로토타이핑을 하고, 프로덕션 배포는 MLflow로 전환하는 전략을 사용합니다.

실전 적용 시 주의사항

1. 민감 데이터 로깅 제어

LLM 입출력에 개인정보가 포함될 수 있으므로 로깅 전 필터링이 필요합니다:

import mlflow
from mlflow.utils.autologging_utils import disable_for_unsupported_versions

# 특정 필드 제외
mlflow.langchain.autolog(log_inputs=False, log_outputs=True)

# 또는 커스텀 필터 적용
def sanitize_output(output):
# 이메일, 전화번호 등 마스킹
return re.sub(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', '[EMAIL]', output)

2. 대규모 Trace 저장 비용

Tracing을 모든 요청에 활성화하면 스토리지 비용이 급증할 수 있습니다. 샘플링 전략 적용:

import random
import mlflow

# 10% 샘플링
if random.random() < 0.1:
mlflow.langchain.autolog()
else:
mlflow.langchain.autolog(disable=True)

3. Evaluation Judge 모델 비용

LLM-as-a-Judge 메트릭은 평가 데이터셋 크기에 비례해 API 호출 비용이 발생합니다. 캐싱 전략 활용:

from mlflow.metrics.genai import make_genai_metric

custom_metric = make_genai_metric(
name="faithfulness",
# ...
model="openai:/gpt-4-turbo",
parameters={
"temperature": 0.0, # 결정적 출력으로 캐싱 효율 향상
"seed": 42
}
)

4. MLflow Server 고가용성

프로덕션 환경에서는 MLflow Tracking Server를 고가용성으로 구성:

  • Backend Store: PostgreSQL/MySQL (복제 구성)
  • Artifact Store: S3/GCS (자동 복제)
  • Load Balancer: 여러 MLflow 서버 인스턴스 앞단 배치
  • 모니터링: Prometheus + Grafana로 서버 상태 추적

References

LLM이 가장 잘 이해하는 Table Format에 대한 평가실험

· 약 8분
김성연
AI Research Engineer, Brain Crew

TL;DR

재무제표 데이터를 LLM에 전달할 때 데이터 포맷에 따라 성능과 비용이 크게 달라집니다. 11가지 포맷을 비교한 결과, TSV(tab-separated) 포맷이 정확도 100%, 최소 토큰 사용(7,192개), 최단 응답시간(8.24초)으로 모든 지표에서 최고 성능을 보였습니다. 반면 DICT와 XML은 프로그래밍 문법의 메타 문자로 인해 토큰을 2배 이상 낭비했고, STRING 포맷은 정확도가 75%로 떨어졌습니다. 실무에서는 TSV가 이론적 최적이지만, Markdown Key-Value가 가독성과 효율성의 균형점으로 더 실용적일 수 있습니다.

Key Takeaways

  • 간결한 포맷이 LLM 성능과 비용 모두 우수: TSV는 XML 대비 토큰을 57% 절감하고 응답속도를 3배 향상시켰습니다. 메타 문자를 최소화하는 것이 핵심입니다.
  • 구조화된 포맷이 필수: STRING 같은 비구조화 포맷은 정확도가 25% 하락합니다. 테이블 형태의 명확한 구조가 LLM의 이해도를 크게 높입니다.
  • 프로그래밍 문법은 토큰 낭비: DICT, XML처럼 {, }, ' 등 메타 문자가 많은 포맷은 실제 데이터 대비 구조 표현에 토큰을 과다 소모합니다. JSON이나 TSV 같은 간결한 대안을 선택하세요.
  • 가독성과 효율성의 트레이드오프 고려: 이론적으로 TSV가 최적이지만, 실무에서는 Markdown Key-Value처럼 사람과 기계 모두 읽기 쉬운 포맷이 유지보수와 확장성 면에서 더 나을 수 있습니다.
  • 포맷 선택은 비용에 직결: 대규모 RAG 시스템에서 포맷 최적화만으로 토큰 비용을 50% 이상 절감할 수 있습니다. 초기 설계 단계에서 포맷을 신중히 선택하세요.

상세 내용

배경: 왜 테이블 포맷이 중요한가?

많은 RAG(Retrieval-Augmented Generation) 파이프라인에서 재무제표, 스프레드시트, 데이터베이스 쿼리 결과 등 테이블 형태의 데이터를 LLM에 전달해야 합니다. 하지만 같은 데이터라도 어떤 포맷으로 인코딩하느냐에 따라 LLM의 이해도, 토큰 사용량, 응답 속도가 크게 달라집니다.

예를 들어, Elasticsearch에서 추출한 재무제표 데이터를 LLM에 전달할 때:

  • JSON으로 보낼 것인가?
  • CSV나 TSV로 보낼 것인가?
  • Markdown 테이블이나 HTML을 사용할 것인가?

이 선택은 시스템 정확도와 운영 비용에 직접적인 영향을 미칩니다. IBK Capital 프로젝트에서는 이 질문에 답하기 위해 체계적인 실험을 수행했습니다.

실험 설계

평가 대상 포맷 (11가지)

  1. TSV (tab-separated values)
  2. JSON
  3. CSV
  4. Markdown Table
  5. HTML Table
  6. Markdown Key-Value
  7. DICT (Python dictionary list)
  8. LaTeX
  9. XML
  10. NumPy array
  11. STRING (자연어 형식)

평가 지표

  • 정확도: LLM이 데이터 기반 질문에 정확히 답변한 비율 (0-1)
  • 토큰 사용량: 프롬프트와 응답에 소요된 총 토큰 수
  • 응답 속도: 질의응답 완료까지 걸린 시간 (초)

실험 환경

  • 모델: AWS Bedrock Claude Sonnet 4.5
  • 데이터: Elasticsearch에서 추출한 실제 재무제표 데이터 (매출액, EBIT, EBITDA 등 약 40개 항목)
  • 질문 형식: "2020년 12월의 매출액은 얼마인가요?" 같은 특정 값 조회

종합 점수 계산 각 지표를 정규화(0-1)한 후 가중 평균:

  • 정확도: 0.5
  • 토큰 효율: 0.3
  • 속도: 0.2

실험 결과: TSV의 압도적 우위

종합 순위

순위포맷종합점수정확도토큰수지연시간(초)
1TSV1.00001.007,1928.24
2JSON0.89731.009,94110.25
3HTML0.87681.0010,22911.59
4Markdown0.85051.009,80516.17
5Markdown KV0.81761.0011,07515.41
..................
10XML0.55231.0016,85225.37
11STRING0.37730.759,18315.30

핵심 발견

  • TSV를 제외한 대부분 구조화 포맷은 정확도 1.00 달성
  • STRING 포맷만 정확도 0.75로 하락 → 구조화가 필수
  • 토큰 사용량 차이: 최소(TSV 7,192) vs 최대(XML 16,852) = 2.3배
  • 응답 속도 차이: 최소(TSV 8.24초) vs 최대(DICT 31.44초) = 3.8배

왜 TSV가 최고 성능을 보이는가?

동일한 재무제표 데이터를 세 가지 포맷으로 표현한 예시로 분석해보겠습니다.

TSV (7,192 토큰)

재무제표	2019/12	2020/12	2021/12	2022/12	2022/09	2023/09
매출액 594,159 591,566 578,744 606,454 473,909 385,849
EBIT 5,148 52,063 13,045 3,755 10,252 -16,558

특징

  • 구분자: 탭 문자 하나만 사용
  • 메타 문자: 거의 없음 (줄바꿈뿐)
  • 정보 밀도: 매우 높음 (실제 데이터에 집중)

Markdown Key-Value (11,075 토큰)

## Record 1

재무제표: 매출액
2019/12: 594,159
2020/12: 591,566
2021/12: 578,744
...

특징

  • 구조 요소: ## Record N, 키-값 구분 :
  • 가독성: 레코드별 명확한 구분
  • 토큰 증가 원인: 마크다운 헤더와 구분자

DICT (14,436 토큰 - TSV의 2배)

[{'재무제표': '매출액', '2019/12': '594,159', '2020/12': '591,566', ...}, 
{'재무제표': 'EBIT', '2019/12': '5,148', '2020/12': '52,063', ...}]

특징

  • 메타 문자 과다: {, }, [, ], ', :, , 반복
  • 키 중복: 각 레코드마다 '재무제표': 반복
  • 토큰 낭비: 프로그래밍 문법에 토큰 소모

토큰 차이 분석

요소TSVMarkdown KVDICT
레코드 구분줄바꿈## Record N}, {
키-값 구분:': '
데이터 구분줄바꿈', '
컨테이너없음코드블록[, ]

TSV가 최소 토큰을 사용하는 이유:

  1. 불필요한 메타 문자 제거: DICT의 {, }, ' 같은 문법 요소 없음
  2. 키 중복 없음: 헤더에 한 번만 키 정의
  3. 구분자 최소화: 탭 하나로 열 구분 (CSV는 쉼표 + 따옴표 필요)

왜 Markdown Key-Value가 DICT보다 나은가?

실험 결과 Markdown Key-Value(11,075 토큰)가 DICT(14,436 토큰)보다 23% 효율적입니다.

DICT의 문제점

# 각 레코드마다 반복되는 메타 문자
{'재무제표': '매출액', '2019/12': '594,159', ...} # { } ' ' : , 모두 토큰 소모
{'재무제표': 'EBIT', '2019/12': '5,148', ...} # 키 이름 '재무제표' 반복

Markdown Key-Value의 장점

## Record 1
재무제표: 매출액
2019/12: 594,159
  1. 간결한 구분자: : 하나로 키-값 구분
  2. 키 중복 최소화: 레코드 헤더로 한 번만 정의
  3. LLM 친화적: 마크다운은 LLM 학습 데이터에 흔한 형식
  4. 가독성: 사람이 읽고 디버깅하기 쉬움

실무 의사결정: TSV vs Markdown Key-Value

이론적 최적: TSV

  • 토큰 최소 (7,192)
  • 속도 최고 (8.24초)
  • 비용 최저

실무 선택: Markdown Key-Value

프로덕션 환경에서 Markdown Key-Value를 선택한 이유:

  1. 가독성과 유지보수성

    # TSV - 기계 최적화
    항목 2020 2021 2022
    매출 100 110 120

    # Markdown KV - 사람과 기계 모두 고려
    ## 2020년 실적
    매출: 100
    영업이익: 10
  2. 디버깅 용이성

    • 로그 파일에서 데이터 확인 시 TSV는 읽기 어려움
    • Markdown은 구조가 명확해 문제 파악 빠름
  3. 확장성

    • 추가 메타데이터 삽입 용이
    • 중첩 구조 표현 가능
    • 주석 추가 가능
  4. 성능 트레이드오프 합리성

    • TSV 대비 54% 토큰 증가 (7,192 → 11,075)
    • 하지만 DICT 대비 23% 절감 (14,436 → 11,075)
    • 실용성 고려 시 충분히 효율적

코드 예시: 포맷 변환 함수

def convert_to_markdown_kv(df, record_name="Record"):
"""DataFrame을 Markdown Key-Value 형식으로 변환"""
result = []
for idx, row in df.iterrows():
result.append(f"## {record_name} {idx + 1}\n")
result.append("```")
for col, value in row.items():
result.append(f"{col}: {value}")
result.append("```\n")
return "\n".join(result)

def convert_to_tsv(df):
"""DataFrame을 TSV 형식으로 변환"""
return df.to_csv(sep='\t', index=False)

# 사용 예시
import pandas as pd

df = pd.DataFrame({
'재무제표': ['매출액', 'EBIT'],
'2020/12': [591566, 52063],
'2021/12': [578744, 13045]
})

# 비용 최적화가 중요한 경우
tsv_format = convert_to_tsv(df)

# 가독성과 유지보수가 중요한 경우
markdown_format = convert_to_markdown_kv(df)

실무 적용 가이드

시나리오별 포맷 선택

시나리오추천 포맷이유
대용량 배치 처리TSV비용과 속도 최우선
프로덕션 APIMarkdown KV가독성과 효율 균형
디버깅/개발Markdown KV사람이 읽기 쉬움
레거시 시스템 연동JSON표준 호환성
실시간 응답TSV최저 지연시간

비용 절감 계산 예시

# GPT-4 기준 (input $2.50/1M tokens)
COST_PER_1M_TOKENS = 2.50

# 일일 100만 건 처리 시
DAILY_QUERIES = 1_000_000

# 포맷별 토큰 사용량
tokens_xml = 16_852
tokens_tsv = 7_192

# 비용 계산
cost_xml = (tokens_xml * DAILY_QUERIES / 1_000_000) * COST_PER_1M_TOKENS
cost_tsv = (tokens_tsv * DAILY_QUERIES / 1_000_000) * COST_PER_1M_TOKENS

print(f"XML 사용 시: ${cost_xml:,.2f}/day") # $42.13/day
print(f"TSV 사용 시: ${cost_tsv:,.2f}/day") # $17.98/day
print(f"절감액: ${cost_xml - cost_tsv:,.2f}/day") # $24.15/day (57% 절감)
print(f"연간 절감: ${(cost_xml - cost_tsv) * 365:,.2f}") # $8,815/year

피해야 할 포맷과 이유

  1. STRING (자연어)

    • 정확도 75% → 25% 오류율은 실무에서 치명적
    • 구조 없어 파싱 불안정
  2. XML

    • 토큰 2.3배 낭비 (16,852 vs 7,192)
    • 태그 중복으로 비효율적
  3. DICT

    • 프로그래밍 문법 메타 문자로 토큰 과다 소모
    • JSON이 더 표준적이고 효율적

추가 고려사항

대용량 데이터 처리

1000개 이상 레코드 처리 시:

  • TSV/CSV는 100줄마다 헤더 반복 권장
  • Markdown은 청크 단위로 분할
  • JSON은 스트리밍 파싱 고려
def chunk_tsv_with_headers(df, chunk_size=100):
"""TSV를 헤더 반복하며 청크로 분할"""
chunks = []
for i in range(0, len(df), chunk_size):
chunk = df.iloc[i:i+chunk_size]
chunks.append(chunk.to_csv(sep='\t', index=False))
return chunks

다른 연구 결과와의 비교

improvingagents.com의 연구(1000개 직원 레코드, 8개 속성)에서도 유사한 결과:

  • Markdown Key-Value: 60.7% 정확도
  • INI: 55.7%
  • YAML: 54.5%
  • Markdown Table: 51.8%

차이점:

  • 본 실험은 재무 데이터로 정확도가 더 높음 (대부분 100%)
  • 도메인 특성상 테이블 구조가 더 명확해 LLM이 잘 이해

결론 및 제언

핵심 요약

  1. TSV가 이론적 최적: 토큰 57% 절감, 속도 3.8배 향상
  2. Markdown Key-Value가 실무 최적: 효율성과 실용성의 균형
  3. 간결함이 핵심: 메타 문자 최소화가 성능과 비용에 직결
  4. 구조화는 필수: STRING 같은 비구조화 포맷은 정확도 25% 하락

실무 적용 체크리스트

  • 비용이 최우선이면 TSV 사용
  • 팀 협업과 유지보수 고려 시 Markdown Key-Value
  • STRING, XML, DICT는 피하기
  • 대용량 데이터는 청크 분할 및 헤더 반복
  • 포맷 변경만으로 연간 수천~수만 달러 절감 가능

향후 연구 방향

  • 다양한 LLM 모델(GPT-4, Claude, Llama 등)에서 재현성 검증
  • 비정형 데이터(텍스트 포함 테이블)에서의 포맷 영향 분석
  • 멀티모달 환경(이미지 + 테이블)에서의 최적 포맷 연구

References