챗봇이 그룹에서 답장하지 않는 문제: AI 버그가 아닌 Slack 구현(plumbing) 버그였다
요약
챗봇이 그룹 채팅에서 오작동하는 문제는 AI 모델이나 프롬프트의 문제가 아닌 Slack 이벤트 처리(plumbing) 구조적 문제일 수 있습니다. 특히, 빠른 응답 시간(약 3초)을 맞추지 못하면 이벤트 생명주기 처리가 깨지고 중복 처리 위험에 노출됩니다.
핵심 포인트
- 챗봇 오작동은 AI 모델 문제가 아닌 Slack 구현 버그일 수 있다.
- Slack은 빠른 HTTP 200 응답(약 3초)을 기대한다.
- 느린 작업은 요청 경로에서 분리하여 백그라운드 워커로 처리해야 한다.
만약 봇이 DM에서는 잘 작동하지만 Slack 채널에서는 이상하게 침묵한다면, 이벤트 처리(event handling)를 의심부터 해보세요.
GPT-5 때문이 아닙니다.
Claude 때문도 아닙니다.
프롬프트 때문도 아닙니다.
저희는 이 문제를 어리석은 방식으로 배우느라 많은 시간을 낭비했습니다.
저희의 봇은 1:1 채팅에서는 훌륭했습니다. Slack DM에서도 컨텍스트를 유지하고, 도구(tool)를 호출하며, 결과를 요약하는 등 전반적으로 유능한 AI 팀원처럼 보였습니다.
그런 다음 똑같은 봇을 바쁜 채널에 투입했습니다.
그것은 유령이 되었습니다.
가끔 답장할 때도 있었고.
가끔 두 번 답장하기도 했습니다.
때로는 백엔드가 성공적으로 완료되었는데 Slack에는 아무것도 표시되지 않았습니다.
때로는 잘못된 스레드에 답변했습니다.
이런 종류의 실패는 매우 짜증납니다. 왜냐하면 가장 먼저 잘못된 계층(layer)을 디버깅하게 만들기 때문입니다.
저희는 모델 탓을 했습니다.
GPT-5를 Claude로 교체해 보았습니다.
프롬프트를 다듬었습니다.
Llama와 Qwen 중 어느 것이 채널에서 더 신뢰할 수 있을지 논쟁했습니다.
하지만 그 어떤 것도 중요하지 않았습니다.
진짜 문제는 우리가 Slack 그룹 채팅을 DM처럼 취급했고, Slack은 절대 그렇게 작동하지 않는다는 점이었습니다.
첫 번째 버그: DM과 멘션(mentions)은 다른 이벤트 유형이다
이것이 제가 어떤 Slack 봇에서 가장 먼저 확인할 것들입니다.
앱으로 오는 다이렉트 메시지(DM)는 `channel_type=
만약 여러분의 봇이 주로 DM(다이렉트 메시지)이나 트래픽이 적은 채널에서 활동한다면, 동기식 핸들러(synchronous handler)는 몇 주 동안 문제없이 보일 수 있습니다.
그러다가 한 사건 채널(incident channel)이 바빠집니다. 다섯 명이 봇을 언급합니다. 하나의 도구 호출(tool call)에 8초가 걸립니다. 하나의 GitHub 요청이 지연됩니다. 재시도(retry)가 들어옵니다. 그리고 갑자기 여러분의 'AI 신뢰성 문제'는 명백히 깨진 이벤트 생명주기 처리(broken event lifecycle handling)일 뿐이라는 것을 알게 됩니다.
Slack은 빠른 HTTP 200 응답을 기대합니다.
실질적인 규칙은 간단합니다:
응답(ack)할 시간이 약 3초밖에 없습니다.
만약 빠르게 응답하지 않으면, Slack이 이벤트를 재시도할 수 있고, 여러분은 이제 중복 처리(duplicate-processing) 영역에 놓이게 됩니다.
위험한 흐름은 다음과 같습니다:
app_mention수신- GPT-5 또는 Claude 호출
- 벡터 DB 쿼리
- GitHub 호출
- Jira 호출
- 모든 내용 요약
- 마지막으로 Slack에 답장
처음 구축할 때는 자연스럽게 느껴집니다.
하지만 이것은 부하가 걸리면 사라져 버리는 봇을 만드는 방식이기도 합니다.
실제로 효과를 본 지루한 해결책
우리는 모든 느린 작업을 요청 경로(request path)에서 분리했습니다.
이는 다음을 의미합니다:
- 즉시 응답(ack immediately)
- 작업 예약(enqueue work)
- 백그라운드 워커(background worker)에서 처리
- 나중에 최종 답변 게시(post the final answer later)
Slack Bolt의 Python으로 구현된 형태는 다음과 같습니다:
from slack_bolt import App
app = App(process_before_response=True)
...
이것이 아이디어이지만, 실제 운영 환경에서는 모든 것을 리스너(listener) 내부에서 처리하기보다 큐(queue)에 넣는 것이 일반적입니다.
좀 더 이런 형태가 좋습니다:
from fastapi import FastAPI, Request, BackgroundTasks
import os
import json
...
중요한 부분은 프레임워크 자체가 아닙니다.
중요한 부분은 분리(split)입니다:
- 웹훅 경로(webhook path): 유효성 검사 + 중복 제거 + 작업 예약
- 워커(worker): 비용이 많이 드는 작업 수행
신뢰할 수 있는 Slack 봇은 AI 애플리케이션보다 이벤트 시스템(event system)입니다.
두 번째 버그: 백엔드가 성공했음에도 Slack이 메시지를 삭제할 수 있음
이것은 더 까다로웠습니다.
우리는 워커가 작업을 완료하고 chat.postMessage를 호출하면 메시지가 표시될 것이라고 가정했습니다.
하지만 이는 바쁜 채널에서는 안전하지 않습니다.
Slack은 chat.postMessage에 대해 채널당 초당 약 1개의 메시지로 속도 제한(rate-limits)을 걸고 있습니다.
채널당입니다.
즉, 봇이 다음과 같이 게시한다고 가정해 봅시다:
- “thinking...”
- “checking GitHub...”
- “checking Datadog...”
- “found 3 issues...”
- “here’s the answer”
그리고 세 사람이 동시에 같은 스레드에서 언급하면, 기본적으로 속도 제한(rate-limit) 장치를 만든 것입니다.
문제는 사용자들은 침묵을 보는데 로그만 여전히 정상으로 보일 수 있다는 점입니다.
우리가 변경한 사항
우리는 아웃바운드 메시지를 자유롭게 작성하는 것처럼 취급하는 것을 멈췄습니다.
새로운 규칙은 다음과 같습니다:
- 즉시 확인 응답(Ack immediately)
- 모든 작업을 대기열화(Queue all work)
- 재시도 중복 제거(Dedupe retries)
- 채널별 아웃바운드 게시물 직렬화(Serialize outbound posts per channel)
- 채널당 초당 약 1개의 메시지로 제한(Throttle to about 1 msg/sec/channel)
- 다섯 가지의 귀여운 업데이트보다 하나의 확실한 답변을 선호
이것이 어떤 프롬프트 조정보다도 신뢰성을 향상시켰습니다.
모델이 더 똑똑해져서가 아닙니다.
플러밍(plumbing) 시스템이 앱과 싸우는 것을 멈췄기 때문입니다.
스레드 상태 관리: 많은 봇들이 조용히 무너지는 지점
DM(Direct Message, 다이렉트 메시지)에서는 상태 관리가 쉽습니다.
하지만 채널에서는 상태가 스레드 형태를 가집니다.
올바른 thread_ts 없이 답장하면, 답변이 잘못된 곳에 도착하거나 채널에서 쓸모없는 노이즈가 됩니다.
많은 팀들이 다음과 같은 작업을 수행합니다:
- 새 메시지 수신
conversations.replies호출- 전체 스레드 재구축
- 이를 모델에 전송
- 영원히 반복
이것은 처음에는 작동합니다.
하지만 비용이 많이 들고, 느리며, 점점 더 취약해집니다.
더 나은 패턴은 자체적인 간결한 스레드 상태를 유지하는 것입니다.
저장할 내용:
channel(채널)- 루트
thread_ts - 정규화된 사용자 턴(normalized user turns)
- 도구 출력(tool outputs)
- 모델 출력(model outputs)
- 간결한 순환 요약(a compact rolling summary)
Slack 기록을 주 메모리 시스템이 아닌 복구용으로 사용하세요.
여기 트레이드오프가 있습니다:
| 접근 방식 | 실제로 발생하는 일 |
|---|---|
| 매 턴 Slack에서 스레드 재구성 | 시작하기는 쉽지만, 부하가 걸리면 더 느리고, 노이즈가 많고, 취약함 |
| 자체 스레드 상태 유지 | 엔지니어링 작업은 약간 더 필요하지만, 장기 실행 에이전트에게 훨씬 더 신뢰적임 |
만약 n8n, Make, Zapier, OpenClaw 또는 자체 워커 스택에서 워크플로우를 실행하고 있다면, 이것은 더욱 중요합니다.
이러한 도구들은 봇을 빠르게 배포하는 것을 용이하게 만듭니다.
또한 트래픽이 급증할 때까지 상태 버그(state bugs)를 숨기기도 쉽게 만듭니다.
당신을 당황시키지 않는 실용적인 이벤트 파이프라인
만약 제가 이것을 처음부터 다시 구축한다면, 이렇게 할 것입니다.
1) 대화 유형별 인그레스 분할(Split ingress by conversation type)
DM과 언급(mentions)은 별도로 처리합니다.
def route_event(event: dict):
event_type = event.get("type")
channel_type = event.get("channel_type")
...
다른 경로는 다음 사항에 대해 다른 로직을 가져야 합니다:
- 권한(permissions)
- 언급 파싱(mention parsing)
- 답장 라우팅(reply routing)
- 스레드 처리(thread handling)
2) 먼저 확인하고, 나중에 생각하기(Ack first, think later)
느린 모든 것은 백그라운드 작업으로 보냅니다.
여기에는 다음이 포함됩니다:
- GPT-5 호출(calls)
- Claude 호출(calls)
- 임베딩/검색(embeddings/search)
- GitHub 조회(lookups)
- Jira 조회(lookups)
- Notion 읽기(reads)
- Google Drive 스캔(scans)
- 내부 도구 호출(internal tool calls)
당신의 웹훅은 지루해야 합니다.
지루함이 좋습니다.
3) 재시도(retries)를 정상으로 취급하기
Slack의 재시도는 예외 케이스가 아닙니다.
event_id를 저장하고 처리를 멱등성(idempotent) 있게 만드세요.
유사 코드:
def process_event(event_id: str, event: dict):
if already_processed(event_id):
return
...
이것을 건너뛰면, 중복된 답장은 시간문제일 뿐입니다.
4) 채널별 아웃바운드 메시지 속도 제한(Rate-limit outbound messages per channel)
채널당 하나의 큐가 합리적인 기본값입니다.
유사 코드:
from collections import defaultdict
from queue import Queue
import time
...
실제 코드를 작성할 때는 적절한 워커(worker), 락(lock) 또는 비동기 큐(async queue)를 사용하겠지만, 디자인 포인트는 변함없습니다:
전역이 아닌 채널별로 속도 제한하기(throttle per channel, not globally)
5) 스팸보다 편집을 선호하기(Prefer edits over spam)
진행 상황 업데이트를 원한다면, 다섯 개의 새 메시지를 게시하는 대신 하나의 메시지를 업데이트하세요.
이것이 사용자에게 더 깔끔하게 보이고 속도 제한에도 더 친화적입니다.
6) 스레드 상태 소유하기(Own your thread state)
Slack은 전송 계층(transport layer)일 뿐입니다.
그곳이 당신의 진실 공급원(source of truth)이 되어서는 안 됩니다.
최소한의 스레드 기록 예시:
{
"channel": "C123ABC456",
"thread_ts": "1712345678.123456",
...
}
이것은 장기 실행 에이전트(long-running agents)를 위한 안정적인 컨텍스트 소스를 제공합니다.
Slack과 Discord의 실패 유형은 동일하다
레이블은 다르지만, 아키텍처 교훈은 같습니다.
Slack은 다음 기능을 가지고 있습니다:
app_mention- 스레드(threads)
chat.postMessage- 채널별 동작(channel-specific behavior)
Discord는 다음 기능을 가지고 있습니다:
- 상호작용(interactions)
- 지연 응답(deferred responses)
- 후속 조치(followups)
- 수정 시간(edit windows)
다른 API이지만, 규칙은 같습니다:
빠르게 응답하고, 느린 작업은 큐에 넣고, 재시도는 중복 제거하며, 아웃바운드 쓰기를 제어하라.
만약 장시간 실행되는 에이전트(agent) 작업을 인라인으로 처리하면, 두 플랫폼 모두 당신에게 불이익을 줄 것입니다.
빠른 로컬 디버깅 체크리스트
봇이 DM에서는 작동하지만 채널에서는 작동하지 않을 때, 제가 순서대로 확인하는 체크리스트입니다.
수신 이벤트 유형 확인
이벤트 형태를 기록(log)하세요.
import json
def debug_event(payload):
...
실제로 app_mention을 받고 있다고 확인하고, 모든 트래픽이 message라고 가정하지 마세요.
채널 멤버십 및 스코프 확인
앱이 실제로 해당 채널에 속해 있는지, 그리고 필요한 스코프(scopes)를 가지고 있는지 확인하세요.
빠른 응답 시간 확인 (Fast ack timing)
웹훅을 반환하기 전에 얼마나 오래 걸리는지 기록하세요.
import time
start = time.time()
...
만약 이 숫자가 계속 증가하고 있다면, 작업을 요청 경로(request path) 안으로 다시 끌어들이고 있다는 의미입니다.
중복 제거 확인 (Dedupe)
event_id와 재시도 헤더를 기록하세요.
중복된 답장이 존재한다면, 아마도 아이디엠포턴시(idempotency)를 제어하고 있지 않을 가능성이 높습니다.
스레드 라우팅 확인 (Thread routing)
답장이 올바른 thread_ts를 사용하는지 확인하세요.
아웃바운드 속도 제한 확인 (Outbound rate limiting)
만약 봇이 한 채널에서 너무 많은 메시지를 보내는(chatty) 경우, 달리 증명되기 전까지는 속도 제한이 문제의 일부라고 가정하세요.
이것이 AI 인프라와 연결되는 지점
이 버그는 모델 문제처럼 보였는데, 실제로는 모델 호출이 가장 눈에 띄게 느린 단계였기 때문입니다.
이는 에이전트 시스템에서 흔히 발생하는 일입니다.
사람들은 실제 문제가 오케스트레이션(orchestration)에 있을 때 GPT-5, Claude, Grok 또는 도구 호출 신뢰성 문제로 책임을 돌립니다:
- 잘못된 큐 설계(bad queue design)
- 잘못된 재시도 처리(bad retries)
- 잘못된 이벤트 라우팅(bad event routing)
- 잘못된 속도 제한 처리(bad rate limiting)
- 잘못된 상태 관리(bad state management)
이것이 예측 가능한 AI 인프라가 중요한 이유이기도 합니다.
봇(bot), 자동화(automation) 또는 장기 실행 에이전트(long-running agents)를 구축할 때, 모든 토큰이나 모든 재시도 경로에 집착하지 않고 느린 작업을 오프로드(offload)하고 싶은 마음입니다.
Standard Compute의 매력은 바로 여기에 있습니다. 앱을 OpenAI와 호환되게 유지하면서, 정액 요금제 엔드포인트(flat-rate endpoint)로 교체할 수 있고, 에이전트가 백그라운드에서 실행되도록 하면서도 토큰당 비용에 대한 불안감이 모든 아키텍처 결정에 스며드는 것을 막을 수 있다는 점입니다.
특히 Slack, GitHub, Jira, Notion, n8n, Make, Zapier 또는 사용자 지정 워커(custom workers)를 연결하는 경우, 비싼 부분은 보통 단일 프롬프트가 아닙니다. 전체 루프(loop)입니다.
짜증 나는 진실
우리 봇에게 더 나은 개성이 필요했던 것이 아니었습니다.
더 나은 매너가 필요했습니다.
진짜 해결책은 프롬프트 엔지니어링이 아니었습니다.
다음과 같았습니다:
message.im및app_mention에 대한 분리된 처리- 3초 이내의 응답 확인(ack)
- 느린 작업을 위한 백그라운드 워커(background workers)
- 재시도 시 중복 제거(dedupe on retries)
- 스레드 인식 답변(thread-aware replies)
- 채널별 아웃바운드 제한(per-channel outbound throttling)
- Slack 외부의 상태 관리(state outside Slack)
만약 채팅봇이 DM에서는 완벽하게 작동하지만 그룹에서 답장을 멈춘다면, 지루한 플러밍(plumbing)부터 시작하세요.
그것이 실제로 프로덕션 봇을 고치는 것들입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기