Ohhnews

分类导航

$ cd ..
Baeldung原文

Spring AI 子代理协调指南

#spring ai#子代理协调#多代理系统#人工智能#技术教程

1. 概述

随着 AI 系统能力的增强,单一的“通才”智能体往往效率低下。它试图在一个上下文窗口中处理所有事务,导致提示词杂乱、响应变慢,输出质量下降。更好的做法是将职责拆分给专门的智能体,并让一个中央编排器来协调它们。

这正是 Spring AI 中子代理编排所实现的功能。利用 spring-ai-agent-utils 库中的 Task 工具,我们可以构建层次化的智能体系统,每个子代理在独立的上下文中工作,仅返回关键结果。

在本教程中,我们将使用 Spring 生态中的真实 API 从端到端实现这一模式。我们将学习编排的工作原理、如何配置子代理,以及如何构建一个能够动态委派任务的可用系统。

2. 理解子代理编排

子代理编排是一种模式,其中主 AI 智能体将工作委派给更小、更专门的智能体。每个子代理针对特定职责而设计,并在自己的上下文窗口中运行。这种隔离确保了提示词的聚焦,并防止无关信息污染推理过程。

与传统的服务编排不同,委派决策并非硬编码。主智能体利用 LLM 来决定何时应该委派任务。这一决策基于为每个子代理提供的自然语言描述,使系统灵活且自适应。这种方法改进了关注点分离、提示词清晰度、可维护性、可扩展性和模型专用化。

2.1. 为何使用专门的子代理?

专门的子代理帮助我们创建更模块化的 AI 系统。例如,在 AI 旅行助手中,一个子代理可以搜索航班,另一个推荐酒店,还有一个根据用户偏好构建个性化行程。这使得每个子代理可以专注于专门的职责,而不是依赖单个大型提示词。

每个子代理获得明确的职责,而不是在共享工作流中争夺上下文。这种模式在企业系统中尤其有用,因为 AI 应用程序的复杂性会随时间增长。

2.2. Spring AI 如何支持编排

Spring AI Community Agent Utils 通过 TaskToolClaudeSubagentReferences 提供编排工具。这些组件帮助我们注册子代理、动态加载子代理定义、自动委派任务,以及编排多个 AI 工作流。

这种方法最有趣的一点是,子代理可以用 Markdown 文件定义,而不是 Java 类。

3. 项目设置

在实现子代理编排之前,我们将使用 Spring Initializr 设置一个简单的 Spring Boot 应用程序,包含 Spring AI starter OpenAIstarter-testSpring AI Community Agent Utils。我们首先添加所需的依赖项,然后配置 OpenAI 模型和基于 Markdown 的子代理定义。

在此应用程序中,我们将构建一个 AI 驱动的编排系统,将工作委派给多个专门的子代理。一个子代理审查 Spring Boot 应用程序的代码质量,另一个生成简洁的技术文档。中央编排器分析用户请求并协调这些子代理,生成组合响应。

3.1. 添加 Maven 依赖

首先,配置所需的依赖项:

$ xml
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-openai</artifactId>
        <version>2.0.0-M5</version>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>org.springaicommunity</groupId>
        <artifactId>spring-ai-agent-utils</artifactId>
        <version>0.4.2</version>
    </dependency>
</dependencies>

此配置添加了应用程序所需的核心依赖项。这些依赖项提供了 Spring Boot 应用支持、通过 Spring AI 的 OpenAI 集成、子代理编排工具和测试支持。添加依赖项后,我们将配置 OpenAI API 密钥。

3.2. 配置 OpenAI 访问

接下来,在 application.properties 中配置 OpenAI API 密钥、默认 LLM 模型以及基于 Markdown 的子代理定义位置:

$ properties
spring.ai.openai.api-key=${OPENAI_API_KEY}
spring.ai.openai.chat.options.model=gpt-4.1-mini
spring.application.name=spring-ai-subagent
agent.tasks.paths=classpath:/agents/*.md

我们应该将 OPENAI_API_KEY 替换为有效的 OpenAI API 密钥,或通过环境变量暴露,以避免将敏感凭据直接存储在代码库中。

agent.tasks.paths 属性指向位于 src/main/resources/agents/ 下的 Markdown 格式子代理定义。Spring AI 在应用启动时使用这些文件动态加载和注册专门的子代理。

4. 创建专门的子代理

本项目使用 Markdown 文件定义专门的子代理。它们的位置被声明为类路径资源。这种方法使智能体的行为外部化且易于维护。首先,在路径 src/main/resources/ 下创建一个 agents 目录。

4.1. 创建代码审查子代理

让我们创建一个负责审查代码质量的子代理。Markdown 正文充当子代理的系统提示词,并定义其专门行为。

我们在路径 src/main/resources/agents/ 下添加一个 code-reviewer.md 文件,包含以下指令:

$ config
---
name: code-reviewer
description: >
  专业代码审查员。在编写或修改代码后主动使用,以发现质量、安全和可读性问题。
tools: Read, Grep, Glob
disallowedTools: Edit, Write
model: sonnet
---
你是一位资深代码审查员,专精于软件质量。
**调用时:**
1. 运行 `git diff` 以识别最近的更改
2. 检查修改过的文件及其上下文
3. 检查以下领域的问题
**检查清单:**
- 代码清晰度和可读性
- 适当的命名约定
- 错误处理和边界情况
- 安全漏洞
**输出:** 按文件组织的清晰、可操作的反馈,附带行号引用。

此子代理仅专注于代码分析职责。将行为外部化为 Markdown 文件使得在不修改 Java 类的情况下更容易演化提示词。

4.2. 创建文档编写子代理

接下来,创建另一个专门处理技术文档的子代理。我们在 src/main/resources/agents/ 下添加一个 documentation-writer.md 文件,并定义以下指令:

$ config
---
name: documentation-writer
description: >
  技术文档专家,负责架构说明、工作流总结和简洁的开发人员文档。
model: default
---
你是一位资深技术文档专家。
你的职责:
- 生成简洁的技术文档
- 解释 Spring Boot 和 Java 应用程序架构
- 清晰地总结工作流
- 提供对开发者友好的解释
- 保持文档简洁且技术准确

此子代理完全专注于生成开发文档。现在我们有了两个专门的子代理:code-reviewerdocumentation-writer

5. 配置主编排器智能体

编排器负责加载子代理、注册编排工具以及执行委派的工作流。 此主智能体是用户直接交互的入口点。其底层大语言模型(LLM)可以访问 Tasktool,该工具暴露了一个可用子代理的目录。当主智能体决定某个专门智能体能更好地处理用户请求的一部分时,它会调用 Tasktool,传入子代理名称和任务描述。

5.1. 配置子代理引用

Spring AI Community Agent Utils 提供了 ClaudeSubagentReferences 来加载基于 Markdown 的子代理。以下配置从 Markdown 资源目录动态加载子代理:

$ java
@Configuration
public class AgentConfig {
    @Value("${agent.tasks.paths}")
    private List<Resource> agentPaths;

    @Bean
    @Primary
    public ChatClient orchestratorChatClient(ChatClient.Builder chatClientBuilder) {
        SubagentType claudeType = ClaudeSubagentType.builder()
          .chatClientBuilder("default", chatClientBuilder.clone())
          .build();
        ToolCallback taskTool = TaskTool.builder()
          .subagentReferences(ClaudeSubagentReferences.fromResources(agentPaths))
          .subagentTypes(claudeType)
          .build();
        return chatClientBuilder.clone()
          .defaultToolCallbacks(taskTool)
          .build();
    }
}

两个关键的协作类是 TaskTool(编排器 LLM 调用的工具)和 ClaudeSubagentType(告诉工具如何生成子代理以及使用哪个 ChatClient.Builder)。注意 chatClientBuilder 上的 .clone() 调用。这一点很重要,因为编排器和每个子代理都需要自己独立的 ChatClient.Builder 实例,以便不共享默认选项或系统提示词。

5.2. 创建编排器服务

此服务作为编排后的 AI 请求的入口点。主智能体的 LLM 会根据智能体注册表中的任务描述自动决定是否委派给子代理:

$ java
@Service
public class OrchestratorService {
    private final ChatClient chatClient;

    public OrchestratorService(ChatClient chatClient) {
        this.chatClient = chatClient;
    }

    public String ask(String userMessage) {
        return chatClient.prompt(userMessage).call().content();
    }
}

服务本身保持得非常小,因为编排的复杂性通过 Spring AI 工具在内部处理。注意,这里没有手动调用子代理的代码。委派在 LLM 推理循环中透明地发生,当模型选择 Task 工具时。

5.3. 执行编排工作流

现在我们将执行编排工作流。以下方法会触发多个专门子代理:

$ java
public class SpringAiSubagentApplication {
    private static final Logger logger = LoggerFactory.getLogger(SpringAiSubagentApplication.class);

    public static void main(String[] args) {
        SpringApplication.run(SpringAiSubagentApplication.class, args);
    }

    @Bean
    CommandLineRunner demo(OrchestratorService orchestratorService) {
        return args -> {
            String response = orchestratorService.ask(
              """
              执行以下任务:
              - 审查代码质量。
              - 生成简洁的技术文档,如用户指南。
              """
            );
            logger.info("{}", response);
        };
    }
}

该提示词允许编排器将职责委派给多个子代理。运行应用程序时,Spring AI 会加载基于 Markdown 的子代理,通过编排层路由请求,并从专门的智能体生成组合响应。

以下截图显示了多子代理编排工作流的输出: [LOADING...]

从生成的响应中可以清楚地看到每个子代理如何处理自己的专门职责。code-reviewer 子代理专注于分析代码质量问题,而 documentation-writer 子代理则生成简洁的技术文档。这种职责分离是子代理编排的主要优势之一。

6. 测试子代理编排

AI 编排工作流仍应像其他应用程序组件一样进行测试。应用程序中包含编排行为的集成测试。

6.1. 测试子代理加载

以下测试验证 Spring AI 能否正确加载子代理定义:

$ java
@Test
void givenSubagentDefinitions_whenLoadingSubagents_thenReferencesAreCreated() {
    var references = ClaudeSubagentReferences.fromResources(agentPaths);
    assertThat(references).isNotNull();
}

此测试确保编排层能够成功加载基于 Markdown 的子代理。

6.2. 测试编排响应

接下来,验证编排器能否成功执行请求:

$ java
@Test
void givenPrompt_whenExecutingOrchestration_thenResponseIsGenerated() {
    List<Resource> agentResources = List.of(
      new ClassPathResource("agents/test-agent.md")
    );
    SubagentType claudeType = ClaudeSubagentType.builder()
      .chatClientBuilder("default", chatClientBuilder.clone())
      .build();
    ToolCallback taskTool = TaskTool.builder()
      .subagentReferences(ClaudeSubagentReferences.fromResources(agentResources))
      .subagentTypes(claudeType)
      .build();
    ChatClient chatClient = chatClientBuilder.clone()
      .defaultToolCallbacks(taskTool)
      .build();
    String result = chatClient.prompt("解释身份验证模块如何工作。")
      .call()
      .content();
    assertThat(result).isNotBlank();
    assertThat(result).contains("authentication");
}

此集成测试验证了编排流程。这里,单元测试应模拟模型层并验证编排输出。该测试验证了服务契约,并使编排行为在无外部 API 调用的情况下可验证。

7. 结论

在本文中,我们使用 Spring AI 和 Spring AI Community Agent Utils 构建了一个子代理编排系统。我们创建了专门的子代理用于代码审查和技术文档,然后通过集中式的 AI 工作流编排它们。这种方法有助于保持 AI 系统的模块化、可维护性,并更易于演化。

我们还探讨了 Spring AI 如何提供一种干净且声明式的方式来构建层次化的多智能体系统。通过将每个子代理隔离在自己的上下文窗口中,我们可以减少常影响单智能体工作流的上下文污染。基于 Markdown 的配置使得定义和版本控制子代理变得直接,而无需引入额外的 Java 实现。我们还看到了 TaskTool 如何充当编排器与专门子代理之间的桥梁。

随着 AI 应用程序的复杂性不断增长,子代理编排提供了一种实用的方法,将职责分配给聚焦的智能体,而不是依赖单个大型提示词。

和以往一样,本文的代码可在 GitHub 上获取