中文 ▾
获取 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 兼容,因此可以在此处使用。小型字典用于保存对话历史。你不需要数据库即可开始。

你需要 Python 3.9 或更高版本、可以创建应用的 Discord 账户,以及来自 API 密钥页面的 API 密钥。新账户只需一键即可领取 $0.50 的试用额度,足以构建和测试此机器人。

步骤 1:创建 Discord 应用

  1. 打开 Discord 开发者门户并创建新应用。
  2. 在机器人部分,创建机器人用户并复制其令牌。将其视为密码。
  3. 在同一页面上,在特权网关意图下开启 消息内容意图。如果没有它,机器人接收到的消息文本为空,你会花一个小时困惑为什么它从不回复。
  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 个 token 的上下文窗口,你的成本会随每条消息增加。12 条记录足以进行关于一个错误的对话。对于拥有长线程的服务器,可以提高该值或总结较旧的对话。

Discord 的 2,000 字符限制

Discord 拒绝超过 2,000 个字符的消息,而代码回复很容易达到该限制。split_reply 辅助函数在接近限制时的换行符处截断。它不了解代码围栏,因此代码块可能会被截断。如果这让你困扰,请添加逻辑,在一个部分的末尾关闭围栏,并在下一部分的开头重新打开。

用于并发的信号量

完整密钥允许 8 个并发请求,试用密钥允许 2 个。繁忙的服务器可能同时产生多个请求。信号量在你的进程内对额外调用进行排队,而不是发送它们并收到带有错误代码 concurrency 的 429 错误。如果你仍在使用试用密钥,将 Semaphore(6) 改为 Semaphore(2)。

错误不计费

失败或拒绝的请求不会计费,因此错误后的重试不会使成本翻倍。代码在失败时从历史记录中移除用户输入,因此重试不会留下模型已经看到两次的悬空问题。

提升回答体验

一旦基本循环工作,一些小的更改可以极大地改善它。

  • 流式输出答案。 传入 stream=True 并每隔约一秒用已生成的文本更新机器人的消息。不要在每个 token 时都更新,因为 Discord 对消息编辑应用了自己的速率限制。流式请求会占用并发槽位直到结束,因此要让信号量包住整个流。
  • 使用斜杠命令。 类似 /ask 的命令避免了提及解析,完全不需要消息内容意图,如果机器人扩展到超过少量服务器,这也使机器人更容易验证。
  • 按用户的历史记录。 如果多个人在一个频道中询问不相关的问题,请以频道和用户为键存储历史记录。
  • 调整系统提示词。 命名你的社区使用的语言,要求简短回答,并告诉模型在问题模糊时提出澄清问题。提示词更改是免费的,通常比模型更改更有效。
  • 降低温度。 对于代码,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 万输入 token $0.15,每 100 万输出 token $0.50,无订阅费。以一个典型交互为例:系统提示词加上十二条历史记录大约构成 2,500 个输入 token,而回答大约为 400 个 token。

输入: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 端完成,小型虚拟机或容器主机即可胜任。在进程管理器下运行它,以便在崩溃后重启,并将两个 token 保存在环境变量或密钥存储中,切勿放在仓库中。

考虑你的服务器允许的内容。该模型经过调整,可在合法成人用途下不进行内容拒绝,因此它会讨论安全测试或边缘小说,而更严格的助手可能会拒绝这些内容。如果你的社区包含未成年人,请在系统提示词中设置规则,将机器人限制在特定频道,并记住 Discord 自身的条款也适用于你的机器人。此 API 禁止任何涉及未成年人的性内容,无例外。

完整的参数详情,如 stop、seed 和函数调用,请参阅 API 文档。如果你希望机器人运行代码或读取仓库,函数调用是途径,由你决定模型可以请求哪些函数。针对该模型的编程用途,请参阅关于 无审查 AI 编程模型 的页面。

问答

我需要消息内容意图吗?

是的,如果机器人读取普通消息和提及。如果你仅使用斜杠命令,则不需要。

机器人可以流式输出回复吗?

可以。设置 stream=True 并以平稳的间隔编辑 Discord 消息,例如每秒一次,以便你保持在 Discord 自身的限制范围内。

如果太多人同时提问会发生什么?

当完整密钥的并发请求超过 8 个时,API 返回 HTTP 429 状态码及 concurrency 错误代码。代码中的信号量会将多余的请求排队。

重启后机器人会记住对话吗?

使用此处使用的字典则不会。如果需要重启后保留历史记录,请将其存储在 SQLite 或 Redis 中。

API 密钥在 Discord 机器人中安全吗?

如果它保留在你服务器上的环境变量中,则是安全的。切勿将其发布到频道或提交到公共仓库。

只差一张表单,即可获得密钥

创建账户,复制密钥,更改基础 URL。这就是全部设置。

获取 API 密钥