RAG Vector Search ReRanking

양현지·2026년 5월 28일

연구

목록 보기
25/26
post-thumbnail

스키마 및 메타데이터가 충분한 질의에 대한 답변

스키마 및 메타데이터가 충분한 질의에 대한 엉터리 답변

질의 처리 함수 토대로 현재 Rag Retreival 절차를 조회해보자.
  public String ask(String question, boolean dbConfirmed) {
        ChatClient chatClient = chatClientBuilder.build();

        // ① 질문 유형 분류
        String type = classifyQuestion(question, chatClient);
        log.info("질문 유형: {}", type);

        // ② DB 접근 허용 요청 (사실 허용 요청보다 DB 접속 정보 재확인)
        if ((type.contains("DB") || type.contains("BOTH")) && !dbConfirmed) {
            return "__DB_CONFIRM__:" + extractDbInfo(datasourceUrl);
        }

        String dbContext = "";
        String docContext = "";

        // ③-1. DB 질문이면 SQL 생성
        if (type.contains("DB") || type.contains("BOTH")) {
            String tableList = getTableList();
            String sql = generateSqlDirect(question, tableList, chatClient);
            log.info("생성된 SQL: {}", sql);

            if (sql != null && !sql.isBlank() && !sql.equalsIgnoreCase("NONE")) {
                String result = executeQuery(sql);
                if (result.startsWith("SQL 실행 오류")) {
                    log.info("Self-correction 시도");
                    String schema = fetchLiveSchema(extractTableFromSql(sql));
                    String correctedSql = refineSql(question, schema, sql, result, chatClient);
                    result = executeQuery(correctedSql);
                }
                dbContext = result;
            }
        }

        // ③-2. 문서 질문이면 벡터 검색
        if (type.contains("DOC") || type.contains("BOTH")) {
            docContext = fetchVectorContext(question);
        }

        log.info("DB: {}자, DOC: {}자", dbContext.length(), docContext.length());
        return generateAnswer(question, dbContext, docContext, chatClient);
    }

[질의 라우팅] 질문 유형 분류

질의 라우팅은 Naive RAG 에서 Advanced RAG로의 발전에 필요한 기술이다. 막연하게 LLM에게 모든 질문에 대한 판단과 답변을 맡기는 대신 가장 좋은 방향의 선택지를 지능적으로 전달하기 위한 과정이다.
   private String classifyQuestion(String question, ChatClient chatClient) {
        String result = chatClient.prompt()
                .system("""
                        질문을 분석해서 답변 생성에 필요한 데이터 소스를 판단하세요.
                        
                        DB: 현재 데이터 조회 (실시간, 현재, 건수, 현황, 수량, 날짜별, 작업, 재고, 에러, 알람, 위치)
                        DOC: 사용법, 절차, 설명, 매뉴얼, 설계서 내용, 운영 방안
                        BOTH: DB + 문서 둘 다 필요
                        LLM: 일반 지식으로 답변 가능 (기술 설명, 개념 등)
                        
                        DB, DOC, BOTH, LLM 중 하나만 출력하세요.
                        """)
                .user(question)
                .call()
                .content()
                .trim()
                .toUpperCase();

        if (result.contains("BOTH")) return "BOTH";
        if (result.contains("DB")) return "DB";
        if (result.contains("DOC")) return "DOC";
        return "LLM";
    }

ClassifyQuestion 함수 내 프롬프트 라우팅으로 RAG는 검색 전략 유형을 수립하도록 한다.
위 버전은 초기 RAG 질의 라우팅에 사용한 코드이다.

*폐쇄망을 고려하기에 일반적인 웹 검색은 고려하지 않도록 한다.

[DB 조회] getTableList

  • DB 질문이면 SQL 생성 이전에 String tableList = getTableList(); 을 호출하여, 연결된 데이터베이스의 테이블 및 뷰 정보를 가져오게끔 한다.
    이는 정적 RAG 정보 (스키마 및 DB 메타 데이터를 RAG 데이터로 주입함)에만 의존하지 않고, 현재 실시간 데이터베이스의 정보도 참조하기 위함이다.

가령, 스키마에는 있지만 데이터베이스에는 없는 테이블, 혹은 그 반대의 경우가 존재함을 방지하고자 함이다.

private String getTableList() {
        try {
            List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
                SELECT 
                    TABLE_NAME AS NAME, 
                    'TABLE' AS OBJ_TYPE,
                    c.COMMENTS                            
                FROM ALL_TABLES 
                WHERE OWNER='HMX_KCTC'
                UNION ALL
                SELECT 
                    VIEW_NAME AS NAME, 
                    'VIEW' AS OBJ_TYPE,
                    c.COMMENTS
                FROM ALL_VIEWS 
                WHERE OWNER='HMX_KCTC'
                ORDER BY OBJ_TYPE, NAME
                """);

                return rows.stream()
                                .map(r -> {
                                    String line = r.get("OBJ_TYPE") + ": " + r.get("NAME");
                                    if (r.get("COMMENTS") != null) {
                                        line += " -- " + r.get("COMMENTS");
                                    }
                                    return line;
                                })
                                .collect(Collectors.joining("\n"));
        } catch (Exception e) {
            log.error("getTableList ERROR: {}", e.getMessage());
            return "";
        }
    }

[DB 조회] generateSqlDirect

  • RAG 스키마 검색 및 실시간 DB 조회 기반 SQL 생서
    private String generateSqlDirect(String question, String tableList, ChatClient chatClient) {
        // Chroma에서 관련 스키마 검색
        String schemaContext = fetchVectorContext(question + " 컬럼 DDL 스키마");
        return chatClient.prompt()
                .system("""
                        Oracle DBA 엔지니어이자 자동화창고 도메인 전문가이자 시스템 WCS*WMS 전문가입니다.
                        [테이블/뷰 목록][스키마 정보]를 참고하여
                        질문에 맞는 SQL을 생성하세요.
                         
                        규칙:
                        - SELECT 문만 생성
                        - SQL 코드만 출력 (마크다운 없이)
                        - 반드시 HMX_KCTC.테이블명 형식
                        - FETCH FIRST 20 ROWS ONLY
                        - 세미콜론 절대 포함 금지
                        - [테이블/뷰 목록]에 없는 컬럼명 절대 사용 금지
                        - [테이블/뷰 목록]에 없는 테이블,뷰 절대 사용 금지 (엄수해야한다.)
                        - [스키마 정보]에 있더라도 [테이블/뷰 목록]에 없으면 사용 금지
                        - 뷰와 테이블 모두 키워드를 포함한다면, 뷰 우선 사용 (e,g. V_T_WORK_MONIT와 T_WORK 둘 다 있으면 V_T_WORK_MONIT 조회 결과 우선 사용)
                        - DB 조회 불필요하면 NONE
                        """)
                .user("""
                        [테이블/뷰 목록]
                        %s
                        
                        [스키마 정보]
                        %s
                        
                        [질문]
                        %s
                        
                        SQL:
                        """.formatted(tableList, schemaContext, question))
                .call()
                .content()
                .replaceAll("```sql", "")
                .replaceAll("```", "")
                .trim();
    }

이때, fetchVectorContext 함수는 RAG 벡터 검색을 수행한다.

    private String fetchVectorContext(String question) {
        try {
            // 벡터 내 유사도 검색 (코사인 유사도 거리가 높은 5순위까지)
            List<Document> docs = vectorStore.similaritySearch(
                    SearchRequest.builder()
                            .query(question)
                            .topK(5)
                            .build()
            );
            // 26.05.28 Reranking 로직 추가 가능 (예: LLM으로 간단히 재점수화)

            log.info("벡터 검색 결과: {}건", docs.size());
            docs.forEach(doc -> log.info("문서 미리보기: {}",
                    doc.getText().substring(0, Math.min(200, doc.getText().length()))));

            if (docs.isEmpty()) return "";
            return docs.stream()
                    .map(Document::getText)
                    .collect(Collectors.joining("\n---\n"));
        } catch (Exception e) {
            log.error("벡터 검색 오류: {}", e.getMessage());
            return "";
        }
    }

0개의 댓글