데보션앱 소개페이지 바로가기
로그인 선택

신고하기

CLOSE
신고사유 (대표 사유 1개)
상세내용 (선택)
0/200
  • 신고한 게시글은 더 이상 보이지 않습니다.
  • 이용약관과 운영정책에 따라 신고사유에 해당하는지 검토 후 조치됩니다.
  • 허위 신고인 경우, 신고자의 서비스 이용이 제한될 수 있으니 유의하시어 신중하게 신고해 주세요.
(이 회원이 작성한 모든 댓글과 커뮤니티 게시물이 보이지 않고, 알림도 오지 않습니다.)

미리보기

커뮤니티

      1,234

      badge 23.06.15

      글 등록

      카테고리를 선택해주세요.

      DEVOTEE를 활성화 시키면
      지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.

      버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.

      임시저장함에 저장되었습니다. 저장일시 : 2022.5.17 14:29:08

      임시저장함

      제목을 선택하시면 이어서 작성이 가능하며,
      최대 20건까지 저장합니다.
      컨텐츠 유형, 제목, 저장일시, 삭제로 이뤄진 임시저장 목록
      컨텐츠 유형 제목 저장일 삭제

      데보션 블로그 게재 요청

      CLOSE
      • *
      • *

      본인인증

      효율적인 데보션 서비스 이용 및
      고객님의 소중한 개인정보보호를 위해
      본인인증을 진행해주세요. 본인인증 미 진행 시 로그인이 제한됩니다.
      본인인증 실패

      본인인증 로그인에 실패하였습니다.
      회원이 아니시거나 본인인증 등록이
      완료되지 않은 사용자입니다.

      회원정보 연결

      특정 YouTube 영상의 댓글에 대한 자연어 분석 RAG 시스템 개발 with LangGraph + Qdrant

      smartcow.jung 25.09.01
      2,648 3 1
      DEVOTEE 요약
      필자는 대학 시절 유튜브 댓글을 읽는 것에 귀찮음을 느껴, RAG(Retrieval-Augmented Generation) 기술을 활용하여 YouTube 댓글의 과부하 문제를 해결하는 시스템을 만들었습니다. 이 시스템은 댓글을 수집, 분석, 검색하여 사용자가 원하는 정보(예: 가장 좋아하는 부분)나 감정 분석 등을 즉각적으로 제공하며, 의미적으로 관련 있는 댓글을 기반으로 답변을 생성합니다. 현재 MVP(Minimum Viable Product) 단계로 한계점(쿼리 분류 방식, 고정된 검색 전략, 효율성, 언어 처리 등)이 있지만, 지속적으로 개선해 나갈 계획입니다.

      들어가며

      대학생 시절, 마음 맞는 친구들과 유튜브 채널을 운영한 적이 있습니다.

      당연히 거의 모든 영상이 별로 인기가 없었지만, 학과를 소개하는 영상 두개 정도가 꽤나 시청자 반응이 좋아 댓글이 많이 달렸습니다.

      시청자들이 우리의 영상에 대해 어떠한 반응을 하는지 궁금해 댓글을 여러번 읽었지만, 몇번 해보니 배부른 소리지만 꽤나 귀찮게 느껴졌습니다.

      제 천성이 게을러서 그런지 자연스레 더 편한 방법은 없을까 고민하게 되었습니다.


      시간이 흘러 AI 기술이 발전함에 따라 드디어 과거의 제 귀찮음을 기술적으로 해결할 수 있게 되었습니다.

      바로 RAG(Retrieval-Augmented Generation) 입니다.


      문제 정의: YouTube 댓글의 정보 과부하

      현재 YouTube 댓글 시스템의 한계

      YouTube의 기본 댓글 시스템은 몇 가지 문제점을 가지고 있습니다:

      1. 정렬 옵션의 한계: '인기순'과 '최신순' 두 가지 정렬만 제공

      2. 검색 기능 부재: 특정 키워드나 주제로 댓글을 검색할 수 없음

      3. 전체 맥락 파악 어려움: 수천 개의 댓글을 일일이 읽어야 전체 반응을 알 수 있음

      4. 감정 분석 불가: 긍정/부정 반응의 비율이나 주요 감정을 파악하기 어려움

      우리가 원하는 것

      Q: "시청자들이 이 영상에서 가장 좋아한 부분이 뭐야?"
      A: "댓글 분석 결과, 시청자들이 가장 좋아한 부분은 
         1) 3:45 부분의 편집 퀄리티 (32개 댓글)
         2) 출연자의 자연스러운 리액션 (28개 댓글)
         3) 배경음악 선곡 (15개 댓글) 입니다."

      이렇게 자연어로 질문하고 답을 받을 수 있다면 얼마나 편할까요?


      해결 방안: RAG 시스템 아키텍처

      전체 워크플로우

      graph LR
          A[YouTube URL 입력] --> B[상위 500개 댓글 수집]
          B --> C[벡터 DB에 저장]
          C --> D[사용자 질문 입력]
          D --> E[쿼리 타입 분석]
          E --> F[적응형 검색]
          F --> G[AI 응답 생성]

      왜 RAG인가?

      RAG는 검색(Retrieval)과 생성(Generation)을 결합한 방식입니다. 이 프로젝트에서 RAG를 선택한 이유:

      1. 정확성: 실제 댓글 데이터를 기반으로 답변

      2. 맥락 이해: 의미적으로 유사한 댓글들을 찾아 종합

      3. 유연성: 다양한 형태의 질문에 대응 가능


      핵심 기술 스택

      1. YouTube Data API v3

      • 목적: YouTube 댓글 수집

      • 특징: order="relevance"로 가장 관련성 높은 댓글 우선 수집

      2. LangGraph

      • 목적: 워크플로우 orchestration

      • 특징: 상태 기반 그래프로 복잡한 로직을 명확하게 표현

      3. Qdrant Vector Database

      • 목적: 댓글 임베딩 저장 및 의미 검색

      • 특징: 빠른 벡터 유사도 검색

      4. Gemini 2.5 Flash

      • 목적: 최종 응답 생성

      • 특징: 빠른 추론 속도와 한국어 지원


      구현 과정

      1. 댓글 수집: YouTube API 활용

      YouTube API를 통해 관련성 높은 댓글 500개를 수집합니다:

      def get_video_comments(self, video_id: str, max_results: int = 500) -> List[Document]:
          comments = []
          next_page_token = None
          
          while len(comments) < max_results:
              request = self.youtube.commentThreads().list(
                  part="snippet,replies",
                  videoId=video_id,
                  order="relevance",  # 핵심: 관련성 높은 댓글 우선
                  maxResults=min(100, max_results - len(comments)),
                  pageToken=next_page_token
              )
              response = request.execute()
              
              for item in response.get('items', []):
                  # 메인 댓글 처리
                  top_comment = item['snippet']['topLevelComment']['snippet']
                  doc = Document(
                      page_content=top_comment['textDisplay'],
                      metadata={
                          'author': top_comment['authorDisplayName'],
                          'likes': top_comment.get('likeCount', 0),
                          'reply_count': item['snippet'].get('totalReplyCount', 0),
                          'is_top_level': True
                      }
                  )
                  comments.append(doc)
                  
                  # 답글도 포함 (최대 5개)
                  if 'replies' in item:
                      for reply in item['replies']['comments'][:5]:
                          # 답글 처리...

      핵심 포인트:

      • order="relevance"로 YouTube 알고리즘이 선정한 중요 댓글 우선 수집

      • 답글도 포함하여 대화 맥락 보존

      • 메타데이터(좋아요 수, 작성자 등) 함께 저장

      2. 벡터 저장: Qdrant 활용

      수집한 댓글을 OpenAI의 text-embedding-3-small 모델로 임베딩하여 Qdrant에 저장:

      # vector_store.py
      youtube_comments_vector_store = QdrantVectorStore(
          client=qdrant_client,
          collection_name=youtube_comments_collection,
          embedding=OpenAIEmbeddings(model="text-embedding-3-small"),
      )
      
      # 댓글 저장
      youtube_comments_vector_store.add_documents(documents=comments)

      3. 적응형 검색 전략

      사용자 질문 타입에 따라 다른 검색 전략을 적용합니다:

      def analyze_query_node(state: GraphState, config: RunnableConfig) -> dict:
          last_message = state.messages[-1].content
          
          # 키워드 기반 쿼리 타입 판단
          comprehensive_keywords = ["overall", "general", "전체", "전반", "요약"]
          sentiment_keywords = ["positive", "negative", "긍정", "부정", "좋아", "싫어"]
          
          # 타입별 처리
          if any(kw in last_message for kw in comprehensive_keywords):
              query_type = "comprehensive"
          elif any(kw in last_message for kw in sentiment_keywords):
              query_type = "sentiment"
          else:
              query_type = "specific"

      각 타입별 검색 전략:

      def search_node(state: GraphState, config: RunnableConfig) -> dict:
          query = state.messages[-1].content
          
          if state.query_type == "comprehensive":
              # 포괄적 분석: 다양한 관점에서 댓글 수집
              batch_queries = [
                  query,
                  "positive opinion",
                  "negative opinion", 
                  "suggestions improvements"
              ]
              all_results = []
              for batch_query in batch_queries:
                  results = search_comments(batch_query, video_id, k=30)
                  all_results.extend(results)
              # 중복 제거 후 최대 100개 반환
              
          elif state.query_type == "sentiment":
              # 감정 분석: 감정 키워드 중심 검색
              search_results = search_comments(query, video_id, k=20)
              
          else:
              # 구체적 질문: 표준 검색
              search_results = search_comments(query, video_id, k=15)

      4. LangGraph 워크플로우 구성

      전체 프로세스를 LangGraph로 orchestration:

      def create_graph():
          workflow = StateGraph(GraphState, input=InputState, config_schema=Configuration)
          
          # 노드 추가
          workflow.add_node("collect_comments", collect_comments_node)
          workflow.add_node("analyze_query", analyze_query_node)
          workflow.add_node("search", search_node)
          workflow.add_node("generate_response", generate_response_node)
          
          # 조건부 라우팅
          workflow.set_conditional_entry_point(route_initial)
          workflow.add_conditional_edges(
              "collect_comments",
              route_after_collect,
              {
                  "analyze_query": "analyze_query",
                  "end": END
              }
          )
          
          # 순차적 엣지
          workflow.add_edge("analyze_query", "search")
          workflow.add_edge("search", "generate_response")
          
          return workflow.compile()

      워크플로우 특징:

      • 댓글이 이미 수집되어 있으면 바로 검색으로 진행

      • 각 노드가 독립적으로 동작하여 유지보수 용이

      • 상태 관리로 전체 프로세스 추적 가능


      코드 예시 & 실제 사용법

      설치 및 환경 설정

      # 필요 패키지 설치 (pyproject.toml 기준)
      uv pip install langchain-google-genai langchain-qdrant google-api-python-client
      
      # 환경 변수 설정 (.env)
      YOUTUBE_API_KEY=your_youtube_api_key
      OPENAI_API_KEY=your_openai_api_key
      QDRANT_URL=your_qdrant_url
      QDRANT_API_KEY=your_qdrant_api_key
      GOOGLE_API_KEY=your_google_api_key

      사용 예시

      from agent.graph import graph
      from langchain_core.messages import HumanMessage
      
      # 1. YouTube 영상 댓글 수집 및 분석
      youtube_url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
      
      # 전반적인 반응 분석
      response = graph.invoke({
          "messages": [HumanMessage(content=f"{youtube_url} 이 영상의 전반적인 반응이 어때?")],
          "youtube_url": youtube_url
      })
      
      # 구체적인 질문
      response = graph.invoke({
          "messages": [HumanMessage(content="편집에 대한 의견은 어때?")],
          "youtube_url": youtube_url,
          "comments_collected": True,
          "video_id": "dQw4w9WgXcQ"
      })
      
      # 감정 분석
      response = graph.invoke({
          "messages": [HumanMessage(content="부정적인 반응들은 주로 무엇 때문이야?")],
          "youtube_url": youtube_url,
          "comments_collected": True,
          "video_id": "dQw4w9WgXcQ"
      })

      실제 응답 예시

      질문: "이 영상의 전반적인 반응이 어때?"


      응답:

      YouTube 댓글 분석 결과, 시청자들의 전반적인 반응은 매우 긍정적입니다.
      
      **긍정적 측면:**
      - 편집 퀄리티에 대한 높은 평가 (특히 트랜지션과 효과음)
      - 출연자의 자연스러운 케미스트리
      - 유머러스한 내용 구성
      
      **부정적 측면:**
      - 영상 길이가 너무 길다는 의견
      - 일부 장면의 음질 문제 지적
      - 자막 싱크가 맞지 않는 부분
      
      **전반적 감정:**
      대부분의 시청자가 재미있게 시청했으며, 다음 영상을 기대한다는 반응이 많습니다.
      
      (Analysis based on 87 relevant comments)


      후기

      기술로서 과거의 불편함을 일부 해결

      대학생 시절의 귀찮음이 몇 년 뒤 기술로 해결되는 경험은 꽤나 만족스러웠습니다.

      하루 만에 MVP를 만들어내고, 실제로 YouTube 영상의 댓글을 분석하며 "이 영상의 전반적인 반응이 어때?"라는 질문에 즉각적인 답변을 받을 수 있다는 것이 신기했습니다.


      특히 RAG 시스템을 통해 단순히 키워드 매칭이 아닌, 의미적으로 유사한 댓글들을 찾아 종합적인 인사이트를 제공할 수 있다는 점이 인상적이었습니다.

      아직 남은 한계들

      하지만 MVP답게 여러 한계점도 명확합니다:

      1. 키워드 기반 쿼리 타입 분류: 현재 구현은 미리 정의한 키워드(comprehensive_keywords, sentiment_keywords)로만 쿼리 타입을 판단합니다.

        예를 들어 "전체"라는 단어가 없으면 포괄적 분석으로 인식하지 못합니다. LLM을 활용한 의미 기반 분류로 개선이 필요합니다.

      # 현재의 단순한 키워드 매칭 방식
      comprehensive_keywords = ["overall", "general", "summary", "전체", "전반", "요약"]
      if any(kw in last_message for kw in comprehensive_keywords):
          query_type = "comprehensive"
      1. 고정된 검색 전략: 각 쿼리 타입별로 검색 개수(k=15, 20, 30)가 하드코딩되어 있어, 댓글 수나 질문 복잡도에 따른 동적 조정이 불가능합니다.

      2. 재수집의 비효율성: 새로운 댓글을 반영하려면 전체를 다시 수집해야 하는 구조입니다. 증분 업데이트 방식이 필요합니다.

      3. 언어 혼재 처리: 영어와 한국어가 섞인 댓글들을 처리할 때, 임베딩 품질이 일관되지 않을 수 있습니다.

      이 외에도 수많은 문제점들이 존재하는데, 앞으로의 스터디 기간을 통해 차차 개선해나가보도록 하겠습니다.

      댓글 0

      DEVOTEE를 활성화 시키면
      지금 작성한 댓글에 AI가 댓글을 달아줍니다.

      smartcow.jung 님의 최신 블로그

      더보기
      동영상 기고하기