6장. 문서 기반 챗봇
RAG 란 무엇인가
시험 문제를 받았는데 책을 덮고 풀어야 하면, 기억나는 대로 쓰다가 그럴듯하게 지어내기도 합니다. 책을 펴 놓고 보는 오픈북 시험이면 다릅니다. 문제에 맞는 쪽을 찾아 펴고, 거기 적힌 대로 답을 씁니다. 답 옆에 "교과서 52쪽"이라고 적어 둘 수도 있습니다. RAG 는 모델에게 오픈북 시험을 보게 하는 방법입니다.
왼쪽 파랑 상자는 모델이 배운 것만으로 답하는 방식이고, 오른쪽 보라 상자는 질문에 맞는 문서 조각을 찾아 함께 건네는 방식입니다. 여기서 책이 우리 회사의 문서(여행 약관), 쪽을 찾아 펴는 일이 검색, 펴 놓은 쪽을 보며 답을 쓰는 일이 생성입니다. 비유와 다른 점은, 학생은 펴 놓은 쪽이 엉뚱하면 알아채고 다시 찾지만, 모델은 우리가 건넨 조각만 보고 답한다는 것입니다. 그래서 RAG 의 답은 검색이 얼마나 맞는 조각을 찾았는지에 크게 기댑니다.
모델만으로 답하기와 RAG 견주기
| 견줄 것 | 모델만으로 답하기 | RAG |
|---|---|---|
| 답의 바탕 | 모델이 배운 글 | 우리가 건넨 문서 조각 |
| 우리 회사 규정 | 모른다(배운 적이 없다) | 문서에 있으면 안다 |
| 규정이 바뀌면 | 모델을 다시 가르쳐야 한다 | 문서만 바꿔 다시 색인한다 |
| 근거 보여 주기 | 어렵다 | 찾은 조각의 쪽과 조항을 보여 준다 |
| 지어낼 위험 | 크다(1장의 환각) | 줄어든다. 검색이 빗나가면 여전히 있다 |
| 더 드는 것 | 없음 | 색인 만들기, 검색 한 번, 프롬프트가 길어진다 |
짧은 예시
모델은 부르지 않고, 프롬프트에 약관 조각을 넣었을 때와 넣지 않았을 때 모델이 받을 메시지를 견줍니다. 조각은 PDF 에서 "반려동물"이라는 낱말로 찾았습니다. 이 장의 약관은 연습용으로 지어낸 가상 문서입니다.
from langchain_community.document_loaders import PyPDFLoader
from langchain_core.prompts import ChatPromptTemplate
question = "반려동물을 데려가도 되나요?"
# PDF 를 쪽마다 읽고, "반려동물"이 든 줄과 그다음 줄을 근거로 꺼낸다
pages = PyPDFLoader("travel_terms.pdf").load()
lines = [ln for p in pages for ln in p.page_content.split("\n")]
at = next(i for i, ln in enumerate(lines) if ln.startswith("제12조"))
context = " ".join(lines[at: at + 3])
closed = ChatPromptTemplate.from_messages([("human", "{question}")])
openbook = ChatPromptTemplate.from_messages([
("system", "아래 약관만 보고 답하라. 약관에 없으면 모른다고 답하라.\n\n[약관]\n{context}"),
("human", "{question}"),
])
for name, prompt in [("책을 덮고", closed), ("오픈북", openbook)]:
msgs = prompt.invoke({"question": question, "context": context}).to_messages()
print(f"[{name}] 메시지 {len(msgs)}개")
for m in msgs:
print(" ", m.type, "|", m.content)[책을 덮고] 메시지 1개
human | 반려동물을 데려가도 되나요?
[오픈북] 메시지 2개
system | 아래 약관만 보고 답하라. 약관에 없으면 모른다고 답하라.
[약관]
제12조 반려동물 반려동물은 '함께 여행' 메뉴의 동반 상품에서만 데려갈 수 있다. 몸무게 7kg 이하의 개와 고양이만 되며, 이동장에 넣어야 한다.
human | 반려동물을 데려가도 되나요?책을 덮은 쪽은 질문 하나만 갑니다. 모델은 구름다리여행의 규정을 배운 적이 없으니 일반적인 이야기를 하거나 지어냅니다. 오픈북 쪽은 시스템 메시지 안에 제12조가 함께 갑니다. 모델은 "7kg 이하의 개와 고양이, '함께 여행' 메뉴"라는 근거를 보고 답할 수 있습니다. 이 장의 나머지는 이 근거를 낱말 찾기 대신 검색으로 알맞게 골라 오는 법입니다.
코드가 의미하는 것
이 코드는 같은 학생에게 책을 덮고 보는 시험지와 오픈북 시험지를 차례로 내미는 장면입니다. 책에서 쪽을 찾아 펴고(PDF 읽기와 근거 꺼내기), 시험지 두 장을 만들고(프롬프트 두 개), 각 시험지가 모델에게 어떤 메시지로 가는지 펼쳐 봅니다.
| 코드 | 하는 일 | 챗봇에서 뜻하는 것 |
|---|---|---|
PyPDFLoader("travel_terms.pdf").load() | PDF 를 쪽마다 읽는다 | 챗봇이 기댈 책을 손에 드는 일입니다. 모델이 배운 적 없는 회사 문서가 여기서 들어옵니다 |
ln.startswith("제12조") 와 lines[at: at + 3] | "제12조"로 시작하는 줄부터 세 줄을 근거로 꺼낸다 | 사람이 쪽을 알고 직접 편 것입니다. RAG 에서는 이 자리를 검색기가 맡습니다 |
{context} 가 든 시스템 메시지 | 근거를 넣을 빈칸을 시스템 지시 안에 둔다 | 오픈북 시험지의 "아래 약관만 보고 답하라"입니다. 근거와 "모르면 모른다"는 규칙이 함께 갑니다 |
prompt.invoke({...}).to_messages() | 빈칸을 채워 실제로 보낼 메시지 목록을 만든다 | 모델을 부르지 않고도 모델이 받을 글을 미리 볼 수 있습니다. RAG 를 점검할 때 가장 먼저 보는 곳입니다 |
결과에서 읽을 것은 두 가지입니다.
- 메시지 수가 1개와 2개로 갈립니다. 책을 덮은 쪽은 질문만 가고, 오픈북 쪽은 제12조가 든 시스템 메시지가 하나 더 갑니다. RAG 가 모델에 더하는 것은 이 메시지 한 칸입니다.
- 모델은 그대로이고 프롬프트만 길어졌습니다. 두 쪽 모두 같은 질문 "반려동물을 데려가도 되나요?"를 보냅니다. 답이 달라질 바탕은 오직 함께 보낸 "7kg 이하의 개와 고양이" 같은 글입니다.
주의할 점
- RAG 는 모델을 다시 가르치는 것이 아닙니다. 요청마다 문서 조각을 프롬프트에 끼워 보낼 뿐이라, 모델 자체는 그대로입니다.
- 위 예시는 "제12조"라는 줄을 사람이 알고 골랐습니다. 손님은 "강아지"라고 묻지 "제12조"라고 묻지 않습니다. 질문의 뜻으로 알맞은 조각을 찾는 것이 RAG 의 어려운 부분입니다.