RAG


먼저 이름부터 짚어보겠습니다. RAG는 retrieval-augmented generation(검색 증강 생성)의 줄임말입니다. 질문을 받는 순간 관련된 텍스트를 직접 가져와 모델에 건네주면, 모델은 얼어붙은 기억이 아니라 여러분이 제공한 사실을 바탕으로 답변합니다. 이 장에서는 이 "검색 후 답변" 루프가 어떻게 동작하는지, 그리고 어디서 잘못될 수 있는지 살펴봅니다.
검색 후 생성 패턴
모델은 회사의 도움말 문서나 제품 정보, 학습 이후에 벌어진 일을 알지 못합니다. 그런데도 물어보면 모른다고 인정하거나, 더 나쁘게는 그럴듯하게 들리는 지어낸 답변을 내놓습니다. RAG는 질문을 받는 순간 관련 텍스트를 모델에 직접 건네줌으로써 이 문제를 해결합니다.
이 아이디어는 임베딩을 알고 나면 확실히 이해가 됩니다. 임베딩은 텍스트를 벡터로 바꿔서 얼마나 가까운지 비교할 수 있게 해주는 기법입니다(코사인 유사도는 두 벡터가 얼마나 비슷한 방향을 가리키는지를 측정합니다). **검색 증강 생성**은 일반적인 모델 호출에 두 단계를 추가한 것입니다.
- 검색(Retrieve). 사용자의 질문을 받아 문서에서 가장 관련성 높은 부분을 찾아, 상위 후보들을 모읍니다.
- 생성(Generate). 그 텍스트들을 프롬프트에 컨텍스트로 넣고, 모델에게 그 컨텍스트만 사용해서 질문에 답하도록 시킵니다.
글을 쓰는 능력은 모델의 일반적인 언어 능력이 담당하고, 사실은 검색해온 텍스트가 제공합니다. 그 결과 여러분의 콘텐츠에 근거를 둔 유창한 답변을 얻을 수 있고, 무엇을 넣을지 직접 통제하기 때문에 최신 정보도 유지할 수 있습니다.
예측 루프의 관점에서 보면 이 방식이 왜 이렇게 잘 통하는지 더 잘 이해할 수 있습니다. RAG가 없으면 모델은 파라미터에 얼어붙어 있는 패턴에서 사실을 기억해내야 하는데, 그 사실이 아예 없을 수도 있어서 자신 있게 추측하는 결과로 이어집니다. RAG를 쓰면 그 사실이 컨텍스트 안에 바로 놓여 있으므로, 모델이 할 일은 "이걸 기억해내라"에서 "눈앞에 있는 걸 읽고 그걸 바탕으로 답하라"로 바뀝니다. 검색은 작업의 성격을 기억에서 읽기로 바꿔줍니다. 시험에 비유하면 명확합니다. 참고 자료 없이 보는 시험은 추측을 부르지만, RAG는 관련 페이지가 책상 위에 펼쳐진 채로 보는 오픈북 시험으로 바꿔줍니다.
청킹
문서 전체를 그대로 임베딩하지는 않습니다. 먼저 더 작은 **청크(chunk)**로 나눈 뒤, 한 문단이나 몇 문장 단위로 잘라서 임베딩합니다. 이유는 두 가지입니다. 첫째, 검색이 더 정밀해집니다. 40페이지짜리 매뉴얼 전체가 아니라 관련 있는 한 문단만 뽑아낼 수 있습니다. 둘째, 청크는 컨텍스트 윈도우에 맞아떨어지는데, 거대한 문서 전체는 그렇지 않을 수 있습니다.
# 간단한 청커: 문단 단위로 나눕니다
def chunk(text):
return [c.strip() for c in text.split("\n\n") if c.strip()]청크 크기는 균형의 문제이며, 그 이유는 임베딩에서 찾을 수 있습니다. 각 청크는 하나의 벡터로 변환되어 전체 의미를 요약합니다. 청크를 너무 크게 잡으면 여러 주제를 아우르게 되어, 그 하나의 벡터가 모든 주제의 흐릿한 평균값이 되어 어떤 구체적인 질문에도 잘 맞지 않습니다. 너무 작게 잡으면 뜻을 통하게 해주던 주변 맥락을 잃어버려서, 벡터가 조각난 부분만을 가리키게 됩니다. 문단 단위 청크는 각각 하나의 완결된 아이디어를 담고 있습니다. 검색 결과를 보면서 여기서부터 조정해 나가면 됩니다.
벡터는 어디에 저장할까
청크가 몇 개뿐이라면 벡터를 메모리에 두고 임베딩 장에서 다룬 코사인 유사도 함수로 비교하면 됩니다. 뒤에 나오는 예제도 이 방식을 사용합니다. 청크가 수천, 수백만 개라면 이 방식은 느려지고, 그때는 **벡터 스토어**를 쓰게 됩니다. 이는 대규모로 가장 가까운 벡터를 빠르게 찾아내도록 만들어진 데이터베이스입니다.
RAG를 배우는 데는 벡터 스토어가 필요 없고, 지금 당장 무엇을 쓸지 고를 필요도 없습니다. 호스팅 서비스와 라이브러리로 여러 종류가 존재하지만, 중요한 건 개념입니다. 임베딩을 저장해두고 빠르게 검색할 수 있는 곳이라는 점입니다. 일단 메모리에서 시작하고, 컬렉션이 커지면 벡터 스토어로 옮기세요.
근거 확보와 출처 표시
생성 단계는 결국 프롬프트이며, 어떻게 작성하느냐가 RAG가 실제로 지어낸 답변을 줄여주는지를 결정합니다. 두 가지 지시가 핵심적인 역할을 합니다. 모델에게 제공된 컨텍스트에서 만 답하도록 지시하고, 컨텍스트에 답이 없으면 그렇다고 말하도록 지시하는 것입니다.
context_text = "\n\n".join(retrieved_chunks)
system_prompt = f'''아래 컨텍스트만 사용해서 질문에 답하세요.
컨텍스트에 답이 없다면 "그 정보는 갖고 있지 않습니다"라고 말하세요.
답변에서 컨텍스트의 관련 부분을 인용하세요.
컨텍스트:
"""
{context_text}
"""'''여기에는 앞서 배운 두 가지 내용이 함께 담겨 있습니다. 구분자와 "이것만 사용해서 답하라"는 명시적인 지시는 프롬프팅에서 나온 것입니다. "그 정보는 갖고 있지 않습니다"라고 말할 수 있게 허용하는 것은 환각 방어책입니다. 도망갈 구멍이 있는, 근거에 기반한 모델은 추측만 하도록 내버려진 모델보다 훨씬 덜 지어냅니다. 근거 확보와 도망갈 구멍, 이 둘이 함께 지어낸 답변을 줄여줍니다. 출처를 인용하도록 요청하면 사용자가 확인할 수 있는 근거도 함께 생깁니다.
최근접 탐색을 넘어서: 하이브리드 검색과 재순위화
순수한 벡터 검색은 의미를 기준으로 매칭하며, 대부분의 경우 이게 원하는 방식입니다. 하지만 맹점이 있습니다. 중요한 정확한 단어를 놓칠 수 있다는 점입니다. E_4021 같은 오류 코드나 특정 제품명을 검색하면, 의미 기반 검색은 같은 주제에 관한 청크들을 돌려주면서도 정확한 문자열을 담고 있는 그 청크를 빠뜬 채로 넘길 수 있습니다.
이 문제를 해결하는 방법에는 이름이 있습니다. **하이브리드 검색**입니다. 의미 기반 검색과 예전 방식의 키워드 검색을 결합해서, 정확한 용어와 전반적인 의미 둘 다 반영되도록 하는 것입니다. RAG를 시작하는 데 이걸 굳이 만들 필요는 없습니다. 정확한 용어가 중요하고 순수 벡터 검색이 자꾸 그걸 놓칠 때 도입하세요.
실전에서
최소한의 RAG 답변 예제입니다. 임베딩 장에서 만든 search와 embed 헬퍼를 재사용합니다.
def answer_from_docs(question, index):
# 1. 검색: 질문에 가장 잘 맞는 상위 청크들
query_vector = embed(question)
scored = [{"text": item["text"], "score": cosine_similarity(query_vector, item["vector"])} for item in index]
top = sorted(scored, key=lambda x: x["score"], reverse=True)[:3]
# 2. 생성: 그 청크들만 사용해서 답하기
context = "\n\n".join(t["text"] for t in top)
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": f'이 컨텍스트만 사용해서 답하세요. 여기 없으면 모른다고 말하세요.\n\n"""{context}"""'},
{"role": "user", "content": question},
],
)
return response.choices[0].message.content가장 가까운 청크 세 개를 검색해서 프롬프트에 넣고, 모델이 그것들을 바탕으로 답하게 하는 것입니다. 이것이 **RAG 파이프라인**의 전부입니다. 임베딩 검색이 모델 호출에 재료를 공급하고, 근거에 기반한 프롬프트가 이를 하나로 묶어줍니다. 품질의 거의 전부는 올바른 청크를 검색하는 데 달려 있습니다.
RAG가 필요 없을 때
RAG는 공짜가 아니며, 항상 정답도 아닙니다. 다음의 경우에는 건너뛰세요.
- 데이터가 작아서 프롬프트에 다 들어갈 때. 전체 지식 베이스가 몇 문단 정도라면 그냥 직접 붙여넣으세요. RAG는 한 번에 다 보낼 수 없을 만큼 많을 때를 위한 것입니다.
- 긴 컨텍스트 모델이 전부 담을 수 있을 때. 일부 모델은 매우 큰 입력을 받아서 문서 전체를 한 번에 넣을 수 있는데, 이는 검색 시스템을 구축하는 것보다 일이 적을 수 있습니다.
그리고 진짜 한계를 기억하세요. RAG의 품질은 대부분 검색의 품질입니다. 검색 단계가 잘못된 청크를 돌려주면, 완벽한 프롬프트로도 답을 구할 수 없습니다. 실제 RAG 시스템에서 대부분의 노력은 최종 모델 호출이 아니라 올바른 텍스트를 검색하는 데 들어갑니다.
임베딩과 RAG는 모델 자체를 바꾸지 않고도 질문 시점에 지식을 제공해줍니다. 다음 장인 파인튜닝에서는 또 다른 선택지를 다룹니다. 여러분의 예제로 모델 자체를 바꾸는 방법, 그리고 그것이 프롬프팅이나 RAG보다 나은 상황을 살펴봅니다.

