跳到主要內容

探索 Web 聊天機器人 ( Python ChatBot + Vue UX)

 前言

本文將透過開發 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. 實作代碼

responses.json
對話規則型回覆檔,可後續新增或調整對話內容。


{

    "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 專案

參看文章 Vue 3 初探

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="輸入訊息..." />

    &nbsp;

    <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




Web 聊天畫面











留言

這個網誌中的熱門文章

初探 Vue 呼叫 API 出現 CORS 跨來源資源共享 問題原因

提要:   在 {初探Vue 與 Spring boot 的對話} 專案 ,前端 Vue 應用程式 串接 後端 API 伺服器 ,axios 呼叫 API 時出現以下,”無法取得回應內容 (No 'Access-Control-Allow-Origin' header is present on the requested resource):” 錯誤訊息,根據查找相關資料 ,出現以下原因。 瀏覽器開發工具 錯誤訊息 畫面 錯誤原因: “ Access to XMLHttpRequest at ” from origin ‘http://localhost:8080’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource” 瀏覽器為了安全考量,實施了同源政策。 當您的前端應用程式 (http://localhost:8080) 嘗試呼叫一個不同來源 (不同協議、不同域名或不同埠號) 的 API 伺服器 (http://localhost:8088) 時,瀏覽器會主動阻止這個請求,除非伺服器明確地允許這個跨來源的存取。 同源政策限制(Same-Origin Policy): 同源政策限制了程式碼和不同網域資源間的互動,同源是指兩份網頁具備相同協定、埠號(如果有指定)以及主機位置 範例: 表列哪些 URL 與 URL http://www.example.com/api/p1 屬於同源: URL                                                   | 結果   | 原因 --------------------------------------------------------------------- http://www.example.com/api/p2     |...

初探 Vue 與 Spring boot 的對話之Frontend (Vue-Frontend)

  Front-end Vue 使用 REST API 建立 Vite 專案 可參考 { Vue 3 初探}  文章 danny@Danny-Yu projects % npm create vite@latest Need to install the following packages: create-vite@8.2.0 Ok to proceed? (y) y > npx > "create-vite" │ ◇   Project name: │   vue-frontend │ ◇   Select a framework: │   Vue │ ◇   Select a variant: │   TypeScript │ ◇   Use rolldown-vite (Experimental)?: │   No │ ◇   Install with npm and start now? │   Yes │ ◇   Scaffolding project in /Users/danny/Desktop/projects/vue-frontend... │ ◇   Installing dependencies with npm... added 47 packages, and audited 48 packages in 27s 6 packages are looking for funding   run `npm fund` for details found 0 vulnerabilities │ ◇   Starting dev server... > vue-frontend@0.0.0 dev > vite   VITE v7.2.4   ready in 411 ms   ➜   Local:   http://localhost:5173/   ➜   Network: use --host to expose   ➜   press h + enter to show...

初探 Spring 中的循環依賴

原因: 當兩個或多個 bean 直接或間接地相互依賴時, 就會出現 Circular Dependency (循環依賴) 如: Bean A -> Bean B -> Bean A import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class BeanA {          @Autowired     private BeanB beanB;     public String sayHi() {         return "Hi! 我是 Class A.";     } } import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class BeanB {          @Autowired     private BeanA beanA;     public String sayHi() {         return "Hi! 我是 Class B.";     } } 編譯時不會出現問題 danny@Danny-Yu demo % mvn clean install -Dmaven.test.skip=true                           ... ... [INFO] Installing /Users/danny/Desktop/projects/demo/target/dem...