
대부분의 문서 기반 검색 시스템은 RAG입니다.
하지만, 최근 발표된 NotebookLM은 조금 다른 부분이 있습니다.
거기에서, 주목을 받는 용어가 CAG입니다.
Local RAG와 Notebook LM처럼 구글의 자료를 기반으로 문서를 작성합니다.
이 코드는 RAG(Retrieval Augmented Generation)와 CAG(Citation Augmented Generation)의 개념적인 차이를 보여주기 위한 간단한 파이썬 샘플입니다. 실제 LLM(Large Language Model) API를 사용하지 않고, 가상적인(Mock) 함수를 사용하여 검색된 문서를 기반으로 응답을 생성하는 과정을 시뮬레이션합니다.
핵심 차이:
- RAG: 외부 문서에서 관련 정보를 검색하여 LLM에 컨텍스트로 제공하고, LLM은 이 컨텍스트를 활용하여 응답을 생성합니다. 생성된 응답은 검색된 정보를 기반으로 하지만, 명시적인 출처 표시는 포함되지 않을 수 있습니다. (구현 방식에 따라 다름)
- CAG: RAG와 유사하게 관련 정보를 검색하지만, LLM이 응답을 생성할 때 응답의 특정 부분이 검색된 어떤 문서를 기반으로 하는지 명시적으로 출처를 표시합니다. 이를 통해 사용자는 정보의 근거를 확인할 수 있습니다.
샘플 코드 구성:
corpus
: 검색 대상이 될 가상 문서 목록입니다.retrieve_documents
: 쿼리와 관련된 문서를 검색하는 가상 함수입니다. 여기서는 간단히 쿼리 키워드가 포함된 문서를 찾습니다.mock_llm
: 검색된 문서와 쿼리를 받아 응답을 생성하는 가상 LLM 함수입니다.mode
인자를 통해 RAG 모드와 CAG 모드의 응답 형식을 시뮬레이션합니다.run_rag
/run_cag
: 실제 RAG 및 CAG 파이프라인을 시뮬레이션하는 함수입니다. 검색 후mock_llm
을 호출하여 결과를 출력합니다.
import re
# 1. 가상 문서 코퍼스 (Corpus)
corpus = [
"Source 1: 파리의 수도는 프랑스입니다. 파리는 에펠탑으로 유명합니다.",
"Source 2: 일본은 동아시아의 섬나라입니다. 수도는 도쿄입니다.",
"Source 3: 에펠탑은 프랑스 파리에 있는 유명한 랜드마크입니다. 1889년에 완성되었습니다.",
"Source 4: 도쿄는 큰 대도시이며 일본 황제의 거주지입니다.",
"Source 5: 서울은 대한민국의 수도입니다."
]
# 2. 가상 문서 검색 함수 (Retrieval)
def retrieve_documents(query, corpus, top_k=2):
"""
쿼리와 관련된 문서를 코퍼스에서 검색하는 가상 함수 (단순 키워드 매칭)
실제로는 벡터 검색, BM25 등 고급 검색 기법 사용
"""
print(f"DEBUG: Searching for documents related to: '{query}'")
query_keywords = query.lower().split()
scored_docs = []
for i, doc in enumerate(corpus):
score = sum(keyword in doc.lower() for keyword in query_keywords if len(keyword) > 1) # 짧은 단어 제외
if score > 0:
scored_docs.append((score, doc))
# 점수 기준으로 내림차순 정렬 후 top_k개 반환
scored_docs.sort(key=lambda x: x[0], reverse=True)
retrieved = [doc for score, doc in scored_docs[:top_k]]
print(f"DEBUG: Retrieved {len(retrieved)} documents.")
return retrieved
# 3. 가상 LLM 함수 (Mock LLM for Generation)
def mock_llm(query, retrieved_context, mode="rag"):
"""
검색된 문서를 기반으로 응답을 생성하는 가상 LLM 함수
'mode'에 따라 RAG (합성된 응답) 또는 CAG (합성된 응답 + 출처) 형식을 시뮬레이션
"""
context_str = "\n".join(retrieved_context)
print(f"DEBUG: Mock LLM received context:\n{context_str}")
# --- 응답 생성 시뮬레이션 로직 ---
# 실제 LLM은 이 context_str과 query를 읽고 자연어 응답을 생성
# 여기서는 간단히 컨텍스트 내용을 조합하여 응답 생성 시뮬레이션
response_parts = []
citations = {}
citation_counter = 1 # CAG 모드에서 사용할 출처 번호
# 쿼리 키워드와 관련된 정보를 컨텍스트에서 찾아 응답 구성
if "프랑스" in query or "파리" in query or "에펠탑" in query:
paris_info = next((doc for doc in retrieved_context if "파리" in doc or "프랑스" in doc or "에펠탑" in doc), None)
if paris_info:
# 예시: "프랑스의 수도는 파리이며, 파리는 에펠탑으로 유명합니다."와 같은 응답 생성
match_capital = re.search(r"수도는 (.+?)입니다", paris_info)
match_landmark = re.search(r"(.+?)으로 유명합니다", paris_info)
match_landmark_alt = re.search(r"(.+?)에 있는 유명한 랜드마크입니다", paris_info)
fact1 = ""
fact2 = ""
if match_capital:
fact1 = f"프랑스의 수도는 {match_capital.group(1)}입니다."
if mode == "cag":
citations[citation_counter] = paris_info # 해당 사실의 출처
fact1 += f" [{citation_counter}]"
citation_counter += 1
response_parts.append(fact1)
if match_landmark or match_landmark_alt:
landmark = match_landmark.group(1) if match_landmark else match_landmark_alt.group(1)
fact2 = f"{match_capital.group(1) if match_capital else '그곳은'} {landmark}으로 유명합니다."
if mode == "cag":
citations[citation_counter] = paris_info # 해당 사실의 출처
fact2 += f" [{citation_counter}]"
citation_counter += 1
response_parts.append(fact2)
elif "일본" in query or "도쿄" in query:
japan_info = next((doc for doc in retrieved_context if "일본" in doc or "도쿄" in doc), None)
if japan_info:
# 예시: "일본은 섬나라이며, 수도는 도쿄입니다."와 같은 응답 생성
match_country_type = re.search(r"(.+?)의 섬나라입니다", japan_info)
match_capital = re.search(r"수도는 (.+?)입니다", japan_info)
fact1 = ""
fact2 = ""
if match_country_type:
fact1 = f"{match_country_type.group(1)}의 섬나라인 일본의"
if mode == "cag":
citations[citation_counter] = japan_info
fact1 += f" [{citation_counter}]"
citation_counter += 1
response_parts.append(fact1)
if match_capital:
# 만약 fact1이 이미 있다면 이어서, 없다면 시작
fact2 = f"수도는 {match_capital.group(1)}입니다."
if mode == "cag":
citations[citation_counter] = japan_info
fact2 += f" [{citation_counter}]"
citation_counter += 1
if response_parts and response_parts[-1].endswith("일본의"):
response_parts[-1] += " " + fact2 # fact1에 이어서 붙임
else:
response_parts.append(fact2) # fact2만 추가
elif "대한민국" in query or "서울" in query:
korea_info = next((doc for doc in retrieved_context if "대한민국" in doc or "서울" in doc), None)
if korea_info:
fact1 = korea_info.replace("Source 5: ", "") # 소스 정보 제거
if mode == "cag":
citations[citation_counter] = korea_info
fact1 += f" [{citation_counter}]"
citation_counter += 1
response_parts.append(fact1)
# 기본 응답 (키워드 매칭이 약하거나 없는 경우)
if not response_parts and retrieved_context:
response_parts.append("제공된 문서를 기반으로 응답을 생성합니다.")
if mode == "cag":
# 컨텍스트 전체를 출처로 표시
for i, doc in enumerate(retrieved_context):
citations[f"Context {i+1}"] = doc
response_parts[-1] += " [컨텍스트 참조]"
generated_text = " ".join(response_parts)
# CAG 모드인 경우 출처 정보 추가
if mode == "cag" and citations:
generated_text += "\n\n--- 출처 ---"
for citation_id, source_text in citations.items():
generated_text += f"\n[{citation_id}] {source_text}"
# 컨텍스트 참조가 있다면 실제 컨텍스트 내용을 출처에 추가
if "[컨텍스트 참조]" in generated_text and "Context 1" in citations:
pass # 이미 위에 추가됨
return generated_text
# 4. RAG 및 CAG 실행 함수
def run_rag(query, corpus, top_k=2):
print("\n" + "="*40)
print(f"--- RAG 실행 ---")
print(f"쿼리: '{query}'")
print("="*40)
retrieved_docs = retrieve_documents(query, corpus, top_k)
print("\n[검색된 문서]:")
for i, doc in enumerate(retrieved_docs):
print(f"- {doc}")
print("\n[RAG 응답 생성]:")
rag_response = mock_llm(query, retrieved_docs, mode="rag")
print(rag_response)
print("="*40 + "\n")
return rag_response
def run_cag(query, corpus, top_k=2):
print("\n" + "="*40)
print(f"--- CAG 실행 ---")
print(f"쿼리: '{query}'")
print("="*40)
retrieved_docs = retrieve_documents(query, corpus, top_k)
print("\n[검색된 문서]:")
for i, doc in enumerate(retrieved_docs):
print(f"- {doc}")
print("\n[CAG 응답 생성]:")
# CAG는 RAG와 동일한 검색 결과를 사용하지만, LLM의 응답 형식이 다름
cag_response = mock_llm(query, retrieved_docs, mode="cag")
print(cag_response)
print("="*40 + "\n")
return cag_response
# --- 샘플 실행 ---
query1 = "프랑스의 수도와 유명한 것은 무엇인가요?"
query2 = "일본의 수도에 대해 알려주세요."
query3 = "대한민국의 수도는 어디인가요?"
query4 = "에펠탑에 대해 말해주세요." # 쿼리 키워드와 직접 연결되지 않는 경우
# RAG와 CAG 비교 실행
run_rag(query1, corpus)
run_cag(query1, corpus)
run_rag(query2, corpus)
run_cag(query2, corpus)
run_rag(query3, corpus)
run_cag(query3, corpus)
run_rag(query4, corpus)
run_cag(query4, corpus)
윤영기(尹泳祺)
Yoon, Young-Ki
younggiyoon@gmail.com
newton@eqboard.com