ansir 님의 블로그

[ SK 네트웍스 Family AI 캠프 수업 내용 복습 ] RAG Langchain 2025-05-12 본문

SK 네트웍스 family AI 캠프/수업 내용 복습

[ SK 네트웍스 Family AI 캠프 수업 내용 복습 ] RAG Langchain 2025-05-12

ansir 2025. 5. 12. 18:09

RAG와 LangChain을 이용한 한글 문서 기반 대화형 챗봇

1. RAG (Retrieval-Augmented Generation)

정의:
RAG는 외부 지식을 활용하여 LLM의 응답 생성을 보완하는 기술로, 사용자의 질문에 직접적으로 답변하기 위해 관련 문서를 검색한 뒤 해당 문서를 바탕으로 답변을 생성합니다.

핵심 구성 요소:

  • 문서 검색 (Retrieval):
    사용자의 질문과 의미적으로 유사한 문서를 벡터 데이터베이스에서 검색합니다.
  • 문서 임베딩 (Embedding):
    문서를 벡터로 변환하여 의미적 유사성 기반 검색이 가능하게 합니다.
    (예: 한글 문장은 다국어 임베딩 모델 또는 KoBERT, KoSimCSE 등으로 처리)
  • 답변 생성 (Generation):
    검색된 문서를 LLM에 전달하여 질문에 맞는 맥락적 답변을 생성합니다.

장점:

  • LLM을 새로 fine-tuning 하지 않아도 됨.
  • 문서만 업데이트하면 정보 갱신 가능.
  • 최신 정보 반영이 쉬움.
  • 외부 지식 기반이므로 **모델의 환각(hallucination)**을 줄일 수 있음.

한글 처리 시 고려사항:

  • 한글은 띄어쓰기/형태소 처리 문제가 있어, 적절한 임베딩 모델 선택이 중요함.
  • 다국어 지원 모델(MiniLM, LaBSE 등) 또는 한국어 특화 임베딩 모델(KoBERT, KoSimCSE 등)을 사용하는 것이 좋음.

2. LangChain

정의:
LangChain은 LLM 기반 애플리케이션을 쉽게 구축할 수 있도록 도와주는 오픈소스 프레임워크입니다.

주요 기능:

  • Prompt Templates:
    프롬프트를 동적으로 생성할 수 있도록 지원. 예: 사용자 질문에 따라 자동으로 프롬프트 구성.
  • Memory:
    이전 대화 내용을 기억하여 대화의 맥락을 유지할 수 있게 함.
    예: 사용자가 "그 사람은 누구야?"라고 물어봤을 때, 이전에 언급된 인물을 기억하고 답변 가능.
  • Chains:
    여러 처리 단계를 **하나의 작업 흐름(Chain)**으로 구성 가능.
    예: 검색 → 프롬프트 생성 → 응답 생성 등의 단계 통합.
  • Retrieval:
    벡터 DB나 문서에서 외부 정보를 검색하는 기능 내장. RAG 구현 시 필수.
  • Document Loading 및 청크 분할:
    • TextLoader 등으로 텍스트 문서를 로드.
    • 로드된 문서는 Document 객체로 변환됨.
    • 이후 문서를 일정 길이로 나눔(청크 단위 분할)하여 처리 효율성 향상 및 LLM 입력 토큰 제한에 대응.

기타 장점:

  • 한글 지원이 잘 되며, OpenAI, HuggingFace 등의 API와 쉽게 연동 가능함.
  • 다양한 로더, 임베딩 모델, 벡터스토어(Faiss, Pinecone 등)와 호환.

 


# LangChain 및 관련 커뮤니티 패키지 설치
!pip install langchain -q
!pip install -U langchain-community

# 필요한 모듈 불러오기
from langchain.document_loaders import TextLoader                   # 텍스트 파일 로더
from langchain.text_splitter import CharacterTextSplitter, RecursiveCharacterTextSplitter  # 문서 분할기
from langchain_openai import OpenAIEmbeddings           # OpenAI 임베딩 사용
from langchain.vectorstores import Chroma                          # Chroma 벡터 데이터베이스

# 문서 로드: 텍스트 파일을 Document 객체로 불러오기
loader = TextLoader("/content/korea_culture.txt")
documents = loader.load()  # 파일 내 모든 텍스트를 읽고 Document 객체 리스트로 반환
documents  # 불러온 문서 확인

# 문서 분할: 긴 문서를 작은 청크로 나누기 (LLM 입력 한계 대비)
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=100,           # 청크 하나의 최대 길이
    chunk_overlap=50,         # 청크 간 중복 길이 (문맥 유지용)
    separators=['\\n\\n', '\\n', '.', '!', '?', ',', ' '],  # 가능한 분할 기준
    keep_separator=True       # 분할 기준 문자 포함 여부
)

# 문서 분할 실행: Document 객체 리스트를 분할해 청크 리스트 생성
texts = text_splitter.split_documents(documents)
texts  # 분할된 청크 확인

# 문서 통계 출력: 전체 텍스트 길이와 주요 구분점 개수 확인
with open("/content/korea_culture.txt", encoding="utf-8") as f:
    text = f.read()
    backslash = '\\n'
    print(f"문서 길이: {len(text)}")
    print(f"줄바꿈 수: {text.count(backslash)}")
    print(f"구분점 수: {text.count('.')}")


코드 설명

1. 문서 로딩

loader = TextLoader("/content/korea_culture.txt")
documents = loader.load()
  • TextLoader를 이용해 텍스트 파일을 읽고 LangChain의 Document 형식으로 변환합니다.
  • Document는 LangChain의 문서 처리 파이프라인에서 표준 입력 형식입니다.

2. 문서 분할

text_splitter = RecursiveCharacterTextSplitter(...)
texts = text_splitter.split_documents(documents)
  • RecursiveCharacterTextSplitter는 문장을 자연스러운 단위로 분할하려고 여러 구분자를 재귀적으로 적용합니다.
  • chunk_size=100, chunk_overlap=50 설정은 하나의 청크가 100자 이내이되 앞뒤 청크가 50자 정도 겹치도록 설정합니다. 이는 LLM이 청크 간 문맥을 더 잘 이해하도록 돕습니다.

3. 원문 정보 확인

with open(...) as f:
    ...
  • 문서 전체의 문자 수, 줄바꿈 수(\\n), **구분점 수('.')**를 출력해 텍스트 구조를 파악합니다.
  • 이는 문서의 청크 분할 전략을 설정할 때 참고 정보가 됩니다.

보충 설명

  • 위 단계까지 진행되면 청크 단위의 문서 벡터화 및 저장을 다음 단계로 진행할 수 있습니다.
  • 일반적으로 다음 과정은 다음과 같습니다:
    1. OpenAIEmbeddings 또는 다른 임베딩 모델을 이용해 texts를 벡터화
    2. Chroma.from_documents(...)를 통해 벡터 DB에 저장
    3. retriever 설정 후 RAG 응답 체인 구성

 

# 함수로 변환
def load_and_seperare_document(texts="", chunk_size=100, chunk_overlap=10):
  # 문서 로드
  loader = TextLoader("/content/korea_culture.txt")
  documents = loader.load()

  # 문서 내용 및 길이 확인
  text = documents[0].page_content
  backslash = '\\n'
  print(f"문서 길이: {len(text)}")
  print(f"줄바꿈 수: {text.count(backslash)}")
  print(f"구분점 수: {text.count('.')}")

  # 문서 분할
  text_splitter = RecursiveCharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap,
                                                separators=['\\n\\n', '\\n', '.', '!', '?', ',', ' '],
                                                keep_separator=True)
  texts = text_splitter.split_documents(documents) # rangchain으로 불러온 문서를 넣어야 함
      
  # 분할된 청크 반환
  print(f"분할된 청크 수: {len(texts)}")
  for idx, chunk in enumerate(texts):
    print(f"청크 {idx+1}: {chunk.page_content}")
  return texts

 


RAG 구현 상세 정리

1. 벡터 데이터베이스 (Vector Database)

RAG에서는 질문과 문서를 의미적 벡터로 변환하고, 이 벡터들을 저장하고 검색하기 위해 벡터 데이터베이스를 사용합니다.

  • Chroma:
    • 경량화된 벡터 DB로 빠르고 설치가 간편함.
    • LangChain과의 호환성이 뛰어나며, 실습 및 프로토타입 구현에 적합.
    • 로컬 환경에서 테스트 용도로 사용하기 적절함.

2. 임베딩 (Embedding)

문서와 질문을 LLM이 이해할 수 있도록 벡터 형태로 변환하는 작업입니다.

  • OpenAIEmbeddings:
    • OpenAI에서 제공하는 임베딩 모델.
    • 다국어 지원이 잘 되어 있어 한글 문서 처리에 적합.
    • 예: text-embedding-ada-002 모델은 다양한 언어의 문장을 높은 품질로 벡터화 가능.

3. 검색 (Retrieval)

  • 사용자 질문 또한 임베딩을 통해 벡터로 변환됩니다.
  • 이 벡터는 문서 벡터들과의 유사도 검색(cosine similarity 등)을 통해 관련 문서를 찾아냅니다.
  • 일반적으로 가장 유사한 상위 N개의 문서(예: top-3)를 반환합니다.

4. RetrievalQA 체인

LangChain에서는 이 모든 과정을 하나의 체인으로 묶어 관리할 수 있습니다.

RetrievalQA의 동작 흐름:

  1. 사용자 질문 입력
  2. 질문을 벡터화
  3. 벡터 데이터베이스에서 관련 문서 검색
  4. 검색된 문서를 프롬프트와 함께 LLM에 전달
  5. LLM이 문서 기반으로 맥락적 답변 생성

예시 구조:

from langchain.chains import RetrievalQA
from langchain.vectorstores import Chroma
from langchain.embeddings import OpenAIEmbeddings
from langchain.chat_models import ChatOpenAI

# 1. 임베딩 모델 정의
embedding = OpenAIEmbeddings()

# 2. 벡터 DB 로드
vectordb = Chroma(persist_directory="db", embedding_function=embedding)

# 3. 검색기 정의
retriever = vectordb.as_retriever()

# 4. LLM 정의
llm = ChatOpenAI(temperature=0)

# 5. RetrievalQA 체인 생성
qa_chain = RetrievalQA.from_chain_type(llm=llm, retriever=retriever)

# 6. 질문에 대한 응답 생성
result = qa_chain.run("질문 입력")

요약

구성 요소 설명
Chroma 가볍고 빠른 벡터 DB, LangChain과 잘 호환됨
OpenAIEmbedding 다국어 지원 임베딩 모델로 질문 및 문서 벡터화
유사도 검색 질문 벡터와 문서 벡터의 의미 유사도를 계산하여 관련 문서 검색
RetrievalQA 체인 검색 → 응답 생성 전체 과정을 통합한 LangChain 체인 구조

 


# 필수 패키지 설치
!pip install langchain -q
!pip install langchain_openai -q
!pip install chromadb

# 필요한 모듈 임포트
from langchain_openai import OpenAI                           # OpenAI LLM 사용
from langchain_openai import OpenAIEmbeddings                 # OpenAI 임베딩 사용
from langchain.vectorstores import Chroma                     # Chroma: 벡터 저장소
from langchain.chains import RetrievalQA                      # Retrieval 기반 QA 체인

# LLM 초기화 (환경 변수에서 API 키를 가져옴)
llm = OpenAI(openai_api_key=os.environ["OPENAI_API_KEY"])

# 임베딩 모델 초기화
embeddings = OpenAIEmbeddings()

# 문서 로드 및 청크 분할 함수 호출 (texts는 분할된 Document 객체 리스트여야 함)
texts = load_and_seperare_document()  # 사용자 정의 함수라고 가정됨

# 벡터 저장소 생성 및 문서 저장 (Chroma는 내부적으로 persist 처리함)
vectorstore = Chroma.from_documents(texts, embeddings, persist_directory='./db')

# Retrieval QA 체인 생성
qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",  # 문서가 작으므로 전체를 LLM에 전달 (기본 체인 유형)
    retriever=vectorstore.as_retriever(search_kwargs={'k': 2}),  # 유사도 상위 2개 문서 검색
    return_source_documents=True  # 응답에 참조 문서 포함
)

# 사용자 질문
query = "한국이 세계적으로 영향력을 끼치고 있는 분야는 무엇인가요?"
result = qa_chain({"query": query})

# 응답 출력
print(f"질문: {query}")
print(f"답변: {result['result']}")
print(f"참조 문서: {[doc.page_content for doc in result['source_documents']]}")

# 참고용: RAG 없이 단순 LLM 호출
llm.invoke(query)

전체 기능 설명

단계 설명
1. 문서 로드 및 분할 문서를 작은 청크로 분할해 LLM의 입력 제한에 대응
2. 임베딩 및 벡터화 각 청크를 OpenAI Embedding을 이용해 벡터로 변환
3. Chroma 저장소 구성 청크 임베딩을 Chroma DB에 저장
4. 질의 처리 (RAG) 사용자의 질의도 벡터화 후, 벡터 유사도 기반으로 관련 문서 검색
5. 응답 생성 검색된 문서를 LLM에 넣어 답변 생성
6. 결과 출력 생성된 응답과 함께 참조 문서도 출력
7. 비교 (Optional) 동일한 질문을 RAG 없이 LLM에게 직접 질의하여 결과 비교
 

보충 설명

  • chain_type="stuff":
    • 문서 분할 없이 모든 텍스트를 한 번에 넣는 방식. 문서가 작거나 소량일 때 적합.
    • 문서가 많아지면 "map_reduce", "refine" 체인을 쓰는 것이 바람직합니다.
  • vectorstore.persist():
    • Chroma 0.4.x 이후는 명시적 .persist() 없이도 자동 저장됩니다.
    • persist_directory를 지정하면 로컬 디렉토리에 임베딩 DB가 생성됨.

 


RAG 구현 순서 (LangChain 기반, 한글 문서 지원)

1. 문서 선택

  • 다양한 형식의 문서를 지원: .txt, .pdf, .docx, .html 등
  • LangChain에서는 다양한 로더를 통해 문서 로드 가능
    • TextLoader – 일반 텍스트 파일
    • PyPDFLoader – PDF 파일
    • UnstructuredFileLoader – 복잡한 문서 구조를 파싱
  • 이 단계에서 문서를 Document 객체로 변환 (LangChain 내부 형식)

보충 설명

  • 문서 로딩 시 인코딩 오류 방지와 같은 전처리가 중요
  • 문서가 크면 이후 단계를 위해 청크(조각) 단위 분할이 필요함

2. 벡터 데이터베이스 생성 – RAG의 Retrieval 파트

  • 문서의 의미를 담은 임베딩(embedding) 벡터 생성
    • OpenAIEmbedding, HuggingFaceEmbedding 등 사용 가능
    • 한글 지원이 필요한 경우 다국어 임베딩 모델 사용 권장
  • 벡터를 저장할 벡터 저장소(vectorstore) 구성
    • 예: Chroma, FAISS, Weaviate 등
    • Chroma는 로컬 개발에 적합하고 LangChain과 호환성 좋음

처리 과정

  • 문서를 일정 크기(chunk_size)로 나눔
  • 각 청크를 벡터로 임베딩
  • 벡터 저장소에 저장 (저장소는 검색 기능도 함께 제공)

3. LangChain 기반 QA 시스템 구성 – RAG의 Generation 파트

  • Retrieval + LLM을 조합해 RAG QA 시스템 구성
    • LangChain의 RetrievalQA 체인 사용
  • 처리 흐름:
    1. 사용자의 질문을 임베딩
    2. 질문 벡터와 유사한 문서 청크를 벡터 저장소에서 검색
    3. 검색된 문서들을 LLM에게 전달
    4. 문맥을 고려해 답변 생성

보충 설명

  • chain_type:
    • "stuff": 검색 문서를 한 번에 LLM에 전달 (문서가 적을 때)
    • "map_reduce": 문서가 많을 때 병렬 처리 후 요약
    • "refine": 초기 응답을 개선하는 방식

전체 요약 흐름도

[문서 로드 및 분할]
     ↓
[문서 임베딩 → 벡터 저장소 생성]
     ↓
[질문 입력 → 질문 임베딩]
     ↓
[유사 문서 검색 (벡터 유사도)]
     ↓
[검색 문서를 기반으로 LLM 응답 생성]

 


# 회사 규정을 문자열로 정의하고 텍스트 파일로 저장
company_rules = '''
우리 회사의 정규직 전환은 수습기간 4개월후 평가를 거쳐 결정된다.
근태관리 기준은 8출근 5퇴근을 기준으로 한다
금요일은 전사가 오전 근무만 한다
재택근무는 본인이 원하면 누구의 승인도 받지 않고 자유롭게 실시한다
'''
with open("/content/company_rule.txt", "w") as f:
    f.write(company_rules)

# OpenAI 임베딩 객체 생성 (다국어 포함 의미 임베딩 생성용)
embeddings = OpenAIEmbeddings()

# 문서 로드 및 청크 분할 함수 호출 (문서명, 청크 크기, 중첩 크기)
texts = load_and_seperare_document("company_rule.txt", 50, 20)

# 분할된 문서를 Chroma 벡터 DB에 저장 (persist_directory는 저장 디렉터리)
vectorstore = Chroma.from_documents(texts, embeddings, persist_directory='./db')

# 문서 검색 및 응답 생성을 위한 RetrievalQA 체인 구성
qa_chain = RetrievalQA.from_chain_type(
    llm=llm,  # 사용 중인 LLM (OpenAI 모델)
    chain_type="stuff",  # 작은 문서일 경우 문서 전체를 한번에 전달
    retriever=vectorstore.as_retriever(search_kwargs={'k': 2}),  # 유사 문서 상위 2개 검색
    return_source_documents=True  # 참조된 문서 반환
)

# 질의 정의
query = "우리회사의 재택근무는 어떻게 되나요?"

# 체인 실행 → 검색된 문서를 바탕으로 LLM이 답변 생성
result = qa_chain({"query": query})

# 결과 출력
print(f"질문: {query}")
print(f"답변: {result['result']}")
print(f"참조 문서: {[doc.page_content for doc in result['source_documents']]}")
문서 길이: 122
줄바꿈 수: 5
구분점 수: 1
분할된 청크 수: 3
청크 1: 우리 회사의 정규직 전환은 수습기간 4개월후 평가를 거쳐 결정된다.
청크 2: 근태관리 기준은 8출근 5퇴근을 기준으로 한다
금요일은 전사가 오전 근무만 한다
청크 3: 재택근무는 본인이 원하면 누구의 승인도 받지 않고 자유롭게 실시한다

질문: 우리회사의 재택근무는 어떻게 되나요?
답변:  우리 회사의 재택근무는 본인의 승인 없이 자유롭게 실시됩니다.
참조 문서: ['우리 회사의 재택근무는 본인이 원하면 누구의 승인도 받지 않고 자유롭게 실시한다', '우리 회사의 재택근무는 본인이 원하면 누구의 승인도 받지 않고 자유롭게 실시한다', '우리 회사의 재택근무는 본인이 원하면 누구의 승인도']

전체 코드 설명

이 코드는 회사 내부 규정을 담은 텍스트 파일을 기반으로 RAG(Retrieval-Augmented Generation) 방식의 챗봇을 구현합니다. 주요 흐름은 다음과 같습니다:

  1. 회사 규정 작성 및 저장
    • 규정 내용을 company_rule.txt로 저장
  2. 문서 로드 및 분할
    • 텍스트 파일을 로드한 뒤, 의미 단위로 청크(chunk) 분할
    • load_and_seperare_document(filename, chunk_size, overlap) 함수 사용
  3. 벡터 저장소 생성
    • 각 청크를 임베딩(벡터화)하고, Chroma 벡터 데이터베이스에 저장
    • 이후 검색 가능
  4. RetrievalQA 체인 구성
    • 유저 질문을 임베딩하여, 관련 문서 검색
    • 검색된 문서를 LLM(OpenAI)에 전달하여 응답 생성
    • chain_type="stuff": 작은 문서라 전체를 한 번에 LLM에 전달
  5. 질문 처리 및 응답 출력
    • 실제 질문 "우리회사의 재택근무는 어떻게 되나요?"에 대해 관련 규정을 찾아 자연어 답변 생성

추가 보충

  • persist_directory='./db'는 로컬에 벡터 DB를 저장하여 재사용 가능하게 합니다.
  • return_source_documents=True는 답변에 근거한 원문(출처)을 제공하므로 신뢰성과 검증 가능성을 높여줍니다.

 


LangChain으로 다양한 문서 포맷 불러오기

PDF 파일

!pip install pymupdf

# pdf 파일
from langchain.document_loaders import PyMuPDFLoader
loader = PyMuPDFLoader("/content/company_rules.pdf")
pages = loader.load()  # 페이지 단위로 문서를 분리
pages

word 파일

!pip install unstructured
!pip install python-docx

# 워드문서 로드
from langchain.document_loaders import UnstructuredWordDocumentLoader
loader = UnstructuredWordDocumentLoader("/content/company_rules.docx")
pages = loader.load()  # 페이지 단위로 문서를 분리
pages

csv 파일

# csv 로드
from langchain.document_loaders.csv_loader import CSVLoader
loader = CSVLoader("example.csv")
pages = loader.load()
pages

웹 페이지 로드

# 웹 페이지 로드
from langchain.document_loaders import WebBaseLoader
loader = WebBaseLoader("<https://www.ytn.co.kr/>")
pages = loader.load()
pages

 


RAG 기반 FAQ 구현

# 필수 라이브러리 설치
!pip install -U langchain-community -q  # LangChain 커뮤니티 버전 설치
!pip install langchain_openai -q  # OpenAI 모델과 LangChain을 통합하는 라이브러리 설치
!pip install chromadb  # Chroma 벡터 저장소 설치
!pip install pymupdf  # PDF 파일 처리를 위한 PyMuPDF 라이브러리 설치

# 필수 모듈 임포트
from langchain.document_loaders import TextLoader, PyMuPDFLoader  # 텍스트 및 PDF 파일 로드용 로더
from langchain.text_splitter import RecursiveCharacterTextSplitter  # 텍스트 분할용 텍스트 스플리터
from langchain.embeddings import OpenAIEmbeddings  # OpenAI 임베딩을 생성하는 모듈
from langchain.vectorstores import Chroma  # 벡터 저장소 Chroma
from langchain_openai import OpenAI, OpenAIEmbeddings  # OpenAI 모델 및 임베딩 사용을 위한 래퍼
from langchain.chains import RetrievalQA  # 검색 기반 QA 체인 생성 모듈
from langchain.prompts import PromptTemplate  # 프롬프트 템플릿을 만들기 위한 모듈
import os  # 운영 체제 관련 기능을 위한 표준 라이브러리

# OpenAI API 키 환경 변수 설정
os.environ["OPENAI_API_KEY"] = "private api key"  # OpenAI API를 사용할 때 필요한 API 키 설정

코드 설명

  1. 라이브러리 설치:
    • langchain-community: LangChain의 커뮤니티 버전 라이브러리로, 다양한 기능을 제공합니다.
    • langchain_openai: OpenAI 모델을 LangChain 프레임워크와 통합하는 데 필요한 라이브러리입니다.
    • chromadb: Chroma는 벡터 저장소로, 임베딩된 문서를 벡터 형태로 저장하고 검색하는 데 사용됩니다.
    • pymupdf: PDF 파일에서 텍스트를 추출하고 처리하는 데 사용되는 라이브러리입니다.
  2. 모듈 임포트:
    • TextLoader, PyMuPDFLoader: 텍스트 파일과 PDF 파일을 로드하는 데 사용되는 로더입니다.
    • RecursiveCharacterTextSplitter: 긴 문서를 일정한 크기의 청크로 분할하여, 각 청크를 벡터화하고 검색에 사용할 수 있도록 합니다.
    • OpenAIEmbeddings: OpenAI 임베딩을 생성하여 텍스트를 벡터화하는 기능을 제공합니다.
    • Chroma: 텍스트 벡터를 저장하고 검색하는 벡터 저장소입니다.
    • RetrievalQA: 질문을 기반으로 벡터 검색을 수행하고, 해당 벡터를 기반으로 답변을 생성하는 QA 체인입니다.
    • PromptTemplate: 동적으로 생성된 프롬프트를 관리하는 데 사용됩니다.
    • os: 운영체제와 관련된 기능을 다루는 표준 Python 라이브러리로, 환경 변수를 설정하는 데 사용됩니다.
  3. API 키 설정:
    • os.environ["OPENAI_API_KEY"]: OpenAI API를 사용할 수 있도록 환경 변수를 설정하는 코드입니다. 이 부분에 본인의 OpenAI API 키를 넣어야 합니다.

 

# 문서 로드 및 분할 함수
def load_and_split_text(file_path):
  # 파일 확장자에 따라 다른 로더 선택
  if file_path.endswith('.pdf'):  # 파일이 PDF일 경우
    loader = PyMuPDFLoader(file_path)  # PDF 로더 사용
  else:  # 파일이 PDF가 아니면
    loader = TextLoader(file_path)  # 일반 텍스트 파일 로더 사용

  # 문서 로드
  documents = loader.load()

  # 텍스트 분할기 생성 (100자 청크 크기, 20자 중첩)
  text_splitter = RecursiveCharacterTextSplitter(
                    chunk_size=100,  # 한 청크에 포함될 최대 문자 수
                    chunk_overlap=20,  # 청크 간 겹치는 문자 수
                    separators=['.','\\n',' ',''],  # 청크를 나눌 구분자
                    keep_separator=True  # 구분자를 청크에 포함시킬지 여부
                  )
  
  # 문서를 청크로 분할하고 반환
  return text_splitter.split_documents(documents)

코드 설명

  1. 파일 확장자에 따른 로더 선택:
    • if file_path.endswith('.pdf'): 파일 경로가 .pdf로 끝나면 PDF 파일로 인식하고, PyMuPDFLoader를 사용하여 PDF 파일을 로드합니다.
    • else: 그 외의 파일은 TextLoader를 사용하여 일반 텍스트 파일로 로드합니다.
  2. 문서 로드:
    • documents = loader.load(): 로더를 통해 문서를 로드합니다. 이 문서는 텍스트나 PDF 파일에서 추출된 내용을 포함하게 됩니다.
  3. 텍스트 분할기 생성:
    • text_splitter = RecursiveCharacterTextSplitter(...): 로드된 문서를 작은 청크로 나누는 분할기입니다.
      • chunk_size=100: 각 청크의 최대 크기는 100자입니다.
      • chunk_overlap=20: 각 청크는 20자씩 겹치도록 설정되어 있습니다. 이를 통해 문맥을 유지하며 텍스트를 나눕니다.
      • separators=['.','\n',' ','']: 문장을 나누는 구분자입니다. 마침표(.), 줄 바꿈(\n), 공백( ) 등을 기준으로 텍스트를 분할합니다.
      • keep_separator=True: 구분자를 청크의 일부로 포함하도록 설정합니다.
  4. 문서 분할:
    • return text_splitter.split_documents(documents): 로드된 문서를 설정된 기준에 따라 청크 단위로 분할하여 반환합니다.

 

# RAG 챗봇 구현 함수
def create_rag_chatbot(texts):
  # 벡터 데이터베이스 생성
  vectorstore = Chroma.from_documents(texts, OpenAIEmbeddings(), persist_directory='./db3') 
  # 벡터 데이터베이스는 텍스트 데이터와 OpenAI 임베딩을 사용하여 생성하고, 데이터를 디스크에 저장합니다.

  # LLM 준비 (Language Model)
  llm = OpenAI(temperature=0.2, max_tokens=2048) 
  # OpenAI API를 사용하여 LLM을 초기화. 
  # temperature는 0.2로 설정하여 출력의 창의성을 낮추고, max_tokens는 최대 2048으로 설정하여 답변의 길이를 제한합니다.

  # 프롬프트 엔지니어링 (한글 답변 최적화)
  prompt_template = PromptTemplate(
      input_variables=['context', 'question'],
      template='''문서 : {context}
      질문:{question}
      다음 단계을 따라 한글로 답변하세요
      1. 문서에서 질문과 관련된 문장 검색.
      2. 문장이 있으면 문장기반으로 답변구성
      3. 문장이 없으면 "문서에 정보 없음"을 명시하고, 일반지식으로 간결히답변
      답변은 정확해야 합니다.
      '''
  )
  # 주어진 문서에서 질문과 관련된 내용을 찾아 한글로 정확한 답변을 작성하기 위한 템플릿입니다.
  # 문서에서 질문 관련 문장을 먼저 찾고, 없으면 "문서에 정보 없음" 메시지를 반환합니다.

  # RetrivalQA 체인
  rag_chatbot = RetrievalQA.from_chain_type(
      llm=llm,
      chain_type='stuff',  # 'stuff' 체인은 작은 문서들은 한번에 전달하는 방식입니다.
      retriever=vectorstore.as_retriever(search_kwargs={'k': 3}),  # 벡터 데이터베이스에서 유사도가 높은 상위 3개 문서를 검색
      chain_type_kwargs={'prompt': prompt_template},  # 프롬프트 엔지니어링을 사용하여 답변을 생성
      return_source_documents=True,  # 참조된 문서도 함께 반환
  )

  # 최종적으로 RAG 챗봇을 반환
  return rag_chatbot

코드 설명

  1. 벡터 데이터베이스 생성:
    • Chroma.from_documents(texts, OpenAIEmbeddings(), persist_directory='./db3'):
      • 주어진 texts를 벡터화하여 Chroma 벡터 데이터베이스를 생성합니다.
      • 이 데이터베이스는 검색을 빠르게 하기 위해 문서를 벡터로 변환하며, 변환된 데이터는 디스크(./db3)에 저장됩니다.
  2. LLM(언어 모델) 준비:
    • llm = OpenAI(temperature=0.2, max_tokens=2048):
      • OpenAI 모델을 사용하여 LLM을 초기화합니다.
      • temperature=0.2는 모델의 창의성을 낮추어 더 일관성 있는 답변을 유도합니다.
      • max_tokens=2048은 모델이 생성할 수 있는 최대 토큰 수를 설정합니다.
  3. 프롬프트 엔지니어링 (한글 답변 최적화):
    • prompt_template: 모델에 전달할 프롬프트의 형식을 정의합니다.
      • 문서에서 질문과 관련된 문장을 먼저 찾고, 그 문장을 기반으로 답변을 생성합니다.
      • 만약 관련 문장이 없다면 "문서에 정보 없음"이라고 명시하고 일반 지식으로 답변을 제공합니다.
      • 이 템플릿은 한글로 답변을 제공하며, 정확한 답변을 요구합니다.
  4. RetrivalQA 체인:
    • rag_chatbot = RetrievalQA.from_chain_type(...):
      • RetrievalQA 체인을 사용하여 RAG 기반 챗봇을 생성합니다.
      • llm: OpenAI LLM 모델을 사용하여 답변을 생성합니다.
      • chain_type='stuff': 'stuff' 체인 타입은 작은 문서들을 한 번에 전달하는 방식입니다.
      • retriever=vectorstore.as_retriever(search_kwargs={'k': 3}): 벡터 데이터베이스에서 유사도가 높은 상위 3개 문서를 검색하여 답변에 사용합니다.
      • chain_type_kwargs={'prompt': prompt_template}: 위에서 정의한 프롬프트 템플릿을 사용하여 생성된 답변을 최적화합니다.
      • return_source_documents=True: 답변 생성에 사용된 참조 문서도 함께 반환합니다.
  5. 최종 결과:
    • 함수는 rag_chatbot 객체를 반환합니다. 이 객체는 주어진 질문에 대해 벡터 데이터베이스에서 관련 문서를 검색하고, 이를 바탕으로 정확한 답변을 생성할 수 있습니다.

 

# 문서로드 및 분할
texts = load_and_split_text("/content/산림병해충 방제규정(산림청훈령)(제1664호)(20241210).pdf")
print("문서개수:", len(texts))

# RAG 챗봇 생성
qa_chain = create_rag_chatbot(texts)

# 질문 생성
query = '위험평가의 실시'

# RetrivalQA 체인으로 질문에 대한 답변 얻기
result = qa_chain({"query": query})

# 결과 출력
print(f'\\n\\n질문 : {query}')
print(f'답변 : {result["result"]}')
print(f'참조 문서 : {[doc.page_content for doc in result["source_documents"]]}')

코드 설명:

  • 이 방식은 RetrievalQA 체인에서 제공하는 표준적인 방식으로, 해당 쿼리를 입력하면 관련 문서와 답변을 반환합니다.

 


벡터DB 데이터 검색

# 벡터 데이터베이스 검색
documents = '''제4조의5(위험평가의 실시) ① 위험평가는 다음 각 호의 항목을 포함하여 실시하여야 한다.
1. 대상 산림병해충의 외래병해충 여부
2. 대상 산림병해충의 생리ㆍ생태적 특성
3. 대상 산림병해충으로 인한 예상 피해 정도
4. 긴급방제 추진의 필요성과 방제방법
② 제1항에 의한 산림병해충의 위험등급은 별표 1의 산림병해충 위험평가표를 기준으로 계량화된 점수를 산정
하고 별표 2의 산림병해충 종합위험도 판정기준에 따라 다음 각 호와 같이 판정한다.
1. 종합평가점수 ‘높음’ : ‘고위험 병해충’
2. 종합평가점수 ‘중간’ : ‘중위험 병해충’
3. 종합평가점수 ‘낮음’ : ‘저위험 병해충’
4. 산림과 생활환경 및 경제에 미치는 영향에 대한 위험요소 항목 모두가 ‘가장 높은 점수’로 평가되는 경우 다른
평가 항목의 평가점수가 낮아도 ‘고위험 병해충’으로 판정할 수 있다.
5. 산림과 생활환경 및 경제에 미치는 영향에 대한 위험요소 항목 모두가 ‘가장 낮은 점수’로 평가되는 경우 다른
평가 항목의 평가점수가 높아도 ‘저위험 병해충’으로 판정할 수 있다.
6. 병해충의 정보가 부족하여 평가가 제한적인 경우 종합위험도 판정을 유예하되, 산림과 생활환경 및 경제에 미
치는 영향에 대한 위험성이 예상된다면 위험관리방안 수준을 고려하여 종합위험도를 판정할 수 있다.
③ 기존에 평가된 병해충에 대하여 필요 시 재평가를 실시할 수 있다.
④ 위원장은 제4조에서 규정한 예찰조사 결과를 위험평가에 반영할 수 있다.
'''
with open('documents.txt','w') as f:
  f.write(documents)

# 벡터 데이터베이스 생성 및 검색
def create_and_search_vectorstore(texts):
  embedding = OpenAIEmbeddings()
  vectorstore = Chroma.from_documents(texts, embedding, persist_directory='./db10')
  return vectorstore

# 문서로드 및 벡터화
texts = load_and_split_text("documents.txt")
vt = create_and_search_vectorstore(texts)

# 질문 생성 및 유사도 검색
query = '위험평가의 실시'
search_results = vt.similarity_search(query, k=5)

# 결과 출력
print(f"질문: {query}")
for idx, result in enumerate(search_results):
    print(f"결과 {idx+1}: {result.page_content}")

코드 설명:

  • create_and_search_vectorstore 함수는 texts를 임베딩하여 벡터 데이터베이스를 생성하고 반환합니다.
  • similarity_search(query, k=5)는 쿼리와 가장 유사한 5개의 문서를 검색합니다.
  • search_results는 검색된 문서들의 목록을 반환하며, 각 문서의 page_content를 출력합니다.

이렇게 실행하면, 위험평가의 실시와 관련된 가장 유사한 5개의 문서를 출력할 수 있습니다.

 


PDF Loaders

# 파일 경로 지정 (PDF 파일)
filepath = '/content/[이슈리포트 2022-2호] 혁신성장 정책금융 동향.pdf'

# 문서 로드 및 분할 함수 호출
result = load_and_split_text(filepath)

# 각 문서의 내용 출력
for doc in result:
  print(doc.page_content)

코드 설명:

  1. filepath는 PDF 파일의 경로를 지정합니다.
  2. load_and_split_text(filepath) 함수는 지정된 파일을 로드하고, 내용을 분할하여 result 변수에 저장합니다.
    • 이 함수는 PyMuPDFLoader 또는 TextLoader를 사용하여 PDF 파일을 로드하고, RecursiveCharacterTextSplitter로 텍스트를 작은 청크로 분할합니다.
  3. for doc in result:는 분할된 문서 각각을 순차적으로 처리하여 doc.page_content를 출력합니다.
    • doc.page_content는 분할된 각 문서 조각의 텍스트 내용을 나타냅니다.

 


OCR 기능을 이용해서 이미지내 텍스트 추출

# 필요한 라이브러리 설치
!pip install rapidocr-onnxruntime
!pip install pypdf

# langchain의 PyPDFLoader를 사용하여 PDF 파일을 로드하는 코드
from langchain.document_loaders import PyPDFLoader

# PDF 파일 경로 지정
filepath = '/content/[이슈리포트 2022-2호] 혁신성장 정책금융 동향.pdf'

# PyPDFLoader 사용 (이미지 추출을 활성화)
loader = PyPDFLoader(filepath, extract_images=True)

# PDF 파일을 페이지별로 분할하여 로드
pages = loader.load_and_split()  # 페이지별로 분할하여 반환

# 첫 번째 페이지의 내용 출력
print(pages[0])

코드 설명:

  1. 라이브러리 설치:
    • rapidocr-onnxruntime: OCR(광학문자 인식) 기능을 제공하는 라이브러리로, 이미지에서 텍스트를 추출할 수 있습니다.
    • pypdf: PDF 파일을 읽고 처리할 수 있는 라이브러리입니다.
  2. PyPDFLoader 사용:
    • PyPDFLoader는 langchain의 문서 로더로, PDF 파일을 로드하고 텍스트를 추출합니다.
    • extract_images=True 옵션을 사용하여 PDF 내의 이미지를 추출할 수 있습니다.
  3. 문서 로드 및 페이지 분할:
    • load_and_split() 함수는 PDF 파일을 페이지별로 분할하여 각 페이지를 리스트로 반환합니다.
  4. 첫 번째 페이지 출력:
    • pages[0]는 PDF의 첫 번째 페이지 내용으로, 이를 출력하여 확인할 수 있습니다.

이 코드는 PDF 파일을 페이지별로 분할하고 첫 번째 페이지의 내용을 출력하는 역할을 합니다.

 

# 필요한 라이브러리 설치
!pip install pypdfium2

# langchain.document_loaders에서 PyPDFium2Loader 가져오기
from langchain.document_loaders import PyPDFium2Loader

# PDF 파일 경로 지정
filepath = '/content/[이슈리포트 2022-2호] 혁신성장 정책금융 동향.pdf'

# PyPDFium2Loader 사용 (이미지 추출을 활성화)
loader = PyPDFium2Loader(filepath, extract_images=True)

# PDF 파일을 페이지별로 분할하여 로드
pages = loader.load_and_split()  # 페이지별로 분할하여 반환

# 첫 번째 페이지의 내용 출력
print(pages[0])

코드 설명:

  • loader.load_and_split(): PDF 파일을 페이지별로 분할하고 각 페이지를 리스트로 반환합니다.

 


이미지에 있는 text를 추출할 때는

  • 이미지를 word에 넣어서 pdf로 변환 후 pdfloader를 이용한다.
  • 이미지 자체를 ocr 기능을 이용해서 텍스트를 추출
# easyocr 라이브러리 설치
!pip install easyocr

# easyocr 및 cv2 라이브러리 임포트
import easyocr
import cv2

# 이미지를 로드 (파일 경로는 /content/test.png)
image = cv2.imread("/content/test.png")

# easyocr의 Reader 객체 생성, 언어 설정 ('ko'는 한국어, 'en'은 영어)
reader = easyocr.Reader(["ko", "en"])

# 이미지에서 텍스트 인식 수행
results = reader.readtext(image)

# 인식된 텍스트 결과 출력
print(results)

코드 설명:

  1. easyocr 라이브러리 설치: easyocr는 다양한 언어를 지원하는 OCR(Optical Character Recognition) 라이브러리입니다.
  2. cv2 라이브러리 임포트: 이미지 처리를 위한 OpenCV 라이브러리입니다.
  3. 이미지 로드: cv2.imread()로 지정한 경로에서 이미지를 읽어옵니다.
  4. easyocr Reader 객체 생성: easyocr.Reader()로 텍스트 인식을 위한 객체를 생성하고, 이 객체에 지원할 언어를 ["ko", "en"]로 설정합니다. 즉, 한국어와 영어를 지원합니다.
  5. 텍스트 인식: reader.readtext(image) 메서드를 호출하여 이미지에서 텍스트를 인식하고, 결과는 리스트 형태로 반환됩니다.
  6. 결과 출력: 인식된 텍스트는 results 변수에 저장되고, 이를 print() 함수로 출력합니다.

results에는 텍스트 내용뿐만 아니라 위치 정보도 포함되어 있습니다. 반환 형식은 [(bbox, text, confidence), ...]로, bbox는 텍스트의 위치 정보, text는 인식된 텍스트, confidence는 인식 확신도를 나타냅니다.

 


문서를 대상으로 정확매칭 - 유사검색 - 생성형 답변

  • 메타데이터를 추가해서 문단 단위로 분할
# PDF 문서 로드 및 메타데이터 추가 함수
def load_and_prepare_document():
  # PyPDFLoader로 PDF 문서 로드
  loader = PyPDFLoader('/content/산림병해충 방제규정(산림청훈령)(제1664호)(20241210).pdf')
  documents = loader.load()  # 페이지 단위로 문서 로드
  
  # 각 페이지에 메타데이터 추가 (페이지 번호 및 섹션 제목)
  for doc in documents:
    page_content = doc.page_content  # 페이지 내용 추출
    page_num = doc.metadata.get("page", 0) + 1  # 페이지 번호 추출
    # 섹션 제목 설정 (첫 줄이 "제"로 시작하면 해당 줄을 제목으로, 아니면 페이지 번호를 제목으로 설정)
    section_title = page_content if page_content.startswith("제") else f"페이지 {page_num}"
    doc.metadata.update({"page": page_num, "section": section_title})  # 메타데이터 업데이트

  # 문서를 청크로 분할
  text_splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20,
                    separators=['.','\\n',' ',''], keep_separator=True)  # 구분자를 청크 끝에 포함
  return text_splitter.split_documents(documents)  # 분할된 문서 반환

# 벡터 데이터베이스 생성 함수
def create_and_search_vectorstore(texts):
  ''' Chroma 벡터 데이터베이스 생성, 질문에 대한 유사 문장 검색
  '''
  embedding = OpenAIEmbeddings()  # OpenAI 임베딩을 사용하여 텍스트를 벡터화
  vectorstore = Chroma.from_documents(texts, embedding, persist_directory='./chroma_db')  # Chroma를 사용해 벡터 데이터베이스 생성
  return vectorstore  # 생성된 벡터 데이터베이스 반환

# 유사 문장 검색 함수
def test_simularity_search(vectorstore, question='병해충 예찰 및 발생조사 에 대해서?'):
  results = vectorstore.similarity_search(question, k=3)  # 벡터 데이터베이스에서 질문에 대해 가장 유사한 3개의 문서 검색
  if results:  # 결과가 있을 경우
    for i, doc in enumerate(results, 1):  # 결과 출력
      print(f'{doc.page_content}')  # 유사한 문장 출력
  else:  # 결과가 없으면
    print('유사문장 없음')  # 유사한 문장이 없다는 메시지 출력
    
# 문서 로드 및 분할, 벡터 데이터베이스 생성
texts = load_and_prepare_document()
vectorstore = create_and_search_vectorstore(texts)

# 예시 질문을 던져서 유사 문장 검색
question = '제 4조는 어떤 내용인가'
test_simularity_search(vectorstore, question)

코드 설명:

  1. load_and_prepare_document():
    • PDF 파일을 로드하고 각 페이지에 메타데이터를 추가합니다.
    • 페이지 번호와 섹션 제목을 설정하여 각 페이지에 대한 정보를 메타데이터로 추가합니다.
    • 문서는 RecursiveCharacterTextSplitter를 사용하여 청크로 분할됩니다.
  2. create_and_search_vectorstore():
    • 텍스트 데이터를 벡터화하고, Chroma를 사용하여 벡터 데이터베이스를 생성합니다.
    • 이 데이터베이스는 추후 질문에 대한 유사 문장 검색에 사용됩니다.
  3. test_simularity_search():
    • 주어진 질문에 대해 벡터 데이터베이스에서 가장 유사한 문장을 검색합니다.
    • 유사 문장이 있을 경우 해당 문서를 출력하고, 없으면 "유사문장 없음" 메시지를 출력합니다.

주요 흐름:

  • PDF 문서를 로드하고, 이를 청크로 분할하여 벡터화 후 Chroma 벡터 데이터베이스를 생성합니다.
  • 주어진 질문을 벡터화하여 가장 유사한 문장을 찾고 출력합니다.

 


# 필요한 라이브러리 임포트
import os
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_openai import ChatOpenAI
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate

# 1. PDF 문서 로드 및 준비
def load_and_prepare_documents():
    """
    PDF 문서를 로드하고, 페이지별 메타데이터를 추가해 분할.
    Returns: 분할된 텍스트 청크 리스트
    """
    print("=== PDF 문서 로드 및 준비 ===")
    try:
        # PDF 파일 로드
        loader = PyPDFLoader("/content/산림병해충 방제규정(산림청훈령)(제1664호)(20241210).pdf")
        documents = loader.load()  # 페이지 단위로 문서 로드
        print(f"로드된 문서 페이지 수: {len(documents)}")
    except Exception as e:
        print(f"PDF 로드 에러: {e}")
        return []

    # 페이지별 메타데이터 추가 (페이지 번호, 조항명 추정)
    for doc in documents:
        page_content = doc.page_content  # 페이지의 내용
        page_num = doc.metadata.get("page", 0) + 1  # 페이지 번호 추출
        # 첫 번째 줄을 조항명으로 설정 ("제"로 시작하는 경우)
        first_line = page_content.split("\\n")[0].strip()
        section_title = first_line if first_line.startswith("제") else f"페이지 {page_num}"
        doc.metadata.update({"page": page_num, "section": section_title})  # 메타데이터 업데이트

    # 문서 청크 분할 (한글 문서에 최적화)
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=200,  # 각 청크 크기
        chunk_overlap=20,  # 청크 간 겹치는 부분
        separators=["\\n\\n", "\\n", ".", " ", ""],  # 분할 기준
        keep_separator=True  # 구분자를 청크 끝에 포함
    )
    texts = text_splitter.split_documents(documents)  # 문서를 청크로 분할
    print(f"분할된 청크 수: {len(texts)}")
    return texts

# 2. RAG 챗봇 생성 (CoT 프롬프트)
def create_rag_chatbot(texts):
    """
    PDF 문서를 기반으로 CoT 프롬프트 RAG 챗봇 생성.
    Args:
        texts: 분할된 문서 청크
    Returns: RetrievalQA 체인
    """
    print("\\n=== RAG 챗봇 생성 (COT 프롬프트) ===")
    try:
        # 벡터화된 문서 데이터베이스 생성
        embeddings = OpenAIEmbeddings()  # OpenAI 임베딩을 사용해 벡터화
        vectorstore = Chroma.from_documents(texts, embeddings, persist_directory="./chroma_db")  # Chroma를 사용해 벡터 데이터베이스 생성
        print("벡터 데이터베이스 생성 완료")

        # LLM (대형 언어 모델) 초기화
        llm = ChatOpenAI(model_name="gpt-4.1-2025-04-14", temperature=0.7)  # GPT-4 모델 사용

        # CoT (Chain of Thought) 프롬프트 설정
        template = """문서: {context}
질문: {question}
다음 단계를 따라 한글로 답변하세요:
1) 문서에서 질문과 관련된 문장을 검색합니다.
2) 관련 문장이 있으면 해당 항목들을 나열한다
3) 문장에 없으면 "문서에 정보 없음"을 명시하고, 일반 지식으로 간결히 답변합니다.
답변은 정확하게 먼저 문서에서 찾아서 관련항목들을 나열한다"""
        prompt_template = PromptTemplate(input_variables=["context", "question"], template=template)

        # RetrievalQA 체인 생성
        qa_chain = RetrievalQA.from_chain_type(
            llm=llm,
            chain_type="stuff",
            retriever=vectorstore.as_retriever(search_kwargs={"k": 3}),  # 검색 결과 상위 3개 문서
            return_source_documents=True,
            chain_type_kwargs={"prompt": prompt_template}  # CoT 프롬프트 사용
        )
        return qa_chain
    except Exception as e:
        print(f"RAG 챗봇 생성 에러: {e}")
        return None

# 3. 챗봇 테스트
def test_chatbot(qa_chain):
    """
    CoT 프롬프트로 챗봇 테스트.
    Args:
        qa_chain: RetrievalQA 체인
    """
    print("\\n=== 챗봇 테스트 ===")
    questions = [
        "산림병해충 방제의 원칙은 무엇인가?",
        "위험평가의 대상은"
    ]
    for q in questions:
        print(f"\\n질문: {q}")
        try:
            result = qa_chain.invoke({"query": q})  # 질문에 대한 응답 받기
            print(f"COT 답변: {result['result']}")
            # 문서 참조 (필요한 경우 참조 문서도 출력)
            # print(f"참조 문서: {[f'페이지 {doc.metadata['page']} ({doc.metadata['section']}): {doc.page_content}' for doc in result['source_documents']]}")
        except Exception as e:
            print(f"COT 에러: {e}")
        print("-" * 50)

# 메인 실행
def main():
    # 문서 로드 및 분할
    texts = load_and_prepare_documents()
    if not texts:
        print("문서 처리 실패, 종료.")
        return

    # CoT 기반 챗봇 생성
    qa_chain = create_rag_chatbot(texts)
    if not qa_chain:
        print("챗봇 생성 실패, 종료.")
        return

    # 챗봇 테스트
    test_chatbot(qa_chain)

# 스크립트 실행
if __name__ == "__main__":
    main()

코드 설명:

  1. load_and_prepare_documents():
    • PDF 파일을 로드하고, 각 페이지에 메타데이터(페이지 번호와 조항명)를 추가합니다.
    • 문서는 RecursiveCharacterTextSplitter를 사용하여 청크로 분할됩니다.
    • 각 청크는 최대 200자 크기로, 20자씩 겹치며 분할됩니다.
  2. create_rag_chatbot():
    • 로드된 문서를 벡터화하여 Chroma 벡터 데이터베이스를 생성합니다.
    • GPT-4 모델을 기반으로 한 RetrievalQA 체인을 생성하며, Chain of Thought(Cot) 프롬프트를 사용하여 질문에 대해 적절한 답변을 생성합니다.
  3. test_chatbot():
    • 생성된 챗봇을 테스트하는 함수로, 주어진 질문에 대해 유사 문장을 검색하고 답변을 생성합니다.
  4. main():
    • 문서 로드, 챗봇 생성, 테스트 과정을 실행합니다.

흐름:

  • 문서 로드 및 분할: PDF 파일을 로드하고 페이지별로 분할 및 메타데이터를 추가한 후, 텍스트 청크로 분할합니다.
  • RAG 챗봇 생성: 벡터화된 문서 데이터를 이용하여 RAG(질문응답) 챗봇을 생성합니다.
  • 챗봇 테스트: 예시 질문을 입력하여 챗봇의 응답을 테스트하고 출력합니다.

 


RAG - QA 구축

# PDF 파일 로드
loader = PyPDFLoader('/content/대한민국헌법(헌법)(제00010호)(19880225).pdf')
# PDF 파일을 로드하고 페이지별로 분할
pages = loader.load_and_split()

# 청크로 분할 1000 - size
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
# 페이지를 1000자 크기의 청크로 분할하고, 청크들 사이에 100자 정도의 겹침을 포함
docs = text_splitter.split_documents(pages)

# 임베딩
embeddings = OpenAIEmbeddings()
# OpenAI의 임베딩을 사용하여 문서를 벡터화
vectorstore = Chroma.from_documents(docs, embeddings)
# 벡터화된 문서들로 Chroma 벡터 데이터베이스 생성
retriever = vectorstore.as_retriever()
# 벡터 데이터베이스에서 검색을 할 수 있도록 설정

# 프롬프트 모델
llm = ChatOpenAI(model='gpt-4o-mini')
# GPT-4 모델을 기반으로 한 언어 모델을 사용

# 프롬프트 엔지니어링
prompt = '''
당신은 질문 답변 어시스턴트 입니다.
제공된 문맥을 이용해서 질문에 답하세요.
문맥에 답변이 없으면 "정보 없음"이라고 표시하고 일반 지식으로 간결히 답변하세요.
답변은 최대 세 문장으로 작성하세요.

문맥 : {context}
질문 : {question}
'''
prompt_template = PromptTemplate(input_variables=["context", "question"], template=prompt)
# 질문과 문맥을 받아서 답변을 생성할 프롬프트 템플릿 정의

# RetrievalQA 체인 생성
qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",  # 검색된 문서를 모두 포함하여 답변을 생성
    retriever=vectorstore.as_retriever(search_kwargs={"k": 3}),  # 상위 3개의 관련 문서 검색
    return_source_documents=True,  # 참조된 문서도 반환
    chain_type_kwargs={"prompt": prompt_template}  # 프롬프트 템플릿 사용
)

# 테스트: "대통령의 의무"에 대한 답변 요청
result = qa_chain.invoke('대통령의 의무')

# 결과 출력
print(f"결과 : {result['result']}")
print("참조문서 : ")
for doc in result['source_documents']:
    # 참조된 문서의 내용 출력
    print(doc.page_content)

코드 설명:

  1. PDF 로드 및 분할:
    • PyPDFLoader를 사용하여 PDF 문서를 로드하고, 페이지별로 분할합니다. load_and_split() 메서드를 통해 페이지를 나누는 작업을 수행합니다.
  2. 청크 분할:
    • RecursiveCharacterTextSplitter는 각 페이지를 청크로 분할합니다. 각 청크는 1000자 크기이며, 인접한 청크들 사이에 100자씩 겹치도록 설정되었습니다.
  3. 임베딩 및 벡터화:
    • OpenAIEmbeddings를 사용하여 각 문서를 벡터화하고, Chroma를 사용해 벡터화된 문서를 저장합니다. 이를 통해 검색 시스템을 구축할 수 있습니다.
  4. 언어 모델:
    • ChatOpenAI를 사용하여 GPT-4 모델을 기반으로 한 질문 응답 시스템을 설정합니다. 모델 이름은 'gpt-4o-mini'로 설정되어 있습니다.
  5. 프롬프트 엔지니어링:
    • 질문에 대해 문맥을 기반으로 답변을 생성하는 템플릿을 정의합니다. 문맥에 답이 없을 경우 "정보 없음"이라고 응답하도록 설정되어 있습니다.
  6. RetrievalQA 체인:
    • RetrievalQA는 문서를 검색하고, 검색된 문서들을 바탕으로 답변을 생성하는 체인입니다. 이 체인에서는 검색된 상위 3개의 문서에 기반하여 답변을 제공합니다.
  7. 결과 출력:
    • 질문인 "대통령의 의무"에 대해 qa_chain.invoke()를 사용하여 답변을 생성하고, 해당 답변과 참조된 문서들을 출력합니다.

실행 결과:

  • result['result']는 질문에 대한 답변을 출력합니다.
  • result['source_documents']는 해당 질문에 대해 참조된 문서들을 출력합니다.

 


RAG-memory

  • 페이지 별 메타데이터 활용
  • 대화 메모리 포함 세션별 기록 저장
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.chains import create_history_aware_retriever
from langchain_core.messages import HumanMessage, AIMessage
# 위의 코드를 재사용하면서

# 청크로 분할 1000 - size
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
# 각 페이지를 1000자 크기의 청크로 분할하고, 100자 정도 겹침을 포함
docs = text_splitter.split_documents(pages)

# 임베딩
embeddings = OpenAIEmbeddings()
# OpenAI의 임베딩을 사용하여 문서를 벡터화
vectorstore = Chroma.from_documents(docs, embeddings)
# 벡터화된 문서들로 Chroma 벡터 데이터베이스 생성
retriever = vectorstore.as_retriever()
# 벡터 데이터베이스에서 검색을 할 수 있도록 설정

# 채팅 히스토리와 사용자의 최신 질문 문맥을 고려해서 질문 재구성
context_prompt = ''' 대화 기록과 최신 사용자 질문을 바탕으로, \\
대화 기록 없이도 이해할 수 있는 독립적인 질문을 구성하세요. \\
질문에 답변하지 말고, 필요한 경우 질문만 재구성하거나 그대로 반환하세요
'''
contextualize_prompt = ChatPromptTemplate.from_messages([
    ("system", context_prompt),  # 시스템 프롬프트로 대화 기록과 질문을 바탕으로 재구성
    MessagesPlaceholder(variable_name="chat_history"),  # 대화 기록을 동적으로 삽입
    ("human", "{input}")  # 사용자의 입력을 그대로 받음
])

# 대화 기록을 고려한 검색기 생성
# llm = ChatOpenAI(model='gpt-4o-mini')  # 모델 선택
llm = ChatOpenAI()  # 기본 모델로 설정
history_aware_retriever = create_history_aware_retriever(llm, retriever, contextualize_prompt)
# 대화 기록과 질문을 바탕으로 정보를 검색하는 검색기 생성

# 대화 기록 예시
chat_history = [
    HumanMessage(content="대통령의 임기는 몇년이야?"),
    AIMessage(content="대통령의 임기는 5년 입니다.")
]

# 새로운 질문에 대해 대화 기록을 고려하여 질문을 재구성
result = contextualize_prompt.invoke({"input": "국회의원의 임기는??", "chat_history": chat_history})

# 대화 기록을 고려하여 검색 결과를 얻기
result = history_aware_retriever.invoke({"input": "국회의원의 임기는??", "chat_history": chat_history})

# 결과 출력
print(result[0].page_content)  # 검색된 문서의 내용 출력

코드 설명:

  1. 청크 분할:
    • RecursiveCharacterTextSplitter는 페이지를 1000자 크기의 청크로 분할하고, 각 청크가 100자의 겹침을 가지도록 설정됩니다. 이를 통해 긴 텍스트를 처리하기 쉽게 만듭니다.
  2. 임베딩 및 벡터화:
    • OpenAIEmbeddings를 사용하여 각 청크를 벡터화하고, Chroma 벡터 데이터베이스에 저장합니다. 이를 통해 검색을 효율적으로 수행할 수 있게 됩니다.
  3. 채팅 히스토리와 질문 문맥 고려:
    • context_prompt는 대화 기록과 최신 질문을 바탕으로 독립적인 질문을 재구성하도록 지시합니다. 시스템 프롬프트로 대화 기록을 처리하며, 필요한 경우 질문을 재구성하거나 그대로 반환합니다.
    • ChatPromptTemplate는 context_prompt와 chat_history(대화 기록), 사용자의 입력을 받아서 질문을 재구성하는 템플릿을 만듭니다.
  4. 검색기 생성:
    • create_history_aware_retriever는 대화 기록과 질문을 바탕으로 정보를 검색하는 검색기를 생성합니다. 이를 통해 과거 대화가 현재 질문에 어떻게 영향을 미치는지 고려하여 문서 검색을 수행합니다.
  5. 질문 재구성 및 검색:
    • result = contextualize_prompt.invoke({"input": "국회의원의 임기는??", "chat_history": chat_history})는 대화 기록과 최신 질문을 바탕으로 질문을 재구성합니다.
    • result = history_aware_retriever.invoke({"input": "국회의원의 임기는??", "chat_history": chat_history})는 재구성된 질문을 바탕으로 벡터 데이터베이스에서 문서를 검색합니다.
  6. 결과 출력:
    • 검색된 문서의 내용을 print(result[0].page_content)를 통해 출력합니다.

 


RAG(문서기반 검색 시스템) 개요

RAG(Reading-Answer Generation)은 문서에서 정보를 추출하여 질문에 대한 답변을 생성하는 시스템입니다. 주로 벡터 데이터베이스를 이용하여 빠르게 필요한 정보를 찾고, 이를 **LLM(Large Language Model)**과 연계하여 자연어 처리를 수행합니다. 이를 통해 사용자는 다양한 문서 형식에서 정보를 추출하고, 요약 및 질문에 대한 답변을 생성할 수 있습니다.

시스템 구성

1. 벡터 DB (Vector Database)

벡터 DB는 문서나 텍스트 데이터를 벡터화하여 저장하는 데이터베이스입니다. 벡터화된 데이터는 검색 및 유사도 기반 질의를 통해 빠르게 검색될 수 있습니다. LangChain에서는 다양한 문서 형식을 벡터화하고, 이들을 Chroma, FAISS, Pinecone 등 벡터 데이터베이스에 저장하여 검색 효율을 극대화할 수 있습니다.

2. LangChain: LLM에 RAG 데이터 전달

LangChain은 LLM과 연결되어 데이터에서 의미 있는 정보를 추출하고 생성된 데이터를 기반으로 다양한 형태의 결과를 생성하는 시스템입니다. RAG는 일반적으로 LLM을 통해 문서 기반 검색 결과를 받아오고, 이를 바탕으로 더 높은 수준의 이해와 자연어 생성 작업을 진행합니다.

3. 프롬프트 엔지니어링 (Prompt Engineering)

RAG 시스템에서 중요한 부분은 프롬프트 엔지니어링입니다. 사용자의 질문에 적합한 형태로 문서를 변환하여 LLM에 전달하는 작업입니다. 이는 모델이 효율적으로 원하는 정보를 추출하고 자연스럽게 답변할 수 있도록 도와줍니다.

4. 다양한 문서 형식 로드 가능

RAG 시스템은 다양한 형식의 문서 데이터를 로드하고 이를 벡터화하여 검색할 수 있도록 설계됩니다. 지원되는 주요 문서 형식은 다음과 같습니다:

  • JSON: JSON 파일을 로드하여 데이터를 처리합니다.
  • DOCX: Microsoft Word 문서를 로드하여 텍스트를 추출하고 벡터화합니다.
  • PDF: PDF 파일을 로드하여 텍스트를 추출하고 벡터화합니다.
  • CSV: CSV 파일에서 데이터를 로드하고 텍스트 형식으로 변환합니다.
  • TXT: 일반 텍스트 파일을 로드하여 텍스트를 처리합니다.
  • OCR (이미지 텍스트 추출): 이미지를 텍스트로 변환하여 OCR 기술을 사용해 문서화된 내용을 벡터화합니다.
  • Notion: Notion에서 데이터를 가져와 벡터화하여 검색 및 분석 작업을 할 수 있습니다.

5. 문서 기반으로 요약 및 QA

RAG 시스템은 문서에서 직접 정보를 추출하고, 이를 바탕으로 요약이나 QA (질문-답변) 작업을 수행할 수 있습니다. 예를 들어:

  • 문서 요약: 주어진 문서 내용을 짧고 간결하게 요약할 수 있습니다.
  • 질문-답변: 문서 내에서 사용자가 질문한 내용에 대해 가장 관련 있는 답변을 찾아 제공합니다.

6. 일반 GPT 및 특화된 파인튜닝 모델 사용

  • 일반 GPT 모델: 문서에 정보가 없거나, 문서 내에서 정확한 정보를 찾을 수 없을 때는 일반 GPT 모델을 통해 답변을 생성할 수 있습니다.
  • 특화된 파인튜닝 모델: 특정 도메인에 대해 더 높은 정확도로 답변을 제공하기 위해, 해당 분야에 맞게 파인튜닝된 모델을 사용할 수 있습니다.

전체 흐름

  1. 문서 로드: 다양한 형식의 문서를 로드하고, 이를 텍스트 데이터로 변환합니다.
  2. 벡터화: 로드된 문서를 벡터화하여 벡터 데이터베이스에 저장합니다.
  3. 질문 처리: 사용자가 질문을 입력하면, 벡터 데이터베이스에서 관련 문서를 검색합니다.
  4. 답변 생성: 검색된 문서를 바탕으로 LLM을 사용해 답변을 생성합니다. 문서에 답이 없을 경우, 파인튜닝된 GPT 모델을 사용하여 답변을 생성할 수 있습니다.

사용 예시

  • 법률 문서: 법률 문서나 규정에 대해 질문하고, 문서 내 관련 내용을 추출하여 답변을 생성.
  • 의료 데이터: 의료 연구나 임상 데이터에 대해 질의하고, 문서 기반으로 의학적인 답변을 제공.
  • 학술 논문: 학술 논문을 분석하여 특정 연구 결과나 논의에 대한 답변을 제공.

장점

  • 빠른 검색: 대량의 문서에서 필요한 정보를 벡터 데이터베이스를 통해 빠르게 검색할 수 있습니다.
  • 고도화된 문서 분석: 자연어 처리 및 생성 모델을 통해 문서를 효율적으로 분석하고 답변을 생성합니다.
  • 다양한 문서 형식 지원: 다양한 파일 형식을 지원하여 유연하게 문서를 처리할 수 있습니다.

결론

RAG 시스템은 특히 정보가 방대하거나 다루기 어려운 문서들을 처리하는 데 유용하며, 벡터 DB와 LLM을 결합한 강력한 정보 추출 및 생성 기술을 제공합니다. 이를 통해 질문 기반 시스템을 더욱 효율적이고 정교하게 구현할 수 있습니다.

반응형