如何打造你專屬的 Agent 框架???

@mfpiccolo
英語2 個月前 · 2026年5月28日
339K
1.9K
242
35
4.9K

TL;DR

作者認為傳統的 Agent 框架過於僵化,因此提出了一種模組化的「iii」引擎方案,將框架中的每個組件(從憑證到策略引擎)都設計為獨立且可替換的 worker。

大多數的 Agent 團隊並不會自己打造一個「框架」。他們會直接採用一個。LangChain、LangGraph、OpenAI Agents SDK、Anthropic SDK、CrewAI、AutoGen——這些框架中的循環、工具、記憶與編排機制,都是作為一個單一決策被直接從貨架上拿下來使用。這個框架就是你引入的一個套件。如果它內部的某個部分不合用,你要嘛得自己 fork 一份、要嘛跟它硬碰硬、要嘛繞道而行。

Mike Piccolo - inline image

我認為這種架構思維是錯誤的,而這也正是為什麼每個長期經營 Agent 的團隊,最終都會走上從頭改寫自己框架的道路。這個框架不是單一一個東西。它是十到十二個不同東西的捆綁包,因為周圍的生態系統沒有提供一個組合它們的方式。Pi agent 套件正走在正確的路上,但它們仍然停留在「新增另一個服務,然後將它與所有其他服務整合」的範例中。而 iii 引擎則將所有工作單元(worker)一視同仁,並完全移除了整合邏輯。提供者路由器、憑證保管庫、政策引擎、核准閘道、模型目錄、會話儲存、預算追蹤器、通話後鉤子分發器,以及持久化的交互回合循環——這些都是各自獨立的關注點。它們都可以與你的佇列、HTTP/API 伺服器、串流、甚至是瀏覽器中的工作單元互通操作。一個將它們作為單一區塊來發布的框架,是在強迫你接受一個你根本不需要做出的取捨。

iii 背後的核心賭注是,它們不應該是一個區塊。應該有一套基於共享引擎的工作單元,每一個都是可替換的、各自獨立版本控制、並透過一個單一原語連接:一個觸發器(iii.trigger()),而這個觸發器所有其他工作單元也都在使用。這個框架變成了一個可安裝工作單元的堆疊,而「打造自己的框架」不再意味著「fork 一個框架」,而是「替換幾個工作單元」。

這篇文章將帶你實際了解這是什麼樣的概念。內容包含:目前驅動一個 iii Agent 交互回合的完整堆疊、為什麼每一層都是一個獨立的工作單元,以及你如何替換其中任何一個。

Agent 框架必須完成的 15 項工作

如果將一個生產環境級的 Agent 框架拆解到其核心職責,你會得到一份類似這樣的清單:

  1. 從客戶端接收一個交互回合請求並將其持久化儲存
  2. 解析用於呼叫模型提供者的憑證
  3. 查詢所選模型實際能做些什麼(視覺、工具、串流、上下文視窗)
  4. 驅動每個交互回合的狀態機:準備、串流助手回應、執行工具、引導、拆卸
  5. 載入並提供描述每個函數請求結構、錯誤代碼和使用說明的技能(skill)本體
  6. 組裝系統提示詞、模式段落、身份前文、工作目錄與預設技能附錄
  7. 當模型產生 Token 時,將其串流回客戶端
  8. 在執行前,根據政策檢查每一個工具呼叫(本質上就是一個函數)
  9. 暫停需要人類決策的工具呼叫,並將答案路由回正確的交互回合
  10. 追蹤 LLM 花費,針對每個工作區或每個 Agent 的預算進行管理
  11. 在工具呼叫前後執行鉤子(紀錄、編輯、自訂副作用)
  12. 將會話持久化儲存為一個分支樹,以便支援分叉和恢復
  13. 當上下文視窗滿載時壓縮會話歷史記錄
  14. 發出一個 UI 可以訂閱的事件串流
  15. 每個 Agent 公司在開發過程中都會缺少的一塊:在每一步驟中攜帶一個 OpenTelemetry 追蹤,以便你進行除錯

每一個嚴肅的 Agent 框架都必須處理大部分這些工作。昂貴的框架會全部實現。便宜的框架則會偷工減料,等到上線生產環境後再來重頭補救。而框架們則將這些工作捆綁成一個整體,並為每個部分發布一個版本。最後這一點是真正會讓你付出代價的部分,因為一年後,你會發現你需要的政策引擎,並不是那個框架附帶的,而替換它意味著你要替換整個框架。

iii 的框架則將上述十三項工作中的每一項,都作為一個獨立的工作單元發布在 workers.iii.dev 註冊表上。每一個都使用相同的 WebSocket 協定。每一個都在同一個引擎匯流排上註冊函數和觸發器。每一個都可以透過 iii worker add 指令來新增、替換,並且可以用任何擁有 SDK 的語言來編寫。

堆疊:按工作單元劃分

以下是來自 iii-hq/workers 單一儲存庫的實際生產環境堆疊,每個工作單元的職責濃縮成一行。整個套件發布於 github.com/iii-hq/workers/harness

Mike Piccolo - inline image

十一個工作單元。一個引擎。每一個都有自己的發布版本。每一個都可以作為一個獨立程序執行(開發時使用 pnpm dev:<worker>,發布二進位檔案時使用 iii worker add <specific-worker>),或者作為將它們一起啟動的複合入口點的一部分。

這之所以重要,是因為:該表格中的每一個方框,都是一個你可以讓別人提供一個不同的工作單元,而你則保留其餘部分的地方。不喜歡靜態的模型目錄?插入一個註冊了 models::list 並從即時 API 讀取資料的工作單元。不喜歡基於檔案的憑證?插入一個註冊了 auth::get_token 並從金鑰管理服務讀取資料的工作單元。想要為一個分支邏輯不同的工作流程提供一個不同的交互回合狀態機?替換掉 turn-orchestrator,每個依賴它的工作單元都透過同一個匯流排呼叫 run::start 並讀取 turn_state,因此堆疊的其餘部分不需要改變。

實際的運作循環

一個交互回合的運作流程如下,依序說明每個工作單元如何被觸發。

瀏覽器 / CLI / 聊天介面透過 harness::trigger 發送一個包含 {session_id, message_id, payload} 的交互回合。harness 元工作單元將 payload 轉發給 run::start。這一步的存在是為了讓 OpenTelemetry 的跨度包裝器能夠將會話 ID 和訊息 ID 作為 baggage 進行傳播,而這個 baggage 會傳播給堆疊中每個工作單元內部的每一個巢狀 iii.trigger 呼叫。最後形成的追蹤樹是一個完整的連通圖。

run::start 到達 turn-orchestrator。它會持久化儲存執行請求,在 iii 狀態機中,於 session/<sid>/turn_state 初始化初始的 TurnStateRecord,然後立即返回。實際的工作發生在持久化的每個狀態機內部,由發布到 turn-step 先進先出佇列的事件喚醒。

兩個終端狀態是 stopped(透過 finishSession() 正常退出)和 failed(一個未預期的處理程序拋出錯誤時路由至此,會確認佇列訊息使其停止重試,並發出 message_complete{stop_reason:'error'} 加上 agent_end 事件,讓 UI 顯示原因)。拆卸(teardown)是一個內聯的 finishSession() 埠,從任何交互回合結束的路徑呼叫,而不是一個單獨排入佇列的步驟。

provisioning 做了三件事。如果執行需要隔離環境,它會啟動一個 iii-sandbox 微虛擬機。它會為 system_default_skills 中的每個命名空間(預設為 ["iii://iii-directory/index"])呼叫 directory::skills::download,以便 iii-directory 預先快取該執行啟動時所需的技能本體。它還會分三層組裝系統提示詞:一個從 run_request.modeplanaskagent)中選取的模式段落;告訴模型 agent_trigger 慣例和 directory::skills::get 按需發現模式的 iii 身份前文;以及 Agent 啟動時搭載的預設技能索引。呼叫者可以透過在 run::start 時傳遞 system_prompt 來覆蓋整個提示詞;否則由 turn-orchestrator 建構。函數的 JSON Schema 來自即時的引擎目錄。

assistant_streaming 會根據執行請求的 provider 欄位,呼叫對應提供者工作單元的 provider::<name>::stream。該提供者工作單元透過 auth::get_tokenauth-credentials)拉取憑證,將模型的 SSE 回應串流到一個 iii 通道中,然後 turn-orchestrator 從該通道中讀取數據,並在 agent::events 上發出 message_update 事件,供 UI 進行分發。通道的建立和讀取循環位於 provider-stream.ts 中一個基於拉取模式的 MessagePump 之後,因此串流狀態可以專注於狀態轉換。

當助手回應包含工具呼叫時,FSM 會進入 function_execute 狀態。每個工具呼叫都會通過 dispatchWithHook,這是 transform-orchestrator 中的單一瓶頸點。consultBefore 會直接呼叫 policy::check_permissions,並設定 5 秒超時。政策工作單元(在預設堆疊中即 harness 元工作單元)會讀取 iii-permissions.yaml,將呼叫的 function_id 與規則集進行比對,並返回三種結果之一:

  • allow:繼續分發;turn-orchestrator 觸發目標函數並寫入結果
  • deny:分發短路,返回一個 DenialEnvelope,結果成為一條拒絕記錄
  • needs_approval:該特定呼叫會被放入該交互回合的 awaiting_approval 列表中。批次中的其餘呼叫會繼續分發。只有當一個或多個項目處於待處理狀態時,交互回合才會轉換到 function_awaiting_approval 狀態。

核准喚醒是反應式的且是共享的。turn-orchestratorapprovals 範圍上註冊了一個 turn::on_approval 狀態觸發器。當控制台呼叫 approval::resolve 時,approval-gate 工作單元會將 approvals/<sid>/<cid> = {decision, reason} 寫入 iii 狀態。該寫入動作會觸發 turn::on_approval,從而推進受影響的會話。function_awaiting_approval 狀態只會讀取剛剛確定的決策,每收到一個就分發一個(allow 變成一個預先核准的分發,denyaborted 變成一個合成的拒絕),並在 awaiting_approval[] 清空時繼續推進。無需註冊每個呼叫的恢復函數。無需在啟動時重新掃描來恢復待處理的核准。一個觸發器即可涵蓋所有會話。

預設為關閉(fail-closed)的設計:如果政策工作單元無法連線或 5 秒超時,consultBefore 會返回一個 gate_unavailable 封包來拒絕該呼叫。如果 iii::durable::publish 本身發生錯誤,鉤子分發器會返回 publish_failed: trueturn-orchestrator 會將其視為拒絕。

這種架構帶來了一些延遲上的優勢。在函數呼叫後的鉤子中,當沒有持久化訂閱者註冊於該主題時,publish_collect 會透過一個訂閱者存在快取來短路操作,從而為每個執行的函數呼叫節省大約 500 毫秒。tearing_down 被內聯到 finishSession() 中,每個交互回合減少了持久化佇列的一次跳轉。context-compaction 訂閱了 turn-orchestrator 在交互回合邊界發出的一個專用 agent::turn_end 串流,因此壓縮器的喚醒是按交互回合而非按事件進行的。session-create 分發狀態觸發器僅根據範圍進行閘控並在進程內匹配,因此之前每次寫入時都會發生的 harness::session::is_create_event RPC 調用已經被移除。

批次完成後,steering_check 會決定是繼續、停止,還是已達最大交互回合數。如果繼續,則返回 assistant_streaming。如果停止或已達最大數,則內聯執行 finishSession():發出 agent_end,釋放沙箱,轉換到 stopped 狀態。

在整個執行過程中,每個參與的工作單元都會發出帶有 iii.session.idiii.message.idiii.function.id 標籤的 OpenTelemetry 跨度。這些標籤正是引擎的 engine::traces::group_by 用來在追蹤 UI 中填充「按會話分組」/「按訊息分組」/「按函數分組」選項的依據。這種儀器化是自動的:src/runtime/worker.ts 會用一個代理物件包裝每一個 registerFunction,這樣每個工作單元就不需要在自己的程式碼中記得手動添加跨度。

打造你自己的框架

有趣的部分在於,上述的工作單元沒有一個是特殊的。每一個都是一個開啟 WebSocket 連接到引擎、註冊一些函數和觸發器、然後運行的程序。這個合約與每個應用程式工作單元所使用的合約完全相同。這個框架建立在與你的業務邏輯相同的原語之上。

這意味著「打造你自己的框架」可以分解為與「編寫任何工作單元」相同的操作。你選擇要替換的層,編寫一個在匯流排上註冊相同函數的工作單元,使用 iii worker add 指令新增它,然後堆疊的其餘部分就會開始使用你的工作單元。

有兩個層次沒有出現在上面的工作單元表格中,但對於框架的行為至關重要。技能(Skills)是每個工作單元用來宣傳其函數功能的方式。每個工作單元都可以在 iii://<worker>/<function> 發布一個技能,Agent 在首次呼叫該函數之前,會透過 directory::skills::get 來獲取這個技能。系統提示詞則是在每個交互回合中,由一個模式段落、iii 身份前文以及該執行配置的預設技能本體組合而成。這兩者都是由匯流排驅動的:技能由 iii-directory 工作單元提供,系統提示詞則由 turn-orchestrator 組裝。兩者都是可替換的。

以下是五個具體的替換範例。

用即時 API 替換模型目錄。編寫一個註冊了 models::listmodels::getmodels::supports 的工作單元。讓它每隔 N 分鐘從你的提供者目錄端點獲取資料並進行快取。發布它。執行 iii worker add your-org/dynamic-models-catalog。停止靜態的 models-catalog 工作單元。turn-orchestrator 完全不會察覺到差異。它呼叫 iii.trigger('models::list'),引擎會將請求路由到最近註冊了該函數 ID 的工作單元。

新增一個新的提供者。 provider-kimiprovider-lmstudio 的工作單元已經證明了這種模式的可行性。每個都是一個工作單元,註冊了 provider::<name>::streamprovider::<name>::complete,將來自上游 API 的 SSE 串流導入一個 iii 通道,並將模型使用情況透過 budget::record 寫入 llm-budget。新增第五個提供者,就是編寫一個包含一個 iii.worker.yaml 和一個 register.ts 的資料夾。發布到註冊表,或者本地保留。turn-orchestrator 會根據互動回合的 provider 欄位選擇提供者;新的提供者在其工作單元連線的那一刻即可使用。

從私有構件儲存庫提供技能。編寫一個註冊了 directory::skills::getdirectory::skills::list 的工作單元,其後端是你內部的文檔系統或一個私有的 S3 儲存桶。斷開或重新命名預設的 iii-directory 工作單元。turn-orchestrator 的啟動過程會為每個命名空間呼叫 directory::skills::download;你的工作單元會回應。Agent「在呼叫新函數之前獲取該函數的技能」的模式會繼續不變地工作,因為線上的資料格式相同。

完全覆蓋系統提示詞。 run::start 接受一個可選的 system_prompt 欄位。傳遞這個欄位,turn-orchestrator 就會完整地使用你的字串,跳過模式段落 + 身份前文 + 技能附錄的組裝過程。當你有一個既有的提示詞資產,希望框架不加修改地使用時,這會很有用。即使使用自訂提示詞,技能下載仍然會在啟動過程中執行,因此 Agent 仍然可以透過 directory::skills::get 進行按需發現。

替換核准閘道的 UI 介面。預設的 approval-gate 工作單元註冊了 approval::resolve。線上的資料格式就是一個函數呼叫:

該處理程序會將 approvals/<sid>/<cid> = {decision, reason} 寫入 iii 狀態。turn-orchestrator 的單一 turn::on_approval 狀態觸發器會捕捉到這個寫入動作並喚醒正確的會話。如果你想透過 Slack 而非控制台來驅動核准,可以編寫一個 Slack 工作單元,監聽 /approve <id>/deny <id> 斜線指令,然後使用正確的 payload 呼叫 approval::resolveturn-orchestrator 完全不會察覺到差異。現有的 approval-gate 工作單元保持不變。你只是新增了一個新工作單元;你並沒有替換掉現有的那個。

如果你想要一個不同的政策引擎(OPA、Cedar、你自己的 DSL),編寫一個註冊了 policy::check_permissions 並返回 { decision, rule_id?, matched_constraint? } 的工作單元。斷開預設的政策工作單元(它被包裹在 harness 元工作單元內部,因此你可以停用該處理程序或執行一個精簡版的元工作單元)。turn-orchestratorconsultBefore 不會察覺到差異。同樣的 5 秒超時,同樣的預設為關閉語義,同樣的線上資料格式。

這些範例的重點不在於特定的替換內容,而在於操作的模式。在 iii 堆疊中,每一層框架都可以透過匯流排上的一個或兩個函數 ID 來觸及。替換一層就是編寫一個註冊這些 ID 的工作單元。系統的其餘部分保持不變。

框架是可調整的滑塊,而非只能二選一的分岔路

經典的框架辯論通常將其框架化為「薄型 vs 厚型」。Anthropic 的薄循環對比 LangGraph 的顯式 DAG。這種框架預設你必須選擇一方並接受其後果。

當框架是由共享同一個匯流排的多個工作單元組成時,「薄 vs 厚」就只是你安裝了多少個工作單元的計數問題。一個薄型框架只需要 turn-orchestrator 加上 provider-anthropic 加上 auth-credentials 再加上一個最小化的 harness 元工作單元。就這些。沒有核准、沒有預算、沒有政策引擎、沒有鉤子分發器。運行任何東西。信任模型。適用於自主研究 Agent、實驗性循環,或任何內部工具。

一個厚型框架則包含所有十三個工作單元,再加上 context-compaction、一個自訂的政策工作單元、一個自訂的核准閘道、一個整合了 Slack 的核准介面,以及執行每個工作區上限的預算工作單元。適用於運行客戶工作流程的 Agent,其中每個工具呼叫都需要可稽核,並且每個模型花費都必須匯總到財務儀表板。

薄型和厚型之間的架構距離並不是一次重寫。它只是一個配置變更。相同的線協定,相同的追蹤結構,相同的可觀測性方案。滑塊的移動方式是從你的 config.yaml 中加入或移除工作單元。其他一切都保持不變。

這種特性也適用於單個工作單元內部。turn-orchestrator 剛完成了一次重構,將其 FSM 從十一個狀態縮減為七個,刪除了每個呼叫的 turn::approval_resume::<sid>/<cid> 機制,改用在 approvals 範圍上的一個反應式 turn::on_approval 狀態觸發器,並將 tearing_down 內聯到一個 finishSession() 埠中。堆疊中的其他所有工作單元(approval-gatesessionllm-budgetprovidersmodels-catalogauth-credentialshook-fanoutcontext-compaction)都保持不變。approval::resolve 的線資料格式沒有變化。合約保持不變。這就是組合特性帶來的好處:對一個工作單元進行重大的內部重寫是一個自包含的變更,因為它所有的相鄰組件都透過匯流排層級的函數 ID 與它通信。

這是框架模型無法提供給你的部分。一個框架會為你選定滑塊上的一個位置,並將你鎖定在那裡。而工作單元模型則將滑塊的控制權交到你手中。

這在實踐中意味著什麼

如果你一直在某個框架之上運行 Agent,並且遇到了大多數團隊在規模化時都會遇到的相同邊界問題,那麼答案很可能不是「用我們自己的框架重寫這個框架」。政策引擎無法按你需要的方式擴展。核准 UI 被硬編碼到框架的聊天介面中。憑證儲存無法與你的金鑰管理服務通訊。預算追蹤器位於追蹤無法看到的輔助資料庫中。答案是切換到一個框架從一開始就被分解的基礎設施上。

最能讓你感受這個論點的方式是,複製 github.com/iii-hq/workers 倉庫,執行 pnpm installpnpm build,然後執行複合入口點。你會得到一個指向某個 iii 引擎的完整十四个工作單元框架。你可以從啟動清單中移除任何工作單元的條目來停用它。你可以透過編寫一個註冊相同函數 ID 的替換工作單元來替換任何一個。你可以透過為其鉤子主題新增一個訂閱者來擴展任何一個工作單元。hook-fanout::publish_collect 是每個 iii 鉤子所依賴的通用型。

文件位於 iii.dev/docs。引擎位於 github.com/iii-hq/iii。工作單元註冊表位於 workers.iii.dev。框架套件位於 github.com/iii-hq/workers/harness

核心賭注

一個框架並非你安裝的一個東西。一個框架是你的系統為了讓一個 Agent 能夠持久、安全且可觀測地運行所必須完成的一系列工作。框架時代將這些工作捆綁在一起,是因為底層沒有任何東西提供組合它們的方式。

iii 的賭注是,一個單一原語:一個透過 WebSocket 連接到引擎並註冊函數和觸發器的工作單元,它足夠小,可以分別吸收上述的每一項工作;而由此產生的堆疊比任何框架都更有用,因為每一層都可以獨立替換。

你並非「採用」iii 框架。而是安裝你想要的 Worker,編寫你需要的 Worker,最終得到一個完全符合你系統形狀的框架。每一層都使用相同的協定。每一次呼叫都共享相同的追蹤。對於你從註冊表取用的部分與你自己發布的部分,都使用相同的 iii worker add 指令。

這就是當底層基礎設施形狀正確時,「打造你自己的 Agent 框架」的真實樣貌。挑選工作單元。編寫缺少的。組合。這個框架本身就是組合的結果。

加入我們,共同打造現代世界所需的完美 Agent 框架:discord.gg/iiidev

iii 是開源的。從 iii.dev/docs 開始。框架工作單元位於 github.com/iii-hq/workers,引擎位於 github.com/iii-hq/iii

— Mike Piccolo,Founder & CEO @iiidevs

一鍵儲存

使用 YouMind AI 深度閱讀爆款文章

保存原文、追問細節、總結觀點,並在一個 AI 工作空間裡把爆款文章沉澱成可複用筆記。

了解 YouMind
寫給創作者

把你的 Markdown 變成乾淨的 𝕏 文章

圖片上傳、表格、程式碼區塊,往 𝕏 上手動重排太痛苦。YouMind 把整篇 Markdown 一鍵轉成乾淨、可直接發佈的 𝕏 文章草稿。

試試 Markdown 轉 𝕏

更多可拆解樣本

近期爆款文章

探索更多爆款文章