主题
22 · 规划器(Planners)
选择规划器(Choosing a Planner)
Embabel 支持多种规划策略。它们大多是确定性的,但行为各不相同 —— 不过永远是可预测的。
所有规划策略在 Java 或 Kotlin 中都是完全类型安全的。
开箱即用、目前支持的规划策略有:
| 规划器 | 最适合 | 说明 |
|---|---|---|
| GOAP(默认) | 有明确产出的业务流程 | 目标导向的确定性规划。用前置条件与效果,规划出一条从当前状态到目标的路径。 |
| Utility | 探索型、事件驱动型系统 | 每一步都选择"可用动作中价值最高"的那个。适合你还不知道最终结果会是什么的场景。 |
| Hybrid | Reducer 式流水线(先收集一堆产出上下文的动作,再跑一个合成器,然后停止) | 动作挑选方式与 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 的差异
| 场景 | UTILITY | HYBRID |
|---|---|---|
| 真实目标已满足、没有可达成动作 | 空计划(终止) | 空计划(终止) |
| 真实目标已满足、但仍有其它可达成动作 | 挑净价值最高的动作并执行 | 空计划(终止)—— 这是关键的修正 |
| 真实目标未满足、存在 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: FinalReportLLM 依据这些信息决定动作顺序 —— 做出有依据的决策,而不被声明的依赖关系束缚。
互操作性(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 编排的组合能力,但处在不同层级:
| 方面 | AgenticTool | SupervisorInvocation |
|---|---|---|
| 层级 | 工具(可在动作内部使用) | 调用(运行一个完整工作流) |
| 子组件 | 其它 Tool 实例 | 来自 @EmbabelComponent 的 @Action 方法 |
| 输出 | Tool.Result(文本、产物或错误) | 带类型的目标对象(例如 Meal) |
| 状态管理 | 极少(仅 LLM 对话) | 完整的黑板,含带类型产物 |
| 类型感知 | 工具有名称与描述 | 动作有带类型的输入/输出与 schema |
| 柯里化 | 无 | 黑板上的输入会被柯里化掉 |
| 使用场景 | 动作内部的"迷你编排" | 产出类型化结果的完整多步工作流 |
需要"在工具内部编排其它工具"时,用 AgenticTool; 需要"产出类型化结果、且带完整黑板状态管理"的完整工作流时,用 SupervisorInvocation。
原文页面末尾附有一张概念关系图(PlanningSystem / Action / Condition / Blackboard / WorldState / Plan / IoBinding / Planner / Goal 之间的连接), 此处以文字列出其关系,便于检索: 动作 → 条件:动作具有前置条件;动作 → 黑板:读写黑板;动作 → 世界状态:产生效果; 计划 → 动作:包含动作序列;规划器 → 动作:选择动作;规划器 → 计划:生成计划; 规划器 → 目标:满足目标;目标 → 条件:由条件定义;黑板 → 世界状态:决定世界状态。