實戰(zhàn):用Worker與Durable Object構(gòu)建AI Agent)
1. 從cloudflare-os這個標題說起它到底在指什么第一次看到cloudflare-os這個詞我下意識以為是 Cloudflare 官方又出了什么新操作系統(tǒng)。翻了一圈資料才反應(yīng)過來這更像是一個社區(qū)里自發(fā)形成的概念標簽用來描述一種把 Cloudflare 全家桶Workers、Durable Objects、KV、R2、D1、Queues、AI Gateway 等當成操作系統(tǒng)來用的開發(fā)范式。換句話說它不是某個具體的產(chǎn)品而是一種架構(gòu)思路把邊緣網(wǎng)絡(luò)當作內(nèi)核把各類綁定binding當作系統(tǒng)調(diào)用把 Worker 當作進程把 Durable Object 當作有狀態(tài)的守護進程。這個思路為什么最近被反復提起因為 AI Agent 這類應(yīng)用對基礎(chǔ)設(shè)施的要求和傳統(tǒng) Web 應(yīng)用完全不一樣。傳統(tǒng) CRUD 應(yīng)用是請求-響應(yīng)模型無狀態(tài)、短生命周期、可預測。而 Agent 是長任務(wù)、多輪工具調(diào)用、需要記憶、需要并發(fā)協(xié)調(diào)、需要流式輸出、需要按 token 計費。你拿一臺固定區(qū)域的 VPS 去跑很快就會遇到三個問題一是冷啟動和并發(fā)調(diào)度跟不上二是狀態(tài)管理要自己造輪子三是全球用戶訪問延遲參差不齊。Cloudflare 這套東西恰好在這三點上都有對應(yīng)的原語。Workers 負責無狀態(tài)計算Durable Objects 負責單點強一致狀態(tài)KV 負責最終一致的讀多寫少緩存R2 負責對象存儲D1 負責關(guān)系型數(shù)據(jù)Queues 負責異步解耦AI Gateway 負責模型調(diào)用的可觀測性和緩存。把它們組合起來確實有了一點操作系統(tǒng)的味道——你不再關(guān)心服務(wù)器在哪、怎么擴容、怎么容災你只關(guān)心我要調(diào)度哪個能力。這篇文章我想聊的不是Cloudflare 有多好而是如果你真的打算用這套東西去搭一個 AI Agent 系統(tǒng)哪些地方是坑、哪些設(shè)計決策要想清楚、哪些能力被高估了、哪些被低估了。適合已經(jīng)寫過至少一個 Worker、對 Agent 有基本概念、準備把它推到生產(chǎn)環(huán)境的開發(fā)者。純新手也能看但可能需要邊看邊查文檔。2. 把邊緣網(wǎng)絡(luò)當內(nèi)核這套操作系統(tǒng)的原語到底怎么分工2.1 Worker 是進程不是函數(shù)很多人第一次寫 Worker腦子里還是云函數(shù)的模型一個請求進來跑一段代碼返回結(jié)束。這個理解在簡單場景下沒錯但一旦涉及 Agent就會出問題。Agent 的一次任務(wù)可能持續(xù)幾十秒甚至幾分鐘中間要調(diào)用多次模型、多次工具、多次讀寫狀態(tài)。如果你還按一個請求一個函數(shù)來設(shè)計就會被迫把整個 Agent 循環(huán)塞進一個請求里然后撞上 CPU 時間限制和請求超時。正確的做法是把 Worker 當成進程調(diào)度器而不是進程本身。一個請求進來Worker 只負責解析意圖、創(chuàng)建或喚醒一個 Durable Object、把任務(wù)投遞進去然后立刻返回一個任務(wù) ID 或者建立一條流式連接。真正的 Agent 循環(huán)跑在 Durable Object 里它可以長時間存活、可以被多次喚醒、可以主動推送結(jié)果。這個分工想清楚了后面很多設(shè)計就順了。我見過太多人把 Agent 邏輯直接寫在 fetch handler 里然后抱怨為什么跑一半就斷了。不是平臺的問題是模型用錯了。2.2 Durable Object 是這套架構(gòu)里最被低估的原語如果說 Worker 是進程那 Durable Object后面簡稱 DO就是帶狀態(tài)的守護進程 單點鎖。每個 DO 實例有全局唯一的 ID同一時刻只有一個實例在跑所有請求串行進入。這個特性聽起來像限制實際上是 Agent 場景里最需要的東西。想象一個多輪對話 Agent用戶發(fā)消息Agent 要讀歷史、調(diào)模型、調(diào)工具、寫回歷史。如果這些操作并發(fā)進來沒有串行保證歷史就會亂。用傳統(tǒng)方案你得自己上分布式鎖、自己處理競態(tài)。用 DO天然就是串行的你只要把狀態(tài)放在 DO 的 storage 里讀寫都是強一致的。但 DO 也有代價。它的單點特性意味著吞吐有上限一個 DO 實例大概每秒能處理幾百到上千個請求取決于你的邏輯復雜度。所以設(shè)計時要考慮分片一個用戶一個 DO還是一個會話一個 DO還是一個租戶一個 DO我的經(jīng)驗是一個會話一個 DO最穩(wěn)用戶級別的聚合狀態(tài)另開一個 DO 或者放 D1。這樣單個會話的并發(fā)壓力可控跨會話的查詢走 D1。2.3 KV、R2、D1、Queues 各自的邊界這四個存儲原語經(jīng)常被混用我列個表說清楚它們各自適合什么原語一致性適用場景不適用場景KV最終一致寫入后全球生效有延遲配置、feature flag、讀多寫少的緩存需要立即讀到剛寫入的數(shù)據(jù)R2強一致單區(qū)域大文件、模型產(chǎn)物、日志歸檔高頻小對象讀寫D1強一致單主庫關(guān)系型數(shù)據(jù)、需要 SQL 查詢高并發(fā)寫入、跨區(qū)域低延遲讀Queues至少一次投遞異步任務(wù)、削峰、解耦需要嚴格順序或精確一次Agent 場景里最常見的誤用是拿 KV 存會話狀態(tài)。KV 寫入后全球生效有幾十秒延遲你這邊剛寫完下一個請求打到另一個邊緣節(jié)點讀到舊值A(chǔ)gent 就失憶了。會話狀態(tài)必須放 DO 或者 D1KV 只適合放那些變了也無所謂的東西。2.4 AI Gateway 不是必須的但強烈建議加上AI Gateway 的核心價值不是幫你調(diào)模型而是三件事緩存、限流、可觀測。Agent 場景里模型調(diào)用是成本大頭緩存能省的錢很可觀——尤其是那些 system prompt 固定、用戶問題重復率高的場景。限流能防止某個用戶或某個 bug 把你的額度打爆??捎^測能讓你看到每次調(diào)用的延遲、token 數(shù)、失敗率。我自己的做法是所有模型調(diào)用都走 AI Gateway哪怕只是加一層轉(zhuǎn)發(fā)。多這一跳的延遲可以忽略但換來的是排查問題時的救命稻草。有一次線上 Agent 突然變慢就是靠 Gateway 的日志發(fā)現(xiàn)某個工具的返回體異常大把上下文撐爆了。3. 用 Worker DO 搭一個最小可用的 Agent 骨架3.1 為什么不用現(xiàn)成的 Agent 框架現(xiàn)在 Agent 框架滿天飛LangChain、LlamaIndex、各種國產(chǎn)的、各種一行代碼搭 Agent的。我的建議是如果你要做生產(chǎn)系統(tǒng)先別急著上框架。原因有三個。第一框架的抽象層會掩蓋底層行為。Agent 出問題時你需要在模型返回了什么工具被調(diào)了幾次狀態(tài)在哪一步寫壞了這些層面排查??蚣馨堰@些都包起來了你只能看它給你的日志很多時候不夠用。第二框架的更新速度跟不上模型和平臺的變化。你今天用的框架可能下個月就不維護了或者某個 API 變了它沒跟上。生產(chǎn)系統(tǒng)最怕這種依賴。第三Agent 的核心邏輯其實不復雜。無非是讀狀態(tài) → 調(diào)模型 → 解析工具調(diào)用 → 執(zhí)行工具 → 寫狀態(tài) → 循環(huán)。這個循環(huán)自己寫一遍兩百行代碼比學一個框架的 API 快。所以我下面給的骨架是裸寫的不依賴任何 Agent 框架。你可以后面按需引入但先理解底層。3.2 項目結(jié)構(gòu)agent-worker/ ├── src/ │ ├── index.ts # Worker 入口路由分發(fā) │ ├── session.ts # Durable Object會話狀態(tài)與 Agent 循環(huán) │ ├── tools/ │ │ ├── index.ts # 工具注冊表 │ │ ├── search.ts # 示例工具搜索 │ │ └── calc.ts # 示例工具計算 │ └── llm.ts # 模型調(diào)用封裝 ├── wrangler.toml └── package.json這個結(jié)構(gòu)的關(guān)鍵是session.ts里的 DO 承擔了 Agent 循環(huán)index.ts只做路由和鑒權(quán)。工具用注冊表模式方便擴展。3.3 wrangler.toml 的關(guān)鍵配置name agent-worker main src/index.ts compatibility_date 2024-11-01 [[durable_objects.bindings]] name SESSION class_name SessionDO [[migrations]] tag v1 new_classes [SessionDO] [[kv_namespaces]] binding CACHE id your-kv-id [[d1_databases]] binding DB database_name agent-db database_id your-d1-id [ai] binding AI幾個容易踩的點compatibility_date一定要寫而且盡量用新的否則一些新 API 用不了。DO 的 migration 每次加新類都要加 tag不能改舊的。AI binding 是 Workers AI 的綁定如果你用外部模型比如 OpenAI、Anthropic不需要這個直接 fetch 就行。3.4 DO 里的 Agent 循環(huán)長什么樣核心邏輯大概是這樣export class SessionDO { state: DurableObjectState; env: Env; messages: Message[] []; constructor(state: DurableObjectState, env: Env) { this.state state; this.env env; this.state.blockConcurrencyWhile(async () { this.messages (await this.state.storage.get(messages)) || []; }); } async fetch(request: Request) { const { input } await request.json(); this.messages.push({ role: user, content: input }); let step 0; while (step 10) { const response await callLLM(this.env, this.messages); this.messages.push(response); if (response.toolCalls response.toolCalls.length 0) { for (const call of response.toolCalls) { const result await executeTool(call.name, call.args); this.messages.push({ role: tool, tool_call_id: call.id, content: JSON.stringify(result), }); } step; continue; } await this.state.storage.put(messages, this.messages); return new Response(JSON.stringify({ reply: response.content })); } return new Response(JSON.stringify({ error: max steps exceeded }), { status: 500 }); } }這段代碼有幾個設(shè)計決策值得說。blockConcurrencyWhile保證構(gòu)造時狀態(tài)加載完再處理請求避免競態(tài)。step 10是防止 Agent 陷入死循環(huán)生產(chǎn)環(huán)境這個值要按業(yè)務(wù)調(diào)。每輪循環(huán)都寫 storage 是浪費只在最終返回時寫一次中間狀態(tài)放內(nèi)存——但如果 DO 被驅(qū)逐內(nèi)存會丟所以關(guān)鍵節(jié)點還是要落盤。3.5 流式輸出怎么接Agent 場景里流式輸出幾乎是必須的用戶等 30 秒看不到任何反饋會以為卡死了。DO 支持返回 ReadableStream你可以把模型的流式響應(yīng)直接透傳出去。但要注意一旦開始流式輸出就不能再改 HTTP 狀態(tài)碼了所以錯誤處理要在流開始前做完。我的做法是DO 先做一輪預檢檢查額度、檢查輸入合法性通過了再開始流式。流中間如果出錯發(fā)一個特殊的 SSE 事件告訴前端出錯了前端負責展示。這樣用戶體驗最好。4. 狀態(tài)、記憶與并發(fā)Agent 最容易翻車的地方4.1 短期記憶和長期記憶要分開設(shè)計Agent 的記憶這個詞被用爛了實際上要拆成兩層。短期記憶是當前會話的上下文就是那個 messages 數(shù)組放 DO 內(nèi)存 storage。長期記憶是跨會話的知識比如這個用戶偏好簡潔回答這個用戶是開發(fā)者放 D1 或者向量庫。很多人一上來就搞向量數(shù)據(jù)庫做長期記憶結(jié)果發(fā)現(xiàn)召回質(zhì)量差、成本高、還慢。我的建議是先用結(jié)構(gòu)化字段。用戶偏好、常用工具、歷史摘要這些用 D1 存幾個字段就夠了查詢快、可控、可解釋。真正需要語義召回的場景比如幫我找上次討論過的那個方案再上向量。4.2 上下文窗口管理是個工程問題不是模型問題模型上下文再大也有上限而且越大越貴越慢。Agent 跑多輪之后messages 數(shù)組會膨脹。你必須主動管理。我常用的策略是三層最近 N 輪完整保留更早的輪次做摘要壓縮再早的直接丟棄但保留關(guān)鍵實體。摘要壓縮用便宜的小模型做比如把 10 輪對話壓成 200 字。關(guān)鍵實體人名、項目名、數(shù)字單獨抽出來放一個結(jié)構(gòu)化字段不參與壓縮。這里有個坑摘要會丟信息而且丟得不可預測。所以涉及金額、日期、ID 這類不能錯的信息一定要單獨存不能只靠摘要。4.3 并發(fā)請求下的狀態(tài)一致性同一個會話用戶可能快速發(fā)兩條消息。如果兩條都進 DODO 是串行處理的第二條會等第一條跑完。這聽起來對但實際體驗很差——用戶發(fā)第二條時第一條可能還在跑 Agent 循環(huán)要等幾十秒。我的處理方式是DO 內(nèi)部維護一個任務(wù)隊列新消息進來先入隊返回已收到。Agent 循環(huán)處理完當前任務(wù)后檢查隊列有就繼續(xù)。前端通過 WebSocket 或者輪詢拿結(jié)果。這樣用戶感覺是即時響應(yīng)實際是異步處理。另一個坑是 DO 的 storage 寫入有頻率限制。如果你每輪循環(huán)都寫高頻會話下會撞限流。批量寫、延遲寫、只寫關(guān)鍵狀態(tài)這三點要做到。4.4 工具調(diào)用的冪等性Agent 調(diào)工具可能失敗重試如果工具不冪等就會重復執(zhí)行。比如發(fā)郵件這個工具重試兩次就發(fā)兩封。解決辦法是給每次工具調(diào)用生成一個唯一 ID工具內(nèi)部用這個 ID 去重。對于有副作用的工具寫數(shù)據(jù)庫、發(fā)消息、扣款冪等設(shè)計是必須的不是可選的。我見過一個案例Agent 調(diào)支付工具網(wǎng)絡(luò)抖動重試用戶被扣了兩次錢。這種事故在 Agent 系統(tǒng)里比傳統(tǒng)系統(tǒng)更容易發(fā)生因為 Agent 會自作主張重試。5. 部署、觀測與成本上線之后才是真正的開始5.1 本地開發(fā)和線上行為的差異wrangler dev本地跑和線上跑行為有差異。最明顯的是 DO 的行為本地是單進程模擬線上是真正的分布式。本地測著沒問題的并發(fā)邏輯線上可能出問題。所以關(guān)鍵路徑一定要在 staging 環(huán)境用wrangler deploy --env staging跑一遍。另一個差異是 KV 的一致性。本地 KV 是立即一致的線上是最終一致。本地測著寫完立刻讀能讀到線上可能讀不到。這個坑我踩過排查了半天。5.2 觀測要抓的三個指標Agent 系統(tǒng)的觀測我重點關(guān)注三個每輪循環(huán)的 token 消耗、工具調(diào)用的成功率和延遲、端到端任務(wù)完成率。前兩個決定成本第三個決定體驗。Cloudflare 自帶的 Analytics 能看到請求量、錯誤率、CPU 時間但看不到業(yè)務(wù)層面的指標。我的做法是在 Worker 里埋點把關(guān)鍵事件寫到 Analytics Engine 或者直接推到外部比如自建的時序庫。別嫌麻煩出問題時這些數(shù)據(jù)是唯一能救你的。5.3 成本控制的幾個實操手段Agent 成本主要是模型調(diào)用。控制手段按性價比排序緩存 小模型分流 上下文壓縮 限流。緩存用 AI Gateway 的緩存功能相同輸入直接返回。小模型分流是指簡單任務(wù)分類、抽取、格式化用便宜模型復雜推理才用大模型。上下文壓縮前面說過。限流是兜底防止異常流量。我算過一筆賬一個中等復雜度的 Agent不做任何優(yōu)化每次任務(wù)大概 0.05 到 0.2 美元。做了緩存和分流之后能降到 0.02 到 0.08。量大了這個差距很可觀。5.4 冷啟動和 DO 驅(qū)逐DO 長時間不活動會被驅(qū)逐下次請求要重新加載狀態(tài)。如果你的 DO 構(gòu)造時要讀大量數(shù)據(jù)冷啟動會慢。解決辦法是構(gòu)造時只讀必要的最小狀態(tài)其余按需加載。另外 DO 的 storage 讀取是異步的構(gòu)造時用blockConcurrencyWhile包住避免請求進來時狀態(tài)還沒加載完。Worker 本身的冷啟動已經(jīng)很快了毫秒級基本不用管。真正要管的是 DO 的狀態(tài)加載。6. 幾個我踩過的坑和對應(yīng)的解法6.1 坑一把 DO 當數(shù)據(jù)庫用一開始我把所有會話數(shù)據(jù)都塞進 DO storage覺得方便。結(jié)果數(shù)據(jù)量一大DO 的 storage 讀寫變慢而且沒法做跨會話查詢。后來改成 DO 只存活躍會話的上下文歷史歸檔到 D1查詢走 D1。DO 的 storage 有大小限制別把它當無限倉庫。6.2 坑二忽略 Worker 的 CPU 時間限制Worker 有 CPU 時間限制免費版 10ms付費版 30s 可配。注意是 CPU 時間不是墻鐘時間。等模型響應(yīng)的時間不算 CPU但 JSON 解析、字符串處理、加密這些算。我有一次在 Worker 里做大量 JSON 處理直接撞限制。解決辦法是把重計算移到 DO 或者用流式處理別一次性加載大對象。6.3 坑三工具描述寫得太隨意Agent 靠工具描述來決定調(diào)哪個工具。描述寫得模糊Agent 就會亂調(diào)。比如你有個工具叫search描述寫搜索信息Agent 不知道搜什么、什么時候搜。正確的寫法是寫清楚這個工具做什么、什么時候用、輸入格式、返回什么、有什么限制。工具描述是 prompt 工程的一部分要認真寫。6.4 坑四沒有超時和重試策略模型調(diào)用、工具調(diào)用、外部 API 調(diào)用都要有超時。沒有超時一個卡住的調(diào)用會把整個 Agent 循環(huán)拖死。重試要有退避策略不能無腦重試。我的默認配置是模型調(diào)用超時 30s重試 2 次指數(shù)退避工具調(diào)用超時 10s重試 1 次外部 API 按對方文檔來。6.5 坑五日志里泄露敏感信息Agent 的 messages 里可能有用戶隱私、API key、內(nèi)部數(shù)據(jù)。日志默認會記錄請求體一不小心就泄露了。我的做法是日志只記元數(shù)據(jù)長度、hash、工具名不記內(nèi)容確需記內(nèi)容的先脫敏。這個在合規(guī)要求高的場景是硬性要求。7. 這套架構(gòu)適合什么、不適合什么聊了這么多技術(shù)細節(jié)最后說點判斷層面的東西。Cloudflare 這套邊緣操作系統(tǒng)不是銀彈它有明確的適用邊界。適合的場景全球用戶、對延遲敏感、流量波動大、團隊小不想管服務(wù)器、Agent 任務(wù)以 IO 等待為主調(diào)模型、調(diào) API。這些場景下它的彈性、全球分布、按量計費優(yōu)勢明顯。不適合的場景需要長時間 CPU 密集計算比如本地跑推理、需要特定硬件GPU、需要復雜的事務(wù)跨多個數(shù)據(jù)源、團隊已經(jīng)有成熟的 K8s 體系且遷移成本高。這些場景下硬上 Cloudflare 會別扭。我自己的判斷標準是如果我的 Agent 任務(wù) 90% 的時間在等外部響應(yīng)那 Cloudflare 是優(yōu)選如果 90% 的時間在算那要找別的地方。這個標準幫我省了很多糾結(jié)。另外提醒一句這套東西的生態(tài)還在快速變化。今天的最佳實踐半年后可能就過時了。保持關(guān)注官方 changelog別把架構(gòu)設(shè)計得太死留好替換空間。我一般會把模型調(diào)用、存儲訪問、工具執(zhí)行都封裝成接口底層實現(xiàn)可替換。這樣平臺變了、模型換了上層邏輯不用動。寫到這里關(guān)于 cloudflare-os 這個概念的拆解基本覆蓋了從架構(gòu)選型到上線運維的主要環(huán)節(jié)。如果你正準備用這套東西搭 Agent建議先從最小骨架跑通再逐步加狀態(tài)、加工具、加觀測。別一上來就設(shè)計大而全的架構(gòu)Agent 系統(tǒng)的復雜度是長出來的不是設(shè)計出來的。