S7 · 라우팅 & Adaptive RAG — 질의 분류와 쿼리 재작성
Agentic RAG 실전 (14시간) · Day 2
검색/직답을 명시적 그래프로 분리하고, 질의 분류(라우팅)와 쿼리 재작성을 더한다. 상황에 맞게 경로를 바꾸는 RAG를 Adaptive RAG라 한다. S0 .venv/.env 로 실행.
목표
- 구조화 출력으로 질의를 안정적으로 분류한다.
- 모호·구어체 질의를 검색에 좋은 형태로 재작성한다.
- 둘을 합친 Adaptive RAG 그래프를 저수준으로 만든다.
1. 라우팅
질문을 유형으로 분류해 경로를 나눈다.
- 사내 문서 필요 → 검색 경로
- 인사·일반 상식 → 직답 경로(검색 생략)
이 교시는 '검색 vs 직답' 두 갈래에 집중한다(웹 폴백은 선택 과제).
2. 안정적 분류 — 구조화 출력
LLM에게 그냥 물으면 장황한 문장으로 답해 분기에 쓰기 어렵다. with_structured_output 으로 미리 정한 틀(Pydantic)에 맞춰 받으면 decision.destination 같은 정확한 값을 꺼내 분기할 수 있다.
3. 쿼리 재작성
"재택 며칠 되는거임?" 같은 구어체를 "재택근무 주당 가능 일수" 처럼 검색에 좋은 질의로 LLM이 다시 쓴다. 검색 결과가 부실할 때 다시 쓰는 적극적 교정(grade & retry)은 S8에서 다룬다 — 여기서는 검색 전 1회 정리.
4. 그래프 구조
START → router ──direct──▶ direct ──▶ END
│ search
▼
rewrite ▶ retrieve ▶ generate ▶ END
표준 루프가 아니므로 저수준 StateGraph 로 그린다.
5. 실행 준비
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}
6. 인덱스 연결
import os
from pinecone import Pinecone
from langchain_pinecone import PineconeVectorStore, PineconeEmbeddings
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
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_1500': 12, 'chunk_200': 89, 'chunk_1000': 18, 'demo': 3, 'chunk_500': 34}
연결 완료
7. 라우터 노드 — 구조화 출력 분류
Route 틀로 LLM이 search/direct 중 하나를 고르게 한다.
라우팅 품질은 시스템 프롬프트의 구체성에 크게 좌우된다. 기준이 막연하면 모델이 대부분 direct 로 분류해 검색을 건너뛴다 — 어떤 질문이 '사내 문서'에 해당하는지 예시와 함께 명시한다.
from pydantic import BaseModel, Field # 데이터 '틀'을 선언하는 표준 도구
from typing import Literal # 값을 정해진 후보로 제한
from langchain_core.messages import SystemMessage, HumanMessage
# 라우팅 결정의 '틀' — destination 은 반드시 두 값 중 하나
class Route(BaseModel):
"""사용자 질문을 어떤 경로로 보낼지 결정한다."""
destination: Literal["search", "direct"] = Field(
description="사내 문서 검색이 필요하면 'search', 인사·감사·잡담·일반상식이면 'direct'"
)
router_llm = llm.with_structured_output(Route)
# ★ 라우팅 기준을 구체적으로 준다.
# "질문을 분류하세요" 같은 막연한 지시만으로는 모델이 대부분 direct 로 몰려(검색을 건너뜀)
# Adaptive RAG 가 동작하지 않는다. 어떤 질문이 '사내 문서'에 해당하는지 명시한다.
ROUTER_SYSTEM = (
"당신은 '누구나테크' 사내 문서 도우미의 라우터입니다. 사용자 질문을 두 경로로 분류하세요.\n"
"- search: 회사 정책·규정·절차·기술·FAQ에 관한 질문. 예) 휴가/연차, 재택·근무시간, 배포·릴리스, "
"장애 대응, 보안·계정, 경비, 온보딩, 성과평가, 개인정보, 고객지원, 코드리뷰 등 사내 문서로 답해야 하는 질문.\n"
"- direct: 인사·감사·잡담이나 회사와 무관한 일반 상식처럼 사내 문서가 필요 없는 질문.\n"
"회사 생활·업무 규정과 조금이라도 관련 있으면 search 로 분류하세요."
)
def classify(q: str) -> str:
# router_node 와 단독 테스트가 같은 기준을 쓰도록 헬퍼로 통일
decision = router_llm.invoke([SystemMessage(content=ROUTER_SYSTEM), HumanMessage(content=q)])
return decision.destination
def router_node(state):
return {"route": classify(state["question"])} # "search" 또는 "direct"
# 단독 테스트: router_node 와 동일한 기준(ROUTER_SYSTEM)으로 분류
print(classify("연차 며칠?")) # → search 기대
print(classify("고마워요")) # → direct 기대
search
direct
8. 재작성 · 검색 · 생성 · 직답 노드
각 노드는 상태 일부를 받아 변경분만 반환한다.
def rewrite_node(state):
q = state["question"]
# 사용자의 원문을 '검색에 좋은 질의'로 다시 쓰게 한다
prompt = ("다음 질문을 벡터 검색에 적합한 간결한 한국어 검색어로 바꾸세요. "
"핵심 명사 위주로, 군더더기 없이 한 줄로.\n질문: " + q + "\n검색어:")
search_query = llm.invoke(prompt).content.strip() # 재작성된 검색어
return {"search_query": search_query}
def retrieve_node(state):
sq = state["search_query"] # 재작성된 검색어로
docs = vector_store.similarity_search(sq, k=3) # 벡터 검색
context = "\n\n".join(f"[{d.metadata['title']}] {d.page_content}" for d in docs)
return {"context": context}
def generate_node(state):
# 검색된 근거'만' 사용해 답하도록 지시 (환각 억제)
prompt = ("다음 [근거]만 사용해 한국어로 답하고, 답변 끝에 참고 문서 제목을 괄호로 표기하세요.\n\n"
f"[근거]\n{state['context']}\n\n[질문]\n{state['question']}")
return {"answer": llm.invoke(prompt).content}
def direct_node(state):
# 검색 없이 바로 답 (인사·일반 상식)
return {"answer": llm.invoke(state["question"]).content}
9. 그래프 조립
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class RAGState(TypedDict):
question: str # 사용자 원본 질문
route: str # 라우터 결정 ("search"/"direct")
search_query: str # 재작성된 검색어
context: str # 검색된 근거
answer: str # 최종 답변
g = StateGraph(RAGState)
g.add_node("router", router_node) # 분류
g.add_node("rewrite", rewrite_node) # 검색어 재작성
g.add_node("retrieve", retrieve_node) # 벡터 검색
g.add_node("generate", generate_node) # 근거로 답 생성
g.add_node("direct", direct_node) # 직답
g.add_edge(START, "router")
# 라우터의 결정값(state["route"])에 따라 갈림길 — search→rewrite, direct→direct
g.add_conditional_edges("router", lambda s: s["route"],
{"search": "rewrite", "direct": "direct"})
g.add_edge("rewrite", "retrieve") # 검색 경로: 재작성 → 검색 → 생성
g.add_edge("retrieve", "generate")
g.add_edge("generate", END)
g.add_edge("direct", END) # 직답 경로: 바로 끝
app = g.compile()
print("Adaptive RAG 그래프 컴파일 완료")
Adaptive RAG 그래프 컴파일 완료
from IPython.display import Image, display
try:
display(Image(app.get_graph().draw_mermaid_png())) # 분기가 보이는 그래프 그림
except Exception:
print(app.get_graph().draw_mermaid())

10. 실행 — 라우팅과 재작성 확인
def run(q):
out = app.invoke({"question": q})
print("Q:", q)
print(" 경로:", out["route"], "| 재작성 검색어:", out.get("search_query", "—"))
print(" A:", out["answer"][:120], "...\n")
run("재택 며칠 되는거임?") # 구어체 → search 경로 + 검색어 재작성 후 검색
run("배포 승인 절차 알려줘") # search 경로
run("도와줘서 고마워요!") # direct 경로 (검색 생략)
Q: 재택 며칠 되는거임?
경로: search | 재작성 검색어: 재택 근무 기간
A: 재택근무는 주 최대 3일까지 가능하며, 매주 금요일은 전사 권장 재택일로 운영됩니다. 다만, 신규 입사자는 온보딩 기간(첫 4주) 동안에는 주 1회로 제한됩니다. (재택근무 및 유연근무 제도) ...
Q: 배포 승인 절차 알려줘
경로: search | 재작성 검색어: 배포 승인 절차
A: 배포 승인 절차는 다음과 같습니다. 먼저 QA 승인이 완료되어야 하며, 이후 릴리스 담당자가 배포 태그를 생성합니다. 마지막으로 엔지니어링 리드가 수동으로 승인하는 2단계 게이트를 거칩니다. 이 승인 기록과 변경 내 ...
Q: 도와줘서 고마워요!
경로: direct | 재작성 검색어: —
A: 천만에요! 언제든지 도와드릴게요. 필요한 게 있으면 말씀해 주세요! ...
🔍 관찰 포인트
- "재택 며칠 되는거임?" → search 경로 + 재작성 검색어가 깔끔해진다.
- "고마워요" → direct 경로, 검색 생략(검색어 —).
- 구조화 출력 덕에 router가 항상 search/direct만 반환 → 안정적 분기.
- 커스텀 흐름이라 create_agent 가 아닌 저수준 StateGraph.
✏️ 미니 실습
- Route 에 "web" 후보를 추가해 최신 정보 질문 분기를 설계한다(웹검색 연결은 선택).
- 재작성 프롬프트를 바꿔(동의어 확장 등) 검색 품질 변화를 본다.
- 재작성 없이 직접 검색한 결과와 재작성 후를 비교한다.
# 1) Route에 "web" 후보를 추가해 최신 정보 질문을 별도 경로로 분기하는 설계 예시
from typing import Literal
class RouteWithWeb(BaseModel):
"""사용자 질문을 search, direct, web 중 하나로 분류한다."""
destination: Literal["search", "direct", "web"] = Field(
description=(
"사내 문서 검색이 필요하면 'search', 인사·감사·잡담·일반 상식이면 'direct', "
"최신 시세·오늘 뉴스·방금 발표처럼 사내 문서 밖의 최신 정보가 필요하면 'web'"
)
)
router_llm_web = llm.with_structured_output(RouteWithWeb)
ROUTER_SYSTEM_WEB = (
"당신은 '누구나테크' 도우미의 라우터입니다. 질문을 search, direct, web 중 하나로 분류하세요.\n"
"- search: 회사 정책·규정·절차·기술·FAQ처럼 사내 문서 검색이 필요한 질문\n"
"- direct: 인사·감사·잡담이나 일반 상식처럼 검색 없이 답할 수 있는 질문\n"
"- web: 환율, 주가, 오늘 뉴스, 방금 발표, 최신 일정처럼 외부의 최신 정보가 필요한 질문"
)
def classify_with_web(q: str) -> str:
# 기존 classify 와 같은 방식으로 구조화 출력 결과의 destination 만 꺼낸다.
decision = router_llm_web.invoke(
[SystemMessage(content=ROUTER_SYSTEM_WEB), HumanMessage(content=q)]
)
return decision.destination
def router_node_web(state):
return {"route": classify_with_web(state["question"])}
def web_node(state):
# 실습 단계에서는 실제 웹 검색 API 대신, 왜 web 경로로 갔는지 설명만 반환한다.
return {
"answer": (
"이 질문은 최신 외부 정보가 필요하므로 web 경로로 분기되었습니다. "
"실서비스에서는 여기서 웹 검색 API를 호출하면 됩니다."
)
}
g_web = StateGraph(RAGState)
g_web.add_node("router", router_node_web)
g_web.add_node("rewrite", rewrite_node)
g_web.add_node("retrieve", retrieve_node)
g_web.add_node("generate", generate_node)
g_web.add_node("direct", direct_node)
g_web.add_node("web", web_node)
g_web.add_edge(START, "router")
g_web.add_conditional_edges(
"router",
lambda s: s["route"],
{"search": "rewrite", "direct": "direct", "web": "web"},
)
g_web.add_edge("rewrite", "retrieve")
g_web.add_edge("retrieve", "generate")
g_web.add_edge("generate", END)
g_web.add_edge("direct", END)
g_web.add_edge("web", END)
app_web = g_web.compile()
for q in ["연차는 며칠인가요?", "고마워요", "오늘 달러 환율 알려줘"]:
out = app_web.invoke({"question": q})
print("Q:", q)
print(" 경로:", out["route"])
print(" A:", out["answer"][:100], "...\n")
Q: 연차는 며칠인가요?
경로: search
A: 연차 휴가는 입사 1년 차에 15일이 부여되며, 근속 2년마다 1일씩 가산되어 최대 25일까지 늘어납니다. 입사 첫해에는 1년을 채우지 못한 경우 매월 만근 시 1일씩 비례하여 부 ...
Q: 고마워요
경로: direct
A: 천만에요! 도움이 필요하면 언제든지 말씀해 주세요. 😊 ...
Q: 오늘 달러 환율 알려줘
경로: web
A: 이 질문은 최신 외부 정보가 필요하므로 web 경로로 분기되었습니다. 실서비스에서는 여기서 웹 검색 API를 호출하면 됩니다. ...
실행 결과 해설
- 연차는 며칠인가요?는 회사 규정 질문이므로 search로 간다. 즉, web 후보를 추가해도 기존의 사내 문서 질문 분류가 무너지지 않는지 함께 확인하는 예시다.
- 고마워요는 검색 근거가 필요 없는 짧은 반응이라 direct가 자연스럽다. 라우터가 모든 질문을 검색으로 보내지 않고, 불필요한 검색을 줄이는 역할을 한다는 뜻이다.
- 오늘 달러 환율 알려줘가 web으로 분기되면, 이 라우터가 '사내 문서가 없는 최신 외부 정보'를 별도 유형으로 인식한다는 뜻이다. 실서비스에서는 바로 이 지점에 웹 검색 도구(Web search tool)나 외부 Application Programming Interface(API) 호출을 연결하면 된다.
# 2) 재작성 프롬프트를 바꿔 동의어 확장을 추가하고 검색 품질 변화를 관찰한다
# (제목이 같아도 점수·청크가 다를 수 있으므로 그것까지 출력해 비교한다)
def show_hits(query, k=3):
"""제목만으로는 차이가 안 보이므로 점수·청크번호·본문 미리보기까지 출력한다."""
for rank, (doc, score) in enumerate(vector_store.similarity_search_with_score(query, k=k), 1):
title = doc.metadata.get("title", "제목 없음")
chunk = doc.metadata.get("chunk", "?")
preview = doc.page_content[:45].replace("\n", " ")
print(f" {rank}. score={score:.3f} | {title} (chunk {chunk}) | {preview}...")
def rewrite_node_with_synonyms(state):
q = state["question"]
prompt = (
"다음 질문을 벡터 검색에 적합한 한국어 검색어 한 줄로 바꾸세요. "
"핵심 명사를 남기고, 필요하면 문서에서 자주 쓰일 동의어를 자연스럽게 덧붙이세요. "
"예: 재택 -> 재택근무, 연차 -> 연차휴가, 배포 -> 릴리스 승인 절차.\n"
f"질문: {q}\n검색어:"
)
return {"search_query": llm.invoke(prompt).content.strip()}
# 재작성 효과가 더 잘 드러나도록, 맥락이 부족한 구어체 질문도 함께 본다.
for sample_question in ["재택 며칠 되는거임?", "그거 며칠이나 가능함?"]:
basic_q = rewrite_node({"question": sample_question})["search_query"]
syn_q = rewrite_node_with_synonyms({"question": sample_question})["search_query"]
print("=" * 72)
print("원본 질문:", sample_question)
print("기본 재작성:", basic_q, "| 동의어 확장:", syn_q)
print("\n[기본 재작성 상위 문서]"); show_hits(basic_q)
print("[동의어 확장 재작성 상위 문서]"); show_hits(syn_q)
print()
========================================================================
원본 질문: 재택 며칠 되는거임?
기본 재작성: 재택 근무 기간 | 동의어 확장: 재택근무 기간, 재택근무 일수
[기본 재작성 상위 문서]
1. score=0.871 | 재택근무 및 유연근무 제도 (chunk 0.0) | 재택근무는 주 최대 3일까지 가능하며, 매주 금요일은 전사 권장 재택일로 운영된다...
2. score=0.844 | 재택근무 및 유연근무 제도 (chunk 2.0) | 월 1회 이상은 팀 동기화를 위해 사무실 출근이 권장되며, 분기마다 전사 오프사이...
3. score=0.836 | 재택근무 및 유연근무 제도 (chunk 1.0) | 재택 시에는 회사가 지급한 노트북과 VPN을 사용해야 하며, 개인 기기에서 사내 ...
[동의어 확장 재작성 상위 문서]
1. score=0.860 | 재택근무 및 유연근무 제도 (chunk 0.0) | 재택근무는 주 최대 3일까지 가능하며, 매주 금요일은 전사 권장 재택일로 운영된다...
2. score=0.837 | 재택근무 및 유연근무 제도 (chunk 2.0) | 월 1회 이상은 팀 동기화를 위해 사무실 출근이 권장되며, 분기마다 전사 오프사이...
3. score=0.835 | 휴가 및 연차 정책 (chunk 2.0) | 병가는 연 60일 한도로 사용 가능하며, 연속 3일을 초과하는 병가는 의사 진단서...
========================================================================
원본 질문: 그거 며칠이나 가능함?
기본 재작성: 가능 기간 | 동의어 확장: 가능 기간, 이용 가능일, 사용 가능 기간
[기본 재작성 상위 문서]
1. score=0.805 | 휴가 및 연차 정책 (chunk 1.0) | 반차는 오전(09:00~13:00)과 오후(14:00~18:00)로 나누어 사용할...
2. score=0.803 | 휴가 및 연차 정책 (chunk 0.0) | 누구나테크의 연차 휴가는 입사 1년 차에 15일이 부여되며, 근속 2년마다 1일씩...
3. score=0.800 | 휴가 및 연차 정책 (chunk 2.0) | 병가는 연 60일 한도로 사용 가능하며, 연속 3일을 초과하는 병가는 의사 진단서...
[동의어 확장 재작성 상위 문서]
1. score=0.827 | 휴가 및 연차 정책 (chunk 1.0) | 반차는 오전(09:00~13:00)과 오후(14:00~18:00)로 나누어 사용할...
2. score=0.816 | 휴가 및 연차 정책 (chunk 2.0) | 병가는 연 60일 한도로 사용 가능하며, 연속 3일을 초과하는 병가는 의사 진단서...
3. score=0.811 | 휴가 및 연차 정책 (chunk 0.0) | 누구나테크의 연차 휴가는 입사 1년 차에 15일이 부여되며, 근속 2년마다 1일씩...
실행 결과 해설
- 먼저 기본 재작성과 동의어 확장 재작성을 비교해, LLM(대규모 언어 모델)이 질문을 얼마나 문서 친화적인 표현으로 바꿨는지 본다. 여기서는 재택이 재택근무처럼 더 공식적인 용어로 바뀌는지가 핵심이다.
- 상위 문서 제목이 두 경우 모두 같다면, 현재 코퍼스에서는 기본 재작성만으로도 이미 충분했다는 뜻이다. 즉, 동의어 확장이 항상 성능을 크게 올리는 것은 아니며 데이터 분포에 따라 효과가 달라진다.
- 반대로 실무 코퍼스가 더 크고 표현이 다양하면, 이런 동의어 확장은 검색 recall(재현율, 관련 문서를 놓치지 않는 정도)을 높이는 데 도움이 될 수 있다. 그래서 이 실습은 '프롬프트를 바꾸면 검색 결과도 달라질 수 있다'는 감각을 익히는 데 의미가 있다.
# 3) 재작성 없이 직접 검색 vs 재작성 후 검색을 점수·청크까지 비교한다
def show_hits(query, k=3):
"""제목만으로는 차이가 안 보이므로 점수·청크번호·본문 미리보기까지 출력한다."""
for rank, (doc, score) in enumerate(vector_store.similarity_search_with_score(query, k=k), 1):
title = doc.metadata.get("title", "제목 없음")
chunk = doc.metadata.get("chunk", "?")
preview = doc.page_content[:45].replace("\n", " ")
print(f" {rank}. score={score:.3f} | {title} (chunk {chunk}) | {preview}...")
# 이미 검색 친화적인 질문 / 모호한 구어체 질문 두 가지로 효과 차이를 본다.
for compare_question in ["배포 승인 절차 알려줘", "배포 그거 누가 오케이 해줌?"]:
rewritten_query = rewrite_node({"question": compare_question})["search_query"]
print("=" * 72)
print("원본 질문:", compare_question, "| 재작성 검색어:", rewritten_query)
print("\n[재작성 없이 직접 검색]"); show_hits(compare_question)
print("[재작성 후 검색]"); show_hits(rewritten_query)
print()
print("해석 가이드:")
print("- 제목이 같아도 score·chunk 가 다르면 재작성이 순위/근거를 바꾼 것이다.")
print("- 모호한 구어체일수록 재작성 전후 차이가 커지는 경향을 확인한다.")
========================================================================
원본 질문: 배포 승인 절차 알려줘 | 재작성 검색어: 배포 승인 절차
[재작성 없이 직접 검색]
1. score=0.856 | 배포 및 릴리스 프로세스 (chunk 1.0) | 프로덕션 배포는 QA 승인 이후 릴리스 담당자가 배포 태그를 생성하고, 마지막으로...
2. score=0.839 | 배포 및 릴리스 프로세스 (chunk 2.0) | 문제가 감지되면 직전 배포 태그로 롤백하며, 롤백은 평균 1분 이내에 완료되도록 ...
3. score=0.825 | 배포 및 릴리스 프로세스 (chunk 0.0) | 모든 코드는 기능 브랜치에서 작업한 뒤 Pull Request를 통해 main 브...
[재작성 후 검색]
1. score=0.855 | 배포 및 릴리스 프로세스 (chunk 1.0) | 프로덕션 배포는 QA 승인 이후 릴리스 담당자가 배포 태그를 생성하고, 마지막으로...
2. score=0.840 | 배포 및 릴리스 프로세스 (chunk 2.0) | 문제가 감지되면 직전 배포 태그로 롤백하며, 롤백은 평균 1분 이내에 완료되도록 ...
3. score=0.827 | 배포 및 릴리스 프로세스 (chunk 0.0) | 모든 코드는 기능 브랜치에서 작업한 뒤 Pull Request를 통해 main 브...
========================================================================
원본 질문: 배포 그거 누가 오케이 해줌? | 재작성 검색어: 배포 승인 담당자
[재작성 없이 직접 검색]
1. score=0.817 | 배포 및 릴리스 프로세스 (chunk 1.0) | 프로덕션 배포는 QA 승인 이후 릴리스 담당자가 배포 태그를 생성하고, 마지막으로...
2. score=0.816 | 배포 및 릴리스 프로세스 (chunk 2.0) | 문제가 감지되면 직전 배포 태그로 롤백하며, 롤백은 평균 1분 이내에 완료되도록 ...
3. score=0.809 | 신규 입사자 온보딩 가이드 (chunk 1.0) | 2주 차에는 작은 규모의 첫 과제(스타터 티켓)를 맡아 PR 작성부터 배포까지 전...
[재작성 후 검색]
1. score=0.834 | 배포 및 릴리스 프로세스 (chunk 1.0) | 프로덕션 배포는 QA 승인 이후 릴리스 담당자가 배포 태그를 생성하고, 마지막으로...
2. score=0.830 | 배포 및 릴리스 프로세스 (chunk 2.0) | 문제가 감지되면 직전 배포 태그로 롤백하며, 롤백은 평균 1분 이내에 완료되도록 ...
3. score=0.812 | 배포 및 릴리스 프로세스 (chunk 0.0) | 모든 코드는 기능 브랜치에서 작업한 뒤 Pull Request를 통해 main 브...
해석 가이드:
- 제목이 같아도 score·chunk 가 다르면 재작성이 순위/근거를 바꾼 것이다.
- 모호한 구어체일수록 재작성 전후 차이가 커지는 경향을 확인한다.
실행 결과 해설
- 이 비교는 '재작성 자체가 항상 필요한가?'를 확인하는 가장 단순한 실험이다. 원문 질문으로 검색한 결과와 재작성 후 검색한 결과가 비슷하면, 그 질문은 애초에 검색 친화적 표현이었던 것이다.
- 현재 예시의 배포 승인 절차 알려줘는 이미 핵심 명사와 의도가 분명해서, 재작성 검색어도 거의 같은 표현으로 유지된다. 그래서 두 검색 결과가 유사하게 나오는 것이 자연스럽다.
- 이런 사례는 재작성 노드(Node)를 무조건 모든 질문에 적용하기보다, 구어체·모호한 질문에서 특히 가치가 크다는 점을 보여준다. 즉, Adaptive RAG에서는 '언제 재작성이 필요한가'까지 함께 설계하는 것이 중요하다.
정리
- 라우팅(구조화 출력)과 쿼리 재작성을 합친 Adaptive RAG를 저수준으로 구현했다.
- 경로를 명시적으로 나눠 제어·디버깅이 쉬워졌다.
- 다음(S8): 검색·답변을 스스로 평가해 되돌아가는 자기 교정(CRAG·Self-RAG).
'AI System > Agentic RAG: Agent 시스템을 위한 Vector RAG 설계' 카테고리의 다른 글
| d02 - 10. Rerank (0) | 2026.06.16 |
|---|---|
| d02 - 9. CRAG & Self-RAG (0) | 2026.06.16 |
| d02 - 7. create_agent (0) | 2026.06.16 |
| d01 - 6. AgenticRAG 실습 (0) | 2026.06.15 |
| d01 - 5. LangGraph (0) | 2026.06.15 |