LLM 엔지니어링6장 · RAG 의 원리와 로컬 모델로 옮기기4 / 23← 이전목차다음 →MetaContents

6장. RAG 의 원리와 로컬 모델로 옮기기

RAG 의 세 단계

오픈북 시험에서는 문제를 읽고, 교재에서 그 내용이 있는 쪽을 찾고, 그 쪽을 펼쳐 옆에 둔 채 보며 답을 씁니다. 외운 것에만 기대지 않으니 교재에 있는 것은 틀리기 어렵습니다. RAG 가 이 방식입니다.

질문, 검색, 증강, 생성, 답의 다섯 상자를 화살표로 이은 RAG 흐름도

1번 질문이 들어오면, 2번 검색에서 문서 조각 가운데 관련된 것을 몇 개 고릅니다. 3번 증강에서 그 조각을 질문과 함께 프롬프트에 붙이고, 4번 생성에서 LLM 이 그것을 보고 답합니다. 여기서 교재가 문서, 쪽 찾기가 검색, 펼쳐 두기가 증강, 보고 쓰기가 생성입니다.

단계마다 하는 일

단계하는 일이 장에서 쓰는 것
준비문서를 조각으로 나눠 찾기 좋게 둔다빈 줄로 조항 나누기
검색질문과 가까운 조각 k 개를 고른다TF-IDF 키워드 검색
증강조각과 질문을 프롬프트에 붙인다[문서], [질문] 꼴의 프롬프트
생성LLM 이 답을 쓴다Qwen2.5(transformers, Ollama)

세 단계를 글자로만 흉내 내기

모델 없이 흐름만 보면 이렇습니다. 검색은 겹치는 글자 수, 생성은 아직 비워 둡니다.

Python
docs = ["제14조(결제 수단) 휴대폰 결제는 한 달에 50만원까지입니다.",
        "제16조(쿠폰) 주문 하나에 쿠폰은 한 장만 쓸 수 있습니다.",
        "제24조(고객센터) 평일 오전 9시부터 오후 6시까지 상담합니다."]
question = "휴대폰 결제 한도가 있나요?"

# 1) 검색: 질문의 글자 두 개짜리 조각이 문서에 몇 번 나오는지 센다
pieces = {question[i:i + 2] for i in range(len(question) - 1)}
scores = [sum(p in d for p in pieces) for d in docs]
best = docs[scores.index(max(scores))]

# 2) 증강: 찾은 조각을 질문과 함께 프롬프트로 만든다
prompt = f"[문서]\n{best}\n\n[질문]\n{question}"
print(scores)
print(prompt)
# 3) 생성: 이 prompt 를 LLM 에 넘긴다(실습 4부터)

점수는 [7, 2, 0] 으로, "휴대", "대폰", "결제" 같은 조각이 겹친 제14조가 뽑힙니다. 쿠폰 조항도 " 한", " 있"처럼 어디에나 나오는 조각이 겹쳐 2점을 얻습니다. 흔한 조각에 무게를 덜 주도록, 실습 3에서는 이 셈을 TF-IDF 로 다듬습니다.

코드에 나오는 함수

코드쉬운 뜻
question[i:i + 2]질문의 i 번째부터 두 글자를 잘라 낸다
{... for i in range(len(question) - 1)}한 칸씩 옮기며 자른 두 글자 조각을 모은다. 중괄호라서 같은 조각은 한 번만 남는다
sum(p in d for p in pieces)조각마다 문서 d 에 들어 있으면 1, 없으면 0 으로 세어 더한다. 곧 겹치는 조각의 수다
scores.index(max(scores))가장 높은 점수가 몇 번째에 있는지 찾는다
f"[문서]\n{best}\n\n[질문]\n{question}"찾은 조항과 질문을 정해진 꼴의 글로 이어 붙인다. \n 은 줄바꿈이다

이 코드가 뜻하는 것

오픈북 시험으로 다시 읽어 봅니다. 문제의 낱말을 쪽마다 대 보고, 가장 많이 겹치는 쪽을 펼쳐 문제 옆에 놓는 일입니다.

줄하는 일쉽게 말하면
pieces = {...}질문을 두 글자 조각으로 자른다문제에서 눈에 띄는 낱말을 뽑는다
scores = [...]조항마다 겹치는 조각 수를 센다쪽마다 그 낱말이 몇 개 나오나 센다
best = docs[...]점수가 가장 높은 조항을 고른다(검색)가장 잘 맞는 쪽을 고른다
prompt = f"..."조항과 질문을 한 글로 붙인다(증강)그 쪽을 문제 옆에 펼쳐 둔다
마지막 주석이 글을 LLM 에 넘기는 일(생성)은 실습 4부터 한다펼친 쪽을 보며 답을 쓴다

결과의 [7, 2, 0] 은 조항 셋의 점수이고, 7점인 제14조가 프롬프트에 붙었습니다. 이 코드만으로 RAG 의 앞 두 단계(검색, 증강)가 끝난 것입니다.

작게 줄여서 눈으로 보기

어떤 조각이 겹쳐서 점수가 났는지 직접 찍어 봅니다. 질문을 짧게 줄이고 조항도 둘만 둡니다.

Python
docs = ["휴대폰 결제는 한 달에 50만원까지입니다.",
        "쿠폰은 주문 하나에 한 장만 쓸 수 있습니다."]
question = "휴대폰 결제 한도"

# 질문을 두 글자씩 겹치게 자른 조각
pieces = sorted({question[i:i + 2] for i in range(len(question) - 1)})
print("질문 조각:", pieces)
# 문서마다 어떤 조각이 들어 있는지
for d in docs:
    print(d[:6], "→", [p for p in pieces if p in d])
출력
질문 조각: [' 결', ' 한', '결제', '대폰', '제 ', '폰 ', '한도', '휴대']
휴대폰 결제 → [' 결', ' 한', '결제', '대폰', '폰 ', '휴대']
쿠폰은 주문 → [' 한']
결과읽는 법
질문 조각 8개빈칸도 글자로 치므로 " 결", "폰 " 처럼 빈칸이 붙은 조각이 생긴다
휴대폰 조항의 6개"휴대", "대폰", "결제" 같은 뜻 있는 조각이 겹쳐 점수가 높다
쿠폰 조항의 1개뜻과 상관없는 " 한"(빈칸 다음의 "한")만 겹쳤다. 흔한 조각 때문에 엉뚱한 조항도 점수를 얻는다

"한도"는 두 조항 어디에도 없어서 아무 점수도 보태지 못했습니다. 글자가 겹쳐야만 찾는 검색의 한계가 여기서 보입니다.

주의할 점

  • 검색이 틀리면 생성도 틀립니다. 엉뚱한 쪽을 펼치면 아무리 좋은 모델도 답을 못 찾습니다. RAG 가 틀렸을 때는 검색 결과부터 봅니다.
  • 조각을 많이 붙일수록 답이 들어 있을 가능성은 커지지만, 프롬프트가 길어져 느려지고 모델이 헷갈리기 쉽습니다. 이 장은 3개를 붙입니다.
← 이전 절6장 · LLM 이 모르는 것다음 절 →6장 · RAG 와 미세조정