技術分享 · 內部教材
5 章 × 30 頁
兩門課程 + 四段技術深化
Agent 智能體
接口設計 · 公網部署 · 生產優化
從產品形態判斷到容器化演進 —— 一條問題驅動的完整技術棧。
開場先給結論:這份簡報不是 FastAPI 教學,而是一條「從產品形態推導到生產維運」的判斷路徑。整份 deck 刻意採問題驅動節奏——先看故障現象,再推方案。
總覽 / Overview
五章結構與敘事邏輯
產品形態判斷→
業務接口設計→
HTTP 協議實作→
公網部署→
生產維運→
容器化演進
| 章節 | 主題 | 頁數 | 核心來源 |
| 第一章 | Agent 類型判斷與業務接口設計方法論 | P1–P6 | 接口設計篇 第 1 章 |
| 第二章 | FastAPI 技術實作核心 | P7–P12 | 接口設計篇 第 2–4 章 |
| 第三章 | 從本機到公網:VPS 部署實戰 | P13–P18 | 部署上線篇 第 1–4 章 |
| 第四章 | 生產級維運與進階優化 | P19–P24 | 部署上線篇 第 5–6 章 + SSE/Celery 深化 |
| 第五章 | 容器化演進與未來擴展策略 | P25–P30 | Docker 容器化 + limit_req 演算法深化 |
這條敘事線刻意不從術語開場,而先讓聽眾看到具體畫面或故障現象(HTTP 明文可讀、SSE 斷線遺失事件),再推導方案。目的是讓產品經理與技術主管也能跟上邏輯脈絡。
P1 – P6
Agent 類型判斷與
業務接口設計方法論
- 9 類主流 Agent 的產品形態與業務出口
- 六維度裝配卡:從業務出口推到 HTTP 形態
- ai-todo 案例驗證與五步公式
第一章是全份簡報的方法論骨架。後面四章都是這套骨架的實作與延伸。
第一章 · 方法論
為什麼接口設計要從「產品形態」談起
- Agent 不是文本分析腳本的簡單升級 —— 它對外暴露的出口不止一種
- 對話型 Agent 至少要有「接收訊息 + 流式回覆 + 查看歷史」三個出口
- 本章方法論:先看產品畫面,再拆業務出口,最後才進協議層
反例 / ANTI-PATTERN
先寫協議、後補產品邏輯
漏掉關鍵出口,或把不該同居的能力硬塞進一個接口 —— 這是許多團隊接口反覆重構的根源。
順序即成本。「想」的成本投入越多,「寫」與「改」的成本就越低。
把文本分析腳本包成 HTTP 服務只需一句話;Agent 完全不同。工具調用型還要讓前端即時看到「現在在調什麼工具」;檔案處理型要有上傳、非同步解析、取結果三個出口。動手前沒想清楚產品形態,就會漏出口。
第一章 · 類型盤點非互斥分類,而是疊加關係
9 類主流 Agent 類型總覽
① 對話助手ChatGPT / Claude
接收訊息、餵 LLM、流式回覆
② 工具調用型Perplexity
新增「事件流」,把過程產品化
③ 任務執行型Manus / Operator
任務獨立於會話,有自己生命週期
④ 工作流型Dify / Coze / n8n
定義 + 執行兩個物件,節點級狀態可見
⑤ 資料分析型ADA / Julius AI
引入沙箱隔離代碼執行
⑥ 檔案/文件理解Kimi / ChatPDF
上傳、解析、進度、結果、下載全占滿
⑦ RAG 知識庫型NotebookLM / Glean
回覆必須帶引用來源、可追溯
⑧ 代碼 AgentCursor / Claude Code
新增雙向確認機制
⑨ 多智能體協作LangGraph / CrewAI
訊息新增發送者/接收者維度
關鍵不在背誦分類,而在理解疊加關係:RAG 型本質是「對話助手 + 文件管理」,代碼 Agent 是「對話 + 任務執行」。判斷類型的真正目的是決定接口骨架的必需項與可延後項。
第一章 · 繼承鏈
類型疊加邏輯與拓展出口
基礎包 / BASE
對話助手 = 所有類型的地基
聊天流式
會話歷史會話管理
拿到新專案時不必從零發想 —— 先判斷「屬於哪幾類的組合」,基礎接口清單自動浮現,再補差異化出口。
拓展出口 —— 獨立成一張附加檢查表
- 用戶與權限管理
- 資源歸屬與可見性(會話/檔案/知識庫屬於誰)
- 工具與能力開關(自動執行 vs 需授權)
- 危險動作二次確認(刪檔、發信等副作用)
- 產物列表與清理
- 運行中斷與人工接管
- 審計日誌
拓展出口不屬於任何單一類型,是各類產品上線後都會需要的增量能力。很多團隊在 MVP 階段完全忽略,直到上線才發現缺權限控管或審計能力。
第一章 · 核心工具全書最可複用的判斷工具
六維度裝配卡 —— 決定 HTTP 形態
① 入參複雜度簡單欄位走 Path/Query;嵌套物件走 Body
② 是否改變狀態只讀 GET;改動依語意選 POST/PATCH/PUT/DELETE
③ 要不要邊算邊返一次返回用 JSON;邊生成邊返用 StreamingResponse
④ 純文本流 or 多事件打字機用 text/plain;需區分事件類型用 SSE
⑤ 是否涉及檔案上傳 UploadFile;下載 FileResponse
⑥ 是不是長任務短任務同步;長任務拆「創建 → 查狀態 → 取結果」三段式
這六個維度把「該用哪個 HTTP 方法」從憑經驗判斷,轉成可重複套用、可教新人的決策樹。建議直接轉成團隊 checklist,新接口上線前逐條核對。第四題只在需要即時回應時才問。
第一章 · 速查表
十一類業務接口的 HTTP 形態速查
| 業務接口 | HTTP 形態 | 適用場景 |
| 會話歷史/任務狀態 | GET + Path/Query | 拿 ID 反查狀態,最簡形態 |
| CRUD(待辦/文件管理) | GET/POST/PATCH/PUT/DELETE | 資源式路由,RESTful 風格 |
| 聊天接口(非流式) | POST + Body | 一發一收,適合短回覆 |
| 聊天流式接口 | POST + StreamingResponse(text/plain) | 打字機效果 |
| 工具結果接口 | GET + JSONResponse | 事後拉取結構化結果 |
| 工具調用過程/日誌流 | StreamingResponse(text/event-stream) | SSE 多事件,即時展示過程 |
| 檔案上傳 | POST + UploadFile + multipart/form-data | 接收使用者上傳檔案 |
| 檔案解析(長任務特化) | POST + BackgroundTasks 三段式 | 上傳後的非同步重活 |
| 報告/導出下載 | GET + FileResponse | 觸發瀏覽器下載 |
| 長任務接口(通用三段式) | POST + GET status + GET result | 任何提交→異步→拿結果 |
| 雙向實時接口 | WebSocket | 僅用於真正需要雙向的場景 |
最重要的洞察是:大多數 Agent 接口依然是 GET/POST/PATCH/DELETE + JSON 或檔案回應。絕大多數 Agent 用不到 WebSocket,只有真正需要雙向即時(語音、多人協作)才需要。
第一章 · 案例驗證
ai-todo 案例驗證與五步方法論
類型判斷→
業務接口清單→
HTTP 形態翻譯→
FastAPI 組件實作→
反向驗證
9
條業務接口
對話 2 + 會話 3 + CRUD 4
跳步的代價:聊天接口若跳過形態判斷直接用 GET + Query,需求變成「傳整段對話歷史」時 URL 被反向代理截斷 —— 全部路由要翻成 POST + Body,前端、API 文件、SDK 一起重寫。
ai-todo 是「對話助手 + 工具調用」的混合體。反向驗證步驟的價值常被低估:逐一用六維度反推「為什麼不用 WebSocket/檔案上傳/三段式」,是避免過度工程的關鍵習慣。
P7 – P12
FastAPI
技術實作核心
- CRUD、Response 矩陣與 SSE 事件協議
- 檔案上傳下載、長任務三段式、WebSocket 選型
- 鑑權/CORS/配置/錯誤處理四大輔助組件
第二章把第一章推導出的形態,逐一落到 FastAPI 的具體組件上。
第二章 · 實作
CRUD 與 Pydantic Schema 設計
同一資源路徑 + 不同動詞 = 不同操作
GET 讀取POST 創建
PATCH 部分改PUT 整體替換DELETE 刪除
實務上 PATCH 使用頻率遠高於 PUT —— 整體替換場景很少。
建立 Schema:Field(..., min_length=1)
更新 Schema:每個欄位 Optional
body.model_dump(exclude_unset=True)
常見錯誤
建立與更新共用同一個 Schema
PATCH 時未傳欄位會被覆蓋成 None —— 實務上極容易踩的坑。
免費紅利
校驗代價從執行期挪到聲明期
用 dict 接請求體,畸形資料要等跑到深層才崩、回 500 對前端毫無定位價值;嵌套 Schema 能精確報到 messages[0].role 欄位層級。
exclude_unset=True 是實現「只改傳入欄位」語意的關鍵——它只打包前端實際傳入的欄位。嵌套 Schema 讓 FastAPI 遞迴校驗每一層,錯誤訊息能定位到欄位層級。
第二章 · 回應形態實際專案通常只需要 3 類
聊天流式接口與 Response 矩陣
JSONResponse結構化資料,絕大多數接口的預設形態
StreamingResponse流式生成資料/SSE
PlainTextResponse健康檢查等純文字
三種常見誤用
① JSON 回檔案 —— base64 膨脹 4/3 且不觸發下載 ② JSON 回打字機流 —— 等數秒才整段「啪」地刷出 ③ text/plain 回工具調用過程 —— 前端分不出思考與結果
接入真實 Agent 框架時,agent.astream() 吐出的是 AIMessageChunk 物件而非裸字串,需用 .content 取出文字。
第二章 · 工程含金量最高的一頁
SSE 事件協議:三件套與翻譯層
event: tool_call
data: {"name": "add_todo", "args": {…}}
(空行 —— 告知前端這條事件結束)
普通流式只能推純文字,前端拿到什麼就貼什麼;SSE 能推帶名字的多種事件,前端依類型渲染到不同 UI 區塊。
入口分兩檔
簡單輸入走 GET + Query(原生 EventSource);嵌套輸入走 POST + Body(建議 @microsoft/fetch-event-source)
核心設計 / TRANSLATION LAYER
不要把框架事件名透傳給前端
on_chat_model_stream 直送前端=把前端跟 LangChain 內部命名鎖死,框架升級改名,前端就得跟著改。
寫一層 translate(),把幾十種框架事件映射成 7–8 種穩定業務事件:
run_startedmessage_delta
tool_calltool_result
requires_actionrun_completedrun_failed
前端只對接這組業務事件名,框架升級完全不影響前端。灰色文字渲染思考態、卡片渲染工具結果、聊天氣泡渲染最終回覆。
第二章 · 檔案與長任務
檔案上傳/下載與長任務三段式
STEP 1
POST /tasks
立即回傳 task_id 與 queued,重活丟背景
STEP 2
GET /tasks/{id}/status
前端輪詢進度
STEP 3
GET /tasks/{id}/result
完成後取最終產物
UploadFile vs 裸 bytes
UploadFile 內部是 SpooledTemporaryFile(小檔進記憶體、大檔落磁碟)。裸 bytes = File(...) 一次全讀進記憶體 —— 50MB 沒事,500MB 吃爆。
為什麼不能同步硬跑十分鐘
瀏覽器/反向代理/客戶端通常都有 30–60 秒逾時,請求掛在那裡必然 504。下載則靠 Content-Disposition: attachment 才會彈下載視窗。
另常搭配 DELETE /tasks/{id} 取消與 POST /tasks/{id}/retry 重試兩個輔助端點。同樣回傳 JSON,用 JSONResponse 瀏覽器只顯示文字,用 FileResponse(filename=…) 才觸發下載。
第二章 · 選型判斷準則只有一件事
WebSocket 雙向通道與 SSE/WS 選型
客戶端要不要在「不發新 HTTP 請求」的前提下,主動推資料給伺服器?
要 → WebSocket · 不要(只是服務端推給客戶端)→ SSE
SSE 做不到的事
語音 Agent「使用者中途說話打斷」在 SSE 上做不到 —— 客戶端根本沒有反向推送能力。而 HTTP 請求-回應每次都要新建連接,音訊這種二進位高頻小包受不了。
真正用得上 WS 的三類場景
實時語音(Realtime API)· 多人協作編輯(Figma/Notion 游標)· 實時遊戲。
絕大多數 Agent 是單向推送(進度、工具事件、token 流),SSE 就足夠。
WebSocket 是基於 HTTP 升級握手的全雙工長連接,支援二進位幀與低延遲。先判斷產品是否真的存在「雙向即時」需求,再談要不要上。
第二章 · 橫切關注點
鑑權/CORS/配置/錯誤處理
① 鑑權 Depends同時支援 Header 與 Query —— 原生 EventSource 不能帶自訂 Header,SSE 鑑權必須走 Query 兜底
② CORSMiddlewareport 不同就算跨域。EventSource 規則更嚴:不能加自訂標頭、不能帶 cookie
③ 配置管理必填欄位用 os.environ["KEY"],缺欄位當場 KeyError 讓部署腳本攔下
④ HTTPExceptionstatus_code + detail,FastAPI 自動生成標準 JSON 錯誤回應
鑑權三種掛法(依粒度)
單一路由級 · 路由組級 APIRouter(dependencies=…) · 全域級 FastAPI(dependencies=…)
生產環境保留一個公開端點
/health 不掛鑑權依賴,供外部監控探測。
四件組件的共通價值是把橫切關注點從業務邏輯抽離。鑑權若散落在每個 endpoint 重複寫,改一處要改十幾處,且嚴重降低可讀性。
P13 – P18
從本機到公網:
VPS 部署實戰
- 基線環境、SSH 公鑰加固與金鑰演算法選擇
- uvicorn → systemd → nginx 反向代理三層
- 域名解析與 HTTPS 憑證自動續期
第三章的推導起點:本機能跑 ≠ 公網能連。每一層都對應一個具體的失敗現象。
第三章 · 部署第一步
VPS 基線環境與 SSH 安全加固
第一件事不是上傳代碼
基線裝包
Python 3.12python3.12-venv
gitcurlsnapd → certbot
公鑰認證取代密碼登入
私鑰留本機、公鑰貼到 authorized_keys,服務器驗證簽章即放行 —— 沒有可爆破的密碼。
選點策略
港區 VPS + 國際域名可規避 ICP 備案,加快上線節奏。
| 金鑰演算法 | 結論 |
| ed25519 | 推薦。約 400 位元組、簽名快、強度等效 RSA-3072;OpenSSH 6.5+ 全面支援 |
| rsa -b 4096 | 古董系統相容兜底 |
| ecdsa | NIST 曲線參數透明度疑慮,不推薦 |
| dsa | OpenSSH 7.0 起預設禁用 |
常見錯誤
ssh-keygen 問 Overwrite (y/n)? 誤選 y —— 舊私鑰直接被覆蓋,所有已授權主機全部失聯。
ed25519 是現代 SSH 的標準做法,主流雲廠商與代碼託管平台全面覆蓋。覆蓋私鑰前務必確認路徑,或用 -f 指定新檔名。
第三章 · 第一個失敗現象
從 uvicorn 跑起來,到公網連得上
服務器上 curl localhost 有回應,但瀏覽器連不上 —— 三個地方擋著
① 綁定位址預設 127.0.0.1 只收本機封包。公網服務要 --host 0.0.0.0,讓所有網卡都收。
② 雲廠商安全組主機防火牆之外還有一層雲端安全組。80/443 沒開,封包連機器都到不了。
③ 主機防火牆ufw 只放行必要 port(22/80/443),應用 port 不對外裸露。
依賴隔離用 venv,不要裝進系統 Python —— 系統套件與應用套件混在一起,升級一個就拖垮另一個。應用 port 只綁在 127.0.0.1,對外一律由 nginx 代理,是下一頁之後的標準姿勢。
這頁的教學價值在於:同一個「連不上」現象有三個完全不同的成因,排查順序應該是由外而內——先確認安全組,再確認防火牆,最後看綁定位址。
第三章 · 第二個失敗現象
systemd 常駐化:告別 nohup
nohup 的三個問題
① 進程崩了不會自己起來 ② 服務器重開機服務就沒了 ③ 日誌散落在隨手指定的檔案,沒有輪替
systemd 換來什麼
崩潰自動重啟、開機自啟、日誌統一進 journalctl、狀態一條 systemctl status 看完。
Unit 檔的六個關鍵欄位
| User | 用非 root 的專用帳號跑,縮小爆破面 |
| WorkingDirectory | 相對路徑的基準點 |
| EnvironmentFile | 指向 .env,權限設 600 |
| ExecStart | venv 內 uvicorn 的絕對路徑 |
| Restart | always/on-failure |
| WantedBy | multi-user.target 才會開機自啟 |
最常見的踩坑是 ExecStart 寫相對路徑或直接寫 uvicorn,systemd 找不到執行檔。改 Unit 檔後要 daemon-reload 才生效。
第三章 · 為什麼要多一層
nginx 反向代理與 SSE buffering
location /api/chat/stream {
proxy_pass http://127.0.0.1:8000;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_set_header Connection '';
}
最容易被誤判為「後端壞了」的一頁
忘記關 buffering 的症狀
SSE/流式效果完全消失,前端等數十秒後一次性收到全部內容。後端與前端代碼都沒錯,錯在中間這一層。
長連接同時需要放寬 proxy_read_timeout,否則串到一半被代理層掐斷。
這是第二章 P9 提到的「常見錯誤」的具體解法。Connection 標頭清空是為了避免 HTTP/1.0 的 keep-alive 干擾長連接。
第三章 · 對外門牌
域名解析與 nginx 虛擬主機
STEP 1
A 記錄指向 VPS
把域名(或子域)解析到公網 IP
STEP 2
server_name 對上
nginx 依 Host 標頭選 server 區塊
STEP 3
驗證解析生效
dig/nslookup 確認,再申請憑證
為什麼要先有域名
憑證簽發需要一個可驗證的域名,不能簽給裸 IP。域名解析生效是 HTTPS 的前置條件,順序不能顛倒。
子域分流
同一台機用 api.example.com/app.example.com 分開 server 區塊,比用路徑前綴更好維護,也讓後續拆機更容易。
配合 P13 的港區選點策略,國際域名 + 港區 VPS 可規避 ICP 備案。憑證申請前務必先 dig 確認解析已生效,否則 certbot 的 HTTP-01 驗證會直接失敗。
第三章 · 收尾HTTP 明文可讀 = API Key 裸奔
HTTPS 憑證與自動續期
走 HTTP,中間任何一跳都能明文讀到 Authorization 標頭與整段對話內容
Agent 服務傳的是使用者的完整輸入與 LLM 回覆 —— 這是最不該裸奔的一類流量。
簽發certbot --nginx 自動改寫 server 區塊、掛上憑證、加 80 → 443 跳轉
有效期Let's Encrypt 憑證 90 天。手動續期必然有一天忘記,然後全站告警
續期snap 版 certbot 自帶 timer;用 certbot renew --dry-run 驗證流程真的能跑
憑證換新後 nginx 需要 reload 才會載入 —— 續期 hook 一定要帶上 reload,否則憑證更新了但線上還在用舊的。
這一頁是第三章的收束:從裸 VPS 到 HTTPS 域名服務的六步全部走完。到期前一週的告警也建議一起設上。
P19 – P24
生產級維運與
進階優化
- 可觀測性、進程模型與 SSE 的併發成本
- SSE 進階:心跳、事件 id、斷線補發
- Celery 長任務架構:BackgroundTasks 的天花板
第四章從「跑得起來」轉到「撐得住」。每一頁都對應一個上線後才會出現的故障現象。
第四章 · 維運底線
可觀測性三件套:探針、日誌、指標
① 健康探針/health 不掛鑑權、不碰 LLM、只確認進程活著與依賴可連。掛了鑑權,監控就永遠回 401
② 應用日誌systemd 統一收進 journalctl -u。請求帶 request_id,才能把一次對話的多筆日誌串起來
③ 代理層日誌nginx access log 是唯一能區分「請求沒進來」與「進來後失敗」的證據
Agent 服務要多記三件事
模型與版本、token 用量、工具調用結果。少了這三項,成本異常與行為退化都查不出原因。
日誌絕不能記的東西
API Key、完整 Authorization 標頭、使用者個資原文。日誌通常保留最久、權限最寬。
request_id 建議由 nginx 產生並透傳給應用,這樣代理層與應用層的日誌能對齊。token 用量記錄是後續成本歸因的唯一依據。
第四章 · 併發模型SSE 是併發規劃的最大變數
進程模型與長連接的併發成本
Agent 服務的併發瓶頸不在 CPU,而在「同時掛著的長連接數」
一次對話的 SSE 可能掛 30 秒到數分鐘。只要程式碼裡有一個同步阻塞調用,這條連接就霸佔整個事件迴圈。
async 全鏈路路由是 async def,內部卻用同步 HTTP 客戶端 —— 等於把 async 的好處全部作廢
阻塞調用的出路無法改 async 的同步庫,丟到執行緒池(run_in_threadpool),別讓它擋事件迴圈
多 worker用多進程吃滿多核;但有記憶體狀態就不能靠進程記憶體共享,得外移到 Redis
worker 數不是越多越好 —— 每個 worker 都是一份完整的 Python 進程與模型客戶端。先量測單 worker 能撐幾條並行 SSE,再決定要開幾個。
這頁的核心判斷:Agent 服務的資源模型跟一般 CRUD API 完全不同——它是 IO 密集 + 長連接密集。任何同步阻塞都會被長連接放大。
第四章 · SSE 進階(一)
心跳、事件 id 與斷線重連
故障現象
Agent 思考 40 秒沒吐任何 token,連接就被中間層當成閒置而斷開 —— 使用者看到「無回應」。
: heartbeat ← 註解行,每 15 秒一發
id: 128
event: message_delta
data: {"text": "…"}
retry: 3000 ← 建議重連間隔
三個必備欄位
註解心跳讓連接保持活躍;遞增 id 讓前端知道自己收到哪裡;retry 由服務端決定重連節奏,而非讓前端自己猜。
Last-Event-ID
原生 EventSource 重連時會自動帶上最後收到的 id。後端必須讀這個標頭,否則自動重連只會從頭再來一次或直接漏掉中間事件。
心跳間隔要小於鏈路上最短的那個 idle timeout —— nginx、雲負載平衡器、客戶端各有一個。
用 fetch + ReadableStream 手寫解析時,Last-Event-ID 與 retry 都要自己實作,這是建議用成熟函式庫的主要理由。
第四章 · SSE 進階(二)重連只是第一半
事件持久化與斷線補發
連上了,但斷線那 8 秒的 tool_result 永久遺失 —— 前端卡在「正在調用工具」再也不動
因為事件是「產生即推送」,沒推出去的就沒了。Agent 那次工具調用不會為了你重跑一次。
① 事件先落地translate() 產生的業務事件先寫進以 run_id 為鍵的序列(Redis Stream/List),再推給連接
② 重連即補發依 Last-Event-ID 取出缺口區間 replay,補完再接上實時流
③ 設定過期run 事件序列給 TTL(如數小時),別讓它無限長大成第二個資料庫
額外紅利:事件一旦落地,「重新打開頁面看完整過程」與「多裝置同時觀看同一次 run」就都免費拿到了 —— 這兩個需求幾乎一定會被提出來。
判斷準則:如果某類事件掉了會讓前端進入死狀態(tool_result、run_completed),就必須持久化;純 token delta 掉幾個字可以接受。
第四章 · 長任務架構
BackgroundTasks 的四個天花板
① 同進程重活跟 API 搶同一份 CPU 與記憶體,一個大解析拖慢所有對話
② 重啟即丟部署或崩潰,正在跑的任務直接消失,且沒人知道它消失了
③ 無重試失敗就是失敗,沒有退避重試,也沒有死信可查
④ 無法跨機任務綁在收到請求的那台機器上,加機器也分不走負載
Celery + Redis:把任務從「請求的副作用」變成「有身份的物件」
API 只負責入列並回傳 task_id;worker 是獨立進程、可獨立擴容;broker 持久化佇列,重啟不丟;結果與狀態回寫 backend,供 P10 的三段式接口查詢。
什麼時候還是該用 BackgroundTasks?寄一封通知信、寫一筆審計日誌 —— 秒級、可丟、不需回報。判準是:任務失敗了使用者需不需要知道。
這頁刻意不說「BackgroundTasks 不好」,而是給出邊界。過早引入 Celery 也是過度工程——多兩個要維運的組件。
第四章 · 落地細節
Celery 實作四個要點
① 任務狀態機
對外只暴露自己定義的狀態(queued / running / done / failed / cancelled),不要把 Celery 內部狀態直接回給前端 —— 那是換 broker 就會破的契約。進度另用一個欄位回寫。
② 重試與冪等
預設語意是至少一次,任務有可能被執行兩遍。凡有副作用(寫檔、發信、扣費)都要靠 task_id 去重。重試用指數退避,別把下游打死。
③ 佇列隔離
把「30 秒的解析」與「10 分鐘的批處理」分到不同佇列與 worker。共用一個佇列時,長任務會把短任務全部排在後面。
④ 結果過期與產物歸屬
result backend 要設 TTL;產出的檔案要記錄歸屬與清理策略 —— 這正好接回 P3 的「產物列表與清理」拓展出口。
Agent 任務有一個特別之處:失敗前可能已經花掉了 token。無腦重試三次=成本三倍,所以重試前要先判斷是可重試錯誤(逾時、429)還是不可重試(參數錯、內容政策拒絕)。
冪等是最常被跳過也最貴的一項。task_id 去重表是最簡單的實作,配合唯一索引即可。
P25 – P30
容器化演進與
未來擴展策略
- Dockerfile 多階段建置與映像瘦身
- compose 四服務編排
- nginx limit_req 漏桶演算法與擴展路線圖
第五章回答「下一步該往哪走」,並用 limit_req 這個具體演算法收束「別在不理解機制時抄配置」的態度。
第五章 · 動機
為什麼要容器化:三類環境漂移
① 執行期漂移本機 Python 3.12、服務器 3.10;本機 macOS、服務器 Ubuntu。同一份 requirements 裝出不同結果
② 系統依賴漂移文件解析要的 native 庫(字型、影像、PDF 工具鏈)不在 requirements 裡,換機就少一個
③ 流程漂移部署步驟寫在某個人的筆記本裡,第二個人照著做結果不一樣
容器把「環境」從口述知識變成版本控管的檔案
Dockerfile 就是可執行的部署文件 —— 第三章那六步從「照著做」變成「build 一次,到處跑同一個映像」。
但它不是免費的。你多了映像倉庫、build 流程與容器內除錯這三件要學的事。單人單機、依賴簡單的專案,systemd 那套完全夠用 —— 別為了時髦付這筆學費。
Agent 服務特別容易撞上第二類漂移,因為文件理解與資料分析類功能經常依賴系統層的 native 工具鏈。
第五章 · 建置小 ≠ 好
Dockerfile:層序、多階段與瘦身
python:3.12-slim
~0.13 GB
python:3.12-alpine
~0.05 GB
官方 tag 概略量級,隨版本浮動。alpine 最小卻常是錯的選擇 —— musl libc 讓多數 wheel 無法直接安裝,得從源碼重編,build 時間與體積雙輸。slim 是預設答案。
① 層序決定 build 速度
先 COPY requirements.txt 再裝依賴,最後才 COPY .。反過來寫的話,改一行代碼就要重裝全部依賴。
② 多階段建置
build 階段裝編譯工具鏈,runtime 階段只複製裝好的產物 —— 編譯器不進最終映像。
③ 非 root 執行
建一個專用 user 再 USER 切過去,跟 P15 的 systemd User 欄位同一個道理。
.dockerignore 要排掉 .env、.git、venv —— 否則密鑰被烤進映像層,刪不掉。
映像層是疊加且不可變的:某一層 COPY 了 .env,後面就算 rm 掉,那一層裡的檔案依然可被取出。
第五章 · 編排
compose 四服務編排
nginx唯一對外暴露 port 的服務:TLS、靜態資源、限流
apiFastAPI + uvicorn,只在內部網路開 8000
workerCelery worker,與 api 共用同一個映像、只換啟動命令
redisbroker + result backend + SSE 事件序列,掛 volume 保存
compose 幫你解決的三件事
服務名即 DNS(redis://redis:6379,不用管 IP)· depends_on 定啟動順序 · restart: unless-stopped 接手 systemd 的角色
兩個一定要顯式處理的點
healthcheck:depends_on 只保證「啟動了」,不保證「可用了」,Redis 沒 ready 時 worker 會啟動失敗。只有 nginx 對外映射 port,其餘服務留在內部網路。
單機部署到這裡就完整了:一份 compose 檔描述整個系統,新環境 up -d 就能起。日誌則交給容器 runtime 收集。
第五章 · 限流深化(一)抄配置最容易出事的一個指令
limit_req 的本質是漏桶
rate=10r/s 不是「每秒放 10 個」,而是「每 100ms 放 1 個」
漏桶以固定速率出水。同一秒內湧進 10 個請求,第 1 個過,其餘 9 個因為「還沒到下一滴的時間」全被拒 —— 這就是照抄配置後「明明沒超過 10 就被擋」的真相。
為什麼 Agent 服務特別需要限流
每個請求背後是真金白銀的 token 成本與可能被打爆的上游 API 配額。這裡的限流不只是防 DDoS,是防成本失控。
key 選什麼
按 IP($binary_remote_addr)擋不了同 NAT 出口的多用戶,也擋不了換 IP。按 API Key/用戶 ID 限流才對得上計費主體。
流式端點還有一個陷阱:limit_req 限的是請求數,不是連接時長。一個掛著 10 分鐘的 SSE 只算一個請求 —— 長連接的數量要另外用 limit_conn 控。
漏桶模型是理解下一頁 burst 與 nodelay 的前提。桶的容量就是 burst,出水速率就是 rate。
第五章 · 限流深化(二)
burst 與 nodelay 的三種組合
| 組合 | 行為 | 適用場景 |
| rate only | 超出速率立刻 503/429,零容忍 | 幾乎不適用真實流量 —— 正常使用者都會被誤擋 |
| rate + burst | 多餘請求排隊等待,桶滿才拒 | 可接受延遲的批次或後台調用 |
| rate + burst + nodelay | 桶內請求立刻放行,同時佔用桶位並按 rate 慢慢釋放 | 互動式 API 的正解:突發不排隊,持續超量才擋 |
為什麼互動式要 nodelay
沒有 nodelay 時,burst 內的請求是被刻意延遲到符合 rate 才處理。使用者感受到的不是「被限流」,而是「這個網站很慢」—— 比直接回 429 更糟。
回什麼狀態碼
nginx 預設回 503,語意是「服務不可用」,會讓客戶端與監控誤判成故障。用 limit_req_status 429 改成「請求過多」,並在回應帶上 Retry-After,客戶端才知道該退避多久。
實務建議:先按真實流量分佈估 burst(覆蓋一般使用者的自然突發),再配 nodelay,最後把 429 與 Retry-After 一起補上。
第五章 · 收束升級由訊號驅動,不由時髦驅動
擴展路線圖與選型收束
STAGE 1
單機 systemd
一台 VPS、nginx + uvicorn。單人專案的終點站,不是恥辱
STAGE 2
單機 compose
api / worker / redis / nginx 四服務固化成檔案
STAGE 3
多機 + 編排
無狀態 api 水平擴、worker 按佇列獨立擴
升到 STAGE 2 的訊號出現第二個部署環境、有 native 系統依賴、或需要獨立的 worker 進程
升到 STAGE 3 的訊號單機資源已量測到上限、需要零停機部署、或有多團隊共用平台
升級的前置條件先把狀態外移:會話、SSE 事件序列、任務狀態、上傳產物都不能放在單機本地磁碟或進程記憶體
整份簡報的同一個判準,在三個層面重複出現:接口別上 WebSocket、長任務別上 Celery、部署別上 K8s —— 除非你能說出它解決了哪個你已經量測到的問題。
呼應 P6 的反向驗證:架構選型與接口選型是同一種判斷力的不同尺度。狀態外移是所有水平擴展的前置條件,做不到就談不上 STAGE 3。
收束 / TAKEAWAY
下週就能動手的五件事
- 先畫產品形態,再寫接口 —— 把六維度裝配卡變成上線前的 checklist
- 抽一層 translate() —— 把 LangChain 事件名鎖在後端,前端只認 7 種業務事件
- 把長任務搬離 BackgroundTasks —— 補上重試、冪等與可查詢的狀態機
- 為 SSE 補心跳、事件 id 與 replay —— 讓斷線不再遺失 tool_result
- 用 compose 固化四服務 —— 再拿量測數字決定要不要上多機編排
整條技術棧只有一個貫穿的判準:每一層技術,都要能說出它解決了哪個你已經量測到的問題。
收束用五個動詞開頭的提案,讓聽眾帶走可執行的下一步,而不只是一堆術語。最後一句是整份簡報的核心態度。