Memory & Payload 注入
「我希望 AI 知道使用者偏好用繁體中文」、「我希望 AI 看得到使用者目前在哪個頁面」、「我希望 AI 記得過去的互動」 — 這些都是「動態影響 Workflow 行為」的需求, 本指南拆解如何透過 Payload 機制達成。
本指南是寫給已完成整合模式 (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 概念還陌生? → 先回到 總覽與選型 完成選型, 再來看這篇。
為什麼需要這個能力
Workflow 本身的 Prompt、Toolset、Model 是設計時就固定下來的, 但實際對話中, 每位使用者、每次對話都有不同的上下文需求:
- 使用者偏好(語言、語氣、emoji 頻率)
- 個人記憶(過去學到的個人事實)
- 業務上下文(目前查看的訂單、所屬部門)
- 動態權限(可使用的工具集、可查的資料表)
這些資料如果寫死在 Prompt 中, 就無法做到 per-user 個人化。Payload 機制 讓你能在訊息送出時夾帶這些動態資料, Workflow 在執行階段透過 prevPayload.* 讀取並影響行為。
Memory 是 Payload 機制的一個典型用例: 你的後端把使用者記憶讀出來, 夾在 Payload 裡讓 Workflow 看到。
三種注入點與支援模式
Payload 注入的本質是「在某個位置把資料塞進訊息的 Payload 欄位」。可注入的位置決定了你能放什麼樣的資料、安全性等級:
| 注入點 | 可信任度 | 特性 | 支援的整合模式 (Pattern) |
|---|---|---|---|
| A. 前端注入 | ❌ 使用者可見可改 | 適合非敏感的 UI 上下文(頁面 URL、時區、主題等) | Direct Connect / Workflow Auth / Backend Relay |
| B. 後端注入 | ✅ 可信任 | 適合敏感資料(身份、權限、Memory), 需要自己的後端 | Backend Relay |
| C. Workflow 內部組裝 | △ 視來源而定 | 在 Workflow 內透過 HTTP / Update Context 動態組出 | Workflow Auth(含 runtime HTTP) / Backend Relay;Hosted Embed、Direct Connect 僅支援 design-time |
想深入理解 Payload 注入與整合架構的關係? → 架構深入解析
Workflow 端如何讀取 Payload
不管從哪一邊注入 Payload, Workflow 內讀取 prevPayload.* 的語法都一樣。這個一致性讓你可以從不同 Pattern 升級時不必動 Workflow。
在 Prompt 模板中讀取
Workflow 的 LLM Completion Processor 的 prompt 欄位是 Handlebars 模板:
注意 {{{ ... }}}(三花括號)用於不做 HTML escaping, 適合塞純文字。
在 Expression / Router 中讀取
Update Context Processor 或 Router Processor 的 Expression 也能讀:
(() => {
return prevPayload.allowed_tables || [];
})()
// Router case-1: 只有 admin 才能用某些工具
(() => {
return prevPayload.user_config?.role === 'admin';
})()
在 Toolset 動態載入中使用
LLM Completion Processor 的 toolsets 設定也可用表達式, 依 payload 動態決定可用工具集:
(() => {
return prevPayload.user_config?.plan === 'premium'
? 'basic-tools,premium-tools'
: 'basic-tools';
})()
三種注入點詳解
A. 前端注入
若你採用 Direct Connect / Workflow Auth / Backend Relay: 前端在送訊息時把資料塞進 payload。最簡單的注入點。
適用情境
- 不敏感的當前頁面上下文(URL、語言、時區)
- 使用者主動選擇的偏好(主題、字體大小)
- 不會被使用者竄改也無妨的資料
實作
利用 SDK 的 onBeforeSendMessage:
<Chatbot
onBeforeSendMessage={(params) => ({
...params,
payload: {
...params.payload,
page_url: window.location.href,
user_locale: navigator.language,
user_timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
ui_theme: currentTheme,
},
})}
/>
Workflow 內就能讀到 prevPayload.page_url、prevPayload.user_locale 等。
限制
- 內容會被使用者看到: 任何敏感資料(API token、其他人的個資)都不可放
- 可被竄改: Workflow 不能信任
prevPayload.user_role = 'admin', 因為使用者可以改 - 大小受限: 整個 SSE 請求建議控制在 100KB 內
B. 後端注入
僅 Backend Relay 能支援。你的後端從 DB 撈出資料, 在轉發到 Edge Server 之前塞進 payload。這是最強大的注入點。
適用情境
- 使用者 Memory(個人化的長期記憶)
- 使用者 Config(語言、語氣、付費等級)
- 業務上下文(目前所屬組織、可用配額)
- 動態權限(可查資料表、可用工具集)
- 任何不應暴露給使用者的資料
實作範例
- Go
- Node.js
// POST {base}/message/sse — 後端中繼 SSE Relay endpoint;從 DB 撈個人化資料注入 Payload 後轉發到 Edge Server
func (h *ChatHandler) SendMessage(w http.ResponseWriter, r *http.Request) {
userID, _ := h.auth.VerifySession(r)
var req SendMessageRequest
json.NewDecoder(r.Body).Decode(&req)
// 從你的 DB 撈個人化資料
userConfig, _ := h.repo.GetUserConfig(ctx, userID)
memories, _ := h.repo.ListMemories(ctx, userID)
channelMeta, _ := h.repo.GetChannelMeta(ctx, req.ChannelID)
// 組裝 payload
payload := map[string]interface{}{
"user_id": userID,
"user_config": userConfig,
"memories": memories,
"channel_title": channelMeta.Title,
"allowed_tools": userConfig.AllowedTools,
}
// 把任何使用者送來的 payload 合併(但 BE 注入的 key 優先,防止覆寫)
for k, v := range req.Payload {
if _, exists := payload[k]; !exists {
payload[k] = v
}
}
message := &models.GenericBotMessage{
CustomChannelId: req.ChannelID,
Text: req.Text,
Action: models.PostBackActionNone,
Payload: payload,
}
stream, _ := bpClient.NewStreamer(ctx, message, nil)
// ...轉發 SSE 事件
}
import {
PostBackActionNone,
type GenericBotMessage,
} from '@asgard-js/nodejs';
// POST {base}/message/sse — 後端中繼 SSE Relay endpoint;從 DB 撈個人化資料注入 Payload 後轉發到 Edge Server
app.post('/message/sse', async (req, res) => {
const userId = await auth.verifySession(req);
const body = req.body as SendMessageRequest;
// 從你的 DB 撈個人化資料
const userConfig = await repo.getUserConfig(userId);
const memories = await repo.listMemories(userId);
const channelMeta = await repo.getChannelMeta(body.channelId);
// 組裝 payload(BE 注入的 key 優先,使用者送來的同名 key 不可覆寫)
const payload: Record<string, unknown> = {
...(body.payload ?? {}),
user_id: userId,
user_config: userConfig,
memories,
channel_title: channelMeta.title,
allowed_tools: userConfig.allowedTools,
};
const message: GenericBotMessage = {
customChannelId: body.channelId,
customMessageId: body.messageId,
text: body.text,
action: PostBackActionNone,
payload,
};
const streamer = await bpClient.newStreamer(message);
// ...轉發 SSE 事件
});
Memory 是怎麼動態變化的?
Memory 不只是「讀出來注入」, LLM 還可以主動申請更新:
- 你在 Workflow 設計時加上一個 Toolset 工具, 例如
upsert_memory(content, id?) - LLM 判斷使用者說了什麼值得記住的事實時, 呼叫這個工具
- 你的後端攔截 SSE 事件
asgard.tool_call.complete, 看到upsert_memory工具被呼叫 - 後端把新 memory INSERT 到 DB
- 下次該使用者送訊息時, 後端撈出最新 memory(包含剛剛新增的)注入 payload
整個迴圈讓 AI 看起來「越用越懂你」。完整流程見 [Pattern 4: Backend Relay 的 Case Study](./pattern-backend-relay.mdx#case-study 虛構的 ai-個人筆記助理)。
限制
- 仍要控制 payload 大小, Memory 不要無限堆積(建議篩選最相關的 N 筆)
- 注意 Memory 內容會被 LLM 看到, 進而透過回應反映給使用者 — 不要存純內部的敏感欄位
C. Workflow 內部組裝
若你採用 Workflow Auth 或 Backend Relay 並需要 runtime 組裝: Workflow 在執行階段可用 HTTP Request Processor + Update Context Processor 自行組出 Payload-like 的變數。其他 Pattern 僅能在 design-time 寫死。
適用情境
- Workflow Auth Pattern 下, 從外部 Auth API 取回的權限資訊
- 從外部知識庫即時拉取的補充資料
- 不需要持久化的「臨時上下文」
實作
- 用 HTTP Request Processor 打你的 API
- 用 Update Context Processor 把回應存進 context 變數
- 在後續 Prompt 用
{{{ allowed_scopes }}}等讀取
完整範例見 Pattern 3: Workflow Auth。
限制
- 每次都要等 HTTP 回應, 影響回應延遲
- 邏輯散落在 Workflow, 複雜化時難維護
- 沒有持久化(不適合儲存使用者 Memory)
組合使用: 不同注入點的最佳組合
實務中常常是多個注入點協同:
職責劃分原則:
- 前端注入: 當下、瞬時、不敏感的 UI 上下文
- 後端注入: 持久、敏感、需驗證的使用者資料
- Workflow 注入: 動態的、來自外部即時系統的補充
反模式與常見陷阱
❌ 反模式 1: 把整段對話歷史塞進 Payload
現象: payload.history = [...50 則訊息...] 每次送訊息都塞進去。
問題:
- Payload 體積爆炸, 網路延遲增加
- Asgard 的
customChannelId機制本來就會維持 Channel 上下文, 你重複塞是多餘 - 整段歷史進 LLM context 會把 Token 用量推高很多
正確做法: 依賴 customChannelId 機制, Payload 只塞「Channel 機制無法表達的東西」。詳見 Conversation History。
❌ 反模式 2: 從前端注入敏感資料
現象: payload.user_role = 'admin' 從前端塞進 payload, Workflow 信任這個值決定權限。
問題: 使用者可竄改 user_role。任何安全相關的判斷必須用後端注入(注入點 B)或 Workflow 內驗證(注入點 C)。
❌ 反模式 3: Memory 無限累積
現象: 每次 LLM 呼叫 upsert_memory, 後端就無條件加一筆 memory, 從不刪除。
問題: 半年後 memory 累積到上百筆, 全部注入 Payload 後 Token 費用倍增, 且 LLM 抓不到重點。
正確做法: 設計 Memory 的「精選機制」 — 限制總數、定期 summarize、按相關性排序、套用 TTL。
❌ 反模式 4: 同一個 key 被前端與後端同時注入, 且後端被覆寫
現象: 前端送 payload.user_id = 'fake', 後端注入時被前端覆寫。
問題: 如果後端的合併邏輯是「以 req.Payload 為準」, 前端就能偽造身份。
正確做法: 後端合併 payload 時,內部信任的 key 必須優先於前端送來的 key, 如同前面 Go SDK 範例所示。
❌ 反模式 5: Payload 內塞純內部的敏感欄位
現象: 後端把 payload.internal_user_uuid、payload.internal_billing_id 等不應對外的欄位塞進 payload。
問題: 此問題的前提是「該欄位最終會進到 LLM context」 — 也就是 Workflow 把它注入了 Prompt 模板(透過 {{{ prevPayload.* }}})。在這種情況下, LLM 有可能在回應中無意地把 UUID 「複述」出來給使用者看。反之, 若 payload 純粹給 Workflow 內部的 Router / Expression 判斷使用、不進 Prompt, 則無此風險。
正確做法: 盤點 Workflow 內哪些欄位會被注入 Prompt, 只把這些欄位放進 payload; 不會進 Prompt 的純內部 ID、Hash 仍可保留在 payload 中供路由判斷使用, 但若連路由都用不到, 就不要塞。
延伸閱讀
- 完整模式深探: Direct Connect(前端注入)、Workflow Auth(Workflow 內組裝)、Backend Relay(後端注入)
- Memory 完整 case study: [Pattern 4 的虛構 AI 個人筆記助理](./pattern-backend-relay.mdx#case-study 虛構的 ai-個人筆記助理)
- 想深入理解 Payload 注入與整合架構的關係: 架構深入解析