iDempiere Agent SDK 企業級設計:本地模型 + 脫敏恢復架構

文檔版本: 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 必須被保留和追蹤,否則恢復層就無法工作。這個架構確保了脫敏和恢復的完整性。

留言

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *