一、Agent 与 Embabel 概览
1.什么是 Agent
Agent(智能体)不是"回答问题的大模型",而是一个能够围绕目标持续执行任务的软件系统。
一个完整的 Agent 通常包含:
•目标:最终要完成什么
•状态:当前已经知道什么
•推理与规划:下一步应该做什么
•工具:能够调用哪些外部能力
•记忆:保存任务过程和历史信息
•执行循环:观察结果并调整后续行动
•完成条件:如何判断任务已经结束
普通大模型调用通常是:
用户输入 → LLM → 文本回答
Agent 的执行过程则是:
理解目标 → 感知信息 → 制定计划 → 执行行动 → 观察反馈 → 完成

例如,用户提出"帮我修改订单收货地址",Agent 可能需要:
1.从自然语言中提取订单号和新地址。
2.查询订单状态。
3.检索地址修改政策。
4.判断当前订单是否允许修改。
5.修改地址或创建人工工单。
6.返回操作结果和依据。
这里不仅涉及文本生成,还涉及状态管理、工具调用、业务决策和任务闭环。
2.Agent 与普通大模型应用的区别
普通大模型应用主要解决"生成什么内容",Agent 更关注"怎样完成一个目标"。
| 维度 | 普通大模型应用 | Agent |
|---|---|---|
| 输入 | Prompt | 目标、状态和上下文 |
| 输出 | 一段文本 | 任务结果或业务状态 |
| 执行过程 | 通常一次模型调用 | 多步骤循环执行 |
| 工具调用 | 可选 | 通常是核心能力 |
| 状态管理 | 主要依赖上下文窗口 | 显式保存中间状态 |
| 路径 | 通常固定 | 可以根据结果动态调整 |
| 完成判断 | 模型生成结束 | Goal 达成或触发终止条件 |
| 错误处理 | 重试模型调用 | 重规划、降级或人工介入 |
例如,一个普通大模型可以告诉用户"应该怎样修改地址",但生产级 Agent 需要真正完成:
理解请求 → 验证身份 → 查询订单 → 判断政策
→ 修改地址或创建工单 → 返回操作凭证
因此,Agent 的关键不是让模型"更聪明",而是系统具备了完成任务所需的执行能力和控制机制。
3.Workflow、ReAct、GOAP、Supervisor、Utility AI的区别
这五种模式解决的是不同问题
Workflow:代码按剧本执行
ReAct:LLM 边想边调用工具
GOAP:规划器奔着 Goal 找路径
Supervisor:上层 LLM 调度 Actions
Utility AI:每次选择当前最值得做的 Action
| 模式 | 谁决定下一步 | 是否需要明确 Goal | 决策方式 | 适用场景 |
|---|---|---|---|---|
| Workflow | 开发者 | 否 | 按预先定义的流程执行 | 固定审批、支付、数据同步 |
| ReAct | LLM | 通常不强制 | 推理、调用工具、观察结果、继续推理 | 搜索、排障、短链路探索 |
| GOAP | 规划器 | 是 | 根据前置条件和效果搜索到 Goal 的路径 | 目标明确、存在多条执行路径 |
| Utility AI | 效用计算器 | 否 | 计算每个可执行 Action 的当前价值,选择最高者 | 事件响应、持续探索、动态运营 |
| Supervisor | 上层 LLM | 可以有 | 根据语义动态选择 Action 或 Subagent | 开放任务、动态分工、多 Agent 协作 |
Workflow:预先定义流程
Workflow 由开发人员提前确定执行顺序和分支。优点是确定性强、容易测试和审计。缺点是路径需要提前穷举,环境变化时通常需要修改流程代码。
css
A → B → 判断条件 → C 或 D → E
适合:
•审批流程
•支付流程
•数据同步
•固定审核链路
•强合规业务
示例代码:
typescript
@Service
public class TicketWorkflow {
public ResolvedTicket process(Ticket ticket) {
// 第一步:分类
TicketCategory category = classify(ticket);
// 第二步:固定分支
if (category.urgency() >= 9) {
return handleCritical(ticket);
}
if ("BUG".equals(category.name())) {
return handleBug(ticket);
}
return handleGeneral(ticket);
}
private TicketCategory classify(Ticket ticket) {
String description = ticket.description().toLowerCase();
if (description.contains("down")) {
return new TicketCategory("CRITICAL", 10);
}
if (description.contains("bug")) {
return new TicketCategory("BUG", 6);
}
return new TicketCategory("GENERAL", 2);
}
private ResolvedTicket handleCritical(Ticket ticket) {
return new ResolvedTicket(
ticket.id(),
"Escalated to on-call engineer",
"CRITICAL_RESPONSE_TEAM"
);
}
private ResolvedTicket handleBug(Ticket ticket) {
return new ResolvedTicket(
ticket.id(),
"Bug created in issue tracker",
"ENGINEERING_TEAM"
);
}
private ResolvedTicket handleGeneral(Ticket ticket) {
return new ResolvedTicket(
ticket.id(),
"FAQ response sent",
"SUPPORT_TEAM"
);
}
}
ReAct:模型边推理边调用工具
ReAct 是 Reasoning + Acting,LLM 根据当前观察结果决定下一步调用哪个 Tool,即模型在推理和行动之间循环:
思考下一步
↓
调用 Tool
↓
观察 Tool 返回
↓
继续思考
↓
调用下一个 Tool
ReAct 的灵活性较高,但执行路径受模型影响,生产环境需要限制工具范围、调用次数、成本和副作用。
适合:
•开放式搜索
•故障排查
•数据探索
•短链路工具组合
•难以提前确定步骤的任务
示例代码:
typescript
public class TicketTools {
@LlmTool(description = "查询客户等级和未关闭工单数量")
public CustomerContext queryCustomer(String customerId) {
return new CustomerContext(
customerId,
"VIP",
2
);
}
@LlmTool(description = "查询系统是否存在服务故障")
public String querySystemStatus() {
return "payment-service is healthy";
}
@LlmTool(description = "将紧急工单升级给值班工程师")
public String escalate(String ticketId) {
return "Ticket " + ticketId + " escalated";
}
@LlmTool(description = "在问题跟踪系统中创建 Bug")
public String createBug(String ticketId, String description) {
return "BUG-" + ticketId;
}
}
@Agent(description = "使用工具处理客服工单")
public class ReActTicketAgent {
private final TicketTools ticketTools;
public ReActTicketAgent(TicketTools ticketTools) {
this.ticketTools = ticketTools;
}
@AchievesGoal(description = "完成工单处理")
@Action
public ResolvedTicket resolve(
Ticket ticket,
OperationContext context) {
return context.ai()
.withDefaultLlm()
.withToolObject(ticketTools)
.createObject("""
处理下面的客服工单。
你可以根据需要调用工具:
- 查询客户信息
- 查询系统状态
- 升级紧急工单
- 创建 Bug
根据工具返回结果决定下一步。
最终返回 ResolvedTicket。
工单:%s
""".formatted(ticket),
ResolvedTicket.class
);
}
}
可能出现的运行过程:
LLM:先查询客户信息
Tool:返回 VIP 客户
LLM:再查询系统状态
Tool:服务正常
LLM:判断为产品 Bug
Tool:创建 Bug
LLM:生成 ResolvedTicket
这里真正做决策的是 LLM。
GOAP:围绕 Goal 搜索路径
GOAP 是 Goal-Oriented Action Planning,即目标导向行动规划,GOAP 的核心思想就是把规划变成寻路游戏:
把你所有可用的 Action 当作地图上的节点,每个 Action 都有"进门条件"(前置条件)和"出门效果"(后置效果),然后用 A* 搜索找到从当前位置到目标的最短路径。A* 搜索算法(A-star search algorithm)是一种在图形或网格中寻找起点到终点最低成本(最短路径)的启发式搜索算法
开发者声明:
•当前有哪些事实
•系统有哪些 Action
•每个 Action 需要什么输入
•每个 Action 会产生什么结果
•最终 Goal 是什么
规划器根据这些信息动态搜索路径:
当前状态 + Action 能力图 → 可达路径 → Goal
GOAP 与 ReAct 的核心区别是:GOAP 的路径由规划器根据显式能力和状态计算,不需要让 LLM 决定每一个业务步骤。
适合:
•目标明确
•存在多条可达路径
•环境状态会发生变化
•需要重规划
•需要解释和测试执行路径
示例代码:
typescript
@Agent(
description = "通过目标规划处理客服工单",
planner = PlannerType.GOAP
)
public class GoapTicketAgent {
@Action(cost = 0.1)
public TicketCategory classify(Ticket ticket) {
String text = ticket.description().toLowerCase();
if (text.contains("down")) {
return new TicketCategory("CRITICAL", 10);
}
if (text.contains("bug")) {
return new TicketCategory("BUG", 6);
}
return new TicketCategory("GENERAL", 2);
}
@Action(cost = 0.2)
public CustomerContext loadCustomer(Ticket ticket) {
return new CustomerContext(
ticket.customerId(),
"VIP",
2
);
}
@Action(cost = 0.4)
public TechnicalEvidence investigate(
Ticket ticket,
TicketCategory category) {
return new TechnicalEvidence(
"Configuration error",
"Restore previous configuration"
);
}
@AchievesGoal(description = "得到完整的工单处理结果")
@Action
public ResolvedTicket resolve(
Ticket ticket,
TicketCategory category,
CustomerContext customer,
TechnicalEvidence evidence) {
String team = category.urgency() >= 9
? "CRITICAL_RESPONSE_TEAM"
: "SUPPORT_TEAM";
return new ResolvedTicket(
ticket.id(),
evidence.suggestion(),
team
);
}
}
规划器看到最终 Goal Action 需要:
Ticket
TicketCategory
CustomerContext
TechnicalEvidence
然后根据类型依赖反向寻找生产这些对象的 Action:
markdown
┌─ classify ───────→ TicketCategory ─┐
Ticket ────────────────┼─ loadCustomer ───→ CustomerContext ├─→ resolve
└─ investigate ─────→ TechnicalEvidence ┘
它可能形成:
classify
→ investigate
→ loadCustomer
→ resolve
也可能先执行 loadCustomer。重要的不是代码书写顺序,而是对象依赖和 Goal 是否可达。
Supervisor:由上层模型动态分派
Supervisor 通常是一种多 Agent 协作模式,核心思想是多 Agent 系统中的"管理者":自己不一定完成所有具体工作,而是通过任务拆解、动态分派、结果评估和循环协调,让多个专业 Agent 共同完成目标,使用一个上层 LLM 判断:
•需要完成哪些子任务
•应该调用哪个 Agent 或 Action
•是否继续、停止或重新分配任务
用户任务
↓
Supervisor LLM
├── Research Agent
├── Coding Agent
└── Review Agent
典型过程:

Supervisor 只是按照固定步骤调用三个普通 Java 方法,那么它更像 Workflow;如果它能动态选择、分派和协调具备自主决策能力的 Subagent,才是典型的 Supervisor 多 Agent 模式
适合高度开放、需要动态分工的任务,例如复杂调研、软件开发和跨领域分析。
它的灵活性最高,但模型调用成本、行为不确定性和治理难度也更高。
示例代码:
kotlin
@Agent(
description = "由 Supervisor 调度工单调查和处理",
planner = PlannerType.SUPERVISOR
)
public class SupervisorTicketAgent {
@Action(description = "查询客户等级、历史记录和未解决工单")
public CustomerContext researchCustomer(
Ticket ticket,
Ai ai) {
return ai.withDefaultLlm()
.createObject(
"查询并整理客户背景:" + ticket.customerId(),
CustomerContext.class
);
}
@Action(description = "分析工单描述并调查可能的技术原因")
public TechnicalEvidence investigateProblem(
Ticket ticket,
Ai ai) {
return ai.withDefaultLlm()
.createObject(
"调查下面工单的技术原因:" + ticket.description(),
TechnicalEvidence.class
);
}
@Action(description = "根据工单内容判断分类和紧急程度")
public TicketCategory classify(
Ticket ticket,
Ai ai) {
return ai.withDefaultLlm()
.createObject(
"判断工单分类和紧急程度:" + ticket.description(),
TicketCategory.class
);
}
@AchievesGoal(description = "综合已有信息生成最终处理结果")
@Action(description = "汇总客户、分类和技术调查结果")
public ResolvedTicket compileResolution(
Ticket ticket,
CustomerContext customer,
TicketCategory category,
TechnicalEvidence evidence,
Ai ai) {
return ai.withDefaultLlm()
.createObject("""
综合以下信息生成工单处理结果:
工单:%s
客户:%s
分类:%s
技术证据:%s
""".formatted(ticket, customer, category, evidence),
ResolvedTicket.class
);
}
}
Supervisor LLM 看到的能力类似:
scss
researchCustomer(Ticket) → CustomerContext
investigateProblem(Ticket) → TechnicalEvidence
classify(Ticket) → TicketCategory
compileResolution(...) → ResolvedTicket
它可能选择:
classify
→ investigateProblem
→ researchCustomer
→ compileResolution
也可能根据输入先调查客户。
Utility AI:没有固定终点、持续选择当前最有价值的动作
Utility AI 是一种基于效用评分的动态决策机制,而不是一套固定流程。它根据当前状态对候选动作进行评分,使 Agent 在运行时选择综合效用最高的下一步行动,核心思想就是不先搜索完整路径,而是在每一步选择当前 value - cost 最大的可执行 Action。
典型过程:

它通过一个简单的四步循环来做出决策,而不是按照固定的流程图死板执行:
•感知环境: 收集智能体自身和周围环境的数据(例如:NPC当前的血量、与敌人的距离、子弹数量)。
•计算效用: 将这些数据输入到评分曲线(曲线函数)中,计算出每个候选行为的"效用值"(分值在 0 到 1 之间)。
•行为评估: 比如"攻击"得分 0.8,"逃跑"得分 0.3,"加血"得分 0.9。
•执行最高分: 智能体最终决定去执行最高分的行为(在这个例子中是"加血")。
适合:
•智能客服和工单处理
◦根据工单状态选择处理策略:(立即升级、分配工程师、知识库自动回复、请求补充信息)
•游戏角色决策
◦候选动作:攻击、逃跑、治疗、寻找掩体。
◦状态变量:血量、敌人距离、弹药、危险程度
•告警和故障处置
◦系统出现多个告警时,动态决定优先处理哪个(数据库不可用、磁盘使用率、接口响应变慢、非核心服务异常)
示例代码:
typescript
@Agent(
description = "根据行动效用处理客服工单",
planner = PlannerType.UTILITY
)
public class UtilityTicketAgent {
@Action(
description = "读取客户历史",
value = 0.8,
cost = 0.1
)
public CustomerContext loadCustomerHistory(Ticket ticket) {
return new CustomerContext(
ticket.customerId(),
"VIP",
2
);
}
@Action(
description = "收集技术诊断信息",
value = 0.9,
cost = 0.3
)
public TechnicalEvidence collectDiagnostics(Ticket ticket) {
return new TechnicalEvidence(
"Database connection pool exhausted",
"Increase pool size and restart service"
);
}
@Action(
description = "判断工单分类",
value = 0.7,
cost = 0.05
)
public TicketCategory classify(Ticket ticket) {
boolean critical = ticket.description()
.toLowerCase()
.contains("down");
return critical
? new TicketCategory("CRITICAL", 10)
: new TicketCategory("GENERAL", 2);
}
@AchievesGoal(description = "完成工单处理")
@Action(
description = "根据完整证据解决工单",
value = 1.0,
cost = 0.1
)
public ResolvedTicket resolve(
Ticket ticket,
CustomerContext customer,
TicketCategory category,
TechnicalEvidence evidence) {
return new ResolvedTicket(
ticket.id(),
evidence.suggestion(),
category.urgency() >= 9
? "CRITICAL_RESPONSE_TEAM"
: "SUPPORT_TEAM"
);
}
}
初始只有 Ticket 时,三个 Action 都可执行:
| Action | value | cost | 净价值 |
|---|---|---|---|
| loadCustomerHistory | 0.80 | 0.10 | 0.70 |
| collectDiagnostics | 0.90 | 0.30 | 0.60 |
| classify | 0.70 | 0.05 | 0.65 |
所以第一步会选择:
loadCustomerHistory:0.70
执行后重新计算,再选择:
classify:0.65
然后:
collectDiagnostics:0.60
当所需对象齐备后,resolve() 才变成可执行 Action:
ini
resolve:1.00 - 0.10 = 0.90
于是得到最终 ResolvedTicket。
官方定义的基础净效用计算为:
ini
net value = value - cost
规划器每一步选择当前可执行 Action 中净价值最高的一个。
4.Embabel 的定位
Embabel 是 Spring 创始人 Rod Johnson 打造的 JVM 原生 Agent 框架。我们可以用一句话来定义它:Embabel 就是声明式编程 + 自动规划的在 JVM 上运行的 Agent 框架。它关注的不是单次模型调用,而是如何将大模型能力安全地嵌入真实业务系统。
你在领域模型即 Java Bean 里写 @Action(做什么,描述能力)、@Condition(什么时候可以做,描述执行条件),@Goal(目标是什么),它用 OODA 循环自动规划执行 Agent。
Embabel 的核心概念包括:
•@Agent:定义一个 Agent
•@Action:声明可规划的业务能力
•@AchievesGoal:声明能够达到最终目标的 Action
•Domain Model:描述业务事实
•Blackboard:保存一次执行过程中的状态
•Planner:根据状态搜索可达路径
•AgentProcess:管理一次完整执行
•Tool:提供给 LLM 调用的能力
•Subagent:执行具有独立 Goal 的子任务
| 概念 | 含义 | 示例 |
|---|---|---|
| Domain Model | Agent运行过程中使用和产生的领域对象 | 用户需求、研究报告、审核结果 |
| Action | Agent能够执行的一个步骤 | 搜索资料、生成报告、审核内容 |
| Condition | Action执行或Goal完成所依赖的条件 | 已取得研究资料、报告已审核 |
| Goal | Agent最终需要达成的状态 | 得到一份审核通过的报告 |
| Plan | 为达成Goal动态组合出的Action序列 | 搜索→撰写→审核→修改 |
一个简化的 Embabel Agent:
kotlin
@Agent(description = "处理客户订单请求")
class OrderSupportAgent {
@Action
SupportRequest extract(UserInput input) {
return requestExtractor.extract(input.content());
}
@Action
OrderEvidence queryOrder(SupportRequest request) {
return orderService.query(request.orderId());
}
@AchievesGoal(description = "返回订单处理结果")
@Action
SupportOutcome respond(
SupportRequest request,
OrderEvidence evidence) {
return outcomeService.create(request, evidence);
}
}
框架可以根据方法参数和返回类型推导:
erlang
UserInput
↓ extract
SupportRequest
↓ queryOrder
OrderEvidence
↓ respond
SupportOutcome(Goal)
Embabel 的重点不是替代业务代码,而是把 LLM、领域服务、工具和规划能力组织在一起。
5.Embabel 与 Spring AI 的关系
作为 Java 开发者,我们用 Spring Boot 快速搭建项目,用 Spring Data 访问数据库,用 Spring Security 保护应用安全等等。20 年来,Spring 生态已经成为企业级开发的标准。而现在,AI 浪潮席卷而来。作为 Java 开发者,我们有一个天然的优势:我们不需要从零开始学习一套全新的生态系统。Spring 生态已经在扩展,Spring AI 作为基础设施层加入进来,而 Spring 之父 Rod Johnson 又带来了 Embabel------一个站在 Spring AI 肩膀上的智能体框架。
Spring 生态远不止是一个 IoC 容器------它是一个完整的企业级开发生态系统。Spring 生态的真正价值不在于它提供了多少功能,而在于两个核心点:
•一是降低认知负担,你不需要学 10 种不同的数据访问方式,Spring Data 用一套统一的抽象解决了这个问题。同样,Spring Security 用一套方式解决了各种安全场景。
•二是渐进式能力叠加,你可以先只用 Spring Boot,然后需要数据访问时加 Spring Data,需要安全时加 Spring Security,需要微服务时加 Spring Cloud。每一步都是渐进的,不会让你重写代码。

Spring AI:AI 集成的基础设施层
Spring AI 是基础设施层,不是应用框架层,Spring AI 的价值不是提供新的大模型,而是通过统一抽象、自动配置和生态集成,降低 Java 应用接入模型、RAG 与工具调用的成本,如果直接写代码调用 OpenAI 的 API,需要处理 API 认证、处理各种模型参数、解析 JSON 响应、处理流式输出、换模型提供商时(比如从 OpenAI 换到 Anthropic)需要重写一堆代码适配不同的模型,Spring AI 就是来解决这些问题的,它提供了一套统一的抽象,让我们可以用同样的代码调用不同的 AI 模型。它的核心设计理念是:将 Spring 生态系统设计原则(如可移植性和模块化设计)应用于 AI 领域,并推广使用 POJO(Plain Ordinary Java Object,简单的 Java 对象 / 普通 JavaBeans)作为 AI 领域应用程序的构建块。Spring AI 用 Java 开发者熟悉的方式在做 AI。

Spring AI 的核心能力
1.多模型提供商支持 支持 OpenAI、Anthropic、Amazon Bedrock、Google Vertex AI、Ollama 等模型平台。应用通过统一方式调用不同模型,切换模型通常只需调整依赖或配置。
2.统一的模型抽象 就像 JDBC 统一了数据库访问方式,Spring AI 为不同厂商的模型提供统一 API。开发者不必针对每个模型重复编写适配代码。
3.完整的 RAG 支持 集成多种向量数据库,并提供文档加载、内容分块、Embedding、向量存储、相似度检索和上下文增强等能力,帮助开发者快速构建企业知识库问答。
4.Tool Calling 工具调用 可以将现有 Java 方法、业务服务和外部 API 暴露给 LLM,让模型根据用户意图选择并调用工具,将语言理解与真实业务操作连接起来。
5.Spring Boot 自动配置 通过 Starter 引入依赖,配置模型地址和 API Key,即可获得所需的模型客户端与相关组件,减少大量初始化和适配代码。
6.与 Spring 生态无缝集成 可以直接复用 Spring 的依赖注入、配置管理、AOP、事务、安全、监控和测试能力,这正是熟悉的"Spring 开发体验"。
Embabel:Spring 生态中的 Agent 框架
Embabel站在 Spring AI 肩膀上的智能体编排框架,最底层是 Spring 生态,我们已经拥有的一切。中间层是 Spring AI,AI 集成的基础设施层。最上层是 Embabel,智能体编排的应用框架层。

为什么需要 Embabel?而不是直接用 Spring AI?
1.更高级的动态规划 Spring AI 提供模型调用能力,但复杂任务的执行顺序通常仍需开发者自己控制。 Embabel 引入 Goal、Action、Condition 和 Planner,根据当前状态动态生成计划,并在每个 Action 完成后重新规划。它不仅支持 GOAP,还支持 Utility、Supervisor 等规划策略。
css
Spring AI:开发者编写 A → B → C
Embabel:定义目标和能力,Planner 动态选择 A、B、C 的组合
1.更好的扩展性和复用性 传统 Workflow 增加一个步骤,往往需要修改原有流程图或条件分支。 Embabel 可以通过增加新的:Action、Goal、Condition、Domain Model、扩展系统能力,而不一定需要修改已有 Action。
原有能力:搜索 → 分析 → 生成报告
新增能力:数据库查询
Planner 可以自动判断是否将"数据库查询"加入计划
1.强类型和领域模型 很多 Agent 框架主要通过字符串、Map 或 JSON 在节点之间传递信息。 Embabel 使用 Java/Kotlin 类型作为 Action 的输入输出:
typescript
@Action
public Analysis analyze(
UserQuestion question,
KnowledgeContext context
) {
return new Analysis(...);
}
•它带来的价值包括:
•编译期类型检查
•IDE 自动补全
•安全重构
•明确的 Action 输入输出契约
•领域对象可以包含业务行为
•Planner 也可以根据类型关系判断 Action 是否具备执行条件
1.编程模型与运行平台分离 Embabel 将 Agent 的定义与底层运行机制分开:
sql
编程模型:Agent、Goal、Action、Condition
运行平台:AgentPlatform、AgentProcess、Planner、Blackboard
•同一套 Agent 代码可以在本地运行,也可以接入生产环境中的:状态持久化
•生命周期管理
•重试机制
•事件监听
•可观测性
•分布式执行能力
1.支持多模型组合 复杂 Agent 不一定所有任务都应该使用同一个模型。 Embabel 可以针对不同 Action 使用不同模型进行平衡:
◦效果
◦成本
◦延迟
◦隐私
◦稳定性
2.直接复用 Spring 和 JVM 生态 Embabel 本身基于 Spring,因此 Agent 可以直接使用企业应用已有能力:
◦Spring 依赖注入
◦Spring AOP
◦事务管理
◦Spring Data
◦Spring Security
◦数据库和缓存
◦消息中间件
◦企业内部 Java 服务
3.从设计阶段支持测试 直接使用 LLM 编写复杂执行逻辑时,业务代码、Prompt、工具调用和流程控制容易混在一起。 Embabel 将任务拆成独立 Action,因此可以分别测试:
◦普通 Java 业务逻辑
◦单个 Action
◦Prompt
◦Tool 调用
◦Planner 生成的路径
◦Agent 端到端执行结果
6.代码对比:手动编排 vs 自动规划
Spring AI(手动编排),自己写流程的每一步:先做什么,再做什么
arduino
@Service
public class SpringAIWeatherService {
private final ChatClient chatClient;
private final WeatherApiClient weatherApi;
public SpringAIWeatherService(ChatClient chatClient,
WeatherApiClient weatherApi) {
this.chatClient = chatClient;
this.weatherApi = weatherApi;
}
public String getWeatherResponse(String city) {
// 步骤1:先调用天气API
WeatherData weather = weatherApi.fetch(city);
// 步骤2:构建提示词
String prompt = String.format(
"用自然语言描述天气: %s", weather);
// 步骤3:调用LLM
return chatClient.prompt(prompt).call().content();
}
}
Embabel(自动规划),用 Embabel,你只需要定义你有什么 Action(能力),以及你想达成什么 Goal(目标)。然后 Embabel 自动规划:需要调用哪些 Action,按什么顺序调用。

二、Embabel 核心架构

接入层
接入层负责把任务交给 Embabel,这一层只负责"任务从哪里来",不负责决定任务怎样完成:
•REST API
•Spring Shell
•定时任务
•MQ 消息
•业务代码直接调用
•其他 Agent 调用
Agent Platform 平台层
AgentPlatform 是整个框架的运行平台,类似 Embabel 的"容器"。
主要职责包括:
•Agent 注册与发现
•Agent、Goal 的选择
•创建 AgentProcess
•管理执行生命周期
•发布执行事件
•异常处理
•可观测性支持
三种常见运行模式:
| 模式 | 含义 |
|---|---|
| Focused | 调用方明确指定 Agent 或 Goal |
| Closed | 平台从注册的 Agent 中动态选择 |
| Open | 平台组合可用 Goal 和 Action 完成任务 |
可以把 AgentPlatform 类比为:
Spring 容器负责管理 Bean,AgentPlatform 负责管理和运行 Agent
描述"系统具备哪些智能能力"
Agent 定义层
"系统具备哪些智能能力"
Agent
Agent 是一个领域能力集合,内部组织 Goal、Action 和 Condition。
kotlin
@Agent(description = "负责处理客户工单")
public class TicketAgent {
}
Agent 更像一个能力边界,并不等于每一次运行实例。
Goal
Goal 描述期望得到的最终结果:
完成工单处理
生成研究报告
制定旅行计划
它强调"要得到什么",不规定"严格按照什么顺序执行"。
Action
Action 是 Planner 可以组合和选择的基本执行单元:
typescript
@Action
public ClassifiedTicket classify(Ticket ticket) {
// 分类逻辑
}
Action 可以执行普通 Java 代码,也可以调用:
•LLM
•Tool
•MCP
•RAG
•数据库
•远程服务
•Subagent
Condition
Condition 表示当前是否满足某个条件:
工单是否完成分类
是否已经找到知识
是否需要人工审批
Goal 是否已经完成
它可以作为 Action 的前置条件,也可以作为 Goal 的完成条件。
Domain Model
领域模型是 Action 之间交换的类型化数据:
arduino
public record Ticket(String id, String content) {}
public record ClassifiedTicket(
Ticket ticket,
String category
) {}
public record ResolvedTicket(
String id,
String resolution
) {}
类型不仅是数据结构,也参与规划过程。
规划与运行时层
Embabel 的核心
AgentProcess
AgentProcess 表示一次具体的 Agent 运行。
Agent:能力定义,可以被重复使用
AgentProcess:某一次任务的运行实例
例如,处理 100 个工单:
1 个 TicketAgent 定义
100 个独立的 AgentProcess
100 个独立的 Blackboard
Blackboard
Blackboard 它是类型化的共享工作区,保存当前任务的实际数据:
•用户输入
•已知事实
•领域对象
•Action 中间结果
•工具调用结果
•RAG 检索结果
•条件状态
•最终结果
初始 Blackboard
└── Ticket
执行分类 Action 后
├── Ticket
└── ClassifiedTicket
执行知识检索 Action 后
├── Ticket
├── ClassifiedTicket
└── KnowledgeContext
执行处理 Action 后
├── Ticket
├── ClassifiedTicket
├── KnowledgeContext
└── ResolvedTicket
因此,Blackboard 可以理解为"上下文",但它比普通 Prompt Context 更精确:
| 普通 LLM Context | Blackboard |
|---|---|
| 主要是文本消息 | 主要是类型化对象和状态 |
| 给 LLM 阅读 | 给 Planner、Action、Condition 共同使用 |
| 受 Token 窗口限制 | 可以独立于 Prompt 长期保存 |
| 语义比较松散 | 可以按名称、类型进行绑定 WorldState |
WorldState 是根据 Blackboard 推导出来的逻辑状态:
ini
ticketReceived = true
ticketClassified = true
knowledgeFound = false
ticketResolved = false
| Blackboard | WorldState |
|---|---|
| 保存对象和事实 | 保存条件的判断结果 |
| 数据视角 | 规划视角 |
| ClassifiedTicket 对象 | ticketClassified=true |
Planner
Planner 根据以下信息选择可执行路径:
•当前 WorldState
•当前 Blackboard
•目标 Goal
•可用 Action
•Action 前置条件与效果
•Action 成本和价值
•类型依赖关系
Action Runner
Action Runner 负责真正执行 Planner 选出的 Action,包括:
•从 Blackboard 绑定方法参数
•调用 Action 方法
•处理返回值
•将输出写入 Blackboard
•记录执行事件
•处理异常和结果
规划策略层
Planner 是可替换的,Action、Goal 和 Blackboard 不必因为规划算法变化而重写。同一组 Action 可以由不同 Planner 调度。
| Planner | 决策方式 |
|---|---|
| GOAP | 根据前置条件、效果和成本寻找通往 Goal 的路径 |
| Utility | 选择当前净效用最高的 Action |
| Hybrid | Utility 式选择 Action,Goal 达成后停止 |
| Supervisor | LLM 根据 Action 描述和类型动态选择调用顺序 |
能力与集成层
这一层是 Action 执行时可以使用的能力。
LLM / AI
负责:
•语义理解
•内容生成
•结构化对象生成
•信息抽取
•分类和判断
•Agentic Tools
Agentic Tool 是提供给 LLM 或 Agent 使用的能力,协调其他工具的工具,例如:
•SimpleAgenticTool
•PlaybookTool
•StateMachineTool
| 类型 | 控制方式 | 适用场景 |
|---|---|---|
| SimpleAgenticTool | 所有子工具立即可见 | 简单探索和短链路组合 |
| PlaybookTool | 满足条件后逐步解锁工具 | 调研、审核、分阶段任务 |
| StateMachineTool | 根据明确状态开放工具 | 订单、审批、发布等状态流程 |

示例代码
scss
SimpleAgenticTool researchTool =
new SimpleAgenticTool(
"researchTopic",
"搜索资料并生成调研报告"
)
.withTools(
searchTool,
ragTool,
summarizeTool
);
context.ai()
.withTool(researchTool)
.generateText("分析新能源汽车行业");
MCP
MCP 是外部工具的标准接入方式:
arduino
Embabel Action / LLM
↓
MCP Client
↓
MCP Server
↓
GitHub、数据库、搜索、文件系统
MCP 属于能力接入层,不负责 Embabel 的顶层规划。
RAG
RAG 为 Agent 提供企业知识:
知识库检索 → 相关文档 → Action/LLM → 类型化结果
RAG 是知识获取能力,不是 Planner。
Subagent
Subagent 是可以被委派子任务的独立 Agent:
研究 Agent
编码 Agent
审核 Agent
数据分析 Agent
它通常通过以下方式接入:
•Action 调用 Subagent
•Supervisor 调度 Subagent
•将 Subagent 包装成 Tool
Subagent 属于多 Agent 协作能力,不是基础 GOAP 模型的必要组成部分
基础设施层
Embabel 构建在 Spring 和 JVM 生态之上,因此可以直接复用:
•Spring Bean 和依赖注入
•Spring AI
•模型提供商
•数据库与事务
•向量数据库
•HTTP Client
•消息中间件
•OpenTelemetry
•Metrics 和 Tracing
这一层负责"能力怎么落地",上层负责"能力如何组合"。
三、Embabel核心流程
三种模式的核心区别,是谁来决定 Agent 和 Goal,以及允许使用多大的能力范围
| 模式 | Agent 谁选择 | Goal 谁选择 | Action 范围 | 确定性 |
|---|---|---|---|---|
| Focused | 业务代码指定 | 由指定 Agent 决定 | 指定 Agent 内部 | 最高 |
| Closed | LLM 根据意图选择 | 被选 Agent 内部 | 只能使用被选 Agent | 中等 |
| Open | 不固定单个 Agent | LLM 从全部 Goal 中选择 | 可以组合多个 Agent 的 Action | 最低 |
Focused:明确指定 Agent
核心思想:
业务代码明确知道要运行哪个 Agent,不需要 LLM 选择 Agent。
适合:
•REST API
•定时任务
•Webhook
•订单、支付、审批等确定性业务
•对审计和稳定性要求较高的场景
css
业务代码
↓ 明确指定
TicketAgent
↓ 内部规划
Action A → Action B → Goal
示例代码:
arduino
@Agent(description = "处理客户工单")
public class TicketAgent {
public record TicketRequest(String id, String content) {}
public record ClassifiedTicket(
String id,
String content,
String category
) {}
public record TicketResult(
String id,
String resolution
) {}
@Action(description = "对工单进行分类")
public ClassifiedTicket classify(TicketRequest request) {
String category = request.content().contains("宕机")
? "CRITICAL"
: "GENERAL";
return new ClassifiedTicket(
request.id(),
request.content(),
category
);
}
@AchievesGoal(description = "完成工单处理")
@Action(description = "生成工单处理方案")
public TicketResult resolve(ClassifiedTicket ticket) {
String resolution = switch (ticket.category()) {
case "CRITICAL" -> "立即升级到值班工程师";
default -> "发送知识库解决方案";
};
return new TicketResult(ticket.id(), resolution);
}
}
@Service
public class FocusedTicketService {
private final AgentPlatform agentPlatform;
public FocusedTicketService(AgentPlatform agentPlatform) {
this.agentPlatform = agentPlatform;
}
public TicketAgent.TicketResult handle(
TicketAgent.TicketRequest request
) {
// 1. 明确找到要运行的 Agent
Agent ticketAgent = agentPlatform.agents()
.stream()
.filter(agent ->
agent.getName().contains("TicketAgent"))
.findFirst()
.orElseThrow();
// 2. 创建本次运行实例
AgentProcess process =
agentPlatform.createAgentProcessFrom(
ticketAgent,
ProcessOptions.DEFAULT,
request
);
// 3. 同步执行
AgentProcess completed = process.run();
// 4. 取得类型化结果
return completed.last(TicketAgent.TicketResult.class);
}
}
Closed:动态选择一个 Agent
核心思想:
LLM 根据用户意图,从所有已注册 Agent 中选择最合适的一个;选中后,只能运行该 Agent 内部的 Action 和 Goal。
适合:
•智能客服入口
•企业助手
•多业务意图路由
•用户使用自然语言,但仍需保持 Agent 边界
假设系统中存在:
TicketAgent:处理故障和工单
OrderAgent:处理订单和退款
KnowledgeAgent:回答制度和产品问题
用户输入
"生产数据库宕机了,帮我提交紧急工单"
Closed 模式会选择 TicketAgent,之后不会调用 OrderAgent 或 KnowledgeAgent 的 Action。
示例代码:
arduino
@Service
public class ClosedModeService {
private final Autonomy autonomy;
public ClosedModeService(Autonomy autonomy) {
this.autonomy = autonomy;
}
public AgentProcessExecution execute(String userIntent) {
return autonomy.chooseAndRunAgent(
userIntent,
ProcessOptions.DEFAULT
);
}
}
-- 调用
AgentProcessExecution execution =
closedModeService.execute(
"生产数据库宕机了,帮我提交紧急工单"
);
sql
用户意图
↓
LLM 对所有 Agent 排名
↓
选择 TicketAgent
↓
只使用 TicketAgent 的 Goal、Action 和 Condition
Open:选择 Goal 并组合多个 Agent
核心思想:
LLM 不是选择一个固定 Agent,而是从所有 Goal 中选择最符合用户意图的目标,然后组合系统中可用的 Action 达成目标。
适合:
•跨领域研究
•多 Agent 协作
•开放式任务
•无法提前确定执行路径的复杂问题
•需要动态组合能力的场景
假设有三个 Agent:
CustomerAgent
└── 查询客户信息
KnowledgeAgent
└── 检索知识库
TicketAgent
├── 分析故障
└── 生成处理方案
用户提出:
"查询客户等级,结合历史工单和知识库,
分析这次数据库故障并生成处理方案。"
Open 模式可以组合:
erlang
CustomerAgent.queryCustomer
KnowledgeAgent.searchKnowledge
TicketAgent.analyzeIncident
TicketAgent.createResolution
它不受单一 Agent 边界限制
四、OODA 与 GOAP 原理
OODA 解决"Agent 如何持续适应变化",GOAP 解决"Agent 如何规划一条通向目标的行动路径"。
二者不是竞争关系,而是不同层次:
OODA:外层决策循环
GOAP:循环中 Decide 阶段使用的规划算法
OODA:持续观察和调整的决策循环
OODA 由美国空军上校 John Boyd 提出,最初用于描述动态对抗环境中的快速决策过程,包含四个阶段:
Observe:观察
获取当前环境以及上一步执行结果。 在 Agent 中可能包括:
•用户的新输入
•Tool 调用结果
•MCP 返回数据
•RAG 检索结果
•Action 执行结果
•外部系统状态
•异常和失败信息
例如:
观察到:
工单内容包含"生产数据库无法连接"
客户等级为 VIP
SLA 只剩 10 分钟
Orient:判断
把原始信息转换成对当前情况的理解。
这一阶段并不只是"读取数据",还要结合:
•当前上下文
•领域知识
•历史经验
•业务规则
•风险与约束
•已经完成的动作
例如:
markdown
数据库无法连接 + VIP 客户 + SLA 即将超时
↓
判断为:高优先级生产故障
在 Embabel 中,可以类比为:
markdown
Blackboard 中的对象
↓
WorldStateDeterminer
↓
Condition / WorldState
Decide:决策
根据当前状态决定下一步采取什么行动。
决策方式可以是:
•固定 Workflow
•规则引擎
•状态机
•Utility AI
•LLM Supervisor
•GOAP
如果使用 Embabel 默认规划器,这一步主要由 GOAP 完成。
Act:行动
执行选定的 Action,例如:
•查询知识库
•调用 MCP Tool
•查询数据库
•发送通知
•创建工单
•调用 Subagent
•生成人工审批任务
Action 执行完成后,不是直接机械执行原计划的剩余步骤,而是返回 Observe,重新观察结果。
OODA 的关键不是"四个步骤",而是循环
传统流程通常是:
制定计划 → 从头执行到尾
OODA 强调的是:
执行一步
↓
观察结果
↓
重新判断
↓
调整下一步
例如,Agent 原来准备自动回复用户:
计划:分类 → 查询知识库 → 自动回复
查询知识库后发现没有可靠答案:
新观察:知识库没有匹配方案
新判断:自动回复风险过高
新决策:转人工专家
新的执行路径变为:
分类 → 查询知识库 → 转人工专家
所以,OODA 的价值在于:
•应对不确定结果
•根据反馈及时调整
•避免错误计划执行到底
•支持异常恢复
•适合动态环境
GOAP:面向目标的行动规划
GOAP 全称:Goal-Oriented Action Planning,面向目标的行动规划。
GOAP 不要求开发人员写死完整执行顺序,而是声明四类信息:
•当前状态 State
•目标状态 Goal
•可执行动作 Action
•动作的前置条件、执行效果和成本
GOAP模型
当前状态
当前状态可以表示为一组事实:
ini
ticketReceived = true
ticketClassified = false
knowledgeFound = false
ticketResolved = false
在 Embabel 中:
•Blackboard 保存真实对象
•WorldState 保存由对象推导出的条件状态
ini
Blackboard:
Ticket{id="T-1001"}
WorldState:
ticketReceived = true
ticketClassified = false
目标
Goal 描述期望达到的状态:
ini
ticketResolved = true
或者通过类型表示:
Blackboard 中存在 ResolvedTicket
Action
每个 Action 可以抽象为:
ini
Action = 前置条件 + 执行效果 + 执行成本
例如:
| Action | 前置条件 | 执行效果 | 成本 |
|---|---|---|---|
| 分类工单 | 已收到工单 | 工单已分类 | 1 |
| 查询知识库 | 工单已分类 | 找到知识 | 2 |
| 自动处理 | 找到知识 | 工单已解决 | 1 |
| 人工处理 | 工单已分类 | 工单已解决 | 5 |
Plan
Planner 需要寻找一组 Action
css
当前状态
──执行 P──>
目标状态
GOAP 如何搜索路径
以工单处理为例,存在两条路径。
路径一:人工处理
ini
分类工单 → 人工处理
成本:
1 + 5 = 6
路径二:自动处理
ini
分类工单 → 查询知识库 → 自动处理
成本:
1 + 2 + 1 = 4
GOAP 会优先选择成本更低的第二条路径:
A* 的基本思想
A* 搜索通过下面的评分选择下一条候选路径:
scss
f(n) = g(n) + h(n)
其中:
g(n):到达当前状态已经付出的成本
h(n):从当前状态到目标的预计剩余成本
f(n):整条路径的预计总成本
Planner 不需要尝试所有组合,而是优先搜索更可能以低成本达到 Goal 的路径
代码示例
typescript
@Agent(description = "处理客户支持工单")
public class TicketAgent {
public record Ticket(
String id,
String description
) {}
public record ClassifiedTicket(
Ticket ticket,
String category
) {}
public record KnowledgeResult(
String solution
) {}
public record ResolvedTicket(
String id,
String resolution
) {}
@Action(
description = "分析工单内容并完成分类",
cost = 0.1
)
public ClassifiedTicket classify(Ticket ticket) {
String category =
ticket.description().contains("数据库")
? "DATABASE"
: "GENERAL";
return new ClassifiedTicket(ticket, category);
}
@Action(
description = "根据工单分类查询知识库",
cost = 0.2
)
public KnowledgeResult searchKnowledge(
ClassifiedTicket ticket
) {
return new KnowledgeResult(
"检查数据库连接池和网络配置"
);
}
@AchievesGoal(description = "工单已经处理完成")
@Action(
description = "根据知识库结果解决工单",
cost = 0.1
)
public ResolvedTicket resolve(
ClassifiedTicket ticket,
KnowledgeResult knowledge
) {
return new ResolvedTicket(
ticket.ticket().id(),
knowledge.solution()
);
}
}
类型关系已经形成:
Ticket
↓ classify
ClassifiedTicket
↓ searchKnowledge
KnowledgeResult
↓ resolve
ResolvedTicket
因为 resolve() 需要:
ClassifiedTicket + KnowledgeResult
Planner 会反向分析:
要得到 ResolvedTicket
→ 需要执行 resolve
要执行 resolve
→ Blackboard 必须有 ClassifiedTicket 和 KnowledgeResult
要得到 KnowledgeResult
→ 需要执行 searchKnowledge
要执行 searchKnowledge
→ 必须先得到 ClassifiedTicket
最终生成计划:
classify → searchKnowledge → resolve
五、Blackboard:类型化共享状态
一个 Agent 在执行过程中,数据到底放在哪里?用户输入放哪儿?中间结果放哪儿?每一步的执行状态怎么保存?在 Embabel 里,这一切数据都由一个核心组件来负责:Blackboard(黑板)
什么是Blackboard
Agent 执行过程中的一张共享工作台,保存输入、Action 中间结果、工具产物和最终结果,并通过 Java 类型组织这些数据。
每个 Action:
•从 Blackboard 获取自己需要的输入对象;
•执行业务逻辑或调用 LLM;
•返回新的类型化对象;
•框架自动把返回对象放回 Blackboard;
•Planner 根据 Blackboard 当前拥有的类型,决定下一步可以执行哪些 Action。
Blackboard设计模式
Blackboard 是 Embabel 中的共享内存系统,维护整个 Agent 过程执行期间的状态。你可以把它想象成 Agent 的工作台,所有需要的数据、中间结果都放在这里。每个 Action 执行时从这里取需要的输入,执行完后把输出放回去。
Blackboard 的几个核心特性:
•统一管理运行状态
◦Blackboard 是 Agent 执行过程中的"中央工作台",统一保存用户输入、中间结果和最终产物。Action 不需要层层传递参数,只需声明自己需要的数据类型,框架就会自动从 Blackboard 中获取。
•按类型存取数据
◦Blackboard 主要通过对象类型匹配数据,而不是依赖容易出错的字符串 Key。例如,Action 需要 WeatherRequest 或 SearchResult,框架就会查找对应类型的对象。这种方式更符合面向对象设计,也具备更好的类型安全性。
•默认使用最新数据
◦Blackboard 会记录数据的加入顺序。同一类型存在多个对象时,框架默认获取最近加入的对象。例如,先后产生两次天气查询结果,后续 Action 默认使用最新结果。旧数据并未被覆盖,仍保留在执行历史中。
•保留完整执行轨迹
◦对象写入 Blackboard 后通常不会被直接删除;不再参与规划的数据可以被隐藏。通过保留各阶段产生的对象,可以减少状态被意外覆盖的风险,并支持问题排查、过程审计和执行回溯。
•保存并驱动任务条件
◦Blackboard 不仅保存业务对象,也维护影响规划的条件状态,例如"天气数据是否已经获取""用户是否已经登录""订单是否通过审核"。Planner 会结合当前对象和条件,判断哪些 Action 可以执行,并选择下一步操作。
场景示例
假设正在开发一个电商平台的客服工单系统,用户通过简单文本提交问题:
sql
user-456 支付服务不可用,我无法完成付款
系统需要:
1.从文本中提取用户 ID 和问题描述;
2.自动验证用户身份;
3.创建客服工单;
4.分析问题等级;
5.根据诊断结果完成处理;
6.返回最终工单处理结果;
完整类型链路:

领域对象
arduino
package com.example.ticket.domain;
// 从用户输入中提取的工单请求
public record TicketRequest(
String userId,
String description
) {
}
// 验证完成的用户
public record VerifiedUser(
String userId,
String customerLevel,
boolean verified
) {
}
// 已创建的工单
public record Ticket(
String ticketId,
String userId,
String description,
String status
) {
}
// 工单诊断结果
public record Diagnosis(
String ticketId,
String severity,
String reason
) {
}
// 最终处理结果
public record ResolvedTicket(
String ticketId,
String userId,
String resolution,
String handledBy,
String status
) {
}
业务服务
用户服务
typescript
package com.example.ticket.service;
import com.example.ticket.domain.VerifiedUser;
import org.springframework.stereotype.Service;
@Service
public class UserService {
public VerifiedUser verify(String userId) {
// 示例中固定认为 user-456 是 VIP 用户
if ("user-456".equals(userId)) {
return new VerifiedUser(
userId,
"VIP",
true
);
}
return new VerifiedUser(
userId,
"NORMAL",
true
);
}
}
工单服务
java
package com.example.ticket.service;
import com.example.ticket.domain.Ticket;
import org.springframework.stereotype.Service;
import java.util.UUID;
@Service
public class TicketService {
public Ticket create(
String userId,
String description
) {
String ticketId =
"T-" + UUID.randomUUID()
.toString()
.substring(0, 8);
return new Ticket(
ticketId,
userId,
description,
"CREATED"
);
}
}
升级服务
typescript
package com.example.ticket.service;
import org.springframework.stereotype.Service;
@Service
public class EscalationService {
public void escalate(
String ticketId,
String team,
String reason
) {
System.out.printf(
"工单 %s 已升级到 %s,原因:%s%n",
ticketId,
team,
reason
);
}
}
Embabel Agent
java
package com.example.ticket.agent;
import com.embabel.agent.api.annotation.AchievesGoal;
import com.embabel.agent.api.annotation.Action;
import com.embabel.agent.api.annotation.Agent;
import com.embabel.agent.domain.io.UserInput;
import com.example.ticket.domain.Diagnosis;
import com.example.ticket.domain.ResolvedTicket;
import com.example.ticket.domain.Ticket;
import com.example.ticket.domain.TicketRequest;
import com.example.ticket.domain.VerifiedUser;
import com.example.ticket.service.EscalationService;
import com.example.ticket.service.TicketService;
import com.example.ticket.service.UserService;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Agent(description = "接收用户问题并自动完成工单处理")
public class CustomerSupportAgent {
private static final Pattern USER_ID_PATTERN =
Pattern.compile("(user-\d+)");
private final UserService userService;
private final TicketService ticketService;
private final EscalationService escalationService;
public CustomerSupportAgent(
UserService userService,
TicketService ticketService,
EscalationService escalationService
) {
this.userService = userService;
this.ticketService = ticketService;
this.escalationService = escalationService;
}
/**
* 第一步:理解用户输入
*
* Blackboard:
* UserInput → TicketRequest
*/
@Action(description = "从用户文本中提取用户ID和工单问题")
public TicketRequest extractTicketRequest(
UserInput userInput
) {
String content = userInput.getContent();
Matcher matcher =
USER_ID_PATTERN.matcher(content);
if (!matcher.find()) {
throw new IllegalArgumentException(
"用户输入中缺少用户ID,例如 user-456"
);
}
String userId = matcher.group(1);
String description = content
.replaceFirst(Pattern.quote(userId), "")
.trim();
if (description.isBlank()) {
throw new IllegalArgumentException(
"工单问题描述不能为空"
);
}
return new TicketRequest(
userId,
description
);
}
/**
* 第二步之一:验证用户
*
* Blackboard:
* TicketRequest → VerifiedUser
*/
@Action(description = "验证提交工单的用户身份")
public VerifiedUser verifyUser(
TicketRequest request
) {
return userService.verify(
request.userId()
);
}
/**
* 第二步之二:创建工单
*
* Blackboard:
* TicketRequest → Ticket
*/
@Action(description = "根据用户问题创建客服工单")
public Ticket createTicket(
TicketRequest request
) {
return ticketService.create(
request.userId(),
request.description()
);
}
/**
* 第三步:诊断工单
*
* Blackboard:
* Ticket → Diagnosis
*/
@Action(description = "分析工单问题并判断严重等级")
public Diagnosis diagnoseTicket(
Ticket ticket
) {
String description =
ticket.description().toLowerCase();
if (description.contains("支付")
&& description.contains("不可用")) {
return new Diagnosis(
ticket.ticketId(),
"P1",
"支付核心服务不可用"
);
}
if (description.contains("无法付款")
|| description.contains("付款失败")) {
return new Diagnosis(
ticket.ticketId(),
"P2",
"用户支付失败"
);
}
return new Diagnosis(
ticket.ticketId(),
"P3",
"一般业务问题"
);
}
/**
* 第四步:完成工单处理
*
* 需要 Blackboard 中同时存在:
* Ticket
* VerifiedUser
* Diagnosis
*
* 返回 ResolvedTicket 并达成 Goal
*/
@AchievesGoal(
description = "用户工单已经完成处理"
)
@Action(
description = "根据用户身份和诊断结果处理工单",
pre = {
"spel:verifiedUser.verified == true"
}
)
public ResolvedTicket resolveTicket(
Ticket ticket,
VerifiedUser verifiedUser,
Diagnosis diagnosis
) {
String handledBy;
String resolution;
if ("P1".equals(diagnosis.severity())) {
handledBy = "PAYMENT_ON_CALL_TEAM";
resolution = "立即升级到支付系统值班团队";
escalationService.escalate(
ticket.ticketId(),
handledBy,
diagnosis.reason()
);
} else if ("P2".equals(diagnosis.severity())) {
handledBy = "PAYMENT_SUPPORT_TEAM";
resolution = "转交支付客服团队处理";
} else {
handledBy = "CUSTOMER_SUPPORT_TEAM";
resolution = "发送标准问题解决方案";
}
// VIP 用户可以采用更高的处理优先级
if ("VIP".equals(
verifiedUser.customerLevel()
)) {
resolution = "[VIP 优先] " + resolution;
}
return new ResolvedTicket(
ticket.ticketId(),
verifiedUser.userId(),
resolution,
handledBy,
"RESOLVED"
);
}
}
调用Agent
kotlin
package com.example.ticket.controller;
import com.embabel.agent.api.AgentInvocation;
import com.embabel.agent.core.AgentPlatform;
import com.embabel.agent.domain.io.UserInput;
import com.example.ticket.domain.ResolvedTicket;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/support")
public class SupportController {
private final AgentPlatform agentPlatform;
public SupportController(
AgentPlatform agentPlatform
) {
this.agentPlatform = agentPlatform;
}
@PostMapping("/tickets")
public ResolvedTicket submit(
@RequestBody String text
) {
var invocation = AgentInvocation.create(
agentPlatform,
ResolvedTicket.class
);
return invocation.invoke(
new UserInput(text)
);
}
}

六、Action 与类型驱动规划
开发者定义"能做什么",规划器根据当前已有的数据类型和目标,动态决定"按什么顺序做"。它不是让 LLM 自由编排工具,也不是要求开发者预先写死工作流;Embabel 会把 Action 的方法签名转换为规划关系,并默认使用 GOAP(Goal-Oriented Action Planning)寻找可执行路径。每个 Action 完成后,系统都会重新评估状态并规划下一步。
Action 是带类型契约的能力
Action 通过方法参数声明执行前所需的领域类型,通过返回值声明执行后产生的领域类型,使 Embabel 能依据类型契约自动规划执行路径。
arduino
import com.embabel.agent.api.annotation.AchievesGoal;
import com.embabel.agent.api.annotation.Action;
import com.embabel.agent.api.annotation.Agent;
import com.embabel.agent.api.common.Ai;
// ---------- 领域类型 ----------
public record UserRequest(String text) {}
public record CustomerId(long value) {}
public record Customer(
CustomerId id,
String name,
String email
) {}
public record OrderHistory(
CustomerId customerId,
int orderCount,
double totalAmount
) {}
public record CustomerReport(String content) {}
@Agent(description = "生成客户分析报告")
public class CustomerReportAgent {
private final CustomerRepository customerRepository;
private final OrderRepository orderRepository;
public CustomerReportAgent(
CustomerRepository customerRepository,
OrderRepository orderRepository
) {
this.customerRepository = customerRepository;
this.orderRepository = orderRepository;
}
/**
* 类型契约:
*
* 前置输入:UserRequest
* 执行结果:CustomerId
*/
@Action
public CustomerId extractCustomerId(
UserRequest request,
Ai ai
) {
return ai.withDefaultLlm()
.createObject(
"""
从下面的用户请求中提取客户 ID:
%s
""".formatted(request.text()),
CustomerId.class
);
}
/**
* 类型契约:
*
* 前置输入:CustomerId
* 执行结果:Customer
*/
@Action
public Customer loadCustomer(
CustomerId customerId
) {
return customerRepository
.findById(customerId.value())
.orElseThrow(() ->
new IllegalArgumentException(
"Customer not found: " + customerId.value()
)
);
}
/**
* 类型契约:
*
* 前置输入:Customer
* 执行结果:OrderHistory
*/
@Action
public OrderHistory loadOrderHistory(
Customer customer
) {
return orderRepository.summarizeByCustomerId(
customer.id().value()
);
}
/**
* 类型契约:
*
* 前置输入:Customer + OrderHistory
* 执行结果:CustomerReport
*
* @AchievesGoal 表示执行完成后目标达成。
*/
@AchievesGoal(
description = "生成包含客户信息和消费情况的分析报告"
)
@Action
public CustomerReport createReport(
Customer customer,
OrderHistory orderHistory,
Ai ai
) {
String prompt = """
根据以下信息生成客户分析报告。
客户:
- 姓名:%s
- 邮箱:%s
订单:
- 订单数量:%d
- 总消费金额:%.2f
分析客户价值,并给出后续运营建议。
""".formatted(
customer.name(),
customer.email(),
orderHistory.orderCount(),
orderHistory.totalAmount()
);
return ai.withDefaultLlm()
.createObject(prompt, CustomerReport.class);
}
}
规划器看到的不是方法调用顺序,而是下面这些类型契约:
makefile
extractCustomerId:
UserRequest → CustomerId
loadCustomer:
CustomerId → Customer
loadOrderHistory:
Customer → OrderHistory
createReport:
Customer + OrderHistory → CustomerReport(目标)
什么是类型驱动规划
当 Blackboard 初始只有 UserRequest,而目标是 CustomerReport 时,规划器可以推导出:
markdown
UserRequest
↓ extractCustomerId
CustomerId
↓ loadCustomer
Customer
↓ loadOrderHistory
OrderHistory
↓ createReport,需要 Customer + OrderHistory
CustomerReport
类型不能表达的规则交给 Condition
类型只能说明"数据存在",不能说明"数据符合业务要求"。
例如,拥有 Budget 对象并不意味着预算足够:
arduino
import com.embabel.agent.api.annotation.Action;
import com.embabel.agent.api.annotation.Condition;
import com.embabel.agent.api.annotation.ConditionalOnCondition;
public record Budget(double amount) {}
public record Hotel(
String id,
String name,
double price
) {}
public record Reservation(
String reservationId,
String hotelId
) {}
public class HotelBookingAgent {
/**
* 显式业务条件:
* 有 Budget 和 Hotel 对象,不代表预算一定足够。
*/
@Condition("hasEnoughBudget")
public boolean hasEnoughBudget(
Budget budget,
Hotel hotel
) {
return budget.amount() >= hotel.price();
}
/**
* 只有 hasEnoughBudget 条件成立时,
* 该 Action 才能被规划和执行。
*/
@Action
@ConditionalOnCondition("hasEnoughBudget")
public Reservation bookHotel(
Hotel hotel,
Budget budget
) {
return new Reservation(
"RES-10001",
hotel.id()
);
}
}
建模时最重要的原则
更好的建模方式是使用具有明确业务含义的领域类型,避免所有 Action 都使用 String:
arduino
public record UserInput(String text) {}
public record SearchRequest(String keywords) {}
public record SearchResults(List<String> items) {}
public record ResearchReport(String content) {}
七、Tools 与 Agentic Tools
在 Embabel 中:
•普通 Tool:一个可以被 LLM 调用的具体操作。
•Agentic Tool:外表仍是一个 Tool,但内部会启动另一个 LLM 工具调用循环,由内部 LLM 编排多个子 Tools。
关键差别不在于"是否使用 LLM 选择工具"------普通 Tool 也可能由外层 LLM 选择;区别在于:
普通 Tool 的内部是一次具体执行;Agentic Tool 的内部是一个能够继续推理、选择并调用子工具的小型 Agent 循环。
Embabel 提供三种 Agentic Tool:SimpleAgenticTool、PlaybookTool 和 StateMachineTool
普通 Tool
普通 Tool 通常完成一个边界清晰的原子操作,例如:
•查询客户;
•计算金额;
•调用天气 API;
•创建订单;
•读取文件;
•更新邮箱。
执行原理
典型过程是:
1.Embabel 把 Tool 名称、描述和参数 JSON Schema 发给模型。
2.LLM 决定是否调用 Tool,并生成结构化参数。
3.Embabel 将参数绑定到 JVM 方法。
4.方法执行确定性的 Java/Kotlin 逻辑。
5.返回值序列化后交给 LLM。
6.外层 LLM 根据结果继续回答或调用其他 Tool。
这里的"确定性"是指 Tool 本身按代码执行;数据库、网络等外部系统当然仍可能产生不同结果。
Agentic Tool
Agentic Tool 自身实现了 Tool 接口,但它的 Handler 不是简单调用一个业务方法,而是启动一个内部 PromptRunner
官方描述的内部过程是:
1.获取当前 AgentProcess。
2.按指定的 LlmOptions 创建内部 PromptRunner。
3.将子 Tools 加入内部 PromptRunner。
4.使用输入启动 LLM---Tool 循环。
5.把内部 LLM 的最终结果作为 Agentic Tool 的结果返回。
所以它会产生嵌套调用:
sql
外层 LLM
└── 调用 research-assistant
└── 内层 LLM
├── 调用 search
├── 调用 fetch
└── 调用 summarize
Agentic Tool解决什么问题
Agentic Tool 是为了把"一组需要 LLM 动态判断和多步调用的工具"封装成一个可复用的高层 Tool。
普通 Tool 解决的是:
执行一个明确操作。
Agentic Tool 解决的是:
面对一个局部目标,动态决定调用哪些工具、以什么顺序调用,以及何时结束。
例如"研究一家公司"并不是一次原子操作,可能需要:
搜索公司
→ 读取官网
→ 查询财务数据
→ 补充搜索管理层信息
→ 交叉验证
→ 生成摘要
实际执行顺序取决于中途得到的信息,不适合写成单个普通 Tool,也不一定值得建立一套完整 GOAP Agent。此时可以将其封装成:
markdown
research-company Agentic Tool
└── 内部 LLM
├── searchWeb
├── fetchPage
├── queryFinancialData
└── summarize
外层 LLM 只需要调用:
scss
research-company("OpenAI")
主要解决的问题:
1.普通 Tool 粒度太小
如果外层 LLM 直接面对几十个底层工具:
sql
search
fetch
parse
filter
calculate
validate
format
save
...
模型容易:
•选错工具;
•遗漏步骤;
•参数传递错误;
•在相似工具之间混淆;
•消耗大量 Tool Schema Token。
Agentic Tool 把它们包装成一个高层能力:
sql
底层:search + fetch + analyze + summarize
对外:research
1.固定代码无法处理动态路径
实际研究可能需要根据中间结果动态决定:
搜索结果不足 → 换关键词再次搜索
发现官方文档 → 优先读取官方文档
内容相互冲突 → 搜索第二来源
信息已经充分 → 停止继续搜索
1.提供分层编排
Agentic Tool 形成了两层决策:
markdown
外层 LLM:决定调用哪个高层能力
↓
Agentic Tool:决定如何完成这个局部任务
↓
普通 Tool:执行具体操作
八、MCP:Agent 的外部能力协议
MCP(Model Context Protocol)是一套让 AI 应用统一发现、读取和调用外部能力的协议。REST API 让应用调用服务,MCP 让 Agent 能够理解并调用服务。
MCP 不是工具本身,也不负责规划,而是规定:
•外部系统如何描述自己有哪些能力;
•工具需要哪些参数;
•Agent 如何调用工具;
•工具如何返回结果和错误;
•外部资源和 Prompt 如何提供给 AI;
为什么需要 MCP?
没有 MCP 时,每接入一个外部系统都要单独适配:
Agent → GitHub SDK
Agent → 数据库驱动
Agent → Jira API
Agent → 企业内部 HTTP API
Agent → 文件系统 API
每套接口的鉴权、参数和返回格式都不一样。
有了 MCP 后:
arduino
Agent
↓
统一的 MCP Client
↓
不同的 MCP Server
├── GitHub
├── Jira
├── 数据库
├── 文件系统
└── 企业工单系统
MCP Server 把不同系统包装成统一协议,Agent 不再关心底层使用 REST、数据库还是第三方 SDK。
可以把 MCP 理解成 Agent 世界里的 USB-C:
USB-C 统一设备连接方式
MCP 统一 Agent 能力连接方式
核心架构
三个核心角色:
Host
承载 Agent 和 LLM 的应用,例如:
•Embabel 应用
•IDE
•AI 桌面客户端
•企业智能助手
Host 负责:
•管理 MCP Client;
•决定向 LLM 暴露哪些能力;
•管理权限和用户确认;
•将工具结果交回 LLM或Agent。
Client
运行在 Host 内部,负责与某个 MCP Server 通信。
通常是:
arduino
一个 MCP Client ↔ 一个 MCP Server
Server
对外暴露具体能力,例如:
verify_user
create_ticket
get_ticket
notify_on_call
MCP 采用 Host--Client--Server 架构,Server 只获得完成调用所需的信息,不应默认看到完整对话或其他 Server 的数据。
九、RAG 与 Agentic RAG
普通 RAG 是"系统先查一次,再让 LLM 回答";Agentic RAG 是"把检索能力交给 Agent,由 Agent 决定查什么、查几次、使用哪种检索方式以及什么时候停止"。在 Agent 中使用 RAG,不一定就是 Agentic RAG。
如果检索步骤和查询语句仍然是固定的,本质上还是普通 RAG。
普通 RAG
普通 RAG 原理
RAG 是 Retrieval-Augmented Generation,即检索增强生成。
它分为两个阶段。
知识入库
例如讲以下知识库入库:
文档1:
支付服务返回 PAY-503 时,首先检查支付网关状态。
如果网关不可用,应升级 PAYMENT_ON_CALL_TEAM。
文档2:
VIP 用户的 P1 工单需要在 5 分钟内响应。
文档3:
支付失败但网关正常时,应检查用户账户和支付渠道。
检索并回答
检索过程通常是固定的:
用户问题
→ 向量检索一次
→ 返回 TopK 文档
→ 拼接 Prompt
→ LLM 回答
普通 RAG 的局限
用户提问:
VIP 用户支付失败,错误码 PAY-503,
昨天升级过但今天又出现了,应该怎么处理?
一次向量检索可能遇到以下问题:
•用户问题过长,检索重点不明确;
•"PAY-503"适合关键词检索,不一定适合向量检索;
•需要同时查询故障手册、SLA 和历史工单;
•查到一个片段后,还需要读取它的上下文;
•第一次检索结果不足,但系统不会主动再查;
•检索得到冲突信息时,没有验证环节。
这些问题引出了 Agentic RAG。
Agentic RAG
Agentic RAG 原理
Agentic RAG 把"检索"从固定的前置步骤变成 Agent 可以调用的工具。
核心循环是:
思考检索目标
→ 选择检索工具
→ 构造查询
→ 观察结果
→ 判断信息是否充分
→ 继续检索或生成答案
对比总结
| 对比项 | 普通 RAG | Agentic RAG |
|---|---|---|
| 是否检索 | 每次固定检索 | Agent 判断 |
| 查询内容 | 通常直接使用用户问题 | Agent 可拆分、改写查询 |
| 检索次数 | 通常一次 | 可以多次 |
| 检索方式 | 通常单一向量检索 | 向量、全文、正则、多数据源 |
| 结果判断 | 直接交给 LLM | Agent 可以评估是否充分 |
| 上下文扩展 | 固定 TopK | 可以展开相邻块和父章节 |
| 执行路径 | 固定 | 动态 |
| 延迟和成本 | 较低 | 较高 |
| 可控性 | 较高 | 相对较低 |
| 复杂问题效果 | 一般 | 通常更好 |
十、完整业务案例
智能保险平台项目,是一个面向车险场景的 AI Agent 演示系统:用户可以用自然语言发起投保或咨询,系统由核保 Agent、理赔 Agent、客服 Agent 协同,将大模型的语言理解能力与可审计的业务规则、人工审批、数据库事务和权限控制结合起来,演示从"投保---报价---审批---支付---出单---理赔---结案"的完整业务闭环。
项目背景
业务痛点
传统车险流程通常存在以下问题:
•客户输入是自然语言或非结构化描述,业务系统要求结构化字段,人工录入成本高。
•核保和理赔包含大量规则判断,同时存在需要人工复核的灰区。
•条款、理赔指南和 FAQ 分散,客服检索慢、回答一致性不足。
•单纯使用大模型直接决策,结果不可控、不可审计,也无法可靠执行数据库和交易操作。
•单纯使用固定工作流,又难以处理语言表达差异和复杂上下文。
解决思路
项目采用"LLM 负责理解,规则负责决策,Agent 负责规划,服务负责执行,人工负责兜底"的分工:
| 能力 | 主要责任 |
|---|---|
| LLM | 从自然语言提取车辆、事故信息;基于知识库生成答案 |
| Embabel Agent | 根据当前状态和目标规划动作,完成状态路由 |
| Java 规则服务 | 计算核保风险、保费和欺诈风险,保证结果确定、可测试 |
| Spring Service/JPA | 查询客户车辆、保存报价/保单/理赔单、处理事务 |
| 人工岗位 | 处理中风险报价和中风险理赔 |
| Guardrail(护栏) + Security | 输入输出校验、认证、权限隔离和越权指令检测 |
业务价值
•自动结构化:降低投保和报案录入成本。
•自动分流:低风险自动通过,高风险自动拒绝,中风险交给人工。
•人机协同:AI 不替代必要的审批岗位,而是聚焦标准场景和前置判断。
•知识一致:客服回答优先引用本地保险文档,减少无依据回答。
•工程可控:关键金额与状态变更由 Java 代码完成,不让 LLM 直接操作核心交易。
•可测试:规则、Agent 动作、集成流程和真实模型 E2E 分层验证。
目标用户与角色
| 角色 | 业务诉求 | 当前系统能力 |
|---|---|---|
| 投保客户 | 快速获得车险报价、支付并查看保单 | 自然语言投保、保单查询、知识问答 |
| 核保员 | 处理系统无法自动批准的中风险申请 | 审批 REFERRED 报价、调整保费、填写备注 |
| 理赔员 | 审核疑似风险但不足以自动拒绝的案件 | 审核 INVESTIGATING 理赔,批准或拒绝 |
| 管理员 | 管理知识库和访问全部业务能力 | 文档摄入/重建、全部接口权限 |
| 技术人员 | 学习和验证 Java Agent 工程模式 | Agent 状态路由、RAG、护栏、测试、可观测性配置 |
业务案例
案例 A:低风险客户自动投保并完成小额理赔
客户 Alice,41 岁、15 年驾龄、1 次事故,为 2022 年 Toyota RAV4 投保。
1.核保员或业务渠道提交自然语言投保申请。
2.LLM 提取车型、品牌和车牌。
3.系统查询 Alice 与车辆档案。
4.规则引擎算出风险分 15,命中低风险区间 ≤ 60。
5.系统自动创建 APPROVED 报价。
6.综合险保费:300000 × 2% × 0.8 × 1.0 = 4800 元。
7.客户支付后,系统签发一年期 ACTIVE 保单。
8.后续客户提交小额理赔;若欺诈分 < 30,系统自动批准。
9.赔付金额不超过 年保费 × 5 的当前演示上限。
业务结果:标准低风险业务实现端到端自动化。
案例 B:中风险客户由人工核保和人工理赔
客户 Bob,27 岁、4 年驾龄、2 次事故,为 2018 年 Honda Civic 投保。
1.风险评分为 63,命中中风险区间 60 < score < 80。
2.系统创建 REFERRED 报价,等待核保员处理。
3.核保员可保留系统保费,也可输入调整后的保费并填写意见。
4.审批后报价变为 APPROVED,客户才能支付并获得保单。
5.客户后续提交中等风险理赔;若欺诈分为 30--69,生成 INVESTIGATING 理赔单。
6.理赔员通过专用审核接口作出 APPROVED 或 DENIED 终态决定。
业务结果:AI 完成资料理解和风险预判,人类保留灰区案件的最终决策权。
案例 C:高风险客户自动拒保
客户 Charlie,21 岁、1 年驾龄、3 次事故,为 2013 年 BMW X5 投保。
1.原始风险累计超过 100,系统钳制为 100。
2.命中高风险区间 ≥ 80。
3.系统保存 DECLINED 报价,保费记为 0,并记录拒绝原因。
4.被拒报价不能支付,因此不会生成保单,也不能进入后续理赔链路。
业务结果:高风险申请在交易发生前被拦截。
案例 D:知识客服多轮问答
客户询问"综合险包含什么?"或"发生事故后如何理赔?":
1.ChatService 校验输入并创建/恢复用户会话。
2.ChatbotAgent 挂载 insurance_docs_textSearch 工具。
3.LLM 主动搜索保险条款、理赔指南和 FAQ。
4.LLM 阅读检索片段,按用户语言综合回答并提示文档来源。
5.服务保存最多 20 轮对话;会话 30 分钟无操作后过期。
业务结果:以企业知识为依据提供连续、统一的客户服务。
总体架构
分层职责
| 层次 | 组件 | 职责 |
|---|---|---|
| 接口层 | InsuranceController、ChatController、RagAdminController | HTTP 入参/出参、状态码、认证用户获取 |
| 编排层 | AgentService、ChatService | 启动 Agent、等待结果、超时、人工审核、会话管理 |
| Agent 层 | 3 个 Agent | 将业务目标拆解为动作,并依据状态选择后续路径 |
| 领域服务层 | Risk、Premium、Payment、Policy、Data 等 Service | 可预测、可单测的业务计算和事务操作 |
| 数据层 | JPA Repository + Entity | 客户、车辆、报价、保单、理赔单持久化 |
| AI/知识层 | DeepSeek、Lucene、ToolishRag | 语言理解、生成、知识检索 |
| 横切能力 | Security、Guardrail、Cache、日志 | 权限、安全、性能与诊断 |