← Agentic System
Agentic System

Agent 系統是 I/O bound 特化系統:User-First Agent 為什麼選 TypeScript

2026-07-11 · — views

我最近接了一個新任務:設計一個靠近 User 的 Agent。有 Chat 介面、回應要逐字串流、使用者隨時可以按停止。我的背景是 Python,FastAPI 和 LangChain 都熟,照理說直接開工就好。

但這樣的場景讓我想認真評估一次:TypeScript 和 Python,這兩個對 Agent 開發支援最完整的生態,哪個更適合這件事?我之前沒寫過 TypeScript,趁這個機會研究了一輪,結論是值得一試。評估過程中最關鍵的發現是 workload 的形狀:Agent 系統 99% 的時間都在等待,它是一個極端 I/O bound 的特化系統,而這個形狀和 Node.js event loop 幾乎完全吻合。這篇記錄整個評估的推理過程:用 TypeScript 寫 Agent 有什麼優勢、有哪些要注意的地方,以及什麼情況下還是該選 Python。

讀完精華版(2 分鐘),你會理解:

  • 為什麼 Agent 系統是「極端 I/O bound」,以及這個形狀如何直接決定架構選擇
  • Python asyncio 和 JS event loop 的本質差異:一個是可選模式,一個是存在方式
  • 什麼情況下這個結論會反過來,你還是該選 Python

這篇不是 TypeScript 教學,也不是語言優劣論戰。它是一個選型決策的推理過程,你可以拿同一套框架去檢驗自己的場景。


精華版

維度Python (asyncio)TypeScript (Node.js)
併發模型async 是後來加上的可選模式event loop 是 runtime 的存在方式
生態一致性sync / async 兩個平行世界(requests vs aiohttp)全生態只有 async 一種
Agent workload 契合度可以做到,但要自律避開 sync 陷阱天然契合,單 process 全 async 就是預設
Streaming 到瀏覽器做得到,但 SSE 到前端要跨語言接SSE 直通,前後端同語言
資料 / ML 生態不可替代幾乎沒有
  • Agent workload:一次 Agent run 的時間軸裡,99% 在等 LLM 回應和 tool 的 HTTP/DB 回來,CPU 幾乎沒事做,所以單一 process 的全 async 架構就能撐起很高的併發。
  • Python asyncio:asyncio 是 Python 後來才加進來的第二套併發模式,生態至今分裂成 sync 和 async 兩個世界,寫 Agent 時你要不斷自律「別讓 sync 呼叫混進來堵住 loop」。
  • Node.js event loop:TypeScript 跑在 Node.js 上,繼承 JS 從第一天就有的 single-threaded event loop,所有 I/O 天生非同步,生態裡不存在 sync 版本的誘惑,「頂到底全 async」不是紀律是預設。

幾個關鍵決策問題:

Q:Agent 系統為什麼不需要 worker pool / celery? A:那是 CPU bound 世界的預設問題。I/O bound 的系統瓶頸在等待不在計算,單 process 全 async 就是答案,一開始就想 scale 是過早優化。

Q:單線程不怕併發上不去嗎? A:等待不佔 CPU。event loop 在等 LLM 回應的時候可以同時服務幾百個其他請求,真正的瓶頸會先出現在 LLM provider 的 rate limit,不是你的 process。

Q:什麼時候還是選 Python? A:看交付物。交付的是報告、資料、pipeline,或系統要碰 NumPy/PyTorch 生態,選 Python。交付的是給使用者的即時互動介面,才輪到 TypeScript。


以下是完整版,按需取用。

什麼是 User-First Agent?

User-First Agent 是指有一個人在螢幕前即時等待回應的 Agent 系統:回應要逐字串流、過程要可見、隨時可以被打斷。選型討論最常見的錯誤是脫離場景空談語言優劣,所以先把我的場景釘死:

  • 使用者透過 Chat 介面和 Agent 對話
  • 回應必須逐字串流,不能讓人盯著空白畫面等 20 秒
  • 使用者隨時可以按「停止生成」,可以關掉分頁再回來
  • Agent 會呼叫 tools(搜尋、查 DB、打外部 API),過程要對使用者可見

用一個光譜來定位這種系統:

● 光譜 · Agent 越靠近 User,TypeScript 越重要
Python 兩者皆可 TypeScript
數據 / 模型層
  • ML 訓練 / Fine-tuning
  • Data pipeline
  • NumPy / PyTorch
  • 科學計算
邏輯 / 任務層
  • Workflow agent
  • Task agent
  • MCP Server
  • CLI 工具
介面層
  • Chat UI
  • Streaming 介面
  • SaaS 產品
  • Next.js app
越靠右,TypeScript 的優勢越顯著;越靠左,Python 生態越難取代

Agent 距離 User 越近,TypeScript 的優勢越大;越靠近數據和模型,Python 越難被取代。這張光譜圖我在上一篇展開過,當時講的是「為什麼值得學」。這篇要回答的是更硬的問題:真的要蓋一個光譜右側的系統時,選型的推理過程長什麼樣。

我的場景落在光譜的最右端。但「靠近 User」只是結論的一半,另一半藏在 workload 的形狀裡。

為什麼說 Agent 系統是極端 I/O bound 的特化系統?

一次 Agent run 裡,CPU 真正在做事的時間不到 1%,其餘時間全在等 LLM 回應和 tool 的 HTTP/DB 回來。把時間軸攤開來看:

● 一次 Agent run 的時間軸
撈對話歷史(DB) ~20ms
LLM 第一輪回應 2–10s
Tool call:外部 API 0.5–3s
Tool call:查 DB ~50ms
LLM 第二輪回應 2–10s
寫回狀態(DB) ~20ms
本地計算(組 prompt、parse) <1%
等待 I/O ≈ 99%
CPU 真正在做事的時間不到 1%,Agent 系統本質上是「編排等待」的系統

這不是「偏 I/O bound」,是極端 I/O bound,比一般的 CRUD backend 還極端。CRUD API 至少還有序列化、模板渲染這些 CPU 工作,Agent 系統連這些都少,它本質上是一個「編排等待」的系統。

這個形狀直接推出三個架構含義:

單一 process 可以撐很高的併發。 等待不佔 CPU。一個 event loop 在等某個使用者的 LLM 回應時,可以同時處理幾百個其他使用者的請求。你不需要一開始就想 horizontal scale,真正的瓶頸會先出現在 LLM provider 的 rate limit。

不需要 worker pool,不需要 celery。 這是我從 Python 帶過來的預設問題:「併發要開幾個 worker?任務要不要丟 queue?」在 I/O bound 的世界裡,這個問題本身就是過早優化。單 process、全 async、按 I/O 邊界切模組,設計起點就這麼簡單。

唯一要防的是 CPU bound 混進來。 event loop 是單線程的,一段 50ms 的重運算(regex 掃大檔、tokenize 長文本、parse 超大 JSON)會堵住所有人。但注意這是「防守少數例外」,不是「處處要防」,和 CPU bound 系統的設計負擔完全不同量級。

到這裡都還是 workload 分析,和語言無關。Python asyncio 一樣可以寫出單 process 全 async 的 Agent。差異在下一節:兩個生態對「async」這件事的態度,根本不同。

Python asyncio 和 TypeScript event loop 的差異是什麼?

核心差異一句話:asyncio 是 Python 的可選模式,event loop 是 TypeScript runtime 的存在方式。Python 的 asyncio 是語言發展多年之後才加進來的第二套併發模式。在它出現之前,整個生態早已用 sync 的方式累積了大量的套件和寫法。結果是今天的 Python 有兩個平行世界:

# sync 世界              # async 世界
requests               aiohttp / httpx
psycopg2               asyncpg
time.sleep()           asyncio.sleep()
def f():               async def f():

寫 Python Agent 時,這個分裂是每天的心智負擔。你要確認每個依賴套件有沒有 async 版本;某個 SDK 只有 sync 版時,你要決定是接受它堵 loop 還是包進 run_in_executor;team 裡有人在 async 函式裡呼叫了 requests.get(),整個 event loop 靜止,而且這種 bug 不會報錯,只會表現成「系統偶爾變很慢」。async 在 Python 是一種需要全隊自律才能維持的紀律。

TypeScript 沒有這個問題,因為它沒有選擇。TypeScript 編譯後就是 JavaScript,跑在同一個 Node.js runtime 上,繼承的是 JS 從第一天就有的 single-threaded event loop,語言裡不存在 blocking I/O 的正統寫法。fetch 是 async 的,DB driver 是 async 的,檔案讀寫是 async 的,你想找一個 sync 的 HTTP client 來誤用都找不到。「頂到底全 async」在 TypeScript 不是架構決策,是唯一的寫法。

這在實際寫 Agent 時的樣子:

// 獨立的 tool call 天然併發,不用想「這裡能不能 async」
const [weather, news] = await Promise.all([
  tools.getWeather.execute(input1),
  tools.getNews.execute(input2),
]);

// 容錯併發:一個 tool 掛掉不拖垮整批
const results = await Promise.allSettled(toolCalls.map(runTool));

Python 的 asyncio.gather 能做到一樣的事。差別不在能不能,在於一個生態把這當成預設路徑,另一個生態要你時刻記得自己走在特殊路徑上。Agent 系統的每一行都在做 I/O,這個差別會被放大到每一天的開發體驗裡。

Event loop 不是免費午餐

誠實面,選了 Node.js 之後有兩個雷是 Python 工程師直覺之外的:

Node 的 fetch 預設永不超時。 Python 的 requests 有預設 timeout 的文化,Node 沒有。一個 tool 呼叫的外部 API hang 住,你的 agent loop 就永遠掛在那。每個對外呼叫都要自己給 AbortSignal.timeout():

const res = await fetch(url, { signal: AbortSignal.timeout(30_000) });

單線程不等於沒有並發問題。 單線程消滅了「兩條線程同時寫一個變數」,但沒有消滅 await 之間的交錯。兩個請求同時「讀對話歷史 → push 新訊息 → 寫回」,交錯點就在 await,後寫的會蓋掉先寫的。check-then-act 的 race 在單線程照樣發生,解法和多線程世界一樣:交給 DB 層的 atomic 操作,不要在應用層讀改寫。

這兩個雷不改變結論,但它們提醒一件事:event loop 給你的是「契合的併發模型」,不是「不用思考併發」。

靠近 User 的另一半理由:前端就在隔壁

Workload 形狀是我這次選型最重的論據,但光譜右側還有一組加成,快速帶過:

  • SSE 直通瀏覽器:Agent 的逐字輸出用 Server-Sent Events 流出去,瀏覽器原生支援,前後端同語言接起來沒有縫
  • 型別一次定義:事件流的 schema 是前後端共用的 contract,一個 interface AgentEvent 兩邊 import,改了 schema 前端立刻編譯報錯
  • User-facing AI tooling 是 TS-first:Vercel AI SDK、官方 MCP SDK,越靠近介面層的工具,TypeScript 的支援越完整

這三點我在上一篇有完整展開,這裡不重複。值得補一句的是:這組理由和 workload 形狀是疊加關係。就算你的 Agent 暫時沒有 UI,只要它是 User-First 的(streaming、可中斷、事件驅動),I/O bound 的論證就已經成立;等哪天 UI 進來,前端鄰近的加成才開始兌現。

什麼時候還是該選 Python?

交付物是資料、報告、pipeline,或系統要碰 NumPy/PyTorch 生態時,選 Python,這個結論沒有懸念。一個誠實的選型文要能說清楚自己的邊界,判斷只需要問一個問題:你的 Agent 最終交付的是什麼?

交付物選擇
資料、報告、檔案Python
ML pipeline、需要 NumPy/PyTorch 的任何東西Python,沒有懸念
自己用的 CLI 工具Python(你熟什麼用什麼)
Workflow / batch task agent,沒有人在等Python 完全夠
MCP Server兩者都行
給使用者的產品:Chat UI、即時串流、可中斷的互動TypeScript

判斷的核心是「有沒有一個人在螢幕前等待」。沒有人在等,streaming、TTFT、可中斷這些 User-First 的需求全部消失,I/O bound 的論證雖然還在,但 Python asyncio 的自律成本在一個沒有即時性壓力的系統裡完全付得起,你沒有理由放棄熟悉的生態。

有人在等,整組需求一起出現:逐字串流、停止按鈕、斷線重連、tool 執行過程可見。這時 workload 形狀、event loop 契合、前端鄰近三個論據同向疊加,TypeScript 從「可以考慮」變成「難以拒絕」。

我的場景是後者,所以這個系列接下來的兩篇,會繼續講選完語言之後真正難的部分:User-First Agent 的 run 模型該怎麼設計,以及錯誤和信任邊界該怎麼劃。


結語:選型不是選語言,是先看清 workload 的形狀。Agent 系統是一個 99% 時間在編排等待的 I/O bound 特化系統,而 event loop 是為這個形狀而生的併發模型。