자바로 배우는 AI (3) — RAG

RAG(Retrieval-Augmented Generation)의 정의는 한 문장이다. “답하기 전에 관련 자료를 검색해서 프롬프트에 붙여넣는다.” 전체 코드: github.com/dadaok/ai-playground

왜 필요한가

모델은 우리 회사 내부 문서를 모른다. 학습 데이터에 없으니까. 그냥 물으면 그럴듯하게 지어낸다(hallucination). 그래서 질문과 비슷한 문서 조각을 검색해 그 조각만 근거로 답하게 시킨다.

파이프라인 — 딱 3단계

단계하는 일
1. 로드 & 청크 분할문서를 읽어 문단 단위로 쪼갬
2. 검색질문과 유사한 청크 top-k 뽑기
3. 생성뽑은 청크를 프롬프트에 넣고 “이것만 근거로 답해”

docs/ 에 가상의 “달빛커피” 내부 문서 4개(배송정책·구독요금·원두정보·회사소개)를 두고 실습했다.

1. 로드 & 청크 분할

for (String para : Files.readString(p).split("\\n\\s*\\n")) {  // 빈 줄 = 문단 = 청크
    String t = para.strip();
    if (!t.isBlank()) out.add(new Chunk(p.getFileName().toString(), t));
}

2. 검색 — 여기가 핵심

진짜 RAG는 이 단계에 “임베딩(embedding) 벡터 + 벡터DB”를 쓴다. “환불”↔”돈 돌려받기”처럼 단어가 달라도 의미가 비슷하면 찾아낸다. Anthropic은 임베딩 API가 없어서 보통 Voyage AI를 쓴다. 이번엔 두 번째 API 키 없이 개념만 보려고 키워드 유사도(TF-IDF 코사인) 로 구현했다. 구조는 동일하다.

한국어는 조사(“환불은”) 때문에 단어가 딱 안 맞는다. 그래서 단어 + 한글 단어의 2글자 조각(bigram)을 함께 토큰으로 쓴다.

static List<String> tokenize(String text) {
    List<String> tokens = new ArrayList<>();
    for (String raw : text.toLowerCase().split("[^\\p{L}\\p{N}]+")) {
        if (raw.isBlank()) continue;
        tokens.add(raw);
        boolean hangul = raw.codePoints().anyMatch(cp -> cp >= 0xAC00 && cp <= 0xD7A3);
        if (hangul && raw.length() >= 2) {
            for (int i = 0; i + 2 <= raw.length(); i++) tokens.add(raw.substring(i, i + 2));
        }
    }
    return tokens;  // "환불은" -> [환불은, 환불, 불은] -> 문서의 "환불" 과 매칭
}

TF-IDF: 청크별 단어 빈도(tf)와 단어별 희소성(idf = ln(1 + N/df))을 구해 코사인 유사도로 랭킹. retrieve() 내부만 임베딩 검색으로 갈아끼우면 실무 RAG가 된다.

3. 검색 결과를 프롬프트에 삽입

if (context == null) {
    b.system("너는 달빛커피 고객센터 상담원이다. 한국어로 간결히 답해라.");
    b.addUserMessage(question);
} else {
    b.system("너는 달빛커피 고객센터 상담원이다. 아래 <자료> 안의 내용만 근거로 답해라. "
            + "자료에 없는 내용은 지어내지 말고 '자료에서 확인되지 않습니다'라고 답해라. 한국어로 간결히.");
    b.addUserMessage("<자료>\n" + context + "\n</자료>\n\n질문: " + question);
}

RAG 유/무 비교

질문마다 두 번 호출해서 비교했다.

질문: 구독 해지하면 환불은 언제 돼?

── RAG 없음 ──
(모델이 그럴듯하게 지어냄. "보통 7~14일 내 환불됩니다" 같은 추측)

── 검색된 청크 (top 3) ──
  [0.412] 배송정책.txt: 구독을 해지하면 다음 결제일부터 청구가 멈춘다...

── RAG 있음 ──
발송 전이면 고객센터 요청 시 전액 환불되고, 영업일 기준 5일 이내에 처리됩니다.
(← docs/배송정책.txt 의 실제 내용)

자료에 없는 질문(“매장에서 커피 마실 수 있어?”)은 RAG 버전이 “자료에서 확인되지 않습니다” 라고 답한다. 환각 억제가 RAG의 핵심 효과다.

agent vs RAG — 다른 축이다

헷갈렸던 부분. 둘은 배타적이지 않다.

 무엇에 관한 것
에이전트제어 흐름 — 다음에 뭘 할지 누가 정하나
RAG프롬프트 내용 — 뭘 넣고 물어보나

그래서 합칠 수 있다. 에이전트의 도구 중 하나를 search_knowledge_base(query) 로 만들면 모델이 필요할 때 스스로 자료를 검색해 쓰는 “RAG를 품은 에이전트”가 된다. 실무에서 제일 흔한 형태다.

정리

RAG는 마법이 아니라 “검색 결과 붙여넣기” 다. 임베딩+벡터DB는 검색 단계를 의미 기반으로 업그레이드하는 것일 뿐, RAG의 정의 자체는 아니다. 다음 편은 MCP — 도구/데이터를 여러 앱이 공유하도록 표준 규격으로 빼내는 방법.


© 2023 Lee. All rights reserved.