Query Routing - 질문 유형별로 검색 파이프라인 분기하기
지난 글에서 멀티모달 RAG로 텍스트뿐 아니라 이미지·표까지 검색 대상에 포함시켰습니다. 그런데 지금까지 다룬 벡터 검색, GraphRAG, Hybrid Search를 모든 질문에 똑같이 다 돌리는 건 비효율적입니다. 이번 글에서는 질문 유형을 먼저 판단해서 적절한 검색 경로로 분기하는 Query Routing을 다룹니다.
1. 왜 모든 검색 방식을 한 질문에 다 쓰면 안 되는가
지금까지 다룬 검색 방식들은 저마다 강점이 다릅니다.
"재택근무 정책이 뭐야?" → 벡터 검색이 가장 적합
"김철수 팀장이 승인권을 가진 정책은?" → GraphRAG가 가장 적합
"SHD-2024-0091 계약서 찾아줘" → Hybrid Search(BM25)가 가장 적합
"3분기 매출 얼마였어?" → 벡터 검색이 아니라 DB 직접 조회가 맞음
모든 질문마다 벡터 검색 + 그래프 검색 + BM25를 전부 돌리면, 지난 부하 테스트 글에서 다룬 것처럼 불필요한 LLM 호출과 DB 조회가 늘어나 응답 시간과 비용이 함께 나빠집니다. 질문 유형에 맞는 경로만 선택적으로 타는 것이 훨씬 효율적입니다.
2. Query Routing의 기본 구조
Query Routing은 질문이 들어오면 먼저 "이 질문이 어떤 유형인가"를 판단하는 라우터 단계를 거친 뒤, 그 판단에 따라 서로 다른 검색 파이프라인으로 분기합니다.
질문
↓
[라우터] 질문 유형 분류
├─ 일반 정보 질문 → 벡터 검색 (기존 RAG)
├─ 관계·종합 질문 → GraphRAG
├─ 정확한 식별자 질문 → Hybrid Search
└─ 정형 데이터 질문 → SQL 직접 조회 (Text-to-SQL)
3. LLM으로 질문 분류하기
라우터는 별도의 저비용 LLM 호출로 구현합니다. 질문을 미리 정의한 카테고리 중 하나로 분류하도록 요청합니다.
@Service
@RequiredArgsConstructor
public class QueryRouterService {
private final ChatClient chatClient;
public QueryType classify(String question) {
String prompt = """
다음 질문을 아래 네 가지 유형 중 하나로 분류해줘. 유형 이름만 정확히 출력해.
- GENERAL: 정책, 개념, 절차 등 일반적인 정보를 묻는 질문
- RELATIONAL: 특정 인물/조직과 여러 정책·권한 사이의 관계를 종합해야 하는 질문
- EXACT_MATCH: 문서 번호, 계약 코드, 정확한 고유명사를 찾는 질문
- STRUCTURED_DATA: 수치, 통계, 집계 등 정형 데이터를 묻는 질문
질문: %s
""".formatted(question);
String result = chatClient.prompt().user(prompt).call().content().trim();
return QueryType.valueOf(result);
}
}
public enum QueryType {
GENERAL, RELATIONAL, EXACT_MATCH, STRUCTURED_DATA
}
지난 멀티턴 대화 글에서 다룬 질문 재구성처럼, 이 분류 작업도 짧고 단순한 작업이라 gpt-4o-mini 같은 저비용 모델로 충분히 처리할 수 있습니다.
4. 분류 결과에 따라 검색 경로 분기하기
@Service
@RequiredArgsConstructor
public class RagOrchestratorService {
private final QueryRouterService queryRouterService;
private final VectorSearchService vectorSearchService;
private final GraphSearchService graphSearchService;
private final HybridSearchService hybridSearchService;
private final TextToSqlService textToSqlService;
private final ChatClient chatClient;
public String answer(String question) {
QueryType type = queryRouterService.classify(question);
String context = switch (type) {
case GENERAL -> vectorSearchService.search(question);
case RELATIONAL -> graphSearchService.search(question);
case EXACT_MATCH -> hybridSearchService.search(question);
case STRUCTURED_DATA -> textToSqlService.queryAndFormat(question);
};
String promptText = "다음 정보를 참고해서 답변해줘.\n[정보]\n%s\n[질문]\n%s"
.formatted(context, question);
return chatClient.prompt().user(promptText).call().content();
}
}
switch 하나로 질문 유형에 맞는 검색 서비스만 호출하는 구조입니다. 지금까지 각 글에서 따로따로 구현했던 벡터 검색, GraphRAG, Hybrid Search가 여기서 하나의 진입점 아래 정리됩니다.
5. 정형 데이터 질문은 검색이 아니라 SQL로 처리하기
"3분기 매출 얼마였어?" 같은 질문은 애초에 벡터 검색 대상이 아니라, DB에 직접 쿼리를 날려야 하는 질문입니다. 이런 경우 LLM에게 자연어 질문을 SQL로 변환하도록 요청하는 Text-to-SQL 방식을 함께 사용합니다.
@Service
@RequiredArgsConstructor
public class TextToSqlService {
private final ChatClient chatClient;
private final JdbcTemplate jdbcTemplate;
private static final String SCHEMA_INFO = """
테이블: sales (id, quarter, revenue, region)
""";
public String queryAndFormat(String question) {
String sqlPrompt = """
다음 스키마를 참고해서 질문에 답하는 SELECT 쿼리만 작성해줘. 다른 설명은 하지 마.
[스키마]
%s
[질문]
%s
""".formatted(SCHEMA_INFO, question);
String sql = chatClient.prompt().user(sqlPrompt).call().content().trim();
if (!sql.trim().toUpperCase().startsWith("SELECT")) {
throw new IllegalArgumentException("허용되지 않은 쿼리 형태입니다.");
}
List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql);
return rows.toString();
}
}
LLM이 생성한 SQL을 그대로 실행하는 것은 보안상 위험할 수 있으므로, SELECT로 시작하는지 검증하고 읽기 전용 DB 계정으로만 실행하는 등 최소한의 방어 장치를 반드시 함께 둬야 합니다.
6. 라우팅 정확도가 떨어질 때의 안전장치
분류가 항상 정확하지는 않기 때문에, 잘못 분류되더라도 완전히 엉뚱한 답이 나오지 않도록 보완 장치를 둡니다.
public String answer(String question) {
QueryType type = queryRouterService.classify(question);
String context = executeSearch(type, question);
if (isContextTooSparse(context)) {
context = vectorSearchService.search(question); // 벡터 검색으로 폴백
}
return generateAnswer(context, question);
}
검색 결과가 지나치게 빈약하거나 비어있으면, 가장 범용적인 벡터 검색으로 다시 시도하는 폴백 로직을 넣어두면 라우팅 오분류로 인한 답변 실패를 줄일 수 있습니다.
7. Query Routing 적용 전후 비교
| 항목 | 적용 전 (단일 파이프라인) | 적용 후 (Query Routing) |
|---|---|---|
| 관계 질문 정확도 | 벡터 검색만으로는 낮음 | GraphRAG로 분기되어 개선 |
| 정형 데이터 질문 | 답변 불가 또는 부정확 | SQL 직접 조회로 정확한 답변 |
| 불필요한 검색 호출 | 모든 방식 병렬 실행 시 비용 증가 | 필요한 경로만 실행되어 비용 절감 |
| 구현 복잡도 | 단순 | 라우터 단계와 폴백 로직 추가 필요 |
8. 정리
- 벡터 검색, GraphRAG, Hybrid Search, SQL 조회는 각각 강점이 다르기 때문에, 질문 유형에 맞는 경로로 분기하는 것이 정확도와 비용 양쪽에서 유리합니다.
- 라우팅은 저비용 LLM으로 질문을 미리 정의한 카테고리로 분류한 뒤, 해당 검색 서비스로 위임하는 구조로 구현합니다.
- 정형 데이터 질문은 Text-to-SQL로 처리하되, 생성된 쿼리의 실행 범위를 반드시 제한하는 방어 로직이 필요합니다.
다음 글에서는 지금까지 벡터 DB 안의 텍스트 문서만 다뤘던 범위를 넓혀서, 엑셀이나 사내 테이블 같은 구조화된 데이터를 RAG에 통합하는 방법을 다뤄보겠습니다.
'RAG를 활용한 LLM 서비스 구축' 카테고리의 다른 글
| STEP 24 - OCR과 레이아웃 인식 (0) | 2026.07.21 |
|---|---|
| STEP 23 - 구조화된 데이터(엑셀·테이블) RAG에 통합하기 (0) | 2026.07.21 |
| STEP 21 - 멀티모달 RAG (0) | 2026.07.20 |
| STEP 20 - GraphRAG (0) | 2026.07.15 |
| STEP 19 - RAG 시스템 부하 테스트와 모니터링 (0) | 2026.07.15 |