서버리스로 Slack AI Bot 구축하기

Seongwoo Choi
12 min readJan 21, 2024

--

2022년 11월 ChatGPT의 출현 이후 1년이 조금 지난 현재, 다양한 산업군과 기업에서 LLM을 통해 QA, 문서 요약, 언어 번역 등 다양한 작업을 수행하도록 하는 어시스턴트를 개발하여 사용하고 있다. LangChain과 같은 오픈소스 도구의 등장으로 이러한 개발에 대한 복잡성이 낮아지고 접근성이 향상되며 개인도 이러한 AI 어시스턴트를 개발하는 경우를 자주 볼 수 있게 되었다.
직접 개발한 어시스턴트를 로컬에서 혼자 사용할 수도 있겠지만, 회사의 동료와 같이 사용하여 업무 효율을 보다 높일 수도 있을 것이다. 오늘의 포스팅에서는 글로벌에서 가장 많이 사용하는 업무용 메신저인 Slack에서 AI 봇을 연동하는 방법에 대해 소개하려 한다. 시나리오는 스픽과 같은 영어 문장 교정의 예시로 구성해 보았다.

Architecture

Architecture for Slack AI bot

LLM을 구동하기 위한 모든 환경은 AWS에서 서버리스로 구성하여 비용 효율적으로 사용할 수 있다. AWS Lambda를 호출하는 Function URL과 연동된 Slack의 slash command(e.g. /command)를 통해서 AWS로 진입을 시작한다. Slack의 Slash command는 요청이 전송된 이후 3초 이내에 응답을 수신해야 하고, 그렇지 않으면 오류가 표시되는 제한이 있다. LLM이 처리하는 쿼리에 따라 3초보다 길게 소요되는 작업이 있을 수 있다.
이를 위해 중간에 큐 역할을 하는 SQS를 두고, 가장 먼저 작동하는 Lambda 함수가 해당 SQS로 메시지를 송신하도록 구성한다. 두 번째 Lambda 함수는 SQS 큐의 메시지 인입 이벤트를 트리거로 작동하며, Amazon Bedrock 내에 통합된 한글을 가장 잘하는 현존 모델 중 하나인 Anthropic의 Claude 2.1 모델이 작업을 처리하도록 한다. 프롬프트에 따라 작업을 수행한 후 chat.postMessage API를 통해 Slack 채널로 메시지를 송신한다.

AWS Configuration

Create an SQS Queue

모든 구성은 Claude 2.1이 사용 가능한 Oregon이나 Virginia 리전에서 진행하기를 권장한다. 먼저 Amazon SQS 콘솔로 진입하여 큐를 생성한다. 유형은 표준으로 구성하며, 이외의 설정은 Default로 설정해도 무방하다. 생성된 이후 [세부 정보]에서 URL를 메모해놓는다.

다음으로 슬랙의 Slash Command와 연결하기 위한 첫 번째 Lambda 함수를 생성한다. Python 3.10 버전으로 구성하였으며, 아래와 같이 코드를 작성했다. QUEUE_URL은 메모한 SQS 큐의 URL로 변경해야 한다. Slack에서 전달받은 값을 디코딩하여 쿼리와 사용자 아이디를 추출하고, SQS로 메시지를 전달하며, 채널에 두 번째 Lambda에서 처리된 메시지가 표시되기 전 사용자를 멘션하는 로직을 구성하였다.

import json
import boto3
import base64
from urllib.parse import unquote

def parse_slack_payload(data):
decoded_data_raw = base64.b64decode(data).decode('utf-8').split('&')
decoded_data_formatted = {}
for item in decoded_data_raw:
key, value = item.split('=')
decoded_data_formatted[unquote(key)] = unquote(value)

return decoded_data_formatted

QUEUE_URL = 'https://sqs.us-west-2.amazonaws.com/{AccountNumber}/{QueueName}'
SQS = boto3.client('sqs')

def lambda_handler(event, context):
body = parse_slack_payload(event["body"])
query = body["text"].replace("+"," ")
user_id = body["user_id"]

SQS.send_message(
QueueUrl=QUEUE_URL,
MessageBody=json.dumps({"query": query})
)

return {
"response_type": "in_channel",
"text": "<@" + user_id + "> 님 안녕하세요!"
}

해당 함수의 [구성] 탭으로 진입하여, 2가지 작업을 수행해야 한다. 먼저 [권한] 탭에서 연결된 IAM Role에 AmazonSQSFullAccess 정책을 할당한다.

Configure Function URL

그리고 [함수 URL] 탭으로 들어가 외부에서 호출할 수 있게 구성한다. Slack에서 IAM 자격 증명을 구성할 수 없으므로, 인증 유형을 NONE으로 설정한다. 별도 인증 과정이 없기 때문에 비즈니스 크리티컬한 로직을 태워서는 안되며, 해당 URL이 외부로 노출되지 않도록 주의해야 한다. 이번 포스팅에서는 영어 교육 목적의 간단한 시나리오를 기획하였으므로 함수 URL로도 충분해 보인다. 만약 보안 구성을 원한다면 Amazon API Gateway를 통해 Lambda 함수를 호출하도록 해야 한다.
생성된 함수 URL을 별도로 메모해놓아야 하며, 이후 Slack Slash Command 설정에서 사용된다.

Add Trigger for Second Lambda Function

두 번째 Lambda 함수를 생성하고 Bedrock 권한과 SQS 연동을 위한 SQS, CloudWatch에 대한 IAM 정책을 미리 연결하고, SQS Queue를 트리거로 연결해 준다. timeout은 3초를 초과할 수 있으니 1분 정도로 세팅해 놓기를 권장한다.
해당 함수는 LangChain을 통해 Bedrock을 호출하며, 영어 문법이 틀렸거나 어색한 영어 문장을 교정해 주는 프롬프트가 작동하도록 코드로 구성하였다. 코드 중 channel_idtoken 값을 자신이 사용하는 슬랙에 맞게 수정해야 한다. Slack 채널 ID는 Slack 채널을 왼쪽 클릭하여 링크 복사를 통해 추출할 수 있으며, token 값은 Slack 설정 파트에서 후술하도록 하겠다.

from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
from langchain.llms.bedrock import Bedrock
import boto3
import urllib
import json


def get_generated_prompt(query, llm):
template = """
Human: You are a competent English teacher. The following sentence was written by a student in your English class.
If the grammar and English expression is incorrect or awkward, it needs to be corrected. Please follow the conditions below:

- Please mark the student's writing as [원문].
- Proofread the student's article and mark it as [교정문].
- Compare each word in [원문] and [교정문], and don't include anything in the [교정 사항 설명] that doesn't change.
- Please explain the corrections in Korean, starting with '교정 사항 설명:' and separating each item with a number.

Input: {question}

Assistant:
"""
prompt = PromptTemplate(
template = template, input_variables = ["question"]
)

question_generator_chain = LLMChain(llm=llm, prompt=prompt, verbose=True)
return question_generator_chain.run({"question": query})

def lambda_handler(event, context):
boto3_bedrock = boto3.client(
service_name='bedrock-runtime',
region_name='us-west-2',
)

query = json.loads(event["Records"][0]["body"])["query"]
llm = Bedrock(
model_id="anthropic.claude-v2:1",
client=boto3_bedrock,
model_kwargs={"max_tokens_to_sample": 2048, "temperature": 0.1, "top_p": 0.9},
region_name='us-west-2'
)
ret = get_generated_prompt(query, llm)

SLACK_URL = "https://slack.com/api/chat.postMessage"
channel_id = "CHANNEL-ID"
data = urllib.parse.urlencode(
(
("token", "token-value"),
("channel", channel_id),
("text", ret)
)
)
data = data.encode("ascii")
request = urllib.request.Request(SLACK_URL, data=data, method="POST")
request.add_header( "Content-Type", "application/x-www-form-urlencoded" )

return {}

Bedrock 호출이 가능한 최신 boto3LangChain 라이브러리를 아래와 같이 첨부한다. Lambda 레이어를 함수에 연결하여 해당 라이브러리들을 가져다 쓸 수 있다.

Slack Configuration

Slack App으로 접속하여, App을 하나 생성한다. 해당 App의 설정 콘솔로 진입하여 slash command를 생성해 준다. [Request URL]에 기 생성한 Lambda 함수 URL을 입력한다.

Install OAuth Token to Workspace

Slack App 콘솔 내 [OAuth & Permissions] 탭으로 진입한다. [Install to Workspace]를 클릭하여 OAuth Tokens을 발급받고, 두 번째 Lambda 함수의 token 값을 해당 값으로 수정해 준다.

Add chat:write scope

동일 탭에서 [Scopes]— [Bot Token Scopes]을 찾아 chat:write 권한을 추가해 준다. 이는 채널에 메시지를 POST할 수 있도록 하는 권한이다.

마지막으로 채널 세부정보 화면의 [통합]에서 해당 App을 추가해주면 모든 사전 작업이 완료된다.

Testing Slack AI Bot

slash command를 통해서 Slack 채널에서 봇이 작동하는 것을 확인할 수 있다. 구성한 로직에 의해 필자의 이름을 한 번 멘션하고, 작성된 프롬프트에 의해 제출한 영어 문장을 교정해 주는 것을 볼 수 있다.

Conclusion

서버리스로 AI 봇을 구축하게 되면, 별도의 서버 관리가 필요 없고, Lambda 함수가 작동하는 잠시의 시간만큼 비용이 과금되기 때문에 비용 면에서 굉장히 효율적이다. 또한 LangChain의 등장으로 프롬프트만 작성하면 LLM이 번역, 요약 등 다양한 다운스트림 작업을 손쉽게 처리하도록 구현 가능하다. 게다가 챗봇 연결을 위한 프론트 화면을 별도로 구성하지 않아도 Slack으로 쉽게 통합할 수 있으니, 나의 복잡한 작업을 대신 처리해 주는 개인 비서로 고용하기에 이보다 더 간편한 조합이 있을 수 없다.
아직 해보지 않았다면, 서버리스 아키텍처를 활용하여 손쉽게 Slack AI 봇 구축을 시도해 보길 추천한다.

--

--