본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 2장: 평가 하네스 아키텍처와 핵심 개념
2026년 3월 4일·AI / ML·

2장: 평가 하네스 아키텍처와 핵심 개념

평가 하네스의 내부 구조를 해부합니다. 태스크 정의 시스템, 모델 백엔드 추상화, 실행 엔진의 배칭과 병렬화, 결과 집계와 리포팅까지 설계 패턴을 코드와 함께 분석합니다.

18분980자9개 섹션
aievaluationmlops
공유
ai-eval-harness2 / 10
12345678910
이전1장: AI 평가의 현재와 평가 하네스의 역할다음3장: lm-evaluation-harness 심층 분석

이 장에서 배울 내용

  • 평가 하네스의 계층형 아키텍처 구조
  • 태스크 정의 시스템과 프롬프트 템플릿 설계
  • 모델 백엔드 추상화 패턴
  • 실행 엔진의 배칭, 병렬화, 캐싱 전략
  • 결과 집계와 리포팅 파이프라인
  • 주요 평가 파이프라인 설계 패턴 3가지

계층형 아키텍처 개관

잘 설계된 평가 하네스는 명확한 계층(Layer) 분리를 통해 확장성과 유지보수성을 확보합니다. 각 계층은 독립적으로 변경할 수 있어야 하며, 상위 계층은 하위 계층의 구현 세부사항을 몰라도 동작해야 합니다.

이 아키텍처에서 핵심 원칙은 **관심사의 분리(Separation of Concerns)**입니다. 태스크 정의는 모델이 무엇인지 모르고, 모델 백엔드는 태스크가 무엇인지 모릅니다. 실행 엔진이 이 둘을 연결하는 중재자 역할을 합니다.


태스크 정의 시스템

태스크의 구조

태스크(Task)는 평가 하네스에서 가장 중요한 추상화입니다. 하나의 태스크는 "모델에게 무엇을 시킬 것인가"와 "결과를 어떻게 판정할 것인가"를 정의합니다.

task_definition.py
python
from dataclasses import dataclass, field
from typing import Callable, Any
 
 
@dataclass
class TaskConfig:
    """평가 태스크의 설정을 정의합니다."""
    
    task_name: str
    dataset_path: str
    dataset_name: str | None = None
    dataset_split: str = "test"
    
    # 프롬프트 구성
    prompt_template: str = ""
    system_prompt: str | None = None
    num_fewshot: int = 0
    fewshot_split: str = "validation"
    
    # 메트릭 구성
    metrics: list[str] = field(default_factory=lambda: ["accuracy"])
    
    # 출력 처리
    output_type: str = "generate"  # "generate" | "loglikelihood" | "loglikelihood_rolling"
    max_gen_tokens: int = 256
    stop_sequences: list[str] = field(default_factory=list)
 
 
@dataclass
class TaskInstance:
    """하나의 평가 인스턴스를 나타냅니다."""
    
    task_name: str
    instance_id: str
    prompt: str
    reference: str | list[str]
    metadata: dict[str, Any] = field(default_factory=dict)

프롬프트 템플릿의 중요성

프롬프트 형식은 평가 결과에 결정적인 영향을 미칩니다. 같은 모델, 같은 데이터셋이라도 프롬프트 템플릿에 따라 정확도가 10% 이상 차이 날 수 있습니다.

Warning

프롬프트 템플릿을 변경하면 이전 결과와의 비교가 무의미해집니다. 벤치마크 결과를 보고할 때는 반드시 사용한 프롬프트 템플릿을 함께 명시해야 합니다.

prompt_template.py
python
from string import Template
 
 
class PromptBuilder:
    """태스크 인스턴스로부터 모델 입력 프롬프트를 생성합니다."""
    
    def __init__(self, config: TaskConfig):
        self.config = config
        self.template = Template(config.prompt_template)
        self._fewshot_examples: list[dict] | None = None
    
    def build_prompt(self, instance: dict, fewshot_examples: list[dict] | None = None) -> str:
        """단일 인스턴스에 대한 전체 프롬프트를 조립합니다."""
        parts: list[str] = []
        
        # few-shot 예제 추가
        if fewshot_examples:
            for example in fewshot_examples:
                parts.append(self._format_example(example, include_answer=True))
            parts.append("")  # 구분자
        
        # 실제 질문 추가
        parts.append(self._format_example(instance, include_answer=False))
        
        return "\n".join(parts)
    
    def _format_example(self, data: dict, include_answer: bool) -> str:
        """단일 예제를 템플릿에 맞게 포맷합니다."""
        text = self.template.safe_substitute(data)
        if include_answer and "answer" in data:
            text += f" {data['answer']}"
        return text

평가 유형별 분류

평가 하네스가 지원하는 평가 유형은 크게 세 가지로 나뉩니다.

유형설명사용 사례
Generate모델이 텍스트를 생성하고 정답과 비교질의응답, 요약, 번역
Loglikelihood각 선택지의 로그 확률을 비교객관식 문제, 문장 완성
Loglikelihood Rolling텍스트 전체의 퍼플렉시티 계산언어 모델링 품질 측정

Generate 방식은 직관적이지만 출력 파싱이 필요합니다. Loglikelihood 방식은 더 안정적인 결과를 제공하지만 API 모델에서는 토큰 확률에 접근하기 어려운 경우가 많습니다.


모델 백엔드 추상화

추상 인터페이스 설계

평가 하네스의 핵심 설계 원칙 중 하나는 **모델에 대한 불가지론(Model Agnosticism)**입니다. 어떤 모델이든 동일한 인터페이스를 통해 평가할 수 있어야 합니다.

model_backend.py
python
from abc import ABC, abstractmethod
 
 
class ModelBackend(ABC):
    """모든 모델 백엔드가 구현해야 하는 인터페이스입니다."""
    
    @abstractmethod
    def generate(
        self,
        prompts: list[str],
        max_tokens: int = 256,
        temperature: float = 0.0,
        stop: list[str] | None = None,
    ) -> list[str]:
        """프롬프트 목록에 대한 생성 결과를 반환합니다."""
        ...
    
    @abstractmethod
    def loglikelihood(
        self,
        contexts: list[str],
        continuations: list[str],
    ) -> list[float]:
        """각 (context, continuation) 쌍의 로그 확률을 반환합니다."""
        ...
    
    @abstractmethod
    def model_info(self) -> dict:
        """모델 메타데이터를 반환합니다."""
        ...
 
 
class HuggingFaceBackend(ModelBackend):
    """HuggingFace Transformers 모델 백엔드."""
    
    def __init__(self, model_name: str, device: str = "auto", dtype: str = "float16"):
        from transformers import AutoModelForCausalLM, AutoTokenizer
        
        self.tokenizer = AutoTokenizer.from_pretrained(model_name)
        self.model = AutoModelForCausalLM.from_pretrained(
            model_name,
            device_map=device,
            torch_dtype=dtype,
        )
    
    def generate(self, prompts, max_tokens=256, temperature=0.0, stop=None):
        # 구현 생략 - 토크나이저와 모델을 사용한 생성
        ...
    
    def loglikelihood(self, contexts, continuations):
        # 구현 생략 - 토큰별 로그 확률 계산
        ...
    
    def model_info(self):
        return {"type": "huggingface", "name": self.model.config.name_or_path}
 
 
class OpenAIBackend(ModelBackend):
    """OpenAI API 모델 백엔드."""
    
    def __init__(self, model_name: str = "gpt-4o", api_key: str | None = None):
        from openai import OpenAI
        self.client = OpenAI(api_key=api_key)
        self.model_name = model_name
    
    def generate(self, prompts, max_tokens=256, temperature=0.0, stop=None):
        results = []
        for prompt in prompts:
            response = self.client.chat.completions.create(
                model=self.model_name,
                messages=[{"role": "user", "content": prompt}],
                max_tokens=max_tokens,
                temperature=temperature,
                stop=stop,
            )
            results.append(response.choices[0].message.content or "")
        return results
    
    def loglikelihood(self, contexts, continuations):
        raise NotImplementedError("OpenAI API는 토큰 로그 확률 직접 접근을 제한합니다.")
    
    def model_info(self):
        return {"type": "openai", "name": self.model_name}
Info

실제 프레임워크에서는 vLLM, TGI(Text Generation Inference), Ollama, Anthropic API 등 다양한 백엔드를 추가로 지원합니다. 각 백엔드는 동일한 추상 인터페이스를 구현하므로, 태스크 정의를 변경하지 않고도 모델을 교체할 수 있습니다.


실행 엔진의 설계

배칭 전략

대규모 평가에서 개별 요청을 하나씩 처리하는 것은 비효율적입니다. 실행 엔진은 여러 요청을 하나의 배치(Batch)로 묶어 처리하여 처리량(Throughput)을 극대화합니다.

execution_engine.py
python
import asyncio
from dataclasses import dataclass
 
 
@dataclass
class EvalRequest:
    instance_id: str
    prompt: str
    task_name: str
 
 
@dataclass
class EvalResult:
    instance_id: str
    output: str
    metadata: dict
 
 
class ExecutionEngine:
    """평가 요청의 배칭, 병렬 실행, 캐싱을 관리합니다."""
    
    def __init__(
        self,
        backend: ModelBackend,
        batch_size: int = 32,
        max_concurrent: int = 8,
        cache_dir: str | None = None,
    ):
        self.backend = backend
        self.batch_size = batch_size
        self.max_concurrent = max_concurrent
        self.cache = EvalCache(cache_dir) if cache_dir else None
        self._semaphore = asyncio.Semaphore(max_concurrent)
    
    async def execute_all(self, requests: list[EvalRequest]) -> list[EvalResult]:
        """모든 평가 요청을 배치 단위로 실행합니다."""
        # 캐시 히트 확인
        cached, uncached = self._partition_cached(requests)
        
        # 미처리 요청을 배치로 분할
        batches = self._create_batches(uncached, self.batch_size)
        
        # 배치를 병렬로 실행
        results = list(cached)
        batch_results = await asyncio.gather(
            *(self._execute_batch(batch) for batch in batches)
        )
        
        for batch_result in batch_results:
            results.extend(batch_result)
            if self.cache:
                for result in batch_result:
                    self.cache.store(result)
        
        return results
    
    async def _execute_batch(self, batch: list[EvalRequest]) -> list[EvalResult]:
        """단일 배치를 실행합니다."""
        async with self._semaphore:
            prompts = [req.prompt for req in batch]
            outputs = self.backend.generate(prompts)
            return [
                EvalResult(
                    instance_id=req.instance_id,
                    output=output,
                    metadata={"task": req.task_name},
                )
                for req, output in zip(batch, outputs)
            ]
    
    @staticmethod
    def _create_batches(items: list, size: int) -> list[list]:
        return [items[i:i + size] for i in range(0, len(items), size)]

캐싱의 중요성

평가 실행은 비용이 큽니다. API 모델의 경우 토큰 비용이 발생하고, 로컬 모델의 경우 GPU 시간이 소요됩니다. 캐싱은 동일한 입력에 대한 중복 추론을 방지합니다.

Tip

캐시 키(Cache Key)는 모델 이름, 프롬프트 전문, 생성 파라미터(temperature, max_tokens 등)의 해시로 구성하는 것이 일반적입니다. temperature가 0이 아닌 경우 결과가 비결정적이므로 캐싱에 주의해야 합니다.


결과 집계와 리포팅

메트릭 계산 파이프라인

평가 결과의 집계는 단순한 평균 계산 이상의 과정을 포함합니다.

metrics.py
python
import numpy as np
from scipy import stats
 
 
class MetricsAggregator:
    """개별 인스턴스 결과를 태스크 수준 메트릭으로 집계합니다."""
    
    METRIC_FUNCTIONS = {
        "accuracy": lambda preds, refs: np.mean([p == r for p, r in zip(preds, refs)]),
        "exact_match": lambda preds, refs: np.mean([p.strip() == r.strip() for p, r in zip(preds, refs)]),
    }
    
    def aggregate(
        self,
        predictions: list[str],
        references: list[str],
        metric_names: list[str],
    ) -> dict[str, float]:
        """지정된 메트릭들을 계산합니다."""
        results = {}
        for name in metric_names:
            func = self.METRIC_FUNCTIONS.get(name)
            if func is None:
                raise ValueError(f"지원하지 않는 메트릭: {name}")
            results[name] = func(predictions, references)
        return results
    
    def compute_confidence_interval(
        self,
        scores: list[float],
        confidence: float = 0.95,
        method: str = "bootstrap",
    ) -> tuple[float, float]:
        """신뢰 구간을 계산합니다."""
        if method == "bootstrap":
            n_bootstrap = 1000
            bootstrap_means = []
            for _ in range(n_bootstrap):
                sample = np.random.choice(scores, size=len(scores), replace=True)
                bootstrap_means.append(np.mean(sample))
            lower = np.percentile(bootstrap_means, (1 - confidence) / 2 * 100)
            upper = np.percentile(bootstrap_means, (1 + confidence) / 2 * 100)
            return (lower, upper)
        
        # Wilson score interval
        n = len(scores)
        mean = np.mean(scores)
        z = stats.norm.ppf((1 + confidence) / 2)
        se = z * np.sqrt(mean * (1 - mean) / n)
        return (mean - se, mean + se)

리포트 구조

평가 결과 리포트는 다음과 같은 계층 구조를 가집니다.

EvaluationReport
  +-- model_info: 모델 메타데이터
  +-- run_config: 실행 설정
  +-- timestamp: 실행 시각
  +-- task_results[]
        +-- task_name: 태스크 이름
        +-- metrics: {metric_name: value}
        +-- confidence_intervals: {metric_name: (lower, upper)}
        +-- num_instances: 평가 인스턴스 수
        +-- instance_results[]: 개별 인스턴스 상세 결과

평가 파이프라인 설계 패턴

실전에서 자주 사용되는 세 가지 설계 패턴을 살펴보겠습니다.

패턴 1: 순차 파이프라인(Sequential Pipeline)

가장 단순한 형태로, 태스크를 하나씩 순서대로 실행합니다. 구현이 간단하고 디버깅이 쉬운 반면, 전체 실행 시간이 태스크 수에 비례하여 증가합니다.

sequential_pipeline.py
python
def run_sequential(tasks: list[TaskConfig], backend: ModelBackend) -> dict:
    results = {}
    for task in tasks:
        dataset = load_dataset(task.dataset_path, task.dataset_name, split=task.dataset_split)
        prompts = build_prompts(task, dataset)
        outputs = backend.generate(prompts)
        metrics = compute_metrics(task.metrics, outputs, dataset["answer"])
        results[task.task_name] = metrics
    return results

패턴 2: 팬아웃/팬인(Fan-out/Fan-in)

여러 태스크를 동시에 실행하고 결과를 모아서 집계합니다. 독립적인 태스크 간의 병렬성을 극대화합니다.

패턴 3: 이벤트 기반(Event-Driven)

각 평가 단계가 이벤트를 발생시키고, 후속 처리기가 이벤트에 반응합니다. 로깅, 모니터링, 조기 종료 등의 기능을 유연하게 추가할 수 있습니다.

Info

대부분의 상용 평가 하네스는 팬아웃/팬인 패턴을 기본으로 하되, 이벤트 기반 패턴의 요소를 결합하여 사용합니다. 3장에서 살펴볼 lm-evaluation-harness가 대표적인 예시입니다.


핵심 요약

  • 평가 하네스는 설정 계층, 코어 계층, 출력 계층의 3계층 아키텍처로 구성됩니다.
  • 태스크 정의 시스템은 데이터셋, 프롬프트 템플릿, 메트릭을 하나의 단위로 묶는 핵심 추상화입니다.
  • 모델 백엔드 추상화를 통해 다양한 모델을 통일된 인터페이스로 평가할 수 있습니다.
  • 실행 엔진의 배칭, 병렬화, 캐싱은 대규모 평가의 효율성을 결정짓는 핵심 요소입니다.
  • 결과 집계 시 신뢰 구간과 통계적 유의성 검증이 필수적입니다.

다음 장 예고

3장에서는 이러한 아키텍처 원칙이 실제로 구현된 대표적인 프레임워크인 lm-evaluation-harness를 심층 분석합니다. 200개 이상의 태스크와 25개 이상의 모델 백엔드를 지원하는 이 프레임워크의 내부 구조, 설치 방법, 커스텀 태스크 작성법까지 실전 중심으로 살펴봅니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#ai#evaluation#mlops

관련 글

AI / ML

3장: lm-evaluation-harness 심층 분석

EleutherAI의 lm-evaluation-harness를 심층 분석합니다. 200개 이상의 태스크, 25개 이상의 모델 백엔드, HuggingFace 리더보드 백엔드로서의 역할, 설치부터 커스텀 태스크 작성까지 실전 가이드를 제공합니다.

2026년 3월 6일·14분
AI / ML

1장: AI 평가의 현재와 평가 하네스의 역할

300개 이상의 모델과 50개 이상의 벤치마크가 공존하는 시대, AI 평가 하네스가 왜 필요한지 그 정의와 핵심 구성요소, 평가 생태계 전체 지도를 살펴봅니다.

2026년 3월 2일·17분
AI / ML

4장: HELM - 종합적 모델 평가 프레임워크

Stanford CRFM의 HELM을 분석합니다. 7가지 메트릭 차원, 16가지 핵심 시나리오, HELM Lite와 MedHELM 변형, 실행 방법과 결과 분석까지 종합적 평가 접근법을 탐구합니다.

2026년 3월 8일·16분
이전 글1장: AI 평가의 현재와 평가 하네스의 역할
다음 글3장: lm-evaluation-harness 심층 분석

댓글

목차

약 18분 남음
  • 이 장에서 배울 내용
  • 계층형 아키텍처 개관
  • 태스크 정의 시스템
    • 태스크의 구조
    • 프롬프트 템플릿의 중요성
    • 평가 유형별 분류
  • 모델 백엔드 추상화
    • 추상 인터페이스 설계
  • 실행 엔진의 설계
    • 배칭 전략
    • 캐싱의 중요성
  • 결과 집계와 리포팅
    • 메트릭 계산 파이프라인
    • 리포트 구조
  • 평가 파이프라인 설계 패턴
    • 패턴 1: 순차 파이프라인(Sequential Pipeline)
    • 패턴 2: 팬아웃/팬인(Fan-out/Fan-in)
    • 패턴 3: 이벤트 기반(Event-Driven)
  • 핵심 요약
  • 다음 장 예고