文檔版本: v2.0 Enterprise | 發布日期: 2026-03-20 | 針對: 企業隱私、資料安全、本地部署
📑 目錄
🎯 核心挑戰 — 三層矛盾
企業要用 AI 分析敏感資料,但面臨三個互相衝突的需求:
| 需求 | 挑戰 | 風險 |
|---|---|---|
| 用 AI 智能分析資料 | LLM 需要看到足夠的上下文才能分析準確 | 如果資料不完整,分析結果錯誤 |
| 資料不離開本地伺服器 | 敏感資料(客戶名、電話、身份證)不能上雲 | 違反 GDPR、個資法、企業隱私政策 |
| 最後還是需要看原始敏感資訊 | 脫敏後的資料需要恢復成原始值供人工審核 | 如果無法準確恢復,所有分析都沒用 |
解決方案: 四層脫敏-分析-恢復架構,確保 masked values 在整個流程中被保留和追蹤。
🏗️ 四層架構設計
架構圖
┌─────────────────────────────────────────────────────────────────┐
│ 第 1 層:原始資料(iDempiere 資料庫) │
│ ───────────────────────────────────────────────────────────── │
│ Customer: ABC Corp │
│ Amount: $50,000 │
│ Phone: +886-2-1234-5678 │
│ Email: [email protected] │
└─────────────────────────────┬─────────────────────────────────────┘
│
🔐 脫敏層 (MASK)
│
┌─────────────────────────────▼─────────────────────────────────────┐
│ 第 2 層:脫敏層(本地加密 + 映射表) │
│ ───────────────────────────────────────────────────────────── │
│ Customer: CUST_A1B2C3D4 ← 記錄映射 (AES-256 加密) │
│ Amount: $50,000 ← 金額不脫敏(需要分析) │
│ Phone: +886-***-****-78 ← 電話遮罩 │
│ Email: j***@abccorp.com ← 郵箱遮罩 │
│ │
│ 映射表(本地密鑰管理): │
│ CUST_A1B2C3D4 ←→ "ABC Corp" (AES-256 加密) │
└─────────────────────────────┬─────────────────────────────────────┘
│
📤 發送給 LLM 分析
(脫敏資料保留 masked values)
│
┌─────────────────────────────▼─────────────────────────────────────┐
│ 第 3 層:LLM 分析(本地 Ollama 或雲端 Claude) │
│ ───────────────────────────────────────────────────────────── │
│ 輸入:「Customer CUST_A1B2C3D4 有 $50,000 訂單...」 │
│ 分析邏輯:趨勢、風險、建議 │
│ 輸出:「Customer CUST_A1B2C3D4 是最大客戶,風險等級...」 │
│ │
│ ⭐ 關鍵:LLM 輸出中仍然包含 CUST_A1B2C3D4 │
└─────────────────────────────┬─────────────────────────────────────┘
│
🔓 恢復層 (REVERSE)
(根據用戶權限查詢映射表)
│
┌─────────────────────────────▼─────────────────────────────────────┐
│ 第 4 層:權限控制恢復 + 審計日誌 │
│ ───────────────────────────────────────────────────────────── │
│ Admin:「Customer ABC Corp 是最大客戶...」 ✅ 完全恢復 │
│ Manager:「Customer ABC Corp 是最大客戶...」 ✅ 有限恢復 │
│ Analyst:「Customer CUST_A1B2C3D4 是最大客戶...」 ❌ 不可恢復 │
│ Viewer:「Customer *** 是最大客戶...」 ❌ 完全遮罩 │
│ │
│ 審計日誌: │
│ [2026-03-20 14:30:45] ADMIN 試圖恢復 CUST_A1B2C3D4 ✅ 成功 │
│ [2026-03-20 14:31:12] ANALYST 試圖恢復 CUST_A1B2C3D4 ❌ 拒絕 │
└─────────────────────────────────────────────────────────────────────┘
四層的角色
| 層 | 位置 | 功能 | 安全機制 |
|---|---|---|---|
| 第 1 層 原始資料 |
iDempiere DB (本地) |
存儲完整的敏感資料 | DB 加密、訪問控制 |
| 第 2 層 脫敏層 |
Python 應用 (本地記憶體) |
脫敏資料、管理映射表、產生脫敏版本 | AES-256 加密、本地密鑰 |
| 第 3 層 LLM 分析 |
Ollama 或 Claude API |
只看脫敏資料,不知道原始值 | LLM 無法反向推測原始值 |
| 第 4 層 恢復層 |
Python 應用 (本地) |
識別 LLM 輸出中的 masked values,根據權限恢復 | 權限檢查、審計日誌 |
⚖️ 本地 LLM vs 雲端模型
| 方面 | 本地 LLM(Ollama) | 雲端模型(Claude API) | 建議方案 |
|---|---|---|---|
| 隱私 | ✅ 資料完全本地 ✅ 沒有外洩風險 |
⚠️ 脫敏後上雲 ✅ 有企業協議保護 |
偏好本地 |
| 分析質量 | ⚠️ Llama 2/Mistral ❌ 複雜邏輯可能不夠準確 |
✅ Claude 很強 ✅ 複雜推理更準確 |
偏好 Claude |
| 成本 | ✅ 一次投資 ✅ GPU: $5-10k ✅ 無持續費用 |
❌ 按使用量付費 ❌ $0.003/1K tokens ❌ 高量時昂貴 |
看量級 |
| 維護 | ❌ 需要 GPU、記憶體 ❌ 模型更新自己管理 |
✅ 完全託管 ✅ 模型自動更新 |
看資源 |
混合方案(推薦)
70% 簡單查詢用本地 Ollama,30% 複雜分析用 Claude。成本和質量的最佳平衡。
💰 5 年成本分析
| 方案 | 初期投資 | 年度運營 | 5 年總成本 |
|---|---|---|---|
| ✅ 本地 Ollama(100%) | $8,000 | $1,000 | $13,000 |
| ❌ Claude API(100%) | $0 | $4,920 | $24,600 |
| ⭐ 混合(70% 本地 + 30% Claude) | $8,000 | $1,876 | $17,380 |
💻 代碼實現 — MASK 和 REVERSE 的完整流程
⭐ 核心要點: Masked values(如 CUST_A1B2C3D4)必須在整個流程中被保留,這樣才能在 LLM 輸出中追蹤和恢復。
Step 1: TOOL 輸出原始資料(未脫敏)
# 從 iDempiere 資料庫查詢
TOOL_OUTPUT = {
"customer_name": "ABC Corp",
"customer_phone": "+886-2-1234-5678",
"customer_email": "[email protected]",
"order_amount": 50000,
"order_date": "2026-03-20"
}
Step 2: MASK 脫敏 — 建立映射表並記錄
from cryptography.fernet import Fernet
import hashlib
class DataMaskingEngine:
def __init__(self):
# 本地密鑰(企業安全存儲)
self.encryption_key = Fernet.generate_key()
self.cipher = Fernet(self.encryption_key)
# 映射表:masked_value → 加密的原始值
self.mapping_table = {}
def mask_customer_name(self, original_name):
"""脫敏客戶名:原始值 → CUST_HASH"""
# 1. 生成 hash
hash_value = hashlib.sha256(
original_name.encode()
).hexdigest()[:8].upper()
masked_value = f"CUST_{hash_value}"
# 2. 加密原始值並存入映射表
encrypted_original = self.cipher.encrypt(
original_name.encode()
)
self.mapping_table[masked_value] = encrypted_original
# 3. 返回 masked value
return masked_value
def mask_phone(self, original_phone):
"""脫敏電話:只保留最後 2 碼"""
return f"{original_phone[:6]}***-****-{original_phone[-2:]}"
def mask_email(self, original_email):
"""脫敏郵箱:只保留第一個字母和域名"""
local, domain = original_email.split('@')
masked_local = f"{local[0]}***"
return f"{masked_local}@{domain}"
# 執行脫敏
masking_engine = DataMaskingEngine()
masked_data = {
"customer_name": masking_engine.mask_customer_name(
TOOL_OUTPUT["customer_name"] # "ABC Corp"
), # 結果:CUST_A1B2C3D4
"customer_phone": masking_engine.mask_phone(
TOOL_OUTPUT["customer_phone"] # "+886-2-1234-5678"
), # 結果:+886-***-****-78
"customer_email": masking_engine.mask_email(
TOOL_OUTPUT["customer_email"] # "[email protected]"
), # 結果:j***@abccorp.com
"order_amount": TOOL_OUTPUT["order_amount"], # 50000(不脫敏)
"order_date": TOOL_OUTPUT["order_date"] # 2026-03-20(不脫敏)
}
print("✅ 脫敏後的資料:")
print(masked_data)
# {
# "customer_name": "CUST_A1B2C3D4",
# "customer_phone": "+886-***-****-78",
# "customer_email": "j***@abccorp.com",
# "order_amount": 50000,
# "order_date": "2026-03-20"
# }
print("\n🔐 映射表(本地加密):")
for masked_val, encrypted_original in masking_engine.mapping_table.items():
print(f" {masked_val} ←→ {encrypted_original[:20]}...")
# CUST_A1B2C3D4 ←→ gAAAAAB...(加密後)
Step 3: 發送脫敏資料給 LLM —— 保留 Masked Values
# 構造給 LLM 的提示
llm_prompt = f"""
分析以下客戶訂單資料:
- 客戶:{masked_data['customer_name']}
- 訂單金額:${masked_data['order_amount']}
- 訂單日期:{masked_data['order_date']}
- 聯絡方式:{masked_data['customer_email']}
請提供:
1. 這筆訂單的風險等級
2. 建議的後續行動
3. 客戶信用評分
"""
print("📤 發送給 LLM 的提示(脫敏 + 保留 masked values):")
print(llm_prompt)
# 注意:CUST_A1B2C3D4 被保留在提示中!
# 呼叫 LLM(本地 Ollama 或 Claude API)
# 重點:LLM 看不到原始的 "ABC Corp",只看到 "CUST_A1B2C3D4"
llm_response = """
分析結果:
- 客戶 CUST_A1B2C3D4 的風險等級:低
- 建議:增加信用額度
- 信用評分:900(優良)
"""
print("\n📥 LLM 輸出(仍然包含 CUST_A1B2C3D4):")
print(llm_response)
Step 4: REVERSE 恢復 —— 識別 Masked Values 並恢復
import re
class DataRecoveryLayer:
def __init__(self, masking_engine, user_role):
self.masking_engine = masking_engine
self.user_role = user_role # admin, manager, analyst, viewer
self.audit_log = []
# 權限定義
self.permissions = {
"admin": ["customer_name", "phone", "email"],
"manager": ["customer_name"],
"analyst": [], # 不能恢復任何敏感資訊
"viewer": []
}
def can_restore(self, field_type):
"""檢查用戶是否有權限恢復該欄位"""
return field_type in self.permissions.get(self.user_role, [])
def unmask_value(self, masked_value):
"""恢復 masked value 為原始值"""
if masked_value not in self.masking_engine.mapping_table:
return masked_value # 不是 masked value
# 從映射表取出加密的原始值
encrypted_original = self.masking_engine.mapping_table[masked_value]
# 解密
original_value = self.masking_engine.cipher.decrypt(
encrypted_original
).decode()
return original_value
def restore_response(self, llm_output):
"""
在 LLM 輸出中識別並恢復 masked values
根據用戶權限決定是否恢復
"""
restored_output = llm_output
# 步驟 1: 在 LLM 輸出中找到所有 masked values
# 模式:CUST_XXXXXXXX(8 位十六進制)
masked_patterns = re.findall(r'CUST_[A-F0-9]{8}', llm_output)
# 步驟 2: 對每個 masked value 進行恢復
for masked_value in masked_patterns:
# 記錄審計日誌
self._log_access_attempt(masked_value)
# 檢查權限
if self.can_restore("customer_name"):
# 有權限:恢復為原始值
original_value = self.unmask_value(masked_value)
restored_output = restored_output.replace(
masked_value,
original_value
)
self._log_access_success(masked_value, original_value)
else:
# 無權限:保留 masked value
self._log_access_denied(masked_value)
return restored_output
def _log_access_attempt(self, masked_value):
"""記錄訪問嘗試"""
self.audit_log.append({
"timestamp": "2026-03-20 14:30:45",
"user_role": self.user_role,
"action": "ATTEMPT_UNMASK",
"masked_value": masked_value
})
def _log_access_success(self, masked_value, original_value):
"""記錄成功恢復"""
self.audit_log.append({
"timestamp": "2026-03-20 14:30:45",
"user_role": self.user_role,
"action": "UNMASK_SUCCESS",
"masked_value": masked_value,
"original_length": len(original_value) # 不記錄原始值本身
})
def _log_access_denied(self, masked_value):
"""記錄被拒絕的訪問"""
self.audit_log.append({
"timestamp": "2026-03-20 14:30:45",
"user_role": self.user_role,
"action": "UNMASK_DENIED",
"masked_value": masked_value,
"reason": "INSUFFICIENT_PERMISSIONS"
})
# 執行恢復 —— 根據用戶角色
print("\n=== REVERSE 過程:根據權限恢復 ===\n")
llm_output = """
分析結果:
- 客戶 CUST_A1B2C3D4 的風險等級:低
- 建議:增加信用額度
- 信用評分:900(優良)
"""
# 案例 1: Admin(有所有權限)
print("【Admin 用戶】")
recovery_admin = DataRecoveryLayer(masking_engine, "admin")
restored_admin = recovery_admin.restore_response(llm_output)
print(restored_admin)
# 輸出:客戶 ABC Corp 的風險等級:低...
# 案例 2: Manager(只能看客戶名)
print("\n【Manager 用戶】")
recovery_manager = DataRecoveryLayer(masking_engine, "manager")
restored_manager = recovery_manager.restore_response(llm_output)
print(restored_manager)
# 輸出:客戶 ABC Corp 的風險等級:低...
# 案例 3: Analyst(無法恢復敏感資訊)
print("\n【Analyst 用戶】")
recovery_analyst = DataRecoveryLayer(masking_engine, "analyst")
restored_analyst = recovery_analyst.restore_response(llm_output)
print(restored_analyst)
# 輸出:客戶 CUST_A1B2C3D4 的風險等級:低... (沒有恢復)
# 案例 4: Viewer(完全遮罩)
print("\n【Viewer 用戶】")
recovery_viewer = DataRecoveryLayer(masking_engine, "viewer")
restored_viewer = recovery_viewer.restore_response(llm_output)
print(restored_viewer)
# 輸出:客戶 CUST_A1B2C3D4 的風險等級:低... (沒有恢復)
# 審計日誌
print("\n=== 審計日誌 ===")
for admin_log in recovery_admin.audit_log:
print(f"✅ {admin_log['timestamp']} | {admin_log['user_role']} | {admin_log['action']}")
for analyst_log in recovery_analyst.audit_log:
print(f"❌ {analyst_log['timestamp']} | {analyst_log['user_role']} | {analyst_log['action']} (拒絕)")
Step 5: 完整流程圖
原始資料(TOOL)
↓
ABC Corp, +886-2-1234-5678, [email protected]
↓
↓ 🔐 MASK (脫敏層)
↓
├─ Customer: ABC Corp → CUST_A1B2C3D4 (記錄映射表)
├─ Phone: +886-2-1234-5678 → +886-***-****-78
└─ Email: [email protected] → j***@abccorp.com
↓
脫敏資料 + Masked Values
↓
├─ Customer: CUST_A1B2C3D4 ← 保留!
├─ Phone: +886-***-****-78
└─ Email: j***@abccorp.com
↓
↓ 📤 發送給 LLM(本地 Ollama 或 Claude API)
↓
LLM 分析脫敏資料
↓
├─ 輸入: "Customer CUST_A1B2C3D4 有 $50,000 訂單"
├─ 分析: 風險評估、信用評分
└─ 輸出: "Customer CUST_A1B2C3D4 的風險等級:低"
↓
LLM 輸出(仍然包含 CUST_A1B2C3D4)
↓
↓ 🔓 REVERSE (恢復層)
↓
檢查用戶權限
↓
├─ Admin ✅ 可以恢復 → "Customer ABC Corp 的風險等級:低"
├─ Manager ✅ 可以恢復 → "Customer ABC Corp 的風險等級:低"
├─ Analyst ❌ 不可恢復 → "Customer CUST_A1B2C3D4 的風險等級:低"
└─ Viewer ❌ 不可恢復 → "Customer CUST_A1B2C3D4 的風險等級:低"
↓
📋 審計日誌
├─ [Admin] UNMASK_SUCCESS: CUST_A1B2C3D4
├─ [Manager] UNMASK_SUCCESS: CUST_A1B2C3D4
├─ [Analyst] UNMASK_DENIED: CUST_A1B2C3D4
└─ [Viewer] UNMASK_DENIED: CUST_A1B2C3D4
關鍵安全要點
| 安全考慮 | 怎麼做 | 為什麼重要 |
|---|---|---|
| 💾 映射表保存 | 本地加密(AES-256)+ 安全存儲(不在代碼中) | 如果映射表洩露,攻擊者可以反推原始值 |
| 🔑 密鑰管理 | 密鑰存在環境變數或 AWS KMS,不在代碼裡 | 代碼洩露時,沒有密鑰,攻擊者無法解密 |
| 📊 Masked Values 保留 | MASK 時使用確定性 HASH(同一值每次都產生相同 masked value) | LLM 輸出中的 masked value 必須與 MASK 時的相同,才能查詢映射表 |
| 🛡️ 權限檢查 | REVERSE 前檢查用戶角色,然後才恢復 | 即使 LLM 輸出正確,無權限的用戶也看不到原始值 |
| 📜 審計日誌 | 記錄所有訪問嘗試(成功和失敗) | 符合 GDPR(日誌保留 7 年)、監測異常訪問、事件追蹤 |
| 🚨 異常檢測 | 監測同一用戶短時間內多次失敗的恢復嘗試 | 可能是在試圖猜測或攻擊,觸發告警 |
✅ 企業部署清單
安全檢查
- ✅ 加密密鑰不在代碼庫中(存在 AWS KMS、Azure Key Vault 或環境變數)
- ✅ 映射表存儲位置是加密的資料庫或本地檔案(不可讀權限)
- ✅ 所有敏感操作都有日誌記錄
- ✅ 定期備份和災難恢復計畫
- ✅ 權限模型已定義並測試(admin/manager/analyst/viewer)
- ✅ SQL injection、XSS 等防護已實施
隱私合規
- ✅ GDPR 合規:資料最小化、訪問控制、刪除權
- ✅ 台灣個資法:敏感個資完全脫敏或刪除
- ✅ 審計日誌保留 7 年
- ✅ 用戶同意條款已更新(說明脫敏機制)
- ✅ 第三方 LLM(Claude API)有資料處理協議(DPA)
性能和可靠性
- ✅ 脫敏和恢復的延遲在可接受範圍(< 100ms)
- ✅ 在大量並發訪問下,映射表查詢仍然快速(考慮 Redis 緩存)
- ✅ 本地 LLM(Ollama)的 GPU 記憶體充足
- ✅ Claude API 配額充足,或有備用方案
- ✅ 網路連線中斷時,本地功能仍可用
運維檢查
- ✅ 團隊了解脫敏-分析-恢復的整個流程
- ✅ 有故障排查指南(映射表損壞、密鑰丟失)
- ✅ 定期演練災難恢復(重新產生映射表、恢復備份)
- ✅ 監控告警已設置(REVERSE 失敗、異常訪問)
- ✅ 文檔完整(架構、API、權限、故障排查)
範例
https://github.com/tm731531/idempiere-agent-sdk-sample/tree/main

🎯 總結
通過四層架構和 Masked Values 追蹤機制,企業可以:
- ✅ 用 AI 智能分析敏感資料
- ✅ 確保資料不會洩露給 LLM
- ✅ 根據用戶權限準確恢復原始值
- ✅ 完整審計,符合法規要求
- ✅ 成本可控(混合本地 + 雲端)
關鍵要點: Masked values 必須被保留和追蹤,否則恢復層就無法工作。這個架構確保了脫敏和恢復的完整性。
發佈留言