Architecture Overview
AI 個人助理的系統架構總覽,包含需求分析、模組拆分、請求生命週期,以及從原型到生產的演進路徑。
我們要設計什麼?
這個系列會一路用同一個例子來說明:一個以 email 檢索為核心的 AI Assistant。用戶可以用自然語言和它對話,它能:
- 回答問題,並記得過去聊過的事
- 搜尋用戶的 email,先找出相關信件,再視需要讀全文
- 從 email 中提取 deadline、會議時間或待辦事項
- 操作外部服務(行事曆、任務管理),但重要操作需要人類確認
- 即時串流回應,而非等幾秒後一次性輸出
聽起來簡單,但當你把這些能力拆開來看,背後是一個有相當複雜度的分散式系統。
這一系列與 RAG 系列怎麼搭配?
這裡會聚焦在「如何把 AI Assistant 做成一個完整系統」。如果你想深入理解檢索本身,像是 Chunking、BM25、Embeddings、RRF 與 Reranking,建議搭配閱讀 RAG Introduction 與後續子文章。
需求分析
功能性需求
| 能力 | 描述 |
|---|---|
| 對話 | 支持多輪對話,記住對話脈絡 |
| 搜尋 | 在非結構化資料(email)中找到精確答案 |
| 記憶 | 跨對話記住用戶偏好、過往資訊 |
| 工具操作 | 整合外部服務,能讀也能寫 |
| HITL 安全機制 | 寫入操作前取得用戶確認 |
非功能性需求
| 需求 | 目標 |
|---|---|
| 延遲 | 首個 token 在 1-2 秒內出現(串流) |
| 成本 | 控制每次對話的 token 消耗 |
| 可靠性 | 任一子系統失敗不應讓整個對話崩潰 |
| 擴展性 | 從單用戶原型可以演進到多用戶生產系統 |
| 安全性 | 敏感操作不能在無人確認下執行 |
模組拆分
一個 AI Assistant 系統可以拆成以下核心模組:
一次請求的生命週期
用戶發送「幫我找 John 上週寄來、提到合約截止日的 email」後,系統會經歷以下步驟:
Phase 1:前處理(~100ms)
- 訊息驗證:檢查訊息格式、確認最後一則是用戶訊息
- HITL 檢查:是否有待處理的用戶決定?有的話先處理
- 上下文組裝:
- 搜尋相關記憶(語意搜尋,取 top 3)
- 搜尋超出視窗的歷史訊息(語意搜尋,取 top 10)
- 搜尋過去的相關對話(語意搜尋,取 top 3)
Phase 2:HITL 前置判斷(~500ms,可選)
Approval Agent:用輕量 Agent 判斷此請求是否需要操作外部服務
- 如果需要 → 送出確認請求,結束此次請求,等待用戶回覆
- 如果不需要 → 繼續
Phase 3:執行(~2-10s)
- 執行已確認的操作:如果用戶在上一輪批准了某個工具,先執行它
- 主 Agent 循環:帶著完整上下文(記憶 + 歷史 + 工具),進入多步驟的思考-行動-觀察循環
- 串流回應:推理過程、工具呼叫、文字回應即時串流給前端
Phase 4:後處理(背景,不阻塞)
- 持久化:儲存用戶訊息和 AI 回應
- 記憶提取:從對話中提取值得記住的資訊
- 對話反思:生成對話摘要、學習筆記
關鍵設計決策
1. 為什麼要拆出 Approval Agent?
直覺的做法是讓主 Agent 直接呼叫工具,呼叫到需要 HITL 的就暫停。問題是:主 Agent 可能先做了好幾步不需要 HITL 的操作(搜尋 email、分析內容),然後才決定要操作行事曆。這時候暫停,前面的工作就浪費了。
更好的做法:在主 Agent 啟動前,用一個輕量的 Approval Agent 快速判斷「這個請求有沒有可能需要外部操作」。如果需要,立刻送出確認請求。用戶批准後,再讓主 Agent 帶著「已批准」的資訊開始工作。
2. 為什麼後處理要異步且容錯?
記憶提取和對話反思都是「錦上添花」的功能,失敗了也不應該讓用戶看到錯誤。用 Promise.allSettled 而非 Promise.all,確保任一後處理失敗不影響其他。
3. 為什麼只傳最新一則訊息給 API?
傳統做法是前端傳整個訊息歷史。但對話越長,payload 越大。既然伺服器已經持久化了所有歷史訊息,前端只需要傳最新的一則。伺服器自己從 DB 讀取歷史,搭配語意搜尋只取相關的部分。
這個設計同時解決了兩個問題:
- 減少網路傳輸
- 控制送進 LLM 的 token 數量
4. 為什麼上下文要分四層?
| 層級 | 來源 | 數量 | 作用 |
|---|---|---|---|
| 記憶 | 跨對話提取 | ≤3 | 用戶偏好、背景知識 |
| 關聯對話 | 過去的對話 | ≤3 | 類似問題的經驗 |
| 歷史訊息 | 同一對話的舊訊息 | ≤10 | 被視窗截斷的上下文 |
| 最近訊息 | 同一對話 | ≤10 | 當前對話脈絡 |
每一層用語意搜尋篩選最相關的,而非全部塞進去。這在保持回答品質的同時,嚴格控制了 token 預算。
技術選型
| 層級 | 選擇 | 原因 |
|---|---|---|
| 前端框架 | Next.js (App Router) | SSR + API Route + Server Actions 一站式 |
| AI SDK | Vercel AI SDK | 統一的 LLM 介面、串流支持、工具定義 |
| LLM | Gemini 2.5 Flash | 速度快、成本低、支持結構化輸出 |
| Embedding | Gemini Embedding | 與 LLM 同一提供者,簡化管理 |
| 外部整合 | MCP over HTTP | 標準協議、動態發現工具 |
| 持久化 | JSON 檔案 → SQL DB | 原型用檔案,生產用資料庫 |
模型無關設計
模型選擇取決於你的需求。本系統的設計是模型無關的,透過 Vercel AI SDK 的抽象層,切換模型只需要改一行配置。選擇 Gemini 是因為它在速度和成本上對個人助理場景有優勢,但架構本身不綁定任何特定模型。
從原型到生產的演進路徑
- 單用戶,本地運行
- JSON 檔案存儲
- 所有搜尋在記憶體中完成
- 沒有認證、沒有多租戶
- 加入認證(NextAuth / Clerk)
- JSON → SQLite / PostgreSQL
- 記憶體搜尋 → 向量資料庫(Pinecone / pgvector)
- 加入速率限制
- 部署到 Vercel / Railway
- 多租戶隔離
- Embedding 預計算 pipeline(寫入向量索引,不在請求路徑上)
- 訊息佇列處理後處理任務(記憶提取、反思)
- 可觀測性(OpenTelemetry / Langfuse)
- A/B 測試不同的 system prompt
- Evaluation pipeline 持續評估回答品質
接下來
這篇文章給了你系統的全貌。接下來的每一篇會深入一個模組:
Streaming Protocol:前後端如何即時通訊Retrieval Pipeline:如何從大量 email 中找到答案Memory Architecture:如何讓 AI 跨對話記住用戶Agent Orchestration:如何協調多步驟的工具使用Human-in-the-Loop:如何在 AI 自主性和安全性之間取得平衡
每篇都可以獨立閱讀,但建議按順序,因為後面的設計會建立在前面的基礎上。如果你在閱讀過程中發現自己卡在檢索原理,可以回跳到 RAG 系列補強底層觀念。
Last updated on