글 목록/Engineering

RAG 완전 정복 (3): 직접 만들어보는 RAG 시스템

이론은 충분합니다. 이제 직접 만들어볼 차례입니다. n8n 노코드 방식부터 LangChain/LlamaIndex TypeScript 코드까지, 복사해서 바로 쓸 수 있는 실전 가이드입니다. 주니어 개발자도 따라할 수 있게 친절하게 설명합니다.

이 글은 기술적 인사이트를 찾는 개발자를 위해 작성되었습니다.
Young

Young

2025년 12월 17일 · 25min

RAGn8nLangChainLlamaIndex실습TypeScript
RAG 완전 정복 (3): 직접 만들어보는 RAG 시스템

팀에 새로운 개발자가 들어올 때마다 같은 질문을 받습니다. '이 서비스 아키텍처가 어떻게 되어 있어요?', '이 에러 나면 어떻게 해요?', '온보딩 문서 어디 있어요?' Confluence와 Notion에 다 정리해뒀는데, 아무도 검색해서 찾지 않습니다. 검색해도 원하는 문서가 안 나오니까요. 그래서 결국 슬랙에 물어보고, 누군가는 같은 답변을 또 타이핑합니다.

RAG(Retrieval-Augmented Generation)는 이 문제를 해결합니다. 흩어진 문서를 벡터 DB에 인덱싱해두면, '인증 에러 해결법 알려줘'라는 자연어 질문에 관련 문서를 찾아서 답변을 생성합니다. 1편에서 RAG의 개념과 필요성을, 2편에서 벡터 DB 선택, 청킹 전략 같은 설계 의사결정을 다뤘습니다. 이제 직접 만들어볼 차례입니다.

이 글에서는 세 가지 방법을 제시합니다. 빠르게 검증하고 싶다면 n8n 노코드 방식으로 30분 만에 동작하는 RAG를 만들 수 있습니다. 세밀한 제어가 필요하거나 기존 백엔드에 통합해야 한다면 LangChain/LlamaIndex TypeScript 코드가 적합합니다. 두 방식 모두 복사해서 바로 실행할 수 있는 수준으로 설명하겠습니다. TypeScript 1~2년차 정도면 충분히 따라올 수 있도록 코드마다 친절하게 주석을 달아뒀으니 걱정 마세요.

[방법 A] n8n으로 RAG 파이프라인 구축하기

n8n은 오픈소스 워크플로우 자동화 플랫폼입니다. 최근 AI/LLM 노드가 크게 강화되면서, 코드 없이도 꽤 정교한 RAG 파이프라인을 만들 수 있게 됐습니다. 특히 2025년 1월 출시된 1.74.0 버전부터 Vector Store를 Tool로 직접 연결할 수 있게 되면서, AI Agent와 RAG를 결합한 워크플로우가 훨씬 쉬워졌습니다.

n8n RAG의 장점은 시각적 디버깅입니다. 각 노드의 입출력을 실시간으로 확인할 수 있어서, '왜 이 문서가 검색됐지?', '왜 이 답변이 나왔지?'를 바로 파악할 수 있습니다. LangChain으로 코딩하면 console.log 지옥에 빠지기 쉬운데, n8n은 그 과정을 시각화해줍니다.

사전 준비

시작하기 전에 몇 가지 준비가 필요합니다. n8n 인스턴스(클라우드 또는 셀프호스팅), OpenAI API 키(임베딩과 LLM용), 그리고 벡터 DB입니다. 벡터 DB는 여러 옵션이 있는데, 이 가이드에서는 Pinecone을 사용합니다. 무료 플랜으로 시작할 수 있고, n8n과의 연동이 가장 매끄럽습니다.

  • n8n: cloud.n8n.io에서 무료 계정 생성 (또는 Docker로 셀프호스팅)
  • OpenAI API: platform.openai.com에서 API 키 발급
  • Pinecone: pinecone.io에서 계정 생성 후 인덱스 생성 (Dimension: 1536, Metric: cosine)

워크플로우 1: 문서 인덱싱 (Ingestion)

RAG 파이프라인은 두 부분으로 나뉩니다. 먼저 문서를 벡터 DB에 저장하는 인덱싱 워크플로우를 만들고, 그 다음 질문을 받아 답변하는 쿼리 워크플로우를 만듭니다. 인덱싱은 문서가 추가/수정될 때만 실행하고, 쿼리는 사용자 요청마다 실행됩니다.

인덱싱 워크플로우의 구조는 이렇습니다: 트리거 -> 문서 가져오기 -> Vector Store 노드 (Insert). 각 단계를 설정해봅시다.

1단계: 트리거 설정. Manual Trigger로 시작합니다. 나중에 Google Drive Trigger나 Schedule Trigger로 바꿔서 자동화할 수 있지만, 지금은 테스트를 위해 수동 실행으로 시작하세요. 2단계: 문서 가져오기. HTTP Request 노드를 추가하고 테스트용으로 공개된 텍스트 파일 URL을 넣어보세요. 실제 환경에서는 Google Drive, Notion, 또는 Read Binary File 노드로 문서를 가져옵니다. 중요한 건 출력이 텍스트 형태여야 한다는 점입니다.

3단계: Vector Store 노드 설정. 여기가 핵심입니다. Pinecone Vector Store 노드를 추가하고, Operation을 Insert Documents로 설정합니다. 그 다음 세 개의 서브노드를 연결해야 합니다.

  1. Embeddings OpenAI 노드: Model을 text-embedding-3-small로 설정합니다. 이 모델이 텍스트를 1536차원 벡터로 변환합니다.
  2. Default Data Loader 노드: 이전 노드에서 넘어온 데이터를 Document 형태로 변환합니다. Binary Mode는 Off, Text Path에는 데이터가 있는 필드명(보통 data 또는 text)을 입력합니다.
  3. Recursive Character Text Splitter 노드: 문서를 청크로 나눕니다. Chunk Size는 1000, Chunk Overlap은 200으로 시작하세요. 이 값은 문서 특성에 따라 조정합니다.

Pinecone 노드의 Credential을 설정합니다. Pinecone 콘솔에서 API 키를 복사해서 n8n Credential에 입력하고, Index Name에는 미리 만들어둔 인덱스 이름을 넣습니다. 모든 연결이 완료되면 워크플로우를 실행해보세요. Pinecone 대시보드에서 벡터가 저장된 걸 확인할 수 있습니다.

Text Splitter의 Chunk Size와 Overlap 설정이 RAG 품질에 큰 영향을 미칩니다. 작은 청크(500~800)는 정밀한 검색에 좋지만 컨텍스트가 부족할 수 있고, 큰 청크(1500~2000)는 맥락이 풍부하지만 노이즈가 섞일 수 있습니다. 시작은 1000/200으로 하고, 검색 결과를 보면서 조정하세요.

워크플로우 2: 질문-답변 (Query)

이제 저장된 문서를 검색해서 답변하는 워크플로우를 만듭니다. 구조는 Chat Trigger -> AI Agent 노드 -> (응답)입니다. AI Agent가 Vector Store를 Tool로 사용해서 관련 문서를 검색하고, 그 내용을 바탕으로 답변을 생성합니다.

1단계: Chat Trigger 설정. Chat Trigger 노드를 추가합니다. n8n의 내장 채팅 인터페이스를 제공하는데, 나중에 Webhook Trigger로 바꿔서 Slack이나 자체 서비스와 연동할 수 있습니다. 2단계: AI Agent 노드 설정. AI Agent 노드를 추가하고 Agent Type을 Tools Agent로 설정합니다. System Message에는 에이전트의 역할을 정의하세요. 예: '당신은 문서 검색 도우미입니다. 사용자 질문에 대해 Vector Store에서 관련 정보를 검색한 후, 검색된 내용만을 바탕으로 답변하세요. 검색된 문서에 없는 내용은 추측하지 마세요.'

3단계: LLM 연결. AI Agent 노드 아래에 OpenAI Chat Model 노드를 연결합니다. Model은 gpt-4o-mini로 시작하세요. 비용 효율적이면서 충분한 성능을 냅니다. 복잡한 추론이 필요해지면 나중에 gpt-4oclaude-sonnet-4로 업그레이드하면 됩니다.

4단계: Vector Store Tool 연결. AI Agent 노드 아래에 Vector Store Tool 노드를 추가합니다. Name은 'DocumentSearch', Description은 '사내 문서에서 정보를 검색합니다. 회사 정책, 제품 정보, FAQ 등을 찾을 때 사용하세요.'처럼 상세하게 작성하세요. 에이전트는 이 Description을 보고 언제 이 Tool을 쓸지 결정합니다. Vector Store Tool 아래에 Pinecone Vector Store 노드를 연결하고(Operation: Retrieve Documents, Top K: 4), Embeddings OpenAI 노드도 연결합니다. 인덱싱 때와 동일한 임베딩 모델(text-embedding-3-small)을 써야 합니다. 다른 모델을 쓰면 검색이 안 됩니다.

마지막으로 AI Agent 노드 아래에 Window Buffer Memory 노드를 연결해서 대화 기록을 유지합니다. 워크플로우를 저장하고 Chat 버튼을 눌러 테스트해보세요. 인덱싱한 문서에 대해 질문하면 관련 내용을 검색해서 답변합니다. n8n의 실행 로그에서 어떤 문서가 검색됐는지, LLM에 어떤 프롬프트가 전달됐는지 바로 확인할 수 있는 게 n8n의 큰 장점입니다.

실무 팁: 실제로는 Notion이나 Google Drive 연동이 많을 거예요. n8n에는 Notion 노드Google Drive 노드가 있어서, 특정 데이터베이스의 페이지들이나 폴더 내 문서를 자동으로 가져올 수 있습니다. Notion Trigger를 쓰면 페이지가 수정될 때마다 자동으로 재인덱싱하는 것도 가능해요.

n8n RAG의 장단점

  • 장점: 빠른 프로토타이핑(30분이면 동작하는 RAG 완성), 시각적 디버깅, 다른 서비스와 쉬운 연동(Slack, Notion, Google Drive 등), 셀프호스팅 가능
  • 단점: 세밀한 제어 한계(커스텀 리트리버, 복잡한 청킹 로직 구현 어려움), 대용량 처리 시 성능 이슈, 코드 기반 시스템 대비 유연성 부족

n8n RAG는 MVP 검증, 내부 도구, 중소규모 문서에 적합합니다. '우리 문서로 RAG가 얼마나 잘 동작하나?' 빠르게 확인하고 싶을 때 최고의 선택입니다. 하지만 프로덕션 수준의 세밀한 튜닝이나 대규모 트래픽 처리가 필요하면 코드 기반 접근이 낫습니다.

[방법 B] LangChain으로 RAG 구현하기

코드로 RAG를 구현하면 더 세밀한 제어가 가능합니다. 커스텀 청킹 로직, 하이브리드 검색, 리랭킹 같은 고급 기법을 적용할 수 있고, 기존 백엔드에 자연스럽게 통합할 수 있습니다. LangChain은 RAG 구현에 가장 널리 쓰이는 프레임워크입니다.

프로젝트 세팅

TypeScript 프로젝트를 새로 만들고 필요한 패키지를 설치합니다. LangChain은 모듈화되어 있어서, 필요한 기능만 골라서 설치합니다.

bash|setup.sh
mkdir rag-langchain && cd rag-langchain
npm init -y
npm install typescript ts-node @types/node -D
npm install @langchain/core @langchain/openai @langchain/community
npm install cheerio # 웹 문서 로딩용

# tsconfig.json 생성
npx tsc --init

환경 변수를 설정합니다. 프로젝트 루트에 .env 파일을 만들고 OpenAI API 키를 넣습니다.

bash|.env
OPENAI_API_KEY=sk-your-api-key-here

기본 RAG 구현

아래는 동작하는 최소한의 RAG 코드입니다. 웹 페이지를 로드하고, 청킹하고, 메모리 벡터 스토어에 저장한 뒤, 질문에 답변합니다.

typescript|src/basic-rag.ts
import "dotenv/config";  // .env 파일에서 환경변수 자동으로 불러옴
import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai";
import { CheerioWebBaseLoader } from "@langchain/community/document_loaders/web/cheerio";
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { createStuffDocumentsChain } from "langchain/chains/combine_documents";
import { createRetrievalChain } from "langchain/chains/retrieval";
import { ChatPromptTemplate } from "@langchain/core/prompts";

async function main() {
  // ========================================
  // 1단계: 문서 로딩
  // - 웹 페이지 HTML을 가져와서 텍스트만 추출합니다
  // - Cheerio는 Node.js용 jQuery 같은 라이브러리예요
  // ========================================
  const loader = new CheerioWebBaseLoader(
    "https://docs.smith.langchain.com/overview"  // 여기에 원하는 URL을 넣으세요
  );
  const docs = await loader.load();
  console.log(`Loaded ${docs.length} documents`);

  // ========================================
  // 2단계: 청킹 (문서를 작은 조각으로 나누기)
  // - chunkSize: 각 조각의 최대 글자 수 (1000자 = 약 250단어)
  // - chunkOverlap: 조각 사이 겹치는 부분 (맥락 유지용)
  // ========================================
  const splitter = new RecursiveCharacterTextSplitter({
    chunkSize: 1000,   // 너무 크면 검색 정확도 떨어지고, 너무 작으면 맥락이 끊김
    chunkOverlap: 200, // 앞뒤 조각이 200자씩 겹쳐서 문맥이 자연스럽게 이어짐
  });
  const splitDocs = await splitter.splitDocuments(docs);
  console.log(`Split into ${splitDocs.length} chunks`);

  // ========================================
  // 3단계: 임베딩 & 벡터 스토어 저장
  // - 텍스트를 숫자 배열(벡터)로 변환해서 저장합니다
  // - MemoryVectorStore는 메모리에만 저장되므로 프로그램 종료시 사라짐
  //   (프로덕션에서는 Pinecone, Qdrant 등 영구 저장소 사용)
  // ========================================
  const embeddings = new OpenAIEmbeddings({
    model: "text-embedding-3-small",  // 저렴하고 충분히 좋은 임베딩 모델
  });
  const vectorStore = await MemoryVectorStore.fromDocuments(
    splitDocs,
    embeddings
  );

  // ========================================
  // 4단계: 리트리버 생성
  // - 질문이 들어오면 비슷한 문서를 찾아주는 역할
  // - k: 몇 개의 문서를 찾을지 (4개면 대부분 충분)
  // ========================================
  const retriever = vectorStore.asRetriever({
    k: 4,  // 상위 4개 문서 검색. 더 정확하게 하려면 늘리되, 비용도 늘어남
  });

  // ========================================
  // 5단계: LLM & 프롬프트 설정
  // - temperature: 0이면 항상 같은 답변, 높을수록 창의적(불안정)
  // ========================================
  const llm = new ChatOpenAI({
    model: "gpt-4o-mini",  // 저렴하면서 충분히 똑똑한 모델
    temperature: 0,        // RAG에선 일관된 답변이 중요하니 0으로
  });

  // 프롬프트 템플릿: {context}와 {input}은 나중에 실제 값으로 채워짐
  const prompt = ChatPromptTemplate.fromTemplate(`
    아래 컨텍스트를 참고해서 질문에 답하세요.
    컨텍스트에 없는 내용은 "문서에서 해당 정보를 찾을 수 없습니다"라고 답하세요.

    컨텍스트:
    {context}

    질문: {input}
  `);

  // ========================================
  // 6단계: RAG 체인 구성
  // - 여러 문서를 하나의 컨텍스트로 합치는 체인
  // - retriever와 LLM을 연결해서 완성된 RAG 파이프라인 만들기
  // ========================================
  const combineDocsChain = await createStuffDocumentsChain({
    llm,
    prompt,
  });

  const ragChain = await createRetrievalChain({
    retriever,         // 문서 검색 담당
    combineDocsChain,  // 검색된 문서로 답변 생성 담당
  });

  // ========================================
  // 7단계: 실제로 질문하기!
  // - 여기서 마법이 일어남: 질문 -> 검색 -> 답변 생성
  // ========================================
  const response = await ragChain.invoke({
    input: "LangSmith가 뭔가요?",  // 여기에 원하는 질문을 넣으세요
  });

  console.log("\n답변:", response.answer);
  console.log("\n참조 문서 수:", response.context.length);
}

main().catch(console.error);

바로 실행해보세요! 터미널에서 npx ts-node src/basic-rag.ts만 치면 됩니다. 웹 페이지를 자동으로 가져오고, 잘게 나누고, 질문에 답변합니다. 에러가 나면 99%는 OPENAI_API_KEY가 .env에 없어서입니다. 코드 자체는 길어 보이지만 주석 빼면 30줄도 안 됩니다.

코드 뜯어보기 (어려우면 넘어가도 OK)

코드가 동작하는 원리가 궁금한 분들을 위해 핵심만 짚겠습니다. 지금 당장 이해 안 해도 됩니다. 일단 돌려보고, 나중에 수정이 필요할 때 다시 보세요. CheerioWebBaseLoader는 웹 페이지 HTML을 가져와서 텍스트만 뽑아내는 역할입니다. 실무에서는 Notion 연동이 많은데, NotionAPILoader를 쓰면 Notion API로 데이터베이스나 페이지를 자동으로 가져올 수 있어요. PDF라면 PDFLoader, Google Drive라면 GoogleDriveLoader도 있습니다.

RecursiveCharacterTextSplitter는 문서를 똑똑하게 자르는 도구입니다. 단순히 1000글자마다 자르는 게 아니라, 먼저 단락(\n\n)으로 나눠보고, 그래도 길면 문장(.)으로, 그래도 길면 단어( )로 나눕니다. 그래서 'Recursive(재귀적)'라는 이름이 붙었어요. 덕분에 문장 중간이 잘리는 일이 적습니다.

MemoryVectorStore는 이름 그대로 메모리에만 저장되는 벡터 DB입니다. 프로그램 끄면 데이터도 사라져요. 그래서 테스트용이고, 실제 서비스에는 Pinecone 같은 영구 저장소를 씁니다. createRetrievalChain은 '질문 받기 -> 문서 검색 -> 답변 생성'을 한 번에 처리하는 편의 함수입니다. 내부 동작이 궁금하면 LangChain 문서를 보면 되는데, 처음엔 그냥 '질문하면 알아서 답변해주는 마법의 함수'라고 생각해도 됩니다.

프로덕션 확장 포인트

기본 코드를 프로덕션 수준으로 올리려면 몇 가지를 추가해야 합니다. 가장 중요한 건 영구 벡터 스토어입니다. Pinecone을 예로 들면 이렇게 바꿉니다.

typescript|src/with-pinecone.ts
import { Pinecone } from "@pinecone-database/pinecone";
import { PineconeStore } from "@langchain/pinecone";

// ========================================
// Pinecone 연결하기
// - pinecone.io에서 무료 계정 만들고 API 키 발급받으세요
// - .env 파일에 PINECONE_API_KEY=pk-xxx... 형태로 저장
// ========================================
const pinecone = new Pinecone({
  apiKey: process.env.PINECONE_API_KEY!,  // !는 "값이 반드시 있다"는 TypeScript 문법
});

// Pinecone 대시보드에서 만든 인덱스 이름을 넣으세요
const index = pinecone.index("your-index-name");

// ========================================
// 케이스 1: 새 문서를 저장할 때 (인덱싱)
// - splitDocs는 위에서 청킹한 문서 배열
// - embeddings는 OpenAIEmbeddings 인스턴스
// ========================================
const vectorStore = await PineconeStore.fromDocuments(
  splitDocs,    // 저장할 문서들
  embeddings,   // 임베딩 모델 (text-embedding-3-small 등)
  { pineconeIndex: index }
);

// ========================================
// 케이스 2: 이미 저장된 문서 검색할 때 (쿼리)
// - 문서가 이미 Pinecone에 있으면 이걸 사용
// - fromExistingIndex는 저장은 안 하고 검색만 함
// ========================================
const existingVectorStore = await PineconeStore.fromExistingIndex(
  embeddings,   // 저장할 때와 같은 임베딩 모델 써야 함!
  { pineconeIndex: index }
);

const retriever = existingVectorStore.asRetriever({ k: 4 });

메타데이터 필터링도 중요합니다. 문서마다 source, date, category 같은 메타데이터를 붙여두면, 검색할 때 필터링할 수 있습니다. '2024년 이후 HR 정책만 검색해줘'같은 요청을 처리할 수 있죠.

typescript|metadata-filtering.ts
// ========================================
// 문서에 메타데이터(추가 정보) 붙이기
// - 나중에 "HR 문서만 검색해줘" 같은 필터링이 가능해집니다
// - 태그 달아두는 거라고 생각하면 됩니다
// ========================================
const docsWithMetadata = splitDocs.map((doc, i) => ({
  ...doc,  // 기존 문서 내용은 그대로 유지
  metadata: {
    ...doc.metadata,         // 기존 메타데이터도 유지
    source: "hr-policy",     // 어떤 종류의 문서인지
    year: 2024,              // 언제 문서인지
    department: "engineering", // 어느 부서 문서인지
  },
}));

// ========================================
// 메타데이터로 필터링해서 검색하기
// - "2024년 이후 HR 정책 문서에서만 찾아줘" 같은 요청 처리 가능
// - $gte는 "greater than or equal", 즉 "이상"이라는 뜻
// ========================================
const retriever = vectorStore.asRetriever({
  k: 4,
  filter: {
    source: "hr-policy",     // source가 "hr-policy"인 것만
    year: { $gte: 2024 },    // year가 2024 이상인 것만
  },
});

RAG의 80%는 데이터 준비입니다. 청킹 전략, 메타데이터 설계, 임베딩 모델 선택이 검색 품질을 좌우합니다. LLM은 좋은 컨텍스트가 주어지면 대부분 좋은 답변을 생성합니다.

LangChain 공식 문서, Best Practices 섹션

[방법 C] LlamaIndex 대안

LlamaIndex는 LangChain의 대안입니다. 둘 다 RAG를 구현할 수 있지만 철학이 다릅니다. LangChain은 범용 LLM 애플리케이션 프레임워크로, RAG 외에도 에이전트, 체인, 메모리 등 다양한 기능을 제공합니다. LlamaIndex는 RAG에 특화되어 있어서, 데이터 인덱싱과 검색에 더 최적화된 추상화를 제공합니다.

LlamaIndex로 같은 RAG 구현하기

bash|setup-llamaindex.sh
mkdir rag-llamaindex && cd rag-llamaindex
npm init -y
npm install typescript ts-node @types/node -D
npm install llamaindex @llamaindex/openai
typescript|src/basic-rag.ts
import "dotenv/config";
import {
  Document,
  VectorStoreIndex,
  SimpleDirectoryReader,
} from "llamaindex";
import { OpenAI } from "@llamaindex/openai";

async function main() {
  // ========================================
  // 1단계: 문서 로딩
  // - ./data 폴더에 있는 모든 파일을 자동으로 읽어옵니다
  // - .txt, .pdf, .md 등 다양한 형식 지원
  // - 먼저 프로젝트 루트에 data 폴더 만들고 파일 넣어두세요!
  // ========================================
  const reader = new SimpleDirectoryReader();
  const documents = await reader.loadData("./data");
  console.log(`Loaded ${documents.length} documents`);

  // ========================================
  // 2단계: 인덱스 생성
  // - 이 한 줄이 청킹 + 임베딩 + 저장을 다 해줍니다!
  // - LlamaIndex의 강점: 보일러플레이트가 적음
  // - 기본 설정으로도 꽤 잘 동작합니다
  // ========================================
  const index = await VectorStoreIndex.fromDocuments(documents);

  // ========================================
  // 3단계: 쿼리 엔진 생성
  // - LangChain의 retriever + chain을 합쳐놓은 것
  // - 질문하면 알아서 검색하고 답변 생성
  // ========================================
  const queryEngine = index.asQueryEngine();

  // ========================================
  // 4단계: 질문하기
  // - 진짜 이게 끝입니다!
  // ========================================
  const response = await queryEngine.query({
    query: "이 문서의 핵심 내용이 뭔가요?",
  });

  console.log("답변:", response.toString());
}

main().catch(console.error);

LlamaIndex 코드가 더 짧습니다. VectorStoreIndex.fromDocuments() 한 줄이 청킹, 임베딩, 저장을 모두 처리합니다. 이게 LlamaIndex의 장점입니다. RAG에 집중한 추상화 덕분에 보일러플레이트가 적습니다.

LangChain vs LlamaIndex: 언제 뭘 쓸까

  • LangChain 선택: RAG 외에 에이전트, 복잡한 체인 로직이 필요할 때. 다양한 LLM/벡터 DB 조합을 실험하고 싶을 때. 커뮤니티와 예제가 풍부해서 참고 자료가 많음.
  • LlamaIndex 선택: RAG만 필요하고 빠르게 구현하고 싶을 때. 복잡한 문서 구조(PDF, 표, 계층적 문서)를 다룰 때. LlamaIndex의 데이터 커넥터와 인덱스 추상화가 더 성숙함.

실무에서는 LangChain으로 시작하는 경우가 더 많습니다. 생태계가 크고, RAG 이후 에이전트로 확장하기 쉽기 때문입니다. 하지만 순수 RAG 품질만 놓고 보면 LlamaIndex가 더 나은 경우도 있습니다. 특히 복잡한 문서(표가 많은 PDF, 계층적 구조의 기술 문서)를 다룰 때 LlamaIndex의 파싱 능력이 빛납니다.

실전 유스케이스 3가지

RAG 파이프라인을 만들었으니 실제 문제에 적용해봅시다. 개발팀에서 가장 흔히 쓰이는 세 가지 유스케이스를 소개합니다.

1. 사내 문서 검색 봇

Confluence, Notion, Google Drive에 흩어진 문서를 RAG로 통합 검색합니다. '연차 신청 절차가 어떻게 돼?', '신입 온보딩 체크리스트 보여줘' 같은 질문에 즉시 답변합니다. HR 담당자가 같은 질문에 반복 답변하는 시간을 줄일 수 있습니다.

구현 팁: 문서 소스별로 메타데이터를 구분하세요. 'HR 정책만 검색', 'Engineering 문서만 검색' 같은 필터링이 가능해집니다. 그리고 문서 업데이트 시 자동으로 재인덱싱하는 파이프라인을 만들어두세요. n8n이라면 Google Drive Trigger로, 코드라면 cron job으로 구현합니다.

2. 고객 FAQ 봇

제품 문서, 도움말, 기존 FAQ를 인덱싱해서 고객 질문에 자동 응답합니다. 단순 FAQ 봇과 다른 점은, 질문 표현이 달라도 의미가 같으면 답변할 수 있다는 겁니다. '환불하고 싶어요', '돈 돌려받을 수 있나요?', '구매 취소 가능한가요?'가 모두 같은 문서를 검색합니다.

구현 팁: 답변 끝에 출처를 표시하세요. '이 답변은 [환불 정책 문서]를 참고했습니다.' 고객 신뢰도가 올라가고, 틀린 답변이 있을 때 추적이 가능합니다. 그리고 '잘 모르겠습니다'라고 답할 수 있게 프롬프트를 설계하세요. 무조건 답변하려고 하면 할루시네이션이 늘어납니다.

3. 코드베이스 Q&A

레포지토리의 코드와 문서를 인덱싱해서 질문에 답변합니다. '이 함수 어디서 호출돼?', '인증 로직이 어떻게 동작해?', '이 에러 메시지 뜨면 어떻게 해야 해?' 같은 질문을 처리합니다. 신규 입사자 온보딩이나 레거시 코드 파악에 특히 유용합니다.

핵심 팁은 코드 전용 Text Splitter를 쓰는 겁니다. LangChain의 RecursiveCharacterTextSplitter는 언어별 구분자를 지원해서, 함수/클래스 경계를 인식하고 자릅니다. 일반 텍스트처럼 자르면 함수 중간이 잘려서 검색 품질이 떨어집니다.

typescript|code-splitter.ts
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
import { Language } from "@langchain/textsplitters";

// ========================================
// 코드 전용 스플리터
// - 일반 텍스트 스플리터 쓰면 함수 중간에서 잘릴 수 있음
// - 코드 전용은 함수, 클래스 경계를 인식해서 자릅니다
// - TypeScript 외에도 Python, JavaScript, Java 등 지원
// ========================================
const splitter = RecursiveCharacterTextSplitter.fromLanguage(Language.TS, {
  chunkSize: 2000,   // 코드는 일반 텍스트보다 크게 잘라야 맥락 유지
  chunkOverlap: 200, // 앞뒤로 조금씩 겹치게
});

// ========================================
// 파일 경로를 메타데이터로 저장하기
// - 나중에 "이 함수는 src/auth/login.ts에 있어요"라고 알려줄 수 있음
// - 검색 결과의 신뢰도가 확 올라갑니다
// ========================================
const docsWithPath = splitDocs.map((doc) => ({
  ...doc,
  metadata: {
    ...doc.metadata,
    filePath: "src/auth/login.ts",  // 실제로는 파일 읽을 때 동적으로 넣으세요
    language: "typescript",
  },
}));

파일 경로를 메타데이터로 저장해두면 'src/auth/login.ts의 authenticateUser 함수를 보세요'처럼 구체적인 위치를 알려줄 수 있습니다. 검색 결과의 신뢰도가 확 올라갑니다.

프로덕션 체크리스트

RAG를 프로덕션에 배포하기 전에 점검해야 할 항목들입니다. MVP에서 잘 돌아가던 게 프로덕션에서 문제 생기는 경우가 많습니다.

모니터링

  • 검색 품질 추적: 사용자 질문, 검색된 문서, 생성된 답변을 로깅합니다. 나중에 검색이 잘못된 케이스를 분석해서 개선할 수 있습니다.
  • 레이턴시 모니터링: 임베딩 생성, 벡터 검색, LLM 호출 각 단계별 시간을 측정합니다. 어디가 병목인지 파악해야 최적화할 수 있습니다.
  • 토큰 사용량 추적: LLM 호출마다 입력/출력 토큰을 기록합니다. 비용 예측과 이상 탐지에 필수입니다.

평가 (어려우면 일단 넘어가세요)

RAG 평가에는 여러 지표가 있는데, 처음에는 복잡한 지표 몰라도 됩니다. 핵심은 '질문했을 때 맞는 문서가 나오는가?', '그 문서로 맞는 답변이 생성되는가?' 두 가지입니다.

  • Retrieval 평가 (검색이 잘 되나?): 질문에 맞는 문서가 검색 결과에 포함되는지 확인합니다. 전문 용어로는 Recall@K(상위 K개 결과에 정답이 있는 비율), MRR(정답이 몇 번째에 나오는지) 같은 지표가 있는데, 지금은 몰라도 됩니다. '10개 질문 던져보고, 관련 문서가 잘 나오면 OK' 정도로 시작하세요.
  • Generation 평가 (답변이 맞나?): 검색된 문서를 바탕으로 올바른 답변이 생성되는지 확인합니다. 10~20개 질문에 대한 기대 답변을 미리 만들어두고, 실제 답변과 비교하면 됩니다.
  • End-to-End 평가: 실제로 써보면서 '이상한 답변이 나오면 메모해두기'. 자동화된 평가도 좋지만, 직접 써보는 게 가장 확실합니다.

처음에는 '50개 질문 만들어서 테스트' 정도면 충분합니다. Recall@K, MRR 같은 전문 지표는 나중에 정교하게 튜닝할 때 공부하세요. 지금 당장 필요하면 LangSmith나 Ragas 문서를 찾아보시면 됩니다.

비용 관리

  • 임베딩 비용: 인덱싱은 한 번이지만, 쿼리마다 질문 임베딩이 필요합니다. text-embedding-3-small은 100만 토큰당 $0.02로 저렴합니다.
  • LLM 비용: 검색된 컨텍스트 + 질문 + 시스템 프롬프트가 입력 토큰입니다. GPT-4o-mini 기준 100만 입력 토큰당 $0.15. 컨텍스트가 길면 비용이 빠르게 늘어납니다.
  • 벡터 DB 비용: Pinecone Starter는 무료지만 제한이 있습니다. 프로덕션은 Standard 플랜($70/월~)이 필요할 수 있습니다.

업데이트 전략

문서가 바뀌면 인덱스도 업데이트해야 합니다. 전략은 세 가지입니다. 전체 재인덱싱(간단하지만 비용 발생), 증분 업데이트(변경된 문서만 재인덱싱, 구현이 복잡), TTL 기반(오래된 벡터 자동 삭제 후 재생성). 문서 양과 변경 빈도에 따라 선택하세요. 대부분은 야간 배치로 전체 재인덱싱하는 것으로 시작해도 충분합니다.

마무리: RAG 너머

'RAG 완전 정복' 시리즈를 마무리합니다. 글 처음에 얘기한 상황, 새 팀원이 들어올 때마다 같은 질문에 답하느라 지쳤던 그 상황으로 돌아가 봅시다. 이제 여러분은 Confluence, Notion, 심지어 코드베이스까지 인덱싱해서 자연어로 검색하는 시스템을 직접 만들 수 있습니다.

  1. 1편 핵심: RAG는 LLM의 한계(할루시네이션, 컷오프, 비공개 데이터)를 검색으로 보완하는 패턴이다.
  2. 2편 핵심: 벡터 DB 선택, 청킹 전략, 임베딩 모델이 RAG 품질을 좌우한다. 하이브리드 검색과 리랭킹으로 정확도를 높일 수 있다.
  3. 3편 핵심: n8n으로 빠르게 프로토타이핑하고, LangChain/LlamaIndex로 프로덕션 수준의 세밀한 제어를 한다.

RAG는 시작입니다. 여기서 더 나아가면 Advanced RAG 기법들이 있습니다. Query Rewriting, Reranking, Multi-hop RAG, Iterative RAG... 지금은 이런 이름만 들어봤다 정도면 됩니다. 기본 RAG로 80%는 해결되고, 나머지 20%가 필요할 때 그때 찾아보세요. 그리고 Agent + RAG를 결합하면 더 강력해지는데, 이것도 기본 RAG가 익숙해진 다음 단계입니다.

지금 당장 할 일은 하나입니다. 이 글의 코드를 복사해서 실행해보세요. 에러가 나면 API 키 확인하고, 돌아가면 여러분의 문서로 테스트해보세요. '우리 데이터로 이게 되네!'를 직접 경험하는 게 다음 단계로 나아가는 가장 좋은 동기부여입니다. 복잡한 건 나중에 필요할 때 배우면 됩니다.

이 글이 도움이 되셨나요?

RAG 시스템 구축이나 AI 파이프라인 설계가 필요하신가요?

AI 도입에 대해 궁금한 점이 있으시다면 언제든 문의해 주세요. 전문가가 맞춤형 솔루션을 제안해 드립니다.

AI 프로젝트를 시작하세요

AI 도입에 대해 궁금한 점이 있으시다면 언제든 문의해 주세요. 전문가가 상담해 드립니다.