昨天的版本已經能同時做出摘要和翻譯,算是這個小專案的第一個雛形。
不過那個版本的內容都是我在程式裡寫死的,也就是說文章是固定的、問題也沒有真正存在。
所以雖然看起來像個問答機器人,但其實它根本還不能讓使用者真的問問題。
今天我想要做出真正能互動的版本:
流程大概是
1.使用者可以自己輸入一段文章。
2.使用者可以再輸入一個問題。
3.系統會:
* 先幫文章做摘要
* 再把整段翻譯成繁體中文
* 最後根據文章回答問題
我一開始照昨天的程式改,只加上 input(),沒想到結果一執行就出錯了
ValueError: model type 'gemma3_text' is not recognized
查了才知道,原來是Gemma-3太新,舊版Transformers還不認得它的架構名稱。
我用的transformers版本是4.44.2,而Gemma-3需要4.46以上才支援。
本來可以升級套件解決,但那樣很容易讓整個環境衝突,尤其Colab的accelerate和torch版本都會被影響。所以我最後決定退一步,改用Gemma-2-2B-IT。
雖然稍微舊一點,但在目前環境非常穩定。
完整程式碼
!pip -q install transformers==4.44.2 accelerate huggingface_hub regex
import os, re, torch, regex as re2
from google.colab import userdata
from huggingface_hub import login
from transformers import AutoTokenizer, AutoModelForCausalLM
hf_token = userdata.get('HF_TOKEN')
if not hf_token:
raise RuntimeError("找不到 HF_TOKEN,請到左側 Secret 新增同名密鑰。")
login(token=hf_token)
MODEL_ID = "google/gemma-2-2b-it"
tok = AutoTokenizer.from_pretrained(
MODEL_ID,
trust_remote_code=True,
use_fast=False
)
model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
device_map="auto",
torch_dtype="auto",
trust_remote_code=True
)
def _apply_chat(messages):
input_ids = tok.apply_chat_template(
messages,
tokenize=True,
add_generation_prompt=True,
return_tensors="pt"
).to(model.device)
attention_mask = torch.ones_like(input_ids, device=model.device)
return dict(input_ids=input_ids, attention_mask=attention_mask)
def _generate_plain(messages, max_new_tokens=256, do_sample=False):
"""最穩定設定:關閉 sampling,不設 temperature/top_p。"""
inputs = _apply_chat(messages)
with torch.no_grad():
out = model.generate(
**inputs,
max_new_tokens=max_new_tokens,
do_sample=do_sample,
eos_token_id=tok.eos_token_id,
pad_token_id=tok.eos_token_id,
)
new_tokens = out[0, inputs["input_ids"].shape[-1]:]
return tok.decode(new_tokens, skip_special_tokens=True)
def _strip_roles_and_blank(text):
lines = [ln.strip() for ln in text.splitlines()]
return [ln for ln in lines if ln and not ln.lower().startswith(("user", "system", "assistant"))]
_word_re = re.compile(r"[A-Za-z]+(?:'[A-Za-z]+)?")
def _is_english_sentence(line, max_words=25):
if re.search(r"[^\x09\x0A\x0D\x20-\x7E]", line): return False # 非 ASCII
if re.search(r"[*_#>|`]{2,}", line): return False # Markdown 噪音
words = _word_re.findall(line)
return 1 <= len(words) <= max_words
_sent_split = re.compile(r'\s*(.+?[.!?])(?:\s+|$)')
def clean_summary_3_lines(text):
joined = " ".join(_strip_roles_and_blank(text))
sentences, pos = [], 0
while len(sentences) < 3 and pos < len(joined):
m = _sent_split.match(joined, pos)
if not m:
frag = joined[pos:].strip()
if frag:
if not re.search(r'[.!?]$', frag): frag += '.'
if _is_english_sentence(frag): sentences.append(frag)
break
cand = m.group(1).strip()
if _is_english_sentence(cand): sentences.append(cand)
pos = m.end()
if not sentences and joined.strip():
fb = joined.strip()
if not re.search(r'[.!?]$', fb): fb += '.'
sentences = [fb]
return "\n".join(sentences[:3])
_cjk_re = re2.compile(r"[\p{Script=Han}。,、!?;:「」『』()《》〈〉—…¥·.-﹔﹖﹗﹙﹚]")
def clean_zh_translation(raw_text):
lines = _strip_roles_and_blank(raw_text)
kept = []
for ln in lines:
cjk = len(_cjk_re.findall(ln))
ratio = cjk / max(len(ln), 1)
if ratio >= 0.35: kept.append(ln.strip())
text = "\n".join(kept)
text = re.sub(r"(\*{2,}|_{2,})+", "", text)
text = re.sub(r"[ \t]+$", "", text, flags=re.MULTILINE)
text = re.sub(r"\n{3,}", "\n\n", text).strip()
return text
def gen_summary_3sent(text):
msgs = [{
"role": "user",
"content": (
"You are a concise summarization assistant.\n"
"Given the TEXT, produce EXACTLY three English sentences summarizing it.\n"
"Rules:\n"
"1) Three lines, one sentence per line.\n"
"2) Each sentence ≤ 25 words.\n"
"3) No headings, no extra text.\n\n"
f"TEXT:\n{text}\n\n"
"OUTPUT (three lines only):"
)
}]
raw = _generate_plain(msgs, max_new_tokens=180, do_sample=False)
return clean_summary_3_lines(raw)
def translate_to_zh(text):
msgs = [
{"role": "user", "content": "你是嚴謹的翻譯助手。請將以下英文完整翻譯為繁體中文,只輸出譯文本身:\n" + text}
]
raw = _generate_plain(msgs, max_new_tokens=900, do_sample=False)
return clean_zh_translation(raw)
def answer_question(context, question):
# 翻譯路由:遇到「翻譯成繁體中文」的需求,就直接回傳翻譯
q = question.lower()
if "translate" in q and "traditional chinese" in q:
return translate_to_zh(context)
#一般QA(只用context)
msgs = [
{"role": "user", "content":
f"You are a question answering assistant. Use only the provided context.\n\n"
f"Context:\n{context}\n\nQuestion: {question}\n\n"
"Answer in one short sentence:"}
]
raw = _generate_plain(msgs, max_new_tokens=200, do_sample=False)
ans = _strip_roles_and_blank(raw)
return (ans[0] if ans else "Not found in the provided text.").strip()
#互動
user_text = input("請輸入一段文章:\n")
question = input("\n請輸入你的問題:\n")
print("\n=== 三句摘要(英文) ===")
print(gen_summary_3sent(user_text))
print("\n=== 原文翻譯(繁中) ===")
print(translate_to_zh(user_text))
print("\n=== 問題 ===")
print(question)
print("\n=== QA 答案 ===")
print(answer_question(user_text, question))
這次的重點在最後的互動部分:
user_text = input("請輸入一段文章:\n")
question = input("\n請輸入你的問題:\n")
print("\n=== QA 答案 ===")
print(answer_question(user_text, question))
這幾行讓整個程式變得更像一個真正的工具。
實際測試結果
我用這段文字測試:
Artificial Intelligence is transforming the field of medicine.
It can help in diagnosing diseases, analyzing medical images,
and even assisting in drug discovery.
This progress is driven by advances in machine learning and deep learning.
However, there are challenges such as data privacy and security.
並嘗試詢問兩個問題:
分別得出的結果如下圖:
可以看到除了有三句英文摘要、中文翻譯外,也都有正確回答問題。
老實說,我原本以為只要加兩行input()就能搞定,沒想到竟然被模型版本卡了一個多小時。
不過也因此學到一件事,開發AI應用,程式邏輯很重要,但環境相容性更關鍵。
Gemma-3雖然更新,但不一定適合所有環境;
Gemma-2看起來舊,卻能穩穩地跑完整個流程。
這種取捨,就像專案實作裡最常遇到的現實狀況。