多租戶 LINE Agent 架構決策:BeauBot 的 5 個踩坑
每家店一個 channel、prompt 隔離、context 共享 — 把 ReAct Agent 變成多租戶 SaaS 路上學到的事。
BeauBot 是給美業店家用的 LINE AI 客服 SaaS。一開始只服務一家美甲店,code 寫得很爽:寫死 LINE Channel ID、提示詞 hardcode、營業時間放 config。要擴成多家店時,5 個結構性問題炸出來。記下這些踩坑,給後來人。
踩坑 1:每家店一個 Channel,token 怎麼存
LINE Messaging API 的 Channel Access Token 是長期有效(可選 30 天 / 不過期),每家店的 token 必須加密存 DB。我的選擇:
// Channel token 用 PG 的 pgcrypto extension 加密
type Tenant struct {
ID string `db:"id"`
Name string `db:"name"`
LineChannelID string `db:"line_channel_id"`
// 不存原始 token,存 pgcrypto encrypt 後 base64
LineChannelTokenEnc string `db:"line_channel_token_enc"`
OperatingHours string `db:"operating_hours"` // JSON
}
// 寫入時:
// INSERT INTO tenants (..., line_channel_token_enc)
// VALUES (..., pgp_sym_encrypt($1, $masterKey))
//
// 讀取時:
// SELECT pgp_sym_decrypt(line_channel_token_enc::bytea, $masterKey) FROM tenants$masterKey 從 env 讀(AES_MASTER_KEY),絕不寫入 DB。
踩坑 2:Prompt 隔離 vs 共用模板
每家店的客服語氣不同:年輕美甲店要活潑,傳統美髮店要正式。但 prompt 結構大致相同(角色設定 + tool list + 範例)。
最初我給每家店一份完整 prompt,結果改 base 邏輯時要 update 所有 tenant — 災難。
改成 layered prompt:
[base layer — 共用]
你是一個美業店家的 LINE AI 客服。可用工具:
- query_services: 查服務項目
- check_availability: 查可預約時段
- create_booking: 建立預約
...
[tenant layer — 每家店覆蓋]
店家名稱:{{store_name}}
語氣:{{tone}} # casual | formal | friendly
特殊規則:{{custom_rules}}
範例對話:{{examples}}base layer 動,所有店一起得到改進;tenant layer 動,只影響該店。
踩坑 3:tools 的 tenant context
ReAct Agent 的工具呼叫要知道「現在是誰的店」。如果 tool function 簽名是 func queryServices(category string),就無法知道要查哪家店的 services。
解法:在 tool registration 時 partial apply tenant ID:
func buildTools(tenantID string, db *sql.DB) []Tool {
return []Tool{
{
Name: "query_services",
Description: "查詢本店所有服務項目",
// 把 tenantID closure 進去,LLM 看不到它
Run: func(args map[string]any) (string, error) {
rows, err := db.Query(
"SELECT name, price FROM services WHERE tenant_id = $1",
tenantID,
)
// ...
},
},
}
}LLM 只負責決定「呼叫 query_services」,租戶隔離永遠在程式層強制執行,不交給 LLM 自由 reasoning。
踩坑 4:rate limit per-tenant
OpenAI API 有全帳號級的 RPM / TPM 限制。一家店突然大量訊息(活動爆量),不能 throttle 到其他店。
加 per-tenant token bucket:
import "golang.org/x/time/rate"
var tenantLimiters sync.Map // map[tenantID]*rate.Limiter
func getLimiter(tenantID string) *rate.Limiter {
if l, ok := tenantLimiters.Load(tenantID); ok {
return l.(*rate.Limiter)
}
// 預設每秒 5 次、桶子 10 個
l := rate.NewLimiter(5, 10)
tenantLimiters.Store(tenantID, l)
return l
}
func RateLimitMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tid := r.Context().Value("tenant_id").(string)
if !getLimiter(tid).Allow() {
http.Error(w, "rate limit exceeded", 429)
return
}
next.ServeHTTP(w, r)
})
}踩坑 5:模型成本控制
GPT-4o 一次 reasoning 約 NT$ 0.5。一家店一天 300 則訊息 × 100 家店 = 30000 次 = NT$ 15000/day。撐不住。
改 意圖分類兩階段:
1. nano 階段(GPT-4o-mini)
判斷意圖類型:trivia / booking / complaint / other
↓
2. 視類型決定模型
trivia → 直接 nano 回(每次 NT$ 0.01)
booking → ReAct Agent 用 4o
complaint → 升級給真人客服成本下降 60%,使用者感受沒差。
學到什麼
多租戶不是把 schema 加 tenant_id 就好。 Prompt、tools、rate limit、成本控制每一層都要 per-tenant 設計。否則一家店壞了會拖垮所有人。
如果你也在做多租戶 AI SaaS,先把這 5 個問題列清楚再開工,會省掉至少 2 個月重構時間。