メインコンテンツまでスキップ

Audit Logging

「誰、在何時、做了什麼?」 — Audit Log 是法遵、安全分析、爭議調查的基礎。Asgard 整合中, Audit Log 的本質是「可信賴、不可被使用者竄改的事件紀錄」, 這個本質直接決定了它只能在某個 Pattern 下實作。

閱讀前提

本指南是寫給已完成整合模式 (Pattern) 選型的讀者。你不需要熟悉全部四種模式, 但至少要對其中一個有完整概念, 後續討論才能跟得上。

四種 Pattern 速覽 — 確認你至少熟悉其中一種:

對 Pattern 概念還陌生? → 先回到 總覽與選型 完成選型, 再來看這篇。

為什麼需要這個能力

當 AI Agent 被用在以下場景時, Audit Log 不是「nice to have」而是「必要」:

  • 法遵需求: 金融、醫療、法律等行業需要保留完整對話紀錄
  • 安全分析: 追蹤異常行為、偵測濫用、調查資料外洩
  • 爭議調查: 使用者主張「AI 給了錯誤建議」時的真相還原
  • 內部審計: 確認員工是否依規範使用 AI 工具
  • 品質追蹤: 分析回應品質、定位 Workflow Bug
  • 計費對帳: 確認 LLM 用量、配額消耗的明細

Audit Log 與一般的「應用 Log」不同點在於:

  • 必須完整、不漏事件
  • 必須可信、不可被竄改
  • 必須長期保留(視法規而定, 可能需 N 年)
  • 必須能精準對到「人」(per-user 識別)

Pattern 對 Audit 的支援程度

Audit Log 的本質需求是「可信賴的事件紀錄點」 — 紀錄必須發生在使用者無法觸及的位置。這直接決定了哪些 Pattern 能做完整的 Audit:

PatternAudit 程度為什麼
Hosted Embed❌ 無紀錄只能落在前端 / 瀏覽器, 使用者完全可控
Direct Connect❌ 無同上, 瀏覽器端任何紀錄都可被改寫或阻擋
Workflow Auth△ 僅 Workflow 內部 loglog 受平台限制, 且看不到 FE 行為
Backend Relay✅ 完整紀錄在你自己掌控的後端, 使用者無法觸及

結論: Audit Log 本質上需要你自己掌控的後端, 也因此只有 Backend Relay 才能做完整的 Audit。

想深入理解這個結論的推導? → [架構深入解析](./architecture-deep-dive.mdx#例-1 為什麼-audit-log-必須採用-backend-relay)

不要把瀏覽器埋點當成 Audit

有時開發者誤以為「用 Sentry / GA 在前端紀錄事件就能 audit」。瀏覽器端任何紀錄都是不可信的 — 使用者可阻止、改寫、刪除。瀏覽器埋點的目的是分析使用者行為, 不是法遵稽核。


Backend Relay 下的 Audit 標準實作

攔截 SSE 事件流是核心模式

Backend Relay 的後端在轉發 SSE 事件給前端的同時,逐一攔截事件寫入 Audit Log。這個「轉發中攔截」的設計讓 Audit 跟業務邏輯共用同一個事件迴圈。

應該記錄哪些事件

最低限度應記錄以下事件:

事件來源紀錄內容
使用者送出訊息(BE 收到請求時)user_id / channel_id / timestamp / 訊息內容 / IP / User-Agent
asgard.run.init(Workflow 開始)run_id / channel_id / payload(已過濾敏感欄位)
asgard.message.complete(AI 完成回應)AI 完整回應內容 / token 用量
asgard.tool_call.complete(工具呼叫完成)工具名稱 / 參數 / 結果
asgard.completion_model_usage(LLM 用量)input_tokens / output_tokens / model
asgard.run.error / 任何錯誤錯誤類型 / 訊息 / stack(視需要)
asgard.run.done(對話結束)總用量摘要
使用者中途中斷 / 關閉頁面由 BE 偵測 SSE 連線斷開時記錄

Audit Log 資料庫 Schema(虛構範例)

建議建立以下複合索引以支援常見查詢:

  • (user_id, created_at DESC) — 依使用者按時間查詢
  • (channel_id, created_at DESC) — 依頻道按時間查詢
  • (event_type, created_at DESC) — 依事件類型按時間查詢

攔截事件並寫 Audit

// POST {base}/message/sse — 後端中繼 SSE Relay endpoint;轉發 SSE 串流的同時逐一攔截事件寫入 Audit Log
func (h *ChatHandler) SendMessage(w http.ResponseWriter, r *http.Request) {
userID, _ := h.auth.VerifySession(r)
var req SendMessageRequest
json.NewDecoder(r.Body).Decode(&req)

// 1. 紀錄使用者送出訊息事件
h.audit.Log(ctx, AuditEntry{
UserID: userID,
ChannelID: req.ChannelID,
EventType: "user_message",
Payload: map[string]interface{}{"text": req.Text},
ClientIP: getClientIP(r),
UserAgent: r.UserAgent(),
})

// 2. 建立 SSE 串流
stream, _ := bpClient.NewStreamer(ctx, buildMessage(userID, req), nil)
defer stream.Close()

// 3. 攔截每個 SSE 事件,寫入 Audit + 轉發給 FE
for stream.Next() {
event := stream.Current()

// 寫 Audit(過濾敏感欄位)
h.audit.Log(ctx, AuditEntry{
UserID: userID,
ChannelID: req.ChannelID,
RunID: event.RequestId,
EventType: string(event.EventType),
Payload: sanitizeForAudit(event),
})

// 轉發給 FE
writeSseEvent(w, event)
}
}

串接外部 Audit Sink

許多企業要求 Audit 同步到統一的日誌平台(Splunk、Datadog、Elasticsearch、Kafka 等)。建議的設計:

  1. 同步寫入 DB: 作為「source of truth」, 業務查詢用
  2. 非同步推播到外部 Sink: 用 Worker / Outbox Pattern 把 audit log 推到外部系統
// 簡化示意
h.audit.Log(ctx, entry) // 同步寫 DB
h.auditQueue.Enqueue(entry) // 非同步推外部

避免直接在請求路徑上同步呼叫外部系統 — 外部系統慢會拖垮使用者體驗。


反模式與常見陷阱

❌ 反模式 1: 用 Sentry / GA 當作 Audit

現象: 「我們有 Sentry 在前端埋點啊, 所有錯誤都會記錄。」

問題: Sentry 等前端工具的目的是「分析」不是「稽核」。使用者可阻擋它(adblock)、可改 JS 不送 event。Audit 必須在使用者無法觸及的位置記錄。

❌ 反模式 2: 同步寫多個外部系統

現象: 在 SSE 事件迴圈中同步呼叫 Splunk / Datadog / Slack 通知。

問題: 任何一個外部系統慢, 整個對話流就卡住, 使用者體驗變差。應改為非同步 / Outbox。

❌ 反模式 3: Audit 失敗就讓請求失敗

現象: Audit DB 暫時連不上, 就把使用者的訊息也回 500。

問題: 視業務需求而定:

  • 嚴格法遵情境: 對, Audit 必須成功才能繼續(類似銀行交易)
  • 一般 SaaS 情境: 錯, 要降級到「先繼續服務、Audit 進 retry 佇列」

延伸閱讀