본문 바로가기
AI System/Agentic RAG: Agent 시스템을 위한 Vector RAG 설계

d02 - 8. Adaptive RAG

by Toddler_AD 2026. 6. 16.

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.
 

✏️ 미니 실습

  1. Route  "web" 후보를 추가해 최신 정보 질문 분기를 설계한다(웹검색 연결은 선택).
  2. 재작성 프롬프트를 바꿔(동의어 확장 등) 검색 품질 변화를 본다.
  3. 재작성 없이 직접 검색한 결과와 재작성 후를 비교한다.
# 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