본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 27. 03:35

진정으로 기억하는 Slack 봇 만들기: slacktag-oss

요약

Mem0의 관리형 메모리 레이어를 활용하여 벡터 데이터베이스 없이도 지속적인 의미론적 메모리를 갖춘 오픈 소스 Slack 봇 구축 방법을 소개합니다. 기존 롤링 윈도우 방식의 한계를 극복하고 대화의 연속성을 유지하는 아키텍처를 제안합니다.

핵심 포인트

  • Mem0를 사용하여 별도의 인프라 없이 지속적인 의미론적 메모리 구현
  • 채널, 스레드, DM 단위의 세밀한 메모리 범위(scope) 설정 가능
  • LangChain을 통한 다양한 LLM 엔드포인트 추상화 및 호환성 확보
  • 상태가 없는(stateless) 봇 프로세스로 자유로운 재시작 및 확장 가능

어떠한 LLM과 Mem0의 관리형 메모리 레이어(managed memory layer)를 사용하여 벡터 데이터베이스 없이도 지속적인 의미론적 메모리(semantic memory)를 갖춘 오픈 소스 Slack 어시스턴트를 구축한 방법입니다.

Slack 봇과 메모리의 문제점

대부분의 AI Slack 봇은 금붕어 같은 기억력을 가지고 있습니다. 모든 대화는 처음부터 다시 시작됩니다. 스프린트 목표에 대해 물어보면 훌륭한 답변을 내놓지만, 3일 후에 후속 질문을 하면 당신이 무엇에 대해 말하고 있는지 전혀 알지 못합니다. 결국 당신은 끊임없이 문맥(context)을 다시 설명해야 합니다.

이에 대한 상용 솔루션은 Claude Tag입니다. 이는 진정한 대화의 연속성을 유지하는 Slack 통합 도구입니다. 하지만 이는 특정 제공업체에 종속되어 있으며 오픈 소스가 아닙니다.

slacktag-oss는 그 경험을 재현하려는 우리의 시도입니다. 완전히 노트북에서 실행되는 모델을 포함하여 어떤 LLM과도 작동하는, 실제적이고 의미론적이며 지속적인 메모리를 가진 Slack 봇입니다.

내가 만든 것

다음 기능을 갖춘 Python Slack 봇입니다:

  • 로컬 개발을 위한 Socket Mode (공용 URL 불필요), 프로덕션용 HTTP 지원
  • 모든 OpenAI 호환 엔드포인트에 대해 LLM 호출을 추상화하기 위한 LangChain
  • 의미론적 메모리(semantic memory)를 위한 Mem0 managed cloud — Qdrant, Pinecone, 또는 실행할 인프라가 필요 없음
  • 세 가지 메모리 범위(scope): 채널별(per-channel), 스레드별(per-thread), DM별(per-DM)
  • 내장된 !clear!memory 명령어
  • 포크(fork)하여 기반으로 구축할 수 있는 깔끔하고 확장 가능한 아키텍처

아키텍처

코드로 들어가기 전에, 전체 요청 라이프사이클(request lifecycle)은 다음과 같습니다:

┌─────────────────────────────────────────────────────────────┐
│                         Slack                               │
│  @mention in channel  ──┐                                   │
...

핵심 설계 결정: Mem0가 유일한 상태 유지 종속성(stateful dependency)입니다. 관리할 데이터베이스도, Redis도, Qdrant도 없습니다. 봇 프로세스 자체는 상태가 없습니다(stateless) — 메모리를 전혀 잃지 않고 자유롭게 재시작할 수 있습니다.

프로젝트 구조

slacktag-oss/
├── main.py
├── config/settings.py       ← .env로부터의 Pydantic settings
...

메모리 레이어: 왜 Mem0인가

봇 메모리에 대한 일반적인 접근 방식은 롤링 윈도우 (rolling window) 방식입니다. 즉, 프롬프트에 마지막 N개의 메시지를 유지하는 것입니다. 이 방식은 금방 한계에 부딪힙니다. 컨텍스트 (context)가 오래되어 쓸모없어지고, 중요한 정보가 윈도우 밖으로 밀려나며, 토큰 비용이 선형적으로 증가하기 때문입니다.

Mem0는 다른 접근 방식을 취합니다. 대화를 저장할 때 다음과 같은 과정을 거칩니다:

  1. 추출 (extraction) 단계를 실행하여 사실(facts), 엔티티 (entities), 선호도 (preferences)를 뽑아냅니다.
  2. 이미 저장된 내용과 비교하여 중복을 제거 (deduplicate)합니다.
  3. 의미론적 검색 (semantic retrieval)을 위해 벡터 임베딩 (vector embeddings)으로 인덱싱합니다.

나중에 질문을 던지면, 단순히 가장 최근의 메시지가 아니라 가장 관련 있는 (relevant) 과거의 기억들을 돌려받게 됩니다. 3주 전에 언급된 사용자의 선호도는 그 사이에 수백 개의 메시지가 오갔더라도 관련 상황이 되면 다시 나타납니다.

클라이언트 설정하기

Mem0의 관리형 클라우드 (managed cloud)를 사용하기 때문에, 전체 백엔드는 단 세 줄로 끝납니다:

# memory/mem0_store.py
from mem0 import MemoryClient
from config.settings import settings
...

벡터 데이터베이스 (vector database) 설정도 필요 없습니다. 선택해야 할 임베딩 모델 (embedding model)도 없습니다. 관리해야 할 컬렉션 (collection) 이름도 없습니다.

메모리 스코핑 (Memory scoping)

Slack 봇을 위한 핵심 통찰은 서로 다른 대화에는 서로 다른 메모리 경계 (memory boundaries)가 필요하다는 점입니다:

# channel_memory.py
def scope_id(self, channel_id: str, thread_ts: str = None) -> str:
    if thread_ts:
...

Mem0는 이 문자열을 user_id로 사용합니다. channel:C12345 아래에 저장된 모든 것은 해당 채널의 모든 사용자가 공유합니다. dm:U67890 아래에 있는 것은 비공개입니다. 스레드 (thread) 메모리는 완전히 격리되어 있어, 스레드 내에서의 디버깅 세션이 메인 채널의 메모리를 오염시키지 않습니다.

BaseMemory 인터페이스

ChannelMemoryDMMemory는 모두 동일한 4개 메서드 인터페이스를 구현합니다:

# memory/base.py
class BaseMemory(ABC):
    @abstractmethod
...

덕분에 나중에 백엔드를 교체하기가 매우 쉽습니다. BaseMemory를 구현하고, 팩토리 (factory)를 업데이트하면 끝입니다.

LLM 레이어: LangChain + 모든 OpenAI 호환 엔드포인트

# llm/client.py
from langchain_openai import ChatOpenAI
from config.settings import settings
...

base_url은 제공자(provider) 간에 바뀌는 유일한 요소입니다. Ollama, LM Studio, OpenAI, Grox, Together AI — 다른 코드를 전혀 건드리지 않고도 모두 작동합니다.

핸들러(Handler): 메모리가 LLM과 만나는 곳

handler.py는 봇의 핵심입니다. 모든 요청에 대해 다음과 같은 작업을 수행합니다:

  1. 내장된 명령어가 있는지 확인
  2. 의미론적으로 관련 있는 과거 컨텍스트(context)를 Mem0에서 검색
  3. 대화의 연속성을 위해 최근 이력(history)을 가져옴
  4. LangChain 메시지 리스트를 구축
  5. LLM을 호출
  6. 대화 내용을 다시 Mem0에 저장
# core/handler.py (단순화 버전)
def handle_channel_mention(channel_id, user_id, text, thread_ts=None):
    scope = channel_memory.scope_id(channel_id, thread_ts)
...

프롬프트(Prompt) 구축하기

LLM에 전달되는 메시지 리스트는 특정 순서로 조립됩니다:

def build_messages(system_prompt, relevant_memories, recent_history, user_input):
    messages = [SystemMessage(content=system_prompt)]

...

두 개의 시스템 메시지(two-system-message) 패턴은 봇의 페르소나(persona)와 지침을 주입된 메모리 컨텍스트(memory context)와 분리하여 유지합니다. 이는 모델이 추론하기에 더 깔끔한 구조를 제공합니다.

Slack 연결하기

slack-bolt를 사용하면 이벤트 핸들링(event handling)을 깔끔하게 처리할 수 있습니다:

# core/bot.py
app = App(token=settings.SLACK_BOT_TOKEN, signing_secret=settings.SLACK_SIGNING_SECRET)

...

router.py는 관련 필드를 추출하고 적절한 핸들러를 호출합니다:

# core/router.py
def route_mention(event, say):
    channel_id = event.get("channel")
...

답변은 항상 동일한 스레드(thread)로 전송됩니다. 만약 멘션이 스레드 내에서 이루어졌다면, 봇도 해당 스레드에 머뭅니다.

Pydantic Settings를 이용한 설정

모든 설정은 검증(validation) 기능과 함께 한 곳에서 관리됩니다:

# config/settings.py
class Settings(BaseSettings):
    SLACK_BOT_TOKEN: str
...

필수 필드(Slack 토큰, Mem0 키 등)가 누락된 경우 시작 시 ValidationError를 발생시킵니다. 이는 이벤트 처리가 시작되기 전에 빠르게 오류를 발견(fail fast)할 수 있게 합니다.

로컬에서 실행하기

# 의존성 설치
pip install -r requirements.txt

...

그게 전부입니다. Docker, Qdrant, ngrok도 필요 없습니다. 봇을 채널에 초대하고 @mention을 하면, 첫 번째 메시지부터 기억을 구축하기 시작합니다.

"의미론적 기억 (semantic memory)"이 실제로 어떻게 작동하는가

현실적인 예시를 들어보겠습니다. 1일 차:

사용자: @slacktag 우리 API 속도 제한 (rate limit)은 테넌트당 분당 100회 요청(req/min)입니다. 용량 계획 (capacity planning) 시 이 점을 유의해 주세요.
봇: 알겠습니다. 용량 관련 논의 시 해당 내용을 반영하겠습니다.

3일 차 (채널에 수백 개의 메시지가 쌓인 후):

사용자: @slacktag 곧 5개의 새로운 엔터프라이즈 테넌트를 온보딩할 예정입니다. 우려되는 사항이 있을까요?
봇: 고려해야 할 몇 가지 사항이 있습니다. 현재 테넌트당 분당 100회 요청인 API 속도 제한을 고려할 때, 5개의 새로운 엔터프라이즈 테넌트는 피크 부하 (peak load)를 크게 증가시킬 수 있습니다. 온보딩 전에 속도 제한 전략을 검토하는 것이 좋습니다...

Mem0는 1일 차의 속도 제한 정보를 찾아냈습니다. 비록 최근 메시지 창(message window) 어디에도 해당 내용이 없었음에도 불구하고, 용량 관련 질문과 의미론적으로 관련이 있었기 때문입니다.

배포 경로

프로덕션 환경을 위해서는 SocketModeHandler를 표준 HTTP 어댑터로 교체하세요:

# Flask 사용 시
from slack_bolt.adapter.flask import SlackRequestHandler
from flask import Flask, request
...

Slack 앱의 요청 URL (Request URL)을 https://your-domain/slack/events로 지정하고, 어디든 (Fly.io, Railway, Cloud Run — 모두 가능) 배포하면 끝입니다. 서버에는 상태 (state)가 저장되지 않으며, Mem0가 모든 것을 보유합니다.

향후 계획 (v2 아이디어)

이 기능을 훨씬 더 강력하게 만들 수 있는 몇 가지 확장 기능입니다:

플러그형 도구 (Pluggable tools)tools/registry.py는 LangChain 도구 통합을 위한 스텁 (stub)이 마련되어 있습니다. 웹 검색 (Tavily, Brave Search)이나 코드 실행 샌드박스 (code execution sandbox)를 추가하면 이 봇은 유능한 에이전트 (agent)로 변모할 것입니다.

Mem0 그래프 메모리 (graph memory) — Mem0는 대화 전반에 걸쳐 엔티티 (entity) 간의 관계를 추적하는 그래프 모드를 지원합니다. 누가 어떤 팀에 속해 있는지, 어떤 프로젝트가 진행 중인지 등을 매핑하고 해당 컨텍스트 (context)를 자동으로 불러올 수 있습니다.

채널별 LLM 설정 (Per-channel LLM config) — 관리자가 채널마다 서로 다른 모델을 설정할 수 있습니다 (예: #architecture 채널에는 강력한 모델을, #random 채널에는 빠르고 저렴한 모델을 설정).

리액션 트리거 (Reaction triggers) — 🧠 이모지로 리액션을 하면 메시지를 메모리에 명시적으로 추가하고, 🗑️ 이모지로 리액션을 하면 사실(fact)을 삭제합니다. 순수 자동 추출 (auto-extraction) 방식보다 훨씬 더 제어하기 쉽습니다.

!summarizemem0.get_all()을 호출하여 LLM이 해당 채널에 대해 알고 있는 모든 내용에 대한 읽기 쉬운 요약을 생성하도록 요청합니다.

참여하기

코드베이스는 의도적으로 작게 유지됩니다. handler.py는 약 100줄 정도이며, 모든 모듈은 한 가지 일만 수행합니다. 기여하고 싶다면 다음을 수행하세요:

git clone https://github.com/harishkotra/slacktag-oss
cd slacktag-oss
python -m venv .venv && source .venv/bin/activate
...

README의 표에서 원하는 기능을 선택하여 구현한 뒤 PR (Pull Request)을 생성하세요. 아키텍처는 복잡하게 얽히지 않고 기능을 추가할 수 있도록 단순함을 유지하도록 설계되었습니다.

링크

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0