Spaces:
Sleeping
Sleeping
File size: 8,911 Bytes
8d5e427 | 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 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | #!/usr/bin/env python3
"""
企業多任務 LLM 評估腳本
測試四大能力: 客服FAQ | 文件問答 | 工單分類 | 資訊抽取
"""
import json
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import PeftModel
MODEL_ID = "Qwen/Qwen2.5-7B-Instruct"
ADAPTER_ID = "Justin-lee/Qwen2.5-7B-Enterprise-ZH"
def load_model(use_adapter=True):
"""Load model with optional LoRA adapter."""
print(f"📦 Loading {'fine-tuned' if use_adapter else 'base'} model...")
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.bfloat16,
)
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
quantization_config=bnb_config,
device_map="auto",
trust_remote_code=True,
torch_dtype=torch.bfloat16,
)
if use_adapter:
model = PeftModel.from_pretrained(model, ADAPTER_ID)
print(f" ✅ LoRA adapter loaded from {ADAPTER_ID}")
return model, tokenizer
def generate(model, tokenizer, messages, max_new_tokens=512):
"""Generate response from messages."""
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tokenizer(text, return_tensors="pt").to(model.device)
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=max_new_tokens,
temperature=0.3,
top_p=0.9,
do_sample=True,
pad_token_id=tokenizer.eos_token_id,
)
response = tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)
return response.strip()
# ── Test Cases ──
EVAL_CASES = {
"客服FAQ": [
{
"system": "你是一個專業的企業客服助手。請根據用戶的問題,提供準確、簡潔、有禮貌的回答。",
"user": "我昨天下的訂單想取消,還來得及嗎?訂單號 ORD-20240420。",
"expected_keywords": ["取消", "訂單", "狀態", "發貨"],
},
{
"system": "你是一個專業的企業客服助手。請根據用戶的問題,提供準確、簡潔、有禮貌的回答。",
"user": "你們的會員制度是怎樣的?有什麼優惠?",
"expected_keywords": ["會員", "優惠", "等級"],
},
{
"system": "你是一個專業的企業客服助手。請根據用戶的問題,提供準確、簡潔、有禮貌的回答。",
"user": "商品收到有破損,怎麼處理?",
"expected_keywords": ["退", "換", "照片", "客服"],
},
],
"文件問答": [
{
"system": "你是一個文件分析助手。請仔細閱讀提供的文件內容,僅根據文件中的資訊回答問題。",
"user": "請根據以下文件回答問題。\n\n【文件內容】\n公司年假制度:入職滿1年的員工享有5天年假,滿3年享有10天,滿5年享有15天。年假需提前3個工作日申請,由直屬主管審批。未使用的年假不可轉入下年度,但可折算為加班費。特殊情況(如家庭重大事件)可申請額外3天事假。\n\n【問題】\n工作3年的員工有幾天年假?年假可以保留到明年嗎?",
"expected_keywords": ["10", "不可", "折算", "加班費"],
},
{
"system": "你是一個文件分析助手。請仔細閱讀提供的文件內容,僅根據文件中的資訊回答問題。",
"user": "請根據以下文件回答問題。\n\n【文件內容】\n退款政策:1. 未發貨訂單:可立即取消並全額退款。2. 已發貨未簽收:需要拒收後由物流退回,退回運費由公司承擔。3. 已簽收:7天內可申請退貨退款,退回運費由買家承擔。4. 特殊商品(定製品、食品、內衣)不支持退貨。退款將原路返回至付款帳戶。\n\n【問題】\n已經簽收的訂單退貨,運費誰出?定製品可以退嗎?",
"expected_keywords": ["買家", "不支持", "定製"],
},
],
"工單分類": [
{
"system": "你是一個工單分類與分流助手。請根據用戶描述的問題,將其分類到最合適的處理類別。",
"user": "請將以下客戶訊息分類到合適的處理部門。\n可選部門:售後服務、物流配送、帳號問題、付款財務、產品諮詢、投訴建議、技術支援、合作洽談\n\n客戶訊息:我買的藍牙耳機左耳沒聲音了,買了才兩個禮拜。",
"expected_keywords": ["售後", "保固", "故障"],
},
{
"system": "你是一個工單分類與分流助手。請根據用戶描述的問題,將其分類到最合適的處理類別。",
"user": "請將以下客戶訊息分類到合適的處理部門。\n可選部門:售後服務、物流配送、帳號問題、付款財務、產品諮詢、投訴建議、技術支援、合作洽談\n\n客戶訊息:付款一直顯示失敗,我用了三張不同的信用卡都不行。",
"expected_keywords": ["付款", "財務"],
},
],
"資訊抽取": [
{
"system": "你是一個資訊抽取助手。請從文本中準確抽取指定類型的實體資訊。",
"user": "請從以下文本中抽取所有關鍵資訊(人名、日期、金額、地址、聯繫方式):\n\n「客戶王大明先生於2024年4月18日來電,反映其於3月25日在台北市大安區忠孝東路四段200號門市購買的筆記型電腦(金額NT$35,800)出現螢幕閃爍問題。客戶要求維修或更換,聯繫電話:0912-345-678,Email: wang.dm@gmail.com。」",
"expected_keywords": ["王大明", "2024年4月18日", "35,800", "忠孝東路", "0912"],
},
{
"system": "你是一個資訊抽取助手。請從文本中準確抽取指定類型的實體資訊。",
"user": "請從以下合約條款中抽取關鍵條件:\n\n「甲方(承租人)李小華需於每月5日前支付租金新台幣28,000元至乙方指定帳戶。租賃期間自2024年5月1日起至2025年4月30日止,共12個月。押金為兩個月租金共56,000元,於退租時無息退還。如逾期付租超過15日,乙方有權終止合約。」",
"expected_keywords": ["李小華", "28,000", "2024年5月1日", "2025年4月30日", "56,000", "15日"],
},
],
}
def evaluate(model, tokenizer):
"""Run evaluation on all task categories."""
print("\n" + "="*70)
print("📊 Enterprise Multi-Task LLM Evaluation")
print("="*70)
results = {}
for task_name, cases in EVAL_CASES.items():
print(f"\n{'─'*60}")
print(f"📋 Task: {task_name} ({len(cases)} cases)")
print(f"{'─'*60}")
task_scores = []
for i, case in enumerate(cases):
messages = [
{"role": "system", "content": case["system"]},
{"role": "user", "content": case["user"]},
]
response = generate(model, tokenizer, messages)
# Check keyword coverage
keywords = case["expected_keywords"]
hits = sum(1 for kw in keywords if kw in response)
score = hits / len(keywords) if keywords else 1.0
task_scores.append(score)
print(f"\n Case {i+1}:")
print(f" Q: {case['user'][:80]}...")
print(f" A: {response[:200]}...")
print(f" Keywords: {hits}/{len(keywords)} ({score:.0%})")
avg_score = sum(task_scores) / len(task_scores) if task_scores else 0
results[task_name] = avg_score
print(f"\n 📈 {task_name} Average: {avg_score:.1%}")
# Summary
print("\n" + "="*70)
print("📊 Overall Results")
print("="*70)
overall = sum(results.values()) / len(results)
for task, score in results.items():
bar = "█" * int(score * 20) + "░" * (20 - int(score * 20))
print(f" {task:10s} {bar} {score:.1%}")
print(f"\n Overall Average: {overall:.1%}")
print("="*70)
return results
if __name__ == "__main__":
import sys
use_adapter = "--base" not in sys.argv
model, tokenizer = load_model(use_adapter=use_adapter)
results = evaluate(model, tokenizer)
# Save results
with open("eval_results.json", "w", encoding="utf-8") as f:
json.dump(results, f, ensure_ascii=False, indent=2)
print(f"\n📝 Results saved to eval_results.json")
|