Como criar um bot do Discord com IA

Um bot do Discord com IA é um bot discord.js que recebe um comando, envia o texto a uma API de modelo de linguagem e devolve a resposta no canal. A chave da API fica em variável de ambiente.

Atualizado em 6 min de leitura

Objetivos de aprendizado

Depois de ler este artigo, você vai conseguir:

  • Montar um comando /ask em discord.js que chama uma API de LLM
  • Evitar os erros típicos: interação expirada, mensagem acima de 2000 caracteres e abuso de custo
  • Colocar o bot no ar com as chaves em variáveis de ambiente
Nesta página

Um bot de IA para o Discord é um bot comum com uma chamada extra: em vez de responder com um texto fixo, ele manda a pergunta do usuário para um modelo de linguagem (LLM) por uma API e publica o que volta. O usuário digita /ask, o Discord entrega a interação ao bot pelo Gateway, o bot chama a API do modelo e edita a resposta com o texto.

O bot não roda modelo nenhum: ele guarda os segredos, aplica limites e formata a saída, por isso continua leve. Em compensação, surgem problemas que um bot comum não tem: a resposta demora, pode passar do tamanho permitido e cada chamada custa dinheiro.

Os exemplos usam discord.js v14 e o SDK @anthropic-ai/sdk (a API da Anthropic, com um modelo pequeno e barato da linha Haiku). O desenho serve para qualquer provedor: só a chamada muda. Se você ainda não tem um bot rodando, comece por como hospedar um bot discord.js. Para entender o modelo por trás, veja o que é um LLM.

UsuárioGatewayBotAPI do LLM/ask1interação2deferReply (até 3 s)3histórico + pergunta4texto gerado5editReply com a resposta6
O bot segura a interação com deferReply, chama o LLM com o histórico e depois edita a resposta com o texto.

O que você precisa antes de começar?#

  • A versão LTS ativa do Node.js e um app criado no Discord Developer Portal, com o token do bot. Veja como guardar o token.
  • Uma chave de API de um provedor de LLM. Aqui usamos uma do Claude Console, lida da variável ANTHROPIC_API_KEY, o nome que o SDK procura sozinho.
  • Um servidor de teste onde você possa convidar o bot com os escopos bot e applications.commands.

Como configurar o projeto?#

bash
mkdir ai-bot && cd ai-bot
npm init -y
npm pkg set type=module
npm install discord.js@14 @anthropic-ai/sdk

Crie o .env (e coloque .env no .gitignore):

bash
DISCORD_TOKEN=cole_o_token_do_bot
CLIENT_ID=id_da_aplicacao
GUILD_ID=id_do_servidor_de_teste
ANTHROPIC_API_KEY=cole_a_chave_da_api
ANTHROPIC_MODEL=id_de_um_modelo_pequeno

O Node.js lê esse arquivo com a flag --env-file, sem pacote extra. Em ANTHROPIC_MODEL, use o ID de um modelo pequeno, rápido e barato (da linha Haiku, por exemplo), copiado da lista de modelos da Anthropic. O modelo fica fora do código porque a Anthropic aposenta modelos com o tempo: trocar de modelo vira só trocar a variável.

Como escrever o bot?#

Crie o index.js. O bot usa só o intent Guilds, mantém as últimas 10 mensagens de cada canal e impõe 10 segundos de espera por usuário:

js
import { Client, Events, GatewayIntentBits, MessageFlags } from "discord.js";
import Anthropic from "@anthropic-ai/sdk";

const MODEL = process.env.ANTHROPIC_MODEL;
if (!MODEL) throw new Error("Defina ANTHROPIC_MODEL com um ID da lista de modelos da Anthropic");
const SYSTEM =
  "Você é um assistente de um servidor do Discord. Responda em português, " +
  "de forma direta e com no máximo 1500 caracteres.";

const anthropic = new Anthropic(); // lê ANTHROPIC_API_KEY
const client = new Client({ intents: [GatewayIntentBits.Guilds] });

const history = new Map(); // channelId -> [{ role, content }]
const lastUse = new Map(); // userId -> timestamp

client.once(Events.ClientReady, (c) => console.log(`Online como ${c.user.tag}`));

client.on(Events.InteractionCreate, async (interaction) => {
  if (!interaction.isChatInputCommand() || interaction.commandName !== "ask") return;

  const wait = 10_000 - (Date.now() - (lastUse.get(interaction.user.id) ?? 0));
  if (wait > 0) {
    await interaction.reply({
      content: `Espere ${Math.ceil(wait / 1000)}s para perguntar de novo.`,
      flags: MessageFlags.Ephemeral,
    });
    return;
  }
  lastUse.set(interaction.user.id, Date.now());

  await interaction.deferReply(); // o Discord exige resposta em 3 s

  const question = `${interaction.user.displayName}: ${interaction.options.getString("pergunta", true)}`;
  const past = history.get(interaction.channelId) ?? [];

  try {
    const response = await anthropic.messages.create({
      model: MODEL,
      max_tokens: 2000,
      output_config: { effort: "low" }, // pensa pouco: resposta curta e barata
      system: SYSTEM,
      messages: [...past, { role: "user", content: question }],
    });

    const text = response.content
      .filter((block) => block.type === "text")
      .map((block) => block.text)
      .join("")
      .trim();

    const answer = text || "Não consegui gerar uma resposta.";
    history.set(
      interaction.channelId,
      [...past, { role: "user", content: question }, { role: "assistant", content: answer }].slice(-10),
    );

    await interaction.editReply(answer.slice(0, 2000));
  } catch (error) {
    console.error(error);
    await interaction.editReply("A IA não respondeu agora. Tente de novo em instantes.");
  }
});

client.login(process.env.DISCORD_TOKEN);

Quatro detalhes importam:

  • deferReply() vem antes da chamada à API. Segundo a documentação do Discord, a primeira resposta precisa sair em até 3 segundos, senão o token da interação é invalidado. Depois do defer, o token vale 15 minutos para o editReply.
  • slice(-10) guarda sempre pares de pergunta e resposta, então o histórico começa por uma mensagem do usuário, como a API exige.
  • effort: "low" e max_tokens: 2000 andam juntos. Se o modelo pensa antes de responder, esse raciocínio conta no max_tokens. O esforço baixo encurta o pensamento, e o teto deixa espaço para ele e para a resposta. Se mesmo assim o texto vier vazio, o bot usa a mensagem de reserva. Se o modelo escolhido não aceitar o parâmetro effort, a API devolve erro 400: remova a linha.
  • slice(0, 2000) respeita o limite de 2000 caracteres do conteúdo de uma mensagem do Discord.

Como registrar o comando?#

Iniciar o bot não cria o comando. Rode um script de registro uma vez, e de novo quando mudar a definição (deploy-commands.js):

js
import { REST, Routes, SlashCommandBuilder } from "discord.js";

const ask = new SlashCommandBuilder()
  .setName("ask")
  .setDescription("Faça uma pergunta à IA")
  .addStringOption((o) =>
    o.setName("pergunta").setDescription("Sua pergunta").setRequired(true).setMaxLength(1000),
  );

const rest = new REST().setToken(process.env.DISCORD_TOKEN);
await rest.put(Routes.applicationGuildCommands(process.env.CLIENT_ID, process.env.GUILD_ID), {
  body: [ask.toJSON()],
});
console.log("Comando registrado.");

O setMaxLength(1000) limita o texto de entrada, o que também limita o custo de cada chamada. Para valer em todos os servidores, troque a rota por Routes.applicationCommands(process.env.CLIENT_ID).

Como testar?#

bash
node --env-file=.env deploy-commands.js
node --env-file=.env index.js

O terminal deve mostrar Online como NomeDoBot. No servidor, rode /ask pergunta: o que é um container?. Você verá "o bot está pensando" e depois a resposta. Faça uma segunda pergunta que dependa da primeira ("e uma VM?") para confirmar que a memória do canal funciona.

Quais erros aparecem e como evitar custo e abuso?#

  • Unknown interaction (código 10062): a primeira resposta passou de 3 segundos. Confirme que o deferReply() é a primeira chamada assíncrona do handler.
  • Erro 401 (authentication_error) da API: a ANTHROPIC_API_KEY não chegou ao processo. O .env local não existe no servidor, então configure a variável lá.
  • Erro 404 (not_found_error) citando o modelo: o ID em ANTHROPIC_MODEL está errado ou o modelo foi aposentado. Escolha outro na lista de modelos e reinicie o bot, sem mexer no código.
  • Invalid Form Body com content acima de 2000: você enviou texto longo sem cortar. Mantenha o slice e peça respostas curtas no prompt de sistema.
  • Fatura inesperada: o custo vem dos tokens de entrada e saída. Os modelos pequenos, como os da linha Haiku, são os mais baratos da tabela da Anthropic, mas o preço por milhão de tokens muda: confira o valor atual antes de publicar. Use o tempo de espera, o max_tokens e o limite de entrada, e veja quanto custa uma API de LLM.
  • Usuário manipulando o bot: qualquer texto que chega ao modelo pode tentar mudar suas instruções. Nunca dê ao bot permissões que você não daria ao usuário. Leia o que é prompt injection.
  • Chave vazada: se a chave foi parar num repositório, revogue e gere outra no console do provedor.

Onde hospedar o bot?#

Um bot de IA precisa ficar ligado o tempo todo e guardar duas chaves em segredo. No computador de casa ele cai quando você desliga a máquina; num VPS você cuida de supervisor, atualizações e firewall (veja como manter um bot online 24/7). Quanto à RAM, o processo continua pequeno, porque o modelo roda no provedor: veja quanta RAM um bot precisa. Para um assistente que atende um site e não só o Discord, veja também como hospedar um chatbot de IA.

Como fazer isso na Square Cloud#

O bot que você escreveu depende de duas coisas: um processo sempre ligado e as chamadas ao modelo. Na Square Cloud você resolve as duas no mesmo lugar.

O processo roda num container isolado e sempre ligado. Com o reinício automático (ligado por padrão no deploy pelo painel), ele volta sozinho se encerrar com erro; se estourar a memória, para até você aumentar a RAM e iniciar de novo.

  1. Envie o projeto (index.js, package.json) por upload de ZIP no painel, CLI, GitHub ou extensão do VS Code. O arquivo squarecloud.app é opcional: o painel o gera com runtime, memória e arquivo principal, ou você o escreve na raiz do projeto.
  2. Escolha Node.js e index.js como arquivo principal. Um bot usa pelo menos 256 MB do pool de RAM do plano.
  3. Cadastre DISCORD_TOKEN, CLIENT_ID, ANTHROPIC_API_KEY e ANTHROPIC_MODEL nas variáveis de ambiente da aplicação. A mudança vale no próximo restart, sem rebuild.
  4. Acompanhe os logs em tempo real e confirme a linha Online como ....

A partir do plano Standard (R$ 44,99 por mês), a chamada ao modelo também pode passar pelo AI Gateway, uma API compatível com a da OpenAI para o modelo cubic, o mesmo do assistente do painel. Ele está em beta, com acesso antecipado sem custo extra. Instale o pacote openai, troque o trecho do anthropic.messages.create e apague as duas linhas do MODEL, que deixam de ser usadas:

js
import OpenAI from "openai";

const ai = new OpenAI({
  baseURL: "https://api.squarecloud.app/v2/ai",
  apiKey: process.env.SQUARECLOUD_API_KEY, // chave da conta com o escopo ai:chat
});

const completion = await ai.chat.completions.create({
  model: "cubic",
  messages: [{ role: "system", content: SYSTEM }, ...past, { role: "user", content: question }],
});
const text = completion.choices[0].message.content?.trim() ?? "";

O histórico por canal continua no bot, porque a API não guarda estado. Antes de trocar, confira os limites do Standard: até 2,5 milhões de tokens e até 1.000 requisições por dia, uma requisição por vez. Uma pergunta que chega enquanto outra está em andamento recebe erro 429; o SDK da OpenAI tenta de novo algumas vezes, e o catch do bot cobre o resto. Limites e planos podem mudar quando o beta acabar. O Hobby, a partir de R$ 12,49 por mês, hospeda o bot, mas não inclui o gateway. Veja como hospedar o bot em bots do Discord.

IA

Hospede apps com IA em containers sempre ligados

Saiba mais

Perguntas frequentes

Preciso do intent MessageContent para um bot de IA?

Não, se o bot responder a slash commands. O intent Guilds basta. MessageContent é privilegiado e só é necessário para ler o texto de mensagens comuns.

Por que a resposta da IA dá erro de interação desconhecida?

O Discord exige uma primeira resposta em até 3 segundos, e um LLM costuma demorar mais. Chame deferReply() logo no início e use editReply() quando a resposta chegar.

Como o bot lembra do que foi dito antes?

A API de modelos não guarda estado: você envia o histórico inteiro a cada chamada. Guarde as últimas mensagens de cada canal e envie junto com a pergunta nova.

Quanto custa manter um bot de IA no Discord?

Há dois custos: a hospedagem do processo e o consumo de tokens da API, que cresce com o uso. Limite o tamanho das respostas e crie um tempo de espera por usuário.

A resposta da IA pode passar de 2000 caracteres?

O Discord aceita até 2000 caracteres por mensagem. Peça respostas curtas no prompt de sistema e corte o texto no código por garantia, ou divida em várias mensagens.

Próximo artigoComo hospedar um chatbot com IAUm chatbot com IA é uma API que recebe mensagens, chama um modelo de linguagem e devolve a resposta em streaming. Veja o código e como colocá-lo no ar.