PT ▾
Obter chave de API

Bot de código para Discord: crie um com API de LLM

Um bot de código para Discord responde perguntas de programação dentro do seu servidor, usando um modelo de linguagem por trás de uma API. Este tutorial constrói um em cerca de 70 linhas de Python e cobre as partes que costumam falhar.

Atualizado

Figura abstrata de um chatbot cercada por balões de fala brilhantes, ilustrando um bot do Discord alimentado por uma API LLM

Pontos principais

  • O bot precisa do discord.py, do pacote openai, de um token de bot e de uma chave de API LLM.
  • Ative a intenção de Conteúdo de Mensagem no portal de desenvolvedores do Discord ou o bot verá mensagens vazias.
  • Mantenha um histórico curto por canal e corte-o, já que o modelo não lembra nada entre as chamadas.
  • Divida as respostas em 2.000 caracteres e limite as chamadas simultâneas para ficar abaixo do limite de 8 requisições concorrentes.

O que o bot fará

O objetivo é um bot de código para Discord que reage quando alguém o menciona. Um usuário escreve "@helper por que minha list comprehension retorna None?", o bot envia a mensagem ao modelo e a resposta do modelo volta como uma resposta no mesmo canal. O bot também lembra as últimas rodadas de cada canal, para que perguntas de acompanhamento funcionem.

O design tem três partes. discord.py cuida da conexão com o Discord. O SDK Python da OpenAI fala com a API de LLM, que funciona aqui porque a API é compatível com OpenAI. Um pequeno dicionário mantém o histórico de conversas. Você não precisa de banco de dados para começar.

Você precisará do Python 3.9 ou mais recente, de uma conta no Discord que possa criar um aplicativo e de uma chave de API na página da chave de API. Contas novas podem reivindicar $0,50 de crédito de teste com um clique, o suficiente para criar e testar este bot.

Passo 1: criar o aplicativo do Discord

  1. Abra o portal de desenvolvedores do Discord e crie um novo aplicativo.
  2. Na seção Bot, crie o usuário do bot e copie seu token. Trate-o como uma senha.
  3. Na mesma página, ative a Intenção de Conteúdo de Mensagem em Intenções de Gateway Privilegiadas. Sem isso, o bot recebe mensagens com texto vazio e você gastará uma hora se perguntando por que ele nunca responde.
  4. Em OAuth2, gere um link de convite com o escopo bot e as permissões para ler mensagens e enviar mensagens. Abra o link e adicione o bot a um servidor de teste.

Instale as bibliotecas e defina duas variáveis de ambiente.

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}'

Passo 2: o código do bot

Salve isso como bot.py. Ele responde quando mencionado, mantém doze mensagens de histórico por canal, mostra o indicador de digitação enquanto espera e divide respostas longas.

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"])

Execute-o com python bot.py, depois mencione o bot no seu servidor de teste. A primeira resposta deve chegar em alguns segundos.

Por que o código é escrito assim

O histórico é cortado propositalmente

O modelo não tem memória, então cada chamada envia as rodadas recentes. Sem del log[:-MAX_TURNS] a lista cresceria para sempre. Eventualmente atingiria a janela de 100.000 tokens e seus custos aumentariam a cada mensagem. Doze entradas são suficientes para uma conversa sobre um bug. Para um servidor com threads longas, aumente isso ou resuma rodadas mais antigas.

Limite de 2.000 caracteres do Discord

O Discord rejeita mensagens com mais de 2.000 caracteres, e respostas de código atingem isso rapidamente. O auxiliar split_reply corta em uma quebra de linha perto do limite. Ele não conhece blocos de código, então um bloco pode ser cortado no meio. Se isso incomodar, adicione lógica que feche um bloco no final de uma parte e o reabra no início da próxima.

Um semáforo para concorrência

Uma chave completa permite 8 requisições simultâneas, e uma chave de teste permite 2. Um servidor ocupado pode gerar várias perguntas ao mesmo tempo. O semáforo enfileira as chamadas extras dentro do seu processo em vez de enviá-las e receber um 429 com o código de erro concurrency. Se você ainda estiver usando a chave de teste, altere Semaphore(6) para Semaphore(2).

Erros não são cobrados

Requisições falhas ou recusadas não são cobradas, então uma nova tentativa após um erro não dobra o custo. O código remove a rodada do usuário do histórico em caso de falha, para que uma nova tentativa não deixe uma pergunta pendente que o modelo já viu duas vezes.

Tornando as respostas melhores

Uma vez que o loop básico funcione, algumas pequenas mudanças melhoram muito.

  • Transmita a resposta. Passe stream=True e edite a mensagem do bot a cada segundo mais ou menos com o texto até o momento. Não edite a cada token, porque o Discord aplica seus próprios limites de taxa às edições de mensagem. Uma solicitação em streaming ocupa seu slot de concorrência até o fim, então mantenha o semáforo em torno de todo o streaming.
  • Use comandos slash. Um comando como /ask evita a análise de menções e não precisa da intenção de Conteúdo de Mensagem, o que também torna o bot mais fácil de verificar posteriormente se ele crescer além de um pequeno número de servidores.
  • Histórico por usuário. Separe o histórico por canal e usuário se várias pessoas fizerem perguntas não relacionadas em um canal.
  • Ajuste o prompt do sistema. Nomeie os idiomas usados pela sua comunidade, peça respostas curtas e diga ao modelo para fazer uma pergunta de esclarecimento quando o problema for vago. Alterações no prompt são gratuitas e muitas vezes superam alterações no modelo.
  • Reduza a temperatura. Para código, valores entre 0,2 e 0,4 dão resultados mais estáveis do que o padrão.

Solução de problemas das falhas comuns

Quando um bot novo fica em silêncio, a causa é quase sempre um dos itens abaixo. Verifique-os nesta ordem.

  1. O bot está online, mas nunca responde. A intenção de Conteúdo de Mensagem está desativada no portal de desenvolvedores, ou intents.message_content = True está faltando no código. Ambos devem ser definidos.
  2. Ele responde apenas em alguns canais. O bot não tem permissão para ler ou enviar mensagens nos outros. Verifique as substituições de permissão do canal, não apenas a função em todo o servidor.
  3. Cada resposta é uma mensagem de "ocupado". Você provavelmente está atingindo um 429. Imprima o corpo da exceção uma vez. Se o código for rate_limited, você passou do limite por minuto de 300 requisições em uma chave completa ou 60 em uma chave de teste. Se for concurrency, reduza o tamanho do semáforo.
  4. Um erro de autenticação aparece na primeira chamada. A chave LLM está faltando no ambiente do processo que executa o bot. Uma chave exportada em uma aba de terminal não está visível para um serviço iniciado em outro lugar.
  5. A resposta é cortada. O max_tokens=900 na amostra interrompe respostas longas. Aumente-o ou remova-o, já que max_tokens é opcional e a conclusão pode usar o restante da janela.
  6. O bot responde às próprias mensagens. Esse loop acontece se você remover a verificação message.author.bot. Mantenha-a.

Para depuração, registre o valor X-Request-Id para cada chamada. Com o SDK OpenAI, você pode lê-lo na resposta bruta chamando llm.chat.completions.with_raw_response.create(...) e depois lendo .headers. Se entrar em contato com o suporte sobre uma falha específica, esse ID permite que eles encontrem a solicitação exata.

Quanto isso custa para executar

O preço é de $0,15 por 1 milhão de tokens de entrada e $0,50 por 1 milhão de tokens de saída, sem assinatura. Tome um exemplo típico: o prompt do sistema mais doze entradas de histórico geram cerca de 2.500 tokens de entrada, e a resposta tem cerca de 400 tokens.

Entrada: 2.500 x $0.15 / 1.000.000 = $0.000375. Saída: 400 x $0.50 / 1.000.000 = $0.0002. Uma resposta custa cerca de $0.000575. Uma comunidade que faz 500 perguntas por dia gastaria 500 x $0.000575 = $0.2875, um pouco menos de trinta centavos. Seus números variarão com colagens de código mais longas, então registre resp.usage por uma semana e leia as médias reais. A página de preços mostra as opções de recarga: qualquer valor inteiro de $20 a $500, com +5% a partir de $50, +10% a partir de $100 e +15% a partir de $200, pagos por cartão ou cripto.

Para limitar o abuso, adicione um tempo de espera por usuário e um limite no tamanho da pergunta. Essas duas verificações protegem seu saldo melhor do que qualquer outra coisa que você possa adicionar.

Observações sobre hospedagem e segurança

O bot é um processo de longa duração. Uma pequena máquina virtual ou um host de contêiner é suficiente, já que o trabalho pesado ocorre no lado da API. Execute-o sob um gerenciador de processos para que ele reinicie após uma falha, e mantenha ambos os tokens em variáveis de ambiente ou em um repositório de segredos, nunca no repositório.

Pense no que seu servidor permite. O modelo é ajustado para responder sem recusas de conteúdo para uso adulto legal, então ele discutirá testes de segurança ou ficção polêmica que um assistente mais rigoroso poderia recusar. Se sua comunidade inclui membros mais jovens, defina regras no prompt do sistema, restrinja o bot a canais específicos e lembre-se de que os próprios termos do Discord se aplicam ao seu bot também. Conteúdo que sexualiza menores de idade é proibido nesta API sem exceção.

Detalhes completos dos parâmetros, como stop, seed e chamada de funções, estão na documentação da API. Se você quiser que o bot execute código ou leia repositórios, a chamada de funções é o caminho, e você decide quais funções o modelo pode solicitar. Para uso focado em programação deste modelo, consulte a página sobre o modelo de IA sem censura para programação.

Perguntas e respostas

Preciso da intenção de Conteúdo de Mensagem?

Sim, se o bot lê mensagens normais e menções. Se você usar apenas comandos slash, não precisa disso.

O bot pode transmitir respostas?

Sim. Defina stream=True e edite a mensagem do Discord em um intervalo tranquilo, como uma vez por segundo, para permanecer dentro dos limites próprios do Discord.

O que acontece se muitas pessoas perguntarem ao mesmo tempo?

Após 8 requisições simultâneas em uma chave completa, a API retorna HTTP 429 com o código concurrency. Um semáforo no seu código enfileira as perguntas extras.

O bot lembrará das conversas após uma reinicialização?

Não com o dicionário usado aqui. Armazene o histórico no SQLite ou Redis se precisar que ele sobreviva às reinicializações.

A chave de API está segura em um bot do Discord?

Sim, se permanecer no seu servidor em uma variável de ambiente. Nunca a publique em um canal ou faça commit em um repositório público.

Sua chave está a um formulário de distância

Crie uma conta, copie a chave e altere a URL base. Essa é toda a configuração.

Obter chave de API