AI 엔지니어링 — 1편: 파운데이션 모델과 평가
『AI 엔지니어링』을 읽으며 메모한 용어를 바탕으로AI를 활용하여 프론트엔드의 관점으로 정리합니다.

『AI 엔지니어링』 : https://product.kyobobook.co.kr/detail/S000217939673
들어가며
최근 AI의 빠른 발전 속에서, 프론트엔드 개발자의 역할이 어디까지 확장될 수 있을지에 대한 얘기가 많이 나오고 있습니다. 풀스택 개발자로 이동하는 흐름도 종종 보입니다. 저도 동료 개발자들과 모이면 항상 이런 얘기를 나누고 있습니다 :)
실제 업무에서 저 역시 Claude Code, Cursor 등을 활용해서 개발을 하고, AI 면접 등 AI 기반 서비스를 만들어보기도 했습니다. 그러다 문득 “AI 엔지니어는 구체적으로 어떤 역할을 하는 직업일까? 내가 해 온 것도 AI 엔지니어링이라 할 수 있을까?”라는 궁금증이 생겨, 이참에 관련 책을 읽으며 공부해 보기로 했습니다.
저처럼 비슷한 고민을 하고 계시다면 이 책을 한번 읽어보시길 추천합니다. 앞으로의 커리어 방향도 함께 고민해볼 수 있는 좋은 기회가 될 것입니다. 자주 사용하는 AI 용어들에 대한 이해를 바탕으로, 올해부터는 프론트엔드 개발자를 “AI 프론트엔드 엔지니어”라고 불러보려 합니다.
1. AI 엔지니어와 파운데이션 모델
언어 모델 (Language Model)
언어 모델은 토큰 시퀀스에 확률을 부여하는 모델입니다. “오늘 날씨가” 다음에 “좋다”가 올 확률과 “바나나”가 올 확률을 계산합니다. 그 확률에 따라 다음 토큰을 하나 고르고, 고른 토큰을 다시 입력에 붙여 그 다음 토큰을 고르는 식으로 문장을 이어갑니다.
여기서 토큰은 모델이 다루는 텍스트의 최소 단위입니다. 단어보다 작고 글자보다 큰 경우가 많습니다. 영어 “unbelievable”은 un believ able 세 토큰으로 쪼개질 수 있고, 한국어는 영어보다 토큰이 더 잘게 나뉘는 편입니다. 텍스트를 토큰으로 바꾸는 규칙이 토크나이저이고 모델마다 다릅니다. API 요금이 토큰 단위로 계산되는 이유가 여기에 있습니다.
언어 모델에는 두 가지 부류가 있습니다.
| 부류 | 학습 방식 | 대표 모델 | 잘하는 일 |
|---|---|---|---|
| 마스크 언어 모델 | 문장 중간의 빈칸을 앞뒤 문맥으로 채움 | BERT | 분류, 감성 분석 |
| 자기회귀 언어 모델 | 앞 토큰만 보고 다음 토큰을 예측 | GPT, Claude, Llama | 텍스트 생성 |
우리가 API로 부르는 모델은 거의 전부 자기회귀 모델입니다. 책에서 “언어 모델”이라고 하면 자기회귀 모델을 가리킨다고 봐도 됩니다.
언어 모델이 이렇게 커질 수 있었던 이유는 자기 지도 학습(self-supervision) 덕분입니다. 텍스트 자체가 정답이 됩니다. “오늘 날씨가 좋다”라는 문장이 있으면 “오늘 날씨가” → “좋다”라는 학습 데이터가 저절로 생깁니다. 사람이 라벨을 달 필요가 없어서 인터넷의 텍스트를 통째로 학습에 쓸 수 있습니다.
LLM (대규모 언어 모델)
언어 모델 앞에 “대규모”가 붙은 것입니다. 파라미터 수와 학습 데이터 양이 커진 언어 모델을 LLM이라고 부릅니다. “대규모”의 기준은 계속 이동합니다. 2019년의 GPT-2(15억 파라미터)는 당시 대규모였고 지금은 소형 모델로 분류됩니다.
규모가 커지면서 능력이 달라졌다는 점이 핵심입니다. 단순히 문장을 잘 이어 쓰는 것을 넘어 번역, 요약, 코드 작성처럼 따로 학습시키지 않은 일을 프롬프트만으로 해내기 시작했습니다.
파운데이션 모델 (Foundation Model)
파운데이션 모델은 스탠퍼드 CRFM이 2021년 논문에서 제안한 용어로, 대규모 데이터로 학습되어 다양한 하위 작업에 적응(adapt)시킬 수 있는 모델을 뜻합니다. 하나의 모델이 여러 애플리케이션의 기반(foundation)이 된다는 의미입니다.
LLM과 겹치지만 두 가지 면에서 더 넓습니다.
첫째, 텍스트에 한정되지 않습니다. 이미지, 오디오, 영상까지 다루는 모델도 포함합니다. GPT-4o, Claude, Gemini는 이미지를 입력으로 받기 때문에 엄밀히 말하면 LLM보다 파운데이션 모델(또는 대규모 멀티모달 모델, LMM)이라는 표현이 맞습니다.
둘째, 용도가 아니라 역할을 강조하는 말입니다. “언어 모델”은 무엇을 하는지를 말하고 “파운데이션 모델”은 어떻게 쓰이는지를 말합니다.
| 용어 | 뜻 | 상황 |
|---|---|---|
| 언어 모델 | 토큰 시퀀스의 확률을 계산하는 모델 | 개념을 설명할 때 |
| LLM | 규모가 큰 언어 모델 | 텍스트 입출력 모델을 가리킬 때 |
| LMM | 이미지 등 여러 모달리티를 다루는 대규모 모델 | 이미지 입력을 강조할 때 |
| 파운데이션 모델 | 여러 작업의 기반이 되는 범용 대규모 모델 | 모델을 “가져다 쓰는” 관점일 때 |
사후 학습 (Post-training)
파운데이션 모델을 처음 학습(사전 학습, pre-training)하면 문장을 잘 잇는 모델이 나오지만 대화가 되지는 않습니다. “한국의 수도는?”이라고 물으면 “한국의 수도는? 일본의 수도는? 중국의 수도는?”처럼 퀴즈 목록을 이어 쓸 수도 있습니다. 이 모델을 우리가 쓰는 채팅 모델로 바꾸는 과정이 사후 학습입니다.
- 지도 파인튜닝: 사람이 작성한 (질문, 좋은 답변) 쌍으로 학습시켜 대화 형식을 가르칩니다.
- 선호도 파인튜닝: 두 답변 중 사람이 더 선호한 쪽을 고르게 학습시킵니다. RLHF, DPO가 여기에 속합니다.
AI 엔지니어링
AI 엔지니어링은 파운데이션 모델을 활용해 애플리케이션을 만드는 과정입니다. 예전에는 AI 앱을 만들려면 모델부터 학습시켜야 했습니다. 지금은 모델이 이미 있습니다. 그래서 일의 중심이 ‘모델 개발’에서 ‘모델 적응(adaptation)‘으로 옮겨갔습니다. 프롬프트 엔지니어링, RAG, 파인튜닝은 모두 적응 기법입니다.
[애플리케이션 개발] 프롬프트, 컨텍스트 구성, 평가, 인터페이스 ← AI 엔지니어의 주 무대
[모델 개발] 아키텍처, 학습, 파인튜닝, 데이터셋, 추론 최적화
[인프라] 서빙, 모니터링, 컴퓨팅 자원아래는 전통적인 ML 엔지니어링과 AI 엔지니어링의 프로세스 차이를 단계별로 비교한 표입니다.
| 전통적 ML 엔지니어링 | AI 엔지니어링 | |
|---|---|---|
| 1단계 | 데이터 수집 | 제품 출시 |
| 2단계 | 모델 학습 | 사용자 데이터 수집 |
| 3단계 | 제품 개발 및 출시 | 수집한 데이터로 모델 개선 |
모델이 이미 준비되어 있으므로, AI 엔지니어링에서는 제품을 먼저 만들어 시장에 내놓고, 이후에 실사용 데이터를 바탕으로 모델을 점진적으로 개선하는 방식이 가능합니다.
AI 엔지니어링 도구로서의 JS/TS 생태계
AI 엔지니어링은 대부분 파이썬으로 개발되어 왔습니다. 그런데 앞 절에서 본 것처럼 AI 엔지니어링은 제품 출시부터 시작하는 경우가 많습니다. 제품을 먼저 내려면 사용자가 쓸 화면이 필요하고, 그러다 보니 프론트엔드와 관련된 도구들이 많이 나왔습니다.
| 도구 | 역할 |
|---|---|
@anthropic-ai/sdk, openai | 모델 제공자의 공식 TS SDK |
Vercel AI SDK | 여러 제공자를 하나의 인터페이스로, 스트리밍·도구 호출·React 훅 |
LangChain.js, LlamaIndex.TS | RAG·에이전트 파이프라인 구성 |
Transformers.js | 브라우저나 Node에서 소형 모델을 직접 실행 |
Chat SDK (Vercel) | Next.js 챗봇 앱 시작 템플릿 |
@anthropic-ai/claude-agent-sdk | Claude Code의 에이전트 루프·내장 도구를 라이브러리로 사용 |
@anthropic-ai/sdk를 활용한 코드 예시는 다음과 같습니다.
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic();
const response = await client.messages.create({
model: 'claude-opus-5',
max_tokens: 1024,
messages: [{ role: 'user', content: '파운데이션 모델과 LLM의 차이를 한 문장으로' }],
});
for (const block of response.content) {
if (block.type === 'text') console.log(block.text);
}2. 아키텍처 - 모델의 생성
seq2seq (Sequence-to-Sequence)
2014년에 등장한 구조로, 입력 시퀀스를 받아 출력 시퀀스를 내는 모델의 통칭입니다. 번역이 대표적인 예입니다. 인코더가 입력 문장을 읽어 하나의 벡터로 압축하고 디코더가 그 벡터로부터 출력 문장을 생성합니다. 초기 seq2seq는 RNN(순환 신경망)으로 만들어졌습니다.
여기에는 두 가지 한계가 있었습니다. 입력 전체를 벡터 하나에 압축하다 보니 문장이 길어지면 앞부분 정보가 사라졌습니다. 그리고 토큰을 순서대로 하나씩 처리해야 해서 병렬화가 어려웠고, 학습 규모를 키우기 힘들었습니다.
어텐션 메커니즘 (Attention Mechanism)
첫 번째 한계를 풀기 위해 나온 아이디어입니다. 디코더가 출력 토큰을 하나 만들 때마다 입력 토큰 전체를 다시 보고 어디에 집중할지 가중치를 계산합니다. “I love you”를 번역해 “사랑”이라는 토큰을 만드는 순간에는 “love”에 높은 가중치를 두는 식입니다. 압축된 벡터 하나에 의존하지 않으니 긴 입력에서도 정보가 살아남습니다.
트랜스포머 (Transformer)
2017년 논문 「Attention Is All You Need」에서 제안된 구조입니다. 제목 그대로 RNN을 걷어내고 어텐션만으로 시퀀스를 처리합니다. 순차 처리가 사라지자 입력 토큰을 한 번에 병렬로 계산할 수 있게 됐고, 그 덕분에 학습 규모를 수천 배 키울 수 있었습니다. 오늘날 LLM은 거의 전부 트랜스포머 계열입니다.
원래 트랜스포머는 인코더-디코더 구조였지만 GPT 계열은 디코더만 사용합니다. 우리가 쓰는 대부분의 생성 모델이 디코더 전용 트랜스포머입니다.
쿼리·키·값 벡터 (Query, Key, Value)
어텐션의 내부 동작입니다. 검색에 비유하면 이해가 빠릅니다.
- 쿼리(Q): 지금 만들고 있는 토큰이 “무엇을 찾고 있는가”를 담은 벡터.
- 키(K): 각 입력 토큰이 “나는 이런 정보를 갖고 있다”라고 내놓는 색인 벡터.
- 값(V): 각 입력 토큰이 실제로 전달할 내용 벡터.
쿼리와 모든 키를 내적(dot product)해 유사도 점수를 구하고, 그 점수를 softmax로 확률처럼 정규화한 뒤, 그 비율로 값 벡터를 가중합합니다.
function attention(q: number[], keys: number[][], values: number[][]) {
const scale = Math.sqrt(q.length);
const scores = keys.map((k) => dot(q, k) / scale); // Q·K / √d
const weights = softmax(scores); // 어디에 얼마나 집중할지
return values[0].map((_, i) => // V의 가중합
values.reduce((sum, v, j) => sum + weights[j] * v[i], 0),
);
}
const dot = (a: number[], b: number[]) => a.reduce((s, x, i) => s + x * b[i], 0);softmax는 다음 절에서 다룹니다. 실제 모델은 이 연산을 여러 개의 “헤드”로 병렬 수행하고(멀티 헤드 어텐션), 층을 수십 개 쌓습니다.
위 설명은 인코더-디코더 구조의 어텐션(디코더가 인코더 출력을 참조하는 크로스 어텐션)에 가깝습니다. 우리가 쓰는 디코더 전용 모델은 셀프 어텐션을 씁니다. 참조 대상이 “입력 토큰”이 아니라 지금까지의 모든 토큰, 즉 프롬프트와 모델이 방금 생성한 토큰을 이어 붙인 하나의 시퀀스입니다. 그리고 모델은 그 둘을 구분하지 않습니다. 이 사실이 뒤에 나올 환각의 첫 번째 가설(자기 기만)과 그대로 이어집니다.
프론트엔드 개발자가 알면 좋은 부분이 있습니다. 추론할 때 모델은 이미 처리한 토큰의 K와 V를 메모리에 저장해 둡니다(KV 캐시). 컨텍스트가 길어질수록 이 캐시가 커지고, 그래서 긴 컨텍스트는 비용도 지연 시간도 함께 늘어납니다.
모델 크기와 파라미터
파라미터는 학습으로 조정되는 가중치의 개수입니다. 모델 이름 뒤에 붙는 7B, 13B, 70B는 파라미터 수를 10억(billion) 단위로 적은 것입니다.
파라미터 수를 알면 모델을 띄우는 데 필요한 메모리를 어림할 수 있습니다. FP16(16비트 부동소수점)이면 파라미터 하나가 2바이트입니다.
| 모델 | 파라미터 | FP16 메모리 (추론용, 대략) |
|---|---|---|
| Llama 3 8B | 80억 | 약 16GB |
| Llama 2 13B | 130억 | 약 26GB |
| Llama 3 70B | 700억 | 약 140GB |
하지만 크기가 곧 성능은 아닙니다. 같은 크기여도 학습에 쓴 토큰 수와 데이터 품질에 따라 성능이 크게 다릅니다. 2022년 Chinchilla 논문은 주어진 연산 예산에서 모델 크기와 학습 토큰 수 사이에 최적 비율이 있다는 것을 보였고, 그 뒤로 “작은 모델을 더 많은 데이터로 오래 학습시키는” 흐름이 생겼습니다.
그리고 희소 모델은 표기된 파라미터가 전부 활성화되지 않습니다. Mixtral 8x7B 같은 MoE(Mixture of Experts) 모델은 총 파라미터는 약 470억이지만 토큰 하나를 처리할 때는 그중 일부(약 130억)만 사용합니다. 메모리는 총량이 필요하고 연산량은 활성 파라미터에 비례합니다.
3. 샘플링 — 같은 질문에 다른 답이 나오는 이유
로짓과 softmax
모델의 마지막 층은 어휘(vocabulary)의 모든 토큰에 대해 점수를 하나씩 내놓습니다. 이 원시 점수를 로짓(logit)이라고 합니다. 어휘 크기가 10만이면 로짓도 10만 개입니다. 로짓은 확률이 아닙니다. 음수일 수도 있고 합이 1도 아닙니다.
softmax는 이 로짓을 확률 분포로 바꾸는 함수입니다. 각 값을 지수 함수에 넣어 전부 양수로 만들고 총합으로 나눕니다.
function softmax(logits: number[]): number[] {
const max = Math.max(...logits); // 오버플로 방지용
const exps = logits.map((l) => Math.exp(l - max));
const sum = exps.reduce((a, b) => a + b, 0);
return exps.map((e) => e / sum);
}
softmax([2.0, 1.0, 0.1]);
// → [0.659, 0.242, 0.099] 합이 1인 확률 분포여기서 나온 확률로 다음 토큰을 뽑는(sampling) 방식이 출력의 성격을 결정합니다. 가장 확률이 높은 토큰만 고르면(greedy) 매번 같은 답이 나오고, 확률에 비례해 무작위로 고르면 매번 다른 답이 나옵니다.
온도 (Temperature)
로짓을 softmax에 넣기 전에 온도 T로 나누는 것입니다. 물리학의 온도 개념을 차용했습니다. 온도가 높으면 입자가 무질서하게 움직이듯 출력도 다양해집니다.
function softmaxWithTemperature(logits: number[], temperature: number) {
return softmax(logits.map((l) => l / temperature));
}
const logits = [2.0, 1.0, 0.1];
softmaxWithTemperature(logits, 0.5); // → [0.864, 0.117, 0.019] 1등이 더 뾰족
softmaxWithTemperature(logits, 1.0); // → [0.659, 0.242, 0.099] 원본
softmaxWithTemperature(logits, 2.0); // → [0.502, 0.304, 0.194] 분포가 평평- T < 1: 확률 높은 토큰이 더 두드러집니다. 분류, 추출, 코드 생성처럼 정답이 정해진 일에 맞습니다.
- T = 0: 항상 최댓값만 고릅니다. 대부분의 API에서 greedy와 같은 의미입니다.
- T > 1: 확률이 낮던 토큰도 자주 뽑힙니다. 브레인스토밍, 창작에 맞습니다.
API에서는 파라미터 하나입니다.
const response = await client.messages.create({
model: 'claude-haiku-4-5',
max_tokens: 256,
temperature: 0, // 분류 작업이라 결정적으로
messages: [{ role: 'user', content: `다음 문의를 [환불, 배송, 기타] 중 하나로 분류: "${text}"` }],
});최신 Claude 모델은 temperature, top_p, top_k를 받지 않습니다.
| 샘플링 파라미터 | 모델 |
|---|---|
| 받지 않음 (400) | Opus 4.7 / 4.8 / 5, Sonnet 5, Fable 5 계열 |
| 아직 받음 | Opus 4.6, Sonnet 4.6, Haiku 4.5 및 그 이전 |
최신 모델에서는 대신 output_config.effort(low~max)와 적응형 추론(thinking: { type: 'adaptive' })으로 “얼마나 깊이 생각할지”를 조절합니다.
top-k
확률 상위 k개 토큰만 남기고 나머지는 버린 뒤 남은 것끼리 재정규화해서 뽑는 방식입니다. k = 50이면 어휘 10만 개 중 상위 50개만 후보가 됩니다. 확률이 아주 낮은 엉뚱한 토큰이 뽑히는 것을 막아줍니다.
단점은 k가 고정이라는 점입니다. 다음 토큰이 거의 확정적인 상황(“대한민국의 수도는” → “서울”)에서도 50개를 후보로 두고, 후보가 정말 많은 상황에서도 50개로 잘라버립니다.
top-p (Nucleus Sampling)
top-k의 단점을 고친 방식입니다. 확률이 높은 순으로 누적하다가 누적 확률이 p를 넘는 순간까지의 토큰만 후보로 씁니다. p = 0.9면 상위 토큰들의 확률을 더해 90%가 되는 지점에서 자릅니다.
function topP(probs: number[], p: number): number[] {
const sorted = probs.map((prob, i) => ({ prob, i })).sort((a, b) => b.prob - a.prob);
const kept: typeof sorted = [];
let cumulative = 0;
for (const item of sorted) {
kept.push(item);
cumulative += item.prob;
if (cumulative >= p) break;
}
const total = kept.reduce((s, x) => s + x.prob, 0);
const result = new Array(probs.length).fill(0);
for (const { prob, i } of kept) result[i] = prob / total; // 재정규화
return result;
}분포가 뾰족하면(한 토큰이 0.95) 후보가 1개가 되고, 평평하면 후보가 수십 개가 됩니다. 상황에 따라 후보 수가 알아서 조절됩니다. 그래서 대부분의 API가 top-k보다 top-p를 기본 옵션으로 노출합니다. 보통 온도와 top-p 중 하나만 조절하라고 권장합니다. 둘을 동시에 만지면 결과를 해석하기 어려워지기 때문입니다.
환각이 발생하는 이유
환각(hallucination) 은 모델이 사실이 아닌 내용을 사실처럼 만들어내는 현상입니다. 환각이 발생하는 이유에는 2가지 가설이 있습니다.
가설 1. 자기 기만(self-delusion). 모델은 자신이 생성한 토큰과 사용자가 준 입력을 구분하지 못합니다. 앞에서 한 번 틀린 토큰을 만들면 그것을 “주어진 사실”로 받아들이고 그 위에 문장을 이어갑니다. 한 번 어긋난 문장이 눈덩이처럼 커지는 이유입니다(DeepMind, Ortega et al., 2021).
가설 2. 모델 지식과 라벨러 지식의 불일치. 사후 학습(SFT) 단계에서 사람이 작성한 답변으로 모델을 학습시킵니다. 그런데 그 답변에 모델이 모르는 사실이 들어 있으면, 모델은 “모르는 것도 아는 것처럼 답하기”를 배우게 됩니다(OpenAI, John Schulman, 2023). 이 가설이 맞다면 “모르면 모른다고 답하라”는 데이터를 넣어 사후 학습하는 것으로 완화할 수 있습니다.
모델의 목표는 그럴듯한 다음 토큰을 확률적으로 고르는 것이고, 그 과정에 사실을 검증하는 단계가 없습니다. 그래서 일반적으로 모델의 출력은 사실이 중요한 화면에서는 출처를 함께 보여주거나(2편의 RAG) 검증 단계를 UI에 넣어야 합니다.
구조화된 출력
모델이 자유 텍스트 대신 정해진 스키마의 JSON을 반환하게 하는 것입니다. @anthropic-ai/sdk, vercel/ai 등의 도구에서 지원하고 있습니다.
import { z } from 'zod';
import { zodOutputFormat } from '@anthropic-ai/sdk/helpers/zod';
const Ticket = z.object({
category: z.enum(['refund', 'shipping', 'other']),
urgency: z.number().min(1).max(5),
summary: z.string(),
});
const response = await client.messages.parse({
model: 'claude-opus-5',
max_tokens: 1024,
messages: [{ role: 'user', content: `다음 문의를 분류해: ${text}` }],
output_config: { format: zodOutputFormat(Ticket) },
});
response.parsed_output; // Ticket 타입, 파싱 실패 시 null4. 평가의 어려움과 측정
평가의 어려움
파운데이션 모델의 가장 어려운 부분은 평가입니다. 그 이유는,
- 정답이 하나가 아닙니다. 분류 모델은 정답 라벨과 비교하면 끝이지만, “이 글을 요약해줘”의 정답은 무수히 많습니다.
- 모델이 사람보다 잘하는 영역이 생겼습니다. 수학 증명이나 전문 코드는 평가자가 정답을 모를 수 있습니다.
- 벤치마크가 오염됩니다. 공개 벤치마크 문제가 학습 데이터에 섞여 들어가면 점수가 실력을 반영하지 않습니다.
- 블랙박스입니다. 상용 모델은 아키텍처도 학습 데이터도 모르는 채로 출력만 보고 판단해야 합니다.
그래서 평가는 “정확도 몇 %“로 끝나지 않고 여러 방법을 조합하는 작업이 됩니다.
엔트로피 (Entropy)
정보 이론에서 온 개념으로, 어떤 분포가 얼마나 예측 불가능한지를 비트 단위로 나타냅니다. 공정한 동전은 1비트입니다. 앞면과 뒷면이 반반이라 결과를 알려면 1비트의 정보가 필요합니다. 항상 앞면만 나오는 동전은 0비트입니다. 결과를 이미 알기 때문입니다.
// H(P) = -Σ p(x) · log₂ p(x)
const entropy = (probs: number[]) =>
-probs.filter((p) => p > 0).reduce((s, p) => s + p * Math.log2(p), 0);
entropy([0.5, 0.5]); // 1.0 공정한 동전
entropy([1.0, 0.0]); // 0.0 결과가 확정
entropy([0.25, 0.25, 0.25, 0.25]); // 2.0 4면 주사위교차 엔트로피 (Cross-Entropy)
실제 분포 P를 모델의 분포 Q로 인코딩할 때 필요한 평균 비트 수입니다. 언어 모델 학습에서 손실 함수로 쓰는 것이 바로 이것입니다. 모델이 정답 토큰에 높은 확률을 줬으면 교차 엔트로피가 낮고, 낮은 확률을 줬으면 높습니다.
수식으로는 H(P, Q) = H(P) + KL(P ‖ Q)인데, 말로 풀면 “데이터 자체의 엔트로피 + 모델이 데이터와 얼마나 다른지”입니다. 모델이 완벽하면 KL 항이 0이 되어 교차 엔트로피가 데이터의 엔트로피와 같아집니다. 그 아래로는 내려갈 수 없습니다. 이게 언어 모델 손실의 하한선입니다.
문자당 비트(BPC)와 바이트당 비트(BPB)
교차 엔트로피는 토큰당 비트로 계산됩니다. 그런데 토크나이저가 다르면 같은 문장이 다른 개수의 토큰으로 쪼개지기 때문에, 토큰당 교차 엔트로피로는 서로 다른 모델을 비교할 수 없습니다.
그래서 문자 수나 바이트 수로 정규화합니다. 문자당 비트(bits-per-character)는 문자 하나를 표현하는 데 필요한 비트, 바이트당 비트(bits-per-byte)는 바이트 하나당 비트입니다. 한글은 UTF-8에서 3바이트라 BPC와 BPB가 꽤 다릅니다.
여기서 재미있는 관점이 나옵니다. 언어 모델은 압축기로도 볼 수 있습니다. 바이트당 비트가 낮다는 것은 그 텍스트를 더 적은 비트로 표현할 수 있다는 뜻이고, 그만큼 텍스트의 패턴을 잘 학습했다는 의미입니다.
퍼플렉시티 (Perplexity)
교차 엔트로피에 2를 밑으로 지수를 취한 값입니다. PPL = 2^H(P, Q). 밑이 2인 이유는 앞에서 교차 엔트로피를 비트(log₂) 단위로 쟀기 때문입니다. 논문이나 학습 로그는 손실을 자연로그(nat) 단위로 쓰는 경우가 더 흔하고, 그때는 PPL = e^loss가 됩니다. 표기만 다를 뿐 같은 값입니다.
직관적인 해석은 “다음 토큰 후보가 몇 개 사이에서 헷갈리는가” 입니다. 퍼플렉시티가 3이면 모델이 평균적으로 3개 후보 중에서 갈팡질팡한다는 뜻이고, 1이면 확신에 차 있다는 뜻입니다. 낮을수록 좋습니다.
교차 엔트로피가 비트 단위라 직관이 안 서는 반면 퍼플렉시티는 “후보 개수”로 읽히기 때문에 논문과 모델 카드에서 훨씬 자주 보입니다.
퍼플렉시티 해석과 활용 사례
퍼플렉시티는 절대 수치로 해석하면 안 됩니다. 몇 가지 경향만 기억하면 됩니다.
- 구조적인 데이터는 낮습니다. 코드나 HTML은 다음 토큰이 예측 가능해서 퍼플렉시티가 낮습니다.
- 어휘가 클수록 높습니다. 후보가 많으니 당연합니다.
- 컨텍스트가 길수록 낮습니다. 앞을 많이 볼수록 다음을 잘 맞춥니다.
- 사후 학습된 모델은 오히려 높습니다. 채팅 모델은 “그럴듯한 다음 토큰”보다 “도움이 되는 답”을 내도록 학습됐기 때문에 퍼플렉시티는 나빠지지만 실제 유용성은 좋아집니다.
마지막 항목 때문에 퍼플렉시티는 채팅 모델의 성능 비교 지표로는 적합하지 않습니다. 대신 다음과 같은 경우에 자주 사용됩니다.
| 활용 | 원리 |
|---|---|
| 학습 데이터 오염 탐지 | 학습에 쓰인 텍스트는 모델이 외워서 퍼플렉시티가 비정상적으로 낮음 |
| 이상 텍스트 탐지 | 스팸, 깨진 문장은 퍼플렉시티가 높음 |
| 데이터 중복 제거·품질 필터 | 퍼플렉시티가 너무 높거나 낮은 문서를 걸러냄 |
기능적 정확성 (Functional Correctness)
가장 확실한 평가입니다. 결과물이 실제로 동작하는가. 코드는 테스트를 통과하는지 보고, SQL은 실행 결과가 맞는지 보고, 예약 봇은 예약이 실제로 됐는지 봅니다. HumanEval 같은 코드 벤치마크가 이 방식입니다.
프론트엔드 개발자에게 가장 친숙한 평가이기도 합니다. 우리가 AI로 컴포넌트를 생성하고 테스트를 돌려보는 것이 곧 기능적 정확성 평가입니다. 문제는 모든 작업이 이렇게 평가되지는 않는다는 점입니다. 요약, 번역, 상담 답변은 “동작한다”의 기준이 없습니다.
참조 데이터 유사도 측정
정답(참조 데이터)을 사람이 만들어 두고 모델 출력과 얼마나 비슷한지 재는 방식입니다. 비슷함을 재는 방법은 세 단계로 정교해집니다.
- 정확 일치: 문자열이 똑같은가. 짧은 답에만 쓸 수 있습니다.
- 어휘 유사도: 겹치는 단어나 n-gram의 비율. BLEU, ROUGE 같은 지표입니다. 정답이 “환불은 7일 이내에 가능합니다”일 때 “7일 이내에 환불이 가능합니다”는 겹치는 어절이 많아 점수가 높지만, 뜻이 완전히 같은 “일주일 안에 돈을 돌려받으실 수 있어요”는 겹치는 어절이 거의 없어 점수가 바닥입니다. 한국어는 조사와 어미가 붙어 같은 단어도 표면형이 달라지기 때문에 이 문제가 영어보다 더 심합니다.
- 의미 유사도: 임베딩으로 벡터를 만들어 거리를 잽니다. 단어가 달라도 뜻이 같으면 점수가 높습니다.
임베딩 (Embedding)
텍스트를 고정 길이의 숫자 벡터로 바꾼 것입니다. 비슷한 의미의 텍스트는 비슷한 벡터가 되도록 학습된 모델(임베딩 모델)이 만듭니다. “강아지”와 “개”의 벡터는 가깝고 “강아지”와 “행렬”의 벡터는 멉니다.
벡터 사이의 가까움은 보통 코사인 유사도로 잽니다. 두 벡터가 가리키는 방향이 같으면 1, 직각이면 0, 반대면 -1입니다.
function cosineSimilarity(a: number[], b: number[]): number {
let dot = 0, normA = 0, normB = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] ** 2;
normB += b[i] ** 2;
}
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}
// embed()는 OpenAI text-embedding-3, Voyage 등 임베딩 API 호출이라고 가정
const [a, b, c] = await Promise.all([
embed('환불은 어떻게 하나요'),
embed('결제 취소 방법 알려주세요'),
embed('오늘 점심 뭐 먹지'),
]);
cosineSimilarity(a, b); // 0.8~0.9 의미가 비슷
cosineSimilarity(a, c); // 0.1~0.2 관련 없음임베딩 모델의 출력 차원은 모델마다 다릅니다. 수백에서 수천 차원 사이가 보통입니다. 임베딩은 평가에만 쓰이는 게 아니라 RAG(의미 기반 검색)와 시맨틱 캐싱에서도 핵심 재료가 됩니다.
AI 평가자 (AI as a Judge)
모델의 출력을 다른 모델이 채점하는 것입니다. 사람이 채점하기엔 너무 많고, 참조 데이터를 만들기엔 너무 열려 있는 작업에서 씁니다.
AI 평가자를 사용하는 이유는 간단합니다. 빠르고 저렴하며, 사람이 일일이 참조 데이터를 만들 필요가 없습니다. 최근에는 강력한 모델일수록 사람이 직접 평가한 결과와 상당히 비슷한 선택을 내리기도 합니다.
평가 방식은 세 가지 정도로 나뉩니다.
- 개별 응답에 점수를 매깁니다 (예: 1~5점, 합/불합격)
- 사람이 만든 정답과 AI의 답을 비교하여 유사도를 평가합니다
- 두 답변 중 어느 쪽이 더 나은지 선택합니다 (쌍 비교 평가)
평가 프롬프트에는 평가 기준, 점수 척도, 각 점수의 예시를 넣어야 합니다. 앞에서 본 구조화된 출력을 쓰면 점수를 바로 파싱할 수 있습니다.
const Verdict = z.object({
score: z.number().min(1).max(5),
reason: z.string(),
});
const judge = await client.messages.parse({
model: 'claude-opus-5',
max_tokens: 512,
system: `당신은 고객 상담 답변을 평가합니다. 기준: 질문에 직접 답했는가, 사실이 틀리지 않았는가, 톤이 정중한가.
5점: 세 기준 모두 충족 / 3점: 답은 맞지만 불필요하게 길거나 톤이 어색함 / 1점: 질문에 답하지 않았거나 사실 오류`,
messages: [{ role: 'user', content: `질문: ${question}\n답변: ${answer}` }],
output_config: { format: zodOutputFormat(Verdict) },
});이러한 평가에는 다음과 같은 한계가 있습니다.
- 비일관성: 같은 답변을 두 번 평가하면 다른 점수가 나올 수 있습니다. 평가자도 확률적 모델이기 때문입니다. 온도를 낮추고 여러 번 평가해 평균을 내는 방식으로 완화합니다.
- 평가 기준의 모호성: 평가자 모델이나 프롬프트가 바뀌면 “4점”의 의미가 달라집니다. 지난달 평균 4.1점과 이번 달 4.3점을 그대로 비교할 수 없습니다. 평가자 버전을 고정하고 기록해야 합니다.
- 비용과 지연 시간 증가: 응답 하나를 만드는 데 모델 호출이 하나 더 붙습니다. 운영 중 실시간 평가에 쓰면 사용자가 기다리는 시간이 늘어납니다. 보통 샘플링해서 비동기로 평가합니다.
- 편향: 세 가지가 알려져 있습니다. 자기 편향(자기가 만든 답을 선호), 위치 편향(두 답을 비교할 때 먼저 제시된 쪽을 선호), 장문 선호(길고 상세해 보이는 답을 선호). 비교 평가 시 순서를 바꿔 두 번 평가하는 것이 위치 편향의 기본 대응입니다.
5. 모델 고르기와 평가 파이프라인
비교 평가 (Comparative Evaluation)
모델 하나에 점수를 매기는 대신 두 모델의 응답을 나란히 놓고 어느 쪽이 나은지 고르게 하는 방식입니다. 그 결과를 체스의 Elo처럼 순위로 환산합니다. LMSYS의 Chatbot Arena가 대표적입니다.
사람은 절대 점수보다 상대 비교를 훨씬 잘합니다. 그래서 비교 평가가 사람의 실제 선호를 더 잘 반영합니다. 대신 두 가지 문제가 있습니다. 모델이 늘어날수록 비교 쌍이 제곱으로 늘어 확장이 어렵고, 순위는 “A가 B보다 낫다”만 말해줄 뿐 A가 우리 서비스에 충분히 좋은지는 말해주지 않습니다.
평가 주도 개발 (Evaluation-Driven Development)
AI에서의 TDD와 비슷한 개념입니다. 무엇을 만들기 전에 먼저 평가 기준을 명확히 정합니다. 평가할 수 없는 것은 개선할 수도 없습니다. 기능을 개발하고 막연히 “괜찮은 것 같은데?” 하고 배포해버리면, 프롬프트를 한 줄만 바꿔도 실제로 더 좋아졌는지, 아니면 나빠졌는지 분명하게 알 수 없습니다.
오픈 웨이트, 오픈 모델, 그리고 라이선스
“오픈소스 모델”이라는 말이 흔히 쓰이지만 실제로는 세 가지 용어가 있습니다.
| 용어 | 공개 범위 |
|---|---|
| 오픈 웨이트 (open weight) | 모델 가중치만 공개. 학습 데이터와 코드는 비공개 |
| 오픈 모델 (open model) | 가중치 + 학습 데이터(또는 그 구성)까지 공개 |
| 오픈소스 (open source) | 위에 더해 라이선스가 OSI 정의를 충족 |
Llama는 가중치는 공개했지만 학습 데이터는 비공개이고, 라이선스에 “월간 활성 사용자 7억 명 이상이면 별도 계약 필요” 같은 조건이 있습니다. 엄밀히는 오픈 웨이트 모델입니다.
평가 기준 네 가지
| 기준 | 평가 대상 | 예시 지표 |
|---|---|---|
| 도메인 특화 능력 | 우리 분야(법률, 코드, 의료)를 아는지 | 도메인 벤치마크, 기능적 정확성 |
| 생성 능력 | 유창한지, 사실에 맞는지, 안전한지 | 사실성 점수, 유해성 비율 |
| 지시 수행 능력 | 형식·길이·언어 지시를 지키는지 | 형식 준수율, JSON 파싱 성공률 |
| 비용과 지연 시간 | 감당할 수 있는지 | 토큰당 비용, TTFT, 총 지연 |
프론트엔드 개발자가 가장 자주 부딪히는 항목은 지시 수행 능력과 지연 시간입니다. “JSON으로만 답해”라고 했는데 앞에 설명을 붙이는 모델, 응답에 4초가 걸려 스켈레톤을 한참 보여줘야 하는 모델은 제품에 사용하기 어렵습니다.
공개 벤치마크
MMLU(다분야 지식), HumanEval(코드), GSM8K(수학) 같은 공개 문제집입니다. 모델 제공자가 발표하는 점수가 대부분 이런 벤치마크입니다. 다음과 같은 한계를 주의해야합니다.
- 오염 : 벤치마크 문제가 학습 데이터에 포함되었을 수 있습니다.
- 포화 : 모든 모델이 너무 잘해서 변별력이 없어질 수 있습니다.
평가 파이프라인 설계하기
1단계: 시스템의 모든 구성 요소를 평가합니다.
AI 시스템은 모델 하나가 아닙니다. 검색기, 의도 분류기, 프롬프트 템플릿, 출력 파서가 이어져 있습니다. 각 구성 요소를 따로 평가하고, 전체를 끝에서 끝까지도 평가합니다. 대화형 서비스라면 턴 단위와 작업 단위를 구분합니다.
2단계: 평가 가이드라인을 만듭니다.
“좋은 응답”이 무엇인지 글로 적습니다. 채점 루브릭, 점수별 예시, 그리고 실패 사례를 포함합니다. 이 문서가 사람 평가자와 AI 평가자 양쪽의 기준이 됩니다.
3단계: 평가 방법과 데이터를 정의합니다.
기준마다 방법을 정합니다. 형식 준수는 코드로 검증, 사실성은 AI 평가자, 톤은 샘플링해서 사람이 봅니다. 평가 데이터는 실제 사용 로그에서 뽑는 것이 가장 좋고, 부족하면 합성 데이터로 채웁니다. 데이터는 슬라이스(신규 사용자/기존 사용자, 한국어/영어, 짧은 질문/긴 질문)로 나눠서 봐야 합니다.