드디어 RAG 3부작의 완결편입니다. 로드와 분할(1편), 임베딩과 저장(2편)을 마쳤으니, 이제 검색 결과를 프롬프트에 끼워 넣어 AI가 '오픈북으로' 답하게 만들 차례입니다.
리트리버: 검색 부품의 표준 규격
리트리버(Retriever)는 '질문을 넣으면 관련 문서 목록이 나오는' 표준 부품입니다. 벡터스토어에 as_retriever()를 붙이면 만들어집니다. 굳이 리트리버로 감싸는 이유는 체인에 파이프로 끼울 수 있는 규격(Runnable)이 되기 때문입니다.
벡터스토어를 리트리버로 변환
# 이전 레슨에서 만든 vectorstore를 이어서 사용합니다
retriever = vectorstore.as_retriever(
search_kwargs={'k': 3}, # 관련 청크를 3개까지 가져오기
)
# 리트리버도 invoke로 실행하는 표준 부품입니다
docs = retriever.invoke('환불 규정이 어떻게 되나요?')
for d in docs:
print('-', d.page_content)
전체 조립: 문서 기반 QA 챗봇
RAG 체인의 데이터 흐름
- 사용자 질문이 들어옵니다
- 리트리버가 질문과 관련된 청크 3개를 찾아옵니다
- 찾은 청크들을 하나의 문자열(context)로 합칩니다
- 프롬프트의 {context} 자리에 끼워 넣습니다
- 모델이 context에 근거해 답하고, 파서가 텍스트로 정리합니다
완성: RAG QA 챗봇 전체 코드
from langchain.chat_models import init_chat_model
from langchain_chroma import Chroma
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import WebBaseLoader
# --- 준비: 로드 -> 분할 -> 저장 (레슨 5~6 복습) ---
docs = WebBaseLoader('https://ko.wikipedia.org/wiki/인공지능').load()
chunks = RecursiveCharacterTextSplitter(
chunk_size=800, chunk_overlap=150
).split_documents(docs)
vectorstore = Chroma.from_documents(
chunks, OpenAIEmbeddings(model='text-embedding-3-small')
)
retriever = vectorstore.as_retriever(search_kwargs={'k': 3})
# --- RAG 체인 조립 ---
prompt = ChatPromptTemplate.from_template(
'아래 참고자료만 근거로 질문에 답하세요. '
'자료에 없는 내용은 모른다고 답하세요.\n\n'
'[참고자료]\n{context}\n\n[질문]\n{question}'
)
def format_docs(docs):
# 찾아온 청크들을 빈 줄로 구분해 하나의 문자열로 합칩니다
return '\n\n'.join(d.page_content for d in docs)
rag_chain = (
# question은 그대로 통과시키고, context는 검색해서 채웁니다
{'context': retriever | format_docs, 'question': RunnablePassthrough()}
| prompt
| init_chat_model('openai:gpt-4o-mini', temperature=0)
| StrOutputParser()
)
print(rag_chain.invoke('인공지능이라는 용어는 언제 처음 등장했나요?'))
체인 첫 줄의 딕셔너리가 낯설 수 있습니다. 이것은 '입력을 두 갈래로 나누는 분배기'입니다. 사용자 질문 하나가 들어오면, 한 갈래는 리트리버를 거쳐 context가 되고, 다른 갈래(RunnablePassthrough)는 그대로 question이 되어 프롬프트의 두 구멍을 채웁니다.
환각(할루시네이션) 줄이는 프롬프트 요령
- '참고자료에만 근거해 답하라'를 명시합니다 (오픈북 규칙)
- '자료에 없으면 모른다고 답하라'로 지어내기를 차단합니다
- 답변에 출처(metadata의 source)를 함께 표시하게 하면 신뢰도가 올라갑니다
RAG 품질이 낮다면 십중팔구 '생성'이 아니라 '검색' 문제입니다. rag_chain 전에 retriever.invoke(질문)를 따로 실행해서 엉뚱한 청크가 나오는지 먼저 확인하세요. 검색이 정확하면 답변은 거의 자동으로 좋아집니다.
k(가져올 청크 수)를 무작정 키우면 관련 없는 내용이 섞여 답변이 오히려 나빠지고 토큰 비용만 늘어납니다. k=3~5에서 시작해 조절하세요.