LLM으로 도메인 특화 애플리케이션을 만들 때 반드시 마주치는 갈림길이 파인튜닝(fine-tuning)과 RAG(Retrieval-Augmented Generation)입니다. 둘 다 “기본 모델을 우리 문제에 맞게 만든다”는 목적은 같지만, 무엇을 바꾸는지가 근본적으로 다릅니다. 파인튜닝은 모델의 가중치(weights)를, RAG는 모델은 그대로 둔 채 추론 시점의 컨텍스트(context)를 바꿉니다.
이 차이를 흐리게 이해하면 “지식을 넣고 싶은데 파인튜닝을 하는” 흔한 실수로 이어집니다. 결론부터 말하면, 지식(what)을 주입하려면 RAG, 행동·형식(how)을 각인하려면 파인튜닝이 기본 원칙입니다. 이 글에서는 두 접근의 작동 원리를 짚고, 어떤 신호를 보고 무엇을 골라야 하는지, 왜 실무에서는 결국 둘을 함께 쓰게 되는지를 정리합니다. 예시 코드는 Anthropic Claude API 기준이지만, 판단 기준은 프로바이더와 무관하게 적용됩니다.
무엇을 바꾸는가: 가중치 vs 컨텍스트
파인튜닝은 입력-출력 쌍으로 추가 학습을 돌려 모델 파라미터를 갱신합니다. 학습이 끝나면 그 지식과 스타일은 모델 안에 내재화돼, 추론 시 별도 컨텍스트 없이도 학습된 패턴대로 반응합니다. 대신 학습 이후 바뀐 사실은 반영되지 않고, 갱신하려면 다시 학습해야 합니다.
RAG는 모델을 건드리지 않습니다. 질문이 들어오면 벡터 검색으로 관련 문서를 찾아 프롬프트에 붙여 넣고, 모델은 그 컨텍스트를 근거로 답합니다. 지식은 모델 밖의 데이터 저장소에 있으므로 문서를 바꾸면 즉시 반영됩니다. 대신 매 요청마다 검색 문서를 토큰으로 실어 보내 컨텍스트 비용과 지연이 붙습니다.
- 파인튜닝: 지식이 가중치에 고정 → 추론 시 컨텍스트 절약, 하지만 갱신은 재학습.
- RAG: 지식이 저장소에 외재 → 실시간 갱신·출처 추적 가능, 하지만 매 요청 컨텍스트 비용.
이 한 줄이 이후의 거의 모든 선택 기준을 파생시킵니다. “정보가 자주 바뀌는가, 출처를 대야 하는가”라면 RAG가, “이 행동 방식을 일관되게 재현해야 하는가”라면 파인튜닝이 자연스럽습니다.
지식 주입에는 RAG가 기본인 이유
“사내 문서 5,000건을 모델에 넣고 싶다”는 요구를 파인튜닝으로 풀려는 시도는 대부분 실패합니다. 파인튜닝은 사실을 암기시키는 데 비효율적이고, 무엇보다 환각(hallucination)을 줄여 주지 않습니다. 학습한 사실도 근사적으로 뭉개져 나오고, 출처를 제시할 방법이 없습니다.
RAG는 이를 구조적으로 해결합니다. 답의 근거가 검색된 실제 문서이므로 어떤 문단을 근거로 삼았는지 인용할 수 있고, 문서가 갱신되면 다음 요청부터 즉시 반영됩니다. 가장 단순한 RAG 루프는 다음과 같습니다.
import anthropic
client = anthropic.Anthropic()
def rag_answer(question: str) -> str:
q_vec = embed(question) # 1) 질문 임베딩
docs = vector_store.search(q_vec, top_k=5) # 유사도 상위 문서 검색
# 2) 검색 문서를 출처 태그와 함께 컨텍스트로 조립
context = "nn".join(
f"[문서 {i} | 출처: {d.source}]n{d.text}" for i, d in enumerate(docs)
)
# 3) 컨텍스트를 근거로만 답하도록 지시
resp = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
system=(
"아래 제공된 문서만 근거로 답하라. "
"문서에 없는 내용은 '자료에 없음'이라고 답하고 지어내지 마라. "
"답변 끝에 사용한 문서 번호를 [출처] 로 표기하라."
),
messages=[{
"role": "user",
"content": f"참고 문서:n{context}nn질문: {question}",
}],
)
return resp.content[0].text
핵심은 시스템 프롬프트에서 “문서에 없으면 지어내지 말라”고 명시하고 출처 표기를 강제하는 것입니다. 이것이 파인튜닝으로는 얻기 어려운 RAG만의 감사 가능성입니다.
파인튜닝이 진짜 필요한 순간
그렇다면 파인튜닝은 언제 쓰는가. 답은 “지식이 아니라 행동을 각인해야 할 때”입니다. 다음 신호가 보이면 파인튜닝을 검토할 가치가 있습니다.
- 고정된 출력 형식을 100%에 가깝게 지켜야 할 때(특정 JSON 스키마, 사내 리포트 양식, 특정 마크업 규칙).
- 고유한 톤·문체를 일관되게 재현해야 할 때(브랜드 보이스, 특정 문서 장르의 어투).
- 같은 지시를 프롬프트로 아무리 길게 써도 미묘한 규칙을 자꾸 어길 때(엣지 케이스가 수십 개인 분류·태깅).
- 프롬프트가 너무 길어져 지연·비용이 문제가 될 때. 규칙을 가중치에 넣으면 프롬프트가 짧아집니다.
예를 들어 “고객 문의를 12개 세부 카테고리로 분류하되 경계가 애매한 케이스가 많은” 작업은 프롬프트에 규칙을 다 담기 어렵습니다. 이럴 때는 잘 라벨링된 예시로 학습시키는 편이 안정적입니다. 학습 데이터는 다음처럼 입력-출력 쌍으로 구성합니다.
import json
# 파인튜닝용 데이터셋: 입력과 "정답 행동"을 쌍으로 (JSONL 한 줄 = 한 예시)
examples = [
{"messages": [
{"role": "user", "content": "결제는 됐는데 주문 상태가 계속 대기중이에요"},
{"role": "assistant", "content": '{"category": "결제-정산지연", "priority": "high"}'},
]},
{"messages": [
{"role": "user", "content": "받은 상품이 사진이랑 색이 달라요"},
{"role": "assistant", "content": '{"category": "상품-불일치", "priority": "mid"}'},
]},
]
with open("train.jsonl", "w", encoding="utf-8") as f:
for ex in examples:
f.write(json.dumps(ex, ensure_ascii=False) + "n")
중요한 것은 일관성입니다. 여러 라벨러가 서로 다른 기준으로 붙인 데이터를 학습시키면 모델은 그 모순까지 배웁니다. 파인튜닝의 품질은 데이터의 품질을 절대 넘지 못합니다.
먼저 프롬프트 엔지니어링을 소진하라
파인튜닝이든 RAG든 손대기 전에 반드시 프롬프트만으로 얼마나 가는지를 확인해야 합니다. 많은 경우 명확한 지시와 몇 개의 예시(few-shot)만으로 파인튜닝이 필요 없는 수준까지 도달합니다. 학습 파이프라인 구축·유지 비용은 결코 작지 않으니, 이 단계를 건너뛰는 것은 값비싼 실수입니다.
# 파인튜닝 전에 먼저: few-shot 프롬프트로 형식을 고정해 본다
resp = client.messages.create(
model="claude-opus-4-8",
max_tokens=256,
system="문의를 아래 예시와 동일한 JSON 형식으로만 분류하라.",
messages=[
{"role": "user", "content": "환불은 언제 들어오나요"},
{"role": "assistant", "content": '{"category": "결제-환불문의", "priority": "mid"}'},
{"role": "user", "content": "쿠폰 코드가 적용이 안 돼요"}, # 실제 질문
],
)
print(resp.content[0].text)
이 few-shot 결과가 목표 정확도를 충족한다면 파인튜닝은 필요 없습니다. 반대로 예시를 늘려도 특정 엣지 케이스에서 계속 틀린다면, 그때가 파인튜닝을 고려할 신호입니다. 파인튜닝은 1순위가 아니라 프롬프트로 안 될 때의 보강책입니다.
비용·지연·갱신 주기로 갈라 보기
두 접근의 운영 특성은 세 축에서 정반대입니다.
- 초기 비용: RAG는 검색 인프라(임베딩·벡터 저장소) 구축이, 파인튜닝은 데이터 라벨링·학습 비용이 큽니다. 소량 시작이라면 RAG가 가볍습니다.
- 추론 비용: RAG는 매 요청 검색 문서 토큰이 더해집니다. 파인튜닝은 규칙이 가중치에 있어 프롬프트가 짧아 요청당 토큰이 적습니다.
- 갱신 주기: 지식이 자주 바뀌면 문서만 교체하면 되는 RAG가 압도적입니다. 파인튜닝은 재학습이 필요합니다.
정리하면 자주 바뀌는 대량 지식은 RAG, 고정된 행동 규칙 + 잦은 반복 호출은 파인튜닝이 유리합니다. RAG의 요청당 컨텍스트 비용은 프롬프트 캐시로 상당 부분 상쇄할 수 있으니, 공유 프리픽스에는 cache_control을 걸어 둡니다.
# RAG의 컨텍스트 비용 완화: 안정적 프리픽스를 프롬프트 캐시로 재사용
resp = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
system=[{
"type": "text",
"text": STABLE_INSTRUCTIONS, # 매 요청 동일 → 캐시 대상
"cache_control": {"type": "ephemeral"},
}],
messages=[{
"role": "user",
"content": f"{retrieved_context}nn질문: {question}", # 매번 달라짐
}],
)
둘을 함께 쓰는 하이브리드 패턴
실무에서 성숙한 시스템은 대개 파인튜닝과 RAG를 겹쳐 씁니다. 잘하는 층이 다르기 때문입니다. 파인튜닝은 “어떻게 답할지(형식·톤)”를, RAG는 “무엇을 근거로 답할지(최신 사실)”를 담당합니다.
예를 들어 사내 지원 봇이라면 말투·형식·에스컬레이션 규칙은 파인튜닝으로 각인하고, 정책 문서·요금표 같은 자주 바뀌는 사실은 RAG로 검색해 붙입니다. 형식은 재학습 없이 안정적이면서, 지식은 문서 교체만으로 최신 상태를 유지합니다.
# 하이브리드: 파인튜닝 모델(행동)에 RAG 컨텍스트(지식)를 주입
def hybrid_answer(question: str) -> str:
docs = vector_store.search(embed(question), top_k=5) # 최신 지식
context = "nn".join(f"[출처:{d.source}] {d.text}" for d in docs)
resp = client.messages.create(
model=FINE_TUNED_MODEL_ID, # 톤·형식이 각인된 모델
max_tokens=1024,
messages=[{"role": "user", "content": f"근거 자료:n{context}nn문의: {question}"}],
)
return resp.content[0].text
하이브리드의 함정은 책임 경계를 흐리는 것입니다. 지식까지 파인튜닝에 억지로 넣으면 갱신이 막힙니다. “형식은 가중치, 사실은 검색”이라는 분담을 명확히 지켜야 합니다.
결정을 평가로 검증하기
어느 쪽을 골랐든 그 선택이 옳았는지는 정량 평가로만 확인할 수 있습니다. 감으로 “좋아진 것 같다”는 판단은 위험합니다. 실패 질문을 모은 평가셋으로 각 접근의 정답률·형식 준수율·환각률을 측정해야 합니다.
# 최소 평가 루프: 핵심 사실 포함 + 출처 표기 여부를 측정
eval_set = load_jsonl("eval.jsonl") # {"q":..., "must_contain":..., "must_cite":true}
def evaluate(answer_fn) -> dict:
rows = [(r, answer_fn(r["q"])) for r in eval_set]
correct = sum(r["must_contain"] in out for r, out in rows)
cited = sum(not r.get("must_cite") or "[출처" in out for r, out in rows)
n = len(rows)
return {"accuracy": correct / n, "citation_rate": cited / n}
print(evaluate(rag_answer)) # RAG vs 하이브리드를 숫자로 직접 비교
print(evaluate(hybrid_answer))
이 평가셋이 있으면 “RAG로 충분한가, 파인튜닝까지 필요한가”를 숫자로 결론낼 수 있습니다. 특히 RAG에서는 검색 품질이 정답률의 상한을 결정하므로, 답이 틀렸을 때 검색이 잘못됐는지 생성이 잘못됐는지를 나눠 봐야 합니다. 엉뚱한 문서를 가져왔다면 파인튜닝을 해도 답은 좋아지지 않습니다.
마무리: 판단 순서
정리하면 결정 순서는 이렇습니다. 첫째, 프롬프트 엔지니어링과 few-shot으로 목표에 도달하는지 먼저 확인합니다. 둘째, 문제가 지식 부족이면 RAG로, 행동·형식 불일치면 파인튜닝으로 갑니다. 셋째, 둘 다 필요하면 “형식은 가중치, 사실은 검색”으로 역할을 나눠 하이브리드로 겹칩니다. 넷째, 어떤 경우든 평가셋으로 검증합니다.
가장 흔한 실수는 “지식을 넣으려고 파인튜닝하는 것”과 “프롬프트로 충분한데 학습 파이프라인부터 짓는 것”입니다. 파인튜닝은 무엇을 아는지가 아니라 어떻게 행동하는지를 바꾸는 도구라는 한 문장만 기억해도 대부분의 선택은 정리됩니다. “이 문제가 지식 문제인가 행동 문제인가”를 먼저 묻는 것이 가장 빠른 판단법입니다.