Audit Logging
「誰、在何時、做了什麼?」 — Audit Log 是法遵、安全分析、爭議調查的基礎。Asgard 整合中, Audit Log 的本質是「可信賴、不可被使用者竄改的事件紀錄」, 這個本質直接決定了它只能在某個 Pattern 下實作。
本指南是寫給已完成整合模式 (Pattern) 選型的讀者。你不需要熟悉全部四種模式, 但至少要對其中一個有完整概念, 後續討論才能跟得上。
四種 Pattern 速覽 — 確認你至少熟悉其中一種:
- Pattern 1: Hosted Embed — UI 由 Asgard 托管, 嵌入一個 URL 就能上線
- Pattern 2: Direct Connect — 前端 SDK 直連 Edge Server, 完整客製 UI
- Pattern 3: Workflow Auth — 前端直連 + Workflow 內驗證關卡
- Pattern 4: Backend Relay — 你的後端作為中繼層, 完整掌控
對 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:
| Pattern | Audit 程度 | 為什麼 |
|---|---|---|
| Hosted Embed | ❌ 無 | 紀錄只能落在前端 / 瀏覽器, 使用者完全可控 |
| Direct Connect | ❌ 無 | 同上, 瀏覽器端任何紀錄都可被改寫或阻擋 |
| Workflow Auth | △ 僅 Workflow 內部 log | log 受平台限制, 且看不到 FE 行為 |
| Backend Relay | ✅ 完整 | 紀錄在你自己掌控的後端, 使用者無法觸及 |
結論: Audit Log 本質上需要你自己掌控的後端, 也因此只有 Backend Relay 才能做完整的 Audit。
想深入理解這個結論的推導? → [架構深入解析](./architecture-deep-dive.mdx#例-1 為什麼-audit-log-必須採用-backend-relay)
有時開發者誤以為「用 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
- Go
- Node.js
// 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)
}
}
// POST {base}/message/sse — 後端中繼 SSE Relay endpoint;轉發 SSE 串流的同時逐一攔截事件寫入 Audit Log
app.post('/message/sse', async (req, res) => {
const userId = await auth.verifySession(req);
const body = req.body as SendMessageRequest;
// 1. 紀錄使用者送出訊息事件
await audit.log({
userId,
channelId: body.channelId,
eventType: 'user_message',
payload: { text: body.text },
clientIp: getClientIp(req),
userAgent: req.header('user-agent') ?? '',
});
// 2. 建立 SSE 串流
const streamer = await bpClient.newStreamer(buildMessage(userId, body));
req.on('close', () => streamer.close());
try {
// 3. 攔截每個 SSE 事件,寫入 Audit + 轉發給 FE
for await (const event of streamer) {
// 寫 Audit(過濾敏感欄位)
await audit.log({
userId,
channelId: body.channelId,
runId: event.requestId,
eventType: event.eventType,
payload: sanitizeForAudit(event),
});
// 轉發給 FE
writeSseEvent(res, event);
}
} finally {
streamer.close();
res.end();
}
});
串接外部 Audit Sink
許多企業要求 Audit 同步到統一的日誌平台(Splunk、Datadog、Elasticsearch、Kafka 等)。建議的設計:
- 同步寫入 DB: 作為「source of truth」, 業務查詢用
- 非同步推播到外部 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 佇列」
延伸閱讀
- 完整模式深探: Backend Relay(唯一支援 Audit 的 Pattern)
- SSE 事件流攔截的標準實作: Pattern 4: Backend Relay 的角色 ④
- 完整的 SSE 事件型別參考: 後端 SDK 使用指南
- 想深入理解為何 Audit 只能在你自己的後端做: [架構深入解析](./architecture-deep-dive.mdx#例-1 為什麼-audit-log-必須採用-backend-relay)