跳至主要内容

Pattern 4: Backend Relay

你的後端作為前端與 Edge Server 之間的中繼層, 完整掌控 Auth、History、Memory、Audit、Payload 注入。最進階、最有彈性的整合方案。

適用場景

這個模式適合以下情境:

  • 🏢 企業級 / Enterprise 整合: 需要 per-user 身份驗證、細粒度權限控管、稽核紀錄
  • 🔐 API Key 必須藏在後端: 不能讓使用者拿到 Bot Provider API Key
  • 📚 需要跨裝置同步的對話歷史: 同一使用者切換瀏覽器、手機 App 都能看到完整歷史
  • 🧠 需要從後端動態組裝 Memory / User Config / 業務上下文 注入 Workflow
  • 📋 需要完整 Audit Log: 每則訊息、每個工具呼叫、每筆扣點都要可追溯
  • 💼 複雜業務邏輯: 訊息分流、Human Handoff、CRM 同步、Webhook 觸發等
  • 🎯 需要 Server-side push: 訂單狀態變更、排程訊息等「不是使用者主動觸發」的訊息推送

如果以上你都不需要, 簡單一點的 Pattern(尤其 Direct Connect)已經夠用。


架構圖

此模式下, 4 個 Plane 全部開放: 畫面層、瀏覽器層、伺服器層、Agent 內部層, 程式化控制權最為完整。不熟悉 Plane / 執行層的概念? → 架構深入解析


Endpoint 介面

前端 SDK 會根據 botProviderEndpoint 自動拼接子路徑發出請求到你的後端, 而你的後端必須實作並提供以下的對應路由:

Endpoint必要性說明
POST {base}/message/sse必須實作接收訊息、中繼到 Edge Server、以 SSE 串流回傳 AI 回應
POST {base}/blob可選支援對話框上傳檔案

{base} 就是你在前端填入的 botProviderEndpoint 舉例, 若路由設計為 POST /api/chat/message/sse, 則前端應設定 botProviderEndpoint: 'https://your-backend.com/api/chat'


能力邊界

✅ 你能做的

  • 完整的 per-user 身份驗證(整合你的 Session / JWT / OAuth / SSO 系統)
  • 細粒度 ACL(per-resource、per-action)
  • Bot Provider API Key 完全藏在後端, 不暴露給前端
  • 跨裝置同步對話歷史(寫入你的 DB)
  • 完整 Audit Log(攔截所有 SSE 事件)
  • 從你的 DB 動態注入 Memory、User Config、業務上下文進 Payload
  • 配額 / Cost 追蹤、計費對帳
  • 訊息分級審核、Human Handoff、外部系統整合(CRM、Webhook、Email、Push)
  • Server-side push(透過後端主動發送訊息給 Bot, 例如「您的訂單已出貨」)
  • 統一錯誤處理、重試策略、降級邏輯

⚠️ 需要承擔的成本

成本項說明
需要建立後端服務你需要部署、維運一個能處理 SSE 串流的後端
需要管理 Bot Provider API Key安全地存放於後端 secrets, 不能 commit 到程式碼
需要設計 DB schemahistory、memory、user_config 等需要設計儲存結構
需要實作 SSE 雙向轉發後端要同時是 SSE Client(對 Edge Server)和 SSE Server(對前端)

想完整理解這個模式為何能開放所有 Plane 的能力? → 架構深入解析


後端的 4 個角色

理解這 4 個角色, 就掌握了 Backend Relay 的全部:

角色 ① — 驗證身份

收到前端請求時, 用你自己的 Auth 機制驗證使用者:

  • 從 Cookie / Authorization header 取出 Session / JWT
  • 查詢你的使用者資料表確認身份與權限

角色 ② — 注入 Payload

驗證通過後, 從你的 DB 撈取個人化資料, 塞入訊息 Payload。Workflow 在執行階段可透過 prevPayload.* 讀取:

  • 使用者偏好(user_config): 語言、語氣、回應風格
  • 使用者記憶(memories): 歷史互動學到的個人事實
  • 業務上下文: 當前帳號餘額、目前查看的訂單、所屬部門等
  • 動態權限資訊: 可使用的工具集、可查的資料表

角色 ③ — 轉發到 Edge Server

正因為 Backend Relay 需要後端主動對 Edge Server 發起通訊, Asgard 提供了專用的後端 SDK 來處理這段連線——包括建立 SSE 串流、傳送訊息、接收回應事件等。詳細 API 見 後端 SDK 使用指南

用後端 SDK 對 Edge Server 建立 SSE 串流。Bot Provider API Key 由後端持有, 從不外洩:

bpClient := client.NewBotProviderClient(
"https://api.asgard-ai.com",
"your-namespace",
"your-bot",
botProviderApiKey, // 從你的 secrets 讀取
)

stream, err := bpClient.NewStreamer(ctx, message, nil)

角色 ④ — 攔截事件流做 side effects

SSE 事件回流時, 後端逐一攔截處理,同時也轉發給前端:

  • asgard.run.init → 把使用者剛送出的訊息寫入 history 表
  • asgard.message.complete → 把 AI 完成的訊息寫入 history 表
  • asgard.tool_call.complete → 處理工具呼叫(例如: Bot 用 tool 申請「更新對話標題」, 後端真的去改 DB)
  • asgard.completion_model.usage → 計算 LLM 用量稽核
  • asgard.run.error → 集中記錄錯誤、觸發告警

這個攔截能力是 Backend Relay 的核心威力 — 你能在 Asgard 不感知的情況下, 圍繞 AI 對話建構你自己的業務系統。


前端的角色

前端需要做兩件事才能讓整個模式運作:

  1. 把請求導向你的後端: 在前端 SDK 的 botProviderEndpoint 填入你自己的後端 base path(路由規格見上方 Endpoint 介面), 而非 Asgard 的 Edge Server。SDK 行為不變, 只是流量改走你的後端。

  2. 帶上 Auth Credential: 後端要能識別「這個請求是誰發的」, 前端必須把 credential 一起送出:

    • Session Cookie: 瀏覽器預設行為, 同 origin 的請求會自動帶上, 通常不需要額外處理
    • JWT / Token: 如果你的 Auth 用 Token 而非 Cookie, 需用 onBeforeSendMessage 把 token 放進 payload, 或透過自訂 header 傳遞

快速起步

這個模式有兩個部分:(A) 後端中繼層(B) 前端改為打你的後端

A. 後端中繼層實作

對應上方 Endpoint 介面 表格的兩條路由, 以下逐一實作。完整 API 參考見 後端 SDK 使用指南

A-1. 訊息串流端點(必須實作): POST {base}/message/sse

這個 handler 就是上面 後端的 4 個角色 ①②③④ 的具體實作 — 在同一個 request 生命週期裡完成驗證、注入、轉發、攔截。

import (
"encoding/json"
"fmt"
"net/http"
"os"
"strings"

"go.asgard-ai.com/asgard-sdk-go/pkg/client"
"go.asgard-ai.com/asgard-sdk-go/pkg/models"
)

// 從環境變數讀取,請勿 hardcode
var (
edgeServerHost = getEnvOrDefault("EDGE_SERVER_HOST", "https://api.asgard-ai.com")
namespace = os.Getenv("NAMESPACE")
botProviderName = os.Getenv("BOT_PROVIDER_NAME")
botProviderApiKey = os.Getenv("BOT_PROVIDER_API_KEY")
)

func getEnvOrDefault(key, defaultVal string) string {
if v := os.Getenv(key); v != "" {
return v
}
return defaultVal
}

// POST https://your-backend.com/api/chat/message/sse
// 接收使用者訊息,中繼到 Edge Server,以 SSE 串流回傳 AI 回應
func SendMessage(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

// 角色 ①:驗證身份 — 從 Authorization header 取出 token
token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
userID, err := verifyToken(token)
if err != nil {
http.Error(w, "Unauthorized", 401)
return
}

// 直接使用 SDK 的 GenericBotMessage 作為 request 模型
var req models.GenericBotMessage
json.NewDecoder(r.Body).Decode(&req)

// 角色 ②:注入 Payload(從 DB 撈個人化資料)
userConfig, _ := repo.GetUserConfig(ctx, userID)
memories, _ := repo.ListMemories(ctx, userID)

req.Payload = map[string]interface{}{
"user_id": userID,
"user_config": userConfig,
"memories": memories,
}

// 角色 ③:轉發到 Edge Server
bpClient := client.NewBotProviderClientWithConfig(&client.BotProviderConfig{
EdgeServerHost: edgeServerHost,
Namespace: namespace,
BotProviderName: botProviderName,
BotProviderApiKey: botProviderApiKey,
})

stream, err := bpClient.NewStreamer(ctx, &req, nil)
if err != nil {
http.Error(w, "Failed to connect", 500)
return
}
defer stream.Close()

// 設定 SSE 回應 headers
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
flusher := w.(http.Flusher)

// 角色 ④:攔截事件並轉發
for stream.Next() {
event := stream.Current()

// 根據業務需求自行決定 side effects
switch event.EventType {
case models.SseEventTypeRunInit:
repo.SaveUserMessage(ctx, userID, req)
case models.SseEventTypeMessageComplete:
repo.SaveAssistantMessage(ctx, userID, event.Fact.MessageComplete)
case models.SseEventTypeToolCallComplete:
handleToolCall(ctx, userID, event.Fact.ToolCallComplete)
case models.SseEventTypeCompletionModelUsage:
billing.DeductCredits(ctx, userID, event.Fact.CompletionModelUsage)
}

// 轉發 SSE 給前端
fmt.Fprintf(w, "event: %s\n", event.EventType)
fmt.Fprintf(w, "data: %s\n\n", toJSON(event))
flusher.Flush()
}

if err := stream.Err(); err != nil {
// 集中錯誤處理
}
}

A-2. 檔案上傳端點(選擇性): POST {base}/blob

只有當你要支援對話框上傳檔案時才需要實作; 若不需要, 可跳過這一節。為了讓每個 snippet 都能獨立閱讀, 以下會重複 A-1 已出現過的 import 與 client 初始化 — 實際整合到同一個 server 時合併即可。

import (
"bufio"
"encoding/json"
"net/http"
"os"

"go.asgard-ai.com/asgard-sdk-go/pkg/client"
)

// 從環境變數讀取,請勿 hardcode
var (
edgeServerHost = getEnvOrDefault("EDGE_SERVER_HOST", "https://api.asgard-ai.com")
namespace = os.Getenv("NAMESPACE")
botProviderName = os.Getenv("BOT_PROVIDER_NAME")
botProviderApiKey = os.Getenv("BOT_PROVIDER_API_KEY")
)

func getEnvOrDefault(key, defaultVal string) string {
if v := os.Getenv(key); v != "" {
return v
}
return defaultVal
}

// POST https://your-backend.com/api/chat/blob
// 接收上傳檔案,轉發到 Edge Server
func UploadBlob(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
channelId := r.PathValue("channel_id")

bpClient := client.NewBotProviderClientWithConfig(&client.BotProviderConfig{
EdgeServerHost: edgeServerHost,
Namespace: namespace,
BotProviderName: botProviderName,
BotProviderApiKey: botProviderApiKey,
})

if err := r.ParseMultipartForm(128 << 20); err != nil { // 128 MiB
http.Error(w, "Invalid form", 400)
return
}
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, "file field required", 400)
return
}
defer file.Close()

mime := header.Header.Get("Content-Type")
blob, err := bpClient.UploadBlob(ctx, channelId, bufio.NewReader(file), header.Filename, &mime)
if err != nil {
http.Error(w, "Upload failed", 500)
return
}
json.NewEncoder(w).Encode(blob)
}

B. 前端: 把 endpoint 改為你的後端

前端仍可使用 @asgard-js/react, 只是 botProviderEndpoint 改為指向你的後端, 而非 Asgard。透過 customHeaders 帶上 Authorization header, 後端即可驗證身份:

import { Chatbot } from '@asgard-js/react';
import '@asgard-js/react/style';

export default function App() {
const accessToken = getAccessToken(); // 從你的 Auth 系統取得

return (
<Chatbot
title="我的 AI 助理"
config={{
botProviderEndpoint: 'https://your-backend.com/api/chat',
// 不需要 apiKey — 已藏在你的後端
}}
customChannelId={channelId}
customHeaders={{
Authorization: `Bearer ${accessToken}`,
}}
/>
);
}

Authorization header 只是示例。請根據你後端的驗證機制調整:若後端驗的是其他 header,改對應的 key;若使用 Cookie-based Session,瀏覽器同源請求會自動帶上 Cookie,customHeaders 可直接省略。

後端須實作的路由規格見 Endpoint 介面。完整 SDK API 見 後端 SDK 使用指南


Case Study: 虛構的「AI 個人筆記助理」

為了具體說明 4 個角色如何協同, 以下用一個虛構產品「AI 個人筆記助理」走一遍完整流程。這個產品的功能是:

「使用者跟 AI 對話時, AI 會自動記住使用者的偏好(例如喜好的解釋深度、慣用語言、領域興趣), 並在後續對話中參考。對話內容跨裝置同步, 公司需要稽核紀錄。」

資料庫設計(簡化)

流程拆解

當使用者送出「我最近在學 Rust, 推薦一些進階主題」時:

下次使用者開啟對話時, 後端會把「使用者目前在學 Rust」這條 memory 注入 Payload, Workflow 的 Prompt 自然會帶入這個上下文 — 使用者感覺 AI「記得」他。

同樣的架構能擴展到哪些功能?

這個架構非常有彈性, 以下功能都是同一個模式的變體:

  • Server-side push(訂單狀態變更時主動推訊息給使用者): 後端用 SDK 主動建立 SSE 連線, 把訊息推進 channel
  • Human Handoff(複雜問題轉真人客服): 後端攔截 asgard.tool_call.complete 事件, 當 LLM 呼叫 request_human 工具時, 觸發你的客服系統
  • CRM 同步: 攔截 asgard.message.complete 事件時把對話內容同步進 Salesforce
  • 配額管理:asgard.completion_model.usage 累積到使用者本月用量, 超量時拒絕新請求
  • 共享對話: 在你的 DB 加 channel_members 表, 讓多位使用者看到同一 channel

限制與注意事項

  1. 後端必須能處理 SSE 雙向: 你的後端需要同時作為 SSE Client(對 Edge Server)與 SSE Server(對前端)。常見錯誤是使用了不支援 streaming 的中介層(例如某些 Lambda 設定、某些 API Gateway 設定), 導致回應被緩衝。

  2. 客戶端斷線時務必清理上游 SSE 連線: 當使用者關閉頁面、切換頁籤, 你的 SSE Server 應該偵測到並關閉對 Edge Server 的上游連線, 避免資源浪費。

  3. 長連線基礎設施: SSE 是長連線, 要評估 Load Balancer 連線數上限、Idle Timeout 設定、Worker 並發模型。Idle Timeout 建議至少設為 5 分鐘, 避免 LLM 長回應在生成途中被 Load Balancer 中途切斷。

  4. 事件處理的 idempotency: 攔截事件做 side effect 時, 要考慮重連或重試導致的重複事件, 避免重複扣點、重複寫入 history。

  5. Bot Provider API Key 安全保管: Key 應放在 secrets manager, 不可寫入程式碼或環境變數明文。


延伸閱讀