카드사 상담원의 업무 효율성 향상을 위한 AI 기반 실시간 문서 검색 및 상담 후 피드백 지원 시스템
1. 데이터 고민
카드사로 선정하면서 우선 가장 큰 장점은 데이터가 방대하다는 것이었다.
카드 상품만 해도 몇 백 개 단위고, 각 카드마다 연결된 제휴 브랜드, 혜택, 관련 카드 등
카드라는 게 워낙 많은 사람들이 일상적으로 사용하는 물건이다보니 꽤 여러 방향으로 탐색할 수 있었다.
내일배움카드나 교통패스 같은 국가 정책과 엮여있는 케이스도 다양한데,
이런 정책 관련 내용은 카드사에서 어디까지를 다루어야 하는지도 고민이 필요했다.
그래서 금감원 홈페이지도 모자라 금융법과 소비자보호법까지 뒤져보면서 의도치 않은 지식들도 얻어왔다.
고객들의 행동 패턴 역시 중요하다.
나라면 어떤 상황에 뭐가 궁금할지, 상담원한테 전화까지 하는 상황이라면 어떤 문의일지,
기존 카드사들에 빈도 높게 들어오는 유형이 무엇인지 등등
이 부분에서 팀원들과 정말 많은 의견을 나눴다.
2. 쉽지 않다.
탐색하면서 든 생각은 데이터가 너무 많고 흩어져있다...
유사하거나 아예 같은 카테고리의 내용인데도 서로 다른 페이지에 적혀있고,
한 쪽에 적혀있는 내용이 다른 쪽엔 없어서 뭔가 기준이 다른가? 싶기도 하고,
카드 분실이나 한도 확인 같은 건 내가 직접 신청해야 후속 정보가 나와서 정확히 어떤 프로세스로 처리하는지도 모호하다.
무엇보다,
일반 고객들 보라고 이쁘게 홈페이지에 박혀있는 이 내용들을
텍스트로 어떻게 정리해서 어떤 형식으로 전처리해야하는가? 참 난감하다.
약관이나 상품안내를 pdf로 주는건 다행인데 문제는 표가 너무 많다! 추출이 문제다.
3. 수집 계획
시간은 가고, 서둘러 데이터 수집에 착수해야 했다.
우리팀은 다음과 같이 데이터를 모아보기로 결정했고, 깃허브에 데이터 관리용 레포를 따로 만들어 push하기로 했다.
1. 카드사의 카드 상품 (제휴카드 포함) 2. 특수 목적 카드 (K패스 등) 3. 이용약관, 개인정보 방침 등 법률 기반 안내 4. 해외이용, 신용카드 용어, 대출안내 등 소비자 가이드 5. 실제 상담 데이터 6. 카드사가 주목하는 사회 이슈 (정보유출 사건, 사칭 사기, 보이스피싱 등)
나는 여기서 3, 4, 6을 담당했다.
3, 4는 크롤링으로 해결되는 영역이었고
6은 카드사의 '공지사항' 페이지를 대상으로 조사했는데, 페이지가 CSR 형식으로 되어있어 HTML 안에 데이터가 없었고, 동적 크롤링을 시도했으나 보안 이슈로 페이지 구조가 탐지가 안되어 수동으로 수집 후 전처리를 거쳤다...
회사생활을 제대로 해본 것도 아니고 직장인 입장에서 필요한 시스템을 구상한다는 건 정말 어려웠다.
게다가 문서 검색(RAG)이 핵심이 되어야 한다는게 가장 중요한 부분이었다.
1. 브레인스토밍
일단 어떻게 접근을 해야할지에 대해 강사님의 조언도 구해보고,
기업 관련 유튜브도 찾아보면서 우리 팀은 다음과 같은 방식으로 기획을 위한 틀을 잡았다.
회사라는 곳은 어떤 공간인가? - 모든 회사는 쌓인 데이터가 많다 - 주로 문서: 한글, pdf, word, ppt - 보고서, 품의서, 결재확인, 영수증 - 어떤 하나의 의사결정을 위해 그 과정에서 생산되고 반려되는 데이터가 아주 많다 - 그것들이 실제로 활용되고 있을까? - 대부분은 그냥 쌓인 데이터 그 자체로만 남아있다
⇒ 이걸 어떻게 써야하는가?
제일 먼저 든 생각은 문서 작업이었다.
도메인을 떠나 모든 회사는 문서가 쌓이고, 비효율적인 업무가 남발하며, 방치되는 문서가 무수히 많다.
두번째는 IT 기업이었다.
아무래도 내 전공 분야고, 가장 이것저것 주워들은게 많은 분야니까.
그 다음은 내 취미에서 비롯되어 영화와 여행 분야였다.
다만 영상 업계가 도메인이 되면 우리 시스템도 멀티모달로 가야할 텐데 그러면 프로젝트가 비대해질 것 같고,
여행 분야는 지역적인 데이터를 요구함과 동시에 지나치게 동적인 업계 특성상 RAG의 영역을 살리기 힘들 것 같았다.
그래도 일단 도메인이 흥미롭고, 주제가 재미있을 것 같다는 팀원들의 의견이 모여 기각되지는 않았다.
주제 고민과 함께 가장 난관이었던 부분은 어떤 도메인이든 회사 내부 데이터를 도무지 구할 수가 없었다는 것.
그러다보니 필연적으로 모든 데이터를 공개하는 공공기관으로 눈을 돌렸고, 이쪽에서 나쁘지 않은 구상이 나온 것 같다.
Miro에서 온갖 방법으로 마인드맵을 그려가며 장렬한 사투를 벌였고
2. 주제 후보 선정
그 결과 우리 팀에서 구상된 주제 후보는 다음과 같았다.
1. IT 기업 커뮤니케이션 지원 및 문서 작성 자동화
2. 공공기관 법령 해석 및 문서 작성 지원을 통한 프로세스 간소화
3. 여행사 투어 상품 기획 도우미
4. 과거 데이터 분석 및 아카이브화를 통한 프로젝트 리뷰 시스템
5. 디자인회사 시각 자료 샘플 제시 및 브랜드 가이드 체크 시스템
6. 콜센터 상담원 업무 지원 및 피드백/교육 시스템
그리고 멘토님으로부터 다음과 같은 피드백을 받았다.
1번 - RAG의 영역이 너무 적어보인다. 그것 말고는 괜찮은데 기획 볼륨이 아쉽다.
2번 - 나쁘지 않은데 특별히 차별화된 서비스는 아닌 것 같다. 일단 킵하고 좀 더 생각해보기.
3번 - 쇼핑, 매장, 식당 등 특정 지역에 대한 로컬 데이터가 굉장히 많이 필요할 텐데 감당할 수 있을지?
4번 - 어떤 데이터를 써야할지 모호하다. 도메인을 확정해야 할듯.
5번 - 대주제에 맞는 괜찮은 기획인데... 이미지/영상 처리 쉽지 않다.
6번 - 데이터가 많고 구체화만 잘 되면 괜찮겠다. 다만 어떤 문서를 어떻게 가져올건지 고민 필요.
3. 주제 확정
일단은 6번으로 확정되었고, 우리의 최종 주제는
콜센터 상담원 업무 지원 및 피드백/교육 시스템이 되었다.
이후는 WBS와 요구사항정의서를 작성하면서 주제 구체화에 많은 시간을 들였다.
도메인 선정... 시장조사... 수집 가능한 데이터 탐색... 제공할 기능 등등
콜센터 전화기가 쉬지 않는 도메인 후보로는
보험, 공공기관, 항공사, 카드사, 통신사, 의료/헬스케어 이렇게 6개를 꼽았다.
4. 도메인 선정
우선 보험과 의료 쪽은 전문성의 벽이 너무 높고,
만약에라도 잘못된 정보를 제공했을 때의 법적 책임도가 타 업계에 비해 너무 무겁다고 생각해 기각되었다.
통신사는 이미 3사가 각각 자체적인 AICC가 너무 잘 되어있어 차별성을 찾기가 어려웠다. 한마디로 도메인 자체가 입구가 높다.
공공기관은 민원의 범위가 너무 넓어 이 안에서 또 도메인을 정해야하고, 지역별로 세세하게 다른 조례나 법령을 명확하게 해석할 수 있어야 해 어려움이 따른다.
항공사는 팀원들 모두가 재밌어보인다고 호응했던 도메인이지만, 공항 특성상 과거 기반 정적 데이터보다는 현재 시점의 동적 데이터(날씨, 비행기 결항, 티켓 현황, 현지 사정 등)가 주를 이룰 것이고 마땅히 참고할만한 데이터가 없어 아쉽게 보내줘야 했다.
결과적으로는 카드사로 낙점되었는데, 다른 걸 소거하고 남아서 어영부영 정해진 건 아니다.
당연히 카드사로 선택한 이유는 있다.
우리가 판단한 카드사의 가장 큰 장점은 우리 생활에 밀접하여 익숙하고,
카드 관련으로 문의해본 경험은 많은 사람들이 있기에 공감이 가능하며,
혜택, 이벤트, 지원 정책 등 데이터가 많다.
물론 혜택 같은건 수시로 바뀌기에 지속적인 갱신이 필요하고,
이쪽도 이미 실무에 적용 중인 AI 챗봇들이 있는 등 우리가 경계해야 할 부분이 꽤 있다.
# Chroma가 내부적으로 사용할 임베딩 함수(모델) 정의
embed_func = embedding_functions.SentenceTransformerEmbeddingFunction(
model_name=MODEL_NAME
)
# 영구 클라이언트 생성
client = chromadb.PersistentClient(path=DB_PATH)
# 컬렉션(테이블) 생성
# embedding_function을 지정하면 add할 때 자동으로 문장을 벡터로 변환해줌
collection = client.get_or_create_collection(
name=COLLECTION_NAME,
embedding_function=embed_func
)
chromadb.utils의 embedding_functions를 통해 Chroma에서 사용할 임베딩 모델을 정의할 수 있다.
콜렉션 생성 시 모델과 함께 임베딩 모델을 파라미터로 넣어주면 DB에 add하면서 자동으로 벡터화를 진행한다.
# documents에 원본 텍스트를 추가
# Chroma가 내부적으로 embed_func를 호출해 벡터로 변환 후 저장
collection.add(
documents=batch_sentences,
ids=batch_ids
)
이 코드를 통해 documents에 벡터화를 거친 sentences들이 저장된다.
UI는 Streamlit으로 구현
개인 실습용이므로 아주 단순하게 진행하되 실제로 누군가가 사용한다는 느낌으로 구성
gTTS를 적용해 음성으로 들을 수 있는 기능을 추가했다.
문제 상황
단일 키워드보단 어느 정도 길이가 되는 문장을 검색해야 유사성 높은 결과를 뽑아준다.
데이터 안에 인삿말이나 감탄사가 꽤 많이 들어가있는데
이런 범용성 높은 표현들이 대체로 상위 유사도를 차지하기 때문에
자꾸 검색어 매칭 시 튀어나오는 것으로 추정된다.
1차적으로 5글자 이하의 짧은 문장을 제거해봤지만
이 부분에 대한 정확한 처리방법은 좀 더 고민해봐야겠다.
DB 선택의 이유
널리 쓰이는 FAISS, Chroma, Pinecone 중에 Chroma로 선정했다.
FAISS
DB에 벡터 인덱스만 저장되므로, 문장과 매칭할 때 원본 텍스트 파일을 요구한다.
DB에서 ID를 찾고 이 ID로 다시 CSV를 조회해야 하는 이중 파이프라인
IndexFlatL2 사용 시 완전 탐색을 하므로 검색 속도가 느리다.
Chroma
텍스트와 벡터를 함께 저장
DB 생성 과정이 느리지만, 서비스 단계에서 탐색이 빠름
DB의 용량이 커지지만 로컬에서 진행하는 소규모 프로젝트에 적합
LangChain과 간단하게 연계 가능
모든 문장을 비교하는게 아닌, '적당히 가까운 문장'에 대한 지도를 생성하는 근사 이웃 탐색(ANN)
플레이어가 배치한 가구의 형태, 위치와 같은 공간 정보를 LLM이 판단하도록 하는 게임이다.
게임 제작이 생각보다 정말 어려웠지만 또 내가 의도한대로 화면이 만들어지니 꽤 재밌는 경험이었다.
게임에 대한 정보는 GitHub에 있으니 여기에는 프로젝트에서 사용한 프롬프팅 기법을 기록해둔다.
1. 백파이어(Backfire)
"고양이를 상상하지 마세요"라고 한다면, 이 말을 들은 사람들의 머리 속에는 이미 고양이가 있을 것이다.
AI 용어는 아니지만 나는 이게 LLM의 동작 원리와 부합한다고 생각했다.
LLM은 제공된 정보를 어떻게든 응답에 참조한다.
system_prompt = (
f"당신은 고객 '{selected_persona['name']}'입니다. 당신의 상세 정보는 다음과 같습니다:\n"
f"- 취향: {selected_persona['taste']}\n"
f"- 성향: {selected_persona['tendency']}\n"
f"- 말투: {selected_persona['tone']}\n\n"
"당신은 지금부터 디자이너에게 방을 의뢰할 것입니다. 다음 3단계에 따라 행동하세요.\n\n"
"1. [내면의 생각] : 당신의 취향과 성향에 따라, 다음 가구 목록에서 '반드시 있었으면 하는' 가구 **3~5개**를 **마음 속으로** 정합니다. 이것은 당신의 '비밀 요구사항(Wishlist)'입니다.\n"
f"<가구 목록>: {FURNITURE_NAMES_LIST}\n\n"
"2. [디자이너에게 할 말] : 1번에서 고른 가구의 **이름을 절대 직접 말하지 마세요.** 대신, 그 가구들이 왜 필요한지 '목적'이나 '행위'를 암시하는 방식으로 **매우 모호하게** 1-2 문장으로 묘사합니다.\n"
" (예: '소파' -> '편안히 기댈 곳이 필요해요.')\n"
" (예: '스토브', '냉장고' -> '집에서 요리하는 것을 좋아합니다.')\n"
" (예: '컴퓨터', '책장' -> '밤에 조용히 작업할 공간이 필요해요.')\n\n"
"3. [출력 형식] : 다른 말은 절대 하지 말고, 오직 다음 형식으로만 응답하세요:\n"
"[WISHLIST]\n"
"(여기에 1번에서 고른 가구 이름들을 쉼표로 구분하여 작성)\n"
"[REQUEST]\n"
"(여기에 2번에서 생성한 모호한 의뢰서 내용을 작성)"
)
'고객이 원하는 가구'를 직접 말하면 안되므로, 내부적으로는 정답을 정해놓고 그것을 빙빙 돌려서 말하도록 유도했다.
2. LLM as a Judge
처음에는 평점 계산 메커니즘으로 코사인 유사도를 사용했는데, 이상하게 모든 경우에 4~5점의 고득점이 나왔다.
알고보니 소파와 침대 같은 어휘들이 높은 유사도를 가지기 때문에,
만약 고객이 소파를 원했는데 소파를 놓지 않고 침대를 놓은 상황에도 평점이 높아진 것이다.
소파가 없으면 확실하게 소파가 없는 것에 대한 패널티를 부여해야 하는데 말이다.
system_prompt = (
f"당신은 고객 '{persona['name']}'입니다. "
f"당신의 성격과 말투는 다음과 같습니다: {persona['tendency']}\n"
"당신은 방금 디자이너의 작업에 점수를 매겼습니다. "
"당신의 성격과 말투에 100% 몰입하여, '왜' 그 점수를 주었는지 1-2문장의 구체적인 피드백을 한글로 작성하세요. "
"점수가 높으면 당신의 방식대로 칭찬하고, 낮으면 당신의 방식대로 비판하세요."
)
LLM을 평가하는 LLM이라는 개념의 LLM as a Judge라는게 있는 것을 알고 적용해봤다.
이렇게 하는게 맞는지는 잘 모르겠지만, 코사인 유사도를 사용하던 때보다 좀 더 다양하고 풍부한 피드백이 나온 것은 사실이다.
EEVE라는 한국어 특화 모델을 사용했는데,
평소 사용하는 GPT 같은 고급 모델에 비해 현저히 떨어지는 성능을 프롬프팅하는게 상당히 어려웠다.
자연어 처리가 평상시 사용하는 언어를 다루는 영역이다보니 활용도도 높아보이고 배우면서도 재밌다. 그리고 그와 별개로 상당히 헷갈리고 어렵다. 모델에 대한 이해도를 확실히 잡아놔야겠다. 원리적인 부분에 집착하기보단 우선 이 모델이 뭐를 받아서 뭐를 뱉고, 어떤 특징을 가지고 있는지를 체크하고 넘어가려 한다.
해커톤 사전과제 제출이 끝났다. 팀원들과 여러 테스트를 해보고 다같이 보는 곳에서 제출했지만 솔직히 합격할지는 모르겠다. 요구사항이 너무 모호했고, 동시다발적으로 갱신되어야 하는 내부값이 많았는데 우리는 각 값을 따로따로 확인하는 식으로 테스트를 해서 어느 부분에 빼먹은게 있을 수도 있겠다. 그래도 과정에서 많이 배우고 많은 경험을 했기에 상당히 만족스럽다.
순차 데이터(Sequential Data): 텍스트, 시계열, 음성처럼 데이터의 순서가 의미를 갖는 데이터
시퀀스 모델링: 순차 데이터의 패턴을 학습하는 방법론
순환 신경망(RNN)
순환 구조를 가지는 모델
각 시점(time step)에서 이전 시점의 정보(은닉 상태 Ht-1)를 현재 시점의 입력(Xt)과 함께 받아 현재의 은닉 상태(Ht) 생성
이 은닉 상태를 통해 과거의 정보를 기억한다.
각 time step이 하나의 layer 역할을 하므로, 역전파를 Through Time (시간을 거슬러) 방식으로 진행
입력 데이터를 요약하고, 새로운 입력이 들어올 때마다 기억을 수정
모든 입력을 처리한 후 남은 기억은 시퀀스를 전체적으로 요약하는 정보로 활용
입력마다 기억을 갱신하며 순환적으로 진행 → 영단어 외우듯 반복 암기
장기 의존성 문제 (Long-Term Dependency)
시퀀스가 길어질수록 맨 처음의 정보가 뒤쪽까지 전달되기 어려워(기울기 소실) 장기적인 패턴을 학습하기 어려움
입출력 형식
One-to-One
One-to-Many
Many-to-One
Many-to-Many (Sequence Labeling)
Many-to-Many (Encoder to Decoder)
하나를 받아 하나 출력
하나를 받아 시퀀스 출력
시퀀스를 받아 하나 출력
각 요소마다 라벨 매핑 (입출력 길이 동일)
입력과 출력이 유동적
이미지 분류
이미지 캡셔닝, AI 노래
감정 분석, 스팸 탐지
품사 태깅, 객체 인식
번역, 챗봇, AICC
LSTM(Long Short-Term Memory)
RNN의 장기 의존성 문제를 해결하기 위해 고안된 모델
셀 상태(Cell State, Ct)
장기 기억을 담당하는 별도의 정보 저장소
정보가 큰 손실 없이 전달될 수 있도록 함
게이트(Gates): 셀 상태의 정보를 제어하는 장치
망각 게이트(Forget Gate, Ft): 과거 정보(이전 셀 상태 Ct-1)를 얼마나 잊을지 결정
입력 게이트(Input Gate, It): 현재 입력으로 들어온 새로운 정보를 얼마나 저장할지 결정
출력 게이트(Output Gate, Ot): 현재 셀 상태의 정보를 얼마나 출력(다음 은닉 상태 Ht로)할지 결정
양방향 LSTM (Bidirectional LSTM)
데이터를 순방향과 역방향으로 동시에 처리하여 문장의 양쪽 문맥을 모두 학습하는 기법
GRU(Gated Recurrent Unit)
LSTM의 구조를 더 단순화한 모델
셀 상태와 은닉 상태를 하나의 은닉 상태로 통합하고 게이트를 리셋과 업데이트 2개만 사용
리셋 게이트 (Reset Gate): 과거의 은닉 상태를 얼마나 무시할지(잊을지) 결정
업데이트 게이트 (Update Gate): 과거의 은닉 상태와 현재 계산된 정보를 얼마나 조합하여 다음 은닉 상태($h_t$)를 만들지 결정
언어 모델링(Language Modeling, LM)
단어 시퀀스(문장)에 확률을 할당하는 모델
주어진 단어들 다음에 어떤 단어가 올 확률을 예측
통계 기반(n-gram): 연속된 n개의 단어 묶음을 기반으로 확률을 계산. 문맥이 n개로 제한되고 데이터 희소성 문제
신경망 기반(NNLM, RNN 등): 단어를 임베딩 벡터로 변환하고 신경망을 이용해 문맥을 학습
퍼플렉서티(Perplexity, PP)
언어 모델이 얼마나 '헷갈려하는지'를 나타내는 지표
값이 낮을수록 성능이 좋음
신경망 기계 번역(NMT)
seq2seq (Sequence to Sequence)
하나의 시퀀스를 다른 시퀀스로 변환하는 구조 → 기계 번역, 텍스트 요약, 챗봇 등
인코더(Encoder): 문장 전체의 의미를 하나의 고정된 크기의 Context Vector로 압축
디코더 (Decoder): 인코더가 전달한 컨텍스트 벡터를 입력받아 한 단어씩 순차적으로 생성하여 출력
압축 과정에서 정보 손실을 무시할 수 없는 문제점이 있음 → Attention 기법과 Transformer의 등장으로 이어짐
Teacher Forcing
디코더가 다음 단어를 예측할 때 모델 자신이 이전에 예측한 단어 대신 실제 정답 단어를 입력으로 넣어주는 기법
"I am a" 다음에 "student"를 예측해야 할 때,
모델이 실수로 "I am a teacher"라고 예측했더라도
다음 단어를 예측하기 위한 입력값으로 정답인 student를 넣어줌
학습 초기 불안정성을 줄이고 수렴 속도를 향상시켜주지만 노출 편향(Exposure Bias) 문제가 있음