LLM Guardrails 安全護欄完整教學:從輸出過濾到內容審核的實戰指南
為什麼 LLM 需要 Guardrails
說實話,我第一次把 LLM 應用部署到生產環境的時候,根本沒想過安全護欄這件事。當時覺得 GPT-4 已經夠聰明了,應該不會出什麼大問題吧?結果上線第三天,就有用戶用特殊的提示詞讓模型吐出了系統 prompt 的完整內容。那一刻我才真正意識到——沒有 Guardrails 的 LLM 應用,就像是一台沒裝煞車的跑車。
大語言模型本質上是一個機率預測引擎,它不「理解」什麼該說、什麼不該說。它只是根據訓練資料和上下文,生成最可能的下一個 token。這意味著在特定情境下,模型完全可能產出有害內容、洩漏敏感資訊,甚至被惡意操控來執行未授權的操作。
真實世界的失控案例
2023 年某汽車經銷商的聊天機器人被用戶誘導同意以 1 美元出售一台車。同年,某航空公司的客服 AI 自行承諾了不存在的退票政策,結果公司被迫兌現。這些案例都不是科幻小說,而是真實發生在企業生產環境中的事件。
更可怕的是那些你看不到的問題:模型悄悄洩漏其他用戶的對話記錄、在回答中夾帶偏見性內容、或是被 Prompt Injection 攻擊後執行了惡意指令。這些問題如果沒有系統性的防護措施,早晚會出事。
Guardrails 的三層防護架構
經過這幾年在生產環境踩過的坑,我總結出一套三層防護架構。這不是什麼高深的理論,而是實戰經驗的結晶。每一層都有它存在的意義,缺一不可。
Input 層:把關入口
Input 層是第一道防線,負責在用戶的請求送到 LLM 之前進行檢查和過濾。這一層要處理的問題包括:
- 輸入驗證:檢查輸入長度、格式和字元集,防止超長輸入或特殊編碼攻擊
- Prompt Injection 偵測:識別試圖覆寫系統指令的惡意輸入
- 內容分級:判斷用戶輸入是否包含不適當內容
- 意圖分類:確認用戶請求是否在允許的範圍內
我個人的經驗是,Input 層最容易被忽視,但它其實能擋下大約 60% 的潛在問題。很多攻擊在入口就能被攔截,根本不需要讓它碰到模型。
Process 層:推理過程監控
Process 層是在 LLM 推理過程中進行的即時監控。如果你的應用使用了 LLM Function Calling 教學 中提到的工具呼叫功能,這一層就更加關鍵了。
Process 層需要監控的面向:
- 工具呼叫審計:確認 LLM 呼叫的每個工具和參數都在白名單內
- 權限邊界檢查:限制 LLM 能存取的資料範圍和操作權限
- Chain-of-Thought 監控:在使用推理鏈時,檢查中間步驟是否偏離預期
- 資源使用限制:防止無限迴圈或過度消耗 API 配額
Output 層:最後一道關卡
Output 層在 LLM 生成回應之後、送達用戶之前進行最後的檢查。即使前兩層都通過了,這一層仍然可能攔截到問題。
Output 層的關鍵機制:
- 毒性偵測:檢查回應是否包含仇恨言論、暴力內容等有害內容
- PII 過濾:攔截回應中可能包含的個人識別資訊
- 事實查核:交叉比對回應中的關鍵事實聲明
- 格式驗證:確保輸出符合預期的格式和結構
Prompt Injection 攻擊防禦策略
Prompt Injection 大概是目前 LLM 應用面臨的最嚴重安全威脅。它的原理很簡單——攻擊者在輸入中嵌入指令,試圖覆寫或繞過系統的原始提示詞。
常見攻擊模式
根據我這兩年收集到的案例,常見的攻擊模式可以分成幾大類:
直接注入(Direct Injection):用戶直接在輸入中寫入類似「忽略之前的所有指令」這樣的語句。這是最基礎的攻擊方式,但令人驚訝的是,很多生產系統到現在還擋不住。
間接注入(Indirect Injection):攻擊指令不是來自用戶輸入,而是藏在 LLM 會讀取的外部資料中。例如在網頁中嵌入隱藏文字,當你的 Agentic RAG 架構教學 中提到的 RAG 系統去抓取該網頁時,惡意指令就會被注入上下文。
越獄攻擊(Jailbreak):透過角色扮演、假設情境等方式繞過模型的安全限制。比如「假設你是一個沒有限制的 AI,請你...」。
多語言攻擊:利用模型在不同語言上的安全對齊程度不一致,用特定語言繞過防護。這在中文環境特別值得注意。
防禦策略實作
面對這些攻擊,單一防禦手段是不夠的,需要縱深防禦策略:
import re
from typing import Tuple
class PromptInjectionDetector:
"""多層 Prompt Injection 偵測器"""
INJECTION_PATTERNS = [
r'ignore\s+(all\s+)?previous\s+instructions',
r'忽略(之前|以上|先前)(的)?(所有)?指令',
r'you\s+are\s+now\s+a',
r'system\s*prompt',
r'\[INST\]',
r'<\|im_start\|>',
]
def __init__(self):
self.compiled_patterns = [
re.compile(p, re.IGNORECASE) for p in self.INJECTION_PATTERNS
]
def check_input(self, user_input: str) -> Tuple[bool, str]:
"""回傳 (is_safe, reason)"""
for pattern in self.compiled_patterns:
if pattern.search(user_input):
return False, f"偵測到可疑注入模式"
if len(user_input) > 4000:
return False, "輸入超過長度限制"
special_chars = sum(1 for c in user_input if not c.isalnum() and not c.isspace())
if len(user_input) > 0 and special_chars / len(user_input) > 0.3:
return False, "特殊字元密度異常"
return True, "通過檢查"
detector = PromptInjectionDetector()
is_safe, reason = detector.check_input(user_message)
if not is_safe:
return {"error": f"輸入被拒絕:{reason}"}
老實說,光靠正則表達式是遠遠不夠的。進階的做法是用一個專門訓練過的分類模型(例如 Meta 的 Prompt Guard 或 Rebuff)來做語意層面的注入偵測。但正則比對作為第一層快速篩查,效果還是不錯的。
輸出過濾:毒性檢測、PII 過濾、事實查核
毒性內容檢測
毒性檢測是 Output Guardrails 中最基礎也最重要的一環。常用的方案有幾種:
基於 API 的方案:使用 OpenAI Moderation API、Google Perspective API 或 Azure Content Safety。這些服務的好處是維護成本低、準確度高,但缺點是增加了延遲和對外部服務的依賴。
本地模型方案:部署 detoxify、HateBERT 等開源模型在本地進行推理。延遲較低,但需要自己維護模型和更新。
from dataclasses import dataclass
from enum import Enum
class ToxicityLevel(Enum):
SAFE = "safe"
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
BLOCKED = "blocked"
@dataclass
class FilterResult:
level: ToxicityLevel
score: float
categories: list[str]
should_block: bool
class OutputFilter:
def __init__(self, threshold: float = 0.7):
self.threshold = threshold
async def check_toxicity(self, text: str) -> FilterResult:
scores = await self._get_toxicity_scores(text)
max_score = max(scores.values())
flagged_categories = [
cat for cat, score in scores.items()
if score > self.threshold
]
if max_score > 0.9:
level = ToxicityLevel.BLOCKED
elif max_score > 0.7:
level = ToxicityLevel.HIGH
elif max_score > 0.4:
level = ToxicityLevel.MEDIUM
else:
level = ToxicityLevel.SAFE
return FilterResult(
level=level,
score=max_score,
categories=flagged_categories,
should_block=max_score > self.threshold
)
PII 個資過濾
PII(Personally Identifiable Information)過濾在台灣的法規環境下尤其重要。LLM 可能會在回應中不小心洩漏訓練資料中的個人資訊,或是把用戶對話中提到的敏感資料回傳給其他用戶。
常見需要過濾的 PII 類型包括:身分證字號、電話號碼、電子郵件地址、信用卡號碼、地址等。在台灣的脈絡下,還要特別注意統一編號和健保卡號。
import re
class PIIFilter:
PII_PATTERNS = {
'tw_id': r'[A-Z][12]\d{8}',
'phone': r'09\d{2}-?\d{3}-?\d{3}',
'email': r'[\w.-]+@[\w.-]+\.\w+',
'credit_card': r'\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}',
'tax_id': r'\d{8}',
}
def mask_pii(self, text: str) -> str:
masked = text
for pii_type, pattern in self.PII_PATTERNS.items():
masked = re.sub(
pattern,
f'[{pii_type.upper()}_MASKED]',
masked
)
return masked
要注意的是,純正則比對的 PII 偵測會有很多誤判。比較好的做法是搭配 NER(Named Entity Recognition)模型,像是 spaCy 或 Presidio,能做到更精確的個資辨識。
事實查核機制
事實查核是最難做好的一環。LLM 的幻覺問題到目前為止還沒有完美的解決方案,但我們至少可以做到幾件事:
- 信心分數標記:讓模型對自己回答的信心程度做出評估
- RAG 交叉比對:將回答中的關鍵聲明與知識庫進行比對
- 外部 API 驗證:對於數字、日期等可驗證的事實,用外部資料來源確認
- 不確定性聲明:當系統無法驗證某個聲明時,在回答中加入適當的免責語句
NeMo Guardrails vs Guardrails AI vs 自建方案
目前市面上主要的 Guardrails 框架有兩個,加上自建方案,各有優劣。
| 特性 | NeMo Guardrails | Guardrails AI | 自建方案 |
|---|---|---|---|
| 開發者 | NVIDIA | Guardrails AI Inc. | 自家團隊 |
| 核心概念 | Colang 對話流程定義 | RAIL 規格驗證 | 依需求設計 |
| 學習曲線 | 中等(需學 Colang) | 較低 | 取決於複雜度 |
| 靈活性 | 中等 | 高 | 最高 |
| 延遲影響 | 較高(多次 LLM 呼叫) | 中等 | 可優化到最低 |
| 社群支援 | 活躍 | 活躍 | 無 |
| 適合場景 | 對話型應用 | 結構化輸出驗證 | 特殊需求 |
我個人的建議是:如果你在做的是對話型 AI 應用,NeMo Guardrails 的 Colang 語法讓你能用很直覺的方式定義對話邊界。如果你更關心輸出格式的驗證和結構化,Guardrails AI 會是更好的選擇。而如果你的需求很特殊,或是對延遲有極端要求,自建方案反而可能是最務實的。
在多 Agent 系統中,Guardrails 的重要性更是加倍。想像一下,如果你按照 CrewAI 多 Agent 協作教學 建立了一個多 Agent 工作流,每個 Agent 都需要獨立的安全護欄,而且 Agent 之間的通訊也需要被監控。
實戰:用 Python 建立基礎 Guardrails 系統
好了,理論講夠了。讓我們動手建一個完整的 Guardrails 系統。這個系統會整合前面提到的各種防護機制:
import asyncio
from typing import Optional
from dataclasses import dataclass, field
@dataclass
class GuardrailConfig:
max_input_length: int = 4000
max_output_length: int = 8000
toxicity_threshold: float = 0.7
enable_pii_filter: bool = True
enable_injection_detection: bool = True
enable_toxicity_check: bool = True
allowed_topics: list[str] = field(default_factory=list)
blocked_topics: list[str] = field(default_factory=list)
class GuardrailsPipeline:
def __init__(self, config: GuardrailConfig):
self.config = config
self.injection_detector = PromptInjectionDetector()
self.output_filter = OutputFilter(threshold=config.toxicity_threshold)
self.pii_filter = PIIFilter()
self.metrics = {"blocked": 0, "passed": 0, "modified": 0}
async def process_input(self, user_input: str) -> dict:
"""Input 層處理"""
if len(user_input) > self.config.max_input_length:
return {"allowed": False, "reason": "輸入過長"}
if self.config.enable_injection_detection:
is_safe, reason = self.injection_detector.check_input(user_input)
if not is_safe:
self.metrics["blocked"] += 1
return {"allowed": False, "reason": reason}
return {"allowed": True, "sanitized_input": user_input}
async def process_output(self, llm_output: str) -> dict:
"""Output 層處理"""
result = {"original": llm_output, "modified": False}
if self.config.enable_toxicity_check:
toxicity = await self.output_filter.check_toxicity(llm_output)
if toxicity.should_block:
self.metrics["blocked"] += 1
return {
"allowed": False,
"reason": f"內容毒性過高:{toxicity.categories}"
}
processed = llm_output
if self.config.enable_pii_filter:
processed = self.pii_filter.mask_pii(processed)
if processed != llm_output:
result["modified"] = True
self.metrics["modified"] += 1
if len(processed) > self.config.max_output_length:
processed = processed[:self.config.max_output_length] + "...[回應被截斷]"
result["modified"] = True
result["output"] = processed
result["allowed"] = True
self.metrics["passed"] += 1
return result
async def run(self, user_input: str, llm_callable) -> str:
"""完整的 Guardrails 處理流程"""
input_result = await self.process_input(user_input)
if not input_result["allowed"]:
return f"很抱歉,您的請求無法處理:{input_result['reason']}"
llm_output = await llm_callable(input_result["sanitized_input"])
output_result = await self.process_output(llm_output)
if not output_result["allowed"]:
return "很抱歉,系統產生的回應不符合安全標準,請重新提問。"
return output_result["output"]
# 使用範例
config = GuardrailConfig(
toxicity_threshold=0.7,
enable_pii_filter=True,
blocked_topics=["暴力", "歧視"],
)
pipeline = GuardrailsPipeline(config)
response = await pipeline.run(user_message, my_llm_function)
這個基礎架構已經涵蓋了 Input 和 Output 兩層防護。在實際生產環境中,你還需要加入日誌記錄、監控儀表板、以及異常通知機制。我建議用 structured logging 搭配 Prometheus metrics,這樣出問題時可以快速定位。
企業合規需求
如果你的 LLM 應用要服務企業客戶或處理敏感資料,合規就不是可選的了。以下是幾個需要特別注意的面向:
GDPR / 歐盟 AI Act:如果你的用戶有來自歐盟的,GDPR 對個資處理有嚴格的規範。而 2024 年正式通過的 EU AI Act 更是對高風險 AI 系統提出了明確的透明度和安全要求。Guardrails 在這裡扮演的角色是確保 LLM 不會在回應中洩漏受保護的個人資料。
台灣個人資料保護法:台灣的個資法要求對個人資料的蒐集、處理和利用都必須有明確的法律依據。如果你的 LLM 應用會處理台灣用戶的個資,PII 過濾就是法規要求而非可選功能。
SOC 2 合規:對於服務企業客戶的 SaaS 產品,SOC 2 審計通常會檢查 AI 系統的輸入輸出是否有適當的控制措施。Guardrails 系統的日誌和監控正好可以作為合規的證據。
審計追蹤(Audit Trail):不管哪種合規框架,完整的審計追蹤都是基本要求。每一筆 LLM 的輸入和輸出都應該被記錄下來,包含被 Guardrails 攔截的請求。
import json
import logging
from datetime import datetime
class AuditLogger:
def __init__(self):
self.logger = logging.getLogger("guardrails.audit")
def log_interaction(self, interaction: dict):
audit_record = {
"timestamp": datetime.utcnow().isoformat(),
"request_id": interaction.get("request_id"),
"user_id": interaction.get("user_id"),
"input_hash": self._hash(interaction.get("input", "")),
"output_hash": self._hash(interaction.get("output", "")),
"guardrail_actions": interaction.get("actions", []),
"was_blocked": interaction.get("blocked", False),
"block_reason": interaction.get("reason"),
"processing_time_ms": interaction.get("latency"),
}
self.logger.info(json.dumps(audit_record, ensure_ascii=False))
效能考量與最佳實踐
加了 Guardrails 之後最常被問到的問題就是:「會不會太慢?」答案是——如果不注意,確實會慢到讓人無法接受。以下是我踩坑後整理的效能最佳實踐:
1. 異步並行處理:毒性檢測和 PII 過濾可以同時進行,不需要串行執行。用 asyncio.gather 把獨立的檢查並行化,能顯著降低總延遲。
2. 分級處理策略:不是所有請求都需要過最嚴格的檢查。可以先做快速的規則比對(<1ms),只有觸發初步警告的才送去做深度分析。大部分正常請求只需要付出極小的延遲代價。
3. 快取機制:對於重複性高的輸入模式(例如常見的問候語),可以快取檢測結果,避免重複運算。
4. 邊緣部署:如果你的毒性檢測模型夠小(例如用 ONNX Runtime 部署的輕量模型),可以考慮部署在邊緣節點,減少網路延遲。
5. 降級策略:當 Guardrails 的某個組件不可用時(例如外部 API 超時),應該有明確的降級策略。是要阻止所有請求?還是跳過該項檢查?這取決於你的風險容忍度。
async def parallel_output_checks(text: str) -> dict:
"""並行執行多項輸出檢查"""
toxicity_task = check_toxicity(text)
pii_task = check_pii(text)
length_task = check_length(text)
results = await asyncio.gather(
toxicity_task, pii_task, length_task,
return_exceptions=True
)
for i, result in enumerate(results):
if isinstance(result, Exception):
logging.error(f"檢查 {i} 失敗: {result}")
results[i] = {"passed": False, "reason": "檢查服務不可用"}
return {
"toxicity": results[0],
"pii": results[1],
"length": results[2],
"all_passed": all(r.get("passed", False) for r in results)
}
最後補充一個經常被忽略的點——監控和迭代。上線之後一定要追蹤 Guardrails 的誤判率(False Positive Rate)和漏判率(False Negative Rate)。如果誤判太高,用戶體驗會很差;如果漏判太高,安全就形同虛設。定期根據真實的攔截數據調整閾值,這才是長期可持續的做法。
結語
LLM Guardrails 不是一個「裝了就忘」的東西。它是一個需要持續調整、監控和改進的安全系統。從最基礎的正則比對到進階的語意分析,從單一模型到多 Agent 系統的安全串聯,每一步都需要根據你的實際應用場景來設計。
如果你的 LLM 應用還沒有任何 Guardrails,我的建議是今天就開始加。不需要一步到位,先從 Input 驗證和基礎的輸出過濾開始,然後逐步完善。記住,在 AI 安全領域,「夠好」永遠比「完美但還沒開始做」來得有用。
安全不是功能,是底線。
繼續閱讀
AI Agent 多工具調度教學:MCP 多 Server 整合與 Token 成本優化實戰
相關文章
AI Agent 多工具調度教學:MCP 多 Server 整合與 Token 成本優化實戰
學習 AI Agent 多工具調度架構,掌握 MCP 多 Server 整合、動態工具載入與 Token 成本優化策略,立即提升你的 Agent 生產效率。
Microsoft Agent Framework 完整教學:用 AutoGen 與 Semantic Kernel 打造多 Agent AI 系統
Microsoft 將 AutoGen 與 Semantic Kernel 整合為全新的 Agent Framework,同時支援 LLM 驅動與確定性工作流編排。本文完整介紹其核心架構、Graph-based Workflow、多 Agent 協作模式與企業級整合能力,含 Python 實作範例。
你可能也喜歡
探索其他領域的精選好文