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-4o 或 Claude)自動分類 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
- 打開 Telegram,搜尋 @BotFather(認明藍色勾勾)
- 發送
/newbot - 依照提示輸入 bot 名稱(顯示名稱)和 username(必須以
bot結尾,例如my_triage_bot) - 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 要監聽哪個聊天室。最簡單的方式:
- 在 Telegram 中對你的 bot 發送一則任意訊息(例如
hello) - 在瀏覽器打開以下網址(替換你的 token):
https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates
- 你會看到類似這樣的 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 看到群組中的所有訊息,你有兩個選擇:
- 關閉 Privacy Mode:在 BotFather 中設定 → Bot Settings → Group Privacy → Turn Off。關閉後需要將 bot 移除再重新加入群組才會生效。
- 設為管理員:將 bot 設為群組管理員,管理員可以接收所有訊息。
Step 3:擷取訊息:文字、圖片、寄件者
getUpdates API 是 Telegram Bot 最核心的方法。它回傳自上次請求以來的所有新訊息。
關鍵參數:
| 參數 | 型別 | 說明 |
|---|---|---|
offset | Integer | 從哪個 update_id 開始回傳。設為上次最大 update_id + 1 可避免重複 |
limit | Integer | 回傳數量,1-100,預設 100 |
timeout | Integer | Long 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 後,需要兩步驟下載實際圖片:
- 呼叫 getFile API 取得
file_path - 用
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 + Polling | Webhook |
|---|---|---|
| 需要公開伺服器 | 不需要 | 需要(HTTPS + 有效 SSL) |
| 設定難度 | 低(一行 crontab) | 中(DNS + SSL + 伺服器) |
| 即時性 | 依 cron 間隔 | 即時 |
| 適合場景 | 個人/小團隊 | 高流量生產環境 |
注意:Polling(getUpdates)和 Webhook 是互斥的。如果你設定了 Webhook,getUpdates 將無法正常運作。要切換回 polling,需先呼叫
deleteWebhookAPI。
設定 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 Pricing 和 Anthropic 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)並加入人工審核環節。



