서비스를 하는 사람은 항상 고객이 필요하고 있다. 고객은 서비스에 대한 질문을 하고 서비스 담당자는 이에 대한 답변을 한다. 서비스가 커질수록 이에 소모되는 비용이 커진다.
질문에 대한 답을 Bot이 할 수 있게 개발해보자.
준비할 것
- LLM (Large Language Model) : 답변을 생성
- Chat History : 질문에 대한 답변 기록
- VectorDB : Chat History를 저장
- Slack Bot : 질문을 받고 LLM에 요청을 보낸 뒤 답변을 반환
앞으로 만들 Slack Bot은 그림과 같이 아래와 같은 Step을 갖는다. 1. 채팅 히스토리를 벡터화해서 VectorDB에 넣는다. 2. Slack Bot으로 들어온 질문을 벡터화한다. 3. 유사한 히스토리를 VectorDB로 부터 찾는다. 4. 찾은 대화로부터 프롬프트를 생성해서 LLM에 질문한다.
준비물 1. LLM
가장 쉽게 사용할 수 있는 LLM은 OpenAI의 API를 사용하는 것이다. 하지만, 이는 보안적으로 안전하지 않다고 판단되는 경우가 많은데 찾아보니 Enterprise privacy에 아래와 같이 API를 사용하는 경우 이 데이터를 학습에 사용하지 않는다고 설명되어 있다.
We do not train on your business data (data from ChatGPT Team, ChatGPT Enterprise, or our API Platform)
Bedrock 혹은 Gemini를 사용하는것도 좋은 거 같다.
여기선 gpt-4o를 사용하고자 한다.
from langchain.chat_models import ChatOpenAI
llm = ChatOpenAI(model_name="gpt-4o")
llm.predict("안녕 너 이름이 뭐야?")
# '안녕하세요! 저는 인공지능 어시스턴트입니다. 당신에게 어떻게 도와드릴 수 있을까요?'
이제 LLM은 준비되었다.
준비물 2. Chat History
LLM은 학습하지 않은 정보에 대한 답변을 할 수 없다. 그렇기에 Chat History를 함께 전달해서 특정 도메인에 대한 답변을 잘 할 수 있게 한다. (RAG)
어떻게든 아래 양식을 갖는 양질의 데이터를 많이 가지고 있으면 된다.
- 질문 :
- 답변 :
Slack의 API를 사용해서 특정 채널의 메시지를 수집하고자 한다.
- slack workspace 생성
- 슬랙 앱 생성 및 권한 추가
- 채널에 앱 추가
- 메시지 가져오기
슬랙앱은https://api.slack.com/apps 에서 생성할 수 있다. 새로 생성한 Workspace에 앱을 생성했다.
해당 앱에 Permission에 아래 항목들에 대해 추가해주자.
- channels:history
- groups:history
- im:history
- mpim:history
그리고 해당 앱을 Workspace에 설치하고 수집하고자 하는 채널에 해당 App을 초대하자.
이제 Slack API를 통해 채널의 메시지를 수집하기 위한 준비가 끝낫다.
import requests
import json
import time
# 슬랙 API 토큰
SLACK_API_TOKEN = 'xoxb-your-slack-bot-token' # Slack App에서 확인 가능
# 채널 ID
CHANNEL_ID = 'C12345678' # 채널 URL에서 확인 가능
# 슬랙 API URL
history_url = 'https://slack.com/api/conversations.history'
replies_url = 'https://slack.com/api/conversations.replies'
# API 요청에 필요한 헤더
headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {SLACK_API_TOKEN}'
}
# 메시지를 저장할 리스트
all_messages = []
# 슬랙 채널의 모든 메시지를 가져오는 함수
def fetch_messages(channel_id, cursor=None):
params = {
'channel': channel_id,
'limit': 200
}
if cursor:
params['cursor'] = cursor
response = requests.get(history_url, headers=headers, params=params)
data = response.json()
if not data['ok']:
raise Exception(f"Error fetching messages: {data['error']}")
messages = data['messages']
for message in messages:
all_messages.append(message)
# 스레드 메시지를 가져오는 부분
if 'thread_ts' in message:
fetch_thread_messages(channel_id, message['thread_ts'])
if data.get('has_more'):
time.sleep(1) # API rate limiting을 피하기 위해 잠시 대기
fetch_messages(channel_id, data['response_metadata']['next_cursor'])
# 스레드 메시지를 가져오는 함수
def fetch_thread_messages(channel_id, thread_ts):
params = {
'channel': channel_id,
'ts': thread_ts
}
response = requests.get(replies_url, headers=headers, params=params)
data = response.json()
if not data['ok']:
raise Exception(f"Error fetching thread messages: {data['error']}")
thread_messages = data['messages']
all_messages.extend(thread_messages)
# 메시지 가져오기
fetch_messages(CHANNEL_ID)
# 가져온 메시지를 파일에 저장 (JSON 형식)
with open('slack_channel_messages_with_threads.json', 'w', encoding='utf-8') as file:
json.dump(all_messages, file, ensure_ascii=False, indent=4)
실행하면 파일이 하나 생성되고 메시지가 잘 가져와진 것을 확인할 수 있다.
이제 중요한 건 아래와 같은 양식으로 만들어야 한다는 점이다.
질문: 수완은 어디에 살아?
답변: 지금 서울 강동구에 살고있어.
import json
# 저장된 메시지 불러오기
with open('slack_channel_messages_with_threads.json', 'r', encoding='utf-8') as file:
slack_data = json.load(file)
# 질문과 답변 추출
qa_pairs = []
thread_messages = {}
# 스레드 메시지를 수집
for message in slack_data:
if "subtype" in message and message["subtype"] == "channel_join":
continue
if "thread_ts" in message and message["thread_ts"] != message["ts"]:
if message["thread_ts"] not in thread_messages:
thread_messages[message["thread_ts"]] = []
thread_messages[message["thread_ts"]].append(message)
else:
if message["ts"] not in thread_messages:
thread_messages[message["ts"]] = []
thread_messages[message["ts"]].append(message)
# 스레드를 질문과 답변 형식으로 변환
for thread_ts, messages in thread_messages.items():
if len(messages) > 1:
question = messages[0]["text"]
answers = [message["text"] for message in messages[1:] if message["text"] != question]
for answer in answers:
qa_pairs.append({"질문": question, "답변": answer})
# 결과 출력
for pair in qa_pairs:
print(f"질문: {pair['질문']}")
print(f"답변: {pair['답변']}\n")
# QA 데아터 저장
with open('qa_data.json', 'w', encoding='utf-8') as f:
json.dump(qa_pairs, f, ensure_ascii=False, indent=4)
위와 같이 출력되었고 해당 내용이 qa_data.json 파일에 저장되었다.
준비물 3. Vector DB
VectorDB는 문서 임베딩을 저장하고 검색하기 위한 DB이다. RAG의 핵심이라 할 수 있다.
OpenAIEmbeddings을 사용해서 질문, 답변 데이터를 Chroma DB에 넣는다. 그렇게 만들어진 vector store를 as_retriever로 변환해준 뒤 chain에 연결해주면 된다.
import json
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_openai import ChatOpenAI
from langchain.docstore.document import Document
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
from langchain import hub
# JSON 파일에서 질문과 답변 데이터 로드
with open('qa_data.json', 'r', encoding='utf-8') as f:
qa_pairs = json.load(f)
# 질문과 답변 데이터를 LangChain 문서 형식으로 변환
documents = []
for pair in qa_pairs:
documents.append(Document(page_content=pair['답변'], metadata={'source': pair['질문']}))
# 임베딩 및 벡터 스토어 설정
embeddings = OpenAIEmbeddings()
vector_store = Chroma.from_documents(documents, embeddings)
# OpenAI LLM 설정
llm = ChatOpenAI(model_name='gpt-4')
# 프롬프트 템플릿 가져오기
prompt = hub.pull("rlm/rag-prompt")
# RAG 체인 설정
rag_chain = (
{"context": vector_store.as_retriever(), "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
# 질문 예시
query = "수완은 어디에 살고 있나요?"
# 질문에 대한 답변 생성
result = rag_chain.invoke(query)
# 결과 출력
print("질문:", query)
print("답변:", result)
준비물 4. Slack Bot
Slack Bolt를 활용한 Python Chatbot 가이드 를 참고해서 Slack Bot을 설정하고 코드를 작성했습니다.
슬랙 봇을 멘션하고 질문을 하면 LLM을 통해 답변을 생성하고 질문을 답변에 대한 스레드에서 하도록 했다.
import os
import json
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_openai import ChatOpenAI
from langchain.docstore.document import Document
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
from langchain import hub
# JSON 파일에서 질문과 답변 데이터 로드
with open('qa_data.json', 'r', encoding='utf-8') as f:
qa_pairs = json.load(f)
# 질문과 답변 데이터를 LangChain 문서 형식으로 변환
documents = []
for pair in qa_pairs:
documents.append(Document(page_content=pair['답변'], metadata={'source': pair['질문']}))
# 임베딩 및 벡터 스토어 설정
embeddings = OpenAIEmbeddings()
vector_store = Chroma.from_documents(documents, embeddings)
# OpenAI LLM 설정
llm = ChatOpenAI(model_name='gpt-4')
# 프롬프트 템플릿 가져오기
prompt = hub.pull("rlm/rag-prompt")
# RAG 체인 설정
rag_chain = (
{"context": vector_store.as_retriever(), "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
# 슬랙 앱 초기화
app = App(token=os.environ.get("SLACK_BOT_TOKEN"))
# 메시지 이벤트 핸들러
@app.event("app_mention")
def handle_message_events(body, say, client):
event = body.get("event", {})
text = event.get("text", "")
user = event.get("user", "")
channel = event.get("channel", "")
thread_ts = event.get("ts") # 이벤트 타임스탬프를 스레드 타임스탬프로 사용
if user and text and channel:
# RAG 체인을 사용하여 답변 생성
answer = rag_chain.invoke(text)
# 슬랙 채널의 스레드에 답변 게시
client.chat_postMessage(
channel=channel,
text=f"<@{user}> {answer}",
thread_ts=thread_ts
)
# 슬랙 봇 실행
if __name__ == "__main__":
handler = SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"])
handler.start()