Discord 编程 AI 机器人:使用 LLM API 构建
Discord 机器人编程 AI 在你的服务器内回答编程问题,通过 API 背后的语言模型实现。本教程用约 70 行 Python 代码构建一个机器人,并涵盖通常容易出问题的部分。
更新于

要点
- 机器人需要 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 应用
- 打开 Discord 开发者门户并创建新应用。
- 在机器人部分,创建机器人用户并复制其令牌。将其视为密码。
- 在同一页面上,在特权网关意图下开启 消息内容意图。如果没有它,机器人接收到的消息文本为空,你会花一个小时困惑为什么它从不回复。
- 在 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 之间的值比默认值提供更稳定的结果。
常见故障排除
当新机器人保持静默时,原因几乎总是下面的一项。按此顺序检查。
- 机器人在线但从不回复。 开发者门户中的消息内容意图已关闭,或者代码中缺少
intents.message_content = True。两者都必须设置。 - 它仅在某些频道中回复。 机器人缺少在其他频道中读取或发送消息的权限。检查频道的权限覆盖,而不仅仅是服务器范围的角色。
- 每条回复都是“忙”消息。 你可能遇到了 429 错误。打印一次异常体。如果代码显示
rate_limited,说明你超过了完整密钥每分钟 300 次请求或试用密钥每分钟 60 次请求的上限。如果显示concurrency,请减小信号量大小。 - 第一次调用时出现身份验证错误。 运行机器人的进程的环境变量中缺少 LLM 密钥。在一个终端选项卡中导出的密钥在别处启动的服务中不可见。
- 答案被截断。 示例中的
max_tokens=900会截断长回复。提高它,或者移除它,因为max_tokens是可选的,补全可以使用窗口的剩余部分。 - 机器人回复自己的消息。 如果省略了
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 密钥