File size: 4,840 Bytes
e664788
 
8b7d822
e664788
 
f4f1821
e664788
 
9d3d109
e664788
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f4f1821
 
e664788
f4f1821
 
e664788
f4f1821
 
 
e664788
 
 
 
 
 
 
 
4ce0569
e664788
 
 
631ee3c
e664788
 
 
631ee3c
e664788
 
631ee3c
e664788
 
631ee3c
e664788
 
8e7355c
e664788
 
bd982eb
e664788
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# ✅ app.py - 升級 TinyLlama-1.1B-Chat 版本

import json
import os
import gradio as gr
import faiss
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
from sentence_transformers import SentenceTransformer

# ✅ 檔案與模型設定
QA_FILE = "qa.json"
TEXT_FILE = "web_data.txt"
DOCS_FILE = "docs.json"
VECTOR_FILE = "faiss_index.faiss"
EMBED_MODEL = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
GEN_MODEL = "TinyLlama/TinyLlama-1.1B-Chat-v1.0"

# ✅ 自動建構向量資料庫(若不存在)
if not (os.path.exists(VECTOR_FILE) and os.path.exists(DOCS_FILE)):
    print("⚙️ 未偵測到向量資料庫,開始自動建構...")
    with open(TEXT_FILE, "r", encoding="utf-8") as f:
        content = f.read()
    docs = [chunk.strip() for chunk in content.split("\n\n") if chunk.strip()]
    embedder = SentenceTransformer(EMBED_MODEL)
    embeddings = embedder.encode(docs, show_progress_bar=True)
    index = faiss.IndexFlatL2(embeddings[0].shape[0])
    index.add(embeddings)
    faiss.write_index(index, VECTOR_FILE)
    with open(DOCS_FILE, "w", encoding="utf-8") as f:
        json.dump(docs, f, ensure_ascii=False, indent=2)
    print("✅ 嵌入建構完成,共儲存段落:", len(docs))

# ✅ 載入資料與模型
with open(QA_FILE, "r", encoding="utf-8") as f:
    qa_data = json.load(f)
with open(DOCS_FILE, "r", encoding="utf-8") as f:
    docs = json.load(f)
index = faiss.read_index(VECTOR_FILE)
embedder = SentenceTransformer(EMBED_MODEL)
tokenizer = AutoTokenizer.from_pretrained(GEN_MODEL, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(GEN_MODEL, trust_remote_code=True).to("cuda" if torch.cuda.is_available() else "cpu")
model.eval()

# ✅ QA 快速匹配

def retrieve_qa_context(user_input):
    for item in qa_data:
        if item["match"] == "OR":
            if any(k in user_input for k in item["keywords"]):
                return item["response"]
        elif item["match"] == "AND":
            if all(k in user_input for k in item["keywords"]):
                return item["response"]
    return None

# ✅ 向量檢索 top-k 段落

def search_context_faiss(user_input, top_k=3):
    vec = embedder.encode([user_input])
    D, I = index.search(vec, top_k)
    return "\n".join([docs[i] for i in I[0] if i < len(docs)])

# ✅ 使用 Few-shot Prompt 生成答案

def generate_answer(user_input, context):
    prompt = f"""
你是一位了解南臺科技大學的智慧語音助理。請根據以下資料回答問題,僅用一至兩句話,以繁體中文表達,回答需清楚具體,不重複問題,不加入身份說明。

[範例格式]
問題:學校地址在哪裡?
回答:南臺科技大學位於台南市永康區南台街一號。

問題:學校電話是多少?
回答:總機電話是 06-2533131,電機工程系分機為 3301。

問題:電機工程系辦公室在哪?
回答:電機工程系辦公室位於 B 棟 B101。

問題:電機工程系有哪些組別?
回答:電機系設有控制組、生醫電子系統組與電能資訊組三個方向。

問題:學生社團活動如何?
回答:南臺有超過 80 個學生社團,涵蓋學術、康樂、服務、體育與藝術領域。

問題:圖書館提供哪些服務?
回答:圖書館提供借書、自修空間、期刊查詢與電子資源服務。

問題:師資如何?
回答:本校師資陣容堅強,擁有 30 多位教授、副教授與助理教授。

問題:悠活館是做什麼的?
回答:悠活館是學生休閒與運動中心,設有羽球場、健身房、桌球室等設施。

問題:怎麼到南臺科技大學?
回答:可從台南火車站搭乘公車,或經永康交流道開車約 10 分鐘抵達。

[資料]
{context}

[問題]
{user_input}
"""
    inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
    outputs = model.generate(**inputs, max_new_tokens=150)
    response = tokenizer.decode(outputs[0], skip_special_tokens=True).strip()
    for line in response.splitlines()[::-1]:
        if len(line.strip()) > 10 and not line.startswith("你是"):
            return line.strip()
    return response[-90:]

# ✅ 問答主流程

def answer(user_input):
    direct = retrieve_qa_context(user_input)
    if direct:
        return direct
    else:
        context = search_context_faiss(user_input)
        return generate_answer(user_input, context)

# ✅ Gradio 介面
interface = gr.Interface(
    fn=answer,
    inputs=gr.Textbox(lines=2, placeholder="請輸入與南臺科技大學相關的問題..."),
    outputs="text",
    title="南臺科技大學 問答機器人(TinyLlama 1.1B)",
    description="支援 QA 關鍵字與語意檢索,自動建立嵌入庫,輸出繁體中文自然回答。",
    theme="default"
)

interface.launch()