embabel-agent
基于 JVM 的 agent 框架,用 Kotlin/Java 编写,可混用 LLM 与代码、领域模型来编排 agent 流程。亮点在于采用 GOAP 与 Utility AI 做动态规划,不同于常见 FSM 框架,能按目标自动组合已有步骤;由 Spring 创始人打造,深度集成 Spring Boot,并提供 Java/Kotlin DSL 与注解两种写法,适合企业级 Java 应用快速接入 AI 能力。
README
Embabel Agent Framework
Embabel (Em-BAY-bel) 是一个在 JVM 上编写 Agentic Flow(智能体流程)的框架,能够无缝地将 LLM 提示交互与代码和领域模型(domain model)相结合。支持面向目标的智能路径规划。使用 Kotlin 编写,但也为 Java 提供了自然的使用模型。 由 Spring 框架的创建者出品。
与文档对话
有问题?通过与 Embabel 驱动的 hub 与文档对话 — 这是一个 Embabel agent,可以用自然语言回答你关于该框架的问题。
核心概念
将 agentic flow 建模为以下要素:
- Actions(动作):agent 所采取的步骤
- Goals(目标):agent 试图达成的目的
- Conditions(条件):在执行某个动作之前或判定某个目标是否已达成之前需要评估的条件。 每个动作执行后都会重新评估条件。
- Domain model(领域模型):支撑流程并指导 Actions、Goals 和 Conditions 的对象。
- Plan(计划):为实现目标而执行的一系列动作序列。计划由系统动态制定,而非程序员预先编写。 系统在每个动作完成后会重新规划(replan),从而能够适应新信息并观察前一动作的效果。 这实际上就是一个 OODA loop(OODA 循环)。
应用程序开发者通常不需要直接处理这些概念, 因为大多数条件源于代码中定义的数据流,系统可以据此推断前置和后置条件。
这些概念构成了与其他 agent 框架相比的差异化优势:
- 复杂的规划能力。 超越了有限状态机(finite state machine)或带嵌套的顺序执行, 引入了真正的规划步骤,使用非 LLM 的 AI 算法。 这使得系统能够通过以新颖的顺序组合已知步骤来执行它未被编程去做的任务, 并能就并行化和其他运行时行为做出决策。
- 卓越的扩展性和复用性:由于动态规划,添加更多领域对象、动作、目标和条件 就能扩展系统的能力,_无需编辑 FSM 定义_或现有代码。
- 强类型和面向对象的好处:Actions、goals 和 conditions 由领域模型驱动, 领域模型可以包含行为。一切都是强类型的,提示词(prompt)和 手写代码能够干净地交互。不再有魔法映射(magic maps)。享受完整的重构支持。
其他优势:
- 平台抽象:编程模型与平台内部实现之间的清晰分离,允许在本地运行, 同时在生产环境中无需修改应用程序代码即可提供更高的 QoS。
- 为 LLM 混合而设计:很容易构建混合使用多种 LLM 的应用程序,确保获得最具成本效益 且能力最强的解决方案。这使得系统能够针对不同任务利用不同模型的优势。特别是,它促进 了在点状任务中使用本地模型。这对成本和隐私可能很重要。
- 基于 Spring 和 JVM 构建,使其易于访问现有企业功能和能力。
例如:
- Spring 可以注入和管理 agent,包括使用 Spring AOP 装饰函数。
- 提供健壮的持久化和事务管理解决方案。
- 从底层即为可测试性而设计。单元测试和 agent 端到端测试都很容易。
Flow 可以通过以下两种方式之一编写:
- 基于注解的模型,类似于 Spring MVC,使用 Spring 原型注解
@Agent标注类型,并使用@Goal、@Condition和@Action方法。 - 惯用的 Kotlin DSL,使用
agent {和action {代码块。
无论哪种方式,flow 都由具有丰富行为的领域对象模型支撑。
我们正在努力支持部署自然语言形式的动作和目标。
规划步骤是可插拔的。
默认的规划方法是 Goal Oriented Action Planning(面向目标的动作规划)。 GOAP 是一种在游戏中广泛使用的 AI 规划算法。它允许基于当前世界状态和 agent 的目标进行动态决策和动作选择。
Goals、actions 和 plans 与 GOAP 无关。Embabel 还开箱即用地支持 Utility AI(效用 AI),它可以运行相同的动作,但 基于(可能是动态的)效用评分(utility score)来选择动作,而不是严格的前置条件和后置条件。这对于探索和 开放式任务很有价值,当我们不需要达成特定目标,而是希望最大化整体效用时。
框架通过 AgentPlatform 实现来执行。
Agent 平台支持以下执行模式:
- **Focused(聚焦)**模式,用户代码请求特定功能:用户代码调用方法运行特定 agent, 传入输入。这非常适合代码驱动的流程,例如响应传入事件而调用的 flow。
- **Closed(封闭)**模式,用户意图(或其他传入事件)被分类以选择 agent。平台尝试在 它已知的所有 agent 中寻找合适的 agent。 Agent 的选择是动态的,但只会执行该特定 agent 内定义的动作。
- **Open(开放)**模式,平台评估用户意图并使用其 所有 资源来尝试实现该意图。平台尝试在
它已知的所有目标中寻找合适的目标,并从起始状态构建一个自定义 agent 来实现它,
包括相关的动作和条件。如果平台对任何目标的适用性不确定,它将不会继续。
GoalChoiceApprover接口为开发者提供了一种进一步限制目标选择的方式。
Open 模式是最强大的,但确定性也最低。
在 open 模式下,平台能够找到开发者未曾设想过的新路径,甚至 组合来自多个提供者的功能。
即使在 open 模式下,平台也只会执行 已经指定的单个步骤。(当然,步骤本身可能是 LLM 变换,在这种情况下提示词由用户代码控制,但 结果仍然是非确定性的。)
未来可能的模式:
- **Evolving(进化)**模式:平台可以在同一进程中处理多个目标,并修改运行中的进程以 添加更多目标和 agent。 例如,一个动作可能意识到实现附加目标变得重要。
Embabel agent 系统还将支持联邦(federation),既包括与其他 Embabel 系统的联邦(允许规划纳入 远程动作和目标),也包括与第三方 agent 框架的联邦。
快速开始
在 5 分钟内启动一个 agent。
通过点击 "Use this template" 按钮,从我们的 Java 或 Kotlin GitHub 模板创建你自己的 agent 仓库。
如果你已经拥有 OPENAI_API_KEY 并且安装了 Maven,
一分钟内就能运行一个 agent。
📚 获取示例和教程,请参阅 Embabel Agent 示例仓库
🚗 查看一个复杂、真实的示例应用,请参阅 Tripper 旅行规划 agent

AI 生成的旅行行程,带有详细推荐

输出中包含地图链接
为什么需要 Embabel?
简而言之:因为 agent 框架的演进还处于早期阶段,还有很大的改进空间;因为在 JVM 上的 agent 框架将带来巨大的商业价值。
- 我们为什么需要 agent 框架?我们可以不用更高级的抽象来编写代码,直接调用
LLM 并在代码中直接控制流程。然而,更高级的 agent 框架提供了令人信服的好处。例如:
- 分解 LLM 交互,使其更简单、更聚焦。这最大化了复用性,并最小化了成本和 错误。它通常允许我们为点状交互使用更便宜的模型。
- 促进单元测试和集成测试,这在 agentic 系统中与在任何其他软件系统中一样重要。
- 提高组合性,子流程和单个动作可以被复用
- 使应用程序更可管理、更健壮,使工作流管理器能够控制其执行并在保持先前状态的同时重试操作
- 通过在多个位置应用护栏(guardrails)来增强安全性
- 当 Python 已有解决方案时,为什么还需要 JVM 的 agent 框架?:虽然 agent 框架最初 主要出现在 Python 生态中,但现在还处于早期,新颖且更优的方法 仍有很大空间。关键邻近资产不是 LLM——它只是一个 HTTP 调用而已——而是现有代码和 基础设施资产,这些在 JVM 上比在 Python 上更有价值。
- 为什么不直接使用 Spring AI? Spring AI 很棒。我们构建于其之上,并拥抱 Spring 组件模型。然而,我们 认为大多数应用程序应该使用更高级的 API。打个比方:Spring AI 处于 Servlet API 的层次, 而 Embabel 更像 Spring MVC。复杂的需求在 Embabel 中比直接使用 Spring AI 更容易表达和测试。
- 为什么不尝试将这个项目贡献给 Spring? 这个项目需要与 Spring 不同的治理模式, Spring 中的大多数项目存在于稳定的环境中,可靠性和稳定性优先于快速创新。其次, 这些概念并非 JVM 特有。我们希望 Embabel 能成为跨平台领先的 agent 框架。虽然 Spring 品牌在 Java 中有价值,但在 TypeScript 或 Python 中则不然。
给我看代码
在 Java 或 Kotlin 中,agent 实现代码直观且易于测试。
Java
@Agent(description = "Find news based on a person's star sign")
public class StarNewsFinder {
private final HoroscopeService horoscopeService;
private final int storyCount;
// Services are injected by Spring
public StarNewsFinder(
HoroscopeService horoscopeService,
@Value("${star-news-finder.story.count:5}") int storyCount) {
this.horoscopeService = horoscopeService;
this.storyCount = storyCount;
}
@Action
public StarPerson extractStarPerson(UserInput userInput, Ai ai) {
return ai
.withLlm(OpenAiModels.GPT_41)
.createObjectIfPossible(
"""
Create a person from this user input, extracting their name and star sign:
%s""".formatted(userInput.getContent()),
StarPerson.class
);
}
@Action
public Horoscope retrieveHoroscope(StarPerson starPerson) {
return new Horoscope(horoscopeService.dailyHoroscope(starPerson.sign()));
}
// toolGroups specifies tools that are required for this action to run
@Action(toolGroups = {CoreToolGroups.WEB})
public RelevantNewsStories findNewsStories(
StarPerson person,
Horoscope horoscope,
Ai ai) {
var prompt = """
%s is an astrology believer with the sign %s.
Their horoscope for today is:
<horoscope>%s</horoscope>
Given this, use web tools and generate search queries
to find %d relevant news stories summarize them in a few sentences.
Include the URL for each story.
Do not look for another horoscope reading or return results directly about astrology;
find stories relevant to the reading above.
For example:
- If the horoscope says that they may
want to work on relationships, you could find news stories about
novel gifts
- If the horoscope says that they may want to work on their career,
find news stories about training courses.""".formatted(
person.name(), person.sign(), horoscope.summary(), storyCount);
return ai
.withDefaultLlm()
.createObject(prompt, RelevantNewsStories.class);
}
// The @AchievesGoal annotation indicates that completing this action
// achieves the given goal, so the agent can be complete
@AchievesGoal(
description = "Write an amusing writeup for the target person based on their horoscope and current news stories",
export = @Export(
remote = true,
name = "starNewsWriteupJava",
startingInputTypes = {StarPerson.class, UserInput.class})
)
@Action
public Writeup writeup(
StarPerson person,
RelevantNewsStories relevantNewsStories,
Horoscope horoscope,
Ai ai) {
var llm = LlmOptions
.withModel(OpenAiModels.GPT_41_MINI)
// High temperature for creativity
.withTemperature(0.9);
var newsItems = relevantNewsStories.getItems().stream()
.map(item -> "- " + item.getUrl() + ": " + item.getSummary())
.collect(Collectors.joining("\n"));
var prompt = """
Take the following news stories and write up something
amusing for the target person.
Begin by summarizing their horoscope in a concise, amusing way, then
talk about the news. End with a surprising signoff.
%s is an astrology believer with the sign %s.
Their horoscope for today is:
<horoscope>%s</horoscope>
Relevant news stories are:
%s
Format it as Markdown with links.""".formatted(
person.name(), person.sign(), horoscope.summary(), newsItems);
return ai
.withLlm(llm)
.createObject(prompt, Writeup.class);
}
}
Kotlin@Agent(description = "Find news based on a person's star sign")
class StarNewsFinder(
// Services such as Horoscope are injected by Spring
private val horoscopeService: HoroscopeService,
// Potentially externalized by Spring
@param:Value("\${star-news-finder.story.count:5}")
private val storyCount: Int = 5,
) {
@Action
fun extractPerson(
userInput: UserInput,
ai: Ai
): StarPerson =
// All prompts are typesafe
ai.withDefaultLlm()
.createObject("Create a person from this user input, extracting their name and star sign: $userInput")
// This action doesn't use an LLM
// Embabel makes it easy to mix LLM use with regular code
@Action
fun retrieveHoroscope(starPerson: StarPerson) =
Horoscope(horoscopeService.dailyHoroscope(starPerson.sign))
// This action uses tools
// "toolGroups" specifies tools that are required for this action to run
@Action(toolGroups = [ToolGroup.WEB])
fun findNewsStories(
person: StarPerson,
horoscope: Horoscope,
ai: Ai,
): RelevantNewsStories =
ai.withDefaultLlm().createObject(
"""
${person.name} is an astrology believer with the sign ${person.sign}.
Their horoscope for today is:
<horoscope>${horoscope.summary}</horoscope>
Given this, use web tools and generate search queries
to find $storyCount relevant news stories summarize them in a few sentences.
Include the URL for each story.
Do not look for another horoscope reading or return results directly about astrology;
find stories relevant to the reading above.
For example:
- If the horoscope says that they may
want to work on relationships, you could find news stories about
novel gifts
- If the horoscope says that they may want to work on their career,
find news stories about training courses.
""".trimIndent()
)
// The @AchievesGoal annotation indicates that completing this action
// achieves the given goal, so the agent run will be complete
@AchievesGoal(
description = "Write an amusing writeup for the target person based on their horoscope and current news stories",
)
@Action
fun writeup(
person: StarPerson,
relevantNewsStories: RelevantNewsStories,
horoscope: Horoscope,
ai: Ai,
): Writeup =
ai
.withLlm(
LlmOptions
.withModel(model)
.withTemperature(0.9)
)
.createObject(
"""
Take the following news stories and write up something
amusing for the target person.
Begin by summarizing their horoscope in a concise, amusing way, then
talk about the news. End with a surprising signoff.
${person.name} is an astrology believer with the sign ${person.sign}.
Their horoscope for today is:
<horoscope>${horoscope.summary}</horoscope>
Relevant news stories are:
${relevantNewsStories.items.joinToString("\n") { "- ${it.url}: ${it.summary}" }}
Format it as Markdown with links.
""".trimIndent()
)
}
以下领域类确保类型安全:
Java
@JsonClassDescription("Person with astrology details")
@JsonDeserialize(as = StarPerson.class)
public record StarPerson(
String name,
@JsonPropertyDescription("Star sign") String sign
) implements Person {
@JsonCreator
public StarPerson(
@JsonProperty("name") String name,
@JsonProperty("sign") String sign
) {
this.name = name;
this.sign = sign;
}
@Override
public String getName() {
return name;
}
}
public record Horoscope(String summary) {
}
@JsonClassDescription("Writeup relating to a person's horoscope and relevant news")
public record Writeup(String text) implements HasContent {
@JsonCreator
public Writeup(@JsonProperty("text") String text) {
this.text = text;
}
@Override
public String getContent() {
return text;
}
}
Kotlindata class RelevantNewsStories(
val items: List<NewsStory>
)
data class NewsStory(
val url: String,
val summary: String,
)
data class Subject(
val name: String,
val sign: String,
)
data class Horoscope(
val summary: String,
)
data class FunnyWriteup(
override val text: String,
) : HasContent
轻松对你的 agent 进行单元测试,以确保它们正确执行逻辑 并向 LLM 传递正确的提示词和超参数。例如:
public class StarNewsFinderTest {
@Test
void writeupPromptMustContainKeyData() {
HoroscopeService horoscopeService = mock(HoroscopeService.class);
StarNewsFinder starNewsFinder = new StarNewsFinder(horoscopeService, 5);
var context = new FakeOperationContext();
context.expectResponse(new com.embabel.example.horoscope.Writeup("Gonna be a good day"));
NewsStory cockatoos = new NewsStory(
"https://fake.com.au",
"Cockatoo behavior",
"Cockatoos are eating cabbages"
);
NewsStory emus = new NewsStory(
"https://morefake.com.au",
"Emu movements",
"Emus are massing"
);
StarPerson starPerson = new StarPerson("Lynda", "Scorpio");
RelevantNewsStories relevantNewsStories = new RelevantNewsStories(Arrays.asList(cockatoos, emus));
Horoscope horoscope = new Horoscope("This is a good day for you");
starNewsFinder.writeup(starPerson, relevantNewsStories, horoscope, context);
var prompt = context.getLlmInvocations().getFirst().getPrompt();
var toolGroups = context.getLlmInvocations().getFirst().getInteraction().getToolGroups();
assertTrue(prompt.contains(starPerson.getName()));
assertTrue(prompt.contains(starPerson.sign()));
assertTrue(prompt.contains(cockatoos.getSummary()));
assertTrue(prompt.contains(emus.getSummary()));
assertTrue(toolGroups.isEmpty(), "The LLM should not have been given any tool groups");
}
}
Dog Food Policy(自产自用原则)
我们相信软件开发和业务的各个方面都可以并且应该 通过使用 AI agent 得到极大加速。最终决策者仍然是人,但他们可以并且应该得到极大的增强。
本项目践行极端的 dogfooding(吃自己的狗粮,即自用自产)。
我们的关键原则:
- 我们将使用 AI agent 来帮助项目的每个方面: 编码、文档、社区管理、制作 营销文案等。任何执行任务的人都应该问为什么它不能自动化,并努力实现最大程度的自动化。
- 开发者保留最终控制权。 开发者负责引导 agent 走向解决方案,并在必要时 进行迭代。提交或合并 agent 贡献的开发者 有责任确保其符合项目编码标准,这些标准 与 agent 的使用无关。例如,代码必须是人类可读的。
- 我们将优先选择基于 Embabel 平台构建的开源 agent, 并贡献改进。虽然商业 agent 在某些领域可能更先进,但我们相信我们的 平台是自动化领域最好的通用解决方案,通过 dogfooding 我们将以最快的速度改进它。 通过开源我们在开源项目上使用的 agent,我们将为社区带来最大利益。
- 我们将优先考虑能加速我们自身进展的 agent。 根据飞行安全建议——先戴好自己的氧气面罩再帮助他人,我们将优先考虑 能加速我们自身进展的 agent。这不仅会产生有用的示例,还会提高整体项目速度。
开发者必须仔细阅读他们提交的所有代码,并在可能的情况下改进生成的代码。
编码 agent 是一个特例。虽然
embabel-agent-code子模块提供了对项目修改的支持, 这对项目引导(bootstrapping)很有用,但编码 agent 是商业 agent 中最成熟的品类,其供应商 正在大力补贴用户,这使得坚持使用我们自己的平台在经济上不合理。
入门
- 获取代码
- 设置你的环境
- 运行应用程序
获取代码
选择以下方式之一:
- 通过
git clone https://github.com/embabel/embabel-agent克隆仓库 - 创建一个新的 Spring Boot 项目并添加必要的依赖(请参阅下面的"在你的项目中使用 Embabel Agent Framework")
环境变量
环境变量与常见用法保持一致,而非 Spring AI 的命名方式。 例如,我们更倾向于使用
OPENAI_API_KEY而不是SPRING_AI_OPENAI_API_KEY。
必需:
OPENAI_API_KEY:用于 OpenAI API
可选:
ANTHROPIC_API_KEY:用于 Anthropic API。编码 agent 需要。MINIMAX_API_KEY:用于 MiniMax API。支持 MiniMax-M3、MiniMax-M2.7 和 MiniMax-M2.7-highspeed 模型。ZAI_API_KEY:用于 Z.ai(智谱 AI)API。支持 GLM-5.2、GLM-4.7、GLM-4.6、GLM-4.5-Air 和 GLM-4.7-Flash 模型。- OCI Generative AI 使用 OCI SDK 认证提供者。添加
embabel-agent-starter-oci-genai并设置embabel.agent.platform.models.ocigenai.compartment-id;支持 OCI 配置文件、实例主体(instance principal)、资源主体(resource principal)、 工作负载身份(workload identity)、会话令牌(session token)和简单密钥(simple key)认证。
我们强烈建议同时提供 OpenAI 和 Anthropic 的密钥,因为一些示例需要两者。而且,重要的是 尝试为给定任务找到最好的 LLM,而不是自动选择一个熟悉的提供者。
服务
你需要 Docker Desktop 版本 >4.43.2。
请务必从目录中激活以下 MCP 工具:
- Brave Search
- Fetch
- Puppeteer
- Wikipedia
你也可以使用 Spring AI 约定设置自己的 MCP 工具。参见
application-docker-desktop.yml文件中的示例。
如果你在本地运行 Ollama,加入 embabel ollama starter 后,Embabel 将自动连接到你的 Ollama
端点,并使所有模型可用。
<dependency>
<groupId>com.embabel.agent</groupId>
<artifactId>embabel-agent-starter-ollama</artifactId>
</dependency>
运行
使用以下命令创建你自己的 agent 项目:
uvx --from git+https://github.com/embabel/project-creator.git project-creator
示例 Agent
📚 获取示例和教程,请参阅 Embabel Agent 示例仓库
# 克隆并运行示例
git clone https://github.com/embabel/embabel-agent-examples
cd embabel-agent-examples/scripts/kotlin
./shell.sh
Shell 命令
Spring Shell 是与 Embabel agent 框架交互的一种简单方式,尤其是在开发期间。
输入 help 查看可用命令。使用 execute 或 x 运行 agent:
execute "Lynda is a Scorpio, find news for her" -p -r
这将查找一个 agent,选择 star finder agent 并
运行流程。-p 将记录提示词,-r 将记录 LLM 响应。
省略这些参数可获得更简洁的日志输出。
选项:
-p记录提示词-r记录 LLM 响应
使用 chat 命令与 agent 进行交互式聊天。
它将尝试为每条命令运行最合适的 agent。
Spring Shell 支持历史记录。输入
!!重复上一条命令。 该历史记录在重启后仍然保留,因此在迭代 agent 时非常方便。
更多示例
Shell 中的示例命令:
# Perplexity 风格的深度研究
# 需要 OpenAI 和 Anthropic 密钥,以及带有 MCP 扩展的 Docker Desktop(或你自己的 Web 工具)
execute "research the recent australian federal election. what is the position of the greens party?"
# x 是 execute 的快捷方式
x "fact check the following: holden cars are still made in australia; the koel is a bird native only to australia; fidel castro is justin trudeau's father"
引入其他 LLM
使用知名提供者的本地模型
Embabel Agent Framework 支持来自以下提供者的本地模型:
- Ollama:只需将
embabel-agent-starter-ollamastarter 添加到你的 pom.xml,你的本地 Ollama 端点就会被查询。所有本地模型都将 可用。 - Docker:将
embabel-agent-starter-dockermodelsstarter 添加到你的 pom.xml,你的本地 Docker 端点就会被查询。所有本地模型都将可用。 - LMStudio:这使用 openAI 兼容客户端。只需将 LMStudio 作为依赖项引入,并确保你的 LMStudio 服务器正在运行。
OCI Generative AI
添加 embabel-agent-starter-oci-genai 以使用 OCI Generative AI 聊天和嵌入(embedding)模型。
<dependency>
<groupId>com.embabel.agent</groupId>
<artifactId>embabel-agent-starter-oci-genai</artifactId>
</dependency>
配置 embabel.agent.platform.models.ocigenai.compartment-id,如果需要,设置
embabel.agent.platform.models.ocigenai.authentication-type 为 FILE、INSTANCE_PRINCIPAL、RESOURCE_PRINCIPAL、
WORKLOAD_IDENTITY、SESSION_TOKEN 或 SIMPLE。当标准 OpenAI 提供者不在 classpath 上时,OCI
starter 为 Embabel 的默认 LLM 和嵌入(embedding)模型提供 OCI 默认值:
emb
