建立單步驟的 AI 操作很簡單:接收輸入、呼叫 LLM、回傳輸出。但建立一個支援分支、迴圈、平行執行、人工審批關卡和自動重試的多步驟工作流引擎,則是完全不同層次的問題。
本文將介紹 JieGou 工作流引擎的架構 — 執行模型、8 種步驟類型、步驟間的資料流動方式、審批如何暫停和恢復執行,以及確保一切可靠運作的安全機制。
執行模型
工作流是一個由步驟組成的有向圖。執行從 executeWorkflow() 開始,它會建立 WorkflowRun 記錄、建立共享的 StepExecutionContext,並呼叫 executeStepList() 來依序處理步驟。
執行環境(context)攜帶一個 previousStepOutputs Map — 這是一個鍵值對儲存空間,每個完成的步驟會將其輸出存放在這裡,供下游步驟參考。這是資料流動的主幹。步驟 B 可以使用像 {{step.stepA.fieldName}} 這樣的範本語法來參考步驟 A 的輸出。
每個工作流都有可配置的逾時設定(預設 5 分鐘),透過執行環境中的 deadlineMs 欄位來強制執行。個別步驟也有自己的逾時設定(預設 60 秒),由 withStepTimeout() 包裝器強制執行。
8 種步驟類型
Recipe 步驟
主力步驟。透過 executeRecipe() 執行可重複使用的提示詞範本,並將解析後的輸出儲存在 previousStepOutputs 中。輸入映射會解析對工作流輸入、先前步驟輸出、靜態值或迴圈項目的參考。
條件步驟
評估布林表達式,並遞迴執行 thenSteps 或 elseSteps。這是真正的分支 — 兩條路徑都可以包含任何步驟類型,包括巢狀條件。引擎會對選中的分支遞迴呼叫 executeStepList()。
迴圈步驟
遍歷一個集合,並為每個項目執行一系列子步驟。集合可以來自 4 個來源:工作流中定義的靜態陣列、先前步驟的輸出、工作流輸入欄位,或父迴圈項目。
每次迭代都有自己的 loopContext Map,因此子步驟可以透過 {{loop_item.fieldName}} 參考當前項目。迭代結果會以陣列形式儲存,並包含巢狀的 stepRuns 以提供可觀測性。
平行步驟
透過 Promise.allSettled() 同時執行多個分支。每個分支都是獨立的步驟列表,擁有自己的 stepRuns 陣列。當多個獨立操作可以同時執行時,這非常有用 — 例如,同時從三個不同的資料來源豐富潛在客戶資料。
審批步驟
架構上最有趣的步驟類型。當執行到達審批步驟時,會拋出一個 ApprovalPauseError。這是一個受控異常 — 不是崩潰。
這個錯誤會在頂層被捕獲,WorkflowRun 會以 pending_approval 狀態持久化,並透過電子郵件通知合格的審批者。執行完全停止。不會佔用任何資源。
當審批者採取行動(透過 API 批准或拒絕)時,resumeWorkflowFromApproval() 會載入已持久化的執行記錄,呼叫 reconstructPreviousOutputs() 從已儲存的 stepRuns 重建 previousStepOutputs Map,並從審批關卡後的下一步恢復執行。
重建是遞迴的 — 它會遍歷 thenStepRuns、elseStepRuns、iterations[] 和 branchStepRuns[] 來恢復每個巢狀輸出。這意味著即使審批位於條件分支或迴圈迭代內部,也能正確運作。
寫入知識庫步驟
捕獲步驟的輸出並將其寫入知識庫文件。這使得工作流能夠建立組織知識 — 例如,客戶支援分流工作流可能會將解決方案摘要寫入知識庫,供未來執行透過 RAG 參考。
移交步驟
透過電子郵件和應用程式內通知來通知目標部門的使用者。對於執行來說這是一個空操作 — 它不產生輸出也不阻塞流程。對於需要提醒人工介入但工作流應該繼續執行的升級流程很有用。
瀏覽器動作步驟
透過瀏覽器擴充功能執行 MCP 工具呼叫。從 MCP 連線池中獲取客戶端,解析範本參數(工作流輸入欄位和先前步驟輸出),並回傳工具結果。
資料流動:範本解析
步驟透過在執行時解析的範本語法相互參考:
{{workflow_input.fieldName}}— 參考工作流的輸入資料{{step.stepId.path.to.value}}— 使用點號表示法參考先前步驟的輸出{{loop_item.fieldName}}— 參考迴圈迭代中的當前項目
解析器使用 getNestedValue() 進行點號表示法的路徑遍歷,處理陣列和巢狀物件。輸入映射會明確宣告其來源:workflowInput、previousStep、static 或 loopItem。
重試與錯誤處理
並非所有失敗都是永久性的。速率限制、暫時性 5xx 錯誤、逾時和連線失敗都是可重試的。客戶端驗證錯誤(4xx)則不可重試。
重試策略使用帶抖動的指數退避:Math.min(30000, 2000 * 2^attempt + random jitter)。大約是 2 秒、4 秒、8 秒、16 秒,上限為 30 秒,並加上隨機抖動以避免驚群問題。預設最大嘗試次數為 3 次,每個步驟可配置。
每次重試都會記錄嘗試索引和錯誤詳情。如果所有嘗試都失敗,步驟會被標記為失敗,工作流會終止(除非工作流層級的錯誤處理另有規定)。
並發控制
每個帳戶透過 Redis 信號量限制為 10 個並發 LLM 呼叫。這可以防止批次工作流壟斷供應商連線。
MCP 連線使用帶 LRU 淘汰的連線池(最多 20 個連線,60 秒閒置逾時)。連線池處理連線設定、保持活動和清理。
兩個系統在 Redis 錯誤時都使用開放失敗語義 — 降級的快取層絕不應阻止工作流執行。
可觀測性
三層可觀測性同時運作:
Prometheus 指標追蹤按狀態分類的 workflowExecutionsTotal 和 workflowDuration。這為成功率和延遲百分位數提供操作儀表板。
OpenTelemetry spans 建立具有工作流層級和步驟層級 span 的追蹤階層,攜帶工作流 ID、名稱和步驟中繼資料。這些可以與任何相容 OTel 的後端整合。
執行追蹤是一個自訂的階層式 span 樹,透過 fire-and-forget 方式持久化到 Firestore。這些為 UI 中的詳細執行檢查器提供動力,精確顯示每個步驟發生的事情,包括輸入、輸出、計時和重試嘗試。
執行前檢查
在執行開始之前,引擎會執行驗證:
- 輸入架構驗證 — 工作流輸入會根據宣告的架構進行檢查
- 映射驗證 — 驗證所有步驟輸入映射(參考的步驟存在,路徑合理)
- MCP 驗證 — 如果任何步驟需要瀏覽器動作,引擎會預先驗證 MCP 客戶端連線,而不是在執行中途失敗
- 自動上下文解析 — 從工作流、部門和明確 ID 來源解析知識庫文件,因此在第一步執行之前 RAG 上下文就已準備就緒
經驗教訓
**將審批關卡視為異常簡化了架構。**我們的第一個設計使用帶有明確暫停/恢復狀態的狀態機。基於異常的方法更簡潔 — 工作流向前執行直到遇到審批,拋出異常,持久化,然後停止。恢復時重建狀態並繼續。無需管理複雜的狀態轉換。
**遞迴輸出重建值得其複雜性。**從持久化的步驟執行記錄重建 previousStepOutputs 需要遍歷每個巢狀層級 — 條件、迴圈、平行分支。這是複雜的程式碼,但這意味著審批在任何巢狀深度都能正確運作,無需特殊處理。
**每個步驟的逾時防止級聯延遲。**單個緩慢的 LLM 呼叫不應該耗盡整個工作流的逾時時間。預設的 60 秒單步逾時能及早捕獲掛起的請求,而工作流層級的逾時則作為最後防線。
**Fire-and-forget 可觀測性保持熱路徑的快速。**執行追蹤、webhook 傳遞和目標輸出都在主執行完成後非同步分派。使用者無需等待可觀測性寫入完成就能獲得結果。