RAG vs 파인튜닝 vs 롱 컨텍스트: 2026년 LLM 아키텍처 선택 가이드
LLM 기반 애플리케이션을 만들고 있는데 자체 데이터를 써야 해요. 사내 문서든, 고객 지원 티켓이든, 법률 계약서든, 상품 카탈로그든. 베이스 모델은 이런 데이터를 하나도 몰라요. 그래서 결국 모든 AI 엔지니어가 마주하는 질문에 부딪히죠.
RAG 쓸까, 모델 파인튜닝 할까, 아니면 컨텍스트 윈도우에 다 넣을까?
1년 전만 해도 두 가지 중 고르는 문제였어요. 2026년에는 세 가지 선택지가 됐고, 잘못 고르면 필요 없는 인프라에 돈을 태우거나 할루시네이션 뿜뿜하는 앱을 배포하게 돼요.
이 글에서 완전한 판단 프레임워크를 드릴게요. 두루뭉술한 이야기 없이. 실제 아키텍처, 진짜 비용 계산, 프로덕션 코드, 그리고 당장 쓸 수 있는 의사결정 트리까지.
세 가지 방식 한눈에 보기
깊이 들어가기 전에, 각각이 뭘 하는 건지 정리해 볼게요.
**RAG (Retrieval-Augmented Generation)**는 쿼리 시점에 관련 데이터 조각들을 검색해서 프롬프트에 주입해요. 모델 가중치는 변하지 않아요. 매번 질문마다 커닝 페이퍼를 건네주는 거예요.
파인튜닝은 우리 데이터로 모델 가중치 자체를 수정해요. 도메인 지식이 모델 안에 구워지는 거죠. 모델이 우리 업무 언어를 네이티브로 말하도록 가르치는 셈이에요.
롱 컨텍스트는 데이터셋 전체(또는 큰 덩어리)를 모델의 컨텍스트 윈도우에 날것으로 넣어요. 검색 파이프라인도 없고 학습도 없이, 원문 넣고 답 받기. Claude 1M 토큰, Gemini 3.1 1M 토큰 시대라서 예전에는 불가능했던 규모도 이제 가능해요.
┌──────────────────────────────────────────────────────────────────┐
│ 우리 데이터 + LLM = 답변 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ RAG 파인튜닝 롱 컨텍스트 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 쿼리 → 검색 │ │ 우리 데이터로 │ │ 전체 데이터를 │ │
│ │ → 상위 K 청크 │ │ 모델 학습 │ │ 프롬프트에 │ │
│ │ → 프롬프트에 │ │ → 새 모델 │ │ 넣고 질문 │ │
│ │ 주입 │ │ 가중치 │ │ │ │
│ │ → 생성 │ │ → 생성 │ │ → 생성 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ 모델 변경 없음 모델 변경됨 모델 변경 없음 │
│ 데이터 외부 보관 데이터 내재화 데이터 프롬프트 내 │
│ 동적 지식 정적 지식 쿼리별 정적 │
│ 인프라 필요 학습 비용 필요 토큰 비용 높음 │
│ │
└──────────────────────────────────────────────────────────────────┘
각각 깊이 파 볼게요.
RAG: Retrieval-Augmented Generation
동작 원리
RAG는 파이프라인을 검색과 생성 두 단계로 나눠요.
- 인덱싱 (오프라인): 문서를 청크로 나누고, 벡터로 임베딩해서, 벡터 DB에 저장
- 검색 (쿼리 시점): 사용자 쿼리를 임베딩하고, 의미적으로 가장 유사한 청크를 검색
- 생성: 검색된 청크를 프롬프트에 컨텍스트로 주입, LLM이 근거 있는 답변 생성
사용자 쿼리
│
▼
┌─────────────┐ ┌──────────────────┐ ┌────────────────┐
│ 쿼리 임베딩 │────►│ 벡터 데이터베이스 │────►│ 상위 K개 청크 │
│ (인덱싱과 │ │ (Pinecone, │ │ (가장 관련 있는 │
│ 같은 모델) │ │ Weaviate, │ │ 컨텍스트) │
└─────────────┘ │ pgvector 등) │ └───────┬────────┘
└──────────────────┘ │
▼
┌───────────────────────────────────────┐
│ 시스템: 제공된 컨텍스트 기반으로 답변. │
│ 컨텍스트: [검색된 청크들] │
│ 사용자: [원래 질문] │
│ │
│ LLM이 답변 생성 │
└───────────────────────────────────────┘
2026년 프로덕션 RAG 파이프라인
요즘 RAG는 "임베딩하고 검색하면 끝"이 아니에요. 프로덕션 수준은 이렇게 생겼어요:
import { OpenAIEmbeddings } from "@langchain/openai"; import { PGVectorStore } from "@langchain/community/vectorstores/pgvector"; import { ChatOpenAI } from "@langchain/openai"; import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"; // 1. 시맨틱 인식 청킹 const splitter = new RecursiveCharacterTextSplitter({ chunkSize: 512, chunkOverlap: 64, separators: ["\n## ", "\n### ", "\n\n", "\n", " "], }); const chunks = await splitter.splitDocuments(documents); // 2. 메타데이터와 함께 임베딩 및 저장 const embeddings = new OpenAIEmbeddings({ model: "text-embedding-3-large", dimensions: 1024, // 비용 절감을 위한 차원 축소 }); const vectorStore = await PGVectorStore.fromDocuments(chunks, embeddings, { postgresConnectionOptions: { connectionString: process.env.PG_URL }, tableName: "documents", }); // 3. 하이브리드 검색: 벡터 검색 + 메타데이터 필터링 async function retrieve(query: string, filters?: Record<string, any>) { const results = await vectorStore.similaritySearchWithScore(query, 10, filters); // 크로스 인코더로 리랭킹해서 정밀도 향상 const reranked = await rerank(query, results); return reranked.slice(0, 5); } // 4. 검색 컨텍스트로 답변 생성 async function generateAnswer(query: string) { const context = await retrieve(query); const contextText = context.map(([doc]) => doc.pageContent).join("\n\n---\n\n"); const llm = new ChatOpenAI({ model: "gpt-4.1", temperature: 0 }); const response = await llm.invoke([ { role: "system", content: `컨텍스트 기반으로 답변하세요. 컨텍스트에 답이 없으면 그렇다고 말하세요. 가능하면 출처 문서를 인용하세요. 컨텍스트: ${contextText}`, }, { role: "user", content: query }, ]); return response.content; }
RAG가 빛나는 순간
RAG를 써야 하는 상황이에요:
- 데이터가 자주 바뀔 때: 상품 카탈로그, 지원 티켓, 뉴스, 매주 업데이트되는 문서
- 출처 표시가 중요할 때: 법률, 의료, 컴플라이언스. 답의 근거가 어디서 왔는지 정확히 짚어야 할 때
- 데이터셋이 클 때: 수십만 건의 문서에서 쿼리당 작은 관련 조각만 필요할 때
- 정확도가 스타일보다 중요할 때: 사실적 정밀도가 답변 톤보다 중요한 경우
- 멀티 테넌트 앱: 사용자마다 다른 데이터 하위 집합에서 답변이 필요할 때
RAG가 약한 순간
- 여러 문서에 걸친 복잡한 추론: 50개 이상 문서를 합쳐야 답을 낼 수 있으면 검색이 핵심 조각을 놓칠 수 있어요
- 스타일/톤/포맷 요구사항: RAG는 모델의 말투를 바꾸지 않아요. 쿼리 시점에 아는 것만 바꿔요
- 지연시간에 민감한 앱: 검색 단계가 요청당 100-500ms를 더해요
- 작고 안정적인 데이터셋: 컨텍스트 윈도우에 다 들어가고 거의 안 바뀌면 RAG는 과해요
RAG 비용 프로필
| 항목 | 일반적 비용 |
|---|---|
| 임베딩 (인덱싱) | ~$0.02 / 1M 토큰 (text-embedding-3-large) |
| 벡터 DB 호스팅 | $70-500/월 (매니지드 Pinecone/Weaviate) |
| 임베딩 (쿼리당) | ~$0.02 / 1M 토큰 |
| LLM 생성 | 모델 + 검색 컨텍스트 크기에 따라 다름 |
| 100만 쿼리 총비용 | ~$500-2,000 (설정에 따라 편차 큼) |
숨겨진 비용이 있어요. 엔지니어링 시간이에요. 프로덕션 RAG 파이프라인 구축과 유지(청킹 전략, 임베딩 모델 선택, 리랭킹, 메타데이터 필터링, 검색 품질 모니터링)에 상당한 엔지니어링 투자가 들어가요.
파인튜닝
동작 원리
파인튜닝은 사전 학습된 LLM을 우리의 태스크별 데이터로 추가 학습시켜서 모델 가중치를 수정해요. 도메인 지식, 스타일, 행동 패턴이 모델에 내재화되는 거예요.
┌─────────────────────────────────────────────────┐
│ 파인튜닝 파이프라인 │
│ │
│ 1. 학습 데이터 준비 │
│ ┌──────────────────────────────────────┐ │
│ │ {"messages": [ │ │
│ │ {"role": "system", "content": ...}, │ │
│ │ {"role": "user", "content": ...}, │ │
│ │ {"role": "assistant", "content": ...}│ │
│ │ ]} │ │
│ └──────────────────────────────────────┘ │
│ │
│ 2. 학습 (지도 파인튜닝) │
│ 베이스 모델 ──► 학습 ──► 파인튜닝된 모델 │
│ (수시간) │
│ │
│ 3. 파인튜닝된 모델 배포 │
│ 쿼리 ──► 파인튜닝된 모델 ──► 답변 │
│ (검색 단계 불필요) │
│ │
└─────────────────────────────────────────────────┘
실전 파인튜닝
OpenAI API로 파인튜닝하는 실제 코드예요:
import openai import json # 1. JSONL 형식의 학습 데이터 준비 training_data = [ { "messages": [ { "role": "system", "content": "당신은 Acme 사의 시니어 보안 엔지니어입니다. " "보안 정책과 사고 대응 절차에 대해 정확하고 " "실행 가능한 언어로 답변하세요. " "항상 관련 정책 섹션 번호를 인용하세요." }, { "role": "user", "content": "데이터 유출이 의심될 때 절차가 뭔가요?" }, { "role": "assistant", "content": "정책 §4.2.1 - 데이터 유출 대응:\n\n" "1. **즉시 격리** (15분 이내): 영향받은 시스템을 " "격리하세요. 전원 끄지 마세요. 포렌식 증거를 보존해야 해요.\n" "2. **에스컬레이션** (30분 이내): 보안 사고 책임자에게 " "#security-incidents 슬랙 채널로 알리세요.\n" "3. **평가** (2시간 이내): DREAD 모델(§4.2.3)로 " "범위를 파악하세요.\n" "4. **통지** (24시간 이내): PII가 관련되면 " "법무팀에 §7.1 GDPR 요건에 따라 통보 필수.\n\n" "법무팀 승인 전까지 외부 커뮤니케이션 금지." } ] }, # ... 수백 개 더 필요 ] # JSONL로 저장 with open("training_data.jsonl", "w") as f: for example in training_data: f.write(json.dumps(example) + "\n") # 2. 업로드 후 파인튜닝 시작 client = openai.OpenAI() file = client.files.create(file=open("training_data.jsonl", "rb"), purpose="fine-tune") job = client.fine_tuning.jobs.create( training_file=file.id, model="gpt-4.1-mini", hyperparameters={"n_epochs": 3, "learning_rate_multiplier": 1.0}, ) # 3. 파인튜닝된 모델 사용 (학습 완료 후) response = client.chat.completions.create( model="ft:gpt-4.1-mini:acme-corp:security-bot:abc123", messages=[{"role": "user", "content": "피싱 사고 대응은 어떻게 해요?"}] ) # 모델이 Acme 사 목소리로, 정책 섹션을 인용하며 답변해요 # 컨텍스트 주입 없이!
파인튜닝이 빛나는 순간
파인튜닝을 써야 하는 상황이에요:
- 모델의 행동을 바꿔야 할 때: 특정 출력 포맷, 톤, 추론 스타일, 브랜드 보이스
- 지식이 안정적일 때: 사내 정책, 도메인 전문지식, 코딩 표준 등 매주 안 바뀌는 것들
- 지연시간이 중요할 때: 검색 단계가 없으니 응답이 빨라요 (모델 추론만)
- 대규모 비용 효율: 안정적 지식 + 높은 쿼리량이면, 작은 파인튜닝 모델이 RAG의 쿼리당 토큰 뻥튀기를 피해요
- 전문 추론 패턴: 의료 진단, 법률 분석, 코드 리뷰 같은 도메인 특화 추론을 가르칠 때
파인튜닝이 약한 순간
- 데이터가 자주 바뀜: 업데이트마다 재학습 필요 (시간 + 비용)
- 고품질 학습 데이터를 못 만들 때: 쓰레기가 들어가면 쓰레기가 나와요
- Catastrophic Forgetting: 좁은 데이터로 너무 빡세게 학습하면 모델이 일반 능력을 "잊어버릴" 수 있어요
- 출처 표시 불가: 파인튜닝된 모델은 어디서 배웠는지 가리킬 수 없어요. 가중치에 구워져 있으니까요
- 소규모 팀: 데이터 준비, 학습, 평가, 배포의 ML 엔지니어링 오버헤드가 상당해요
파인튜닝 비용 프로필
| 항목 | 일반적 비용 |
|---|---|
| 학습 (GPT-4.1-mini) | ~$5 / 1M 학습 토큰 |
| 학습 (GPT-4.1) | ~$25 / 1M 학습 토큰 |
| 추론 (파인튜닝 모델) | 베이스 모델 가격의 ~1.3배 |
| 데이터 준비 | 엔지니어링 20-100시간 |
| 프로젝트 총비용 | $500-10,000+ (규모에 따라) |
여기서도 숨겨진 비용이 있어요. 데이터 큐레이션이에요. 수백에서 수천 개의 고품질 예시 대화가 필요해요. 이 데이터를 만들고, 정리하고, 검증하는 게 프로젝트에서 가장 비싼 부분인 경우가 많아요.
롱 컨텍스트 윈도우
동작 원리
가장 심플한 방법이에요. 문서를 모아서 이어 붙이고, 모델의 컨텍스트 윈도우에 통째로 넣어요. 임베딩 파이프라인 없이, 벡터 DB 없이, 학습도 없이.
import Anthropic from "@anthropic-ai/sdk"; import { readFileSync, readdirSync } from "fs"; import { join } from "path"; const anthropic = new Anthropic(); // 모든 문서 파일 로드 function loadDocuments(dir: string): string { const files = readdirSync(dir).filter((f) => f.endsWith(".md")); return files .map((f) => { const content = readFileSync(join(dir, f), "utf-8"); return `--- ${f} ---\n${content}`; }) .join("\n\n"); } const allDocs = loadDocuments("./docs"); // 50만 토큰 이상 가능 async function askQuestion(question: string) { const response = await anthropic.messages.create({ model: "claude-sonnet-4-20250514", max_tokens: 4096, messages: [{ role: "user", content: `전체 문서입니다:\n\n${allDocs}\n\n` + `위 문서를 기반으로 답변해 주세요: ${question}`, }], }); return response.content[0].text; }
끝이에요. 청킹도, 임베딩도, 벡터 DB도, 리랭킹도 없어요. 그냥 문서와 질문.
2026년 컨텍스트 윈도우 크기
| 모델 | 컨텍스트 윈도우 | 대략적 페이지 수 |
|---|---|---|
| GPT-4.1 | 1M 토큰 | ~3,000 페이지 |
| Claude Sonnet 4.6 | 1M 토큰 | ~3,000 페이지 |
| Gemini 3.1 Pro | 1M 토큰 | ~3,000 페이지 |
| Llama 4 Scout | 10M 토큰 | ~30,000 페이지 |
| GPT-4.1 mini | 1M 토큰 | ~3,000 페이지 |
롱 컨텍스트가 빛나는 순간
- 데이터셋이 작거나 중간 규모일 때: ~50만 토큰 이하(수백 페이지)면 가장 심플한 선택
- 지금 당장 필요할 때: 세팅할 인프라가 제로. 몇 분 만에 질문 시작 가능
- 여러 문서를 아우르는 추론이 필요할 때: 모델이 전부를 한 번에 보니까, RAG가 놓칠 수 있는 교차 문서 정보 합성이 가능
- 프로토타입/MVP 단계: 먼저 동작하게 만들고, 아키텍처 최적화는 나중에
- 쿼리가 드문 경우: 하루 수백 건이면 쿼리당 비용을 감당할 수 있어요
롱 컨텍스트가 약한 순간
- 규모에서의 비용: 50만 토큰을 매 쿼리마다 보내면 쿼리당 ~15,000
- 지연시간: 50만 토큰 처리는 2천 토큰 RAG 프롬프트보다 훨씬 느려요
- "건초더미에서 바늘 찾기" 문제: 긴 컨텍스트 중간에 묻힌 정보를 모델이 놓칠 수 있어요 ("Lost in the Middle" 현상)
- 데이터셋이 윈도우를 초과: 데이터가 1,000만 토큰인데 윈도우가 100만이면 이 방식은 적용 불가
- 동적 업데이트 불가: 매 쿼리마다 전체 문서를 다시 읽어야 해요. 영구 인덱스가 없으니까요
롱 컨텍스트 비용 프로필
| 항목 | 일반적 비용 |
|---|---|
| 인프라 | $0 (벡터 DB 없음, 학습 없음) |
| 엔지니어링 시간 | 시간 단위 (주 단위 아님) |
| 쿼리당 (20만 컨텍스트) | ~$0.30-0.60 |
| 쿼리당 (50만 컨텍스트) | ~$0.75-1.50 |
| 월 10만 쿼리 총비용 | $30,000-150,000 |
숨겨진 비용은 스케일이 안 된다는 거예요. 처음에 가장 싼 옵션이 볼륨이 커지면 가장 비싸져요.
판단 프레임워크
실전 의사결정 트리예요:
시작
│
▼
┌─────────────────┐
│ 데이터가 얼마나 │
│ 자주 바뀌나요? │
└───────┬─────────┘
│
┌──────────┼──────────┐
▼ ▼ ▼
매일/ 매월/ 거의 안 바뀜/
매주 분기 고정
│ │ │
▼ ▼ ▼
┌───────────┐ ┌──────┐ ┌───────────────┐
│ RAG │ │ 얼마나│ │ 뭐가 더 │
│ (동적 │ │ 큰가? │ │ 중요한가? │
│ 검색) │ └──┬───┘ └──────┬────────┘
└───────────┘ │ │
│ ┌────┼─────┐
┌──────┼──────┐ ▼ ▼ ▼
▼ ▼ ▼ 지식 행동
< 50만 50만~ > 500만│ │
토큰 500만 토큰 ▼ ▼
│ 토큰 │ RAG 파인튜닝
▼ ▼ ▼
롱 RAG RAG
컨텍스트 또는 (유일한
하이브리드 선택지)
비교 매트릭스
| 항목 | RAG | 파인튜닝 | 롱 컨텍스트 |
|---|---|---|---|
| 셋업 시간 | 일~주 단위 | 일~주 단위 | 분~시간 |
| 인프라 | 벡터 DB, 임베딩 | 학습 파이프라인 | 없음 |
| 데이터 최신성 | 실시간 | 재학습 필요 | 쿼리당 다시 읽기 |
| 저볼륨 비용 | 중간 | 높음 (초기) | 낮음 |
| 고볼륨 비용 | 낮음~중간 | 낮음 | 매우 높음 |
| 지연시간 | 중간 (+검색) | 낮음 (추론만) | 높음 (긴 입력) |
| 출처 표시 | 가능 (빌트인) | 불가 | 가능 (수동) |
| 최대 데이터 크기 | 무제한 | 학습에 한계 | 윈도우에 한계 |
| 행동 변경 | 불가 | 가능 | 불가 |
| 할루시네이션 위험 | 낮음 (근거 있음) | 중간 | 낮음 (데이터 있음) |
| 엔지니어링 노력 | 높음 | 높음 | 낮음 |
실전 아키텍처 패턴
패턴 1: RAG(동적 지식) + 파인튜닝(행동) 하이브리드
가장 강력한 패턴이에요. 파인튜닝으로 어떻게 행동할지를, RAG로 무엇을 아는지를 설정해요.
사용자 쿼리
│
▼
┌────────────────┐ ┌──────────────┐
│ RAG 검색 │────►│ 컨텍스트 + │
│ (동적 데이터) │ │ 쿼리 │
└────────────────┘ └──────┬───────┘
│
▼
┌──────────────────┐
│ 파인튜닝된 모델 │
│ (도메인 행동, │
│ 출력 포맷, │
│ 추론 스타일) │
└──────────────────┘
│
▼
근거 있고 잘 포맷된 답변
우리 도메인 보이스로
예시: 임상 커뮤니케이션 가이드라인에 맞게 파인튜닝된 헬스케어 챗봇이 RAG로 최신 의학 문헌과 환자 기록을 조회하는 경우.
패턴 2: 롱 컨텍스트로 프로토타이핑 → RAG로 프로덕션
롱 컨텍스트로 접근법을 검증한 다음, 스케일이 필요하면 RAG로 전환해요.
패턴 3: 티어드 아키텍처
세 가지를 하나의 시스템에서 쿼리를 가장 비용 효율적인 방식으로 라우팅해요:
async function routeQuery(query: string, queryType: string) { switch (queryType) { case "factual_lookup": // 단순 팩트 검색: RAG가 가장 싸요 return await ragPipeline(query); case "complex_analysis": // 교차 문서 추론 필요: 롱 컨텍스트 return await longContextAnalysis(query); case "formatted_report": // 특정 출력 포맷 필요: 파인튜닝 모델 + RAG return await fineTunedWithRAG(query); default: return await ragPipeline(query); } } // 나노 분류기가 쿼리를 적절한 파이프라인으로 라우팅 async function classifyQuery(query: string): Promise<string> { const classifier = new ChatOpenAI({ model: "gpt-4.1-nano" }); const result = await classifier.invoke([ { role: "system", content: `쿼리 유형을 분류하세요: factual_lookup, complex_analysis, formatted_report. 분류만 응답하세요.`, }, { role: "user", content: query }, ]); return result.content as string; }
패턴 4: 에이전틱 RAG
2026년 RAG의 진화형이에요. AI 에이전트가 어떻게 검색할지, 어느 소스를 쓸지, 다단계 검색을 할지를 자율적으로 결정해요:
import { ChatOpenAI } from "@langchain/openai"; import { createReactAgent } from "@langchain/langgraph/prebuilt"; const tools = [ vectorSearchTool, // 벡터 DB 검색 sqlQueryTool, // 구조화 DB 쿼리 webSearchTool, // 웹에서 최신 정보 검색 graphTraversalTool, // 지식 그래프 탐색 calculatorTool, // 계산 수행 ]; const agent = createReactAgent({ llm: new ChatOpenAI({ model: "gpt-4.1" }), tools, messageModifier: `당신은 리서치 에이전트입니다. 각 쿼리에 대해: 1. 질문 유형에 따라 어떤 도구를 쓸지 결정 2. 필요하면 여러 소스에서 정보를 검색 3. 정확도를 위해 결과를 교차 검증 4. 인용과 함께 종합적 답변을 합성`, });
흔한 실수 5가지
실수 1: 뭐든 RAG부터 쓰기
RAG가 "안전한" 선택이 됐지만 항상 맞는 건 아니에요. 안정적인 문서 50페이지 + 하루 100건 쿼리면, 롱 컨텍스트가 더 심플하고, 싸고, 종종 더 정확해요. 모델이 검색된 조각이 아니라 전부를 보니까요.
판단 기준: 데이터가 컨텍스트 윈도우에 들어가고 월 1회 미만으로 바뀌면, 롱 컨텍스트부터 시작하세요.
실수 2: RAG가 맞는데 파인튜닝하기
"우리 모델이 우리 제품을 몰라요" → 이건 지식 문제지 행동 문제가 아니에요. RAG가 풀어요. 지식 주입용 파인튜닝은 비싸고, 금방 구식이 되고, 출처 표시도 안 돼요.
판단 기준: "모델이 X를 모른다"면 RAG. "모델이 X처럼 말하거나 생각하지 않는다"면 파인튜닝.
실수 3: "Lost in the Middle" 문제 무시
롱 컨텍스트 윈도우가 인상적이지만, 아주 긴 컨텍스트의 중간 부분에서 정보를 찾는 건 여전히 모델이 약해요. 50만 토큰 중 20만 번째 위치에 있는 핵심 정보를 놓칠 수 있어요.
완화 방법: 가장 중요한 컨텍스트를 프롬프트의 처음과 끝에 배치하세요.
실수 4: RAG 오버엔지니어링
첫날부터 GraphRAG + 에이전틱 검색 + HyDE + 멀티쿼리 확장 + 리랭커를 다 넣을 필요 없어요. 기본 벡터 검색부터 시작하세요. 검색 품질을 측정하세요. 데이터가 필요하다고 말할 때만 복잡도를 추가하세요.
실수 5: 검색 품질 측정 안 하기
RAG 실패의 가장 흔한 원인은 LLM이 아니에요. 검색이 나쁜 거예요. recall@k와 precision@k를 안 재고 있으면 눈 감고 달리는 거예요.
# 간단한 검색 품질 측정 def evaluate_retrieval(test_queries, ground_truth_docs, retriever, k=5): recalls = [] for query, expected_doc_ids in zip(test_queries, ground_truth_docs): retrieved = retriever.retrieve(query, k=k) retrieved_ids = {doc.id for doc in retrieved} expected_ids = set(expected_doc_ids) recall = len(retrieved_ids & expected_ids) / len(expected_ids) recalls.append(recall) avg_recall = sum(recalls) / len(recalls) print(f"Recall@{k}: {avg_recall:.2%}") return avg_recall
비용 계산: 구체적인 예시
실제 시나리오로 비교해 볼게요. 월 50,000건 쿼리를 처리하는 고객 지원 봇, 지식 베이스는 10,000개 FAQ 아티클 (~200만 토큰).
옵션 A: RAG
| 항목 | 비용 |
|---|---|
| 벡터 DB (기존 Postgres에 pgvector) | $0/월 |
| 임베딩 (월 5만 쿼리 × ~100토큰) | ~$0.10/월 |
| LLM 호출 (5만 × ~2K 토큰 프롬프트) | ~$300/월 (GPT-4.1-mini) |
| 월간 반복 비용 | ~$300/월 |
옵션 B: 파인튜닝 + RAG (하이브리드)
| 항목 | 비용 |
|---|---|
| 파인튜닝 (1회성) | ~$200 |
| RAG 파이프라인 (위와 동일) | ~$300/월 |
| 분기별 재학습 | ~$200/분기 |
| 월간 반복 비용 | ~$370/월 |
옵션 C: 롱 컨텍스트
| 항목 | 비용 |
|---|---|
| 인프라 | $0 |
| LLM 호출 (5만 × ~50만 토큰) | ~$37,500/월 (Claude Sonnet) |
| 월간 반복 비용 | ~$37,500/월 |
이 시나리오에서 결론은 명확해요. 스케일에서 RAG가 100배 이상 싸요. 하지만 하루 50건 프로토타입이면? 롱 컨텍스트가 월 ~$60에 인프라 세팅 제로예요.
교훈: 아키텍처 결정 전에 반드시 우리 규모에 맞는 비용 계산을 돌리세요.
2026년 지형: 뭐가 달라졌나
세 가지 큰 변화가 이 결정을 재편하고 있어요.
1. 컨텍스트 윈도우가 계속 커지고 있어요
Llama 4 Scout의 1,000만 토큰 컨텍스트는 코드베이스나 문서 라이브러리 전체를 담을 수 있는 모델이 오고 있다는 신호예요. RAG를 죽이진 않지만, RAG가 반드시 필요한 영역은 줄어들어요.
2. 에이전틱 RAG의 부상
정적인 검색-생성 파이프라인이 AI 에이전트가 어떻게, 어디서, 다단계로 검색할지를 자율 결정하는 시스템으로 진화하고 있어요.
3. 파인튜닝이 점점 싸고 빨라지고 있어요
LoRA, QLoRA 같은 기법으로 파인튜닝 비용이 급감했어요. 70B 파라미터 모델도 GPU 하나에서 수시간 만에 파인튜닝 가능해요.
4. RAFT (Retrieval-Augmented Fine-Tuning)
검색된 컨텍스트와 잘 작동하도록 모델을 파인튜닝하는 하이브리드 접근이 강력한 패턴으로 떠오르고 있어요. 모델이 노이즈 있는 검색 결과에서 관련 정보를 뽑아내고 불필요한 건 무시하는 법을 배워요.
결론
보편적으로 "이게 최고"라고 할 수 있는 방법은 없어요. 맞는 아키텍처는 우리 데이터, 규모, 지연시간 요구사항, 팀 역량에 달려 있어요.
치트시트예요:
데이터가 자주 바뀌나요? → RAG
모델 행동을 바꿔야 하나요? → 파인튜닝
작은 데이터셋, 지금 당장 필요? → 롱 컨텍스트
스케일에서 최고 품질? → 파인튜닝 모델 + RAG
프로토타이핑? → 롱 컨텍스트 → 동작하면 RAG로 전환
이걸 종교 논쟁처럼 대하지 마세요. 우리 규모에 맞는 비용 계산을 돌리세요. 검색 품질을 측정하세요. 심플하게 시작하세요. 데이터가 필요하다고 말할 때만 복잡도를 추가하세요.
2026년 최고의 LLM 앱을 만드는 엔지니어들은 가장 정교한 파이프라인을 가진 사람이 아니에요. 자기 문제에 맞는 방식을 골라서 제대로 실행한 사람이에요.
관련 도구 둘러보기
Pockit의 무료 개발자 도구를 사용해 보세요