23.06.15
DEVOTEE를 활성화 시키면
지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.
버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.
| 컨텐츠 유형 | 제목 | 저장일 | 삭제 |
|---|
본인인증 로그인에 실패하였습니다.
회원이 아니시거나 본인인증 등록이
완료되지 않은 사용자입니다.
대학생 시절, 마음 맞는 친구들과 유튜브 채널을 운영한 적이 있습니다.
당연히 거의 모든 영상이 별로 인기가 없었지만, 학과를 소개하는 영상 두개 정도가 꽤나 시청자 반응이 좋아 댓글이 많이 달렸습니다.
시청자들이 우리의 영상에 대해 어떠한 반응을 하는지 궁금해 댓글을 여러번 읽었지만, 몇번 해보니 배부른 소리지만 꽤나 귀찮게 느껴졌습니다.
제 천성이 게을러서 그런지 자연스레 더 편한 방법은 없을까 고민하게 되었습니다.
시간이 흘러 AI 기술이 발전함에 따라 드디어 과거의 제 귀찮음을 기술적으로 해결할 수 있게 되었습니다.
바로 RAG(Retrieval-Augmented Generation) 입니다.
YouTube의 기본 댓글 시스템은 몇 가지 문제점을 가지고 있습니다:
정렬 옵션의 한계: '인기순'과 '최신순' 두 가지 정렬만 제공
검색 기능 부재: 특정 키워드나 주제로 댓글을 검색할 수 없음
전체 맥락 파악 어려움: 수천 개의 댓글을 일일이 읽어야 전체 반응을 알 수 있음
감정 분석 불가: 긍정/부정 반응의 비율이나 주요 감정을 파악하기 어려움
Q: "시청자들이 이 영상에서 가장 좋아한 부분이 뭐야?"
A: "댓글 분석 결과, 시청자들이 가장 좋아한 부분은
1) 3:45 부분의 편집 퀄리티 (32개 댓글)
2) 출연자의 자연스러운 리액션 (28개 댓글)
3) 배경음악 선곡 (15개 댓글) 입니다."이렇게 자연어로 질문하고 답을 받을 수 있다면 얼마나 편할까요?
graph LR
A[YouTube URL 입력] --> B[상위 500개 댓글 수집]
B --> C[벡터 DB에 저장]
C --> D[사용자 질문 입력]
D --> E[쿼리 타입 분석]
E --> F[적응형 검색]
F --> G[AI 응답 생성]RAG는 검색(Retrieval)과 생성(Generation)을 결합한 방식입니다. 이 프로젝트에서 RAG를 선택한 이유:
정확성: 실제 댓글 데이터를 기반으로 답변
맥락 이해: 의미적으로 유사한 댓글들을 찾아 종합
유연성: 다양한 형태의 질문에 대응 가능
목적: YouTube 댓글 수집
특징: order="relevance"로 가장 관련성 높은 댓글 우선 수집
목적: 워크플로우 orchestration
특징: 상태 기반 그래프로 복잡한 로직을 명확하게 표현
목적: 댓글 임베딩 저장 및 의미 검색
특징: 빠른 벡터 유사도 검색
목적: 최종 응답 생성
특징: 빠른 추론 속도와 한국어 지원
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 알고리즘이 선정한 중요 댓글 우선 수집
답글도 포함하여 대화 맥락 보존
메타데이터(좋아요 수, 작성자 등) 함께 저장
수집한 댓글을 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)사용자 질문 타입에 따라 다른 검색 전략을 적용합니다:
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)전체 프로세스를 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_keyfrom 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답게 여러 한계점도 명확합니다:
키워드 기반 쿼리 타입 분류: 현재 구현은 미리 정의한 키워드(comprehensive_keywords, sentiment_keywords)로만 쿼리 타입을 판단합니다.
예를 들어 "전체"라는 단어가 없으면 포괄적 분석으로 인식하지 못합니다. LLM을 활용한 의미 기반 분류로 개선이 필요합니다.
# 현재의 단순한 키워드 매칭 방식
comprehensive_keywords = ["overall", "general", "summary", "전체", "전반", "요약"]
if any(kw in last_message for kw in comprehensive_keywords):
query_type = "comprehensive"고정된 검색 전략: 각 쿼리 타입별로 검색 개수(k=15, 20, 30)가 하드코딩되어 있어, 댓글 수나 질문 복잡도에 따른 동적 조정이 불가능합니다.
재수집의 비효율성: 새로운 댓글을 반영하려면 전체를 다시 수집해야 하는 구조입니다. 증분 업데이트 방식이 필요합니다.
언어 혼재 처리: 영어와 한국어가 섞인 댓글들을 처리할 때, 임베딩 품질이 일관되지 않을 수 있습니다.
이 외에도 수많은 문제점들이 존재하는데, 앞으로의 스터디 기간을 통해 차차 개선해나가보도록 하겠습니다.
DEVOTEE를 활성화 시키면
지금 작성한 댓글에 AI가 댓글을 달아줍니다.