LangChain 챗봇6장 · 문서 기반 챗봇4 / 21← 이전목차다음 →MetaContents

6장. 문서 기반 챗봇

RAG 파이프라인의 두 단계

도서관을 생각해 봅시다. 새 책이 들어오면 사서는 책을 읽고, 분류 번호를 매기고, 서가의 알맞은 자리에 꽂습니다. 이 일은 책이 들어올 때 한 번만 합니다. 이용자가 "강아지 키우는 법 책 있어요?"라고 물으면, 사서는 분류 번호로 서가를 찾아 책 몇 권을 뽑아 오고, 이용자는 그 책을 읽고 답을 얻습니다. RAG 파이프라인(pipeline, 일이 차례로 흘러가는 관)도 미리 하는 단계와 물을 때마다 하는 단계로 나뉩니다.

위 줄은 읽기, 나누기, 임베딩, 저장의 색인 단계이고, 아래 줄은 질문 임베딩, 검색, 프롬프트에 넣기, 답 쓰기의 질문 단계인 두 줄 흐름도

위 줄(파랑 화살표)이 색인 단계입니다. PDF 를 읽고, 조각으로 나누고, 조각마다 임베딩을 만들어 벡터DB 에 저장합니다. 아래 줄(하늘 화살표)이 질문 단계입니다. 질문도 임베딩으로 바꾸고, 벡터DB 에서 가까운 조각을 찾고, 그 조각을 프롬프트에 넣어 모델이 답을 씁니다. 두 줄을 잇는 점선은 질문 단계가 색인 단계에서 만든 벡터DB 를 꺼내 쓴다는 뜻입니다. 여기서 분류 번호가 임베딩, 서가가 벡터DB, 책을 뽑아 오는 사서가 검색기, 책을 읽고 답하는 이용자가 모델입니다. 비유와 다른 점은, 사서는 책 한 권을 통째로 뽑지만 RAG 는 책을 미리 몇 쪽짜리 조각으로 잘라 두고 조각 단위로 뽑는다는 것입니다.

단계마다 쓰는 부품

단계하는 일이 장에서 쓰는 것언제
읽기PDF 를 쪽마다 Document 로PyPDFLoader문서가 바뀔 때 한 번
나누기쪽을 조각으로RecursiveCharacterTextSplitter, 조항 단위 나누기문서가 바뀔 때 한 번
임베딩조각을 숫자 목록으로BigramEmbeddings(연습용), OpenAIEmbeddings문서가 바뀔 때 한 번
저장숫자 목록을 색인으로FAISS.from_documents, save_local문서가 바뀔 때 한 번
검색질문과 가까운 조각 k 개as_retriever(search_kwargs={"k": 3})질문마다
답 쓰기조각을 넣은 프롬프트로 답RetrievalQA, LCEL 체인질문마다

짧은 예시

세 문장짜리 작은 "문서"로 두 단계를 끝까지 한 번 돌려 봅니다. 임베딩은 다음다음 절에서 볼 연습용 BigramEmbeddings 를 미리 씁니다. 키가 필요 없습니다.

Python
import zlib

from langchain_community.vectorstores import FAISS
from langchain_core.embeddings import Embeddings


class BigramEmbeddings(Embeddings):
    """글자 두 개 묶음(바이그램)을 세어 숫자 목록을 만드는 연습용 임베딩"""

    def __init__(self, size=512):
        self.size = size

    def _vector(self, text):
        text = text.replace(" ", "").replace("\n", "")
        vec = [0.0] * self.size
        for i in range(len(text) - 1):
            # 글자 두 개를 crc32 로 0~511 사이의 칸 번호로 바꿔 그 칸을 1 올린다
            vec[zlib.crc32(text[i:i + 2].encode("utf-8")) % self.size] += 1.0
        norm = sum(v * v for v in vec) ** 0.5 or 1.0
        return [v / norm for v in vec]

    def embed_documents(self, texts):
        return [self._vector(t) for t in texts]

    def embed_query(self, text):
        return self._vector(text)


# 색인 단계: 글 → 임베딩 → 벡터DB (여기서는 나누기를 건너뛰고 문장 하나를 조각 하나로 본다)
texts = ["출발 10일 전까지 취소하면 전액 환불한다.", "국내선은 15kg 짐 한 개가 포함된다.", "여권은 6개월 이상 남아야 한다."]
db = FAISS.from_texts(texts, BigramEmbeddings())

# 질문 단계: 질문 → 임베딩 → 가까운 조각 → 프롬프트에 넣을 글
question = "짐은 몇 kg 까지 되나요?"
found = db.similarity_search(question, k=1)
print("찾은 조각:", found[0].page_content)
print("프롬프트에 넣을 글:", f"[약관] {found[0].page_content}\n[질문] {question}")
출력
찾은 조각: 국내선은 15kg 짐 한 개가 포함된다.
프롬프트에 넣을 글: [약관] 국내선은 15kg 짐 한 개가 포함된다.
[질문] 짐은 몇 kg 까지 되나요?

세 문장 가운데 "짐"과 "kg"이 겹치는 둘째 문장을 찾았습니다. 이 글을 프롬프트에 넣어 모델에 보내면 질문 단계가 끝납니다. 이 장의 실습은 이 짧은 코드의 칸마다 진짜 PDF, 나누기, 저장과 불러오기, 모델을 하나씩 채워 넣습니다.

코드가 의미하는 것

이 코드는 도서관 사서의 하루를 아주 작게 줄인 것입니다. 책 세 권에 분류 번호를 매겨 서가에 꽂고(색인 단계), 이용자의 질문에 맞는 책 한 권을 뽑아 건넵니다(질문 단계).

코드하는 일챗봇에서 뜻하는 것
class BigramEmbeddings(Embeddings)embed_documents 와 embed_query 두 메서드를 가진 임베딩을 만든다분류 번호를 매기는 규칙입니다. 이 두 메서드만 있으면 LangChain 의 벡터DB 에 끼울 수 있습니다
FAISS.from_texts(texts, BigramEmbeddings())글마다 임베딩을 만들어 FAISS 색인에 넣는다색인 단계 전체가 이 한 줄입니다. 책이 들어올 때 한 번만 하는 일입니다
db.similarity_search(question, k=1)질문을 같은 임베딩으로 바꿔 가장 가까운 글 1개를 찾는다질문 단계의 검색입니다. k 는 사서가 뽑아 올 책의 권수입니다
f"[약관] …\n[질문] {question}"찾은 글과 질문을 한 덩어리 글로 잇는다모델에 보낼 프롬프트입니다. 이 예시는 모델을 부르지 않고 여기서 멈춥니다

결과에서 읽을 것은 두 가지입니다.

  • 글자가 겹친 문장이 뽑혔습니다. 찾은 조각은 "국내선은 15kg 짐 한 개가 포함된다."입니다. 질문과 "짐", "kg"이 겹치기 때문이고, 연습용 임베딩은 글자 겹침으로 가깝고 먼 것을 정합니다.
  • 프롬프트에는 찾은 조각만 들어갑니다. 나머지 두 문장(환불, 여권)은 모델에게 가지 않습니다. 모델이 볼 수 있는 근거는 검색이 고른 것뿐입니다.

주의할 점

  • 색인 단계는 문서가 바뀔 때만 다시 합니다. 질문마다 PDF 를 다시 읽고 임베딩을 다시 만들면 시간과 (진짜 임베딩이면) 요금이 질문 수만큼 듭니다. 그래서 만든 색인을 파일로 저장해 둡니다.
  • 색인을 만들 때와 질문을 임베딩할 때는 같은 임베딩을 써야 합니다. 서로 다른 임베딩의 숫자는 같은 지도 위의 점이 아니라서 거리를 재는 뜻이 없습니다.
← 이전 절6장 · RAG 란 무엇인가다음 절 →6장 · PDF 문서 읽기