Shareuhack | Telegram Bot + AI 圖片辨識:手把手打造自動化回饋分類系統
Telegram Bot + AI 圖片辨識:手把手打造自動化回饋分類系統

Telegram Bot + AI 圖片辨識:手把手打造自動化回饋分類系統

發布於 February 14, 2026·更新於 March 7, 2026
LunaMiaEno
撰寫Luna·研究Mia·審查Eno·持續更新·12 分鐘閱讀

Telegram Bot + AI 圖片辨識:手把手打造自動化回饋分類系統

你的產品回饋散落在 Slack 私訊、Line 群組、Email 附件裡,最有價值的往往是一張截圖:一個錯誤畫面、一個 UI 跑版、一個競品的亮點功能。但把圖片從手機搬到 Jira、寫描述、貼標籤,這套流程的摩擦力大到讓一半的回饋直接死在「收藏訊息」裡。

我自己的 side project 就遇過這個問題,每週大約有 10-15 則截圖回饋埋在聊天記錄裡,等我想起來要處理時往往已經過了好幾天。後來我用 Telegram Bot 搭配 LLM Vision API 做了一套自動分類系統,從收到截圖到產出結構化分類結果,全程不需要手動操作。本文從零開始,帶你建立完整的系統。

TL;DR

  • 5 分鐘內透過 BotFather 建立 bot、取得 token 和 chat ID
  • Python 透過 getUpdates API 抓取訊息,提取文字和圖片
  • 送圖片給 LLM Vision API(GPT-4oClaude)自動分類 bug/feature
  • 搭配 cron job 定時排程,實現全自動化
  • 個人用量幾乎零成本

為什麼選 Telegram Bot?

在各種回饋收集方案中,Telegram Bot 的優勢非常明顯:

方案設定難度費用手機 UX圖片支援
Telegram Bot低(5 分鐘)免費原生 App 轉傳原生支援
Slack Bot免費/付費尚可支援
Google Forms免費瀏覽器上傳麻煩
自建 App依架構需開發需開發

Telegram Bot API 是純 HTTP API,不需要安裝任何 SDK,用 curl 或任何語言的 HTTP client 都能呼叫。建立 bot 不需要審核、不需要上架,幾分鐘內就能開始收訊息。

Step 1:建立 Telegram Bot 並取得 Token

  1. 打開 Telegram,搜尋 @BotFather(認明藍色勾勾)
  2. 發送 /newbot
  3. 依照提示輸入 bot 名稱(顯示名稱)和 username(必須以 bot 結尾,例如 my_triage_bot
  4. BotFather 會回傳你的 API Token,格式類似:123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11

重要:Token 等同於 bot 的密碼,任何拿到 token 的人都能完全控制你的 bot。絕對不要把 token commit 到 git,使用環境變數存放。

建立 .env 檔案存放 token:

# .env
TELEGRAM_BOT_TOKEN=123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11

Python 中讀取:

import os
from dotenv import load_dotenv

load_dotenv()
BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")

Step 2:取得 Chat ID

你需要知道 bot 要監聽哪個聊天室。最簡單的方式:

  1. 在 Telegram 中對你的 bot 發送一則任意訊息(例如 hello
  2. 在瀏覽器打開以下網址(替換你的 token):
https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates
  1. 你會看到類似這樣的 JSON 回應:
{
  "ok": true,
  "result": [
    {
      "update_id": 123456789,
      "message": {
        "message_id": 1,
        "from": {
          "id": 987654321,
          "first_name": "Alex",
          "is_bot": false
        },
        "chat": {
          "id": 987654321,
          "type": "private"
        },
        "text": "hello"
      }
    }
  ]
}

chat.id 就是你要的值。私人聊天是正數,群組的 chat ID 是負數(例如 -1001234567890)。

群組使用注意事項

如果你想讓 bot 監聽群組訊息,有個關鍵的隱私設定要處理:

根據 Telegram 官方文件,bot 預設開啟 Privacy Mode,在群組中只能看到以下訊息:

  • 明確指向該 bot 的 /command 指令
  • 一般指令(若 bot 是群組中最後發送訊息的 bot)
  • 透過該 bot 發送的 inline 訊息
  • 回覆給該 bot 的訊息

要讓 bot 看到群組中的所有訊息,你有兩個選擇:

  1. 關閉 Privacy Mode:在 BotFather 中設定 → Bot Settings → Group Privacy → Turn Off。關閉後需要將 bot 移除再重新加入群組才會生效。
  2. 設為管理員:將 bot 設為群組管理員,管理員可以接收所有訊息。

Step 3:擷取訊息:文字、圖片、寄件者

getUpdates API 是 Telegram Bot 最核心的方法。它回傳自上次請求以來的所有新訊息。

關鍵參數:

參數型別說明
offsetInteger從哪個 update_id 開始回傳。設為上次最大 update_id + 1 可避免重複
limitInteger回傳數量,1-100,預設 100
timeoutIntegerLong polling 等待秒數,預設 0(立即回傳)。設為正整數即啟用 long polling

關鍵限制:getUpdates 只保留最近 24 小時的資料。 如果你超過 24 小時沒有 poll,之前的訊息就永久消失了。這是選擇 cron job 排程間隔時最重要的考量。

以下是擷取訊息的 Python function:

import requests

def fetch_new_messages(token, offset=None):
    """從 Telegram 取得新訊息,回傳訊息列表和新的 offset"""
    url = f"https://api.telegram.org/bot{token}/getUpdates"
    params = {"timeout": 10, "limit": 100}
    if offset:
        params["offset"] = offset

    resp = requests.get(url, params=params).json()
    if not resp.get("ok"):
        print(f"Error: {resp}")
        return [], offset

    messages = []
    new_offset = offset

    for update in resp["result"]:
        new_offset = update["update_id"] + 1
        msg = update.get("message", {})
        messages.append({
            "update_id": update["update_id"],
            "chat_id": msg.get("chat", {}).get("id"),
            "from": msg.get("from", {}).get("first_name", "Unknown"),
            "text": msg.get("text"),
            "caption": msg.get("caption"),
            "photo_file_id": msg["photo"][-1]["file_id"] if msg.get("photo") else None,
        })

    return messages, new_offset

幾個重點:

  • msg["photo"] 是一個陣列,實務上按解析度由低到高排列(官方文件未明確保證排序,但這是公認的行為),取最後一個元素就是最高解析度
  • caption 是圖片附帶的文字說明,有些使用者會在截圖時順手打上描述
  • offset 必須持續更新,否則每次都會收到重複訊息

Step 4:下載圖片

取得 file_id 後,需要兩步驟下載實際圖片:

  1. 呼叫 getFile API 取得 file_path
  2. file_path 組成下載 URL
import base64

def download_photo(token, file_id):
    """下載 Telegram 圖片,回傳 base64 編碼字串"""
    # Step 1: 取得 file_path
    file_info = requests.get(
        f"https://api.telegram.org/bot{token}/getFile",
        params={"file_id": file_id}
    ).json()

    file_path = file_info["result"]["file_path"]

    # Step 2: 下載圖片
    download_url = f"https://api.telegram.org/file/bot{token}/{file_path}"
    image_data = requests.get(download_url).content

    return base64.b64encode(image_data).decode("utf-8")

注意:根據 Telegram Bot API 文件,下載連結保證至少有效 1 小時,檔案大小上限 20MB。

Step 5:LLM Vision API 分類回饋

這是整套系統的核心。我們把截圖送給具備視覺能力的 LLM,讓它自動判斷這是 Bug、Feature Request 還是 UX 改進。

模型選擇

任何支援 Vision(圖片輸入)的 LLM 都能勝任這個任務。推薦選項:

  • Google Gemini唯一提供免費方案的選擇,每日請求次數依模型而異(詳見官方定價頁),適合零成本入門
  • GPT-4o:生態系成熟,文件範例多,上手最快
  • Claude Haiku 4.5:付費方案中成本最低,適合大量分類

對於回饋分類這種相對簡單的任務,任一模型都綽綽有餘。想零成本測試的話,建議先用 Gemini API 免費方案。以下範例以 GPT-4o 為主,切換成 Claude 只需改幾行程式碼(後面會示範)。

使用 OpenAI GPT-4o 的範例

from openai import OpenAI

client = OpenAI()  # 從環境變數 OPENAI_API_KEY 讀取

SYSTEM_PROMPT = """你是一個產品回饋分類助手。分析使用者提供的截圖和文字描述,
判斷這是哪種類型的回饋,並以 JSON 格式輸出。

分類類型:
- bug: 明確的錯誤、異常行為、crash
- feature: 新功能需求或改進建議
- ux: UI/UX 體驗問題(不是 bug,但用起來不順)
- question: 使用疑問,不屬於以上類別

輸出格式:
{
  "type": "bug|feature|ux|question",
  "priority": "P1|P2|P3",
  "summary": "一句話摘要",
  "description": "詳細描述,包含從截圖中提取的關鍵資訊",
  "components": ["涉及的功能模組"]
}"""

def classify_feedback(image_base64, text=None):
    """用 GPT-4o Vision 分類回饋"""
    content = []

    if text:
        content.append({"type": "text", "text": f"使用者描述:{text}"})

    if image_base64:
        content.append({
            "type": "image_url",
            "image_url": {
                "url": f"data:image/jpeg;base64,{image_base64}",
                "detail": "high"
            }
        })

    response = client.chat.completions.create(
        model="gpt-4o",
        response_format={"type": "json_object"},
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": content}
        ],
        max_tokens=500
    )

    return response.choices[0].message.content

使用 Claude 的範例

import anthropic

client = anthropic.Anthropic()  # 從環境變數 ANTHROPIC_API_KEY 讀取

def classify_feedback_claude(image_base64, text=None):
    """用 Claude Vision 分類回饋"""
    content = []

    if image_base64:
        content.append({
            "type": "image",
            "source": {
                "type": "base64",
                "media_type": "image/jpeg",
                "data": image_base64
            }
        })

    user_text = "請分析這張截圖並分類這個回饋。"
    if text:
        user_text += f"\n使用者描述:{text}"
    content.append({"type": "text", "text": user_text})

    response = client.messages.create(
        model="claude-haiku-4-5-20251001",
        max_tokens=500,
        system=SYSTEM_PROMPT,
        messages=[{"role": "user", "content": content}]
    )

    return response.content[0].text

結構化輸出範例

{
  "type": "bug",
  "priority": "P2",
  "summary": "Mobile 登入頁面 SSO 流程出現 404",
  "description": "截圖顯示使用者在手機版網頁進行 SSO 登入時,重新導向後出現 404 錯誤頁面。錯誤文字為 'Service Unavailable'。影響所有透過 Google SSO 登入的手機版使用者。",
  "components": ["Auth", "Mobile Web"]
}

Step 6:完整 Polling 腳本

把前面所有 function 組合起來,就是一個可以直接執行的完整腳本:

#!/usr/bin/env python3
"""triage_bot.py - Telegram 回饋自動分類腳本"""

import json
import os
import sys
import base64
import requests
from pathlib import Path
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()

BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
TARGET_CHAT_ID = int(os.getenv("TELEGRAM_CHAT_ID", "0"))
OFFSET_FILE = Path("offset.txt")

openai_client = OpenAI()

# === SYSTEM_PROMPT(同 Step 5)===
SYSTEM_PROMPT = """你是一個產品回饋分類助手。分析使用者提供的截圖和文字描述,
判斷這是哪種類型的回饋,並以 JSON 格式輸出。

分類類型:
- bug: 明確的錯誤、異常行為、crash
- feature: 新功能需求或改進建議
- ux: UI/UX 體驗問題
- question: 使用疑問

輸出格式:
{"type": "bug|feature|ux|question", "priority": "P1|P2|P3",
 "summary": "一句話摘要", "description": "詳細描述",
 "components": ["功能模組"]}"""


def load_offset():
    if OFFSET_FILE.exists():
        return int(OFFSET_FILE.read_text().strip())
    return None


def save_offset(offset):
    OFFSET_FILE.write_text(str(offset))


def fetch_new_messages(token, offset=None):
    url = f"https://api.telegram.org/bot{token}/getUpdates"
    params = {"timeout": 10, "limit": 100}
    if offset:
        params["offset"] = offset
    resp = requests.get(url, params=params).json()
    if not resp.get("ok"):
        print(f"[ERROR] getUpdates failed: {resp}", file=sys.stderr)
        return [], offset
    messages, new_offset = [], offset
    for update in resp["result"]:
        new_offset = update["update_id"] + 1
        msg = update.get("message", {})
        if msg.get("chat", {}).get("id") != TARGET_CHAT_ID:
            continue
        messages.append({
            "from": msg.get("from", {}).get("first_name", "Unknown"),
            "text": msg.get("text"),
            "caption": msg.get("caption"),
            "photo_file_id": msg["photo"][-1]["file_id"] if msg.get("photo") else None,
        })
    return messages, new_offset


def download_photo(token, file_id):
    file_info = requests.get(
        f"https://api.telegram.org/bot{token}/getFile",
        params={"file_id": file_id}
    ).json()
    file_path = file_info["result"]["file_path"]
    image_data = requests.get(
        f"https://api.telegram.org/file/bot{token}/{file_path}"
    ).content
    return base64.b64encode(image_data).decode("utf-8")


def classify_feedback(image_base64=None, text=None):
    content = []
    if text:
        content.append({"type": "text", "text": f"使用者描述:{text}"})
    if image_base64:
        content.append({
            "type": "image_url",
            "image_url": {"url": f"data:image/jpeg;base64,{image_base64}", "detail": "high"}
        })
    if not content:
        return None
    response = openai_client.chat.completions.create(
        model="gpt-4o",
        response_format={"type": "json_object"},
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": content}
        ],
        max_tokens=500
    )
    return json.loads(response.choices[0].message.content)


def main():
    offset = load_offset()
    messages, new_offset = fetch_new_messages(BOT_TOKEN, offset)
    save_offset(new_offset)

    if not messages:
        print("[INFO] No new messages.")
        return

    print(f"[INFO] Processing {len(messages)} new message(s)...")

    for msg in messages:
        image_b64 = None
        if msg["photo_file_id"]:
            image_b64 = download_photo(BOT_TOKEN, msg["photo_file_id"])

        text = msg["text"] or msg["caption"]
        result = classify_feedback(image_b64, text)

        if result:
            print(f"\n--- Feedback from {msg['from']} ---")
            print(json.dumps(result, indent=2, ensure_ascii=False))
        else:
            print(f"[SKIP] Empty message from {msg['from']}")


if __name__ == "__main__":
    main()

使用方式

# 安裝依賴
pip install requests python-dotenv openai

# 設定環境變數(.env 檔案)
TELEGRAM_BOT_TOKEN=your_token_here
TELEGRAM_CHAT_ID=your_chat_id_here
OPENAI_API_KEY=your_openai_key_here

# 執行
python triage_bot.py

腳本會把 offset 存在 offset.txt,每次執行只處理上次之後的新訊息。

Step 7:排程自動化 Cron Job

手動執行腳本很適合測試,但實際使用時你需要自動排程。

為什麼用 Cron 而不是 Webhook?

比較Cron + PollingWebhook
需要公開伺服器不需要需要(HTTPS + 有效 SSL)
設定難度低(一行 crontab)中(DNS + SSL + 伺服器)
即時性依 cron 間隔即時
適合場景個人/小團隊高流量生產環境

注意:Polling(getUpdates)和 Webhook 是互斥的。如果你設定了 Webhook,getUpdates 將無法正常運作。要切換回 polling,需先呼叫 deleteWebhook API。

設定 Crontab

# 開啟 crontab 編輯器
crontab -e

# 每 2 小時執行一次(建議間隔,遠低於 24 小時保留限制)
0 */2 * * * cd /path/to/your/project && /usr/bin/python3 triage_bot.py >> triage.log 2>&1

cron 間隔的關鍵考量:因為 getUpdates 只保留 24 小時的資料,你的 cron 間隔必須遠低於 24 小時。建議設在 2-4 小時,留足緩衝。即使某次執行失敗,下一次仍有時間在資料過期前補撈。

macOS 使用者

macOS 上 cron 可能因為權限問題不穩定,建議改用 launchd。將以下 plist 存為 ~/Library/LaunchAgents/com.triage-bot.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.triage-bot</string>
  <key>ProgramArguments</key>
  <array>
    <string>/usr/bin/python3</string>
    <string>/path/to/your/project/triage_bot.py</string>
  </array>
  <key>WorkingDirectory</key>
  <string>/path/to/your/project</string>
  <key>StartInterval</key>
  <integer>7200</integer>
  <key>StandardOutPath</key>
  <string>/path/to/your/project/triage.log</string>
  <key>StandardErrorPath</key>
  <string>/path/to/your/project/triage-error.log</string>
</dict>
</plist>

載入排程:

launchctl load ~/Library/LaunchAgents/com.triage-bot.plist

低代碼替代方案:n8n

如果你偏好視覺化的工作流,n8n 提供現成的 Telegram Trigger 節點,可以直接拖拉設定,不需要寫程式碼。

延伸應用

這套系統的核心是「訊息擷取 + AI 分類」,分類結果出來後,你可以接上各種下游動作:

  • 自動建立 Jira / Linear ticket:呼叫對應的 REST API,將分類結果直接轉為工單
  • 回傳分類結果到 Telegram:用 sendMessage API 讓 bot 在群組內回覆分類摘要,方便團隊即時查看
  • 存入 Google Sheets / Notion 資料庫:用 API 寫入結構化資料,建立可搜尋的回饋歷史
  • 加入 Webhook 模式:當你有公開伺服器時,切換到 Webhook 實現即時處理

風險與限制

Token 安全

Bot Token 等同 bot 的完整控制權。必須存在環境變數或 secrets manager 中,絕不要 hardcode 在程式碼裡。若懷疑洩漏,立即到 BotFather 使用 /revoke 重新產生。

24 小時資料保留

getUpdates 的資料只保留 24 小時。如果你的 cron job 因為任何原因停止超過一天(伺服器重啟、路徑錯誤、權限問題),這段時間的回饋會永久遺失。建議加入簡單的健康檢查機制,例如每次執行時寫入 timestamp,外部監控檢查 timestamp 是否過期。

LLM 分類的準確度

Vision API 的分類不是 100% 準確。根據實際使用經驗,對於明確的 bug 截圖(有錯誤訊息)準確率很高,但對於模糊的 UX 問題或 feature request 邊界,AI 的判斷可能和你的預期不同。高優先級的回饋建議人工複核。

規模化成本

低用量(每天 10-20 張截圖)幾乎免費。但回饋量大時成本會線性增長,因為每張圖片都需要一次 API 呼叫。建議從免費額度或最低方案開始,觀察實際用量後再決定模型。各家 LLM 的 Vision API 定價可參考 OpenAI PricingAnthropic Pricing

隱私考量

使用者截圖可能包含個人資訊(姓名、Email、甚至密碼)。這些圖片會被傳送到第三方 LLM API 進行分析。如果你的團隊對資料隱私有嚴格要求,需要評估這是否符合你的資料處理政策。

Telegram Rate Limits

根據 Telegram 官方 FAQ,rate limit 有分層結構:全域約 30 msg/sec(對不同聊天),同一聊天約 1 msg/sec,同一群組 20 msg/min。對於 getUpdates polling 來說通常不會觸及限制,但如果你的 bot 需要大量回覆訊息,需注意這些限制。

結語

這套系統的美在於簡單:一個 Python 檔、一行 crontab、一把 API key。沒有框架、沒有部署、沒有持續運行的伺服器。

建議從 Step 1-3 開始:建好 bot、拿到 chat ID、確認能抓到訊息。這三步完成後,你就有了一個可運作的訊息擷取管線。之後再疊加 LLM 分類和 cron job,最後接上 Jira 或任何你習慣的工具。

一旦你體會到「轉傳截圖就自動分類」的便利,你會開始想把更多工作流接上去。

FAQ

getUpdates 和 Webhook 可以同時使用嗎?

不行。Telegram Bot API 中 getUpdates(polling)和 Webhook 是互斥的。設定了 Webhook 後 getUpdates 將無法正常運作。要切換回 polling,需先呼叫 deleteWebhook API 移除 Webhook。

超過 24 小時沒有 poll,訊息會怎樣?

會永久遺失。Telegram 的 getUpdates API 只保留最近 24 小時內的未確認更新。建議 cron 間隔設在 4 小時以內,並加入錯誤通知機制確保排程沒有中斷。

LLM Vision API 的費用大概多少?

Google Gemini API 提供免費方案(每日請求額度依模型不同,詳見官方定價頁),個人用量通常足夠。OpenAI 和 Anthropic 的 Vision API 則需要付費,但低用量(每天幾十張截圖)月費很低。大量使用時成本會線性增長,因為每張圖片都需要一次 API 呼叫。建議先用 Gemini 免費方案測試,確認流程後再決定是否切換。

這套系統適合用在正式的客服場景嗎?

適合作為內部團隊的快速分類工具,但不建議直接面對外部客戶。原因包括:缺乏訊息佇列的可靠性保證、無法處理高並發、LLM 分類可能出錯。正式客服場景建議搭配專用 ticket 系統(如 Zendesk)並加入人工審核環節。

這篇文章對你有幫助嗎?