RAG

먼저 이름부터 살펴봅시다. RAG는 **검색 증강 생성(retrieval-augmented generation)**의 줄임말입니다. 질문이 들어오는 순간 관련 텍스트를 직접 가져와 모델에 전달하면, 모델은 고정된 기억이 아닌 여러분이 제공한 사실을 바탕으로 답변합니다. 이 장에서는 그 검색-후-답변 루프가 어떻게 작동하고 어디서 잘못될 수 있는지를 다룹니다.
검색 후 생성 패턴
모델은 여러분 회사의 도움말 문서, 제품 세부 정보, 또는 학습 이후에 발생한 일들을 알지 못합니다. 그런 내용을 물어보면 모델은 도움을 줄 수 없다고 인정하거나, 더 나쁜 경우에는 그럴듯하게 들리는 내용을 꾸며내기도 합니다. RAG는 질문이 들어오는 순간 모델에 관련 텍스트를 전달함으로써 이 문제를 해결합니다.
이 아이디어는 임베딩을 이해하면 비로소 명확해집니다. 임베딩은 텍스트를 벡터로 변환해 근접도(두 벡터가 얼마나 가까운 방향을 가리키는지를 측정하는 코사인 유사도)로 비교할 수 있게 하는 기법입니다. **검색 증강 생성**은 일반적인 모델 호출에 두 단계를 추가한 것입니다.
- 검색. 사용자의 질문을 받아 문서에서 가장 관련성 높은 부분을 찾고, 상위 결과를 수집합니다.
- 생성. 해당 부분을 컨텍스트로 프롬프트에 삽입한 뒤, 모델에 그 컨텍스트만을 사용하여 질문에 답하도록 요청합니다.
모델의 언어 능력이 글쓰기를 담당하고, 검색된 텍스트가 사실을 제공합니다. 여러분이 무엇을 넣을지 제어하기 때문에, 여러분의 콘텐츠에 근거하고 최신 상태를 유지하는 유창한 답변을 얻을 수 있습니다.
이것이 왜 잘 작동하는지, 예측 루프의 관점에서 살펴보면 도움이 됩니다. RAG 없이는 모델이 파라미터에 고정된 패턴에서 사실을 떠올려야 하는데, 그 사실이 없을 수도 있어 자신감 있는 추측으로 이어집니다. RAG를 사용하면 사실이 컨텍스트 안에 바로 있으므로, 모델의 역할이 "이것을 기억해"에서 "눈앞에 있는 것을 읽고 답해"로 바뀝니다. 검색은 작업을 기억에서 읽기로 전환합니다. 시험으로 비유하자면, 책 없이 보는 시험은 추측을 유도하는 반면, RAG는 관련 페이지가 책상 위에 펼쳐진 오픈북 시험으로 만들어 줍니다.
청킹
문서 전체를 임베딩하지 않습니다. 먼저 문단이나 몇 개의 문장 크기로 작은 **청크**로 분할한 뒤 임베딩합니다. 이유는 두 가지입니다. 첫째, 검색이 더 정밀해집니다. 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를 시작하는 데 이것을 구축할 필요는 없습니다. 정확한 용어가 중요하고 순수 벡터가 계속 놓칠 때 이것을 사용하세요.
실제 적용
임베딩 장의 search와 embed 헬퍼를 재사용한 최소한의 RAG 답변:
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보다 가치 있는 경우입니다.

