Developer's Development
3.3.14 [LLM] 프롬프트 엔지니어링 응용: RAG 본문
RAG (검색 증강 생성)
RAG (Retrieval-Argumented Generation)는 Vector Database와 LLM을 결합하여 검색된 데이터를 활용해 문맥 기반 응답을 생성하는 기술이다.
작동 원리
- 검색: 사용자의 질문을 벡터화해 관련 데이터를 Vector DB에서 검색
- 생성: 검색된 데이터를 바탕으로 LLM이 자연어 응답 생성
구성 요소
- Vector Database, LLM, 검색 엔진
특징
- 최신 정보 활용 가능
- 특정 데이터에 대한 정확한 응답 제공
장점
- 최신 정보 제공: 모델 학습 이후 발생한 데이터도 사용 가능
- 정확성 향상: 검색 데이터를 기반으로 환각(hallucination) 현상 감소
- 유연성: 다양한 도메인 데이터 활용 가능
👉🏻 RAG Workflow
1. 사용자 질문 입력
2. Embedding 모델을 사용해 질문을 벡터화
3. Vector DB에서 유사한 문서 검색
4. 검색된 문서를 LLM이 응답 생성에 활용
5. 사용자에게 응답 반환
👉🏻 활용 사례
- 고객 지원 시스템: 사용자의 질문에 대한 정확한 답변 제공
- 학습 자료 기반 질의응답: 교육 콘텐츠를 기반으로 한 맞춤형 질의응답
- 의료 분야 최신 데이터 제공: 최신 논문과 연구 데이터를 반영한 의료 정보 제공
- 문맥 기반 검색 엔진: 특정 도메인에 특화된 검색 시스템
👉🏻 한계
데이터 품질 의존성: 검색된 데이터가 부정확하면 생성된 응답도 부정확할 수 있음
속도 문제: 검색과 생성 단계를 거치므로 반응 속도가 느려질 수 있음
- RAG 구조 (Vector DB와 LLM 결합)
RAG 구조는 Vector DB와 LLM을 결합해 검색된 데이터를 바탕으로 문맥 기반 응답을 생성하는 기술이다. 유사도 검색과 자연어 생성 단계를 포함하며, 최신 정보 활용과 정확한 응답 제공에 유리하다.
👉🏻 작동 원리
1. 벡터화: 텍스트 데이터를 임베딩 벡터로 변환해 Vector DB에 저장
2. 유사도 검색: 사용자의 질문을 벡터로 변환해 관련 데이터를 검색
3. 응답 생성: 검색된 데이터를 컨텍스트로 사용해 LLM이 자연어 응답 생성
- RAG 성능 최적화 기법
RAG의 검색 성능은 데이터 전처리, 임베딩 모델 선택, 검색 전략 등에 따라 크게 좌우된다. 효과적인 검색을 위해 다음과 같은 최적화 기법을 사용할 수 있다.
1. Chunking 전략 (데이터 단위 분할)
- 문서를 일정 크기의 단위 (Chunk)로 나누어 벡터화하면 검색 정확도가 향상된다.
- 일반적으로 문장 단위, 패러그래프 단위, 슬라이딩 윈도우 방식 등이 사용된다.
2. Embedding 모델 선택
- RAG에서 벡터화를 수행하는 모델 선택이 중요하다.
- 주요 임베딩 모델
| 모델 | 장점 | 단점 |
| text-embedding-ada-002 | OpenAI 제공, 높은 성능 | API 비용 발생 |
| Sentence-BERT | 오픈소스, 빠른 검색 가능 | LLM에 비해 정확도 낮을 수 있음 |
| Cohere Embeddings | 다국어 지원 | 설정이 다소 복잡 |
3. Retrieval 전략 (검색 최적화)
- Top-K 검색: 관련성이 높은 상위 K개의 문서를 검색한다. (일반적으로 K=3~5)
- Re-ranking 기법: 검색된 문서들을 다시 정렬하여 가장 관련성이 높은 문서만 활용한다.
- Hybrid Search: 벡터 검색 + 전통적인 키워드 검색 조합을 사용하는 전략이다.
- RAG 문제점과 최적화
👉🏻 속도 문제 해결
1. 캐싱(Cache) 활용
- 동일한 질문이 반복될 경우, LLM 호출 없이 기존 응답을 반환하면 속도를 향상시킬 수 있다.
2. 사전 연산(Pre-processing)
- 중요한 문서를 미리 벡터화하여 빠르게 검색할 수 있도록 준비한다.
- RAG 실행 전에 자주 검색되는 질문과 답변을 DB에 저장하여 속도를 개선한다.
👉🏻 데이터 품질 문제 해결
1. 데이터 정제 (Data Cleaning)
- OCR 문서, 웹스크래핑한 데이터 등에서 불필요한 공백, 특수 문자 등을 정리한다.
- 정제된 데이터만 벡터화하여 검색 정확도를 향상시킬 수 있다.
2. 지식 그래프(Knowledge Graph) 연동
- RAG가 단순한 문서 검색을 넘어 개념 간의 관계를 파악하도록 지원한다.
- ex. 의료 데이터 → 질병과 치료법 간의 연결 강화
- RAG 활용 사례
👉🏻 기업 내 문서 검색 시스템
ChatGPT Enterprise
- 기업 내 사내 문서(R&D 문서, 매뉴얼, 내부 보고서)를 검색하는 AI 챗봇 시스템
- 기존의 키워드 검색보다 문맥을 이해하는 응답 제공
- OpenAI의 API와 Pinecone을 활용하여 구축 가능
👉🏻 뉴스 및 연구 데이터 검색
Google's Gemini + RAG
- 최신 뉴스, 연구 논문을 검색하고 LLM을 활용하여 요약 및 분석 제공
- 예를 들어, 사용자가 "20024년 AI 관련 주요 연구"를 질문하면, 최신 논문을 검색 후 요약 응답
실습 (RAG)
자체 문서를 사용하여 응답을 생성 (RAG 프로세스에 따라 진행해보기!)
from dotenv import load_dotenv
load_dotenv()
- Phase 1: Indexing
[1] Load
- 데이터 로드
- Document Loader 사용
from langchain_community.document_loaders import PyPDFLoader
loader = PyPDFLoader('./snow-white.pdf')
documents = loader.load()
# documents
# 개행문자 제거
for doc in documents:
doc.page_content = doc.page_content.replace('\n', ' ')
documents
[2] Split
Text Splitter: 큰 문서 → 작은 chunk 분할
- 데이터를 인덱싱하거나 모델에 전달할 때 유용
- chunk가 커도 문제 (그런데 chunk가 너무 작아도 문제)
from langchain.text_splitter import RecursiveCharacterTextSplitter
text = "대한민국의 역사는 매우 길고 다양하다. 고조선부터 시작해서 삼국시대, 고려, 조선, 현대에 이르기까지 수많은 사건과 인물이 존재한다."
splitter = RecursiveCharacterTextSplitter(
chunk_size=30,
chunk_overlap=10
)
splitter.split_text(text)
"""
['대한민국의 역사는 매우 길고 다양하다. 고조선부터',
'고조선부터 시작해서 삼국시대, 고려, 조선, 현대에',
'조선, 현대에 이르기까지 수많은 사건과 인물이',
'사건과 인물이 존재한다.']
"""
text = """
This is a long document. It contains multiple paragraphs, sentences, and words.
The purpose of this text is to demonstrate how to split text into smaller chunks.
This is useful for indexing, searching, and processing large documents.
Each chunk will have a defined maximum size and may overlap with adjacent chunks.
"""
splitter = RecursiveCharacterTextSplitter(
chunk_size=50,
chunk_overlap=10,
separators=['\n\n', '\n', ' ', ''] # 구분자 우선순위 (문단, 라인, 공백 순으로 자르기)
)
chunks = splitter.split_text(text)
chunks
"""
['This is a long document. It contains multiple',
'multiple paragraphs, sentences, and words.',
'The purpose of this text is to demonstrate how to',
'how to split text into smaller chunks.',
'This is useful for indexing, searching, and',
'and processing large documents.',
'Each chunk will have a defined maximum size and',
'size and may overlap with adjacent chunks.']
"""
splitter = RecursiveCharacterTextSplitter(
chunk_size=100,
chunk_overlap=20
)
docs = splitter.split_documents(documents)
docs
[3] Store
분할된 chunk를 저장하고 indexing 할 저장소 필요
- VectorStore(Vector DB), Embedding Model 사용
# 임베딩 모델
from langchain_openai.embeddings import OpenAIEmbeddings
embedding_model = OpenAIEmbeddings(model='text-embedding-3-small')
!pip install langchain-chroma
from langchain_chroma.vectorstores import Chroma
vector_store = Chroma.from_documents(docs, embedding_model)
👉🏻 간단한 유사도 기반 검색
query = '백설공주와 왕비 중에 누가 더 아름답나요?'
# 1. vector store를 통한 직접 검색
results = vector_store.similarity_search_with_score(query)
results
"""
[(Document(id='3677ff65-c968-43b0-a093-8712d3b4fbeb', metadata={'moddate': '2023-09-12T11:20:24+09:00', 'page': 1, 'creationdate': '2023-09-12T11:20:24+09:00', 'author': 'PC', 'total_pages': 6, 'source': './snow-white.pdf', 'title': 'PowerPoint 프레젠테이션', 'creator': 'Microsoft® PowerPoint® 2013', 'producer': 'Microsoft® PowerPoint® 2013', 'page_label': '2'}, page_content='거울아. 이 세상에서 누가 가장 아름답니?” “왕비님도 아름답지만 백설공주가 더 아름답습니다.” 화가 난 왕비는 사냥꾼을 불렀어요. 왕비는 사냥꾼에게 백설공주를 죽이라고'),
0.8402987718582153),
(Document(id='fa4589d8-6cc9-44ca-bf6d-aa2c8bc13f4f', metadata={'creationdate': '2023-09-12T11:20:24+09:00', 'title': 'PowerPoint 프레젠테이션', 'total_pages': 6, 'page_label': '3', 'creator': 'Microsoft® PowerPoint® 2013', 'page': 2, 'source': './snow-white.pdf', 'moddate': '2023-09-12T11:20:24+09:00', 'producer': 'Microsoft® PowerPoint® 2013', 'author': 'PC'}, page_content='왕비는 다시 요술 거울에게 누가 가장 아름다운 지 물었어요. “왕비님도 아름답지만 백설공주님이 천배는 더 아름답습니다.” “사냥꾼이 날 속였구나. 내가 직접 해치우겠어!”'),
0.8604050278663635),
(Document(id='2ad9314c-4101-47ce-be92-b8fbe6f9c403', metadata={'source': './snow-white.pdf', 'total_pages': 6, 'author': 'PC', 'moddate': '2023-09-12T11:20:24+09:00', 'producer': 'Microsoft® PowerPoint® 2013', 'creator': 'Microsoft® PowerPoint® 2013', 'page': 4, 'page_label': '5', 'creationdate': '2023-09-12T11:20:24+09:00', 'title': 'PowerPoint 프레젠테이션'}, page_content='“백설공주님, 못된 왕비의 꾐에 넘어갔군요.” “여전히 아름다운 우리 공주님을 캄캄한 땅속에 묻을 순 없어.” “오래오래 볼 수 있게 유리 관에 모시자.” 어느 날, 한 왕자가'),
0.9580434560775757),
(Document(id='5ae807e1-0967-482a-8451-5179849a5dfc', metadata={'title': 'PowerPoint 프레젠테이션', 'creator': 'Microsoft® PowerPoint® 2013', 'moddate': '2023-09-12T11:20:24+09:00', 'page': 0, 'creationdate': '2023-09-12T11:20:24+09:00', 'source': './snow-white.pdf', 'author': 'PC', 'producer': 'Microsoft® PowerPoint® 2013', 'page_label': '1', 'total_pages': 6}, page_content='백설공주 옛날 어느 왕국에 공주님이 태어났어요. “어쩜 이렇게 어여쁠까? 살결이 눈처럼 하얗구나. 백 설공주라고 불러야겠다.” 왕과 왕비는 갓 태어난 딸을 보며 기뻐했어요. 하지만'),
1.0915120840072632)]
"""
# 2. Retriever를 활용한 검색
retriever = vector_store.as_retriever(
search_type='similarity',
search_kwargs={'k': 3}
)
retriever_result = retriever.batch([query])
retriever_result
"""
[[Document(id='3677ff65-c968-43b0-a093-8712d3b4fbeb', metadata={'creator': 'Microsoft® PowerPoint® 2013', 'page': 1, 'author': 'PC', 'creationdate': '2023-09-12T11:20:24+09:00', 'page_label': '2', 'producer': 'Microsoft® PowerPoint® 2013', 'title': 'PowerPoint 프레젠테이션', 'moddate': '2023-09-12T11:20:24+09:00', 'source': './snow-white.pdf', 'total_pages': 6}, page_content='거울아. 이 세상에서 누가 가장 아름답니?” “왕비님도 아름답지만 백설공주가 더 아름답습니다.” 화가 난 왕비는 사냥꾼을 불렀어요. 왕비는 사냥꾼에게 백설공주를 죽이라고'),
Document(id='fa4589d8-6cc9-44ca-bf6d-aa2c8bc13f4f', metadata={'page_label': '3', 'author': 'PC', 'page': 2, 'moddate': '2023-09-12T11:20:24+09:00', 'title': 'PowerPoint 프레젠테이션', 'producer': 'Microsoft® PowerPoint® 2013', 'source': './snow-white.pdf', 'creationdate': '2023-09-12T11:20:24+09:00', 'total_pages': 6, 'creator': 'Microsoft® PowerPoint® 2013'}, page_content='왕비는 다시 요술 거울에게 누가 가장 아름다운 지 물었어요. “왕비님도 아름답지만 백설공주님이 천배는 더 아름답습니다.” “사냥꾼이 날 속였구나. 내가 직접 해치우겠어!”'),
Document(id='2ad9314c-4101-47ce-be92-b8fbe6f9c403', metadata={'author': 'PC', 'moddate': '2023-09-12T11:20:24+09:00', 'page_label': '5', 'total_pages': 6, 'page': 4, 'creator': 'Microsoft® PowerPoint® 2013', 'title': 'PowerPoint 프레젠테이션', 'producer': 'Microsoft® PowerPoint® 2013', 'creationdate': '2023-09-12T11:20:24+09:00', 'source': './snow-white.pdf'}, page_content='“백설공주님, 못된 왕비의 꾐에 넘어갔군요.” “여전히 아름다운 우리 공주님을 캄캄한 땅속에 묻을 순 없어.” “오래오래 볼 수 있게 유리 관에 모시자.” 어느 날, 한 왕자가')]]
"""
- Phase2: Retrieval and Generation
사용자 입력(질의)이 주어지면
1. Retriever를 사용하여 저장소에서 관련된 chunk 검색
2. 질문과 검색된 데이터를 포함하는 프롬프트를 통해
3. LLM이 답변을 생성
(1) PromptTemplate 생성
- 사용자 질의 + 검색된 문서
from langchain_core.prompts import ChatPromptTemplate
prompt_tpl = ChatPromptTemplate([
('system', '''당신은 어린 아이에게 꿈과 희망을 심어주는 유치원 교사입니다.
질문하는 아이에게 최대한 호응해주며 context 기반으로만 답변해 주세요.'''),
('user', '''어린이의 질문에 context만을 이용해 답변하세요.
context에서 확인할 수 없는 질문이라면 모른다고 답변해야 합니다.
최종 응답에는 참조한 context에 대한 정보를 추가해 주세요.
질문: {query}
context: {context}
<<최종 응답 형식>>
답변:
참조문서:
- <<source>> (page: <<page>>): <<page_content>>
''')
])
prompt_tpl.invoke({'query': query, 'context': retriever_result})
# ChatPromptValue(messages=[SystemMessage(content='당신은 어린 아이에게 꿈과 희망을 심어주는 유치원 교사입니다.\n질문하는 아이에게 최대한 호응해주며 context 기반으로만 답변해 주세요.', additional_kwargs={}, response_metadata={}), HumanMessage(content="어린이의 질문에 context만을 이용해 답변하세요.\ncontext에서 확인할 수 없는 질문이라면 모른다고 답변해야 합니다.\n최종 응답에는 참조한 context에 대한 정보를 추가해 주세요.\n\n 질문: 백설공주와 왕비 중에 누가 더 아름답나요?\n context: [[Document(id='3677ff65-c968-43b0-a093-8712d3b4fbeb', metadata={'creator': 'Microsoft® PowerPoint® 2013', 'page': 1, 'author': 'PC', 'creationdate': '2023-09-12T11:20:24+09:00', 'page_label': '2', 'producer': 'Microsoft® PowerPoint® 2013', 'title': 'PowerPoint 프레젠테이션', 'moddate': '2023-09-12T11:20:24+09:00', 'source': './snow-white.pdf', 'total_pages': 6}, page_content='거울아. 이 세상에서 누가 가장 아름답니?” “왕비님도 아름답지만 백설공주가 더 아름답습니다.” 화가 난 왕비는 사냥꾼을 불렀어요. 왕비는 사냥꾼에게 백설공주를 죽이라고'), Document(id='fa4589d8-6cc9-44ca-bf6d-aa2c8bc13f4f', metadata={'page_label': '3', 'author': 'PC', 'page': 2, 'moddate': '2023-09-12T11:20:24+09:00', 'title': 'PowerPoint 프레젠테이션', 'producer': 'Microsoft® PowerPoint® 2013', 'source': './snow-white.pdf', 'creationdate': '2023-09-12T11:20:24+09:00', 'total_pages': 6, 'creator': 'Microsoft® PowerPoint® 2013'}, page_content='왕비는 다시 요술 거울에게 누가 가장 아름다운 지 물었어요. “왕비님도 아름답지만 백설공주님이 천배는 더 아름답습니다.” “사냥꾼이 날 속였구나. 내가 직접 해치우겠어!”'), Document(id='2ad9314c-4101-47ce-be92-b8fbe6f9c403', metadata={'author': 'PC', 'moddate': '2023-09-12T11:20:24+09:00', 'page_label': '5', 'total_pages': 6, 'page': 4, 'creator': 'Microsoft® PowerPoint® 2013', 'title': 'PowerPoint 프레젠테이션', 'producer': 'Microsoft® PowerPoint® 2013', 'creationdate': '2023-09-12T11:20:24+09:00', 'source': './snow-white.pdf'}, page_content='“백설공주님, 못된 왕비의 꾐에 넘어갔군요.” “여전히 아름다운 우리 공주님을 캄캄한 땅속에 묻을 순 없어.” “오래오래 볼 수 있게 유리 관에 모시자.” 어느 날, 한 왕자가')]]\n\n <<최종 응답 형식>>\n 답변:\n 참조문서:\n - <<source>> (page: <<page>>): <<page_content>>\n", additional_kwargs={}, response_metadata={})])
(2) LLM 생성
from langchain_openai import ChatOpenAI
model = ChatOpenAI(
model='gpt-4o-mini',
temperature=0.5
)
(3) Chain 생성
from langchain_core.output_parsers import StrOutputParser
chain = prompt_tpl | model | StrOutputParser()
사용
query = "왕비가 백설공주에게 먹인 것은 무엇인가요?"
retrievals = retriever.batch([query])
# context_text = '\n'.join([doc.page_content for doc in retrievals[0]])
response = chain.invoke({'query': query, 'context': retrievals})
# response = chain.invoke({'query': query, 'context': context_text})
print(response)
"""
답변: 왕비가 백설공주에게 먹인 것은 독이 발라진 사과예요. 왕비는 먹음직스럽게 생긴 사과를 골라서 백설공주에게 주었죠.
참조문서:
- ./snow-white.pdf (page: 3): "왕비는 먹음직스럽게 생긴 사과를 골라 독을 발랐어요."
"""
# RetrievalQA 사용
from langchain.chains import RetrievalQA
retrieval_qa = RetrievalQA.from_chain_type(
llm=model,
retriever=retriever,
chain_type='stuff'
)
response = retrieval_qa.invoke('난쟁이는 몇 명인가요?')
print(response)
# {'query': '난쟁이는 몇 명인가요?', 'result': '일곱 명입니다.'}'LLM' 카테고리의 다른 글
| 3.3.16 [LLM] 파인튜닝 (0) | 2025.09.14 |
|---|---|
| 3.3.15 [LLM] 프롬프트 엔지니어링 응용: CoT (0) | 2025.09.14 |
| 3.3.13 [LLM] 프롬프트 엔지니어링 응용: LangChain (2) | 2025.09.13 |
| 3.3.12 [LLM] 프롬프트 엔지니어링: 벡터 데이터베이스 (3) | 2025.09.13 |
| 3.3.11 [LLM] Ollama (3) | 2025.09.02 |