主题
20 · 调用 Agent(Invoking Embabel Agents)
虽然很多示例都展示通过 Embabel shell、以 UserInput 方式调用 Agent, 但它们也可以被强类型地以编程方式调用。
这通常就是Web 应用里的用法。它也是最确定的方式 —— 由代码而非"LLM 对用户输入的评估"来决定调用哪个 Agent、以及怎么调用。
以编程方式创建 AgentProcess
你可以直接用 AgentPlatform 创建并执行 Agent 进程:
java
// 用 bindings 创建 agent 进程
AgentProcess agentProcess = agentPlatform.createAgentProcess(
myAgent,
new ProcessOptions(),
Map.of("input", userRequest)
);
// 启动进程并等待完成
Object result = agentPlatform.start(agentProcess).get();
// 或者同步运行
AgentProcess completedProcess = agentProcess.run();
MyResultType result = completedProcess.last(MyResultType.class);你也可以用可变参数对象创建进程并填充其输入 map:
java
// 从对象创建进程(就像在 web controller 里那样)
AgentProcess agentProcess = agentPlatform.createAgentProcessFrom(
travelAgent,
new ProcessOptions(),
travelRequest,
userPreferences
);使用 AgentInvocation
AgentInvocation 提供了更高层、类型安全的调用 API。它会根据期望的结果类型自动找到合适的 Agent。
基本用法
java
// 用显式结果类型做简单调用
var invocation = AgentInvocation.create(agentPlatform, TravelPlan.class);
TravelPlan plan = invocation.invoke(travelRequest);用命名输入调用
java
// 用命名的输入 map 调用
Map<String, Object> inputs = Map.of(
"request", travelRequest,
"preferences", userPreferences
);
TravelPlan plan = invocation.invoke(inputs);自定义 ProcessOptions
配置详细程度(verbosity)、预算以及其它执行选项:
java
var processOptions = new ProcessOptions()
.withVerbosity(new Verbosity()
.withShowPrompts(true)
.withShowLlmResponses(true)
.withDebug(true));
var invocation = AgentInvocation.builder(agentPlatform)
.options(processOptions)
.build(TravelPlan.class);
TravelPlan plan = invocation.invoke(travelRequest);设置在
ProcessOptions上的 verbosity 会被子 Agent 调用继承。 子 Agent 与触发它的进程使用相同的Verbosity—— 在顶层设置一次,就会贯穿整棵调用树。GoalTool与AgentTool属于入口点,无论父上下文如何,默认showPrompts = true。
在调用时传入 Tool Call Context
用 ProcessOptions.withToolCallContext() 挂载带外元数据,它会贯穿整个 Agent 运行、传给每一个被调用的工具 —— 包括远程 MCP 工具(在那里它变成线上的 MCP _meta)。 这里适合放那些来自入站请求的横切基础设施关注点:auth token、tenant ID、correlation ID。
java
// 在 REST controller 或事件处理器里
var processOptions = new ProcessOptions()
.withToolCallContext(Map.of(
"authToken", request.getHeader("Authorization"),
"tenantId", request.getHeader("X-Tenant-Id"),
"correlationId", UUID.randomUUID().toString()
));
var invocation = AgentInvocation.builder(agentPlatform)
.options(processOptions)
.build(CustomerReport.class);
CustomerReport report = invocation.invoke(customerQuery);这里设置的上下文,可以被任何声明了
ToolCallContext参数的@LlmTool方法读取。 它也可以在@Action方法内部用PromptRunner.withToolCallContext()按交互补充;冲突时交互级的值优先。 完整的上下文流水线见/reference/tools#tool-call-context。
异步调用
对长耗时操作,使用异步调用:
java
CompletableFuture<TravelPlan> future = invocation.invokeAsync(travelRequest);
// 完成时处理结果
future.thenAccept(plan -> {
logger.info("Travel plan generated: {}", plan);
});
// 或者等待完成
TravelPlan plan = future.get();Agent 选择机制
AgentInvocation 通过检查 Agent 的目标来自动找到它们:
- 搜索平台中所有已注册的 Agent;
- 找出目标会产出所请求结果类型的 Agent;
- 使用找到的第一个匹配 Agent;
- 如果没有合适的 Agent,抛错。
真实 Web 应用示例
下面是 Tripper 旅行规划应用中 AgentInvocation 的用法 —— 配合 htmx 做异步 UI 更新:
java
@Controller
public class TripPlanningController {
private final AgentPlatform agentPlatform;
private final ConcurrentHashMap<String, CompletableFuture<TripPlan>> activeJobs =
new ConcurrentHashMap<>();
private static final Logger logger =
LoggerFactory.getLogger(TripPlanningController.class);
private static final ConcurrentHashMap<String, TripPlan> tripResultCache =
new ConcurrentHashMap<>();
public TripPlanningController(AgentPlatform agentPlatform) {
this.agentPlatform = agentPlatform;
}
@PostMapping("/plan-trip")
public String planTrip(
@ModelAttribute TripRequest tripRequest,
Model model) {
// 生成用于追踪的唯一 job ID
String jobId = UUID.randomUUID().toString();
// 用自定义选项创建 agent 调用
var processOptions = new ProcessOptions()
.withVerbosity(new Verbosity().withShowPrompts(true));
var invocation = AgentInvocation.builder(agentPlatform)
.options(processOptions)
.build(TripPlan.class);
// 启动异步 agent 执行
CompletableFuture<TripPlan> future = invocation.invokeAsync(tripRequest);
activeJobs.put(jobId, future);
// 设置完成处理器
future.whenComplete((result, throwable) -> {
if (throwable != null) {
logger.error("Trip planning failed for job {}", jobId, throwable);
} else {
logger.info("Trip planning completed for job {}", jobId);
}
});
model.addAttribute("jobId", jobId);
model.addAttribute("tripRequest", tripRequest);
// 返回会轮询结果的 htmx 模板
return "trip-planning-progress";
}
@GetMapping("/trip-status/{jobId}")
@ResponseBody
public ResponseEntity<Map<String, Object>> getTripStatus(@PathVariable String jobId) {
CompletableFuture<TripPlan> future = activeJobs.get(jobId);
if (future == null) {
return ResponseEntity.notFound().build();
}
if (future.isDone()) {
try {
TripPlan tripPlan = future.get();
activeJobs.remove(jobId);
return ResponseEntity.ok(Map.of(
"status", "completed",
"result", tripPlan,
"redirect", "/trip-result/" + jobId
));
} catch (Exception e) {
activeJobs.remove(jobId);
return ResponseEntity.ok(Map.of(
"status", "failed",
"error", e.getMessage()
));
}
} else if (future.isCancelled()) {
activeJobs.remove(jobId);
return ResponseEntity.ok(Map.of("status", "cancelled"));
} else {
return ResponseEntity.ok(Map.of(
"status", "in_progress",
"message", "Planning your amazing trip..."
));
}
}
@GetMapping("/trip-result/{jobId}")
public String showTripResult(
@PathVariable String jobId,
Model model) {
// 从缓存或数据库中取出已完成的结果
TripPlan tripPlan = tripResultCache.get(jobId);
if (tripPlan == null) {
return "redirect:/error";
}
model.addAttribute("tripPlan", tripPlan);
return "trip-result";
}
@DeleteMapping("/cancel-trip/{jobId}")
@ResponseBody
public ResponseEntity<Map<String, String>> cancelTrip(@PathVariable String jobId) {
CompletableFuture<TripPlan> future = activeJobs.get(jobId);
if (future != null && !future.isDone()) {
future.cancel(true);
activeJobs.remove(jobId);
return ResponseEntity.ok(Map.of("status", "cancelled"));
} else {
return ResponseEntity.badRequest()
.body(Map.of("error", "Job not found or already completed"));
}
}
}关键模式:
- 异步执行:用
invokeAsync()避免阻塞 Web 请求; - 任务追踪:维护活跃 future 的 map 以便轮询状态;
- htmx 集成:返回 htmx 可消费的状态更新,用于刷新 UI;
- 错误处理:恰当的异常处理与用户反馈;
- 资源清理:把已完成的任务从内存中移除;
- Process Options:为生产使用配置 verbosity 与调试。
替代方案:直接创建 AgentProcess
对更简单的用例,你可以不经过 AgentInvocation,直接创建并启动 AgentProcess。 Tripper 应用就用了这种方式,它很适合 webhook 或表单提交场景,因为你想要:
- 启动一个长时间运行的 Agent 进程;
- 立刻返回一个进程 ID;
- 用平台内置的 controller 轮询状态。
java
@Controller
@RequestMapping("/journey")
public class JourneyController {
private final AgentPlatform agentPlatform;
public JourneyController(AgentPlatform agentPlatform) {
this.agentPlatform = agentPlatform;
}
@PostMapping("/plan")
public String planJourney(@ModelAttribute JourneyPlanForm form, Model model) {
// 把表单转换成领域对象
TravelBrief travelBrief = new TravelBrief(
form.getFrom(),
form.getTo(),
form.getDepartureDate(),
form.getReturnDate(),
form.getBrief()
);
// 找到合适的 agent
Agent agent = agentPlatform.agents().stream()
.filter(a -> a.getName().toLowerCase().contains("travel"))
.findFirst()
.orElseThrow(() -> new IllegalStateException("No travel agent found"));
// 用输入 bindings 创建 agent 进程
AgentProcess agentProcess = agentPlatform.createAgentProcessFrom(
agent,
new ProcessOptions(
new Verbosity().withShowPrompts(true),
Budget.DEFAULT // 或自定义预算
),
travelBrief // 可变参数输入会被绑定到黑板
);
// 异步启动进程
agentPlatform.start(agentProcess);
// 把进程 ID 放进 model 以便轮询状态
model.addAttribute("processId", agentProcess.getId());
model.addAttribute("travelBrief", travelBrief);
// 返回一个会轮询 /api/v1/process/{processId} 的视图
return "processing";
}
}平台提供了内置的 REST 端点用于状态查询:
GET /api/v1/process/{processId}—— 返回进程状态、结果与相关 URL;DELETE /api/v1/process/{processId}—— 终止一个运行中的进程;GET /events/process/{processId}—— 进程事件的 SSE 流。
每个端点都可以通过配置单独关闭。把对应属性设为 false,该端点就会返回 HTTP 404:
properties
embabel.agent.platform.rest.process-status-enabled=false
embabel.agent.platform.rest.process-kill-enabled=false
embabel.agent.platform.rest.process-events-enabled=false一个简单的状态轮询 controller 可以检查完成情况并重定向到结果页:
java
@Controller
public class ProcessStatusController {
private final AgentPlatform agentPlatform;
public ProcessStatusController(AgentPlatform agentPlatform) {
this.agentPlatform = agentPlatform;
}
@GetMapping("/status/{processId}")
public String checkStatus(
@PathVariable String processId,
@RequestParam String successView,
@RequestParam String resultModelKey,
Model model) {
AgentProcess process = agentPlatform.getAgentProcess(processId);
if (process == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Process not found");
}
switch (process.getStatus()) {
case COMPLETED:
model.addAttribute(resultModelKey, process.lastResult());
return successView;
case FAILED:
model.addAttribute("error", "Process failed: " + process.getFailureInfo());
return "error";
case TERMINATED:
model.addAttribute("error", "Process was terminated");
return "error";
default:
// 仍在运行 —— 返回轮询视图
model.addAttribute("processId", processId);
return "processing";
}
}
}两种方式各自适合什么:
| 方式 | 最适合 |
|---|---|
AgentInvocation.invokeAsync() | 当你需要一个 CompletableFuture 来做程序化处理、链式组合,或与响应式框架集成时 |
直接创建 AgentProcess | Webhook、表单提交,或用 REST/SSE 轮询状态的 UI 流程 |
Webhook 集成示例
对 webhook 触发的工作流(例如 JIRA、GitHub),直接创建进程的方式很好用:
java
@RestController
@RequestMapping("/webhook")
public class WebhookController {
private final AgentPlatform agentPlatform;
public WebhookController(AgentPlatform agentPlatform) {
this.agentPlatform = agentPlatform;
}
@PostMapping("/jira/issue-created")
public ResponseEntity<Map<String, String>> onJiraIssueCreated(
@RequestBody JiraWebhookPayload payload) {
// 找到处理 JIRA issue 的 agent
Agent agent = agentPlatform.agents().stream()
.filter(a -> a.getName().contains("JiraIssue"))
.findFirst()
.orElseThrow(() -> new IllegalStateException("No JIRA agent configured"));
// 从 webhook payload 构造领域对象
JiraIssue issue = new JiraIssue(
payload.getIssue().getKey(),
payload.getIssue().getFields().getSummary(),
payload.getIssue().getFields().getDescription()
);
// 创建并启动 agent 进程
AgentProcess process = agentPlatform.createAgentProcessFrom(
agent,
ProcessOptions.DEFAULT,
issue
);
agentPlatform.start(process);
// 返回进程 ID 用于状态追踪
return ResponseEntity.accepted().body(Map.of(
"processId", process.getId(),
"statusUrl", "/api/v1/process/" + process.getId(),
"sseUrl", "/events/process/" + process.getId()
));
}
}webhook 的调用方随后就可以轮询 /api/v1/process/{processId},或订阅 /events/process/{processId} 的 SSE 事件来跟踪进度。
Agent 也可以被暴露为 MCP server,供 Claude Desktop 之类的工具消费。
用 Autonomy 动态选择 Agent 与目标
Autonomy 类基于用户意图,提供 LLM 驱动的 Agent 与目标动态选择。 它不是由代码指定运行哪个 Agent,而是用 LLM 把可用 Agent 或目标与用户输入做排序,选出最佳匹配。
Embabel Shell 就是这样处理自然语言命令的。
执行模式
Autonomy 支持两种执行模式:
- Closed 模式(
chooseAndRunAgent):LLM 根据用户意图选出最合适的 Agent。 被选中的 Agent 隔离运行,只使用它自己的动作与目标。 - Open 模式(
chooseAndAccomplishGoal):LLM 从所有 Agent 的所有可用目标中选出最合适的目标。 随后 Embabel 组装一个动态 Agent,它可以使用任意 Agent 的任意动作来达成该目标。
Closed 模式示例
当你想要严格的 Agent 边界时,用 closed 模式:
java
@Service
public class IntentHandler {
private final Autonomy autonomy;
public IntentHandler(Autonomy autonomy) {
this.autonomy = autonomy;
}
public AgentProcessExecution handleUserIntent(String userIntent) {
// LLM 对所有 agent 排序,选出最匹配的
return autonomy.chooseAndRunAgent(
userIntent,
ProcessOptions.DEFAULT
);
}
}Open 模式示例
当你希望达成目标的方式最灵活时,用 open 模式:
java
@Service
public class GoalHandler {
private final Autonomy autonomy;
private final AgentPlatform agentPlatform;
public GoalHandler(Autonomy autonomy, AgentPlatform agentPlatform) {
this.autonomy = autonomy;
this.agentPlatform = agentPlatform;
}
public AgentProcessExecution handleUserIntent(String userIntent) {
// LLM 对所有 goal 排序,选出最匹配的
// 然后从可用动作中组装一个 agent 来达成它
return autonomy.chooseAndAccomplishGoal(
ProcessOptions.DEFAULT,
GoalChoiceApprover.APPROVE_ALL,
agentPlatform, // 包含 goals 与 actions 的 AgentScope
Map.of("userInput", new UserInput(userIntent)),
new GoalSelectionOptions()
);
}
}使用任意 Bindings
chooseAndAccomplishGoal 接受任意 bindings,并不限于 UserInput。 BindingsFormatter 负责从 bindings 中抽取意图文本用于目标排序:
java
public AgentProcessExecution processTask(Task task, Person person) {
// bindings 可以是任意对象
Map<String, Object> bindings = Map.of(
"task", task,
"person", person
);
return autonomy.chooseAndAccomplishGoal(
ProcessOptions.DEFAULT,
GoalChoiceApprover.APPROVE_ALL,
agentPlatform,
bindings,
new GoalSelectionOptions(),
BindingsFormatter.DEFAULT // 从 bindings 中抽取意图
);
}默认的 BindingsFormatter 按以下优先级抽取文本:
- 若对象实现了
PromptContributor,用PromptContributor.contribution(); - 若对象实现了
HasInfoString,用HasInfoString.infoString(); - 否则用
toString()。
你也可以提供自定义 formatter:
java
BindingsFormatter customFormatter = bindings -> {
Task task = (Task) bindings.get("task");
Person person = (Person) bindings.get("person");
return String.format("Process task '%s' for %s", task.getDescription(), person.getName());
};
return autonomy.chooseAndAccomplishGoal(
ProcessOptions.DEFAULT,
GoalChoiceApprover.APPROVE_ALL,
agentPlatform,
bindings,
new GoalSelectionOptions(),
customFormatter
);目标选择审批(Goal Choice Approval)
你可以要求在执行被选中的目标之前先经过审批:
java
// 只批准高置信度匹配
GoalChoiceApprover approver = GoalChoiceApprover.approveWithScoreOver(0.8);
// 或实现自定义审批逻辑
GoalChoiceApprover customApprover = request -> {
if (request.getGoal().getName().contains("dangerous")) {
return new GoalChoiceNotApproved("Dangerous goals require manual approval");
}
return GoalChoiceApproved.INSTANCE;
};置信度阈值(Confidence Thresholds)
Autonomy 使用可配置的置信度阈值来过滤匹配结果。 如果没有 Agent 或目标超过阈值,会抛出 NoAgentFound 或 NoGoalFound 异常。
在 application.properties 中配置阈值:
properties
# Agent 选择的最低置信度(0.0 ~ 1.0)
embabel.agent.platform.autonomy.agent-confidence-cut-off=0.6
# 目标选择的最低置信度(0.0 ~ 1.0)
embabel.agent.platform.autonomy.goal-confidence-cut-off=0.6或者按请求覆盖,使用 GoalSelectionOptions:
java
GoalSelectionOptions options = new GoalSelectionOptions(
0.5, // goalConfidenceCutOff —— 覆盖平台默认值
null, // agentConfidenceCutOff —— 沿用平台默认值
false // multiGoal —— 是否选择多个目标
);Shell 用法
Embabel Shell 用 Autonomy 来实现 execute(x)与 choose-goal 命令:
bash
# Closed 模式(默认)—— 选出最佳 agent
x "Find a horoscope for Alice who is a Scorpio"
# Open 模式 —— 选出最佳 goal,使用任意动作
x "Find a horoscope for Alice who is a Scorpio" -o
# 只显示目标排序,不执行
choose-goal "Find a horoscope for Alice"处理选择失败
java
try {
return autonomy.chooseAndRunAgent(userIntent, ProcessOptions.DEFAULT);
} catch (NoAgentFound e) {
// 没有 agent 以足够置信度匹配
logger.info("No matching agent. Rankings: {}", e.getAgentRankings());
return fallbackResponse();
} catch (NoGoalFound e) {
// 没有 goal 以足够置信度匹配(open 模式)
logger.info("No matching goal. Rankings: {}", e.getGoalRankings());
return fallbackResponse();
} catch (GoalNotApproved e) {
// 目标被审批器拒绝
logger.info("Goal not approved: {}", e.getReason());
return requiresApprovalResponse();
}