前言
本文將透過開發 Python Web API 後台 及 Vue 前台,搭配自訂對話規則回覆以及 LLM DeepSeek 開發一個人 local 的 Web 聊天機器人。
前置條件
本專案有使用到 DeepSeek API,故讀者須申請DeepSeek API 關於DeepSeek API,進入 DeepSeek 開發者平台的 API 管理頁面取得 API Key。
目錄清單
charbot/
|-- .env
|-- app.py
|-- chatbot_deepseek.py
|-- deepseek_client.py
|-- llm_guard.py
|-- responses.json
|-- response_loader.py
後台 Web API
Python ChatBot + DeepSeek 版本
1. 安裝相關模型
python-dotenv 是一個專門用於從 .env 檔案載入環境變數到 Python 程式中的函式庫
% pip install python-dotenv
spaCy是一個用於自然語言處理的開源軟件庫
參看: https://pypi.org/project/spacy/
% pip install spacy
安裝 使用 spaCy 的 中文模型
% python3 -m spacy download zh_core_web_sm
安裝 FastAPI 和 ASGI 伺服器Uvicorn 用於執行 FastAPI 應用程式
% pip install fastapi uvicorn
中文模型簡介:
zh_core_web_sm: 最小的中文語言模型,用於基本的語言處理任務(分詞和詞性標註)。
zh_core_web_md: 一個中等大小的中文語言模型,比 zh_core_web_sm 包含更多的詞彙、語言特徵和上下文訊息,可以用於更多的語言處理。
zh_core_web_lg: 最大的中文語言模型,比 zh_core_web_md 包含更多的語言特徵和上下文信息,可以用於更複雜和高級的語言處理。
2. 實作代碼
對話規則型回覆檔,可後續新增或調整對話內容。
{
"greeting": [
"你好!",
"哈囉~",
"嗨,很高興見到你",
"你好呀,有什麼需要幫忙的嗎?",
"嗨!今天過得如何?",
"你好呀,有什麼需要幫忙的嗎?我很樂意協助!🤗",
"嗨!今天過得如何?希望你有美好的一天!🌼",
"Yo!今天準備好聊什麼有趣的話題嗎?🎉"
],
"how_are_you": [
"我很好,謝謝你的關心!心情像陽光一樣燦爛呢~☀️",
"還不錯,你呢?今天有什麼有趣的事情嗎?🤔",
"一切都很順利~覺得今天充滿正能量!💪",
"心情不錯,希望你也是!剛剛學習了新知識~📚",
"還可以,正在努力學習中,每天都要進步一點!🌟",
"感覺很棒!謝謝你的問候,這讓我很開心~🙏",
"好得不得了!像是吃了一顆快樂糖果一樣~🍬",
"平靜而充實,感謝你的關心。你呢?🌸"
],
"goodbye": [
"再見!",
"掰掰,祝你有美好的一天☀️",
"下次再聊!",
"保重~",
"期待再和你聊天"
],
"thanks": [
"不客氣!",
"很高興能幫上忙🤗",
"沒問題!",
"隨時都可以找我",
"這是我應該做的"
],
"name": [
"我是簡單的聊天機器人",
"你可以叫我 ChatBot",
"我是一個用 Python 寫的聊天機器人",
"目前還沒有名字 😄",
"我是你的對話小助手"
],
"help": [
"你可以跟我聊天或問問題",
"我可以陪你練習對話",
"你可以問我一些簡單的問題",
"我還在學習中,但我會盡力回答",
"試著跟我說聲你好吧"
],
"default": [
"我還在學習,能換個方式說嗎?",
"這個我暫時不太懂",
"可以再說清楚一點嗎?",
"不好意思,我沒聽懂",
"這部分我還需要多學習"
],
"weather": [
"今天天氣如何呢?",
"我沒辦法即時查天氣",
"你可以看看窗外",
"天氣好壞都要保持好心情❤️",
"希望今天是個好天氣"
],
"jokes": [
"為什麼程式設計師不喜歡大自然?因為有太多 bug 🐛",
"程式設計師最怕三個字:可以改嗎?",
"世界上最遙遠的距離,是需求文件跟實際功能",
"程式跑不動的時候,先怪電腦,準沒錯",
"程式寫得好不好不重要,能不能跑比較重要",
"有時候程式不是錯,只是不照你的想法執行",
"能跑的程式就是好程式",
"錢不是問題,問題是我沒有錢",
"我每天都很努力,只是努力在想要不要努力",
"有些事情想不通,就先不要想了",
"人生很多問題,睡一覺也不一定會解決,但至少比較不累",
"我對未來很有規劃,只是還沒開始",
"有時候沉默,不是沒話說,是在整理思緒",
"我不是選擇困難,只是每個都想選"
]
}
.env 檔
「你的 API key」指從DeepSeek 開發者平台 的 API 管理頁面取得 API Key
DEEPSEEK_API_KEY=你的 API key
deepseek_client.py
呼叫 DeepSeek API
import os
import requests
from dotenv import load_dotenv
load_dotenv()
DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY")
def call_deepseek(user_input: str) -> str:
"""呼叫DeepSeek API並回傳回應"""
if not DEEPSEEK_API_KEY:
return "目前無法使用進階對話功能,API金鑰未設定"
# 先檢查API狀態
# status = check_deepseek_status()
# if status["status"] != "ok":
# return f"AI服務暫時無法使用:{status['message']}"
url = "https://api.deepseek.com/chat/completions"
headers = {
"Authorization": f"Bearer {DEEPSEEK_API_KEY}",
"Content-Type": "application/json"
}
data = {
"model": "deepseek-chat",
"messages": [{"role": "user", "content": user_input}],
"max_tokens": 50, # 50 很短的回覆, 日常聊天:200-300 tokens
"temperature": 0.7, # 控制AI輸出的隨機性和創造性,中溫(0.5-0.7):像"友善的朋友"
"stream": False # 一次性返回完整回复
}
try:
response = requests.post(url, headers=headers, json=data, timeout=30)
if response.status_code == 200:
result = response.json()
reply = result["choices"][0]["message"]["content"]
return reply
elif response.status_code == 402:
return "AI服務額度不足,請充值或使用網頁版:chat.deepseek.com"
elif response.status_code == 429:
return "請求過於頻繁,請稍後再試"
elif response.status_code == 401:
return "API金鑰無效,請檢查設定"
else:
print(f"[API Error] {response.status_code}: {response.text}")
return f"AI服務暫時無法使用,錯誤代碼:{response.status_code}"
except requests.exceptions.Timeout:
return "請求超時,請稍後再試"
except requests.exceptions.ConnectionError:
return "網路連線失敗,請檢查網路"
except Exception as e:
print(f"[ERROR] 請求異常: {e}")
return "AI服務暫時無法使用"
參看:
max_tokens: 估算token數
日常聊天:200-300 tokens
問答解釋:300-500 tokens
temperature: 控制AI輸出的隨機性和創造性
低溫 (0.1-0.3):像"嚴謹的工程師"
中溫 (0.5-0.7):像"友善的朋友"
高溫度 (0.8-1.2):像"創意的藝術家"
stream=False(預設):一次性返回完整回复
stream=True:即時串流傳輸,逐字/逐塊返回
llm_guard.py
判斷句超過多少以及特定關鍵字就呼叫 LLM,目前為 大於 5 ,您可以依需要自行修改。
注意!DeepSeek API 額度成本。
'''
長句才丟模型:短句 -> 規則, 長句 / 開放式 -> LLM
'''
def should_use_llm(text: str) -> bool:
if len(text) >= 5:
return True
keywords = ["為什麼", "怎麼", "可以嗎", "建議", "有什麼建議", "想法", "覺得"]
return any(k in text for k in keywords)
response_loader.py
修改 responses.json 後,不重啟後端程式,馬上生效
import json
import os
import time
# 取得 目前 Python 檔案所在的資料夾路徑, 相對路徑
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
RESPONSES_PATH = os.path.join(BASE_DIR, "responses.json")
_responses_cache = None
_last_mtime = 0
# 讀取自訂回覆資料
def get_responses():
global _responses_cache, _last_mtime
try:
mtime = os.path.getmtime(RESPONSES_PATH)
except FileNotFoundError:
return {}
if _responses_cache is None or mtime > _last_mtime:
with open(RESPONSES_PATH, "r", encoding="utf-8") as f:
_responses_cache = json.load(f)
_last_mtime = mtime
print("responses.json 已重新載入")
return _responses_cache
chatbot_deepseek.py
聊天邏輯程式
import os
import spacy
import json
import random
from deepseek_client import call_deepseek
from llm_guard import should_use_llm
from dotenv import load_dotenv
from response_loader import get_responses
# 載入中文模型
nlp = spacy.load("zh_core_web_sm")
def get_response(user_input):
responses = get_responses()
return random.choice(responses.get(user_input, responses["default"]))
def chat_to_user(user_input):
doc = nlp(user_input)
text = user_input.lower()
if any(word in text for word in ["你好", "哈囉", "嗨", "hello", "hi"]):
resp = get_response("greeting")
elif any(word in text for word in ["你好吗", "你好嗎", "過得"]):
resp = get_response("how_are_you")
elif any(word in text for word in ["你是誰", "你的名字"]):
resp = get_response("name")
elif any(word in text for word in ["幫助", "能做什麼"]):
resp = get_response("help")
elif any(word in text for word in ["笑話", "講個笑話", "逗我笑", "好笑的"]):
resp = get_response("jokes")
elif any(word in text for word in ["謝謝", "感謝"]):
resp = get_response("thanks")
elif any(word in text for word in ["再見", "掰掰", "bye"]):
resp = get_response("goodbye")
elif should_use_llm(user_input):
# ---------- LLM fallback ----------
resp = call_deepseek(user_input)
else:
resp = get_response("default")
return resp
app.py
Python 主程式
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from chatbot_deepseek import chat_to_user
app = FastAPI(title="Local ChatBot")
# CORS 跨網域呼叫設定
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:5173", # Vue
"http://127.0.0.1:5173",
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
class ChatRequest(BaseModel):
message: str
@app.post("/chat")
def chat_api(req: ChatRequest):
return {"reply": chat_to_user(req.message)}
前台 Vue Web
使用 FastAPI 建立 API服務
1. 使用 Vite 快速創建 Vue.js 專案
參看文章
npm run dev <- 確認執行
1. 直接修改 App.vue
<script setup>
import { ref, nextTick } from "vue"
const message = ref("")
const chats = ref([])
const chatBox = ref(null)
async function sendMessage() {
if (!message.value) return
chats.value.push({ role: "user", text: message.value })
await scrollToBottom()
const res = await fetch("http://127.0.0.1:8000/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: message.value })
})
const data = await res.json()
chats.value.push({ role: "bot", text: data.reply })
message.value = ""
await scrollToBottom()
}
// push 訊息後自動捲動, 使用 nextTick() 可以確保 Vue 把畫面畫完之後再捲動
async function scrollToBottom() {
await nextTick()
if (chatBox.value) {
chatBox.value.scrollTop = chatBox.value.scrollHeight
}
}
</script>
<template>
<div class="container">
<h2>😀 我是在地的聊天機器人 🤖</h2>
<div class="chat-box" ref="chatBox">
<div v-for="(msg, i) in chats" :key="i" class="chat-row" :class="msg.role">
<div class="bubble">
{{ msg.text }}
</div>
</div>
</div>
<input v-model="message" @keyup.enter="sendMessage" placeholder="輸入訊息..." />
<button @click="sendMessage">送出</button>
</div>
</template>
<style>
.container {
width: 500px;
margin: auto;
font-family: sans-serif;
}
.chat-box {
height: 350px;
border: 1px solid #ddd;
padding: 8px;
overflow-y: auto;
background: #f9f9f9;
}
/* 每一行 */
.chat-row {
display: flex;
margin-bottom: 8px;
}
/* 使用者 右邊 */
.chat-row.user {
justify-content: flex-end;
}
/* Bot 左邊 */
.chat-row.bot {
justify-content: flex-start;
}
/* 氣泡本體 */
.bubble {
max-width: 70%;
padding: 8px 12px;
border-radius: 12px;
line-height: 1.4;
word-break: break-word;
white-space: pre-wrap;
}
/* 使用者氣泡樣式 */
.chat-row.user .bubble {
background: #007acc;
color: white;
border-bottom-right-radius: 4px;
}
/* Bot 氣泡樣式 */
.chat-row.bot .bubble {
background: #e5e5e5;
color: #333;
border-bottom-left-radius: 4px;
}
.user {
text-align: right;
margin: 4px;
color: #333;
word-break: break-word;
white-space: pre-wrap;
}
.bot {
text-align: left;
margin: 4px;
color: #007acc;
word-break: break-word;
white-space: pre-wrap;
}
input {
width: 75%;
}
button {
width: 18%;
font-size: 12px;
background: royalblue;
color: white;
border: none;
}
</style>
測試:
啟動 ChatBot
在project 目錄下
% uvicorn app:app --reload
啟動 Vue Web
% npm run dev
留言