Day 18 完成 Structured Output 後,有讀者提出了一個很重要的問題:目前 severity 雖然已經被放進固定的資料結構,但 1、2、3 對應 High、Medium、Low 本身其實是一項確定性的系統規則,真的有必要再交給 LLM 判斷一次嗎?
這個建議讓我重新思考目前的設計。
LLM 擅長的是理解事件內容、產生摘要及解釋可能的風險;但像 Severity 數值轉換這類已有明確規則的工作,交給一般程式處理不但更簡單,結果也更加穩定。
因此今天不急著增加更多 AI 功能,而是先重新整理 Backend 與 LLM 之間的責任。
比較合理的架構應該是:
Security Event
│
┌─────────┴─────────┐
↓ ↓
確定性規則 語意分析
↓ ↓
Backend Gemini
↓ ↓
Severity Summary
High/Medium/Low Attack Type
Risk
└─────────┬─────────┘
↓
AI SOC Analysis
確認 SOC 事件資料庫內容
目前 SQLite 資料庫 soc.db 中共有三筆測試事件,分別為 Network Scan、Login Attempt 與 Web Attack。每筆事件包含來源 IP、目的 IP、連接埠、通訊協定、事件特徵與 Severity 等欄位,接下來將以這些資料作為 AI 分析的輸入。
建立新檔 nano ai_event_summary.py
severity_map = {
1: "High",
2: "Medium",
3: "Low"
}
severity = 2
severity_label = severity_map.get(severity, "Unknown")
print("原始 Severity:", severity)
print("轉換後 Severity:", severity_label)

從 2 → Gemini → Medium 變成 2 → Python → Medium
現在我們再解決第二個問題: severity: str 代表理論上它可以是任何文字:High、HIGH、high、2、Medium Risk、嚴重、ABC...nano ai_event_summary.py
from enum import Enum
class SeverityLevel(str, Enum):
HIGH = "High"
MEDIUM = "Medium"
LOW = "Low"
severity_map = {
1: SeverityLevel.HIGH,
2: SeverityLevel.MEDIUM,
3: SeverityLevel.LOW
}
severity = 2
severity_level = severity_map.get(severity)
print("原始 Severity:", severity)
print("轉換後 Severity:", severity_level.value)

nano ai_event_summary.py
import sqlite3
from enum import Enum
from google import genai
from pydantic import BaseModel, Field
client = genai.Client()
# Severity 只允許固定的三種值
class SeverityLevel(str, Enum):
HIGH = "High"
MEDIUM = "Medium"
LOW = "Low"
# Gemini 負責產生的 AI 分析
class AIAnalysis(BaseModel):
summary: str = Field(
description="用繁體中文簡短說明這個資安事件發生了什麼"
)
attack_type: str = Field(
description="根據事件內容判斷可能的攻擊或活動類型"
)
risk: str = Field(
description="用繁體中文說明此事件可能造成的資安風險"
)
# 確定性的 Severity Mapping
severity_map = {
1: SeverityLevel.HIGH,
2: SeverityLevel.MEDIUM,
3: SeverityLevel.LOW
}
# 從 SQLite 讀取最新事件
conn = sqlite3.connect("soc.db")
conn.row_factory = sqlite3.Row
event = conn.execute(
"SELECT * FROM events ORDER BY id DESC LIMIT 1"
).fetchone()
conn.close()
if event is None:
print("No security events found.")
exit()
# Severity 由 Backend 處理
severity_level = severity_map.get(event["severity"])
if severity_level is None:
print("Unknown severity value:", event["severity"])
exit()
# 準備給 Gemini 的事件資料
event_text = f"""
Event Type: {event["event_type"]}
Source IP: {event["src_ip"]}
Destination IP: {event["dest_ip"]}
Destination Port: {event["dest_port"]}
Protocol: {event["protocol"]}
Signature: {event["signature"]}
"""
prompt = f"""
You are a SOC Analyst Assistant.
The following content is security event data.
Analyze the event and explain:
1. What happened
2. The possible attack type
3. The possible security risk
Keep the explanation concise.
Do not make the final blocking decision.
Security Event:
{event_text}
"""
response = client.models.generate_content(
model="gemini-flash-lite-latest",
contents=prompt,
config={
"response_mime_type": "application/json",
"response_schema": AIAnalysis,
},
)
# SDK 直接解析成 AIAnalysis
result = response.parsed
print("===== Original Security Event =====")
print("Event ID:", event["id"])
print("Event Type:", event["event_type"])
print("Source IP:", event["src_ip"])
print("Destination IP:", event["dest_ip"])
print()
print("===== Backend Deterministic Rule =====")
print("Severity:", severity_level.value)
print()
print("===== Gemini AI Analysis =====")
print("事件摘要:", result.summary)
print("攻擊類型:", result.attack_type)
print("可能風險:", result.risk)

今天我們做了三個重要調整:首先改用程式碼對應(severity_map.get)來決定事件嚴重性,不再交由 Gemini 判斷;其次在 Schema 中透過 Field(description="...") 明確定義 summary、attack_type 與 risk 欄位的作用;最後則將回應解析方式從原本的 json.loads(response.text) 改為直接取用結構化解析後的 response.parsed。