---
# System prepended metadata

title: 在Discord中實做本地運行的AI聊天機器人 (discord.py + Ollama)
tags: [Bot, LLM, Discord, local]

---

# 在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` 可以終止交談。
