
在大多數 Hello World 等級的 Agent 教學中,工作流永遠都是「線性」的:使用者發起請求 $\rightarrow$ LLM 呼叫三個工具 $\rightarrow$ 回傳最終結果,整個過程耗時 3 秒完成。
然而在真實企業級業務場景中,中斷、等待與跨時區人機協作才是常態:
Thread.sleep 阻塞連線。如果你的 Agent 無法在等待時「安全保存狀態」並在人類回覆時「精準喚醒恢復」,系統就會面臨連線超時中斷、記憶體洩漏或上下文遺失的毀滅性打擊。今天我們就來拆解 Embabel 的 @State、Stage 狀態機與 WaitFor 人機協同機制。
@State 的中斷與喚醒狀態機
Embabel 透過將流程劃分為不同的 Stage(階段狀態),並結合 WaitFor 掛起語義,實現優雅的人機協作:

WaitFor 掛起流程,狀態序列化存入 Redis/DB,釋放連線與執行緒。DoneStage 產出最終 ReviewedOffer (Goal)。ReviseStage 結合主管回饋重新調整草稿並迴圈重製。@State 註解與 Stage 標記介面在 Embabel 中,狀態不是一個字串變數,而是具備行為與資料邊界的 Java Record / Class:
@State 的 sealed interface(例如 Stage)。DraftStage、AssessStage、DoneStage)各自實作該介面。@Action。WaitFor 掛起機制(Suspension Primitive)WaitFor 是 Embabel 專門為 Human-in-the-Loop 設計的掛起原語:
WaitFor.formSubmission(prompt, FormType.class):掛起當前 Process,發出事件要求外部提供指定表單物件。WaitFor.userInput(prompt):掛起等待單純的自然語言補充。WaitFor 時,會自動將當前 Blackboard 的狀態持久化(Persistence),並結束當前執行緒。clearBlackboard 與狀態遷移(State Transition)當 Action 回傳一個新的 Stage 時,代表狀態發生遷移。若在 @Action(clearBlackboard = true) 標記,引擎會自動清理上一階段的暫存雜訊,只保留新 Stage 物件中明確攜帶的上下文,有效防止上下文膨脹(Context Window Explosion)。
以下我們實作一個完整的「方案生成 $\rightarrow$ 人工審查 $\rightarrow$ 判定通過或重製」的狀態機流程。
package com.antechinus.travel.state;
import com.antechinus.travel.domain.CustomerQuery;
import com.antechinus.travel.domain.OfferDraft;
import com.antechinus.travel.domain.ReviewedOffer;
import com.embabel.agent.annotation.AchievesGoal;
import com.embabel.agent.annotation.Action;
import com.embabel.agent.annotation.State;
import com.embabel.agent.api.Ai;
import com.embabel.agent.api.WaitFor;
/**
* 流程階段狀態標記介面
*/
@State
public sealed interface CareFlowStage
permits DraftStage, AssessStage, ReviseStage, FinalDoneStage {}
/**
* 人工主管審核回饋物件
*/
public record SupervisorFeedback(
boolean approved,
String rejectionReason,
int maxAllowedDiscount
) {}
package com.antechinus.travel.state;
import com.antechinus.travel.domain.CustomerQuery;
import com.antechinus.travel.domain.OfferDraft;
import com.antechinus.travel.domain.ReviewedOffer;
import com.embabel.agent.annotation.AchievesGoal;
import com.embabel.agent.annotation.Action;
import com.embabel.agent.api.Ai;
import com.embabel.agent.api.WaitFor;
import java.time.Instant;
/**
* 階段 1:初稿生成完畢,準備進入審查
*/
public record DraftStage(CustomerQuery query, OfferDraft draft) implements CareFlowStage {
/**
* 發起人工審查 Action
* 觸發流程掛起,等待外部 SupervisorFeedback 表單提交
*/
@Action
public CareFlowStage initiateReview() {
if (draft.discountPercent() <= 15) {
// 小額折扣免審批,直接進入完成階段
return new FinalDoneStage(query, draft, "系統自動通過");
}
// 大額折扣掛起等待主管審批
return new AssessStage(query, draft);
}
}
/**
* 階段 2:等待與處理人工審查回饋
*/
public record AssessStage(CustomerQuery query, OfferDraft draft) implements CareFlowStage {
/**
* 等待人工輸入 Action
*/
@Action
public SupervisorFeedback awaitSupervisor() {
return WaitFor.formSubmission(
"優惠折扣達 " + draft.discountPercent() + "%,請主管審核並提供回饋",
SupervisorFeedback.class
);
}
/**
* 收到主管回饋後的狀態裁決 Action
* 清理黑板雜訊,根據審核結果轉移至 完成 或 重製 階段
*/
@Action(clearBlackboard = true)
public CareFlowStage decide(SupervisorFeedback feedback, Ai ai) {
if (feedback.approved()) {
return new FinalDoneStage(query, draft, "主管核准: " + feedback.rejectionReason());
}
// 未通過審核,轉移到修訂階段
return new ReviseStage(query, draft, feedback);
}
}
/**
* 階段 3:方案修訂階段 (可由 LLM 根據回饋重新生成)
*/
public record ReviseStage(
CustomerQuery query,
OfferDraft previousDraft,
SupervisorFeedback feedback
) implements CareFlowStage {
/**
* 依據審核意見重新調整方案 Action
*/
@Action
public CareFlowStage reviseDraft(Ai ai) {
// 使用 LLM 結合主管意見進行草稿微調
String prompt = String.format(
"請根據主管意見修訂方案。原方案: %s,主管要求最高折扣不可超過 %d%%,理由: %s",
previousDraft.description(), feedback.maxAllowedDiscount(), feedback.rejectionReason()
);
OfferDraft newDraft = ai.withDefaultLlm().createObject(prompt, OfferDraft.class);
// 修正後回到 Draft 階段重新評估
return new DraftStage(query, newDraft);
}
}
/**
* 階段 4:完成階段,產出最終 Goal 物件
*/
public record FinalDoneStage(
CustomerQuery query,
OfferDraft finalDraft,
String approvalNote
) implements CareFlowStage {
/**
* 達成終端目標 Action
*/
@AchievesGoal(description = "產出最終審核完畢的優惠方案")
@Action
public ReviewedOffer complete() {
return new ReviewedOffer(
query.customerId(),
finalDraft.discountPercent(),
finalDraft.description(),
"APPROVED",
approvalNote,
Instant.now()
);
}
}
package com.antechinus.travel.web;
import com.antechinus.travel.state.SupervisorFeedback;
import com.embabel.agent.api.EmbabelClient;
import com.embabel.agent.api.ProcessResult;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
/**
* 人機協同外部喚醒端點
* 接收主管審批結果並喚醒掛起的 Agent 流程
*/
@RestController
@RequestMapping("/api/agent/care")
public class AgentApprovalController {
private final EmbabelClient embabelClient;
public AgentApprovalController(EmbabelClient embabelClient) {
this.embabelClient = embabelClient;
}
/**
* 接收主管審批回呼 Webhook
*
* @param processId 掛起的流程識別碼
* @param feedback 主管審查表單
*/
@PostMapping("/processes/{processId}/approve")
public ResponseEntity<String> resumeProcess(
@PathVariable String processId,
@RequestBody SupervisorFeedback feedback) {
// 喚醒掛起的進程並注入外部資料
ProcessResult<?> result = embabelClient.resume(processId, feedback);
if (result.isSuccess()) {
return ResponseEntity.ok("流程已成功喚醒並推進至下一階段");
} else {
return ResponseEntity.internalServerError().body("流程恢復失敗: " + result.getErrorMessage());
}
}
}
Thread.sleep 或輪詢 DB 來等人
WaitFor 原語,將狀態持久化後立即返回並釋放 Thread,完全依賴非同步 Webhook 事件喚醒。clearBlackboard = true,僅將必要的參數透過新 Stage 的構造函式傳遞。AssessStage。TimeoutFallbackStage(例如降級為預設 10% 通用優惠)。| 評估維度 | ❌ 傳統線性寫法 (Bad) | ✅ Embabel @State 狀態機 (Good) |
|---|---|---|
| 人機協同支援 | 只能一次性跑完,無法中途停下來等人 | 支援 WaitFor 非同步掛起與 Webhook 喚醒 |
| 狀態保存機制 | 塞在全局 HashMap 或 ThreadLocal(重啟即丟) | 強型別 Record 狀態容器,天然支援 DB/Redis 序列化 |
| 修訂迴圈控制 | 容易寫出 while(true) 死循環耗盡預算 |
明確的 ReviseStage $\rightarrow$ DraftStage 狀態遷移軌跡 |
| 架構清晰度 | 邏輯被大量 if (status == 3) 瓜分破碎 |
每個 Stage 封裝專屬 Action,符合物件導向狀態模式 |
InitialStage、WaitingApprovalStage、CompletedStage)。WaitFor.formSubmission 回傳等待結構,並觀察流程如何被暫停。