LLM을 사용하여 사내가이드 슬랙봇 만드는 방법을 정리합니다.
시작하며
1편 뒤에 바로 2편을 쓴다고 다짐했었지만 게으름과 귀찮음에 사로잡혀 이제서야 글을 써본다. 마음속의 내 모습은 갓생 사는 직장인인데, 현생에서는 그냥 일에 치이고 스트레스가 통제되지 않는 망나니에 가깝다. 언제나 스스로에게 실망하지만 포기하지 않고 끝까지 글을 써본다. 경험들을 하나씩 저장해두면 나중에 큰 자산이 될 것이라고 일단 믿어본다.
설계도면
이번에 만든 슬랙봇 설계도는 다음과 같다.
- 일배치로 원천 데이터를 업데이트 한다.
- 원천 데이터가 업데이트 된다면 Vector DB도 업데이트 한다.
- Vector DB에는 문서 요약본과 문장(청크)이 구성되어 있다.
- 유사도 분석은 문장 단위로 진행된다.
- 질문에 가장 유사한 문장이 속한 문서 요약본을 GPT에 전달한다.
- 질문과 문서 요약본을 보내 정리된 답을 받은 후 슬랙봇을 통해 전달한다.
- 슬랙봇은
@
태그를 하거나 특정 채널에 글이 올라오면 쓰레드로 답변을 달아준다.
준비물
슬랙봇을 만들기 위해서는 여러가지 준비물이 필요하다.
- VectorDB를 만들 원천 데이터
- Slack 앱
- OpenAi 토큰
- 응답을 반환해줄 서버
- 약간의 돈 (또는 회사의 지원)
개발 순서
1. 원천 데이터를 수집 및 가공하기
우리 회사에서는 주요한 가이드를 모두 컨플루언스에 저장하고 관리하고 있었다. 따라서 개인 계정에서 API token을 하나 발급받아서 크롤링을 하면 된다. 크롤링은 빠르게 python의 requests 라이브러리를 사용하여 개발할 수 있다.
나의 경우 먼저 가져오려는 space 이름으로 space id
를 가져온 후 해당 스페이스에 있는 모든 page
데이터를 가져왔다. page
데이터의 경우 사용자가 추가하거나 업데이트 할 수 있기에 일배치로 실행하였다.
1 | # space 이름으로 아이디 가져오기 |
API 관련된 자세한 설명은 컨플루언스 공식 페이지에서 확인할 수 있다. token의 경우 요청을 날릴 때 HTTPBasicAuth
를 사용하여 랩핑한 후 같이 보냈다. 아래 코드는 공식 API 가이드에 있는 예제 코드이다. 공식 페이지에 언어별로 잘 정리되어 있으니 참고하기 바란다.
1 | import requests |
2. Vector DB 생성하기
원천 데이터가 준비 완료되었다면 Vector DB를 생성한다. Vector DB를 생성하는 구조는 다음과 같다.
- 원천 데이터 가져오기
- 요약본 만들기
- 임베딩하기
- S3에 저장하기
요약본을 생성하는 것은 랭체인(langchain)의 TokenTextSplitter
를 사용하여 Document
에 맞게 청크를 생성한 후 각각의 청크를 요약하고 이를 합치는 구조로 만들었다. 특히 load_summarize_chain
를 사용할 때 chain_type을 map_reduce
로 쓰지 않았는데 이는 테스트해보니 너무 많은 부분이 요약되었기 때문이다. 이 요약본의 목적은 이후 GPT에게 답변을 생성할 때 보내지는 데이터이다.
그 다음 UnstructuredHTMLLoader
를 사용하여 HTML 구조의 데이터를 문장 별로 쪼개 train set을 만들었다. 이 문장들을 임베딩하여 이후 유사도 분석에서 사용한다. 임베딩의 경우 OpenAI의 임베더를 사용하였으며, 모델은 text-embedding-3-large
를 사용하였다. OpenAI의 임베더의 경우 한번에 처리할 수 있는 문자의 길이가 8192이기 때문에 한 문장이 이를 넘는다면 쪼개서 작업해야 한다.
이렇게 하여 생성된 Vector DB의 컬럼은 다음과 같다.
1 | ["id", "version", "url", "title", "train", "summary", "content"] |
id
나 version
을 추가한 이유는 업데이트 로직을 위함이다. 사용자가 해당 컨플루언스 페이지를 업데이트하면 API로 데이터를 가져올 때 Version 매트릭이 +1 된다. 이를 이용하여 Vector DB도 변동값이 있는 페이지만 다시 생성하는 것이다. 이렇게 작업한 이유는 간단하다. 임베딩에는 돈이 들어간다. 이미 임베딩이 된 페이지를 다시 진행할 필요는 없기 때문이다.
url
의 경우에는 슬랙봇이 참고한 컨플루언스 URL을 같이 첨부해서 주기 때문이다. 이렇게 기획한 이유는 할루시네이션 이슈도 있고 더 자세한 정보를 알고 싶은 사용자에게 더 많은 데이터를 주기 위함도 있다.
참고로 요약 요청 시 보내는 프롬프트의 경우 매우 간단하게 설정하였다.
1 | 너는 긴 문서를 정리하는 요약 도우미야. |
이렇게 만들어진 Vector DB는 S3에 parquet 파일로 저장해둔다.
3. 유사도 분석 로직 개발하기
이제 요리를 위한 재료 준비는 모두 끝났다. 조리 방법만 잘 정하면 된다. Vector DB를 다룰 수 있는 다양한 라이브러리들이 존재하지만 나는 FAISS를 사용했다. 특히 ChromaDB와 FAISS 중 무엇을 쓸지 고민했었는데 FAISS를 사용한 이유로는 다음과 같다.
- 인덱싱이 빠르다.
- 코드를 유연하게 개발할 수 있다.
- CPU 모드를 지원한다.
- 확장성이 좋다.
나는 이 분야의 전문가가 아니라서 최대한 고수준 API나 기능들을 제공해주는 라이브러리를 쓰고 싶었는데 ChromaDB는 너무 무겁고 생각보다 사용성이 편리하지 않았다. 특히 지금처럼 간단한 인덱싱 작업을 할꺼라면 차라리 FAISS로 개발하는게 훨씬 편한 것 같았다. 이렇게 사용할 라이브러리는 대충 정해졌다. 빠르게 개발을 시작한다.
유사도 측정 알고리즘은 간단하게 내적을 이용하였다. 문장이 길지 않거니와 질문에 대한 정확한 답을 가져오는게 가장 우선시되었기 때문이다. 자세한 내용은 해당 문서에서 확인할 수 있다.
1 | index = faiss.IndexIDMap(faiss.IndexFlatIP(encoded_data.shape[1])) |
4. 응답을 생성할 백엔드 서버 구축하기
RAG 아키텍처에 맞춰서 질문과 함께 요청이 들어오면 다음과 같이 진행한다.
- 질문을 임베딩하고 메모리에 올라가 있는 Vector DB에서 유사도 분석을 진행한다.
- 유사도가 가장 높은 20개의 요약본을 GPT에게 보내 최대 3개까지 질문에 대한 답이 있는 문서 ID를 달라고 요청한다.
- GPT가 뽑아준 문서 ID의 원본을 다시 GPT에게 보내 사용자에게 반환될 응답을 생성한다.
- 슬랙 API로 응답을 보낸다.
이는 간단한 fastapi 앱으로 개발하였다. 백엔드 서버의 경우 데이터팀이 구축 및 관리하고 있는 인프라 클러스터가 있어서 그 위에 살짝 올려두었으며, 매일 Vector DB가 업데이트 되면 다시 parquet 파일을 읽도록 설정해두었다. 이렇게 하면 Vector DB 사이즈가 작은 경우에는 따로 복잡한 미들웨어를 추가할 필요 없이 사용이 가능하다.
5. 슬랙 앱 생성하기
마지막으로 직접적으로 사용자에게 제공할 인터페이스를 만든다. 슬랙 앱을 사용하는 방법은 slack 홈페이지에도 잘 나와있다.
슬랙의 앱 디렉토리로 가서 새로 앱을 생성한 후 설정에 들어가서 몇 가지 조작만 해주면 된다.
나의 경우 Event Subscription 기능을 사용하여 특정 이벤트가 발생한 경우 백엔드 서버로 요청이 올 수 있도록 설정하였다.
Event Subscription 기능에 대해서는 공식 문서에서 자세히 확인할 수 있다.
개발 후기
처음 시작했을 때는 큰 기대 없이 호기심을 해결할 겸 공부할 겸 그렇게 겸사겸사 시작했었다. 하지만 생각보다 좋은 성능을 보여줬고, 사내에서도 반응이 꽤나 좋았다. 가장 잊어버리기 쉬운 와이파이 비밀번호나 프린터 사용법, 근무 규칙 등등의 질문들을 잘 답변해주었고 때로는 특정 업무의 담당자가 누구인지도 잘 설명해주었다. 아직 부족한 부분들도 보이지만 프로토 타입치고는 성능이 좋았다. 다음에는 시간되면 해당 슬랙봇의 성능을 높이기 위한 시도들도 해봐야겠다.