148 lines
4.0 KiB
Python
148 lines
4.0 KiB
Python
"""
|
||
Intent Classifier - Phase 13.3 #85
|
||
===================================
|
||
快速意圖分類,用於智能路由
|
||
|
||
目標: < 100ms 延遲
|
||
策略: 關鍵字優先 → 小模型備援
|
||
|
||
Phase 13.3 (2026-03-26): 初始實作
|
||
"""
|
||
|
||
import re
|
||
from enum import Enum
|
||
|
||
import structlog
|
||
|
||
logger = structlog.get_logger(__name__)
|
||
|
||
|
||
class IntentType(Enum):
|
||
"""意圖類型"""
|
||
|
||
ALERT_TRIAGE = "alert_triage" # 告警分流/處理
|
||
DEPLOYMENT = "deployment" # 部署操作 (kubectl, rollout)
|
||
QUERY = "query" # 資訊查詢 (狀態, 日誌)
|
||
MAINTENANCE = "maintenance" # 維運操作 (重啟, 擴容)
|
||
CODE_REVIEW = "code_review" # 程式碼審查
|
||
UNKNOWN = "unknown"
|
||
|
||
|
||
# 關鍵字映射 (優先匹配,0ms)
|
||
INTENT_KEYWORDS: dict[IntentType, list[str]] = {
|
||
IntentType.ALERT_TRIAGE: [
|
||
"alert", "告警", "警報", "異常", "error", "critical", "warning",
|
||
"高負載", "high cpu", "memory", "oom", "crash", "down",
|
||
],
|
||
IntentType.DEPLOYMENT: [
|
||
"deploy", "部署", "rollout", "kubectl apply", "helm", "release",
|
||
"版本", "upgrade", "更新", "上線",
|
||
],
|
||
IntentType.QUERY: [
|
||
"查詢", "狀態", "status", "describe", "get", "list", "日誌", "log",
|
||
"哪個", "什麼", "how many", "多少",
|
||
],
|
||
IntentType.MAINTENANCE: [
|
||
"restart", "重啟", "scale", "擴容", "縮容", "rollback", "回滾",
|
||
"維護", "maintenance", "patch", "修補",
|
||
],
|
||
IntentType.CODE_REVIEW: [
|
||
"review", "審查", "pr", "pull request", "commit", "diff",
|
||
"程式碼", "code", "merge",
|
||
],
|
||
}
|
||
|
||
|
||
class IntentClassifier:
|
||
"""
|
||
意圖分類器
|
||
|
||
使用兩階段分類策略:
|
||
1. 關鍵字快速匹配 (0ms)
|
||
2. 小模型 LLM 分類 (< 100ms) - 備援
|
||
"""
|
||
|
||
# 小模型,低延遲
|
||
MODEL = "qwen2.5:1b"
|
||
|
||
def __init__(self):
|
||
self._keyword_cache: dict[str, IntentType] = {}
|
||
|
||
async def classify(self, text: str) -> IntentType:
|
||
"""
|
||
分類意圖
|
||
|
||
Args:
|
||
text: 用戶輸入或告警內容
|
||
|
||
Returns:
|
||
IntentType: 分類結果
|
||
"""
|
||
text_lower = text.lower()
|
||
|
||
# 階段 1: 關鍵字快速匹配 (0ms)
|
||
intent = self._keyword_match(text_lower)
|
||
if intent != IntentType.UNKNOWN:
|
||
logger.debug(
|
||
"intent_classified_by_keyword",
|
||
intent=intent.value,
|
||
text_preview=text[:50],
|
||
)
|
||
return intent
|
||
|
||
# 階段 2: LLM 分類 (< 100ms)
|
||
# 目前先用關鍵字,LLM 整合待 Qwen 1B 部署
|
||
logger.debug(
|
||
"intent_fallback_to_unknown",
|
||
text_preview=text[:50],
|
||
)
|
||
return IntentType.UNKNOWN
|
||
|
||
def _keyword_match(self, text: str) -> IntentType:
|
||
"""關鍵字匹配"""
|
||
# 檢查快取
|
||
cache_key = text[:100]
|
||
if cache_key in self._keyword_cache:
|
||
return self._keyword_cache[cache_key]
|
||
|
||
# 計算每個意圖的匹配分數
|
||
scores: dict[IntentType, int] = {}
|
||
|
||
for intent, keywords in INTENT_KEYWORDS.items():
|
||
score = 0
|
||
for keyword in keywords:
|
||
if keyword in text:
|
||
score += 1
|
||
# 完整匹配加分
|
||
if re.search(rf"\b{re.escape(keyword)}\b", text):
|
||
score += 1
|
||
if score > 0:
|
||
scores[intent] = score
|
||
|
||
if not scores:
|
||
return IntentType.UNKNOWN
|
||
|
||
# 選擇最高分
|
||
best_intent = max(scores, key=lambda k: scores[k])
|
||
|
||
# 快取結果
|
||
self._keyword_cache[cache_key] = best_intent
|
||
|
||
return best_intent
|
||
|
||
def classify_sync(self, text: str) -> IntentType:
|
||
"""同步版本 (僅關鍵字匹配)"""
|
||
return self._keyword_match(text.lower())
|
||
|
||
|
||
# 單例
|
||
_classifier: IntentClassifier | None = None
|
||
|
||
|
||
def get_intent_classifier() -> IntentClassifier:
|
||
"""取得 IntentClassifier 單例"""
|
||
global _classifier
|
||
if _classifier is None:
|
||
_classifier = IntentClassifier()
|
||
return _classifier
|