Skip to content
Embabel 文档版本 0.3.0-SNAPSHOT 源快照 215e072c 更新于 2026-09-29

22 · 规划器(Planners)

选择规划器(Choosing a Planner)

Embabel 支持多种规划策略。它们大多是确定性的,但行为各不相同 —— 不过永远是可预测的。

所有规划策略在 Java 或 Kotlin 中都是完全类型安全的。

开箱即用、目前支持的规划策略有:

规划器最适合说明
GOAP(默认)有明确产出的业务流程目标导向的确定性规划。用前置条件与效果,规划出一条从当前状态到目标的路径。
Utility探索型、事件驱动型系统每一步都选择"可用动作中价值最高"的那个。适合你还不知道最终结果会是什么的场景。
HybridReducer 式流水线(先收集一堆产出上下文的动作,再跑一个合成器,然后停止)动作挑选方式与 Utility 相同,但只要任一注册目标已被满足就立刻退出。把"不可满足的目标"(如 NIRVANA)与真正的终结目标配对使用:研究仍有收益时机会性研究就会不断触发;而真正目标一达成,进程立即完成。
Supervisor灵活的多步工作流由 LLM 编排的组合方式。LLM 基于类型 schema 与已收集的产物,选择调用哪些动作。

由于大部分文档都在讲 GOAP,本节讨论替代规划器与嵌套工作流。

Utility AI

Utility AI 在每一步都从所有可用动作中选出净价值最高的那个。 与 GOAP 规划出一条通往目标的路径不同,Utility AI 基于即时价值做贪心决策。

Utility AI 在探索型场景中表现出色 —— 也就是你并不确切知道自己想达成什么的时候。 比如一个 GitHub issue 分类系统:新 issue 到来时你并没有预定目标, 而是想根据 issue 的特征做出恰当反应 —— 也许打标签,也许回复,也许升级。 "正确的"动作取决于你在处理过程中发现了什么。

因此 Utility AI 特别适合以下场景:

  • 没有明确的终点目标 —— 你在探索可能性;
  • 视上下文而定,多个动作都可能有价值;
  • 你希望随着情况变化即时响应;
  • 最优结果无法事先得知。

何时使用 Utility AI

  • 事件驱动系统:对进来的事件(issue、star、webhook)做出最恰当的反应;
  • 聊天机器人:平台提供多个回复选项并挑出最好的那个;
  • 探索:当你想发现"能做什么",而不是达成某个特定目标时。

通过 @EmbabelComponent 使用 Utility AI

使用 Utility AI 时,动作通常由 @EmbabelComponent 提供,而不是 @Agent。 这样平台就能跨多个组件按效用挑选动作,而不是把动作限制在单个 Agent 内。

下面这个例子来自 Shepherd 项目,用于响应 GitHub 事件:

java
@EmbabelComponent // ①
public class IssueActions {

    private final ShepherdProperties properties;
    private final CommunityDataManager communityDataManager;
    private final GitHubUpdater gitHubUpdater;

    public IssueActions(ShepherdProperties properties,
                        CommunityDataManager communityDataManager,
                        GitHubUpdater gitHubUpdater) {
        this.properties = properties;
        this.communityDataManager = communityDataManager;
        this.gitHubUpdater = gitHubUpdater;
    }

    @Action(outputBinding = "ghIssue") // ②
    public GHIssue saveNewIssue(GHIssue ghIssue, OperationContext context) {
        var existing = communityDataManager.findIssueByGithubId(ghIssue.getId());
        if (existing == null) {
            var issueEntityStatus = communityDataManager.saveAndExpandIssue(ghIssue);
            context.add(issueEntityStatus); // ③
            return ghIssue;
        }
        return null; // ④
    }

    @Action(
            pre = {"spel:newEntity.newEntities.?[#this instanceof T(com.embabel.shepherd.domain.Issue)].size() > 0"} // ⑤
    )
    public IssueAssessment reactToNewIssue(GHIssue ghIssue, NewEntity<?> newEntity, Ai ai) {
        return ai
                .withLlm(properties.getTriageLlm())
                .creating(IssueAssessment.class)
                .fromTemplate("first_issue_response", Map.of("issue", ghIssue)); // ⑥
    }

    @Action(pre = {"spel:issueAssessment.urgency > 0.0"}) // ⑦
    public void heavyHitterIssue(GHIssue issue, IssueAssessment issueAssessment) {
        // 对高紧急度 issue 采取行动
    }
}

注记:

  • ① @EmbabelComponent 把动作贡献给平台,而不是某个特定 Agent;
  • ② outputBinding 给结果命名,供后续动作引用;
  • ③ 把实体状态加入 context,使后续动作可以使用;
  • ④ 返回 null 阻止后续动作对该 issue 触发;
  • ⑤ SpEL 前置条件:只有确实新建了 issue 时才触发;
  • ⑥ 通过模板用 AI 评估该 issue;
  • ⑦ 只有评估显示 urgency > 0 时该动作才触发。

平台依据以下两点来选择运行哪个动作:

  • 哪些前置条件已满足(类型可用性 + SpEL 条件);
  • @Action 上的 cost 与 value 参数(净价值 = value − cost)。

动作的成本与价值(Action Cost and Value)

@Action 注解支持 cost 与 value 两个参数(都在 0.0 ~ 1.0 之间):

java
@Action(
        cost = 0.1,  // ①
        value = 0.8  // ②
)
public Output highValueAction(Input input) {
    // 动作实现
}

① 执行成本(0.0 ~ 1.0)—— 越低越便宜 ② 执行后的价值(0.0 ~ 1.0)—— 越高越有价值

Utility 规划器按 value - cost 计算净价值,并从所有可用动作中挑选净价值最高的那个。

Nirvana 目标(The Nirvana Goal)

Utility AI 支持一个特殊的 "Nirvana" 目标,它永远不会被满足。 这让进程持续运行,不断选出价值最高的可用动作,直到没有任何动作可用为止。

可扩展性(Extensibility)

Utility AI 有利于可扩展性。例如:组织内的多个团队可以各自贡献自己的 @EmbabelComponent 类, 用自己专业领域的动作来增强围绕共享类型的行为,同时仍然保有并掌控自己扩展出来的模型。

Utility 与状态(Utility and States)

Utility AI 可以与 @State 注解结合,实现分类 + 路由模式。这在以下情况下尤其有用:

  • 需要在运行时把输入分类到不同类别;
  • 需要把处理路由到各类别专属的处理器;
  • 需要依据分类达成不同的目标。

关键模式是:

  • 一个入口动作负责分类,并返回一个 @State 类型;
  • 每个 @State 类里包含一个 @AchievesGoal 动作,产出最终输出;
  • @AchievesGoal 的产出不是 @State 类型(以防止无限循环)。

下面是一个按严重程度路由工单的示例:

java
@Agent(
        description = "Triage and process support tickets",
        planner = PlannerType.UTILITY // ①
)
public class TicketTriageAgent {

    public record Ticket(String id, String description, String customerId) {}

    public record ResolvedTicket(String id, String resolution, String handledBy) {}

    @State
    public sealed interface TicketCategory permits CriticalTicket, BugTicket, GeneralTicket {} // ②

    @Action
    public TicketCategory triageTicket(Ticket ticket) { // ③
        if (ticket.description().toLowerCase().contains("down")) {
            return new CriticalTicket(ticket);
        } else if (ticket.description().toLowerCase().contains("bug")) {
            return new BugTicket(ticket);
        } else {
            return new GeneralTicket(ticket);
        }
    }

    @State
    public record CriticalTicket(Ticket ticket) implements TicketCategory {
        @AchievesGoal(description = "Handle critical ticket with immediate escalation") // ④
        @Action
        public ResolvedTicket handleCritical() {
            return new ResolvedTicket(
                    ticket.id(),
                    "Escalated to on-call engineer",
                    "CRITICAL_RESPONSE_TEAM"
            );
        }
    }

    @State
    public record BugTicket(Ticket ticket) implements TicketCategory {
        @AchievesGoal(description = "Handle bug report")
        @Action
        public ResolvedTicket handleBug() {
            return new ResolvedTicket(
                    ticket.id(),
                    "Bug logged in issue tracker",
                    "ENGINEERING_TEAM"
            );
        }
    }

    @State
    public record GeneralTicket(Ticket ticket) implements TicketCategory {
        @AchievesGoal(description = "Handle general inquiry")
        @Action
        public ResolvedTicket handleGeneral() {
            return new ResolvedTicket(
                    ticket.id(),
                    "Response sent with FAQ links",
                    "SUPPORT_TEAM"
            );
        }
    }
}

注记:

  • ① 用 PlannerType.UTILITY 进行机会性动作选择;
  • ② 用 sealed interface 作为状态的父类型;
  • ③ 入口动作分类并返回一个 @State 实例;
  • ④ 每个状态都有一个 @AchievesGoal 动作产出最终输出。

处理一张 Ticket 时:

  • triageTicket 动作把它分到某个状态类型;
  • 进入某个状态会清空黑板上的其它对象;
  • Utility 规划器选择该状态对应的 @AchievesGoal 动作;
  • 当 ResolvedTicket 被产出时,目标达成。

这个模式适合以下情况:

  • 分类结果决定了处理路径;
  • 每个类别有各自不同的处理要求;
  • 所有类别的最终输出类型是同一个。

UtilityInvocation:轻量级 Utility 模式

对于简单的 utility 工作流,你不需要创建 @Agent 类。 UtilityInvocation 提供了流式 API,可以直接从 @EmbabelComponent 的动作里运行基于 utility 的工作流。

用 UtilityInvocation 调用:

java
UtilityInvocation.on(agentPlatform)
        .withScope(AgentScopeBuilder.fromInstances(issueActions, labelActions))
        .run(new GHIssue(issueData));

配置项:

方法说明
.withScope(AgentScopeBuilder)定义哪些动作可用
.withAgentName(String)为创建出的 Agent 设置自定义名称(默认取平台名)
.withProcessOptions(ProcessOptions)配置进程级选项
.terminateWhenStuck()加上"无动作可用时提前终止"的策略

设置自定义 Agent 名称:

java
UtilityInvocation.on(agentPlatform)
        .withScope(AgentScopeBuilder.fromInstance(myActions))
        .withAgentName("issue-triage-agent")
        .run(input);

Hybrid

Hybrid 规划器把 Utility AI 的"按价值挑动作"与"目标已满足即终止"结合起来 —— 也就是**"迭代,然后停下"**模式。 它专为 reducer 风格流水线而生:

  • 你希望按 netValue 顺序触发大量机会性的、产出上下文的动作(研究查询、知识图谱查询、信息补全);
  • 你希望随后有一个单一的合成动作运行,产出终结结果;
  • 你希望这一终结结果一上黑板进程就停下,而不是继续触发那些低价值甚至负价值的剩余动作。

为什么需要单独的规划器

GOAP 会最小化通往目标的路径成本;它会跳过不在最便宜计划上的机会性动作,因此研究类动作永远不会触发。 纯 Utility AI 是贪心的单步策略:当它与一个可满足的终结目标配对时,如果没有任何单个动作能一步达成目标,它会在第 1 步就放弃。 纯 Utility AI 配 NIRVANA 则迭代得很漂亮但永不终止 —— 即使真实目标已经满足,它仍会继续挑动作,把算力烧在通往虚无的路上。

Hybrid 就是那个缺失的中间态:它每个 tick 都挑净价值最高的可达成动作(所以研究会发生), 并在选动作之前先检查"目标是不是已经满足了"(所以一旦真实目标达成,终止就很干净)。

双目标模式(The Two-Goal Pattern)

Hybrid 规划器的设计意图,是在同一个 Agent 上放两个目标:

  • 真正的终结目标 —— 成功长什么样(例如 attention-candidate-produced、report-generated);
  • com.embabel.agent.core.support.NIRVANA —— 框架内置的不可满足目标, 用来在"研究仍有收益"期间维持 Utility 式的动作挑选。

每个 tick 规划器会为每个目标各生成一个计划。NIRVANA 返回净价值最高的可达成动作,作为 1 步计划。 真实目标返回:

  • 空计划(netValue = 0)—— 如果目标已经满足;此时它胜过 NIRVANA 那时只剩负价值的剩余动作;
  • 1 步计划 —— 如果存在单个动作就能达成目标;
  • null —— 其余情况。

宿主进程在所有目标之间挑选净价值最高的计划。当研究仍有收益时,NIRVANA 的计划胜出; 而真实目标一旦满足,它的空计划胜出,plan.isComplete() 触发,进程终止。

何时使用 Hybrid

  • 按信号分类的流水线:在最后一次评估用 LLM 调用之前,先收集多个上下文来源;
  • "先研究、再合成"的工作流:多个动作向黑板贡献带类型的产物,最后一个动作消费全部内容;
  • 按包可扩展的 reducer:可以新增机会性动作而无需重写合成步骤 —— 仅凭它们自身的价值就能被调度。

不使用 Hybrid 的情况:如果你确实想要一个没有终结目标的长跑事件循环(聊天界面、探索型分类),就用纯 Utility AI。 改用 GOAP 的情况:如果你有严格的类型依赖顺序,且不需要机会性研究。

调参(Tuning Values)

要让 hybrid 模式按预期工作:

  • 研究类动作(机会性的上下文生产者)应当有高净价值,这样在它们的 canRerun=false 名额还空着时能赢得挑选竞争;
  • 合成器(产出终结输出的最后动作)应当有正的但较低的净价值 —— 它在研究被锁死后胜出;
  • 收尾类动作(把结论提升为目标输出的那些)价值应略高于合成器,这样它们紧随其后触发。

一旦收尾动作写下终结产物,真实目标的空计划就会胜出,进程随即退出 —— 不管还剩下多少负价值动作。

使用 Hybrid

kotlin
import com.embabel.agent.core.Agent
import com.embabel.agent.core.Goal
import com.embabel.agent.core.IoBinding
import com.embabel.agent.core.support.NIRVANA

// Agent 同时声明 NIRVANA 与真实目标。
val agent = Agent(
    name = "per-signal-triage",
    provider = "example",
    version = "0.0.1",
    description = "Reducer pipeline: research → assess → wrap.",
    actions = listOf(/* … */),
    goals = setOf(
        NIRVANA, // ①
        Goal(    // ②
            name = "attention-candidate-produced",
            description = "An AttentionCandidate has been produced.",
            inputs = setOf(IoBinding("attentionCandidate", AttentionCandidate::class.java.name)),
            outputType = null,
        ),
    ),
)

// 让进程使用 PlannerType.HYBRID。
val process = agentPlatform.runAgentFrom(
    agent = agent,
    processOptions = ProcessOptions(plannerType = PlannerType.HYBRID), // ③
    bindings = signalBindings,
)

注记:

  • ① NIRVANA 让"按净价值迭代挑选"在多个 tick 之间持续生效;没有它,如果没有任何单个动作能达成真实目标,规划器会在第 1 步就放弃;
  • ② 真正的终结目标 —— 决定进程何时停止。一旦满足就返回空计划;
  • ③ 通过 ProcessOptions 按进程选择启用。其它进程的默认值(GOAP)不受影响。

与 UTILITY 的差异

场景UTILITYHYBRID
真实目标已满足、没有可达成动作空计划(终止)空计划(终止)
真实目标已满足、但仍有其它可达成动作挑净价值最高的动作并执行空计划(终止)—— 这是关键的修正
真实目标未满足、存在 1 步可达路径1 步计划1 步计划
真实目标未满足、不存在 1 步路径null(规划器放弃)null(规划器放弃 —— 由配对的 NIRVANA 负责迭代)
NIRVANA 目标净价值最高的可达成动作;无可用动作时为 null语义与 UTILITY 完全相同

HYBRID 就是 UTILITY 加了一个额外检查:如果真实目标已经满足,就不管还剩哪些可达成动作,一律返回空计划。 正是这一点让双目标模式能够干净地终止。

Supervisor

Supervisor 规划器用 LLM 动态编排动作。 这是 LangGraph、Google ADK 等框架中流行的模式:由一个 supervisor LLM 决定调用哪些工具、以什么顺序调用。

与 GOAP 和 Utility 不同,Supervisor 规划器是非确定性的。 同一个输入,LLM 可能选出不同的动作序列。这使它不太适合要求可复现的关键业务流程。

类型告知 vs 类型驱动(Type-Informed vs Type-Driven)

supervisor 架构的一个关键设计决策是:类型与组合方式之间是什么关系。

方式说明
类型驱动(Type-Driven,GOAP)类型约束组合。一个需要 MarketData 的动作,只能在一个产出 MarketData 的动作之后运行。确定,但僵化。
类型告知(Type-Informed,Supervisor)类型为组合提供信息。LLM 看到类型 schema,基于语义理解决定调用什么。灵活,但非确定。

Embabel 的 Supervisor 规划器采取类型告知路线,同时把类型的好处最大化:

  • 动作返回的带类型输出会被校验;
  • LLM 能看到类型 schema,从而理解每个动作产出什么;
  • 结果存放在带类型的黑板上,供后续动作使用;
  • 同一批动作可以配任何规划器(GOAP、Utility 或 Supervisor)。

这就是**"类型化 supervisor"模式** —— 处于完全类型驱动(GOAP)与非类型化字符串传递(典型 LangGraph)之间的中间地带。

何时使用 Supervisor

适合:

  • 动作顺序依赖上下文,很难预先定义;
  • 你想让 LLM 跨多个来源综合信息;
  • 工作流受益于灵活组合而非严格顺序;
  • 非确定性在你的场景里可以接受。

不建议:

  • 你需要可复现、可审计的执行路径;
  • 动作之间有必须强制执行的严格依赖顺序;
  • 延迟与成本敏感(每一次决策都要一次 LLM 调用)。

使用 Supervisor

要使用 Supervisor,把 Agent 标注为 planner = PlannerType.SUPERVISOR,并用 @AchievesGoal 标记一个动作:

java
@Agent(
        planner = PlannerType.SUPERVISOR,
        description = "Market research report generator"
)
public class MarketResearchAgent {

    public record MarketDataRequest(String topic) {}
    public record MarketData(Map<String, String> revenues, Map<String, Double> marketShare) {}

    public record CompetitorAnalysisRequest(List<String> companies) {}
    public record CompetitorAnalysis(Map<String, List<String>> strengths) {}

    public record ReportRequest(String topic, List<String> companies) {}
    public record FinalReport(String title, List<String> sections) {}

    @Action(description = "Gather market data including revenues and market share") // ①
    public MarketData gatherMarketData(MarketDataRequest request, Ai ai) {
        return ai.withDefaultLlm().createObject(
                "Generate market data for: " + request.topic(),
                MarketData.class
        );
    }

    @Action(description = "Analyze competitors: strengths and positioning")
    public CompetitorAnalysis analyzeCompetitors(CompetitorAnalysisRequest request, Ai ai) {
        return ai.withDefaultLlm().createObject(
                "Analyze competitors: " + String.join(", ", request.companies()),
                CompetitorAnalysis.class
        );
    }

    @AchievesGoal(description = "Compile all information into a final report") // ②
    @Action(description = "Compile the final report")
    public FinalReport compileReport(ReportRequest request, Ai ai) {
        return ai.withDefaultLlm().createObject(
                "Create a market research report for " + request.topic(),
                FinalReport.class
        );
    }
}

注记:

  • ① 工具型动作带有描述,对 supervisor LLM 可见;
  • ② 当 supervisor 收集到足够信息时,会调用这个目标动作。

supervisor LLM 能看到可用动作的类型 schema:

Available actions:
- gatherMarketData(request: MarketDataRequest) -> MarketData
    Schema: { revenues: Map, marketShare: Map }
- analyzeCompetitors(request: CompetitorAnalysisRequest) -> CompetitorAnalysis
    Schema: { strengths: Map }

Current artifacts on blackboard:
- MarketData: { revenues: {"CompanyA": "$10B"}, marketShare: {...} }

Goal: FinalReport

LLM 依据这些信息决定动作顺序 —— 做出有依据的决策,而不被声明的依赖关系束缚。

互操作性(Interoperability)

使用包装请求类型(例如 MarketDataRequest)能让同一批动作适配任何规划器:

  • GOAP:请求类型依据前置条件/效果在黑板间流转;
  • Utility:当请求类型可用且净价值最高时动作触发;
  • Supervisor:LLM 自行构造请求对象来调用动作。

这意味着换规划器不用改动作代码 —— 适合"用确定性规划器(GOAP)测试、用灵活规划器(Supervisor)部署"的组合。

与 LangGraph 的对比

LangGraph 的 supervisor 模式是多智能体编排的流行做法。同一个工作流在两者中的样子:

LangGraph(Python)

python
from langgraph_supervisor import create_supervisor
from langgraph.prebuilt import create_react_agent

# 工具返回字符串 —— 没有类型信息
def gather_market_data(topic: str) -> str:
    """Gather market data for a topic."""
    return f"Revenue data for {topic}..."  # ①

def analyze_competitors(companies: str) -> str:
    """Analyze competitors."""
    return f"Analysis of {companies}..."  # ①

# 用工具创建 agent
research_agent = create_react_agent(
    model="openai:gpt-4o",
    tools=[gather_market_data, analyze_competitors],
    name="research_expert",
)

# supervisor 永远看到所有工具 ②
workflow = create_supervisor([research_agent], model=model)
app = workflow.compile()

# 状态是消息字典 ③
result = app.invoke({"messages": [{"role": "user", "content": "Research cloud market"}]})

① 工具返回字符串 —— LLM 必须自己解析和解释结果 ② 所有工具始终可见 —— 不基于上下文做过滤 ③ 状态是非类型化的消息历史

Embabel

java
@Agent(planner = PlannerType.SUPERVISOR)
public class MarketResearchAgent {

    // 工具返回带 schema 的类型化对象 ①
    @Action(description = "Gather market data for a topic")
    public MarketData gatherMarketData(MarketDataRequest request, Ai ai) {
        return ai.withDefaultLlm().createObject(
                "Generate market data for " + request.topic(), MarketData.class);
    }

    @Action(description = "Analyze competitors")
    public CompetitorAnalysis analyzeCompetitors(CompetitorAnalysisRequest request, Ai ai) {
        return ai.withDefaultLlm().createObject(
                "Analyze " + request.companies(), CompetitorAnalysis.class);
    }

    @AchievesGoal
    @Action
    public FinalReport compileReport(ReportRequest request, Ai ai) { ... }
}

// 状态是带类型的黑板 ②
// 工具基于可用输入被过滤 ③

① 工具返回带类型、经校验的对象 —— MarketData、CompetitorAnalysis ② 黑板存放带类型的产物,而不只是消息字符串 ③ 输入已满足的工具会通过 currying(柯里化) 被优先考虑

关键优势

Embabel 的 Supervisor 相比典型的 supervisor 实现有几项优势:

方面典型 Supervisor(LangGraph)Embabel Supervisor
输出类型字符串 —— LLM 必须自己解析类型化对象 —— 经校验、有结构
工具可见性所有工具始终可用工具按黑板状态过滤(currying)
领域感知无 —— 工具是不透明的函数类型 schema 对 LLM 可见
确定性完全非确定半确定:工具可用性受类型约束
状态非类型化的消息历史带类型、带命名的黑板产物

黑板驱动的工具过滤(Blackboard-Driven Tool Filtering)

一个关键差异点是 curried tool filtering(柯里化工具过滤)。 当某个动作的输入已经在黑板上时,这些参数会被"柯里化掉",工具签名随之简化。

**柯里化(Currying)**是函数式编程中的一种技术:把多参数函数转换成一系列"每次只接一个参数"的函数。

放到 Embabel 的语境里:如果一个动作需要 (MarketDataRequest, Ai),而 MarketDataRequest 已经在黑板上, 我们就把这个参数"柯里化掉"—— 暴露给 LLM 的工具只需要提供剩下的参数即可。 这简化了 LLM 的任务,并明确提示了哪些工具"已经就绪"。

# 初始状态:黑板为空
Available tools:
- gatherMarketData(request: MarketDataRequest) -> MarketData
- analyzeCompetitors(request: CompetitorAnalysisRequest) -> CompetitorAnalysis

# MarketData 收集到之后:
Available tools:
- gatherMarketData(request: MarketDataRequest) -> MarketData [READY - 0 params needed]
- analyzeCompetitors(request: CompetitorAnalysisRequest) -> CompetitorAnalysis

这缩小了 LLM 的决策空间,并把它引导向合乎逻辑的下一步 —— 输入已满足的工具会以"更少参数"的形式显示为 ready。 它比"把所有工具一视同仁地摆出来"更确定,同时又比 GOAP 的严格排序更灵活。

半确定性(Semi-Determinism)

虽然仍由 LLM 编排,Embabel 的 Supervisor 比典型实现更确定:

  • 类型约束:动作只能产出特定类型 —— 不允许任意的字符串输出;
  • 输入过滤:输入类型不存在时,工具不可用;
  • schema 引导:LLM 看到每个动作产出什么,而不只是描述;
  • 输出校验:结果必须符合声明的类型。

这让调试更容易、行为更可预测,同时保留了 supervisor 模式宝贵的灵活性。

Embabel 这种方式出彩的地方:

  • 领域丰富的流程:当你的领域有清晰的类型(报告、分析、预测)时,schema 能帮助 LLM 理解关系;
  • 多步合成:当动作相互建立在彼此输出之上时,带类型的黑板能清晰追踪进度;
  • 混合确定性:当你想要比纯 LLM 编排更可预测、又比 GOAP 更灵活时。

SupervisorInvocation:轻量级 Supervisor 模式

对于简单的 supervisor 工作流,你不需要创建 @Agent 类。 SupervisorInvocation 提供了流式 API,可以直接从 @EmbabelComponent 的动作里运行 supervisor 编排的工作流。

这尤其适合:

  • 你在一个 @EmbabelComponent 里只有一小组相关动作;
  • 你想要 LLM 编排的组合,但不想为此建一个完整 Agent;
  • 你在定下完整 Agent 设计之前,想先做原型或探索 supervisor 模式。

示例:备餐工作流(Meal Preparation Workflow) 下面是来自 embabel-agent-examples 仓库的完整示例:

Stages —— 把动作写成 @EmbabelComponent

java
@EmbabelComponent
public class Stages {

    public record Cook(String name, int age) {}

    public record Order(String dish, int quantity) {}

    public record Meal(String dish, int quantity, String orderedBy, String cookedBy) {}

    @Action
    public Cook chooseCook(UserInput userInput, Ai ai) {
        return ai.withAutoLlm().createObject(
                """
                From the following user input, choose a cook.
                User input: %s
                """.formatted(userInput),
                Cook.class
        );
    }

    @Action
    public Order takeOrder(UserInput userInput, Ai ai) {
        return ai.withAutoLlm().createObject(
                """
                From the following user input, take a food order
                User input: %s
                """.formatted(userInput),
                Order.class
        );
    }

    @Action
    @AchievesGoal(description = "Cook the meal according to the order")
    public Meal prepareMeal(Cook cook, Order order, UserInput userInput, Ai ai) {
        // 模型会从 UserInput 里取 orderedBy
        return ai.withAutoLlm().createObject(
                """
                Prepare a meal based on the cook and order details and target customer
                Cook: %s, age %d
                Order: %d x %s
                User input: %s
                """.formatted(cook.name(), cook.age(), order.quantity(), order.dish(), userInput.getContent()),
                Meal.class
        );
    }
}

用 SupervisorInvocation 调用

java
Stages stages = new Stages();

Meal meal = SupervisorInvocation.on(agentPlatform)
        .returning(Stages.Meal.class)
        .withScope(AgentScopeBuilder.fromInstance(stages))
        .invoke(new UserInput(request));

配置项

方法说明
.returning(Class)指定要产出的目标类型
.withScope(AgentScopeBuilder)定义哪些动作可用
.withAgentName(String)为创建出的 Agent 设置自定义名称(默认 ${platformName}.supervisor)
.withGoalDescription(String)为目标提供自定义描述
.withProcessOptions(ProcessOptions)配置进程级选项

设置自定义 Agent 名称

java
SupervisorInvocation.on(agentPlatform)
        .returning(Report.class)
        .withScope(AgentScopeBuilder.fromInstance(actions))
        .withAgentName("market-research-supervisor")
        .invoke(request);

supervisor LLM 会看到:

  • 可用动作及其类型签名与 schema;
  • 黑板上当前的产物(包括 UserInput 的内容);
  • 目标是产出一个 Meal。

随后它编排这些动作 —— 调用 chooseCook 与 takeOrder(可能并行), 依赖满足后再调用 prepareMeal。

关键设计要点

  • 动作显式使用 UserInput:每个动作都接收 UserInput 并把它放进 LLM 提示词,确保真正用到用户的请求;
  • @AchievesGoal 标记目标:prepareMeal 被标注为 @AchievesGoal,表示它产出最终输出;
  • 类型驱动的依赖:prepareMeal 需要 Cook 与 Order,这引导了 supervisor 的编排。

SupervisorInvocation 与 @Agent(planner = SUPERVISOR) 的对比

方面SupervisorInvocation@Agent(planner = SUPERVISOR)
声明方式流式 API,无需类注解带注解的 Agent 类
动作来源@EmbabelComponent 或多个组件单个 @Agent 类
最适合快速原型、简单工作流正式化、可复用的 Agent
目标指定.returning(Class) 流式方法动作上的 @AchievesGoal
作用域通过 AgentScopeBuilder 显式指定由 Agent 类隐式确定

与 AgenticTool 的对比

SupervisorInvocation 与 AgenticTool 都提供 LLM 编排的组合能力,但处在不同层级:

方面AgenticToolSupervisorInvocation
层级工具(可在动作内部使用)调用(运行一个完整工作流)
子组件其它 Tool 实例来自 @EmbabelComponent 的 @Action 方法
输出Tool.Result(文本、产物或错误)带类型的目标对象(例如 Meal)
状态管理极少(仅 LLM 对话)完整的黑板,含带类型产物
类型感知工具有名称与描述动作有带类型的输入/输出与 schema
柯里化无黑板上的输入会被柯里化掉
使用场景动作内部的"迷你编排"产出类型化结果的完整多步工作流

需要"在工具内部编排其它工具"时,用 AgenticTool; 需要"产出类型化结果、且带完整黑板状态管理"的完整工作流时,用 SupervisorInvocation。

原文页面末尾附有一张概念关系图(PlanningSystem / Action / Condition / Blackboard / WorldState / Plan / IoBinding / Planner / Goal 之间的连接), 此处以文字列出其关系,便于检索: 动作 → 条件:动作具有前置条件;动作 → 黑板:读写黑板;动作 → 世界状态:产生效果; 计划 → 动作:包含动作序列;规划器 → 动作:选择动作;规划器 → 计划:生成计划; 规划器 → 目标:满足目标;目标 → 条件:由条件定义;黑板 → 世界状态:决定世界状态。

BytesRouter 是社区翻译站点,所有译文仅供参考,一切以官方英文原文为准。