Agent-Ready-Stack
為什麼你的 Stack 選擇,決定了 Agent 能走多遠?
最近常常看到別人分享他們用 vibe coding 打造的應用程式,一方面我恭喜他們,畢竟如果是生平第一次動手做出自己的想要的產品,這種體驗是連許多富豪都會跑下來玩的。另一方面,他們常常會講,選擇的 tech stack 是 TypeScript/React + Supabase ,似乎是 Lovable 或是 Cursor 的推薦,這點讓我全身不對勁,因為明明就有更好、對 AI agent 更加友善、更加代理就緒 (agent-ready) 的選項。
Context Window 的重要性
在談「哪個 tech stack 更加 agent-ready」之前,先要建立一個基礎認知:context 的大小,直接決定了 AI agent 能做做得多好。
這不只是直覺,是有實驗數據支撐的。LongCodeBench 的研究發現,Claude 3.5 Sonnet 在 bug fixing 任務上,context 從 32K 增加到 256K 時,準確率從 29% 崩跌至 3%——不是邊際衰減,是崩潰式衰減。Chroma 測試了 18 個前沿模型,每一個都有這個現象,沒有例外。
更糟的是,coding agent 的工作模式會主動加速這個衰減。每一次 tool call、每一次 file read、每一次 error message,都會留在 context 裡。一個跑 30 步的 agent session,context 消耗可能是單次對話的十倍以上。
所以問題來了:context window 的消耗速度,很大程度上取決於你選的 stack。
AI 在推薦 tech stack 時,傾向於推薦訓練資料最多的選項——TypeScript + React + Supabase 就是這樣被推到主流的。這個邏輯是「訓練資料多 → 生成成功率高 → 看起來是好建議」。但這個邏輯有一個隱藏假設:「生成容易」等同於「長期 AI 輔助開發效率高」。
這個假設是錯的。
Context Efficiency 是框架選擇的關鍵維度
AI 要完成一個任務,不需要讀整個 codebase,只需要讀「任務相關的那幾個檔案」——我們可以稱之為局部子圖(task-relevant subgraph)。這個子圖的大小,直接決定了 context 的消耗量。
關鍵在於:局部子圖的大小,是由框架的架構設計決定的,而不是由模型決定的。
TypeScript + React + Supabase 的問題在於,一個功能天生就會橫跨多個層次——component、hook、state、api client、type definition——每一個都在不同的檔案裡。局部子圖一開始就很大,而且隨著專案規模增長,共用的依賴 (shared dependencies) 越來越多,子圖只會越來越大。
這就是為什麼「訓練資料最多的 stack」不等於「最 agent-ready 的 stack」。
哪些架構設計,對 Agent 更友善
我個人常用的 tech stack ,是 Clojure Stack Lite ,而它至少有三個設計選擇,可以縮小局部子圖,減少 context 的消耗:HTMX、HoneySQL、Blocking IO 。
HTMX:用 curl 驗證前端,不需要模擬 client state
React 應用程式的前端邏輯活在 client 端:useState、useEffect、zustand、react-query——這些 state 分散在多個檔案,互相依賴。Agent 要驗證一個功能是否正確,必須把這些全部帶入 context,還要模擬 browser 的行為。
HTMX 的模型完全不同。所有的互動行為由 server response 驅動,前端幾乎沒有狀態。這帶來一個非常實際的好處:agent 可以直接用 curl 驗證功能。
# 驗證一個登入功能是否正常
curl -X POST http://localhost:3000/login \
-d "email=test@example.com&password=secret"response 就是 HTML fragment,對就是對,錯就是錯,不需要啟動 browser、不需要模擬 React render、不需要追蹤 client state。
對 agent 來說,這意味著驗證行為本身就相對節省 context——curl 的 response 比 browser DevTools 的 output 小一個數量級。
HoneySQL:讓 debug 的子圖縮小三到五倍
ORM 是一個關於 context 的陷阱。
當 ORM 出現 N+1 問題時(這幾乎是每個用 ORM 的專案都會遇到的問題),debug 過程需要把 model definition、association 設定、migration file 全部帶進 context,因為問題的根源藏在 ORM 的隱式行為裡——lazy loading 在你不注意的地方悄悄發生。
HoneySQL 用 SQL-as-data 的方式表達查詢:
(sql/format {:select [:u.id :u.name :p.title]
:from [[:users :u]]
:join [[:posts :p][] [:= :u.id :p.user-id]]
:where [:= :u.active true]})沒有 lazy loading,沒有 association magic。要拿關聯資料,你必須明確寫 JOIN——N+1 不會「不小心」發生,因為你無法在不知情的狀況下讓它發生。
當真的出現 query 問題,debug 的子圖只有一個地方:那個 query function。不需要追蹤 ORM 的 model 定義,不需要看 association 設定。子圖從五個檔案變成一個檔案。
Blocking IO:讓 Error 對 Agent 可見
Async 模型有一個結構性特徵,不是寫法問題,而是設計本質:error path 是隱式的。
在同步程式碼裡,exception 只有一條路——往上傳,直到被 catch。但在 async 模型裡,每一個 async 呼叫點都是潛在的 error 斷點。開發者必須在每個地方明確處理,否則 exception 會被封裝進 rejected Promise,與主流程脫鉤。
在 Node.js 裡,這個問題的典型場景:
async function saveUser(data) {
await db.insert(data); // throws here
}
async function handleRequest(req) {
saveUser(req.body); // missing await — Promise is detached
res.json({ ok: true }); // executes normally
}這段程式碼在現代 Node.js(v15+)不會靜默消失——runtime 會拋出 UnhandledPromiseRejection,甚至讓 process crash。但問題不在於 error 看不見,而在於error 與呼叫點脫鉤:crash 的 stack trace 指向 saveUser 內部,而不是 handleRequest 裡那行忘記 await 的地方。
對 AI coding agent 來說,這是 context 負擔的來源。要定位根因,agent 必須:
追蹤每一個 async 呼叫點,確認是否有 await
將整條呼叫鏈上的檔案納入 context
在沒有明確 error boundary 的情況下,推斷 error 從哪裡脫鉤
這個追蹤過程需要的 context 寬度,隨呼叫鏈的長度線性增長。
Clojure 的 blocking IO 結構不同。exception 只有一條 path:往上傳,在 middleware 統一處理。沒有 async boundary,就沒有脫鉤的可能。Agent debug 時只需要兩個地方:middleware 的 log,以及 log 指向的呼叫點。Context 的範圍是固定的,不隨系統規模擴大。
這是兩種模型在可除錯性上的結構差異,不是 Node.js 與 Clojure 的語言優劣比較。
為什麼『顯式』對 Agent 這麼重要
上述三個特色其實有共同的特色:隱式行為越少,agent 需要帶入 context 的範圍就越小。 HTMX 消除了隱式 client state,HoneySQL 消除了隱式 lazy loading,Blocking IO 消除了隱式執行順序與隱式 error path。
這邊的重點不是在 Clojure 比 TypeScript 好,重點是在於框架設計哲學的觀察:顯式優於隱式,對人類開發者是美德,對 AI agent 是確保它們不會提早變笨。
開始使用 Clojure Stack Lite
如果你對這個方向有興趣,最快的方式就是直接試試看。Clojure Stack Lite 可以讓你用一個指令生成一個 agent-ready 的 empty 專案,然後把剩下的事情交給 agent。
以下是我的 vibe coding 作法。
環境設置
環境設置有四個步驟:
Step 1: 將 Claude Code 的 model 設置為 Haiku 4.5 。由於如果使用了 superpower 的話,對 token 的消耗很激烈,所以我做這個調整,而 greenfield project 通常也不需要太複雜的推理。
claude
> /modelStep 2: 在 Claude Code 安裝 superpower 。要裝這個的原因是:如果用了 superpower 的話,當 Claude Code 做 TDD 時,會主動做 Integration Test。
claude
> /plugin install superpowers@claude-plugins-officialStep 3: 安裝 mise ,之後幾乎所有的套件都可以透過 mise 來安裝與管理。
brew install miseStep 4: 安裝 neil ,因為我要用來生成 empty project 的 Clojure Stack Lite 要用 neil
brew install babashka/brew/neil每個專案啟動時,則需要有以下的步驟:
Step 1: 用 Clojure Stack Lite 生成專案 myproject
neil new io.github.abogoyavlensky/clojure-stack-lite myproject :auth trueStep 2: 基本設置與安裝軟體
cd myproject
mise trust && mise installStep 3: 做個版本控管
git init
git add .
git commit -am 'Initial commit'然後就可以叫 agent 開始工作了。
進階設置
上面是快速起步的設置,而實務上我還會在 agent 正式開始工作之前,再多加設置 brepl 與 nrepl ,這主要是為了讓 Agent 可以透過 nrepl 來做快速地探索 (節省 tokens)。
安裝 brepl
curl -fsSL https://raw.githubusercontent.com/licht1stein/brepl/master/brepl -o ~/.local/bin/brepl
chmod +x ~/.local/bin/brepl在專案資料夾 (myproject) 裡,設置好 brepl
brepl hooks install在專案資料夾 (myproject) 裡的 deps.edn 裡,加入 nrepl 的依賴
neil add nrepl手動修改 bb.edn ,增加一個 alias
clj-nrepl
16 clj-repl {:doc "Run built-in Clojure REPL"
17 :task (shell "clj -A:dev:test")}
18
19 + clj-nrepl {:doc "Run Clojure REPL with nREPL server"
20 + :task (shell "clj -A:dev:test:nrepl")}
21 +
22 fmt-check {:doc "Check code formatting"
23 :task (shell "cljfmt" "check")}手動修改 README.md 裡,將
bb clj-repl改為bb clj-nrepl
來聊聊
如果你對 Clojure Stack Lite 有興趣,或是對 agent-ready-stack 的方向有想法,歡迎加入 Clojure Taiwan Telegram 群一起討論。也可以直接寫信給我:laurence@replware.dev


