S9 · 재순위(Rerank) & 하이브리드 검색 — 근거를 더 정밀하게
Agentic RAG 실전 (14시간) · Day 2
벡터 검색의 순위가 항상 정확하진 않다. "넓게 뽑고 → 정밀하게 다시 줄 세우는" 재순위로 근거 품질을 높이고, 키워드를 함께 보는 하이브리드 검색 개념을 익힌다. S0 .venv/.env 로 실행.
목표
- 재순위의 원리와 "넓게 검색 → 좁게 재정렬" 전략을 이해한다.
- PineconeRerank 로 결과를 재정렬하고 전/후를 비교한다.
- 하이브리드 검색(의미+키워드)의 개념을 안다.
1. 왜 재순위인가
벡터 검색의 유사도 점수가 항상 '진짜 관련성' 순서와 일치하진 않는다. 답이 되는 청크가 뒤로 밀리거나 표현만 비슷한 청크가 위로 오기도 한다.
재순위(reranking) 는 1차 벡터 검색이 뽑은 후보를 더 정교한 모델이 질문과의 관련성으로 다시 평가해 순서를 바로잡는 단계다.
- 1차 벡터 검색: 빠르지만 거칠다 → 후보를 넓게(예: 10개) 확보.
- 재순위: 느리지만 정교하다 → 그 10개를 다시 채점해 상위 3개만 남긴다.
전략: 리콜은 벡터로 넓게, 정밀도는 재순위로 좁게.
2. 크로스 인코더(Cross-Encoder) — 왜 더 정확한가
- 바이 인코더(Bi-Encoder) 도 질문 벡터와 문서 벡터의 유사도를 계산한다.
- 크로스 인코더(Cross-Encoder) 도 결국 질문과 문서의 관련성을 점수로 낸다.
즉, 둘 다 점수를 계산한다. 차이는 점수를 만들기 전에 질문과 문서를 어떻게 보느냐에 있다.
먼저 한 문장으로 요약
- 바이 인코더: 질문과 문서를 각자 따로 읽고 요약한 뒤 비교한다.
- 크로스 인코더: 질문과 문서를 처음부터 같이 읽으면서 비교한다.
바이 인코더(Bi-Encoder): 각자 먼저 요약하고, 나중에 비교
- 질문을 혼자 읽고 벡터(Vector)로 만든다.
- 문서도 혼자 읽고 벡터로 만든다.
- 그다음 두 벡터가 얼마나 가까운지 계산한다.
즉, 질문과 문서는 서로 못 본 상태에서 각자 요약본을 만든 뒤, 그 요약본끼리 비교한다.
이 방식은 빠르다.
- 문서 벡터를 미리 만들어 둘 수 있다.
- 검색할 때는 질문 벡터 하나만 만들면 된다.
- 그래서 수천, 수만 개 문서도 빠르게 찾을 수 있다.
하지만 질문이 정말 원하는 포인트를 놓칠 수 있다.
- 배포라는 주제는 비슷한데
- 질문이 묻는 핵심이 누가 승인하느냐인지
- 언제 배포하느냐인지
- 롤백은 얼마나 빠르냐인지까지는
- 벡터 하나로 압축되는 과정에서 덜 선명해질 수 있다.
크로스 인코더(Cross-Encoder): 처음부터 같이 읽고, 그 자리에서 판단
- 질문과 문서를 처음부터 같이 넣는다.
- (질문, 문서)를 함께 읽는다.
- 이 문서가 이 질문에 답이 되나?를 바로 점수로 낸다.
여기서 (질문, 문서)는 질문 벡터와 문서 벡터가 아니라, 질문 원문 텍스트와 문서 원문 텍스트의 한 쌍을 뜻한다.
즉, 질문과 문서를 따로 요약해 놓고 비교하는 것이 아니라, 둘을 나란히 놓고 바로 대조한다.
같은 예시로 비교
질문이 다음과 같다고 하자.
- 질문: 배포 승인은 누가 하나요?
후보 문서가 두 개 있다.
- 문서 A: 배포는 카나리 방식으로 진행되며, 오류율을 보며 점진적으로 트래픽을 늘린다.
- 문서 B: 프로덕션 배포는 QA 승인 이후 릴리스 담당자가 태그를 만들고, 마지막으로 엔지니어링 리드가 승인한다.
바이 인코더는 어떻게 볼까?
- 질문은 배포, 승인, 절차 같은 의미를 가진 벡터로 압축된다.
- 문서 A도 배포 방식이라는 점에서 질문과 꽤 비슷해 보일 수 있다.
- 문서 B도 배포 승인과 직접 관련 있으니 비슷해 보인다.
- 그래서 둘 다 상위 후보로 들어올 수 있다.
즉, 바이 인코더는 대충 같은 주제인가?를 빠르게 잘 찾는다.
크로스 인코더는 어떻게 볼까?
- (질문, 문서 A)를 함께 읽으면: 배포 주제는 맞지만 누가 승인하느냐에는 답이 없다고 본다.
- (질문, 문서 B)를 함께 읽으면: 릴리스 담당자, 엔지니어링 리드가 나와 있으므로 질문에 직접 답한다고 본다.
즉, 크로스 인코더는 같은 주제인가?보다 이 문서가 질문에 실제로 답이 되는가?를 더 잘 본다.
진짜 차이를 한 줄로 다시 말하면
- 바이 인코더: 질문과 문서를 따로 이해한 뒤, 서로 비슷한지 본다.
- 크로스 인코더: 질문을 옆에 두고 문서를 읽으면서, 질문에 답하는지 본다.
비유로 보면
- 바이 인코더: 질문 요약본 1장, 문서 요약본 1장을 만든 뒤 두 요약본의 분위기가 비슷한지 본다.
- 크로스 인코더: 질문 원문과 문서 원문을 책상 위에 같이 펼쳐 놓고 질문의 핵심 단서가 문서 안에 실제로 있는지 줄마다 확인한다.
그래서 왜 정확도 차이가 나나?
- 바이 인코더는 빠르지만, 질문과 문서를 따로 이해한 뒤 비교한다.
- 크로스 인코더는 느리지만, 질문과 문서를 같이 읽으면서 비교한다.
- 그래서 크로스 인코더가 질문 의도와 문서의 실제 답변성을 더 잘 잡는다.
대신 왜 느리나?
- 문서 1개마다 (질문 원문, 문서 원문) 쌍을 새로 만들어야 한다.
- 후보가 1,000개면 1,000번 함께 읽어야 한다.
- 그래서 전체 코퍼스에 바로 쓰기엔 비싸다.
실무에서는 이렇게 결합한다
- 바이 인코더(벡터 검색)로 후보를 넓게 찾는다. 예: 상위 10개
- 크로스 인코더(재순위)로 그 10개만 다시 정밀 평가한다. 예: 최종 3개
정리하면:
- 바이 인코더(Bi-Encoder): 각자 따로 요약 → 나중에 비교
- 크로스 인코더(Cross-Encoder): 처음부터 같이 읽기 → 그 자리에서 답변성 판단
- 그래서 바이 인코더는 빠른 1차 검색, 크로스 인코더는 정확한 재순위에 잘 맞는다.
3. 실행 준비
S0 .venv 커널. .env 키 로드.
import os
from getpass import getpass
# S0 프로젝트 루트의 .env 에서 키 로드 (없으면 입력으로 폴백)
try:
from dotenv import load_dotenv
load_dotenv()
except ImportError:
pass
for name in ["OPENAI_API_KEY", "PINECONE_API_KEY"]:
if not os.environ.get(name):
os.environ[name] = getpass(f"{name}: ")
print("키:", {k: bool(os.environ.get(k)) for k in ["OPENAI_API_KEY", "PINECONE_API_KEY"]})
키: {'OPENAI_API_KEY': True, 'PINECONE_API_KEY': True}
4. 인덱스 연결
import os
from pinecone import Pinecone
from langchain_pinecone import PineconeVectorStore, PineconeEmbeddings
from langchain_openai import ChatOpenAI
embeddings = PineconeEmbeddings(model="multilingual-e5-large") # 한국어 임베딩(1024차원)
pc = Pinecone(api_key=os.environ["PINECONE_API_KEY"])
index = pc.Index("agentic-rag-lab") # S2/S3에서 색인한 인덱스
# ★ 네임스페이스 미지정 시 빈 기본 네임스페이스를 조회한다. S3에서 색인한 chunk_500 을 메인 코퍼스로 사용.
NAMESPACE = "chunk_500"
vector_store = PineconeVectorStore(index=index, embedding=embeddings, namespace=NAMESPACE)
try:
_stats = index.describe_index_stats()
_ns = getattr(_stats, "namespaces", None) or (_stats.get("namespaces") if isinstance(_stats, dict) else {}) or {}
_cnt = {k: getattr(v, "vector_count", v.get("vector_count") if isinstance(v, dict) else None) for k, v in _ns.items()}
print("네임스페이스별 벡터 수:", _cnt)
if not _cnt.get(NAMESPACE):
print(f"⚠️ '{NAMESPACE}' 네임스페이스에 데이터가 없습니다. S3의 색인 셀을 먼저 실행하세요.")
except Exception as _e:
print("인덱스 통계 확인 생략:", _e)
llm = ChatOpenAI(model="gpt-4.1-mini", temperature=0) # 기본 LLM
print("연결 완료")
네임스페이스별 벡터 수: {'chunk_200': 89, 'chunk_1500': 12, 'chunk_1000': 18, 'demo': 3, 'chunk_500': 34}
연결 완료
5. 재순위 전 — 후보를 넓게 확보
k=10 으로 후보를 넓게 가져온다. 이 순서가 '재순위 전' 상태다.
query = "재택근무할 때 지켜야 하는 보안 규칙은?"
# 후보를 넓게(10개) 확보 — 점수까지 받아 '재순위 전' 순위를 그대로 보여준다.
scored = vector_store.similarity_search_with_score(query, k=10)
candidates = [doc for doc, _ in scored] # 재순위기에 넘길 문서 리스트
# 각 청크를 (제목, 청크번호)로 식별해 '벡터 검색 몇 위였는지' 매핑을 만들어 둔다.
def doc_key(d):
return (d.metadata.get("title"), d.metadata.get("chunk"))
dense_rank = {doc_key(doc): r for r, (doc, _) in enumerate(scored, 1)}
print("재순위 전 — 벡터 검색 상위 10개 (전체 후보):")
for r, (doc, score) in enumerate(scored, 1):
preview = doc.page_content[:42].replace("\n", " ")
print(f" {r:2d}. score={score:.3f} | [{doc.metadata['title']}] (chunk {doc.metadata.get('chunk')}) {preview}...")
재순위 전 — 벡터 검색 상위 10개 (전체 후보):
1. score=0.893 | [재택근무 및 유연근무 제도] (chunk 1.0) 재택 시에는 회사가 지급한 노트북과 VPN을 사용해야 하며, 개인 기기에서 ...
2. score=0.846 | [재택근무 및 유연근무 제도] (chunk 0.0) 재택근무는 주 최대 3일까지 가능하며, 매주 금요일은 전사 권장 재택일로 운...
3. score=0.844 | [재택근무 및 유연근무 제도] (chunk 2.0) 월 1회 이상은 팀 동기화를 위해 사무실 출근이 권장되며, 분기마다 전사 오...
4. score=0.835 | [보안 및 계정 관리 규정] (chunk 1.0) 권한은 최소 권한 원칙(least privilege)에 따라 부여하며, 직무...
5. score=0.831 | [보안 및 계정 관리 규정] (chunk 0.0) 모든 사내 시스템 접근에는 SSO와 2단계 인증(MFA)이 적용되며, MFA...
6. score=0.825 | [데이터 거버넌스 및 개인정보 보호] (chunk 1.0) 개인정보가 포함된 데이터를 분석에 활용할 때는 가명처리 또는 익명처리를 우선...
7. score=0.822 | [보안 및 계정 관리 규정] (chunk 2.0) 보안 사고가 의심되면 즉시 보안팀(security@)에 신고하고 해당 계정의...
8. score=0.803 | [경비 처리 및 법인카드 규정] (chunk 1.0) 식대는 1인 1식 2만 원 한도이며, 팀 회식은 1인 5만 원 한도로 사전 ...
9. score=0.803 | [경비 처리 및 법인카드 규정] (chunk 0.0) 업무 관련 경비는 법인카드 사용을 원칙으로 하며, 개인 결제 후 환급은 예외...
10. score=0.798 | [신규 입사자 온보딩 가이드] (chunk 0.0) 입사 첫날에는 노트북 수령, 계정 발급, 보안 교육을 완료하고 사내 위키 접...
6. 재순위 — PineconeRerank
Pinecone 호스팅 재순위 모델을 쓴다.
- 기본 bge-reranker-v2-m3 는 다국어(한국어 포함) 지원.
- top_n=3: 재순위 후 상위 3개만.
- compress_documents(문서들, 질문): 후보를 질문 기준으로 다시 채점·정렬해 상위 N개 반환.
from langchain_pinecone import PineconeRerank
reranker = PineconeRerank(
model="bge-reranker-v2-m3", # 다국어 재순위 모델(한국어 가능)
top_n=3, # 재순위 후 상위 3개만 남김
)
reranked = reranker.compress_documents(candidates, query) # 10개 후보 → 질문 기준 재정렬 → 상위 3개
print("재순위 후 — 상위 3개 (괄호 안은 벡터 검색에서의 원래 순위):")
for new_rank, d in enumerate(reranked, 1):
score = d.metadata.get("relevance_score") # 재순위 모델이 매긴 관련성 점수
old = dense_rank.get((d.metadata.get("title"), d.metadata.get("chunk")), "?") # 벡터 검색 원래 순위
preview = d.page_content[:42].replace("\n", " ")
print(f" 재순위 {new_rank}위 (← 벡터 {old}위) | score={score:.3f} | [{d.metadata['title']}] {preview}...")
print("\n해석: '벡터 N위 → 재순위 1~3위'로 끌어올려진 청크가 있으면, 재순위가 순서를 교정한 것이다.")
print(" 점수가 1위만 높고 나머지가 0에 가까우면, 이 질문에 강하게 맞는 청크가 사실상 하나라는 뜻.")
재순위 후 — 상위 3개 (괄호 안은 벡터 검색에서의 원래 순위):
재순위 1위 (← 벡터 1위) | score=0.935 | [재택근무 및 유연근무 제도] 재택 시에는 회사가 지급한 노트북과 VPN을 사용해야 하며, 개인 기기에서 ...
재순위 2위 (← 벡터 2위) | score=0.087 | [재택근무 및 유연근무 제도] 재택근무는 주 최대 3일까지 가능하며, 매주 금요일은 전사 권장 재택일로 운...
재순위 3위 (← 벡터 7위) | score=0.024 | [보안 및 계정 관리 규정] 보안 사고가 의심되면 즉시 보안팀(security@)에 신고하고 해당 계정의...
해석: '벡터 N위 → 재순위 1~3위'로 끌어올려진 청크가 있으면, 재순위가 순서를 교정한 것이다.
점수가 1위만 높고 나머지가 0에 가까우면, 이 질문에 강하게 맞는 청크가 사실상 하나라는 뜻.
🔍 관찰 포인트
- 재순위 전/후 1위가 바뀌는지 본다. 밀려 있던 더 정확한 청크가 위로 올라오는 경우가 많다.
- relevance_score 로 확신도를 가늠한다.
- LLM에 넣는 근거가 더 정확한 3개로 압축되어 답 품질↑·잡음↓·토큰↓.
7. RAG 파이프라인에 끼우기
"벡터로 넓게 → 재순위로 좁히기" 를 하나의 검색 함수로 묶어, S5~S8의 검색 단계를 교체할 수 있다.
def search_with_rerank(query: str, fetch_k: int = 10, top_n: int = 3) -> str:
candidates = vector_store.similarity_search(query, k=fetch_k) # 1) 넓게
top = reranker.compress_documents(candidates, query) # 2) 정밀 재정렬(상위 top_n)
return "\n\n".join(f"[{d.metadata['title']}] {d.page_content}" for d in top)
print(search_with_rerank("배포 승인은 누가 하나요?")[:200], "...")
# → 이 함수를 S8의 retrieve_node 안 검색 호출과 바꾸면, 교정 루프 + 재순위가 결합됩니다.
[배포 및 릴리스 프로세스] 프로덕션 배포는 QA 승인 이후 릴리스 담당자가 배포 태그를 생성하고, 마지막으로 엔지니어링 리드가 수동으로 승인하는 2단계 게이트를 거친다. 승인 기록과 변경 내역은 릴리스 노트로 자동 정리되어 추적성을 확보한다. 기능 플래그(feature flag)를 활용해 코드 배포와 기능 노출을 분리하며, 위험한 기능은 내부 사용자와 일 ...
8. 하이브리드 검색 — 의미 + 키워드 (개념)
벡터(의미) 검색은 강력하지만, 고유명사·코드·약어처럼 정확한 단어가 중요한 경우(예: "VPN", "Sev1", "main 브랜치")엔 약할 수 있다.
하이브리드 검색은 둘을 합친다.
- Dense(밀집) = 의미 기반(지금까지 쓴 것)
- Sparse(희소) = 키워드 일치 기반
두 점수를 합쳐 의미와 핵심 키워드가 모두 맞는 문서를 찾는다. Pinecone는 sparse를 지원하며 LangChain에 PineconeSparseVectorStore 가 있다. 다만 sparse 인덱스를 따로 구성해야 하므로 이 과정에서는 개념과 적용 위치만 다룬다.
원칙: 재순위는 거의 항상 도움이 되고 적용도 쉬워 우선 도입한다. 하이브리드는 키워드 정확도가 중요한 도메인에서 추가로 고려한다.
✏️ 미니 실습
- top_n(1·5), fetch_k(5·20)를 바꿔 결과 변화를 본다.
- S3의 청크 크기 비교와 결합해 (청크 크기)×(재순위 유무) 조합을 비교한다.
- search_with_rerank 를 S8의 retrieve_node 에 넣어 재순위+자기교정을 결합한다.
# 모범 답안 1) top_n(1·5), fetch_k(5·20)를 바꿔 결과 변화를 본다
def compare_rerank_configs(query: str, configs):
print("Q:", query)
for fetch_k, top_n in configs:
local_reranker = PineconeRerank(model="bge-reranker-v2-m3", top_n=top_n)
local_candidates = vector_store.similarity_search(query, k=fetch_k)
local_top = local_reranker.compress_documents(local_candidates, query)
print(f"\n[fetch_k={fetch_k}, top_n={top_n}]")
for rank, doc in enumerate(local_top, 1):
score = doc.metadata.get("relevance_score")
title = doc.metadata.get("title", "제목 없음")
preview = doc.page_content[:55].replace("\n", " ")
print(f" {rank}. score={score} | [{title}] {preview}...")
compare_rerank_configs(
query="재택근무할 때 지켜야 하는 보안 규칙은?",
configs=[(5, 1), (5, 5), (20, 3), (20, 5)],
)
Q: 재택근무할 때 지켜야 하는 보안 규칙은?
[fetch_k=5, top_n=1]
1. score=0.9352283 | [재택근무 및 유연근무 제도] 재택 시에는 회사가 지급한 노트북과 VPN을 사용해야 하며, 개인 기기에서 사내 시스템에 접속하는 ...
[fetch_k=5, top_n=5]
1. score=0.9352283 | [재택근무 및 유연근무 제도] 재택 시에는 회사가 지급한 노트북과 VPN을 사용해야 하며, 개인 기기에서 사내 시스템에 접속하는 ...
2. score=0.088504665 | [재택근무 및 유연근무 제도] 재택근무는 주 최대 3일까지 가능하며, 매주 금요일은 전사 권장 재택일로 운영된다. 재택근무를 하려...
3. score=0.0033634924 | [보안 및 계정 관리 규정] 모든 사내 시스템 접근에는 SSO와 2단계 인증(MFA)이 적용되며, MFA 미설정 계정은 로그인이...
4. score=0.0026939143 | [보안 및 계정 관리 규정] 권한은 최소 권한 원칙(least privilege)에 따라 부여하며, 직무 변경 시 기존 권한을 ...
5. score=0.0018386652 | [재택근무 및 유연근무 제도] 월 1회 이상은 팀 동기화를 위해 사무실 출근이 권장되며, 분기마다 전사 오프사이트가 열린다. 팀별...
[fetch_k=20, top_n=3]
1. score=0.9349913 | [재택근무 및 유연근무 제도] 재택 시에는 회사가 지급한 노트북과 VPN을 사용해야 하며, 개인 기기에서 사내 시스템에 접속하는 ...
2. score=0.08787644 | [재택근무 및 유연근무 제도] 재택근무는 주 최대 3일까지 가능하며, 매주 금요일은 전사 권장 재택일로 운영된다. 재택근무를 하려...
3. score=0.023375316 | [보안 및 계정 관리 규정] 보안 사고가 의심되면 즉시 보안팀(security@)에 신고하고 해당 계정의 세션을 강제 종료한다....
[fetch_k=20, top_n=5]
1. score=0.9349913 | [재택근무 및 유연근무 제도] 재택 시에는 회사가 지급한 노트북과 VPN을 사용해야 하며, 개인 기기에서 사내 시스템에 접속하는 ...
2. score=0.08787644 | [재택근무 및 유연근무 제도] 재택근무는 주 최대 3일까지 가능하며, 매주 금요일은 전사 권장 재택일로 운영된다. 재택근무를 하려...
3. score=0.023375316 | [보안 및 계정 관리 규정] 보안 사고가 의심되면 즉시 보안팀(security@)에 신고하고 해당 계정의 세션을 강제 종료한다....
4. score=0.0033766122 | [보안 및 계정 관리 규정] 모든 사내 시스템 접근에는 SSO와 2단계 인증(MFA)이 적용되며, MFA 미설정 계정은 로그인이...
5. score=0.0026626128 | [보안 및 계정 관리 규정] 권한은 최소 권한 원칙(least privilege)에 따라 부여하며, 직무 변경 시 기존 권한을 ...
실행 결과 해설
- 이 출력에서는 fetch_k와 top_n이 서로 다른 역할을 한다는 점이 분명히 드러난다. fetch_k=5, top_n=1에서는 가장 관련성 높은 한 청크만 남고, top_n=5로 늘리면 같은 주제의 보조 청크와 보안 규정 청크까지 함께 들어온다.
- 특히 현재 결과에서는 1위 점수는 항상 매우 높고(약 0.935), 그 아래 점수는 급격히 작아진다. 즉, 핵심 근거 하나는 매우 강하게 잡히지만 나머지는 보조 근거 성격이 크다는 뜻이다. 이런 경우 top_n=1은 매우 정밀하지만, 답변에 필요한 주변 규칙까지 함께 쓰려면 top_n=3이나 top_n=5가 더 실용적일 수 있다.
- fetch_k=20으로 넓히면 3위 이후 후보 구성이 조금 달라지면서 보안 관련 청크가 더 안정적으로 들어온다. 즉, 질문이 복합적이거나 여러 하위 쟁점을 포함할수록 먼저 넓게 가져오고 재순위로 압축하는 편이 안전하다. 반대로 단일 규칙을 묻는 짧은 질문이라면 작은 fetch_k만으로도 충분할 수 있다.
# 모범 답안 2) S3의 청크 크기 비교와 결합해 (청크 크기)×(재순위 유무) 조합을 비교한다
def build_store_for_namespace(namespace: str):
return PineconeVectorStore(index=index, embedding=embeddings, namespace=namespace)
def compare_chunk_and_rerank(query: str, namespaces, k: int = 5):
stats = index.describe_index_stats()
ns_map = getattr(stats, "namespaces", None) or (stats.get("namespaces") if isinstance(stats, dict) else {}) or {}
available = set(ns_map.keys())
print("Q:", query)
for namespace in namespaces:
if namespace not in available:
print(f"\n[{namespace}] 건너뜀: 현재 인덱스에 없는 네임스페이스")
continue
local_store = build_store_for_namespace(namespace)
docs = local_store.similarity_search(query, k=k)
reranked_docs = PineconeRerank(model="bge-reranker-v2-m3", top_n=3).compress_documents(docs, query)
print(f"\n[{namespace}] 재순위 전 상위 3개")
for rank, doc in enumerate(docs[:3], 1):
print(f" {rank}. [{doc.metadata.get('title', '제목 없음')}] {doc.page_content[:50].replace('\n', ' ')}...")
print(f"[{namespace}] 재순위 후 상위 3개")
for rank, doc in enumerate(reranked_docs, 1):
score = doc.metadata.get("relevance_score")
print(f" {rank}. score={score} | [{doc.metadata.get('title', '제목 없음')}] {doc.page_content[:50].replace('\n', ' ')}...")
compare_chunk_and_rerank(
query="배포 승인은 누가 하고, 롤백은 얼마나 빨리 가능한가요?",
namespaces=["chunk_200", "chunk_500", "chunk_1000"],
)
Q: 배포 승인은 누가 하고, 롤백은 얼마나 빨리 가능한가요?
[chunk_200] 재순위 전 상위 3개
1. [배포 및 릴리스 프로세스] 문제가 감지되면 직전 배포 태그로 롤백하며, 롤백은 평균 1분 이내에 완료되도록 자동화되어...
2. [배포 및 릴리스 프로세스] 프로덕션 배포는 QA 승인 이후 릴리스 담당자가 배포 태그를 생성하고, 마지막으로 엔지니어...
3. [배포 및 릴리스 프로세스] 배포는 카나리 방식으로 진행되어 전체 트래픽의 5%에 먼저 적용하고, 오류율과 지연(p95...
[chunk_200] 재순위 후 상위 3개
1. score=0.87037706 | [배포 및 릴리스 프로세스] 문제가 감지되면 직전 배포 태그로 롤백하며, 롤백은 평균 1분 이내에 완료되도록 자동화되어...
2. score=0.086632065 | [배포 및 릴리스 프로세스] 프로덕션 배포는 QA 승인 이후 릴리스 담당자가 배포 태그를 생성하고, 마지막으로 엔지니어...
3. score=0.021656677 | [배포 및 릴리스 프로세스] 배포는 카나리 방식으로 진행되어 전체 트래픽의 5%에 먼저 적용하고, 오류율과 지연(p95...
[chunk_500] 재순위 전 상위 3개
1. [배포 및 릴리스 프로세스] 문제가 감지되면 직전 배포 태그로 롤백하며, 롤백은 평균 1분 이내에 완료되도록 자동화되어...
2. [배포 및 릴리스 프로세스] 프로덕션 배포는 QA 승인 이후 릴리스 담당자가 배포 태그를 생성하고, 마지막으로 엔지니어...
3. [배포 및 릴리스 프로세스] 모든 코드는 기능 브랜치에서 작업한 뒤 Pull Request를 통해 main 브랜치로 병...
[chunk_500] 재순위 후 상위 3개
1. score=0.8705973 | [배포 및 릴리스 프로세스] 문제가 감지되면 직전 배포 태그로 롤백하며, 롤백은 평균 1분 이내에 완료되도록 자동화되어...
2. score=0.3477586 | [배포 및 릴리스 프로세스] 프로덕션 배포는 QA 승인 이후 릴리스 담당자가 배포 태그를 생성하고, 마지막으로 엔지니어...
3. score=0.02870708 | [배포 및 릴리스 프로세스] 모든 코드는 기능 브랜치에서 작업한 뒤 Pull Request를 통해 main 브랜치로 병...
[chunk_1000] 재순위 전 상위 3개
1. [배포 및 릴리스 프로세스] 문제가 감지되면 직전 배포 태그로 롤백하며, 롤백은 평균 1분 이내에 완료되도록 자동화되어...
2. [배포 및 릴리스 프로세스] 모든 코드는 기능 브랜치에서 작업한 뒤 Pull Request를 통해 main 브랜치로 병...
3. [보안 및 계정 관리 규정] 모든 사내 시스템 접근에는 SSO와 2단계 인증(MFA)이 적용되며, MFA 미설정 계정은...
[chunk_1000] 재순위 후 상위 3개
1. score=0.8701566 | [배포 및 릴리스 프로세스] 문제가 감지되면 직전 배포 태그로 롤백하며, 롤백은 평균 1분 이내에 완료되도록 자동화되어...
2. score=0.5115488 | [배포 및 릴리스 프로세스] 모든 코드는 기능 브랜치에서 작업한 뒤 Pull Request를 통해 main 브랜치로 병...
3. score=0.005641909 | [장애 대응 및 온콜 운영] 서비스 장애는 영향 범위에 따라 Sev1(전면 장애), Sev2(주요 기능 장애), Sev...
실행 결과 해설
- 현재 출력에서는 chunk_200, chunk_500, chunk_1000이 모두 같은 문서를 잡아오지만, 상위권 구성과 점수 차이는 꽤 다르다. 즉, 청크 크기가 달라지면 '무엇을 가져오느냐'보다 '어떤 조각이 더 앞에 오느냐'가 달라질 수 있다.
- 예를 들어 chunk_500에서는 2위 점수가 약 0.348로 비교적 높아, 배포 승인 정보와 롤백 정보가 함께 잘 묶여 있다는 신호를 준다. 반면 chunk_1000에서는 2위 점수는 더 높지만 3위가 갑자기 장애 대응 문서로 튀어, 큰 청크가 문맥을 넓게 품는 대신 잡음도 섞을 수 있음을 보여준다.
- 이 결과만 보면 하나의 절대적인 정답 청크 크기가 있다기보다, 질문 유형에 따라 유리한 조합이 달라진다고 보는 편이 맞다. 승인 주체와 롤백 속도처럼 관련 정보가 가까이 붙은 질문은 chunk_500이나 chunk_1000이 유리할 수 있고, 세부 규정 하나만 묻는 질문은 더 작은 청크와 재순위 조합이 잘 맞을 수 있다.
# 모범 답안 3) search_with_rerank 를 S8의 retrieve_node 에 넣어 재순위+자기교정을 결합한다
def retrieve_node_with_rerank(state):
sq = state.get("search_query") or state["question"]
context = search_with_rerank(sq, fetch_k=10, top_n=3)
return {"context": context}
sample_state = {
"question": "배포 승인 절차와 롤백 기준 알려줘",
"search_query": "배포 승인 절차 롤백 기준",
}
retrieved = retrieve_node_with_rerank(sample_state)
print("질문:", sample_state["question"])
print("재작성 검색어:", sample_state["search_query"])
print("\n[retrieve_node_with_rerank 결과 미리보기]")
print(retrieved["context"][:400], "...")
print("\nS8에 적용 위치:")
print("- 기존 retrieve_node의 vector_store.similarity_search(...) 부분을 search_with_rerank(...) 호출로 교체")
print("- 그러면 CRAG/Self-RAG 루프는 그대로 두고, 검색 근거만 더 정밀하게 공급할 수 있다")
질문: 배포 승인 절차와 롤백 기준 알려줘
재작성 검색어: 배포 승인 절차 롤백 기준
[retrieve_node_with_rerank 결과 미리보기]
[배포 및 릴리스 프로세스] 문제가 감지되면 직전 배포 태그로 롤백하며, 롤백은 평균 1분 이내에 완료되도록 자동화되어 있다. 긴급 핫픽스는 별도 승인 절차를 통해 스테이징을 건너뛸 수 있으나, 배포 후 24시간 내 사후 리뷰를 반드시 작성해야 한다. 정기 배포는 화·목요일 오전에만 진행하며, 금요일과 공휴일 전날 오후에는 배포를 금지해 대응 인력이 부족한 시간대의 위험을 줄인다.
[배포 및 릴리스 프로세스] 프로덕션 배포는 QA 승인 이후 릴리스 담당자가 배포 태그를 생성하고, 마지막으로 엔지니어링 리드가 수동으로 승인하는 2단계 게이트를 거친다. 승인 기록과 변경 내역은 릴리스 노트로 자동 정리되어 추적성을 확보한다. 기능 플래그(feature flag)를 활용해 코드 배포와 기능 노출을 분리하며, 위 ...
S8에 적용 위치:
- 기존 retrieve_node의 vector_store.similarity_search(...) 부분을 search_with_rerank(...) 호출로 교체
- 그러면 CRAG/Self-RAG 루프는 그대로 두고, 검색 근거만 더 정밀하게 공급할 수 있다
실행 결과 해설
- 이 출력 미리보기에서는 재순위된 근거가 이미 질문의 두 핵심 요소인 배포 승인 절차와 롤백 기준을 한꺼번에 담고 있다. 즉, retrieve_node_with_rerank는 단순히 문서를 많이 가져오는 것이 아니라, 답을 만드는 데 바로 쓸 수 있는 밀도 높은 근거를 공급한다.
- 현재 예시에서도 첫 번째 문단은 롤백 시간과 핫픽스 예외를, 두 번째 문단은 QA 승인과 엔지니어링 리드 승인 단계를 담고 있다. 이런 식으로 서로 다른 하위 질문이 재순위 후 상위 근거에 함께 들어오면, 뒤쪽의 CRAG나 Self-RAG가 불필요하게 다시 돌 가능성이 줄어든다.
- 실무적으로도 이 결합은 자연스럽다. 재순위는 검색 정밀도를 올리고, 자기교정은 검색 실패와 환각을 줄인다. 즉, 재순위가 '더 좋은 근거를 먼저 주는 역할'을 하고, 자기교정이 '그래도 부족할 때 다시 점검하는 역할'을 맡는 구조라고 이해하면 된다.
정리
- 재순위로 "넓게 검색 → 좁게 정밀화" 를 구현해 근거 품질을 높였다(PineconeRerank, bge-reranker-v2-m3).
- 크로스 인코더가 더 정확한 이유와 2단계 전략의 효율을 이해했다.
- 하이브리드 검색의 개념과 적용 위치를 정리했다.
- 다음(S10): 에이전트를 운영에 올린다 — 영속성·HITL·스트리밍 + RAGAS 평가.
'AI System > Agentic RAG: Agent 시스템을 위한 Vector RAG 설계' 카테고리의 다른 글
| d01 - 12. Capstone (0) | 2026.06.16 |
|---|---|
| d01 - 11. 평가 (0) | 2026.06.16 |
| d02 - 9. CRAG & Self-RAG (0) | 2026.06.16 |
| d02 - 8. Adaptive RAG (0) | 2026.06.16 |
| d02 - 7. create_agent (0) | 2026.06.16 |