프로그래밍/AI 교과 추천 시스템 개발기

5편. LLM으로 강좌 설명 보강하기

Tiboong 2026. 2. 19. 11:00

 

이번 편에서 다루는 것

이전 편에서 언급한 문제:

강좌 설명이 부족하면 임베딩 품질이 떨어진다.

 

실제 데이터를 보니, 상당수의 강좌가 설명이 없거나 너무 짧았다.

이 문제를 LLM(Large Language Model)을 활용해 해결했다.


문제 분석

데이터 현황

강좌 설명을 분석해보니:

상태 비율 예시
설명 없음 15% NULL 또는 빈 문자열
이름과 동일 20% "데이터베이스" → "데이터베이스"
너무 짧음 25% "SQL 기초 학습" (10자 미만)
충분함 40% 2줄 이상의 설명

 

60%의 강좌가 불충분한 설명을 가지고 있었다.

임베딩에 미치는 영향

# 설명이 부족한 강좌
course_1 = "데이터베이스"

# 설명이 충분한 강좌  
course_2 = "데이터베이스 설계 원리와 SQL 쿼리 작성법을 학습합니다. 정규화, 인덱스, 트랜잭션 처리를 다룹니다."

 

두 강좌 모두 DB 관련이지만, 임베딩 품질은 크게 다르다.


해결 전략: LLM으로 설명 생성

아이디어

강좌명과 학과 정보를 바탕으로 LLM에게 설명을 생성하도록 요청한다.

입력: 강좌명="데이터베이스", 학과="컴퓨터공학과"
출력: "데이터베이스의 기초 개념과 설계 원리를 학습합니다. 관계형 데이터베이스, 
        SQL 쿼리 작성, 정규화, 인덱스 등을 다룹니다."

모델 선택

모델 크기 특징 선택 여부

모델 크기 특징 선택여부
GPT-4 - 최고 품질 ❌ API 비용, 외부 의존성
Llama 70B 70B 우수한 품질 ❌ GPU 필수
Llama 7B 7B 적절한 품질 ❌ 한국어 성능 부족
경량 한국어 LLM 1.2B 한국어 특화, CPU 실행 ✅ 선택

 

선택 근거:

  1. 운영 서버에 GPU가 없음
  2. 한국어 강좌 설명 생성이 목적
  3. 배치 작업이므로 실시간 속도는 덜 중요

기본 구현

LLM 로드

from transformers import AutoTokenizer, AutoModelForCausalLM
import torch

class DescriptionEnhancer:
    def __init__(self, model_path: str):
        print(f"LLM 로드 중: {model_path}")
        
        self.tokenizer = AutoTokenizer.from_pretrained(model_path)
        self.model = AutoModelForCausalLM.from_pretrained(
            model_path,
            torch_dtype=torch.float32,  # CPU에서는 float32
            device_map="cpu"
        )
        
        print("LLM 로드 완료")
    
    def generate(self, prompt: str, max_new_tokens: int = 150) -> str:
        """프롬프트에 대한 응답 생성"""
        
        inputs = self.tokenizer(prompt, return_tensors="pt")
        
        with torch.no_grad():
            outputs = self.model.generate(
                **inputs,
                max_new_tokens=max_new_tokens,
                do_sample=True,
                temperature=0.7,
                top_p=0.9,
                pad_token_id=self.tokenizer.eos_token_id
            )
        
        generated = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
        
        # 프롬프트 부분 제거하고 생성된 부분만 반환
        response = generated[len(prompt):].strip()
        
        return response

프롬프트 설계

프롬프트 설계가 결과 품질에 큰 영향을 미쳤다.

def _build_prompt(self, course_name: str, department: str) -> str:
    """강좌 설명 생성을 위한 프롬프트 구성"""
    
    prompt = f"""### 지시사항
다음 대학교 강좌에 대한 간결한 설명을 2-3문장으로 작성하세요.
학생들이 이 강좌에서 무엇을 배우는지 이해할 수 있도록 작성하세요.

### 강좌 정보
- 강좌명: {course_name}
- 개설 학과: {department}

### 강좌 설명
"""
    return prompt

전체 파이프라인

class CourseDescriptionEnhancer:
    def __init__(self, model_path: str):
        self.enhancer = DescriptionEnhancer(model_path)
    
    def enhance_course(self, course: Dict[str, Any]) -> str:
        """단일 강좌 설명 보강"""
        
        name = course['name']
        department = course.get('department', '')
        original_desc = course.get('description', '')
        
        # 설명이 충분하면 그대로 반환
        if self._is_sufficient(original_desc, name):
            return original_desc
        
        # LLM으로 설명 생성
        prompt = self._build_prompt(name, department)
        generated = self.enhancer.generate(prompt)
        
        # 기존 설명과 결합
        if original_desc:
            enhanced = f"{original_desc} {generated}"
        else:
            enhanced = generated
        
        return enhanced
    
    def _is_sufficient(self, description: str, name: str) -> bool:
        """설명이 충분한지 판단"""
        if not description:
            return False
        
        # 이름과 동일하면 불충분
        if description.strip() == name.strip():
            return False
        
        # 너무 짧으면 불충분 (30자 미만)
        if len(description) < 30:
            return False
        
        return True

생성 결과 예시

예시 1: 설명 없는 강좌

입력:

  • 강좌명: "데이터베이스"
  • 학과: "컴퓨터공학과"
  • 기존 설명: (NULL)

생성된 설명:

"데이터베이스의 기초 개념과 설계 원리를 학습합니다. 관계형 데이터베이스 모델링, SQL 쿼리 작성, 정규화 이론을 다루며 실습을 통해 데이터베이스 설계 능력을 기릅니다."

예시 2: 짧은 설명

입력:

  • 강좌명: "머신러닝"
  • 학과: "인공지능학과"
  • 기존 설명: "ML 기초"

보강된 설명:

"ML 기초. 머신러닝의 핵심 알고리즘과 원리를 학습합니다. 지도학습, 비지도학습, 딥러닝 기초를 다루며 Python을 활용한 실습을 진행합니다."

예시 3: 충분한 설명 (보강 안 함)

입력:

  • 강좌명: "웹프로그래밍"
  • 기존 설명: "HTML, CSS, JavaScript를 활용한 웹 페이지 제작을 학습합니다. 반응형 웹 디자인과 기초적인 프론트엔드 개발을 다룹니다."

결과: 보강 없이 기존 설명 그대로 사용


배치 처리

2,000개 강좌 중 약 1,200개가 보강 대상이었다.

class BatchEnhancer:
    def __init__(self, enhancer: CourseDescriptionEnhancer):
        self.enhancer = enhancer
    
    def enhance_all(self, courses: List[Dict]) -> Dict[str, str]:
        """전체 강좌 설명 보강"""
        
        results = {}
        total = len(courses)
        enhanced_count = 0
        
        for i, course in enumerate(courses):
            course_id = course['id']
            original = course.get('description', '')
            
            # 보강 실행
            enhanced = self.enhancer.enhance_course(course)
            results[course_id] = enhanced
            
            # 보강 여부 확인
            if enhanced != original:
                enhanced_count += 1
            
            # 진행 상황 출력
            if (i + 1) % 100 == 0:
                print(f"진행: {i + 1}/{total} (보강: {enhanced_count}건)")
        
        print(f"완료: 총 {enhanced_count}건 보강")
        return results

초기 성능 문제

문제 발견

처음 실행했을 때 성능이 매우 느렸다.

1개 강좌 보강: 약 32초
1,200개 보강 예상 시간: 약 10시간 40분

 

이건 너무 느리다. 원인을 분석했다.

원인 분석

  1. 데이터 타입 문제: 모델을 float16으로 로드했음
  2. CPU 특성: CPU는 float16 연산에 최적화되어 있지 않음
  3. 변환 오버헤드: 매 연산마다 float16 ↔ float32 변환 발생

이 문제는 다음 편에서 자세히 다룬다. (27배 성능 향상)


품질 검증

생성된 설명이 실제로 임베딩 품질을 개선하는지 확인했다.

검증 방법

def evaluate_embedding_quality(
    original_desc: str,
    enhanced_desc: str,
    related_terms: List[str],
    engine: EmbeddingEngine
) -> Dict[str, float]:
    """임베딩 품질 비교"""
    
    # 각각 임베딩
    original_emb = engine.encode(original_desc) if original_desc else None
    enhanced_emb = engine.encode(enhanced_desc)
    
    # 관련 용어들과의 유사도 계산
    term_embs = engine.encode(related_terms)
    
    results = {}
    
    for i, term in enumerate(related_terms):
        term_emb = term_embs[i]
        
        if original_emb is not None:
            original_sim = engine.similarity(original_emb, term_emb)
            results[f"{term}_original"] = original_sim
        
        enhanced_sim = engine.similarity(enhanced_emb, term_emb)
        results[f"{term}_enhanced"] = enhanced_sim
    
    return results

검증 결과 예시

강좌: "데이터베이스"

관련 용어 원본 유사도 보강 후 유사도 개선

관련 용어 원본 유사도 보강 후 유사도 개선
SQL 0.42 0.78 +0.36
데이터 설계 0.51 0.82 +0.31
정규화 0.38 0.71 +0.33
테이블 0.45 0.69 +0.24

 

평균 유사도 개선: +0.31

임베딩 품질이 명확히 개선되었다.


DB 저장 및 업데이트

보강된 설명을 DB에 저장한다.

def update_course_descriptions(
    self,
    enhanced_descriptions: Dict[str, str]
):
    """보강된 설명을 DB에 업데이트"""
    
    for course_id, description in enhanced_descriptions.items():
        stmt = (
            update(CourseTable)
            .where(CourseTable.id == course_id)
            .values(description_enhanced=description)
        )
        self.session.execute(stmt)
    
    self.session.commit()
    print(f"{len(enhanced_descriptions)}건 업데이트 완료")

 

설계 포인트: 원본 설명은 보존하고, 보강된 설명은 별도 컬럼에 저장


CLI 명령어

@app.command()
def enhance(
    year: int = typer.Option(None, "--year", "-y", help="대상 연도"),
    no_save: bool = typer.Option(False, "--no-save", help="DB 저장 안 함"),
    verbose: bool = typer.Option(False, "--verbose", "-v", help="상세 로그")
):
    """강좌 설명을 LLM으로 보강"""
    
    enhancer = CourseDescriptionEnhancer(model_path)
    courses = load_courses(year=year)
    
    print(f"대상 강좌: {len(courses)}건")
    
    results = enhancer.enhance_all(courses)
    
    if not no_save:
        update_course_descriptions(results)
        print("DB 저장 완료")
    else:
        print("DB 저장 건너뜀")

 

실행:

# 전체 보강
python main.py enhance

# 특정 연도만
python main.py enhance --year 2024

# 테스트 (저장 안 함)
python main.py enhance --no-save

성과 및 교훈

성과

지표 수치
보강 대상 1,200개 (60%)
보강 성공 1,180개 (98%)
임베딩 품질 개선 평균 +0.31

배운 점

  1. 데이터 품질이 우선: 알고리즘보다 데이터 품질이 중요
  2. LLM은 도구: 적재적소에 활용하면 효과적
  3. 프롬프트 설계: 명확한 지시가 좋은 결과를 낳는다
  4. 품질 검증 필수: 생성 결과를 반드시 검증

다음 편 예고

이번 편에서 언급한 성능 문제를 다음 편에서 해결한다.

  • CPU에서 float16 vs float32
  • 27배 성능 향상의 비밀
  • 병목 분석 방법

시리즈 목차

  1. 프로젝트 소개 - 왜 AI 추천이 필요했나
  2. 추천을 위한 데이터 파이프라인 설계
  3. XLM-R로 강좌 임베딩 구축하기
  4. 추천 알고리즘 설계 - 가중치 기반 개인화
  5. LLM으로 강좌 설명 보강하기 ← 현재 글
  6. 성능 최적화 - CPU에서 27배 빠르게
  7. API 서버 구축과 배포