Conversation History
「使用者下次回來, 還能看到之前的對話嗎?」、「換裝置還能延續嗎?」 — Conversation History 在不同整合模式下能達成的程度差異很大。本指南拆解三種儲存模型, 以及它們對應的 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 概念還陌生? → 先回到 總覽與選型 完成選型, 再來看這篇。
為什麼需要這個能力
「對話歷史」聽起來簡單, 但其實涵蓋兩個面向, 這兩件事不能混為一談:
| 面向 | 意義 |
|---|---|
| A. 對話連續性 | AI 是否「記得」之前說過什麼, 後續回應能否承接上下文 |
| B. UI 訊息顯示 | 使用者開啟頁面時, 能否看到之前的訊息列表 |
這兩者由不同機制控制:
- A 由
customChannelId的「重用」決定。同一個customChannelId內 AI 才會記得前後文(Channel 狀態由 Edge Server 內部維護) - B 由你「能不能取得過去訊息並重新渲染」決定。
@asgard-js/react提供initMessagesprop 接收初始訊息
理解這兩個面向才能正確選擇儲存模型。
三種儲存模型與支援模式
| 儲存模型 | 對話連續性 | UI 顯示歷史 | 支援的整合模式 (Pattern) |
|---|---|---|---|
| Model A: Ephemeral(每次新 channel) | ❌ | ❌ | Hosted Embed(預設) / Direct Connect / Workflow Auth / Backend Relay |
| Model B: Client-side(localStorage) | ✅ 同裝置 | ✅ 同裝置 | Direct Connect / Workflow Auth / Backend Relay |
| Model C: Server-side(後端 DB) | ✅ 跨裝置 | ✅ 跨裝置 | Backend Relay |
想深入理解 History 與整合模式的選型關係? → 架構深入解析
儲存模型詳解
Model A: Ephemeral
最簡單的模型: 每次使用者進入頁面就產生一個新的 customChannelId, Asgard 端視為全新對話。
行為
- 每次重新整理頁面 → 對話從零開始
- 同一頁面不重新整理時可延續(只要 React 元件未重掛載)
適用情境
- POC、Demo
- 一次性使用的小工具(例如: 特定行銷活動頁的客服)
- 隱私敏感場景, 不希望 AI 持續記得使用者
如何實作
import { v4 as uuidv4 } from 'uuid';
import { useRef } from 'react';
const channelIdRef = useRef(uuidv4());
// 每次元件掛載產生新的 channelId
Hosted Embed 預設就是這個模型。
Model B: Client-side
把 customChannelId 存進瀏覽器 localStorage, 讓同一個瀏覽器跨 session 延續對話。可選擇是否也存訊息列表用於 UI 重渲染。
行為
- 同瀏覽器、同裝置 → 對話延續
- 換瀏覽器 / 換裝置 / 清快取 → 全部消失
適用情境
- 個人助理類產品, 使用者長期使用同一裝置
- 不需要跨裝置同步的場景
如何實作
B-1. 只保留 channelId(對話連續性, 但 UI 重新開始)
const channelIdRef = useRef(
localStorage.getItem('chat:channelId') ??
(() => {
const id = uuidv4();
localStorage.setItem('chat:channelId', id);
return id;
})()
);
下次進入時 AI 還記得上下文, 但 UI 上看不到之前的訊息。適合「我希望 AI 持續學習我」但不在意視覺上的歷史顯示。
B-2. 同時保留訊息列表(完整 UX)
讓使用者重新整理頁面後也能看到先前訊息:
const channelIdRef = useRef(loadOrCreateChannelId());
const [initMessages] = useState(() => loadMessages(channelIdRef.current));
return (
<Chatbot
customChannelId={channelIdRef.current}
initMessages={initMessages}
onSseMessage={(event) => {
// 攔截事件,把完成的訊息寫進 localStorage
if (event.eventType === 'asgard.message.complete') {
appendMessage(channelIdRef.current, event.fact.messageComplete);
}
}}
/>
);
限制
- localStorage 容量上限約 5MB, 大量訊息會撐爆
- 換裝置完全消失
- 訊息暴露在 DevTools, 任何人取得使用者的瀏覽器都能看
Model C: Server-side
僅 Backend Relay 能支援。把訊息持久化到你的資料庫, 跨裝置同步、永久保留(視保留期限政策而定)。
行為
- 同使用者跨裝置、跨瀏覽器 → 完整同步
- 可實作對話搜尋、匯出、分類、收藏
- 可實作共享頻道(多人看同一段對話)
適用情境
- Enterprise 整合
- 多裝置 SaaS 產品
- 需要法遵保留期限管理
- 需要稽核
如何實作
關鍵在於後端攔截 SSE 事件並寫入 DB。簡化的 schema 與後端事件處理範例:
建議在 (user_id, channel_id, created_at) 建立複合索引, 以支援「依使用者、依頻道、按時間排序」這類常見查詢。
後端攔截事件寫入:
- Go
- Node.js
for stream.Next() {
event := stream.Current()
switch event.EventType {
case models.SseEventTypeRunInit:
// 使用者訊息確認進入 Workflow,寫入 history
h.repo.SaveUserMessage(ctx, userID, channelID, req.Text)
case models.SseEventTypeMessageComplete:
// AI 回應完成,寫入 history
h.repo.SaveAssistantMessage(ctx, userID, channelID,
event.Fact.MessageComplete)
}
// 同時轉發給前端
forwardSseEvent(w, event)
}
import {
SseEventTypeRunInit,
SseEventTypeMessageComplete,
} from '@asgard-js/nodejs';
for await (const event of streamer) {
switch (event.eventType) {
case SseEventTypeRunInit:
// 使用者訊息確認進入 Workflow,寫入 history
await repo.saveUserMessage(userId, channelId, body.text);
break;
case SseEventTypeMessageComplete:
// AI 回應完成,寫入 history
await repo.saveAssistantMessage(userId, channelId,
event.fact?.messageComplete);
break;
}
// 同時轉發給前端
forwardSseEvent(res, event);
}
前端在使用者開啟頁面時, 先呼叫你的後端 API 撈取歷史訊息:
useEffect(() => {
fetch(`/api/chat/channels/${channelId}/messages`)
.then((r) => r.json())
.then((messages) => setInitMessages(messages));
}, [channelId]);
return (
<Chatbot
customChannelId={channelId}
initMessages={initMessages}
config={{ botProviderEndpoint: '/api/chat' }}
/>
);
完整 case study
虛構的「AI 個人筆記助理」展示了完整 Server-side History 流程, 見 [Pattern 4: Backend Relay 的 Case Study](./pattern-backend-relay.mdx#case-study 虛構的 ai-個人筆記助理)。
Channel ID 設計策略總整理
customChannelId 是整個 History 機制的核心。常見策略:
| 策略 | 何時用 |
|---|---|
| 每次掛載新 UUID | Model A, Ephemeral |
| 存 localStorage | Model B, 個人助理 |
| 由你的後端發, 綁 user_id | Model C, 例如 {user_id}-{topic} |
| URL 帶入 | 分享對話 snapshot(僅閱讀) |
| 多 channel per user | 支援多會話 / 多話題切換 |
命名建議
- 用 UUID 或 ULID 避免猜測
- 不要使用具有意義的字串(如 email)作為 channelId — 容易被列舉
- Backend Relay 場景下, 建議在你的後端維護一個
channels表, channelId 為主鍵, 綁定user_id與其他 metadata
反模式與常見陷阱
❌ 反模式 1: 用同一個 customChannelId 跨使用者
現象: 所有使用者共用 channelId default-channel。
問題: Asgard 端把這個 channel 當作同一段對話, 不同使用者的訊息會互相污染上下文。每位使用者應該有自己獨立的 channelId(或多個)。
❌ 反模式 2: 把 channelId 設成 user_id 的 email
現象: customChannelId = userEmail。
問題: Email 可被列舉, 他人猜中 email 就能讀到對話(尤其在 Hosted Embed / Direct Connect 模式下 Asgard 端沒有 per-user ACL)。應用隨機 ID。
❌ 反模式 3: 用 Payload 塞整段歷史進每則訊息
現象: 每次送訊息都把過去 50 則訊息塞進 payload.history, 讓 Workflow 看到完整上下文。
問題:
- Payload 過大, 網路延遲增加
- 每則訊息都把整段歷史塞進 LLM context, Token 費用爆炸
- Asgard 的 Channel 機制本來就會維持上下文, 你重複塞是多餘的
正確做法: 依賴 customChannelId 機制, 讓 Edge Server 內部維護 Channel 狀態。Payload 只塞「當前訊息以外的補充資訊」。
❌ 反模式 4: 在 Direct Connect 模式下嘗試做跨裝置 History
現象: 想在沒有自建後端的情況下實作跨裝置同步, 試圖把 localStorage 的內容同步到 iCloud 或某些雲端硬碟。
問題: 沒有自己的後端就無法做可信的跨裝置同步(localStorage 與 iCloud 等都不是可信來源)。請升級到 Backend Relay。
❌ 反模式 5: 把 LLM 對話內容存進 localStorage 後忘了過期清理
現象: localStorage 只增不刪, 撐爆 5MB 上限後新訊息開始寫入失敗。
問題: 設計時要規劃保留策略(例如最近 N 筆、最近 N 天、容量觸發清理)。
延伸閱讀
- 完整模式深探: Hosted Embed / Direct Connect / Workflow Auth / Backend Relay
- Payload 動態注入機制: Memory & Payload 注入
- 想深入理解 History 與整合架構的關係: 架構深入解析