개요
본 프로젝트는 KT, SK브로드밴드, LG U+ 등 국내 주요 통신 3사의 셋톱박스 설명서를 기반으로,
사용자의 자연어 질문에 실시간으로 LLM을 통해 응답하는 RAG기반 기술지원 챗봇을 구축하는 것을 목표로 하였다.
이전 실습을 바탕으로 실무에 적용할 수 있는 주제를 통해 나의 기술 확장성을 키우고자 진행하였다.
기획 배경
기존의 GPT 기반 응답은 인터넷 검색 결과에 의존하여 부정확하거나 일반적인 수준의 답변을 제공하는 한계가 있었다. 이에 따라, 실제 제품 설명서를 벡터 데이터베이스에 구축함으로써, 보다 신뢰도 높은 문서 기반 응답이 가능한 시스템을 구현하고자 하였다.
개발 프로세스
- 주제 선정 및 자료 수집
각 통신사 공식 웹사이트 및 고객지원 채널을 통해 셋톱박스 관련 매뉴얼과 설명서를 수집하였다. - 문서 임베딩 및 벡터 DB 구축
수집한 문서를 텍스트로 전처리하고, OpenAI 임베딩 모델을 활용하여 FAISS 기반 벡터 데이터베이스에 저장하였다. - 정확도 검증 및 리트리버 설계
다양한 질의에 대한 검색 정확도를 검토하며, 적절한 chunk size 및 검색 파라미터를 실험적으로 조정하였다. - LangChain 기반 질의응답 파이프라인 구축
LangChain의 ConversationalRetrievalChain 구조를 활용하여, 검색된 문서를 기반으로 GPT가 응답을 생성하도록 구현하였다. - Gradio UI 연동
실시간 인터페이스를 제공하기 위해 Gradio를 연동하여 사용자 친화적인 데모 환경을 구축하였다.
[자료 수집 출처]
https://www.bworld.co.kr/customer/guide/btv_guide_list.do?menu_id=C03020000
https://help.kt.com/serviceinfo/ManualDownloadInfo.do
https://www.lguplus.com/support/self-troubleshoot/guide
핵심 구현 내용
1. 셋톱박스 설명서 문서 분류 및 벡터화
- 설명서 PDF를 PyPDFLoader로 불러온 후 RecursiveCharacterTextSplitter를 사용하여 문서를 청크 단위로 분할하였다.
- 청크에는 통신사(변수명 : provider)와 기기명(변수명 : device) 정보를 메타데이터로 추가하여 문서 분류와 검색 최적화를 함께 수행하였다.
- OpenAIEmbeddings의 text-embedding-3-small 모델을 사용하여 FAISS 기반 벡터스토어를 구축하였다.
def build_vectorstores():
#splitter 선언 및 하이퍼파라미터 설정
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", ".", " "]
)
#upper 치환 후, if 분기를 통해 설명서 분류
for file in os.listdir(PDF_DIR):
if not file.endswith(".pdf"):
continue
file_path = os.path.join(PDF_DIR, file)
if "KT" in file.upper():
provider = "kt"
elif "BTV" in file.upper():
provider = "skt"
elif "UPLUS" in file.upper():
provider = "uplus"
else:
continue
loader = PyPDFLoader(file_path)
docs = loader.load()
chunks = splitter.split_documents(docs)
# 기기명과 통신사(제공자)를 메타 데이터를 넣어 탐색 속도 향상
for chunk in chunks:
chunk.metadata["provider"] = provider
chunk.metadata["device"] = file.replace(".pdf", "")
vectordb = FAISS.from_documents(chunks, embedding)
save_path = os.path.join(FAISS_DIR, f"{provider}_{file.replace('.pdf','')}")
vectordb.save_local(save_path)
2. 대화형 검색 체인 구성
- 사용자의 입력 문장에서 통신사 키워드를 자동 추출하여 해당 벡터스토어를 선택한다.
- 선택된 문서에서 상위 K개의 관련 청크를 검색하고, 이를 기반으로 ChatOpenAI 모델이 답변을 생성한다.
- ConversationSummaryMemory를 통해 대화 흐름을 유지하며 사용자 경험을 개선하였다.
- 또한 참고 자료가 어디에서 참조되었는지 프로토버전에서는 출력을 진행하였다.(잘 찾았는지 보기위해서)
# 일부분 코드 발췌
retriever = vectordb.as_retriever(search_kwargs={"k": 3})
prompt = ChatPromptTemplate.from_messages([
("system", "당신은 셋톱박스와 TV 설정에 특화된 상담 전문가입니다. 답변은 정확하고 간결하게 제공하세요."),
("human", "질문: {question}\n\n관련 문서 내용:\n{context}")
])
memory = ConversationSummaryMemory(
llm=llm,
memory_key="chat_history",
output_key="answer",
return_messages=True
)
chain = ConversationalRetrievalChain.from_llm(
llm=llm,
retriever=retriever,
memory=memory,
combine_docs_chain_kwargs={"prompt": prompt},
return_source_documents=True,
output_key="answer"
)
3. 프론트엔드 인터페이스
- Gradio 기반으로 실시간 QA 인터페이스를 구축하였다.
전체 코드
import os
import gradio as gr
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain.chains import ConversationalRetrievalChain
from langchain.memory import ConversationSummaryMemory
# -----------------------------
# 0. API 키 로딩 함수
# -----------------------------
def load_api_keys(filepath="api_key.txt"):
with open(filepath, "r") as f:
for line in f:
line = line.strip()
if line and "=" in line:
key, value = line.split("=", 1)
os.environ[key.strip()] = value.strip()
path = '/content/drive/MyDrive/langchain/'
# API 키 로드 및 환경변수 설정
load_api_keys(path + 'api_key.txt')
# -----------------------------
# 1. 경로 및 모델 설정
# -----------------------------
PDF_DIR = "/content/drive/MyDrive/SetTopBox_manuals/"
FAISS_DIR = "./vectorstores"
embedding = OpenAIEmbeddings(model="text-embedding-3-small")
llm = ChatOpenAI(model_name="gpt-4o-mini", temperature=0.3)
# -----------------------------
# 2. 벡터 DB 구축 함수
# -----------------------------
def build_vectorstores():
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", ".", " "]
)
for file in os.listdir(PDF_DIR):
if not file.endswith(".pdf"):
continue
file_path = os.path.join(PDF_DIR, file)
if "KT" in file.upper():
provider = "kt"
elif "BTV" in file.upper():
provider = "skt"
elif "UPLUS" in file.upper():
provider = "uplus"
else:
continue
loader = PyPDFLoader(file_path)
docs = loader.load()
chunks = splitter.split_documents(docs)
for chunk in chunks:
chunk.metadata["provider"] = provider
chunk.metadata["device"] = file.replace(".pdf", "")
vectordb = FAISS.from_documents(chunks, embedding)
save_path = os.path.join(FAISS_DIR, f"{provider}_{file.replace('.pdf','')}")
vectordb.save_local(save_path)
# -----------------------------
# 3. 통신사 자동 추출 함수
# -----------------------------
def extract_provider(text: str) -> str:
text = text.lower()
if "kt" in text:
return "kt"
elif "skt" in text or "sk" in text:
return "skt"
elif "u+" in text or "유플" in text or "lg" in text:
return "uplus"
else:
return "unknown"
# -----------------------------
# 4. LangChain RAG 체인 생성
# -----------------------------
def build_chain(user_input: str):
provider = extract_provider(user_input)
if provider == "unknown":
raise ValueError("통신사를 인식하지 못했습니다. (예: KT, SKT, 유플러스 등)")
matches = [f for f in os.listdir(FAISS_DIR) if f.startswith(provider)]
if not matches:
raise FileNotFoundError(f"📁 {provider} 통신사의 벡터 DB가 없습니다.")
vectorstore_path = os.path.join(FAISS_DIR, matches[0])
vectordb = FAISS.load_local(
vectorstore_path,
embedding,
allow_dangerous_deserialization=True
)
retriever = vectordb.as_retriever(search_kwargs={"k": 3})
prompt = ChatPromptTemplate.from_messages([
("system", "당신은 셋톱박스와 TV 설정에 특화된 상담 전문가입니다. 답변은 정확하고 간결하게 제공하세요."),
("human", "질문: {question}\n\n관련 문서 내용:\n{context}")
])
memory = ConversationSummaryMemory(
llm=llm,
memory_key="chat_history",
output_key="answer",
return_messages=True
)
chain = ConversationalRetrievalChain.from_llm(
llm=llm,
retriever=retriever,
memory=memory,
combine_docs_chain_kwargs={"prompt": prompt},
return_source_documents=True,
output_key="answer"
)
return chain
# -----------------------------
# 5. Gradio 인터페이스
# -----------------------------
chat_history = []
# -----------------------------
# Gradio 응답 처리 함수
# -----------------------------
def chat(user_input, history):
global chat_history
try:
chain = build_chain(user_input)
result = chain(user_input)
answer = result["answer"]
# 출처 문서 요약
sources = result.get("source_documents", [])
source_preview = ""
if sources:
previews = []
for doc in sources:
device = doc.metadata.get("device", "설명서")
preview = f"📄 {device}: {doc.page_content[:200].strip()}..."
previews.append(preview)
source_preview = "\n\n---\n".join(previews)
# 메시지 저장 (OpenAI message style)
chat_history.append({"role": "user", "content": user_input})
chat_history.append({"role": "assistant", "content": answer + "\n\n📚 관련 문서:\n" + source_preview})
return chat_history, ""
except Exception as e:
chat_history.append({"role": "user", "content": user_input})
chat_history.append({"role": "assistant", "content": f"⚠️ 오류 발생: {str(e)}"})
return chat_history, ""
# -----------------------------
# Gradio 인터페이스 구성
# -----------------------------
with gr.Blocks() as demo:
gr.Markdown("## 📺 통신사 셋톱박스 설명서 챗봇")
gr.Markdown("KT, SK브로드밴드, U+ 셋톱박스 관련 질문을 입력하세요.\n\n예: `KT 셋톱박스에서 자막 설정은 어떻게 하나요?`")
chatbot = gr.Chatbot(label="📡 셋톱박스 QA", type="messages")
textbox = gr.Textbox(label="질문 입력", placeholder="예: SK 브로드밴드 셋톱박스에서 VOD 설정 어떻게 해요?", lines=2)
clear = gr.Button("대화 초기화")
textbox.submit(chat, [textbox, chatbot], [chatbot, textbox])
clear.click(lambda: ([], ""), None, [chatbot, textbox], queue=False)
# -----------------------------
# 실행
# -----------------------------
if __name__ == "__main__":
build_vectorstores()
demo.launch(share=True)
결과
다음은 Gradio를 통해 구현한 프론트엔드 인터페이스의 시연 화면이다.
왼쪽 예시는 KT 셋톱박스 관련 질문을, 오른쪽 예시는 SK브로드밴드 셋톱박스 관련 질문을 입력한 사례다.
각 질문에 대해 시스템은 사용자 질문과 벡터 DB에서 검색된 최고 유사도 문서의 내용을 함께 LLM에 전달하여,
보다 정확하고 문맥에 맞는 응답을 생성하였다.
이 과정을 통해 실제 설명서 기반의 정제된 답변이 생성되는 것을 확인할 수 있었으며,
사용자 질문에 따라 통신사별 설명서를 정확히 분기하여 처리하는 구조가 유효함을 검증할 수 있었다.
다음은 Chat GPT 사이트 검색과 구축한 챗봇에 동일한 질문을 한 결과이다
다음은 Gradio를 통해 구현한 화면이다.
기술적 회고 및 개선 과제
다음은 진행하면서 추후에 고도화하여 해결해야하는 과제를 적어보았다.
▸ PDF 내 표와 이미지 처리 문제
설명서에는 표 형태의 설정 정보나 장치 이미지가 삽입된 경우가 많았는데, 현재의 텍스트 추출 방식은 이러한 구조를 완전히 보존하지 못하는 한계가 있다.
또한 추출된 텍스트에는 특수문자, 줄바꿈, 기호 등이 혼합되어 벡터화 품질에 영향을 주는 사례도 관찰되었다.
향후에는 다음과 같은 개선을 고려해보고자 한다
- 표 구조 인식 기능이 강화된 PDF 파서(PDFPlumber 등) 도입
- 이미지에서 텍스트 추출이 필요한 경우 OCR 기반 보완 모듈 추가
- 불필요한 특수문자 필터링 전처리 강화
마무리하며
이번 프로젝트는 이틀동안 진행하였으며,
단순한 문서 검색을 넘어, 실제 유저 질의 흐름에 맞춘 응답 생성이 가능한 기술지원 챗봇을 만드는 데 의의를 두었다.
특히 다수 통신사의 이질적인 매뉴얼을 일관된 방식으로 처리하고, 장치별 문서 구조를 메타데이터화함으로써 유지보수성과 확장성의 기반을 마련할 수 있었다.
또한 이번 과정을 통해 Gradio를 처음 활용하여 머신러닝 프로토타입을 직접 시연할 수 있었던 경험은 개인적으로 매우 뜻깊었다.
모델의 동작 원리를 직관적으로 확인하고, 사용자의 피드백을 실시간으로 반영할 수 있는 인터페이스를 빠르게 구성함으로써,
ML 공부하는 학생으로서 실용성과 전달력을 함께 고려한 개발 접근을 할 수 있었다.
향후에는 사용자 피드백을 반영하여 보다 자연스러운 상호작용과 정확한 문서 기반 답변을 제공할 수 있도록 시스템을 고도화할 계획이다.
'머신러닝 & 딥러닝 & AI 맛보며 친해지기' 카테고리의 다른 글
LangChain으로 구현한 KT 에이블스쿨 QnA RAG 챗봇 개발기 (0) | 2025.05.10 |
---|---|
[DL] 스마트폰 센서 기반 인간 행동 분류 모델 구현 및 성능 평가 (1) | 2025.05.01 |
실시간 객체탐지 YOLO란? (2) | 2025.04.24 |
RAG(Retrieval-Augmented Generation)와 Vector DB 란? (3) | 2025.04.21 |
[ML] 분류(Classification)와 회귀(Regression)의 차이 (0) | 2025.04.18 |