Skip to main content

Memory & Payload 注入

「我希望 AI 知道使用者偏好用繁體中文」、「我希望 AI 看得到使用者目前在哪個頁面」、「我希望 AI 記得過去的互動」 — 這些都是「動態影響 Workflow 行為」的需求, 本指南拆解如何透過 Payload 機制達成。

閱讀前提

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

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

對 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 EmbedDirect Connect 僅支援 design-time

想深入理解 Payload 注入與整合架構的關係? → 架構深入解析


Workflow 端如何讀取 Payload

不管從哪一邊注入 Payload, Workflow 內讀取 prevPayload.* 的語法都一樣。這個一致性讓你可以從不同 Pattern 升級時不必動 Workflow。

在 Prompt 模板中讀取

Workflow 的 LLM Completion Processor 的 prompt 欄位是 Handlebars 模板:

{{#if prevPayload.user_config}}
<user_preferences>
使用者偏好設定:
- 偏好語言: {{{ prevPayload.user_config.locale }}}
- 偏好語氣: {{{ prevPayload.user_config.tone }}}
</user_preferences>
{{/if}}

{{#if prevPayload.memories}}
<memories>
與使用者相關的記憶:
{{#each prevPayload.memories}}
- [id: {{{this.id}}}] {{{this.content}}}
{{/each}}
</memories>
{{/if}}

注意 {{{ ... }}}(三花括號)用於不做 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_urlprevPayload.user_locale 等。

限制

  • 內容會被使用者看到: 任何敏感資料(API token、其他人的個資)都不可放
  • 可被竄改: Workflow 不能信任 prevPayload.user_role = 'admin', 因為使用者可以改
  • 大小受限: 整個 SSE 請求建議控制在 100KB 內

B. 後端注入

Backend Relay 能支援。你的後端從 DB 撈出資料, 在轉發到 Edge Server 之前塞進 payload。這是最強大的注入點。

適用情境

  • 使用者 Memory(個人化的長期記憶)
  • 使用者 Config(語言、語氣、付費等級)
  • 業務上下文(目前所屬組織、可用配額)
  • 動態權限(可查資料表、可用工具集)
  • 任何不應暴露給使用者的資料

實作範例

// 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 事件
}

Memory 是怎麼動態變化的?

Memory 不只是「讀出來注入」, LLM 還可以主動申請更新:

  1. 你在 Workflow 設計時加上一個 Toolset 工具, 例如 upsert_memory(content, id?)
  2. LLM 判斷使用者說了什麼值得記住的事實時, 呼叫這個工具
  3. 你的後端攔截 SSE 事件 asgard.tool_call.complete, 看到 upsert_memory 工具被呼叫
  4. 後端把新 memory INSERT 到 DB
  5. 下次該使用者送訊息時, 後端撈出最新 memory(包含剛剛新增的)注入 payload

整個迴圈讓 AI 看起來「越用越懂你」。完整流程見 [Pattern 4: Backend Relay 的 Case Study](./pattern-backend-relay.mdx#case-study 虛構的 ai-個人筆記助理)。

限制

  • 仍要控制 payload 大小, Memory 不要無限堆積(建議篩選最相關的 N 筆)
  • 注意 Memory 內容會被 LLM 看到, 進而透過回應反映給使用者 — 不要存純內部的敏感欄位

C. Workflow 內部組裝

若你採用 Workflow AuthBackend Relay 並需要 runtime 組裝: Workflow 在執行階段可用 HTTP Request Processor + Update Context Processor 自行組出 Payload-like 的變數。其他 Pattern 僅能在 design-time 寫死。

適用情境

  • Workflow Auth Pattern 下, 從外部 Auth API 取回的權限資訊
  • 從外部知識庫即時拉取的補充資料
  • 不需要持久化的「臨時上下文」

實作

  1. HTTP Request Processor 打你的 API
  2. Update Context Processor 把回應存進 context 變數
  3. 在後續 Prompt 用 {{{ allowed_scopes }}} 等讀取

完整範例見 Pattern 3: Workflow Auth

限制

  • 每次都要等 HTTP 回應, 影響回應延遲
  • 邏輯散落在 Workflow, 複雜化時難維護
  • 沒有持久化(不適合儲存使用者 Memory)

組合使用: 不同注入點的最佳組合

實務中常常是多個注入點協同:

職責劃分原則:

  • 前端注入: 當下、瞬時、不敏感的 UI 上下文
  • 後端注入: 持久、敏感、需驗證的使用者資料
  • Workflow 注入: 動態的、來自外部即時系統的補充

反模式與常見陷阱

❌ 反模式 1: 把整段對話歷史塞進 Payload

現象: payload.history = [...50 則訊息...] 每次送訊息都塞進去。

問題:

  1. Payload 體積爆炸, 網路延遲增加
  2. Asgard 的 customChannelId 機制本來就會維持 Channel 上下文, 你重複塞是多餘
  3. 整段歷史進 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_uuidpayload.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 注入與整合架構的關係: 架構深入解析