프로젝트에 Rag 도입하기

2025. 9. 29. 17:50·백엔드

이번 프로젝트에 Rag를 도입한 경험을 포스팅하려고 한다.

 

주요 기능 중 하나인 컴퓨터공학부 학생들을 위한 학업 로드맵 추천 시스템에서 Rag방식을 도입하였다.


RAG 도입의 핵심 이유

  • 대량의 과목 데이터: 컴퓨터공학부의 수십 개의 과목들 (과목명, 설명, 트랙 정보, 학년/학기 정보 등)
  • 다차원적 매칭: 학생의 트랙, 관심 기술, 학습 스타일을 동시에 고려한 과목 추천
  • 의미적 유사성: 단순 키워드 매칭이 아닌 과목의 의미적 유사성을 통한 추천

 LLM만을 사용하였을 때 할루시네이션의 문제와 이 현상을 해결하기 위한 훈련 과정에서 많은 비용이 들기 때문에 Rag를 도입하였다.

 


먼저 Rag란?

RAG는 Retrieval-Augmented Generation(검색-증강-생성)의 약자로, 정보 검색과 생성 모델을 결합한 자연어 처리(NLP) 기술을 의미한다. 전통적인 생성 모델과는 달리, RAG는 먼저 데이터베이스나 문서 집합에서 관련 정보를 검색하고, 검색한 정보를 바탕으로 텍스트를 생성한다.

 

다시 말해 RAG는 LLM의 한계를 보완하여 더 정확하고 효율적인 답변을 제공할 수 있는 방법이다. 외부 지식을 활용하는 검색 단계를 추가함으로써, 최신 정보 반영, 모델 크기 감소, 맥락 유지, 데이터 편향 완화 등의 이점을 얻을 수 있다.


Rag 과정


과정을 그림과 함께 알아보자.

 

데이터 수집 및 전처리

pdf, html, csv, txt, json 등 다양한 형태의 원본 데이터를 임베딩 모델이 받을 수 있는 청크(chunk) 단위로 분할한다.

임베딩 및 저장

분할된 텍스트를 수치화하여 고차원 공간에 매핑한다. 유사한 의미의 단어는 벡터 공간에서 가까운 위치에 존재한다.

즉, 이 벡터 간의 거리로 의미상의 유사성을 파악할 수 있다.

사용자 질의 과정

사용자의 질의 또한 임베딩 과정을 거쳐 벡터로 변환된다. 벡터 데이터베이스의 문서 벡터들 간 유사도 검색을 통해 사용자 질의와 가장 관련성 높은 문서를 찾는다.

LLM 및 프롬프트

검색된 데이터와 적절한 프롬프트를 구성하여 LLM에게 전달한다. 

LLM이 생성한 최종 결과물을 클라이언트에게 전달한다.


 

RAG를 구현하기 위해선 벡터 데이터베이스, 텍스트 임베딩 모델, LLM 이 세가지 구성 요소가 꼭 필요하다.

 

선택한 세가지 구성요소는 아래와 같다.

  • 백터 데이터베이스: Qdrant
  • 임베딩 모델: text-embedding-ada-002 (OpenAI)
  • LLM: gpt-4o (OpenAI)

이제 Rag의 전반적인 과정과 구성요소를 확인했으니 구현해보자.

 


 

Rag 구현

입력 데이터

// Course 엔티티에서 과목 데이터 수집
@Entity
public class Course {
    private String courseName;        // 과목명
    private String description;        // 과목 설명
    private String courseCode;         // 과목 코드
    private Integer credits;          // 학점
    private Integer openGrade;         // 개설 학년
    private Semester openSemester;     // 개설 학기
}

 

전처리

// CourseEmbeddingService.java - createEmbeddingTextWithTracks()
private String createEmbeddingTextWithTracks(Course course) {
    StringBuilder text = new StringBuilder();
    
    // 과목명
    text.append(course.getCourseName()).append(" ");
    
    // 과목 설명 (있는 경우)
    if (course.getDescription() != null && !course.getDescription().trim().isEmpty()) {
        text.append(course.getDescription()).append(" ");
    }
    
    // 과목 코드, 학점, 개설 학년/학기
    text.append(course.getCourseCode()).append(" ");
    text.append(course.getCredits()).append("학점 ");
    text.append(course.getOpenGrade()).append("학년 ");
    text.append(course.getOpenSemester().name()).append(" ");
    
    // 트랙 정보 추가
    List<TrackRequirement> trackRequirements = trackRequirementRepository.findByCourseId(course.getId());
    for (TrackRequirement tr : trackRequirements) {
        text.append(tr.getTrack().getTrackName()).append(" ");
        text.append(tr.getCourseType().getDescription()).append(" ");
    }
    
    return text.toString().trim();
}

 

분할

// CourseEmbeddingService.java - embedAllCourses()
public void embedAllCourses() {
    // 1. 모든 과목 조회
    List<Course> courses = courseRepository.findAll();
    
    // 2. 과목을 Map으로 변환 (각 과목이 하나의 청크)
    List<Map<String, Object>> courseDocuments = courses.stream()
            .map(this::createCourseDocumentWithTracks)
            .collect(Collectors.toList());
    
    // 3. Qdrant에 저장
    qdrantRepository.addCourseDocuments(courseDocuments);
}

 

임베딩

// QdrantRepository.java - generateEmbedding()
private List<Double> generateEmbedding(String text) {
    try {
        // OpenAI API 호출
        String url = "https://api.openai.com/v1/embeddings";
        
        Map<String, Object> requestBody = new HashMap<>();
        requestBody.put("input", text);
        requestBody.put("model", "text-embedding-ada-002");  // 임베딩 모델
        
        HttpHeaders headers = new HttpHeaders();
        headers.set("Content-Type", "application/json");
        headers.set("Authorization", "Bearer " + openaiApiKey);
        
        HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
        ResponseEntity<Map> response = restTemplate.exchange(url, HttpMethod.POST, entity, Map.class);
        
        if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
            Map<String, Object> responseBody = response.getBody();
            List<Map<String, Object>> data = (List<Map<String, Object>>) responseBody.get("data");
            if (data != null && !data.isEmpty()) {
                List<Double> embedding = (List<Double>) data.get(0).get("embedding");
                return embedding;  // 1536차원 벡터 반환
            }
        }
    } catch (Exception e) {
        return generateDummyVector();  // API 실패 시 더미 벡터
    }
}

 

벡터 데이터베이스 저장

// QdrantRepository.java - addCourseDocument()
public void addCourseDocument(Map<String, Object> document) {
    String id = (String) document.get("id");
    String text = createEmbeddingText(document);
    
    // OpenAI API로 벡터 생성
    List<Double> vector = generateEmbedding(text);
    
    // Qdrant 포인트 생성
    int numericId = Math.abs(id.hashCode());
    Map<String, Object> point = new HashMap<>();
    point.put("id", numericId);
    point.put("vector", vector);        // 벡터 저장
    point.put("payload", document);      // 메타데이터 저장
    
    // Qdrant API로 저장
    String url = String.format("http://%s:%d/collections/%s/points", qdrantHost, qdrantPort, collectionName);
    // ... REST API 호출
}

 

검색

// QdrantRepository.java - searchSimilarCourses()
public List<Map<String, Object>> searchSimilarCourses(String query, int topK) {
    // 쿼리 텍스트를 벡터로 변환
    List<Double> queryVector = generateEmbedding(query);
    
    // Qdrant 검색 API 호출
    String url = String.format("http://%s:%d/collections/%s/points/search", qdrantHost, qdrantPort, collectionName);
    
    Map<String, Object> requestBody = new HashMap<>();
    requestBody.put("vector", queryVector);      // 쿼리 벡터
    requestBody.put("limit", topK);             // 상위 K개 결과
    requestBody.put("with_payload", true);       // 메타데이터 포함
    
    // 유사도 검색 실행
    ResponseEntity<Map> response = restTemplate.exchange(url, HttpMethod.POST, entity, Map.class);
    
    // 결과 반환 (유사도 점수 포함)
    return results.stream()
            .map(result -> {
                Map<String, Object> payload = (Map<String, Object>) result.get("payload");
                Map<String, Object> searchResult = new HashMap<>(payload);
                searchResult.put("score", result.get("score"));  // 유사도 점수
                return searchResult;
            })
            .toList();
}

 

LLM 및 프롬프트

// LlmRoadmapService.java - generateRoadmapRecommendation()
public Map<String, Object> generateRoadmapRecommendation(
        String studentId,
        List<Long> trackIds,
        List<Map<String, Object>> mandatoryCourses,      // 검색된 필수 과목
        List<Map<String, Object>> recommendedCourses,   // 검색된 추천 과목
        String techStack,
        Map<String, Object> semesterInfo) {
    
    // 프롬프트 생성 (검색된 데이터 + 사용자 정보)
    String prompt = buildPrompt(mandatoryCourses, recommendedCourses, techStack, semesterInfo);
    
    // OpenAI API 호출 (GPT-4o)
    Map<String, Object> response = callOpenAI(prompt);
    return response;
}

// 프롬프트 구성
private String buildPrompt(List<Map<String, Object>> mandatoryCourses, 
                          List<Map<String, Object>> recommendedCourses, 
                          String techStack,
                          Map<String, Object> semesterInfo) {
    
    StringBuilder prompt = new StringBuilder();
    prompt.append("당신은 한성대학교 컴퓨터공학부 학생들을 위한 최고의 학업 로드맵 설계 AI입니다...");
    
    // 검색된 필수 과목 목록 추가
    prompt.append("1. 필수 과목 목록 (반드시 로드맵에 포함되어야 함)\n");
    for (Map<String, Object> course : mandatoryCourses) {
        prompt.append(String.format("- %s (%s): %s - %s [개설: %d학년 %s학기]\n", 
            course.get("courseName"), course.get("courseCode"), 
            course.get("courseType"), course.get("description"),
            course.get("openGrade"), course.get("openSemester")));
    }
    
    // 검색된 추천 과목 목록 추가
    prompt.append("2. 추천 과목 목록 (학생의 선호도에 따라 선택적으로 포함)\n");
    for (Map<String, Object> course : recommendedCourses) {
        prompt.append(String.format("- %s (%s): %s - %s [개설: %d학년 %s학기] (유사도: %s)\n", 
            course.get("courseName"), course.get("courseCode"),
            course.get("description"), course.get("tracks"),
            course.get("openGrade"), course.get("openSemester"),
            course.get("score")));
    }
    
    return prompt.toString();
}

 


로드맵 생성 과정을 요약하자면 다음과 같다.

과목 데이터 → Qdrant 벡터화 → 사용자 쿼리 → 유사 과목 검색 → LLM 로드맵 생성

 

1. 데이터 준비 (Embedding & Indexing)

  • 모든 과목 정보(강의 계획서, 과목 설명 등)를 LLM이 이해할 수 있는 벡터(Vector) 로 변환한다. 변환된 벡터들은 의미 기반 검색이 용이하도록 VectorDB에 저장(Indexing)된다.

2. 연관 데이터 검색

  • 사용자의 정보와 설문조사(관심 분야 등) 또한 벡터로 변환된다. VectorDB 내에서 사용자의 질의 벡터와 DB 내 과목 벡터들 간의 코사인 유사도(Cosine Similarity)를 측정하여 의미적으로 가장 관련성 높은 상위 K개의 문서를 신속하게 검색한다.

3. 프롬프트 구성 및 응답 생성

  • 검색된 정보는 사용자의 수강 이력, 트랙 정보 등 개인 데이터에 따라 한 번 더 필터링된다. 최종적으로 정제된 데이터는 로드맵 생성 규칙과 함께 LLM에 전달된다. LLM은 이 컨텍스트를 바탕으로 환각(Hallucination) 현상을 최소화하여 맞춤형 로드맵을 생성한다.

 


 

정리하자면 ...

이처럼 프로젝트에 Rag 방식을 도입하여 학업 로드맵 추천 시스템을 구축했다. 위와 같은 프로세스를 통해 RAG는 기존의 LLM이 가지는 한계를 극복하고, 보다 정확하고 신뢰할 수 있는 답변을 제공한다. 특히, 문서 전처리와 임베딩 과정을 통해 데이터의 품질을 높이고, 검색된 문서와의 연관성을 극대화할 수 있었다.

 

'백엔드' 카테고리의 다른 글

[모의 투자 사이트 개발기] 실시간 주식 시세 중계 시스템 구축하기  (0) 2025.11.14
[모의 투자 사이트 개발기] KIS 오픈API사용법, 토큰관리, API 호출  (1) 2025.11.08
[동시성 제어] 좌석 예매 동시성 문제 해결하기  (3) 2025.08.27
[RabbitMQ + Celery] 메세지큐와 워커를 활용한 비동기처리  (8) 2025.08.07
RabbitMQ + Celery를 이용한 비동기 병렬처리로 사용자 경험 개선  (1) 2025.04.30
'백엔드' 카테고리의 다른 글
  • [모의 투자 사이트 개발기] 실시간 주식 시세 중계 시스템 구축하기
  • [모의 투자 사이트 개발기] KIS 오픈API사용법, 토큰관리, API 호출
  • [동시성 제어] 좌석 예매 동시성 문제 해결하기
  • [RabbitMQ + Celery] 메세지큐와 워커를 활용한 비동기처리
hwanheee
hwanheee
hwanheee 님의 블로그 입니다.
  • hwanheee
    hwanheee 님의 블로그
    hwanheee
  • 전체
    오늘
    어제
    • 분류 전체보기 (20)
      • 백엔드 (8)
      • Java (5)
      • cs (1)
      • 알고리즘 (6)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    비동기처리
    스프링
    우테코 로또
    영속성컨텍스트
    동시성 제어
    최소 신장 트리
    체결엔진
    모의투자
    주식 모의투자
    우테코 레이싱카
    우테코 출석부
    알고리즘
    배낭문제
    celery
    우아한테크코스
    다이나믹 프로그래밍
    생성형AI
    JPA
    비관락
    레디스
    동시성
    유니온파인드
    rabbitmq
    소수 구하기
    우테코
    크루스칼
    최단경로 알고리즘
    프림
    분산락
    최소 스패닝 트리
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
hwanheee
프로젝트에 Rag 도입하기
상단으로

티스토리툴바