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.

SintomaCausaCorreçãoUnknowninteractionPassou de 3 ssem respostadeferReply()no inícioErro 401Chave ausenteno servidorConfigure avariávelErro 404(modelo)ID errado ouaposentadoTroque o IDna variávelInvalidForm BodyTexto acima de2000 caracteresCorte comslice(0, 2000)
Cada erro tem uma causa típica, e a correção cabe numa linha de código ou numa variável de ambiente.

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, ele para e não volta sozinho: você o inicia de novo, de preferência com mais RAM.

  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

API compatível com OpenAI

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.