1. 概述
现代应用越来越多地使用大语言模型(LLM)构建超越简单问答的解决方案。要实现大多数真实场景,我们需要一个能协调 LLM 与外部工具间复杂工作流的 AI 代理。
由 Spring Framework 创始人 Rod Johnson 创建的 Embabel Agent Framework,旨在通过在 Spring AI 之上提供更高级的抽象,简化在 JVM 上创建 AI 代理。
通过采用 目标导向行动规划(GOAP),它使代理能动态找到实现目标的路径,而无需显式编程每个工作流。
在本教程中,我们将通过构建一个名为 Quizzard 的基础测验生成代理来探索 Embabel 框架。我们的代理从博客文章 URL 获取内容,并基于此生成多项选择题。
2. 项目设置
在实现 Quizzard 代理之前,我们需要包含必要的依赖项并正确配置应用。
2.1. 依赖项
首先在项目的 pom.xml 文件中添加必要依赖:
<dependency>
<groupId>com.embabel.agent</groupId>
<artifactId>embabel-agent-starter</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>
对于 Spring Boot 应用,我们导入 embabel-agent-starter 依赖项,它提供了构建 Quizzard 代理所需的所有核心类。
由于我们使用的是库的快照版本,还需要在 pom.xml 中添加 Embabel 快照仓库:
<repositories>
<repository>
<id>embabel-snapshots</id>
<url>https://repo.embabel.com/artifactory/libs-snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
这里我们可以访问 Embabel 的快照构件,而不是标准的 Maven 中央仓库。
2.2. 配置 LLM 模型
接下来在 application.yaml 文件中配置聊天模型:
embabel:
models:
default-llm: claude-opus-4-20250514
演示中,我们指定 Anthropic 的 Claude 4 Opus 作为所有代理操作的默认模型,使用 claude-opus-4-20250514 模型 ID。
**值得注意的是,Embabel 支持同时使用多个 LLM**,通过这种方式我们可以实现成本效益,并根据任务复杂度选择最佳 LLM。
此外,运行应用时需要在 ANTHROPIC_API_KEY 环境变量中传递 Anthropic API 密钥。
2.3. 定义测验生成提示模板
最后,为确保 LLM 针对博客内容生成高质量测验,我们定义一个详细的提示模板。
在 src/main/resources/prompt-templates 目录下创建 quiz-generation.txt 文件:
Generate multiple choice questions based on the following blog content:
Blog title: %s
Blog content: %s
Requirements:
- Create exactly 5 questions
- Each question must have exactly 4 options
- Each question must have only one correct answer
- The difficulty level of the questions should be intermediate
- Questions should test understanding of key concepts from the blog
- Make the incorrect options plausible but clearly wrong
- Questions should be clear and unambiguous
这里我们明确概述了测验要求。提示模板中保留了两个 %s 占位符分别用于博客标题和内容,我们将在代理类中替换为实际值。
为简化起见,我们直接在提示模板中硬编码了问题数量、选项和难度级别。但在生产应用中,可以通过属性使这些可配置,或根据需求接受为用户输入。
3. 创建代理
在 Embabel 中,代理是核心组件,它封装一组称为动作(actions)的能力,并使用它们实现目标。
现在配置已就绪,我们来构建 Quizzard 代理。首先定义一个使用 模型上下文协议(MCP) 服务器从网络获取博客内容的动作,然后定义另一个使用配置的 LLM 从中生成测验的动作。
3.1. 使用网络搜索获取博客内容
**要从博客提取内容,我们将使用 Fetch MCP 服务器**。它提供从 URL 获取内容并转换为 markdown 的工具。也可以改用 Brave Search MCP 服务器。
演示中,我们将使用 MCP 工具包将此 MCP 服务器添加到 Docker Desktop,该工具包在 4.42 或更高版本中可用。如果使用旧版本,可以将 MCP 工具包添加为 Docker 扩展。
要使用此 MCP 服务器,首先将应用配置为 MCP 客户端:
@SpringBootApplication
@EnableAgents(mcpServers = {
McpServers.DOCKER_DESKTOP
})
class Application {
// ...
}
使用 @EnableAgents 注解,应用将能充当 MCP 客户端,并连接到通过 Docker Desktop 集成可用的工具。
接下来定义代理及其第一个动作:
@Agent(
name = "quizzard",
description = "Generate multiple choice quizzes from documents"
)
class QuizGeneratorAgent {
@Action(toolGroups = CoreToolGroups.WEB)
Blog fetchBlogContent(UserInput userInput) {
return PromptRunner
.usingLlm()
.createObject(
"Fetch the blog content from the URL given in the following request: '%s'".formatted(userInput),
Blog.class
);
}
}
这里我们使用 @Agent 注解标注 QuizGeneratorAgent 类,将其声明为代理。
我们在 description 属性中提供其用途,这有助于 Embabel 选择正确的代理处理用户请求,尤其是在应用中定义多个代理时。
接下来定义 fetchBlogContent() 方法并用 @Action 标注,将其标记为代理可执行的能力。此外,为授予动作访问启用的 fetch MCP 服务器的权限,我们在 toolGroups 属性中指定 CoreToolGroups.WEB。
方法内部,使用 PromptRunner 类传递提示,从用户输入中提取 URL 内容。为用提取的信息填充 Blog 对象,在 createObject() 方法中传递 Blog 类作为第二个参数。Embabel 自动向提示添加指令,使 LLM 生成结构化输出。
值得注意的是,Embabel 提供了 UserInput 和 Blog 领域模型,因此我们无需自己创建。UserInput 包含用户的文本请求和时间戳,而 Blog 类型包含博客标题、内容、作者等字段。
3.2. 从获取的博客生成测验
现在有了能获取博客内容的动作,下一步是定义生成测验的动作。
首先定义一个记录表示测验结构:
record Quiz(List<QuizQuestion> questions) {
record QuizQuestion(
String question,
List<String> options,
String correctAnswer
) {
}
}
Quiz 记录包含嵌套的 QuizQuestion 记录列表,每个记录包含 question、options 和正确答案。
最后向代理添加第二个动作,从获取的博客内容生成测验:
@Value("classpath:prompt-templates/quiz-generation.txt")
private Resource promptTemplate;
@Action
@AchievesGoal(description = "Quiz has been generated")
Quiz generateQuiz(Blog blog) {
String prompt = promptTemplate
.getContentAsString(Charset.defaultCharset())
.formatted(
blog.getTitle(),
blog.getContent()
);
return PromptRunner
.usingLlm()
.createObject(
prompt,
Quiz.class
);
}
创建新的 generateQuiz() 方法,将前一个动作返回的 Blog 对象作为参数。
除了 @Action,我们还用 @AchievesGoal 注解标注此方法,表示成功执行此动作即完成代理的主要目标。
方法中,用博客标题和内容替换 promptTemplate 中的占位符。然后再次使用 PromptRunner 类基于此 prompt 生成 Quiz 对象。
4. 与代理交互
构建好代理后,我们与之交互并进行测试。
首先在应用中启用交互式 shell 模式:
@EnableAgentShell
@SpringBootApplication
@EnableAgents(mcpServers = {
McpServers.DOCKER_DESKTOP
})
class Application {
// ...
}
我们使用 @EnableAgentShell 注解标注主 Spring Boot 类,这提供了与代理交互的交互式 CLI*。
或者,也可以使用 @EnableAgentMcpServer 注解将应用作为 MCP 服务器运行,Embabel 会将代理公开为 MCP 兼容工具,供 MCP 客户端使用。但演示中我们保持简单。
启动应用并向代理发出命令:
execute 'Generate quiz for this article: https://www.baeldung.com/spring-ai-model-context-protocol-mcp'
这里,我们使用 execute 命令向 Quizzard 代理发送请求。提供包含要创建测验的 Baeldung 文章 URL 的自然语言指令。
执行此命令时生成的日志如下:
[main] INFO Embabel - formulated plan
com.baeldung.quizzard.QuizGeneratorAgent.fetchBlogContent ->
com.baeldung.quizzard.QuizGeneratorAgent.generateQuiz
[main] INFO Embabel - executing action com.baeldung.quizzard.QuizGeneratorAgent.fetchBlogContent
[main] INFO Embabel - (fetchBlogContent) calling tool embabel_docker_mcp_fetch({"url":"https://www.baeldung.com/spring-ai-model-context-protocol-mcp"})
[main] INFO Embabel - received LLM response com.baeldung.quizzard.QuizGeneratorAgent.fetchBlogContent-com.embabel.agent.domain.library.Blog of type Blog from DefaultModelSelectionCriteria in 11 seconds
[main] INFO Embabel - executing action com.baeldung.quizzard.QuizGeneratorAgent.generateQuiz
[main] INFO Embabel - received LLM response com.baeldung.quizzard.QuizGeneratorAgent.generateQuiz-com.baeldung.quizzard.Quiz of type Quiz from DefaultModelSelectionCriteria in 5 seconds
[main] INFO Embabel - goal com.baeldung.quizzard.QuizGeneratorAgent.generateQuiz achieved in PT16.332321S
You asked: UserInput(content=Generate quiz for this article: https://www.baeldung.com/spring-ai-model-context-protocol-mcp, timestamp=2025-07-09T15:36:20.056402Z)
{
"questions":[
{
"question":"What is the primary purpose of the Model Context Protocol (MCP) introduced by Anthropic?",
"options":[
"To provide a standardized way to enhance AI model responses by connecting to external data sources",
"To replace existing LLM architectures with a new protocol",
"To create a new programming language for AI development",
"To establish a security protocol for AI model deployment"
],
"correctAnswer":"To provide a standardized way to enhance AI model responses by connecting to external data sources"
},
{
"question":"In the MCP architecture, what is the relationship between MCP Clients and MCP Servers?",
"options":[
"MCP Clients establish many-to-many connections with MCP Servers",
"MCP Clients establish 1:1 connections with MCP Servers",
"MCP Servers establish connections to MCP Clients",
"MCP Clients and Servers communicate through a central message broker"
],
"correctAnswer":"MCP Clients establish 1:1 connections with MCP Servers"
},
{
"question":"Which transport mechanism is used for TypeScript-based MCP servers in the tutorial?",
"options":[
"HTTP transport",
"WebSocket transport",
"stdio transport",
"gRPC transport"
],
"correctAnswer":"stdio transport"
},
{
"question":"What annotation is used to expose custom tools in the MCP server implementation?",
"options":[
"@Tool",
"@MCPTool",
"@Function",
"@Method"
],
"correctAnswer":"@Tool"
},
{
"question":"What type of transport is used for custom MCP servers in the tutorial?",
"options":[
"stdio transport",
"HTTP transport",
"SSE transport",
"TCP transport"
],
"correctAnswer":"SSE transport"
}
]
}
LLMs used: [claude-opus-4-20250514]
Prompt tokens: 3,053, completion tokens: 996
Cost: $0.0141
Tool usage:
ToolStats(name=embabel_docker_mcp_fetch, calls=1, avgResponseTime=1657 ms, failures=0)
日志清晰展示了代理的执行流程。
首先,我们看到 Embabel 自动正确地制定了实现定义目标的计划。
接着代理执行计划,从 fetchBlogContent 动作开始。此外可以看到它使用我们提供的 URL 调用了 fetch MCP 服务器。获取内容后,代理继续执行 generateQuiz 动作。
最后代理确认目标达成,并以 JSON 格式打印最终测验,以及令牌使用量和成本等有用指标。
5. 结论
本文中,我们探讨了如何使用 Embabel 代理框架构建智能代理。
我们构建了一个实用的多步骤代理 Quizzard,它能获取网络内容并从中生成测验。我们的代理能够动态确定实现目标所需的动作序列。
一如既往,本文使用的所有代码示例可在 GitHub 上获取。