Embabel 上手:Java Agent 是怎么干活的

109 阅读50分钟

一、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开发者否按预先定义的流程执行固定审批、支付、数据同步
ReActLLM通常不强制推理、调用工具、观察结果、继续推理搜索、排障、短链路探索
GOAP规划器是根据前置条件和效果搜索到 Goal 的路径目标明确、存在多条执行路径
Utility AI效用计算器否计算每个可执行 Action 的当前价值,选择最高者事件响应、持续探索、动态运营
Supervisor上层 LLM可以有根据语义动态选择 Action 或 Subagent开放任务、动态分工、多 Agent 协作
Workflow:预先定义流程

Workflow 由开发人员提前确定执行顺序和分支。优点是确定性强、容易测试和审计。缺点是路径需要提前穷举,环境变化时通常需要修改流程代码。

A → B → 判断条件 → C 或 D → E

适合:

•审批流程

•支付流程

•数据同步

•固定审核链路

•强合规业务

示例代码:

@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 的灵活性较高,但执行路径受模型影响,生产环境需要限制工具范围、调用次数、成本和副作用。

适合:

•开放式搜索

•故障排查

•数据探索

•短链路工具组合

•难以提前确定步骤的任务

示例代码:

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 决定每一个业务步骤。

适合:

•目标明确

•存在多条可达路径

•环境状态会发生变化

•需要重规划

•需要解释和测试执行路径

示例代码:

@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:

                       ┌─ 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 模式

适合高度开放、需要动态分工的任务,例如复杂调研、软件开发和跨领域分析。

它的灵活性最高,但模型调用成本、行为不确定性和治理难度也更高。

示例代码:

@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 看到的能力类似:

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。

•执行最高分: 智能体最终决定去执行最高分的行为(在这个例子中是“加血”)。

适合:

•智能客服和工单处理

◦根据工单状态选择处理策略:(立即升级、分配工程师、知识库自动回复、请求补充信息)

•游戏角色决策

◦候选动作:攻击、逃跑、治疗、寻找掩体。

◦状态变量:血量、敌人距离、弹药、危险程度

•告警和故障处置

◦系统出现多个告警时,动态决定优先处理哪个(数据库不可用、磁盘使用率、接口响应变慢、非核心服务异常)

示例代码:

@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 都可执行:

Actionvaluecost净价值
loadCustomerHistory0.800.100.70
collectDiagnostics0.900.300.60
classify0.700.050.65

所以第一步会选择:

loadCustomerHistory:0.70

执行后重新计算,再选择:

classify:0.65

然后:

collectDiagnostics:0.60

当所需对象齐备后,resolve() 才变成可执行 Action:

resolve:1.00 - 0.10 = 0.90

于是得到最终 ResolvedTicket。

官方定义的基础净效用计算为:

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 ModelAgent运行过程中使用和产生的领域对象用户需求、研究报告、审核结果
ActionAgent能够执行的一个步骤搜索资料、生成报告、审核内容
ConditionAction执行或Goal完成所依赖的条件已取得研究资料、报告已审核
GoalAgent最终需要达成的状态得到一份审核通过的报告
Plan为达成Goal动态组合出的Action序列搜索→撰写→审核→修改

一个简化的 Embabel Agent:

@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);
    }
}

框架可以根据方法参数和返回类型推导:

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 等规划策略。

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 的输入输出:

@Action
public Analysis analyze(
        UserQuestion question,
        KnowledgeContext context
) {
    return new Analysis(...);
}

•它带来的价值包括:

•编译期类型检查

•IDE 自动补全

•安全重构

•明确的 Action 输入输出契约

•领域对象可以包含业务行为

•Planner 也可以根据类型关系判断 Action 是否具备执行条件

1.编程模型与运行平台分离 Embabel 将 Agent 的定义与底层运行机制分开:

编程模型: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(手动编排),自己写流程的每一步:先做什么,再做什么

@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。

@Agent(description = "负责处理客户工单")
public class TicketAgent {
}

Agent 更像一个能力边界,并不等于每一次运行实例。

Goal

Goal 描述期望得到的最终结果:

完成工单处理
生成研究报告
制定旅行计划

它强调“要得到什么”,不规定“严格按照什么顺序执行”。

Action

Action 是 Planner 可以组合和选择的基本执行单元:

@Action
public ClassifiedTicket classify(Ticket ticket) {
    // 分类逻辑
}

Action 可以执行普通 Java 代码,也可以调用:

•LLM

•Tool

•MCP

•RAG

•数据库

•远程服务

•Subagent

Condition

Condition 表示当前是否满足某个条件:

工单是否完成分类
是否已经找到知识
是否需要人工审批
Goal 是否已经完成

它可以作为 Action 的前置条件,也可以作为 Goal 的完成条件。

Domain Model

领域模型是 Action 之间交换的类型化数据:

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 ContextBlackboard
主要是文本消息主要是类型化对象和状态
给 LLM 阅读给 Planner、Action、Condition 共同使用
受 Token 窗口限制可以独立于 Prompt 长期保存
语义比较松散可以按名称、类型进行绑定 WorldState

WorldState 是根据 Blackboard 推导出来的逻辑状态:

ticketReceived = true
ticketClassified = true
knowledgeFound = false
ticketResolved = false
BlackboardWorldState
保存对象和事实保存条件的判断结果
数据视角规划视角
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
HybridUtility 式选择 Action,Goal 达成后停止
SupervisorLLM 根据 Action 描述和类型动态选择调用顺序
能力与集成层

这一层是 Action 执行时可以使用的能力。

LLM / AI

负责:

•语义理解

•内容生成

•结构化对象生成

•信息抽取

•分类和判断

•Agentic Tools

Agentic Tool 是提供给 LLM 或 Agent 使用的能力,协调其他工具的工具,例如:

•SimpleAgenticTool

•PlaybookTool

•StateMachineTool

类型控制方式适用场景
SimpleAgenticTool所有子工具立即可见简单探索和短链路组合
PlaybookTool满足条件后逐步解锁工具调研、审核、分阶段任务
StateMachineTool根据明确状态开放工具订单、审批、发布等状态流程

示例代码

SimpleAgenticTool researchTool =
    new SimpleAgenticTool(
        "researchTopic",
        "搜索资料并生成调研报告"
    )
    .withTools(
        searchTool,
        ragTool,
        summarizeTool
    );

context.ai()
    .withTool(researchTool)
    .generateText("分析新能源汽车行业");

MCP

MCP 是外部工具的标准接入方式:

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 内部最高
ClosedLLM 根据意图选择被选 Agent 内部只能使用被选 Agent中等
Open不固定单个 AgentLLM 从全部 Goal 中选择可以组合多个 Agent 的 Action最低
Focused:明确指定 Agent

核心思想:

业务代码明确知道要运行哪个 Agent,不需要 LLM 选择 Agent。

适合:

•REST API

•定时任务

•Webhook

•订单、支付、审批等确定性业务

•对审计和稳定性要求较高的场景

业务代码
   ↓ 明确指定
TicketAgent
   ↓ 内部规划
Action A → Action B → Goal

示例代码:

@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。

示例代码:

@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(
                "生产数据库宕机了,帮我提交紧急工单"
        );
用户意图
   ↓
LLM 对所有 Agent 排名
   ↓
选择 TicketAgent
   ↓
只使用 TicketAgent 的 Goal、Action 和 Condition
Open:选择 Goal 并组合多个 Agent

核心思想:

LLM 不是选择一个固定 Agent,而是从所有 Goal 中选择最符合用户意图的目标,然后组合系统中可用的 Action 达成目标。

适合:

•跨领域研究

•多 Agent 协作

•开放式任务

•无法提前确定执行路径的复杂问题

•需要动态组合能力的场景

假设有三个 Agent:

CustomerAgent
└── 查询客户信息

KnowledgeAgent
└── 检索知识库

TicketAgent
├── 分析故障
└── 生成处理方案

用户提出:
“查询客户等级,结合历史工单和知识库,
分析这次数据库故障并生成处理方案。”

Open 模式可以组合:

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:判断

把原始信息转换成对当前情况的理解。

这一阶段并不只是“读取数据”,还要结合:

•当前上下文

•领域知识

•历史经验

•业务规则

•风险与约束

•已经完成的动作

例如:

数据库无法连接 + VIP 客户 + SLA 即将超时
                ↓
判断为:高优先级生产故障

在 Embabel 中,可以类比为:

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模型

当前状态

当前状态可以表示为一组事实:

ticketReceived = true
ticketClassified = false
knowledgeFound = false
ticketResolved = false

在 Embabel 中:

•Blackboard 保存真实对象

•WorldState 保存由对象推导出的条件状态

Blackboard:
Ticket{id="T-1001"}

WorldState:
ticketReceived = true
ticketClassified = false

目标

Goal 描述期望达到的状态:

ticketResolved = true

或者通过类型表示:

Blackboard 中存在 ResolvedTicket

Action

每个 Action 可以抽象为:

Action = 前置条件 + 执行效果 + 执行成本

例如:

Action前置条件执行效果成本
分类工单已收到工单工单已分类1
查询知识库工单已分类找到知识2
自动处理找到知识工单已解决1
人工处理工单已分类工单已解决5

Plan

Planner 需要寻找一组 Action

当前状态
  ──执行 P──>
目标状态
GOAP 如何搜索路径

以工单处理为例,存在两条路径。

路径一:人工处理

分类工单 → 人工处理

成本:
1 + 5 = 6

路径二:自动处理

分类工单 → 查询知识库 → 自动处理

成本:
1 + 2 + 1 = 4

GOAP 会优先选择成本更低的第二条路径:

A* 的基本思想

A* 搜索通过下面的评分选择下一条候选路径:

f(n) = g(n) + h(n)

其中:

g(n):到达当前状态已经付出的成本

h(n):从当前状态到目标的预计剩余成本

f(n):整条路径的预计总成本

Planner 不需要尝试所有组合,而是优先搜索更可能以低成本达到 Goal 的路径

代码示例
@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 可以执行,并选择下一步操作。

场景示例

假设正在开发一个电商平台的客服工单系统,用户通过简单文本提交问题:

user-456 支付服务不可用,我无法完成付款

系统需要:

1.从文本中提取用户 ID 和问题描述;

2.自动验证用户身份;

3.创建客服工单;

4.分析问题等级;

5.根据诊断结果完成处理;

6.返回最终工单处理结果;

完整类型链路:

领域对象

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
) {
}

业务服务

用户服务

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
        );
    }
}

工单服务

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"
        );
    }
}

升级服务

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

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

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 能依据类型契约自动规划执行路径。

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);
    }
}

规划器看到的不是方法调用顺序,而是下面这些类型契约:

extractCustomerId:
    UserRequest → CustomerId

loadCustomer:
    CustomerId → Customer

loadOrderHistory:
    Customer → OrderHistory

createReport:
    Customer + OrderHistory → CustomerReport(目标)
什么是类型驱动规划

当 Blackboard 初始只有 UserRequest,而目标是 CustomerReport 时,规划器可以推导出:

UserRequest
    ↓ extractCustomerId
CustomerId
    ↓ loadCustomer
Customer
    ↓ loadOrderHistory
OrderHistory
    ↓ createReport,需要 Customer + OrderHistory
CustomerReport
类型不能表达的规则交给 Condition

类型只能说明“数据存在”,不能说明“数据符合业务要求”。

例如,拥有 Budget 对象并不意味着预算足够:

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:

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 的结果返回。

所以它会产生嵌套调用:

外层 LLM
  └── 调用 research-assistant
        └── 内层 LLM
              ├── 调用 search
              ├── 调用 fetch
              └── 调用 summarize
Agentic Tool解决什么问题

Agentic Tool 是为了把“一组需要 LLM 动态判断和多步调用的工具”封装成一个可复用的高层 Tool。

普通 Tool 解决的是:

执行一个明确操作。

Agentic Tool 解决的是:

面对一个局部目标,动态决定调用哪些工具、以什么顺序调用,以及何时结束。

例如“研究一家公司”并不是一次原子操作,可能需要:

搜索公司
→ 读取官网
→ 查询财务数据
→ 补充搜索管理层信息
→ 交叉验证
→ 生成摘要

实际执行顺序取决于中途得到的信息,不适合写成单个普通 Tool,也不一定值得建立一套完整 GOAP Agent。此时可以将其封装成:

research-company Agentic Tool
  └── 内部 LLM
        ├── searchWeb
        ├── fetchPage
        ├── queryFinancialData
        └── summarize

外层 LLM 只需要调用:

research-company("OpenAI")

主要解决的问题:

1.普通 Tool 粒度太小

如果外层 LLM 直接面对几十个底层工具:

search
fetch
parse
filter
calculate
validate
format
save
...

模型容易:

•选错工具;

•遗漏步骤;

•参数传递错误;

•在相似工具之间混淆;

•消耗大量 Tool Schema Token。

Agentic Tool 把它们包装成一个高层能力:

底层:search + fetch + analyze + summarize
对外:research

1.固定代码无法处理动态路径

实际研究可能需要根据中间结果动态决定:

搜索结果不足 → 换关键词再次搜索
发现官方文档 → 优先读取官方文档
内容相互冲突 → 搜索第二来源
信息已经充分 → 停止继续搜索

1.提供分层编排

Agentic Tool 形成了两层决策:

外层 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 后:

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 通信。

通常是:

一个 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 可以调用的工具。

核心循环是:

思考检索目标
→ 选择检索工具
→ 构造查询
→ 观察结果
→ 判断信息是否充分
→ 继续检索或生成答案
对比总结
对比项普通 RAGAgentic RAG
是否检索每次固定检索Agent 判断
查询内容通常直接使用用户问题Agent 可拆分、改写查询
检索次数通常一次可以多次
检索方式通常单一向量检索向量、全文、正则、多数据源
结果判断直接交给 LLMAgent 可以评估是否充分
上下文扩展固定 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、RagAdminControllerHTTP 入参/出参、状态码、认证用户获取
编排层AgentService、ChatService启动 Agent、等待结果、超时、人工审核、会话管理
Agent 层3 个 Agent将业务目标拆解为动作,并依据状态选择后续路径
领域服务层Risk、Premium、Payment、Policy、Data 等 Service可预测、可单测的业务计算和事务操作
数据层JPA Repository + Entity客户、车辆、报价、保单、理赔单持久化
AI/知识层DeepSeek、Lucene、ToolishRag语言理解、生成、知识检索
横切能力Security、Guardrail、Cache、日志权限、安全、性能与诊断

代码链接

github.com/pysmell/emb…