Models & Algorithms

Multi-hop RAG에서 Query Planning이 실패하는 패턴과 해결책

Query Decomposition을 붙였는데 왜 여전히 틀릴까요? 분해는 시작일 뿐, 진짜 문제는 Sequencing과 Grounding에서 터집니다.

Multi-hop RAG에서 Query Planning이 실패하는 패턴과 해결책

Multi-hop RAG에서 Query Planning이 실패하는 패턴과 해결책

Query Decomposition을 붙였는데 왜 여전히 틀릴까요? 분해는 시작일 뿐, 진짜 문제는 Sequencing과 Grounding에서 터집니다.

Query Planning이란?

Multi-hop RAG에서 복잡한 질문을 처리하려면 세 단계가 필요합니다:

text
Query Planning = Decomposition + Sequencing + Grounding
단계역할실패 시 증상
**Decomposition**복합 질문을 서브쿼리로 분해필요한 정보 누락 또는 불필요한 검색
**Sequencing**서브쿼리 실행 순서 결정의존성 있는 쿼리가 먼저 실행되어 실패
**Grounding**서브쿼리를 실제 문서와 매칭분해는 됐는데 검색 결과 0건

대부분의 Multi-hop RAG 실패는 이 세 단계 중 하나에서 발생합니다.

실패 패턴 #1: Decomposition 실패

1-A. 과분해 (Over-decomposition)

python
query = "Tesla가 가격을 인하한 후 주가가 어떻게 됐어?"

# 과분해 결과
decomposed = [
    "Tesla는 어떤 회사인가?",           # 불필요
    "Tesla의 제품은 무엇인가?",          # 불필요
    "Tesla가 가격을 인하했나?",
    "Tesla가 언제 가격을 인하했나?",
    "가격 인하 폭은 얼마인가?",
    "Tesla 주가는 현재 얼마인가?",        # 잘못된 시점
    "주가 변동의 원인은 무엇인가?"        # 너무 추상적
]

문제점:

  • 불필요한 서브쿼리가 노이즈 문서를 가져옴
  • 토큰 낭비 + 컨텍스트 오염
  • 정작 필요한 "가격 인하 직후 주가 반응"이 희석됨

진단 기준:

python
def is_over_decomposed(decomposed, original):
    # 서브쿼리 수가 원본 질문의 엔티티 수 × 2를 초과하면 과분해
    entities = extract_entities(original)
    return len(decomposed) > len(entities) * 2

1-B. 저분해 (Under-decomposition)

python
query = "Sam Altman이 OpenAI에서 해고됐을 때 Microsoft CEO는 뭐라고 했고, 이후 Sam이 복귀하기까지 누가 임시 CEO를 맡았어?"

# 저분해 결과
decomposed = [
    "Sam Altman 해고와 Microsoft CEO 반응",
    "Sam Altman 복귀 전 임시 CEO"
]

문제점:

  • 첫 번째 서브쿼리가 여전히 Multi-hop
  • "해고 시점" → "Microsoft CEO 발언" 순서가 내재됨
  • 두 번째도 "복귀 시점" → "그 전 CEO" 순서 필요

진단 기준:

python
def is_under_decomposed(sub_query):
    # 서브쿼리에 시간 관계 키워드가 남아있으면 저분해
    temporal_markers = ["때", "후", "전", "이후", "직전", "when", "after", "before"]
    return any(marker in sub_query for marker in temporal_markers)

1-C. 암묵적 조건 누락

python
query = "작년에 가장 많이 팔린 전기차는?"

# 누락된 분해
decomposed = [
    "가장 많이 팔린 전기차는?"  # "작년"이 사라짐
]

# 올바른 분해
decomposed = [
    "2024년 전기차 판매량 순위는?"  # 시간 조건 명시
]

문제점:

  • 상대적 시간 표현("작년", "최근")이 절대 시간으로 변환되지 않음
  • 검색 시 시간 필터링 불가

실패 패턴 #2: Sequencing 실패

2-A. 의존성 무시

python
query = "OpenAI CEO가 해고된 날 Microsoft 주가는 어땠어?"

# 의존성 무시한 병렬 실행
decomposed = [
    "OpenAI CEO가 언제 해고됐나?",      # → 2023-11-17
    "Microsoft 주가는 어땠나?"           # → 언제? (의존성 끊김)
]

# 실제 실행
results = parallel_search(decomposed)  # 두 번째 쿼리 실패

문제점:

  • 서브쿼리 2는 서브쿼리 1의 결과(날짜)가 필요
  • 병렬 실행 시 의존성이 끊어져 잘못된 결과

의존성 그래프:

text
[Q1: 해고 날짜] ──→ [Q2: 그 날짜의 주가]
     ↓
  2023-11-17 (이 값이 Q2에 필요)

2-B. 순환 의존성

python
query = "A 회사 CEO가 B 회사로 이직했을 때, B 회사 주가와 A 회사 주가는 각각 어땠어?"

# 순환 구조로 잘못 분해
decomposed = [
    "A 회사 CEO가 B 회사로 이직한 건 언제인가?",
    "그때 B 회사 주가는?",  # Q1 의존
    "그때 A 회사 주가는?",  # Q1 의존
    "두 주가의 상관관계는?"  # Q2, Q3 의존
]

# Q2와 Q3는 Q1에만 의존 → 병렬 가능
# 하지만 LLM이 순환으로 잘못 판단하면 교착

2-C. 병렬 가능한데 직렬 처리

python
query = "2023년 Tesla와 BYD의 판매량 비교"

# 직렬 처리 (비효율)
step1 = search("2023년 Tesla 판매량")
step2 = search("2023년 BYD 판매량")  # step1 끝날 때까지 대기
answer = compare(step1, step2)

# 병렬 처리 (효율적)
results = parallel_search([
    "2023년 Tesla 판매량",
    "2023년 BYD 판매량"
])
answer = compare(*results)

문제점:

  • 독립적인 쿼리를 직렬로 처리하면 지연 시간 2배
  • 대규모 질문에서 성능 병목

실패 패턴 #3: Grounding 실패

3-A. 쿼리-문서 불일치

python
query = "일론 머스크가 트위터를 인수한 후 첫 해고는 언제였어?"

# 분해는 잘 됨
decomposed = [
    "일론 머스크가 트위터를 인수한 날짜는?",
    "트위터 첫 대규모 해고 날짜는?"
]

# 검색 실패
search("트위터 첫 대규모 해고")
# → 0건 (문서에는 "X" 또는 "Twitter layoffs"로 저장됨)

문제점:

  • 사용자 질문의 표현 ≠ 문서의 표현
  • "트위터" vs "X", "해고" vs "layoffs", "구조조정"

해결 패턴:

python
def expand_query(query):
    synonyms = {
        "트위터": ["Twitter", "X", "트위터(X)"],
        "해고": ["layoffs", "구조조정", "인력 감축", "fired"]
    }
    return generate_variations(query, synonyms)

3-B. Entity Resolution 실패

python
query = "Apple CEO가 WWDC에서 발표한 새 기능 중 가장 반응이 좋았던 건?"

# 분해
decomposed = [
    "Apple CEO는 누구인가?",           # → Tim Cook
    "WWDC에서 발표된 새 기능은?",       # → Vision Pro, iOS 18, ...
    "가장 반응이 좋았던 기능은?"         # → 무엇 기준? 어느 WWDC?
]

# Entity 연결 실패
# "Tim Cook" ↔ "Apple CEO" 연결은 됨
# "WWDC" → 어느 연도? 연결 안 됨
# "반응" → 주가? 트윗? 리뷰? 기준 모호

문제점:

  • 동일 엔티티의 다른 표현을 연결 못함
  • 암묵적 컨텍스트(연도, 기준)가 전파되지 않음

3-C. 중간 결과 손실

python
# Step 1
q1 = "OpenAI CEO 해고 날짜"
a1 = "2023년 11월 17일 Sam Altman이 해고됨"

# Step 2 (a1 일부만 사용)
q2 = "2023년 11월 17일 Microsoft 반응"  # "Sam Altman" 정보 손실

# Step 3 (더 심각한 손실)
q3 = "그 반응의 영향"  # 날짜도, 인물도, 회사도 손실

문제점:

  • 이전 단계의 풍부한 컨텍스트가 다음 단계로 전달 안 됨
  • 각 hop이 독립적으로 실행되면서 맥락이 희석

해결책: 패턴별 대응 전략

Decomposition 실패 대응

python
class SmartDecomposer:
    def decompose(self, query):
        # 1. 엔티티 추출
        entities = self.extract_entities(query)

        # 2. 관계 추출
        relations = self.extract_relations(query)

        # 3. 시간 표현 정규화
        query = self.normalize_temporal(query)  # "작년" → "2024년"

        # 4. 서브쿼리 생성 (엔티티 × 관계)
        sub_queries = []
        for entity in entities:
            for relation in relations:
                if self.is_relevant(entity, relation):
                    sub_queries.append(
                        self.generate_sub_query(entity, relation)
                    )

        # 5. 과분해 체크
        if len(sub_queries) > len(entities) * 2:
            sub_queries = self.merge_similar(sub_queries)

        return sub_queries

Sequencing 실패 대응

python
class DependencyAwareSequencer:
    def sequence(self, sub_queries):
        # 1. 의존성 그래프 생성
        graph = self.build_dependency_graph(sub_queries)

        # 2. 순환 의존성 체크
        if self.has_cycle(graph):
            graph = self.break_cycle(graph)

        # 3. 위상 정렬로 실행 순서 결정
        execution_order = self.topological_sort(graph)

        # 4. 병렬 가능한 쿼리 그룹화
        parallel_groups = self.group_independent(execution_order)

        return parallel_groups

    def build_dependency_graph(self, queries):
        """서브쿼리 간 의존성 파악"""
        graph = {}
        for i, q in enumerate(queries):
            deps = []
            for j, other in enumerate(queries):
                if i != j and self.depends_on(q, other):
                    deps.append(j)
            graph[i] = deps
        return graph

Grounding 실패 대응

python
class RobustGrounder:
    def ground(self, sub_query, previous_results):
        # 1. 컨텍스트 주입
        enriched_query = self.inject_context(sub_query, previous_results)

        # 2. 쿼리 확장 (동의어, 별칭)
        expanded_queries = self.expand_synonyms(enriched_query)

        # 3. 다중 검색 전략
        results = []
        for eq in expanded_queries:
            results.extend(self.search(eq))

        # 4. 결과 검증
        verified = self.verify_relevance(results, sub_query)

        # 5. Entity Resolution
        resolved = self.resolve_entities(verified, previous_results)

        return resolved

    def inject_context(self, query, previous):
        """이전 결과의 컨텍스트를 현재 쿼리에 주입"""
        context = self.extract_key_info(previous)
        return f"{query} (맥락: {context})"

실전 디버깅 체크리스트

1단계: Decomposition 검증

python
def debug_decomposition(original, decomposed):
    checks = {
        "과분해": len(decomposed) > 5,
        "저분해": any(is_still_complex(q) for q in decomposed),
        "시간누락": has_temporal(original) and not any(has_temporal(q) for q in decomposed),
        "엔티티누락": missing_entities(original, decomposed)
    }
    return {k: v for k, v in checks.items() if v}

2단계: Sequencing 검증

python
def debug_sequencing(decomposed, execution_order):
    checks = {
        "의존성무시": has_broken_dependencies(decomposed, execution_order),
        "불필요직렬": has_unnecessary_serial(decomposed, execution_order),
        "순환의존성": has_circular_dependency(decomposed)
    }
    return {k: v for k, v in checks.items() if v}

3단계: Grounding 검증

python
def debug_grounding(sub_query, search_results, expected):
    checks = {
        "결과없음": len(search_results) == 0,
        "관련성낮음": avg_relevance(search_results) < 0.5,
        "엔티티불일치": entity_mismatch(sub_query, search_results),
        "컨텍스트손실": context_lost(sub_query, expected)
    }
    return {k: v for k, v in checks.items() if v}

통합 디버깅 체크리스트

1. Decomposition Check

  • 서브쿼리 수 적정? (2-5개)
  • 각 서브쿼리가 단일 검색으로 해결 가능?
  • 시간/조건 표현이 명시적?

2. Sequencing Check

  • 의존성 그래프가 DAG?
  • 병렬 가능한 쿼리 그룹화됨?
  • 실행 순서가 의존성 존중?

3. Grounding Check

  • 각 서브쿼리가 검색 결과 있음?
  • 이전 결과 컨텍스트가 전파됨?
  • Entity가 일관되게 연결됨?

결론

Query Planning 실패는 단순히 "질문을 잘못 쪼갰다"가 아닙니다. Decomposition, Sequencing, Grounding 세 단계 어디서 깨졌는지를 진단해야 합니다.
text
✓ Decomposition: 적정 수의 서브쿼리, 조건 명시
✓ Sequencing: 의존성 존중, 병렬화 최적화
✓ Grounding: 쿼리 확장, 컨텍스트 전파, Entity Resolution

Multi-hop RAG의 성능은 검색 모델이나 LLM보다 Query Planning 파이프라인의 견고함에 달려 있습니다.

관련 글