KO ▾
API 키 받기

Discord 봇 코딩 AI: LLM API로 만들기

Discord 봇 코딩 AI는 API 뒤의 언어 모델을 사용하여 서버 내에서 프로그래밍 질문에 답변합니다. 이 튜토리얼은 약 70줄의 Python으로 하나를 만들며, 일반적으로 문제가 되는 부분을 다룹니다.

업데이트

LLM API로 구동되는 Discord 봇을 나타내는 빛나는 대화상자에 둘러싸인 추상적인 봇 그림

주요 포인트

  • 봇에는 discord.py, openai 패키지, 봇 토큰 및 LLM API 키가 필요합니다.
  • Discord 개발자 포털에서 메시지 콘텐츠 인텐트를 활성화하지 않으면 봇이 빈 메시지를 수신합니다.
  • 모델은 호출 간에 기억하지 못하므로 채널별 짧은 기록을 유지하고 잘라내야 합니다.
  • 응답을 2,000자 단위로 분할하고 동시 호출을 제한하여 8개의 동시 요청 한도 내에 머물러야 합니다.

봇의 동작

목표는 멘션에 반응하는 Discord 봇 코딩 AI를 만드는 것입니다. 사용자가 "@helper 왜 내 리스트 컴프리헨션이 None을 반환하지?"라고 입력하면 봇은 메시지를 모델에 보내고 모델의 답변이 같은 채널에 반환됩니다. 봇은 각 채널의 최근 대화 몇 턴을 기억하므로 후속 질문이 가능합니다.

디자인은 세 부분으로 구성됩니다. discord.py는 Discord 연결을 처리합니다. OpenAI Python SDK는 LLM API와 통신하며, 이 API는 OpenAI 호환 API이므로 작동합니다. 작은 사전을 사용하여 대화 기록을 저장합니다. 시작하려면 데이터베이스가 필요하지 않습니다.

Python 3.9 이상이 필요하며, 애플리케이션을 생성할 수 있는 Discord 계정이 있고 API 키 페이지에서 API 키가 필요합니다. 새 계정은 한 번의 클릭으로 $0.50의 무료 체험 크레딧을 청구할 수 있으며, 이는 봇을 구축하고 테스트하기에 충분합니다.

단계 1: Discord 애플리케이션 생성

  1. Discord 개발자 포털을 열고 새 애플리케이션을 생성하세요.
  2. 봇 섹션에서 봇 사용자를 생성하고 토큰을 복사하세요. 비밀번호처럼 취급하세요.
  3. 같은 페이지에서 Message Content Intent을 권한 있는 게이트웨이 인텐트 아래에서 활성화하세요. 활성화하지 않으면 봇이 빈 텍스트가 있는 메시지를 수신하고, 답변이 없는 이유를 파악하는 데 1시간을 보낼 수 있습니다.
  4. OAuth2에서 bot 범위와 메시지 읽기 및 보내기 권한이 있는 초대 링크를 생성하세요. 링크를 열고 테스트 서버에 봇을 추가하세요.

라이브러리를 설치하고 두 가지 환경 변수를 설정하세요.

pip install discord.py openai

export DISCORD_TOKEN="your-discord-bot-token"
export LLM_API_KEY="your-llm-api-key"

# quick check that the LLM key works, using model uncensored
curl -s https://api.codingllmapi.com/v1/chat/completions \
  -H "Authorization: Bearer $LLM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"uncensored","messages":[{"role":"user","content":"ping"}],"max_tokens":5}'

단계 2: 봇 코드

이를 bot.py로 저장하세요. 멘션 시 응답하고, 채널당 12개의 메시지 기록을 유지하며, 입력 중 표시를 보여 주고, 긴 응답을 분할합니다.

import asyncio
import os

import discord
from openai import AsyncOpenAI, RateLimitError, APIError

llm = AsyncOpenAI(
    base_url="https://api.codingllmapi.com/v1",
    api_key=os.environ["LLM_API_KEY"],
)

SYSTEM = (
    "You are a helpful programming assistant in a Discord server. "
    "Answer briefly. Put code in fenced blocks. Say so when you are unsure."
)
MAX_TURNS = 12
history = {}                     # channel id -> list of messages
slots = asyncio.Semaphore(6)     # stay under the 8 concurrent request cap

intents = discord.Intents.default()
intents.message_content = True
bot = discord.Client(intents=intents)


def split_reply(text, limit=1900):
    parts = []
    while len(text) > limit:
        cut = text.rfind("\n", 0, limit)
        if cut < limit // 2:
            cut = limit
        parts.append(text[:cut])
        text = text[cut:].lstrip("\n")
    parts.append(text)
    return parts


@bot.event
async def on_message(message):
    if message.author.bot or bot.user not in message.mentions:
        return

    question = message.clean_content.replace(f"@{bot.user.display_name}", "").strip()
    if not question:
        await message.reply("Ask me a coding question after the mention.")
        return

    log = history.setdefault(message.channel.id, [])
    log.append({"role": "user", "content": question})
    del log[:-MAX_TURNS]

    async with message.channel.typing():
        try:
            async with slots:
                resp = await llm.chat.completions.create(
                    model="uncensored",
                    messages=[{"role": "system", "content": SYSTEM}] + log,
                    temperature=0.4,
                    max_tokens=900,
                )
        except RateLimitError:
            await message.reply("The model is busy. Try again in a few seconds.")
            log.pop()
            return
        except APIError:
            await message.reply("The model call failed. Try again shortly.")
            log.pop()
            return

    answer = resp.choices[0].message.content or "(empty reply)"
    log.append({"role": "assistant", "content": answer})
    for part in split_reply(answer):
        await message.reply(part)


bot.run(os.environ["DISCORD_TOKEN"])

python bot.py로 실행한 후 테스트 서버에서 봇을 멘션하세요. 첫 번째 답변은 몇 초 이내에 도착해야 합니다.

코드 작성 이유

기록이 의도적으로 잘립니다

모델은 기억력이 없으므로 각 호출은 최근 대화를 전송합니다. del log[:-MAX_TURNS]이 없으면 목록이 무한히 커집니다. 결국 100,000개의 토큰 컨텍스트 창에 도달하고 메시지마다 비용이 증가합니다. 12개의 항목은 버그에 대한 대화에 충분합니다. 긴 스레드가 있는 서버의 경우 이를 높이거나 오래된 대화를 요약하세요.

Discord의 2,000자 제한

Discord는 2,000자보다 긴 메시지를 거부하며, 코드 답변은 빠르게 그 한도에 도달합니다. split_reply 도우미 함수는 제한 근처의 줄바꿈에서 잘라냅니다. 코드 펜스에 대해 알지 못하므로 블록이 중간에 잘릴 수 있습니다. 이 점이 마음에 걸린다면 한 부분의 끝에 펜스를 닫고 다음 부분의 시작에서 다시 여는 로직을 추가하세요.

동시성을 위한 세마포어

전체 키는 동시 요청 8개를 허용하고, 체험 키는 2개를 허용합니다. 바쁜 서버는 여러 질문을 동시에 처리할 수 있습니다. 세마포어는 추가 호출을 프로세스 내부에 대기열에 넣으므로 외부로 전송하거나 concurrency 오류 코드로 429를 수신할 필요가 없습니다. 아직 체험 키를 사용 중이라면 Semaphore(6)를 Semaphore(2)로 변경하세요.

오류는 요금이 부과되지 않음

실패하거나 거부된 요청은 청구되지 않으므로 오류 후 재시도해도 비용이 두 배가 되지 않습니다. 코드는 실패 시 사용자 턴을 기록에서 제거하므로 재시도가 모델이 이미 두 번 본 답 없는 질문이 남지 않습니다.

응답 품질 개선

기본 루프가 작동하면 몇 가지 작은 변경으로 크게 개선할 수 있습니다.

  • 답변 스트리밍. stream=True을 전달하고 지금까지의 텍스트로 매초마다 봇의 메시지를 수정하세요. Discord는 메시지 수정에 자체 속도 제한을 적용하므로 모든 토큰마다 수정하지 마세요. 스트리밍 요청은 종료될 때까지 동시성 슬롯을 유지하므로 전체 스트림에 대해 세마포어를 유지하세요.
  • 슬래시 명령어 사용. /ask과 같은 명령어는 멘션 파싱을 피하고 Message Content 인텐트가 전혀 필요하지 않아 봇이 소수의 서버를 넘어 확장될 때 나중에 검증하기 더 쉬워집니다.
  • 사용자별 기록. 여러 사람이 한 채널에서 관련 없는 질문을 하는 경우 채널과 사용자별로 기록을 구분해 저장하세요.
  • 시스템 프롬프트 조정. 커뮤니티에서 사용하는 언어 이름을 지정하고 짧은 답변을 요청하며, 문제가 모호할 때 명확한 질문을 하도록 모델에 지시하세요. 프롬프트 변경은 무료이며 모델 변경보다 더 나은 결과를 가져오는 경우가 많습니다.
  • 온도 낮추기. 코드의 경우 0.2에서 0.4 사이의 값이 기본값보다 더 안정적인 결과를 제공합니다.

일반적인 오류 해결

새로운 봇이 침묵할 때 원인은 대개 아래 항목 중 하나입니다. 이 순서로 확인하세요.

  1. 봇은 온라인이지만 응답하지 않음. 개발자 포털에서 메시지 콘텐츠 인텐트가 꺼져 있거나 intents.message_content = True이 코드에 누락되었습니다. 둘 다 설정해야 합니다.
  2. 일부 채널에서만 응답함. 봇이 다른 채널에서 메시지를 읽거나 보낼 권한이 없습니다. 서버 전체 역할뿐만 아니라 채널의 권한 재정의도 확인하세요.
  3. 모든 응답이 "바쁨" 메시지입니다. 429 상태 코드를 받고 계실 가능성이 높습니다. 예외 메시지 본문을 한 번 출력하세요. 코드가 rate_limited라면 전체 키의 분당 300회 또는 체험 키의 분당 60회 요청 한도를 초과했습니다. concurrency라면 세마포어 크기를 줄이세요.
  4. 첫 번째 호출 시 인증 오류 발생. 봇을 실행하는 프로세스의 환경에 LLM 키가 없습니다. 하나의 터미널 탭에서 내보낸 키는 다른 곳에서 시작된 서비스에서는 볼 수 없습니다.
  5. 응답이 잘렸습니다. 샘플의 max_tokens=900은 긴 응답을 제한합니다. 이를 높이거나 제거하세요. max_tokens는 선택 사항이며, 생성된 텍스트가 컨텍스트 창의 나머지 부분을 사용할 수 있습니다.
  6. 봇이 자신의 메시지에 응답함. message.author.bot 검사를 누락하면 이 루프가 발생합니다. 유지하세요.

디버깅을 위해 각 호출에 대한 X-Request-Id 값을 로그에 기록하세요. OpenAI SDK에서는 llm.chat.completions.with_raw_response.create(...)를 호출한 후 .headers을 읽어서 원본 응답에서 이를 읽을 수 있습니다. 특정 오류에 대해 지원팀에 문의하면 해당 ID를 통해 정확한 요청을 찾을 수 있습니다.

실행 비용

가격은 입력 토큰 100만 개당 $0.15, 출력 토큰 100만 개당 $0.50이며 구독료는 없습니다. 일반적인 예시를 들어보겠습니다. 시스템 프롬프트와 12개의 대화 기록을 합하면 입력 토큰이 약 2,500개 정도 생성되고, 답변은 약 400개의 토큰으로 구성됩니다.

입력: 2,500 x $0.15 / 1,000,000 = $0.000375. 출력: 400 x $0.50 / 1,000,000 = $0.0002. 하나의 응답 비용은 약 $0.000575입니다. 하루 500개의 질문을 하는 커뮤니티라면 500 x $0.000575 = $0.2875를 지출하며, 30센트가 조금 안 되는 금액입니다. 긴 코드 붙여넣기로 인해 숫자가 달라질 수 있으므로 resp.usage을 일주일간 로깅하고 실제 평균을 확인하세요. 가격 책정 페이지에는 $20부터 $500까지의 정수 금액 충전 옵션이 있으며, $50부터 +5%, $100부터 +10%, $200부터 +15% 보너스가 제공됩니다. 카드로 또는 암호화폐로 결제할 수 있습니다.

남용을 제한하려면 사용자별 대기 시간과 질문 길이 제한을 추가하세요. 이 두 가지 점검은 추가할 수 있는 다른 어떤 조치보다도 잔액을 더 잘 보호합니다.

호스팅 및 안전성 참고 사항

봇은 장시간 실행되는 프로세스입니다. 무거운 작업은 API 측에서 처리되므로 작은 가상 머신이나 컨테이너 호스트면 충분합니다. 충돌 후 재시작되도록 프로세스 관리자 아래에서 실행하고, 토큰 둘 다 시크릿 스토어 또는 환경 변수에 보관하세요. 저장소에는 넣지 마세요.

서버가 허용하는 내용을 고려하세요. 이 모델은 합법적인 성인 사용에 대해 콘텐츠 거부 없이 답변하도록 조정되었으므로, 더 엄격한 어시스턴트가 거부할 수 있는 보안 테스트나 과감한 소설을 다룰 수 있습니다. 커뮤니티에 미성년자가 포함된다면 시스템 프롬프트에 규칙을 설정하고 봇을 특정 채널로 제한하세요. Discord의 자체 약관도 봇에 적용된다는 점을 기억하세요. 미성년자를 성적으로 묘사하는 콘텐츠는 이 API에서 예외 없이 금지됩니다.

stop, seed 및 함수 호출과 같은 전체 파라미터 세부 정보는 API 문서에서 확인할 수 있습니다. 봇이 코드를 실행하거나 저장소를 읽도록 하려면 함수 호출이 필요하며, 모델이 요청할 수 있는 함수를 결정할 수 있습니다. 이 모델을 프로그래밍 중심의 용도로 사용하려면 무검열 AI 코딩 모델 페이지를 참조하세요.

질문과 답변

메시지 콘텐츠 인텐트가 필요한가요?

봇이 일반 메시지나 멘션을 읽는다면 필요합니다. 슬래시 명령어만 사용하는 경우라면 필요하지 않습니다.

봇이 답변을 스트리밍할 수 있나요?

네. stream=True를 설정하고 Discord의 자체 제한 내에 머무를 수 있도록 일 초에 한 번과 같은 적절한 간격으로 Discord 메시지를 수정하세요.

너무 많은 사람이 동시에 질문하면 어떻게 되나요?

풀 키에서 8개의 동시 요청을 초과하면 API는 HTTP 429와 concurrency 코드를 반환합니다. 코드 내의 세마포어가 추가 요청을 대기열에 넣습니다.

봇은 재시작 후 대화 내용을 기억하나요?

여기서 사용되는 사전(dictionary) 방식으로는 기억하지 않습니다. 재시작 후에도 유지하려면 SQLite나 Redis에 기록을 저장하세요.

API 키는 Discord 봇에서 안전한가요?

서버의 환경 변수에 보관한다면 안전합니다. 채널에 게시하거나 공개 저장소에 커밋하지 마세요.

키는 양식 하나만 작성하면 받을 수 있습니다

계정을 생성하고 키를 복사한 후 기본 URL을 변경하세요. 설정은 이것으로 끝입니다.

API 키 받기