Skip to content

聊天機器人開發完整指南:狀態管理、記憶策略與tech stack選型

2026年3月13日 1 分鐘
TL;DR 聊天機器人不只是接 API。對話狀態管理、記憶機制、Streaming、Guardrails、可觀測性、tech stack選型,每一層都影響使用者體驗。
目錄
  1. 1. 對話狀態管理架構
    1. Session State(對話級別)
    2. User State(使用者級別)
    3. Global State(系統級別)
    4. 完整的狀態管理器
  2. 2. 對話歷史管理策略
    1. 策略一:Sliding Window
    2. 策略二:Summary + Recent
    3. 策略三:Selective Memory
    4. 策略比較
  3. 3. 記憶機制
    1. 短期記憶(In-Context Memory)
    2. 長期記憶(Persistent Memory)
    3. 在對話中注入長期記憶
    4. 個人化與隱私
  4. 4. Streaming 實作(SSE)
    1. 基本流程
    2. Server 端實作
    3. Client 端實作
    4. 使用 EventSource(簡化版)
    5. Streaming 注意事項
  5. 5. Guardrails 三層安全機制
    1. 第一層:Input Guards
    2. 第二層:Output Guards
    3. 第三層:System Guards
    4. 完整的 Guardrails Pipeline
  6. 6. 可觀測性
    1. Langfuse 整合
    2. 關鍵指標
    3. 建立 Dashboard
    4. 告警規則
  7. 7. tech stack選型
    1. TypeScript / Cloudflare Workers
    2. Python / FastAPI
    3. 比較表
    4. 選型決策樹
    5. 混合架構
  8. 總結
  9. 參考資料

大多數人對「聊天機器人」的理解停在「接 OpenAI API 然後把回覆貼出來」。

但只要你做過一個真正上線的 chatbot,你就知道——API 呼叫只占 10% 的工程量。剩下的 90% 是:

  • 對話要記住上下文嗎?記多少?
  • 記憶放在哪裡?記憶體?資料庫?向量庫?
  • 回覆要一次吐出還是 streaming?
  • 使用者輸入了惡意 prompt 怎麼辦?
  • 模型幻覺了怎麼追蹤?
  • 每次對話花多少錢?哪裡可以最佳化?

這篇文章把聊天機器人開發拆成七層,每一層都附上架構圖、程式碼和設計決策。


1. 對話狀態管理架構

一個聊天機器人要管理三種不同生命週期的狀態:

┌─────────────────────────────────────────────────────┐
│                    Chatbot System                     │
│                                                       │
│  ┌─────────────┐  ┌─────────────┐  ┌──────────────┐ │
│  │   Session    │  │    User     │  │    Global     │ │
│  │   State      │  │    State    │  │    State      │ │
│  │             │  │             │  │              │ │
│  │ • messages  │  │ • profile   │  │ • sys prompt │ │
│  │ • context   │  │ • prefs     │  │ • KB version │ │
│  │ • tool call │  │ • history   │  │ • model cfg  │ │
│  │ • temp vars │  │ • feedback  │  │ • rate limit │ │
│  │             │  │             │  │              │ │
│  │ TTL: 1 對話  │  │ TTL: 永久   │  │ TTL: 永久    │ │
│  └──────┬──────┘  └──────┬──────┘  └──────┬───────┘ │
│         │                │                │          │
│         ▼                ▼                ▼          │
│  ┌─────────────────────────────────────────────────┐ │
│  │              Context Assembly Layer              │ │
│  │   system_prompt + user_profile + messages[]      │ │
│  └──────────────────────┬──────────────────────────┘ │
│                         │                             │
│                         ▼                             │
│              ┌────────────────────┐                   │
│              │     LLM Call       │                   │
│              └────────────────────┘                   │
└─────────────────────────────────────────────────────┘

Session State(對話級別)

每個對話 session 獨立的狀態。使用者開新對話就重置。

interface SessionState {
  sessionId: string;
  messages: Message[];
  createdAt: Date;
  metadata: {
    toolCallCount: number;
    totalTokens: number;
    currentIntent: string | null;
  };
}

interface Message {
  role: 'system' | 'user' | 'assistant' | 'tool';
  content: string;
  timestamp: Date;
  tokenCount?: number;
  metadata?: Record<string, unknown>;
}

User State(使用者級別)

跨對話持久化,記住使用者是誰、偏好什麼。

interface UserState {
  userId: string;
  profile: {
    name: string;
    language: string;
    timezone: string;
  };
  preferences: {
    responseStyle: 'concise' | 'detailed';
    topics: string[];
  };
  history: {
    totalSessions: number;
    lastActiveAt: Date;
    frequentQuestions: string[];
  };
}

Global State(系統級別)

所有使用者共享的設定。

interface GlobalState {
  systemPrompt: string;
  knowledgeBaseVersion: string;
  modelConfig: {
    model: string;
    temperature: number;
    maxTokens: number;
  };
  rateLimits: {
    requestsPerMinute: number;
    tokensPerDay: number;
  };
}

完整的狀態管理器

class ChatStateManager {
  private sessions: Map<string, SessionState> = new Map();
  private userStore: KVNamespace; // Cloudflare KV 或其他持久化儲存
  private globalConfig: GlobalState;

  async assembleContext(
    sessionId: string,
    userId: string
  ): Promise<Message[]> {
    const session = this.sessions.get(sessionId);
    const user = await this.getUserState(userId);

    // 組裝 context:system prompt + 個人化資訊 + 對話歷史
    const systemMessage: Message = {
      role: 'system',
      content: this.buildSystemPrompt(user),
      timestamp: new Date(),
    };

    return [systemMessage, ...(session?.messages ?? [])];
  }

  private buildSystemPrompt(user: UserState | null): string {
    let prompt = this.globalConfig.systemPrompt;

    if (user) {
      prompt += `\n\n## 使用者偏好`;
      prompt += `\n- 語言:${user.profile.language}`;
      prompt += `\n- 回覆風格:${user.preferences.responseStyle}`;
      prompt += `\n- 關注主題:${user.preferences.topics.join('、')}`;
    }

    return prompt;
  }

  async getUserState(userId: string): Promise<UserState | null> {
    const raw = await this.userStore.get(userId);
    return raw ? JSON.parse(raw) : null;
  }

  async saveUserState(user: UserState): Promise<void> {
    await this.userStore.put(user.userId, JSON.stringify(user));
  }
}

設計決策:Session State 放記憶體(Map / Redis),User State 放 KV 或 DB,Global State 放環境變數或 config。三層分離讓你可以獨立調整每一層的 TTL 和儲存策略。


2. 對話歷史管理策略

context window 有限,你不可能把所有對話歷史都塞進去。三種主流策略:

策略一:Sliding Window

最簡單。只保留最近 N 輪對話。

function slidingWindow(messages: Message[], windowSize: number): Message[] {
  // 永遠保留 system prompt
  const system = messages.filter((m) => m.role === 'system');
  const conversation = messages.filter((m) => m.role !== 'system');

  // 取最後 N 輪(一輪 = user + assistant)
  const kept = conversation.slice(-windowSize * 2);

  return [...system, ...kept];
}
時間軸 →
[msg1] [msg2] [msg3] [msg4] [msg5] [msg6] [msg7] [msg8]
                                    ├──── window = 2 ────┤
                                    保留 msg5-msg8
優點缺點
實作最簡單早期重要對話會被丟掉
Token 用量可預測使用者提到「前面說的那個」就斷了
適合短對話場景不適合需要長期上下文的任務

策略二:Summary + Recent

舊對話用 LLM 壓縮成摘要,新對話保持原文。兼顧上下文和 token 用量。

class SummaryPlusRecent {
  private llm: LLMClient;
  private maxRecentMessages = 10;
  private summaryTokenThreshold = 2000;

  async manage(messages: Message[]): Promise<Message[]> {
    const system = messages.filter((m) => m.role === 'system');
    const conversation = messages.filter((m) => m.role !== 'system');

    if (this.estimateTokens(conversation) <= this.summaryTokenThreshold) {
      // 還沒超過閾值,全部保留
      return messages;
    }

    // 拆分:舊的要摘要,新的保留
    const recent = conversation.slice(-this.maxRecentMessages);
    const toSummarize = conversation.slice(0, -this.maxRecentMessages);

    const summary = await this.summarize(toSummarize);

    // 把摘要當作一則 system message 插入
    const summaryMessage: Message = {
      role: 'system',
      content: `## 先前對話摘要\n${summary}`,
      timestamp: new Date(),
    };

    return [...system, summaryMessage, ...recent];
  }

  private async summarize(messages: Message[]): Promise<string> {
    const formatted = messages
      .map((m) => `${m.role}: ${m.content}`)
      .join('\n');

    const response = await this.llm.chat([
      {
        role: 'system',
        content:
          '請用繁體中文摘要以下對話的重點,保留關鍵資訊(名字、數字、決定、待辦事項)。用條列式。',
      },
      { role: 'user', content: formatted },
    ]);

    return response.content;
  }

  private estimateTokens(messages: Message[]): number {
    // 粗估:中文約 1.5 token/字,英文約 1.3 token/word
    return messages.reduce((sum, m) => sum + m.content.length * 1.5, 0);
  }
}
┌──────────────────────────────────────────────────┐
│                                                    │
│  [msg1..msg20]  ──LLM──▶  [summary]               │
│      舊對話                  壓縮成一段摘要         │
│                                                    │
│  [summary] + [msg21..msg30]                        │
│     ↑ 摘要       ↑ 最近 10 輪原文                  │
│                                                    │
│  ──── 這就是送進 LLM 的 context ────                │
└──────────────────────────────────────────────────┘
優點缺點
保留長期上下文摘要過程會丟失細節
Token 用量可控每次摘要需要額外 LLM 呼叫(花錢)
體驗最平衡摘要品質依賴 prompt 設計

策略三:Selective Memory

只保留被標記為「重要」的訊息。需要額外的判斷邏輯。

interface SelectiveMessage extends Message {
  importance: 'critical' | 'normal' | 'trivial';
  topics: string[];
}

class SelectiveMemory {
  private llm: LLMClient;

  async classify(message: Message): Promise<SelectiveMessage> {
    const response = await this.llm.chat([
      {
        role: 'system',
        content: `判斷以下訊息的重要性。回傳 JSON:
{ "importance": "critical" | "normal" | "trivial", "topics": ["主題1", "主題2"] }

判斷標準:
- critical:包含決定、數字、期限、人名、明確指令
- normal:一般討論、解釋
- trivial:寒暄、確認、重複`,
      },
      { role: 'user', content: message.content },
    ]);

    const result = JSON.parse(response.content);
    return { ...message, ...result };
  }

  async filterForContext(
    messages: SelectiveMessage[],
    tokenBudget: number
  ): Promise<SelectiveMessage[]> {
    // 優先保留 critical,再看 normal,丟掉 trivial
    const critical = messages.filter((m) => m.importance === 'critical');
    const normal = messages.filter((m) => m.importance === 'normal');

    let result = [...critical];
    let currentTokens = this.estimateTokens(result);

    for (const msg of normal.reverse()) {
      const msgTokens = msg.content.length * 1.5;
      if (currentTokens + msgTokens > tokenBudget) break;
      result.unshift(msg);
      currentTokens += msgTokens;
    }

    return result;
  }

  private estimateTokens(messages: SelectiveMessage[]): number {
    return messages.reduce((sum, m) => sum + m.content.length * 1.5, 0);
  }
}
優點缺點
保留最有價值的資訊需要額外 LLM 呼叫做分類
上下文品質最高分類錯誤會導致重要資訊遺失
可搭配 RAG 做語義檢索實作複雜度最高

策略比較

                    簡單度    上下文保留    Token 成本    適用場景
Sliding Window      ★★★★★    ★★☆☆☆       ★★★★★       短對話、FAQ Bot
Summary + Recent    ★★★☆☆    ★★★★☆       ★★★☆☆       一般客服、助手
Selective Memory    ★★☆☆☆    ★★★★★       ★★☆☆☆       長對話、專案管理

實務建議:從 Sliding Window 開始,發現不夠時升級到 Summary + Recent。Selective Memory 只在確實需要時才用——它的額外 LLM 呼叫成本不低。


3. 記憶機制

對話歷史管理處理的是「單次對話內的記憶」。但真正好的 chatbot 需要三層記憶:

┌──────────────────────────────────────────────────────┐
│                    記憶架構                            │
│                                                        │
│   ┌────────────────┐   短期記憶(In-Context)          │
│   │ Context Window │   ← 當前對話的 messages[]         │
│   └───────┬────────┘                                   │
│           │                                            │
│   ┌───────▼────────┐   長期記憶(Persistent)          │
│   │ Vector Store   │   ← 歷史對話 embedding 檢索       │
│   │ + Database     │   ← 結構化事實儲存                │
│   └───────┬────────┘                                   │
│           │                                            │
│   ┌───────▼────────┐   個人化(User Profile)          │
│   │ User State     │   ← 偏好、習慣、隱私控制          │
│   └────────────────┘                                   │
└──────────────────────────────────────────────────────┘

短期記憶(In-Context Memory)

就是 messages array。上一節的三種策略都是在管理這層。重點是:

  • 不要超過 context window 的 70%,留空間給 system prompt 和回覆
  • token 計算要精確,粗估會出事
  • 永遠優先保留最近的 user/assistant 對
function estimateTokensAccurate(text: string): number {
  // 更精確的估算:使用 tiktoken 或類似工具
  // 這裡用簡化版:中文每字約 2 tokens,英文每 4 字母約 1 token
  const chineseChars = (text.match(/[\u4e00-\u9fff]/g) || []).length;
  const otherChars = text.length - chineseChars;
  return chineseChars * 2 + Math.ceil(otherChars / 4);
}

function fitWithinBudget(
  messages: Message[],
  maxTokens: number
): Message[] {
  const system = messages.filter((m) => m.role === 'system');
  const conversation = messages.filter((m) => m.role !== 'system');

  let totalTokens = system.reduce(
    (sum, m) => sum + estimateTokensAccurate(m.content),
    0
  );

  const result: Message[] = [];

  // 從最近的開始往回加,直到超出預算
  for (let i = conversation.length - 1; i >= 0; i--) {
    const msgTokens = estimateTokensAccurate(conversation[i].content);
    if (totalTokens + msgTokens > maxTokens) break;
    result.unshift(conversation[i]);
    totalTokens += msgTokens;
  }

  return [...system, ...result];
}

長期記憶(Persistent Memory)

用向量資料庫儲存歷史對話,對話時用語義搜尋把相關記憶拉回來。

import { OpenAI } from 'openai';

interface MemoryEntry {
  id: string;
  userId: string;
  content: string;
  embedding: number[];
  timestamp: Date;
  sessionId: string;
  type: 'fact' | 'preference' | 'decision' | 'general';
}

class LongTermMemory {
  private openai: OpenAI;
  private vectorStore: VectorStore; // Qdrant / Pinecone / Cloudflare Vectorize

  async store(
    userId: string,
    sessionId: string,
    messages: Message[]
  ): Promise<void> {
    // 把重要的對話段落存入向量庫
    const facts = await this.extractFacts(messages);

    for (const fact of facts) {
      const embedding = await this.embed(fact.content);
      await this.vectorStore.upsert({
        id: crypto.randomUUID(),
        userId,
        content: fact.content,
        embedding,
        timestamp: new Date(),
        sessionId,
        type: fact.type,
      });
    }
  }

  async recall(
    userId: string,
    query: string,
    topK = 5
  ): Promise<MemoryEntry[]> {
    const queryEmbedding = await this.embed(query);

    // 只搜尋該使用者的記憶
    return this.vectorStore.search({
      vector: queryEmbedding,
      filter: { userId },
      topK,
      minScore: 0.7, // 相似度閾值
    });
  }

  private async extractFacts(
    messages: Message[]
  ): Promise<{ content: string; type: MemoryEntry['type'] }[]> {
    const formatted = messages
      .map((m) => `${m.role}: ${m.content}`)
      .join('\n');

    const response = await this.openai.chat.completions.create({
      model: 'gpt-4o-mini',
      messages: [
        {
          role: 'system',
          content: `從以下對話中提取值得記住的事實。回傳 JSON 陣列:
[{ "content": "事實描述", "type": "fact|preference|decision|general" }]

提取標準:
- fact:使用者提到的具體事實(公司名、角色、專案名)
- preference:偏好(喜歡用什麼工具、語言、風格)
- decision:做出的決定(選了 A 方案)
- general:其他值得記住的背景資訊

不要提取寒暄或太泛泛的內容。`,
        },
        { role: 'user', content: formatted },
      ],
      response_format: { type: 'json_object' },
    });

    return JSON.parse(response.choices[0].message.content ?? '[]');
  }

  private async embed(text: string): Promise<number[]> {
    const response = await this.openai.embeddings.create({
      model: 'text-embedding-3-small',
      input: text,
    });
    return response.data[0].embedding;
  }
}

在對話中注入長期記憶

class MemoryAugmentedChat {
  private shortTerm: SummaryPlusRecent;
  private longTerm: LongTermMemory;

  async chat(
    userId: string,
    sessionId: string,
    userMessage: string
  ): Promise<string> {
    // 1. 從長期記憶中召回相關內容
    const memories = await this.longTerm.recall(userId, userMessage, 3);

    // 2. 組裝 memory context
    const memoryContext =
      memories.length > 0
        ? `\n\n## 相關記憶\n${memories.map((m) => `- [${m.type}] ${m.content}`).join('\n')}`
        : '';

    // 3. 把記憶注入 system prompt
    const systemPrompt = `你是一個友善的助手。${memoryContext}`;

    // 4. 管理短期對話歷史
    const messages = await this.shortTerm.manage([
      { role: 'system', content: systemPrompt, timestamp: new Date() },
      // ... 現有對話歷史
      { role: 'user', content: userMessage, timestamp: new Date() },
    ]);

    // 5. 呼叫 LLM
    const response = await this.llm.chat(messages);

    return response.content;
  }
}

個人化與隱私

使用者資料要能被刪除。GDPR 和隱私法規要求你做到這一點。

class UserMemoryManager {
  private longTerm: LongTermMemory;
  private userStore: KVNamespace;

  // 使用者要求刪除所有記憶
  async deleteAllMemories(userId: string): Promise<void> {
    await this.longTerm.deleteByUser(userId);
    await this.userStore.delete(userId);
  }

  // 使用者要求刪除特定記憶
  async deleteMemory(userId: string, memoryId: string): Promise<void> {
    await this.longTerm.delete(memoryId, userId);
  }

  // 匯出使用者所有資料(GDPR right to data portability)
  async exportUserData(userId: string): Promise<{
    profile: UserState | null;
    memories: MemoryEntry[];
  }> {
    const profile = await this.userStore.get(userId);
    const memories = await this.longTerm.listByUser(userId);

    return {
      profile: profile ? JSON.parse(profile) : null,
      memories,
    };
  }

  // 個人化注入:把使用者資料轉成 system prompt 的一部分
  buildPersonalizationPrompt(user: UserState): string {
    const lines: string[] = ['## 使用者資訊'];

    if (user.profile.name) {
      lines.push(`- 稱呼:${user.profile.name}`);
    }
    if (user.preferences.responseStyle) {
      lines.push(`- 偏好回覆風格:${user.preferences.responseStyle}`);
    }
    if (user.preferences.topics.length > 0) {
      lines.push(`- 關注主題:${user.preferences.topics.join('、')}`);
    }
    if (user.history.frequentQuestions.length > 0) {
      lines.push(
        `- 常問問題:${user.history.frequentQuestions.slice(0, 3).join('、')}`
      );
    }

    return lines.join('\n');
  }
}

隱私設計原則

  1. 最小收集:只記住對改善體驗有直接幫助的資訊
  2. 透明度:讓使用者知道你記住了什麼(提供查看/匯出功能)
  3. 可刪除:使用者說「忘記我」就要真的刪掉,包括向量庫裡的 embedding
  4. 過期機制:設定 TTL,超過 90 天未活躍的記憶自動清理

4. Streaming 實作(SSE)

使用者不想等 5 秒才看到回覆。Streaming 讓第一個字在幾百毫秒內就出現。

基本流程

Client                         Server                         LLM
  │                              │                              │
  │  POST /chat                  │                              │
  │ ──────────────────────────▶  │                              │
  │                              │  stream: true                │
  │                              │ ──────────────────────────▶  │
  │                              │                              │
  │  text/event-stream           │  chunk: "你"                 │
  │ ◀──────────────────────────  │ ◀──────────────────────────  │
  │  data: {"content":"你"}      │                              │
  │                              │  chunk: "好"                 │
  │ ◀──────────────────────────  │ ◀──────────────────────────  │
  │  data: {"content":"好"}      │                              │
  │                              │  chunk: "!"                 │
  │ ◀──────────────────────────  │ ◀──────────────────────────  │
  │  data: {"content":"!"}      │                              │
  │                              │  [DONE]                      │
  │ ◀──────────────────────────  │ ◀──────────────────────────  │
  │  data: [DONE]                │                              │
  │                              │                              │

Server 端實作

import { Hono } from 'hono';
import { streamSSE } from 'hono/streaming';
import { OpenAI } from 'openai';

const app = new Hono();

app.post('/api/chat', async (c) => {
  const { messages, sessionId } = await c.req.json();
  const openai = new OpenAI({ apiKey: c.env.OPENAI_API_KEY });

  return streamSSE(c, async (stream) => {
    try {
      const response = await openai.chat.completions.create({
        model: 'gpt-4o',
        messages,
        stream: true,
      });

      let fullContent = '';

      for await (const chunk of response) {
        const content = chunk.choices[0]?.delta?.content;
        if (content) {
          fullContent += content;

          await stream.writeSSE({
            event: 'message',
            data: JSON.stringify({
              type: 'content',
              content,
            }),
          });
        }
      }

      // 串流結束,送出完整的 metadata
      await stream.writeSSE({
        event: 'message',
        data: JSON.stringify({
          type: 'done',
          usage: {
            totalTokens: fullContent.length * 1.5, // 粗估
          },
        }),
      });
    } catch (error) {
      await stream.writeSSE({
        event: 'error',
        data: JSON.stringify({
          type: 'error',
          message: error instanceof Error ? error.message : 'Unknown error',
        }),
      });
    }
  });
});

export default app;

Client 端實作

class ChatClient {
  private baseUrl: string;

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }

  async sendMessage(
    messages: { role: string; content: string }[],
    onChunk: (content: string) => void,
    onDone: () => void,
    onError: (error: string) => void
  ): Promise<void> {
    const response = await fetch(`${this.baseUrl}/api/chat`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ messages }),
    });

    if (!response.ok) {
      onError(`HTTP ${response.status}`);
      return;
    }

    const reader = response.body?.getReader();
    const decoder = new TextDecoder();

    if (!reader) {
      onError('No response body');
      return;
    }

    let buffer = '';

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;

      buffer += decoder.decode(value, { stream: true });

      // 解析 SSE 事件
      const lines = buffer.split('\n');
      buffer = lines.pop() ?? ''; // 最後一行可能不完整,保留

      for (const line of lines) {
        if (!line.startsWith('data: ')) continue;

        const data = line.slice(6);
        if (data === '[DONE]') {
          onDone();
          return;
        }

        try {
          const parsed = JSON.parse(data);
          if (parsed.type === 'content') {
            onChunk(parsed.content);
          } else if (parsed.type === 'done') {
            onDone();
          } else if (parsed.type === 'error') {
            onError(parsed.message);
          }
        } catch {
          // 忽略解析失敗的行
        }
      }
    }
  }
}

// 使用範例
const client = new ChatClient('https://api.example.com');
let fullResponse = '';

await client.sendMessage(
  [{ role: 'user', content: '什麼是 RAG?' }],
  (chunk) => {
    fullResponse += chunk;
    // 即時更新 UI
    updateChatUI(fullResponse);
  },
  () => {
    console.log('Stream 完成');
  },
  (error) => {
    console.error('Stream 錯誤:', error);
    showErrorUI(error);
  }
);

使用 EventSource(簡化版)

如果你的 API 支援 GET 請求的 SSE:

function connectSSE(url: string): void {
  const source = new EventSource(url);

  source.onmessage = (event) => {
    const data = JSON.parse(event.data);
    if (data.type === 'content') {
      appendToChat(data.content);
    }
  };

  source.onerror = (event) => {
    console.error('SSE 連線錯誤', event);
    source.close();
    // 實作重連邏輯
    setTimeout(() => connectSSE(url), 3000);
  };
}

Streaming 注意事項

  1. TTFT(Time to First Token)是關鍵指標:使用者感受到的「速度」主要取決於第一個字多快出現,不是整體回應時間
  2. 錯誤處理:stream 中間斷掉要能優雅降級,至少顯示已收到的部分
  3. 取消機制:使用者切換對話或按停止時,要能 abort stream
  4. 背壓控制:如果前端處理速度跟不上 stream 速度,需要 buffer 機制
// 取消 stream 的實作
const controller = new AbortController();

fetch('/api/chat', {
  method: 'POST',
  body: JSON.stringify({ messages }),
  signal: controller.signal,
});

// 使用者按停止按鈕
stopButton.onclick = () => controller.abort();

5. Guardrails 三層安全機制

你的 chatbot 面向真實使用者,要有防禦深度。三層 Guardrails:

              使用者輸入

        ┌────────▼────────┐
        │  Input Guards    │  ← Prompt injection、PII、內容分類
        │  (Pre-LLM)      │
        └────────┬────────┘
                 │ ✓ 通過
        ┌────────▼────────┐
        │    LLM Call      │
        └────────┬────────┘

        ┌────────▼────────┐
        │  Output Guards   │  ← Faithfulness、Toxicity、幻覺偵測
        │  (Post-LLM)     │
        └────────┬────────┘
                 │ ✓ 通過
        ┌────────▼────────┐
        │  System Guards   │  ← Rate limit、Token budget、Fallback
        │  (Infrastructure)│
        └────────┬────────┘

              回覆使用者

第一層:Input Guards

interface GuardResult {
  passed: boolean;
  reason?: string;
  action: 'allow' | 'block' | 'modify';
  modifiedContent?: string;
}

class InputGuards {
  // 1. Prompt Injection 偵測
  async detectInjection(input: string): Promise<GuardResult> {
    const injectionPatterns = [
      /ignore\s+(all\s+)?(previous|above|prior)\s+(instructions|prompts)/i,
      /you\s+are\s+now\s+/i,
      /system\s*:\s*/i,
      /\[INST\]/i,
      /<<SYS>>/i,
      /forget\s+(everything|all|your\s+instructions)/i,
      /do\s+not\s+follow\s+(your|the)\s+(rules|instructions)/i,
    ];

    const hasPattern = injectionPatterns.some((p) => p.test(input));

    if (hasPattern) {
      return {
        passed: false,
        reason: 'Potential prompt injection detected',
        action: 'block',
      };
    }

    // 進階:用 LLM 做二次確認(可選,會增加延遲)
    // const llmCheck = await this.llmInjectionCheck(input);

    return { passed: true, action: 'allow' };
  }

  // 2. PII 偵測與遮蔽
  detectAndMaskPII(input: string): GuardResult {
    const piiPatterns: { pattern: RegExp; mask: string; name: string }[] = [
      {
        pattern: /\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/g,
        mask: '[信用卡號]',
        name: 'credit_card',
      },
      {
        pattern: /\b[A-Z]\d{9}\b/g,
        mask: '[身分證字號]',
        name: 'national_id',
      },
      {
        pattern: /\b09\d{2}[-\s]?\d{3}[-\s]?\d{3}\b/g,
        mask: '[手機號碼]',
        name: 'phone',
      },
      {
        pattern:
          /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
        mask: '[電子郵件]',
        name: 'email',
      },
    ];

    let modified = input;
    const detected: string[] = [];

    for (const { pattern, mask, name } of piiPatterns) {
      if (pattern.test(modified)) {
        detected.push(name);
        modified = modified.replace(pattern, mask);
      }
    }

    if (detected.length > 0) {
      return {
        passed: true,
        reason: `PII detected and masked: ${detected.join(', ')}`,
        action: 'modify',
        modifiedContent: modified,
      };
    }

    return { passed: true, action: 'allow' };
  }

  // 3. 內容分類
  async classifyContent(
    input: string
  ): Promise<GuardResult & { category?: string }> {
    const blockedCategories = ['violence', 'illegal', 'sexual_explicit'];

    // 簡單版:關鍵字過濾
    // 生產版:用分類模型或 moderation API
    const response = await fetch('https://api.openai.com/v1/moderations', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ input }),
    });

    const result = await response.json();
    const flagged = result.results[0]?.flagged;

    if (flagged) {
      return {
        passed: false,
        reason: 'Content flagged by moderation',
        action: 'block',
        category: 'moderation_flagged',
      };
    }

    return { passed: true, action: 'allow' };
  }
}

第二層:Output Guards

class OutputGuards {
  private llm: LLMClient;

  // 1. Faithfulness 檢查(回答是否忠於提供的上下文)
  async checkFaithfulness(
    context: string,
    answer: string
  ): Promise<GuardResult & { score: number }> {
    const response = await this.llm.chat([
      {
        role: 'system',
        content: `你是一個事實查核員。判斷 answer 是否忠實於 context。
回傳 JSON:{ "score": 0.0-1.0, "issues": ["問題描述"] }

score 標準:
- 1.0:完全忠實,所有資訊都來自 context
- 0.7-0.9:大致忠實,有些合理推論
- 0.5-0.7:部分忠實,有些資訊無法從 context 驗證
- 0.0-0.5:不忠實,包含 context 中沒有的事實`,
      },
      {
        role: 'user',
        content: `Context:\n${context}\n\nAnswer:\n${answer}`,
      },
    ]);

    const result = JSON.parse(response.content);

    return {
      passed: result.score >= 0.7,
      reason:
        result.score < 0.7
          ? `Faithfulness too low: ${result.score}`
          : undefined,
      action: result.score >= 0.7 ? 'allow' : 'block',
      score: result.score,
    };
  }

  // 2. Toxicity 過濾
  async checkToxicity(output: string): Promise<GuardResult> {
    // 用 OpenAI Moderation API 或自建分類器
    const response = await fetch('https://api.openai.com/v1/moderations', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ input: output }),
    });

    const result = await response.json();

    if (result.results[0]?.flagged) {
      return {
        passed: false,
        reason: 'Output contains toxic content',
        action: 'block',
      };
    }

    return { passed: true, action: 'allow' };
  }

  // 3. 幻覺偵測
  async detectHallucination(
    question: string,
    answer: string,
    sources: string[]
  ): Promise<GuardResult & { claims: { text: string; supported: boolean }[] }> {
    const response = await this.llm.chat([
      {
        role: 'system',
        content: `你是一個幻覺偵測器。把 answer 拆成獨立的事實聲稱(claims),然後判斷每個 claim 是否有 sources 支持。

回傳 JSON:
{
  "claims": [
    { "text": "聲稱內容", "supported": true/false }
  ]
}`,
      },
      {
        role: 'user',
        content: `Question: ${question}\nAnswer: ${answer}\nSources:\n${sources.join('\n---\n')}`,
      },
    ]);

    const result = JSON.parse(response.content);
    const unsupported = result.claims.filter(
      (c: { supported: boolean }) => !c.supported
    );

    return {
      passed: unsupported.length === 0,
      reason:
        unsupported.length > 0
          ? `${unsupported.length} unsupported claims found`
          : undefined,
      action: unsupported.length === 0 ? 'allow' : 'modify',
      claims: result.claims,
    };
  }
}

第三層:System Guards

class SystemGuards {
  private requestCounts: Map<string, { count: number; resetAt: number }> =
    new Map();
  private tokenUsage: Map<string, number> = new Map();

  // 1. Rate Limiting
  checkRateLimit(
    userId: string,
    limit: number,
    windowMs: number
  ): GuardResult {
    const now = Date.now();
    const record = this.requestCounts.get(userId);

    if (!record || now > record.resetAt) {
      this.requestCounts.set(userId, { count: 1, resetAt: now + windowMs });
      return { passed: true, action: 'allow' };
    }

    if (record.count >= limit) {
      return {
        passed: false,
        reason: `Rate limit exceeded: ${limit} requests per ${windowMs / 1000}s`,
        action: 'block',
      };
    }

    record.count++;
    return { passed: true, action: 'allow' };
  }

  // 2. Token Budget
  checkTokenBudget(
    userId: string,
    estimatedTokens: number,
    dailyBudget: number
  ): GuardResult {
    const used = this.tokenUsage.get(userId) ?? 0;

    if (used + estimatedTokens > dailyBudget) {
      return {
        passed: false,
        reason: `Token budget exceeded: ${used}/${dailyBudget}`,
        action: 'block',
      };
    }

    this.tokenUsage.set(userId, used + estimatedTokens);
    return { passed: true, action: 'allow' };
  }

  // 3. Fallback 降級策略
  async withFallback<T>(
    primary: () => Promise<T>,
    fallback: () => Promise<T>,
    maxRetries = 2
  ): Promise<T> {
    for (let i = 0; i <= maxRetries; i++) {
      try {
        return await primary();
      } catch (error) {
        console.error(
          `Attempt ${i + 1} failed:`,
          error instanceof Error ? error.message : error
        );

        if (i === maxRetries) {
          console.warn('All retries failed, using fallback');
          return fallback();
        }

        // 指數退避
        await new Promise((r) => setTimeout(r, 1000 * 2 ** i));
      }
    }

    return fallback(); // TypeScript 需要這行
  }
}

// 使用範例:主模型掛掉時降級到備用模型
const systemGuards = new SystemGuards();

const response = await systemGuards.withFallback(
  // 主路徑:GPT-4o
  () => callLLM({ model: 'gpt-4o', messages }),
  // 降級路徑:GPT-4o-mini
  () => callLLM({ model: 'gpt-4o-mini', messages }),
  2 // 重試 2 次
);

完整的 Guardrails Pipeline

class GuardrailsPipeline {
  private input = new InputGuards();
  private output = new OutputGuards();
  private system = new SystemGuards();

  async process(
    userId: string,
    userMessage: string,
    context: { sources: string[] }
  ): Promise<{
    response: string;
    guardrailsLog: Record<string, unknown>[];
  }> {
    const log: Record<string, unknown>[] = [];

    // === System Guards (先檢查,最便宜) ===
    const rateCheck = this.system.checkRateLimit(userId, 20, 60_000);
    log.push({ guard: 'rate_limit', ...rateCheck });
    if (!rateCheck.passed) {
      return {
        response: '您的請求過於頻繁,請稍後再試。',
        guardrailsLog: log,
      };
    }

    // === Input Guards ===
    const injectionCheck = await this.input.detectInjection(userMessage);
    log.push({ guard: 'injection', ...injectionCheck });
    if (!injectionCheck.passed) {
      return {
        response: '抱歉,我無法處理這類請求。',
        guardrailsLog: log,
      };
    }

    const piiCheck = this.input.detectAndMaskPII(userMessage);
    log.push({ guard: 'pii', ...piiCheck });
    const sanitizedInput =
      piiCheck.action === 'modify'
        ? piiCheck.modifiedContent!
        : userMessage;

    // === LLM Call ===
    const llmResponse = await this.system.withFallback(
      () => callLLM({ model: 'gpt-4o', messages: [/* ... */] }),
      () => callLLM({ model: 'gpt-4o-mini', messages: [/* ... */] })
    );

    // === Output Guards ===
    const toxicityCheck = await this.output.checkToxicity(llmResponse);
    log.push({ guard: 'toxicity', ...toxicityCheck });
    if (!toxicityCheck.passed) {
      return {
        response: '抱歉,我無法提供這類回覆,請換個問題。',
        guardrailsLog: log,
      };
    }

    const faithCheck = await this.output.checkFaithfulness(
      context.sources.join('\n'),
      llmResponse
    );
    log.push({ guard: 'faithfulness', ...faithCheck });

    return {
      response: llmResponse,
      guardrailsLog: log,
    };
  }
}

效能提醒:每一層 guard 都有延遲成本。生產環境中,不需要的 guard 就關掉。Input guards 用 regex 版幾乎零延遲,用 LLM 版就要多幾百毫秒。Output guards 的 faithfulness check 會額外多一次 LLM 呼叫。根據你的場景做取捨。


6. 可觀測性

你的 chatbot 上線了,但你怎麼知道它表現好不好?

Langfuse 整合

Langfuse 是開源的 LLM 可觀測性平台。它可以追蹤每一次 LLM 呼叫的 input/output、token 用量、延遲、成本。

import { Langfuse } from 'langfuse';

const langfuse = new Langfuse({
  publicKey: process.env.LANGFUSE_PUBLIC_KEY!,
  secretKey: process.env.LANGFUSE_SECRET_KEY!,
  baseUrl: process.env.LANGFUSE_HOST, // self-hosted 或 cloud
});

class ObservableChat {
  async chat(
    userId: string,
    sessionId: string,
    userMessage: string
  ): Promise<string> {
    // 建立 trace:追蹤整個對話流程
    const trace = langfuse.trace({
      name: 'chat',
      userId,
      sessionId,
      input: userMessage,
      metadata: {
        environment: 'production',
      },
    });

    // 追蹤 retrieval 步驟
    const retrievalSpan = trace.span({
      name: 'memory-recall',
      input: { query: userMessage },
    });

    const memories = await this.longTerm.recall(userId, userMessage);
    retrievalSpan.end({
      output: { count: memories.length },
    });

    // 追蹤 LLM 呼叫
    const generation = trace.generation({
      name: 'llm-call',
      model: 'gpt-4o',
      input: messages,
      modelParameters: {
        temperature: 0.7,
        maxTokens: 2048,
      },
    });

    const response = await openai.chat.completions.create({
      model: 'gpt-4o',
      messages,
    });

    generation.end({
      output: response.choices[0].message.content,
      usage: {
        promptTokens: response.usage?.prompt_tokens,
        completionTokens: response.usage?.completion_tokens,
        totalTokens: response.usage?.total_tokens,
      },
    });

    // 追蹤 guardrails 步驟
    const guardrailSpan = trace.span({
      name: 'guardrails',
      input: { response: response.choices[0].message.content },
    });

    const guardResult = await this.guardrails.checkOutput(
      response.choices[0].message.content!
    );

    guardrailSpan.end({
      output: guardResult,
    });

    // 結束 trace
    const finalResponse = response.choices[0].message.content!;
    trace.update({
      output: finalResponse,
    });

    // 記錄使用者評分(之後從前端回傳)
    // trace.score({ name: 'user-feedback', value: 4 });

    return finalResponse;
  }

  // 使用者點擊 thumbs up/down 時呼叫
  async recordFeedback(
    traceId: string,
    score: number,
    comment?: string
  ): Promise<void> {
    langfuse.score({
      traceId,
      name: 'user-feedback',
      value: score,
      comment,
    });
  }
}

關鍵指標

指標說明目標值如何衡量
TTFTTime to First Token< 500ms從收到 request 到第一個 SSE chunk 的時間
Latency完整回應時間< 3s(簡單問題)< 10s(複雜問題)從 request 到最後一個 chunk 的時間
Faithfulness回答忠實度> 0.9LLM-as-Judge 或人工抽樣評估
User Satisfaction使用者滿意度> 4/5Thumbs up/down 或 1-5 評分
Token Cost每次對話成本依預算Langfuse 自動追蹤
Error Rate錯誤率< 1%5xx + stream 中斷 + guardrail block
Guardrail Trigger RateGuard 觸發率監控趨勢各層 guard 的 block/modify 比例

建立 Dashboard

// 每日指標彙總(可用 cron job 跑)
async function dailyMetricsSummary(): Promise<void> {
  const langfuse = new Langfuse({ /* ... */ });

  // 用 Langfuse API 撈資料
  const traces = await langfuse.fetchTraces({
    fromTimestamp: new Date(Date.now() - 24 * 60 * 60 * 1000),
    limit: 1000,
  });

  const metrics = {
    totalConversations: traces.data.length,
    avgLatency:
      traces.data.reduce((sum, t) => sum + (t.latency ?? 0), 0) /
      traces.data.length,
    avgTokens:
      traces.data.reduce((sum, t) => sum + (t.totalTokens ?? 0), 0) /
      traces.data.length,
    errorRate:
      traces.data.filter((t) => t.level === 'ERROR').length /
      traces.data.length,
    avgFeedback:
      traces.data
        .filter((t) => t.scores && t.scores.length > 0)
        .reduce(
          (sum, t) => sum + (t.scores?.[0]?.value ?? 0),
          0
        ) /
      traces.data.filter((t) => t.scores && t.scores.length > 0).length,
  };

  console.log('Daily Metrics:', metrics);
  // 推送到 Slack / Discord / 其他告警管道
}

告警規則

interface AlertRule {
  metric: string;
  condition: 'above' | 'below';
  threshold: number;
  window: string; // e.g., '5m', '1h'
  action: (value: number) => void;
}

const alertRules: AlertRule[] = [
  {
    metric: 'faithfulness',
    condition: 'below',
    threshold: 0.7,
    window: '1h',
    action: (v) =>
      sendSlackAlert(
        `Faithfulness 降至 ${v.toFixed(2)},檢查 retrieval 品質`
      ),
  },
  {
    metric: 'latency_p95',
    condition: 'above',
    threshold: 5000,
    window: '5m',
    action: (v) =>
      sendSlackAlert(
        `P95 延遲飆升至 ${v}ms,檢查 LLM provider 狀態`
      ),
  },
  {
    metric: 'error_rate',
    condition: 'above',
    threshold: 0.05,
    window: '10m',
    action: (v) =>
      sendSlackAlert(
        `錯誤率 ${(v * 100).toFixed(1)}%,檢查 tool calling 和 API 狀態`
      ),
  },
  {
    metric: 'guardrail_block_rate',
    condition: 'above',
    threshold: 0.2,
    window: '1h',
    action: (v) =>
      sendSlackAlert(
        `Guardrail 封鎖率 ${(v * 100).toFixed(1)}%,可能有攻擊或規則過嚴`
      ),
  },
];

可觀測性不是選配,是必備。沒有可觀測性的 chatbot 就是瞎飛。你不知道使用者的真實體驗是什麼、哪裡在掉球、每天燒多少錢。


7. tech stack選型

TypeScript / Cloudflare Workers

┌────────────────────────────────────────┐
│         Cloudflare Workers Stack        │
│                                          │
│  Framework:  Hono                        │
│  AI SDK:     Vercel AI SDK / OpenAI SDK  │
│  Vector DB:  Cloudflare Vectorize        │
│  KV Store:   Cloudflare KV / D1          │
│  Embedding:  Workers AI / OpenAI         │
│  Runtime:    V8 Isolate (Edge)           │
│                                          │
│  優勢:                                   │
│  ✓ 邊緣部署,全球低延遲                   │
│  ✓ 冷啟動 < 5ms                          │
│  ✓ 前後端同語言,型別安全                 │
│  ✓ 基礎設施即平台(KV + D1 + Vectorize) │
│  ✓ 免費額度慷慨                           │
│                                          │
│  限制:                                   │
│  ✗ ML 生態不如 Python                     │
│  ✗ 執行時間限制(30s / 15min paid)       │
│  ✗ 不能跑自定義模型                       │
└────────────────────────────────────────┘
// Cloudflare Workers + Hono 的完整 chatbot 入口
import { Hono } from 'hono';
import { streamSSE } from 'hono/streaming';

interface WorkerEnv {
  OPENAI_API_KEY: string;
  VECTORIZE: VectorizeIndex;
  KV: KVNamespace;
  DB: D1Database;
}

const app = new Hono<{ Bindings: WorkerEnv }>();

app.post('/api/chat', async (c) => {
  const { messages, userId, sessionId } = await c.req.json();

  // 1. Rate limit (用 KV 做計數)
  const rateKey = `rate:${userId}:${Math.floor(Date.now() / 60000)}`;
  const count = parseInt((await c.env.KV.get(rateKey)) ?? '0');
  if (count > 20) {
    return c.json({ error: 'Rate limit exceeded' }, 429);
  }
  await c.env.KV.put(rateKey, String(count + 1), { expirationTtl: 120 });

  // 2. Recall memories from Vectorize
  // 3. Build context
  // 4. Stream response
  return streamSSE(c, async (stream) => {
    // ... streaming 邏輯
  });
});

export default app;

Python / FastAPI

┌────────────────────────────────────────┐
│         Python / FastAPI Stack           │
│                                          │
│  Framework:  FastAPI                     │
│  AI SDK:     LangChain / LlamaIndex     │
│  Vector DB:  Qdrant / Weaviate / Chroma │
│  Database:   PostgreSQL + pgvector      │
│  Embedding:  sentence-transformers      │
│  Runtime:    uvicorn / gunicorn          │
│                                          │
│  優勢:                                   │
│  ✓ ML 生態最完整(PyTorch, transformers)│
│  ✓ 可以跑本地模型和 fine-tune            │
│  ✓ LangChain / LlamaIndex 框架成熟      │
│  ✓ 科學計算和資料處理庫最豐富            │
│  ✓ 人才庫大                               │
│                                          │
│  限制:                                   │
│  ✗ 部署比 serverless 複雜                 │
│  ✗ 冷啟動較慢                             │
│  ✗ 沒有內建的型別安全                     │
│  ✗ 併發模型(async)比 Node.js 晚成熟    │
└────────────────────────────────────────┘

比較表

面向TypeScript / CloudflarePython / FastAPI
延遲邊緣部署,全球 < 50ms需自行部署多區域
冷啟動< 5ms數百 ms ~ 數秒
ML 生態有限,依賴 API完整,可跑本地模型
Fine-tuning不支援原生支援
框架成熟度Hono + AI SDK(輕量快速)LangChain / LlamaIndex(功能全面)
型別安全TypeScript 原生支援需要 Pydantic + mypy
部署複雜度wrangler deploy 一鍵Docker + K8s 或 PaaS
成本模型按量計費,免費額度大需維護伺服器
向量搜尋Vectorize(整合平台)Qdrant/Weaviate(更多選擇)
即時通訊整合適合 webhook-based適合長連線場景

選型決策樹

你需要 fine-tune 模型嗎?
  ├── 是 → Python
  └── 否 →
      你需要跑本地模型嗎?
        ├── 是 → Python
        └── 否 →
            你的團隊主要語言是?
              ├── Python → Python / FastAPI
              ├── TypeScript → TS / Cloudflare Workers
              └── 都會 →
                  延遲敏感度?
                    ├── 極高(全球使用者) → TS / Cloudflare Workers
                    └── 一般 →
                        已有基礎設施嗎?
                          ├── AWS/GCP → Python(更多整合)
                          ├── Cloudflare → TypeScript
                          └── 沒有 → 看團隊偏好

混合架構

實務上很多團隊會用混合架構:

┌─────────────────────────────────────────────────┐
│                 混合架構範例                       │
│                                                   │
│  ┌──────────────────┐  ┌──────────────────────┐  │
│  │  Cloudflare       │  │  Python Backend       │  │
│  │  Workers (TS)     │  │  (FastAPI)            │  │
│  │                    │  │                       │  │
│  │  • API Gateway     │  │  • 複雜 RAG Pipeline  │  │
│  │  • SSE Streaming   │  │  • Embedding 生成     │  │
│  │  • Rate Limiting   │  │  • Fine-tuned 模型    │  │
│  │  • Static Assets   │  │  • 批次處理           │  │
│  │  • Session 管理    │  │  • 評估 Pipeline      │  │
│  │                    │  │                       │  │
│  └────────┬───────────┘  └───────────┬───────────┘  │
│           │              ▲            │              │
│           └──── HTTP ────┘            │              │
│                                       │              │
│  ┌────────────────────────────────────▼───────────┐  │
│  │               共用基礎設施                       │  │
│  │  PostgreSQL + pgvector  |  Redis  |  Langfuse  │  │
│  └────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────┘

TypeScript 負責使用者接觸的「邊緣層」——低延遲、streaming、session 管理。Python 負責「核心 AI 層」——RAG pipeline、embedding、模型推論。兩者用 HTTP 或 message queue 溝通。

選型原則總結

  1. 已有基礎設施 → 跟著走,不要為了用新技術而遷移
  2. 從零開始 → 看團隊技能,工程師熟悉什麼就用什麼
  3. 需要 fine-tuning → 必須 Python,這沒有例外
  4. 純 RAG 場景 → 兩者皆可,選部署最輕鬆的
  5. **全球低延遲 → Edge runtime(Cloudflare Workers)**有天然優勢

總結

一個生產級聊天機器人的架構全景:

                        使用者

                   ┌──────▼──────┐
                   │  Frontend    │  React / Next.js
                   │  SSE Client  │  EventSource / fetch
                   └──────┬──────┘

                   ┌──────▼──────┐
                   │  API Layer   │  Hono / FastAPI
                   │  Streaming   │  SSE / WebSocket
                   └──────┬──────┘

              ┌───────────▼───────────┐
              │    Guardrails Pipeline │
              │  Input → LLM → Output │
              └───────────┬───────────┘

         ┌────────────────▼────────────────┐
         │         State Management         │
         │  Session  │  User  │  Global     │
         └────────────────┬────────────────┘

         ┌────────────────▼────────────────┐
         │       Memory Layer               │
         │  Short-term  │  Long-term        │
         │  (context)   │  (vector store)   │
         └────────────────┬────────────────┘

         ┌────────────────▼────────────────┐
         │      Observability (Langfuse)    │
         │  Traces │ Metrics │ Alerts       │
         └─────────────────────────────────┘

每一層的設計決策都會影響最終的使用者體驗。不要試圖一次做完所有層——先把核心的 State Management + Streaming 做好,讓使用者能流暢對話,再逐步加上 Guardrails、Memory 和 Observability。

記住:使用者不在乎你用了什麼模型。他們在乎的是:回覆快不快、記不記得住、安不安全。 這三件事,都不是 API 呼叫能解決的。

參考資料