返回指南列表

AI 應用該記哪些資料?token 用量、生成脈絡與稽核軌跡的三張表

2026年7月30日
黃志宏
AI 導入實作

結論先講

AI 應用比傳統 CRUD 應用多出三種資料需求,而多數專案在第一版都只做了第一種:

需求回答什麼問題沒做的後果
生成結果使用者產出了什麼—(這個大家都會做)
生成脈絡這筆內容當初是用什麼條件產生的無法分析哪組條件成效好,也無法重現
稽核軌跡誰、在什麼時候、用了多少 token、花了多久帳單來了才發現超支,且查不出是誰

具體落到資料結構,就是三張表:使用者(含額度)生成結果(含脈絡)生成日誌(含用量)。以下是每張表的實際欄位與理由。


表一:users —— 比一般會員表多一塊「額度」

// Collection: users
// Document ID: {uid}(來自 Auth 服務)
{
  "uid": "7a8b9c...",
  "email": "editor@example.com",
  "display_name": "Pro Marketer",
  "created_at": "Timestamp",

  // AI 專屬:限流與計費。一般會員表沒有這塊
  "credits": {
    "total_generated": 150,
    "remaining_balance": 500,
    "subscription_tier": "pro"
  },

  // 使用者偏好,用來預填生成條件
  "preferences": {
    "default_industry": "美妝",
    "default_platform": "Instagram"
  }
}

credits 這塊是關鍵。 AI 應用的邊際成本不是零——每一次生成都在花錢。如果沒有一個地方記錄「這個人還剩多少」,你只有兩個選擇:完全不限制(然後被單一使用者打爆帳單),或是每次都去查日誌加總(然後每次生成都多一次昂貴查詢)。

total_generatedremaining_balance 要分開存:前者是累計統計,後者是可扣減的餘額。合成一個欄位,退款或補償時就會算不清楚。


表二:saved_ads —— 生成結果要跟生成脈絡綁在一起

// Collection: saved_ads
// Document ID: {auto-generated-uuid}
{
  "id": "ad_12345...",
  "user_id": "7a8b9c...",

  // 核心內容:AI 生成的結果
  "content": {
    "headline": "通勤 40 分鐘,你的早餐只有便利商店?",
    "body": "五分鐘備好的舒肥雞胸,冷藏七天不走味。",
    "rationale": "以通勤族的時間痛點開場,再給出具體可執行的替代方案。"
  },

  // 數據指標
  "metrics": {
    "predicted_ctr": 4.2,
    "user_rating": 5
  },

  // 生成上下文:這一整塊才是真正的差異所在
  "generation_context": {
    "keyword": "舒肥雞胸",
    "industry": "食品",
    "emotion": "痛點",
    "platform": "Instagram",
    "length": "短文案"
  },

  "tags": ["痛點", "Instagram", "食品"],
  "created_at": "Timestamp",
  "is_archived": false
}

為什麼 generation_context 值得獨立一塊

沒有它,你的資料庫裡只有一堆孤立的產出。有了它,你可以回答:

  • 哪一種情感設定的平均評分最高?
  • 同一組關鍵字換平台,產出的差異在哪?
  • 使用者最常用哪些條件組合?(這直接告訴你該把哪些選項做成預設值)

這些問題無法事後補。 你不可能回頭猜三個月前那筆文案是用什麼條件生的。

為什麼要有 rationale

讓模型在輸出時一併解釋「為什麼這樣寫」,有兩個實際好處:一是使用者更容易判斷要不要採用,二是當產出品質下滑時,你能從理由看出模型是哪一步想歪了。這個欄位設為選填,成本很低。


表三:generation_logs —— 前端永遠不該能寫入的那張表

// Collection: generation_logs
// Document ID: {auto-generated-uuid}
{
  "user_id": "7a8b9c...",
  "timestamp": "Timestamp",
  "prompt_snapshot": "...",        // 當時實際送出的完整提示詞
  "model_used": "...",             // 模型與版本
  "token_usage": {
    "prompt_tokens": 120,
    "completion_tokens": 350,
    "total_tokens": 470
  },
  "latency_ms": 1450,              // 對應非功能性需求的監控
  "status": "success"              // 或 error,例如觸發內容過濾
}

這張表回答四個問題,每一個都會在上線後三個月內找上門:

欄位回答的問題
token_usage這個月的帳單是誰貢獻的?哪個功能最燒錢?
latency_ms使用者抱怨「很慢」時,是真的慢還是體感問題?
model_used換模型之後,品質與成本各變了多少?
prompt_snapshot這次產出很怪,當時到底送了什麼給模型?

三條寫死的規則

  1. 前端不得直接寫入。 這張表若能被前端寫,用量統計與計費就沒有可信度。權限規則寫成完全禁止前端讀寫,只由後端服務寫入。
  2. 失敗也要記。 只記成功會讓錯誤率永遠是 0%。status 欄位存在的意義就是記錄失敗,包括被內容過濾擋下的那些。
  3. 一開始就定保留期限。 這張表的成長速度是其他表的數倍。決定好熱資料保留多久、之後彙總成什麼,寫進文件。

一組可以直接抄的權限規則

以 Firestore 為例,對應上述三張表的安全規則:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    function isAuthenticated() {
      return request.auth != null;
    }
    function isOwner(userId) {
      return request.auth.uid == userId;
    }

    // 使用者只能讀寫自己的資料
    match /users/{userId} {
      allow read, write: if isOwner(userId);
    }

    // 生成結果:只能操作自己的
    match /saved_ads/{adId} {
      allow create: if isAuthenticated()
                    && request.resource.data.user_id == request.auth.uid;
      allow read, update, delete: if isAuthenticated()
                    && resource.data.user_id == request.auth.uid;
    }

    // 日誌:前端一律禁止,只由後端寫入
    match /generation_logs/{logId} {
      allow read, write: if false;
    }
  }
}

最後一條 allow read, write: if false 是刻意的。很多專案在開發階段把整個資料庫開成測試模式,然後忘了關。 測試模式的規則通常會在 30 天後自動改為全部拒絕,所以你不會馬上發現——但在那 30 天內,任何人都能讀走全部資料。


常見問題

為什麼不能只存 AI 生成的結果就好?

因為生成結果無法回答三類問題:成本(這個月花了多少、誰花的)、重現(同樣條件再跑一次會得到什麼)、歸因(這筆內容當初是基於什麼條件產生的)。這三類問題在上線第一個月不會出現,第三個月一定會出現,而那時已經沒有歷史資料可補。

generation_logs 會不會長太快、成本太高?

會長很快,所以要一開始就決定保留策略。常見做法是熱資料保留 30 至 90 天供除錯與用量分析,之後彙總成每日或每月的統計列再刪除明細。真正佔空間的是 prompt_snapshot 欄位,若成本敏感可只存雜湊值與提示詞版本編號,不存全文。

prompt_snapshot 存完整提示詞,會不會有個資風險?

會,而且這是最容易被忽略的一處。使用者輸入可能夾帶個資,一旦存進日誌就等於把個資複製了一份。若應用會接觸個資,應在寫入前做遮蔽,或改存提示詞範本編號加上參數,而非使用者原文。這件事要在設計 schema 時就決定,事後補很痛苦。

這套結構只適用 Firestore 嗎?換成 PostgreSQL 可以嗎?

可以,三張表的切法與資料庫無關。users、生成結果、生成日誌這個分法在關聯式資料庫同樣成立,差別只在巢狀欄位要拆成 JSON 欄位或另建關聯表。重點是「把生成脈絡與生成結果分開存、把日誌與業務資料分開存」這兩個決定。

generation_context 為什麼要獨立成一個物件,不直接攤平?

因為它會變。生成條件的欄位組合會隨產品迭代增加或調整,獨立成物件時新增欄位不會污染主結構,也方便日後整包比對「哪一組條件的成效最好」。攤平的話,三個月後你會分不清哪些欄位是業務資料、哪些是當初的生成參數。

想把這套方法用在自己的團隊?

梵亞行銷提供企業 AI 轉型顧問與內訓服務,從流程盤點、工具選型到落地驗收,陪你把 AI 真正接進日常工作。

了解 AI 轉型顧問服務