# 在Discord中實做本地運行的AI聊天機器人 (discord.py + Ollama)
# 專案概述
起初是想測試 `ollama` 在本地運行的效果,並且把它嵌入某個實際的應用中使用,於是便產生了製作AI聊天機器人的想法。也順帶出現了這個保母級的人人都能照著打的 AI Bot 筆記。
專案里主要透過 `discord.py` 庫創建了一個可以使用本地語言模型交互的AI聊天機器人,並且支持 cogs 動態模塊加載功能。
**實作方向**
**使用技術與庫**
* Python : 作為主要開發語言
* discord.py : 用來與 Discord API 互動,創建機器人。
* Ollama : 本地大型語言模型 (LLM) 的運行庫。
* asyncio : 異步函數,確保 bot 的運行順暢,不被阻塞。
* cogs : 把功能獨立成模塊加載,模組化程式架構。
---
# 環境配置
### 1. 安裝所需的庫
```
pip install discord.py ollama asyncio
```
### 2. 到Discord Developer Portal 註冊一個應用
1. 進入 [Discord Developer Portal](https://discord.com/developers/applications)
2. 點擊 New application 新增應用
3. 把 token 複製下來保存至專案目錄 (可以創一個 `Bot` 的資料夾然後放裡面存成 `token.txt` )
### 3. 準備Ollama
* Windows用戶的話 [Ollama](https://ollama.com) 下載Ollama客戶端,載完後執行,按 `win + r` 輸入 `cmd`
打開終端運行 `ollama run llama3.2` (會使用 llama3.2 的 3B 模型)
* 如果是用 wsl 的 ubuntu 或 ubuntu 的環境的話在終端運行 `curl -fsSL https://ollama.com/install.sh | sh` 會取得 Ollama 的 Linux 版本。
---
# 程式碼介紹
## 1. `bot.py` - Discord Bot 主程式
:::info
可以先把機器人邀入你測試用的伺服器。
:::
### 1. 首先載入程式所需的庫
```python=
import os
import discord
from discord.ext import commands
import asyncio
from ollama import chat
```
* `os` 用於讀取 `cogs` 內的檔案
* 從 `ollama` 載入 `chat` 模組
### 2. 設定 Bot 的 intensions 和設定指令前綴
`Intents` 是拿來規範 Bot 可以接收哪些事件的,這裡設為 `default()` 用預設的即可,怕麻煩開發的過程中可以用 `all()`。這裡 `intents.message = True` 是多餘的因為這個 `intents` 已經包含在 `default()` 裡面了。
但我還是這樣寫就:)
然後建立一對象來讓我們在這個專案中觸發和使用指令,如 : `bot = commands.Bot(command_prefix="!", intents=intents)` ,`command_prefix` 可以依喜好調整,只是之後命令的觸發前綴而已。
```python=
intents = discord.Intents.default()
intents.messages = True
intents.message_content = True
bot = commands.Bot(command_prefix="!", intents=intents)
```
然後把 token 從 `token.txt` 讀出來
```python=
with open('Bot/token.txt', 'r') as file:
token = file.read().strip()
```
`'r'` 是讀取模式 read ,亦有`'rb'`以二進制讀取、`'w'`寫入等模式 , `strip()` 就是去除換行符之類的東西只保留文字 (`"string"`) 。
### 3. `asyncio` 是一個非同步函式的庫。接下來就來處理程式加載模塊,和傳送登入資訊等。
為什麼是非同步呢,因為程式需要在使用者給出指令時或是在一開始執行時告訴你Bot的登入資訊,這些都不會是像傳統的程式一樣由上到下慢慢執行,一行一行執行,而是各自條件被滿足時就動作,這時候就需要非同步函式 `asyncio`。
當機器人成功連線並準備就緒時,傳送登入資訊,如 :
```python=
@bot.event
async def on_ready():
print(f"Logged in as {bot.user}")
```
機器人需要支援 動態載入擴展功能,因此我們可以設計指令來 加載、卸載、重新加載 Cogs 模組。
* 加載
```python=
@bot.command()
async def load(ctx, extension):
await bot.load_extensions(f"cogs.{extension}")
await ctx.send(f"Loaded{ctx} done.")
```
* 卸載
```python=
@bot.command()
async def unload(ctx, extension):
await bot.unload_extension(f"cogs.{extension}")
await ctx.send(f"Unloaded {extension} done.")
```
* 重新加載
```python=
@bot.command()
async def reload(ctx, extension):
await bot.reload_extension(f"cogs.{extension}")
await ctx.send(f"Reloaded {extension} done.")
```
* 直接加載所有模塊
```python=
@bot.command()
async def reload(ctx, extension):
await bot.reload_extension(f"cogs.{extension}")
await ctx.send(f"Reloaded {extension} done.")
```
### 4. 接下來是主程式
```python=
async def main():
async with bot:
await load_extensions()
await bot.start(token)
```
加載模塊,跟啟動 Bot
```python=
if __name__ == "__main__":
asyncio.run(main())
```
`bot.py` 完整程式 :
:::spoiler 打開 `bot.py`
```python=
import os
import discord
from discord.ext import commands
import asyncio
from ollama import chat
# 設定intentions,亦須去developer protal內依需求調整
intents = discord.Intents.default()
intents.messages = True
intents.message_content = True
bot = commands.Bot(command_prefix="!", intents=intents)
# 讀取token
with open('Bot/token.txt', 'r') as file:
token = file.read().strip()
# 成功登入時顯示機器人登入資訊
@bot.event
async def on_ready():
print(f"Logged in as {bot.user}")
# 加載模塊指令
@bot.command()
async def load(ctx, extension):
await bot.load_extensions(f"cogs.{extension}")
await ctx.send(f"Loaded{ctx} done.")
# 卸載模塊指令
@bot.command()
async def unload(ctx, extension):
await bot.unload_extension(f"cogs.{extension}")
await ctx.send(f"Unloaded {extension} done.")
# 重新加載模塊
@bot.command()
async def reload(ctx, extension):
await bot.reload_extension(f"cogs.{extension}")
await ctx.send(f"Reloaded {extension} done.")
# 加載所有模塊
async def load_extensions():
for filename in os.listdir("./cogs"):
if filename.endswith(".py"):
await bot.load_extension(f"cogs.{filename[:-3]}")
# 主程式運行
async def main():
async with bot:
await load_extensions()
await bot.start(token)
if __name__ == "__main__":
asyncio.run(main())
```
:::
:::info
在你的專案目錄下創建 `cogs` 資料夾,把接下來寫的程式都放在裡面。
:::
## 2. `main.py` - 基本拓展模塊的範例
我寫累了,cogs 就差不多長得和底下範例一樣,就看看就好。
`main.py` 完整程式 :
:::spoiler 打開 `main.py`
```python=
# cogs/main.py
import discord
from discord.ext import commands
class Main(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
# Hello指令
@commands.command()
async def hello(self, ctx: commands.Context):
await ctx.send("Hello, world!")
# 監聽用戶輸入,若用戶輸入Hello則觸發Hello指令,回覆"Hello World!"
@commands.Cog.listener()
async def on_message(self, message: discord.Message):
if message.author == self.bot.user:
return
if message.content == "Hello":
await message.channel.send("Hello, world!")
await self.bot.process_commands(message)
# 註冊Cog模塊到Bot
# 這個方法是用來將 Cog 加載到主程式的
async def setup(bot: commands.Bot):
# 使用 bot.add_cog() 方法將 Main Cog 加入 bot 中
await bot.add_cog(Main(bot))
```
:::
:::info
如果現在運行 `bot.py` ,Discord對話框輸入Hello的話機器人應該要回覆 `Hello World!`
:::
## 3. `llamaChat.py` - AI聊天模塊
如果整合 ollama 進來,如下所示,我是希望啟動聊天模式後就能和ChatGPT一樣一直說話,所以就變成了下面的樣子。
`llamaChat.py` 完整程式 :
:::spoiler 打開 `llamaChat.py`
```python=
import discord
from discord.ext import commands
from ollama import chat
# 定義llamaChat Cog,這個Cog負責處理與Ollama模型進行對話的功能
class llamaChat(commands.Cog):
def __init__(self, bot: commands.Bot):
# 初始化時,將bot實例傳遞給Cog,並設置用戶聊天模式與聊天歷史
self.bot = bot
self.chat_mode_activate = {} # 存儲每個用戶是否啟用了聊天模式
self.chat_history = {} # 存儲每個用戶的聊天歷史
self.model_name = "llama3.2" # 設置使用的Ollama語言模型
# 監聽每個消息事件,根據消息內容執行相應的操作
@commands.Cog.listener()
async def on_message(self, message: discord.Message):
# 如果消息來自機器人自己,則不處理
if message.author == self.bot.user:
return
user_id = str(message.author.id) # 用戶的唯一ID,作為識別用戶的標識
# 如果消息內容是 "!chat",啟動聊天模式並初始化聊天歷史
if message.content == "!chat":
self.chat_mode_activate[user_id] = True
self.chat_history[user_id] = [] # 清空過去的聊天記錄
await message.channel.send("llama family activated.") # 回覆用戶已啟動聊天模式
# 如果消息內容是 "!terminate",終止聊天模式
elif message.content == "!terminate":
self.chat_mode_activate[user_id] = False
await message.channel.send("llama family terminated.") # 回覆用戶已終止聊天模式
# 如果用戶啟用了聊天模式,開始進行對話
elif self.chat_mode_activate.get(user_id, False):
# 如果用戶沒有聊天歷史,初始化空的聊天歷史
if user_id not in self.chat_history:
self.chat_history[user_id] = []
# 將用戶發送的消息添加到聊天歷史中
self.chat_history[user_id].append({
"role": "user", # 標記為用戶消息
"content": f"{message.author.name}: {message.content}",
})
response_content = "" # 用來存儲來自模型的回應
try:
# 發送聊天歷史到Ollama模型進行回應生成
for part in chat(model=self.model_name, messages=self.chat_history[user_id], stream=True):
# 逐步拼接模型回應內容
response_content += part["message"]["content"]
# 將模型回應加入聊天歷史
self.chat_history[user_id].append({
"role": "assistant", # 標記為助手(模型)消息
"content": response_content,
})
# 將模型的回應發送給用戶
await message.channel.send(response_content)
except Exception as e:
# 若有錯誤發生,回傳錯誤訊息
await message.channel.send(f"Error: {str(e)}")
# 繼續處理其他指令
await self.bot.process_commands(message)
# 註冊Cog模塊到Bot
# 這個方法會在加載Cog時自動執行
async def setup(bot: commands.Bot):
# 將llamaChat Cog加載到機器人
await bot.add_cog(llamaChat(bot))
```
:::
:::info
完成!
:::
# 使用方法
* 運行 `bot.py` 沒有報錯的話那就可以正常運行了。
* 輸入 `!chat` 可以開始和 AI 交談 (你一直說他就會一直回)
* 輸入 `!terminate` 可以終止交談。