SAA1.1迭代

架构设计

Spring AI Alibaba 项目在架构上包含三个清晰的层次:

  1. Agent Framework:以 ReactAgent 设计理念为核心的 Agent 开发框架,内置了自动上下文工程和 Human In The Loop 等高级能力。
  2. Graph:一个更底层级别的工作流和多代理协调框架,是 Agent Framework 的底层运行时基座,用于实现复杂的工作流编排,同时对用户开放 API。
  3. Augmented LLM**:基于 Spring AI 框架的底层原子抽象,提供了模型、工具、消息、向量存储等构建 LLM 应用的基础。

设计理念

ReactAgent

ReactAgent是 1.1 版本的核心组件之一,它基于 ReAct(Reasoning + Acting) 范式。这意味着 Agent 不仅仅是调用 LLM,它还可以在一个循环中运行,通过“思考(Reasoning)”来分析任务、决定使用哪个“工具(Acting)”,然后“观察(Observation)”工具结果,并迭代此过程,直到任务完成。

就是我们先去进行一个思考,然后再去进行工具的调用,根据结果再去进行思考调用,这里面是一个loop的过程,然后最终完成一个自思考的结果。相比工作流的那种流式编程,ReActAgent思考决策更加完善,应对出现错误的情况更加完善。

ReactAgent的组成

  • Model (模型):Agent 的“大脑”,即 LLM 推理引擎,如 DashScopeChatModel。各个模型
  • Tools (工具):赋予 Agent 执行操作的能力。有一部分内置的和你自己去设计的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SearchTool implements BiFunction<String, ToolContext, String> {
@Override
public String apply(String query, ToolContext toolContext) {
return "搜索结果:" + query;
}
}

// 将其注册为工具回调
ToolCallback searchTool = FunctionToolCallback
.builder("search", new SearchTool())
.description("搜索信息的工具")
.build();


  • System Prompt (系统提示):通过 systemPromptinstruction 参数,塑造 Agent 的角色和行为方式。这个感觉是比较重要的

    比如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    flux.doOnNext(output -> {
    if (output instanceof StreamingOutput<?> streamingOutput){
    assertNotNull(streamingOutput, "NodeOutput should not be null");
    assertNotNull(streamingOutput.tokenUsage(), "TokenUsage should not be null");
    assertNotNull(streamingOutput.agent(), "Agent should not be null");
    assertEquals("test_agent", streamingOutput.agent(), "Agent name should match");

    System.out.println("=== NodeOutput ===");
    System.out.println("Agent: " + streamingOutput.agent());
    System.out.println("TokenUsage: " + streamingOutput.tokenUsage());
    }
    }).blockLast();

    Graph

Graph 是 Agent Framework 的底层运行时基座,是一个低级工作流和多智能体编排框架。它通过 State(状态)Node(节点)Edge(边) 三个核心概念,使开发者能够实现复杂的应用程序编排。

类似与图数据库里面的设计,这个是之前SAA开发的重点,偏底层一点。是一个工作流编排的实现。然后里面的上下文是通过OverAllState实现的,我们通过map的put/get进行上下文的获取

内置多个流程agent:

SequentialAgent(顺序执行):按预定义顺序依次执行多个 Agent (A -> B -> C)。每个 Agent 的输出(通过 outputKey 指定)会传递给下一个 Agent,适用于需要按步骤顺序处理的任务,如”写作 -> 评审 -> 发布”的流程。

ParallelAgent(并行执行):将相同的输入同时发送给所有子 Agent 并行处理,然后使用 MergeStrategy 合并结果。适用于需要同时执行多个独立任务的场景,如同时生成散文、诗歌和总结等。

LlmRoutingAgent(智能路由):使用 LLM 根据用户输入和子 Agent 的 description,智能地选择一个最合适的子 Agent 来处理请求。适用于需要根据上下文动态选择专家 Agent 的场景,如根据问题类型自动路由到编程专家或写作专家。

Context Engineering

构建 Agent 最大的难点是使其可靠。Agent 失败通常不是因为 LLM 能力不足,而是因为没有向 LLM 传递“正确”的上下文

然后AI无法知道我门具体是干了什么?然后导致输出的结果不理想

但是Ai的上下文又是有限的,我们不可能将所有的消息都发送给他,所以上下文工程就是很必须的

Spring AI Alibaba 将上下文分为三类,并提供了精细化的控制机制:

上下文类型 你控制的内容 拦截与处理机制
模型上下文 指令、消息历史、可用工具、响应格式等 Interceptor, Hook
工具上下文 在工具中可以访问和修改的状态(如短期记忆、长期) Interceptor
生命周期上下文 模型和工具调用之间发生的事(如摘要、护栏) Hook

Spring AI Alibaba 1.1 版本提供了一些常用的默认 Hook 和 Interceptor 实现:

  1. 人工介入 (Human-in-the-Loop)
  2. Planning(规划)
  3. 模型调用限制(Model Call Limit)
  4. 工具重试(Tool Retry)
  5. LLM Tool Selector(LLM 工具选择器)
  6. LLM Tool Emulator(LLM 工具模拟器)
  7. Context Editing(上下文编辑)

人工介入

在生产环境中,高风险操作(如删除数据库、发送邮件)需要人工监督。HumanInTheLoopHook (HITL) 完美解决了这个问题。

它允许 Agent 暂停执行,等待人工决策(批准、编辑或拒绝)后再继续。

配置 Hook:在 Agent 上配置 HumanInTheLoopHook,指定需要审批的工具(如 execute_sql)。此功能必须配置 \saver* (检查点)*

1
2
3
4
5
6
7
8
9
HumanInTheLoopHook humanReviewHook = HumanInTheLoopHook.builder()
    .approvalOn("execute_sql", ToolConfig.builder().description("SQL执行需要审批").build())
    .build();

ReactAgent agent = ReactAgent.builder()
    .hooks(humanReviewHook)
    .saver(new MemorySaver()) // 必须配置 Saver
    .tools(executeSqlTool)
    .build();

触发中断:当 Agent 尝试调用 execute_sql 时,执行会暂停。第一次 invokeAndGetOutput 调用会返回一个 InterruptionMetadata (中断元数据)。

1
2
3
4
String threadId = "user-123";
RunnableConfig config = RunnableConfig.builder().threadId(threadId).build();
Optional<NodeOutput> result = agent.invokeAndGetOutput("删除旧记录", config);
// 此时 result.get() 是 InterruptionMetadata

人工决策与恢复:您的应用程序向用户展示中断信息。用户做出决策(例如“批准”)后,您构建一个包含该决策的反馈,并再次调用 Agent 以恢复执行。

1
2
3
4
5
6
7
8
9
10
// 假设用户批准了
InterruptionMetadata approvalMetadata = buildApprovalFeedback(interruptionMetadata);

RunnableConfig resumeConfig = RunnableConfig.builder()
    .threadId(threadId) // 必须使用相同的 threadId
    .addMetadata(RunnableConfig.HUMAN_FEEDBACK_METADATA_KEY, approvalMetadata)
    .build();

// 第二次调用以恢复执行
Optional<NodeOutput> finalResult = agent.invokeAndGetOutput("", resumeConfig);

消息压缩

当对话历史接近 Token 限制时,自动调用 LLM 压缩(总结)旧消息,防止上下文溢出。这是处理长期对话和多轮交互的关键能力,可以确保 Agent 在保持对话上下文的同时,不会因为消息历史过长而超出模型的上下文窗口限制。

使用于:

  • 超出上下文窗口的长期对话
  • 需要大量历史记录的会话
  • 需要保留完整对话实现的上下文的应用程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 创建消息压缩 Hook
SummarizationHook summarizationHook = SummarizationHook.builder()
    .model(chatModel)  // 用于生成摘要的 ChatModel(可以使用更便宜的模型)
    .maxTokensBeforeSummary(4000)  // 触发摘要之前的最大 token 数
    .messagesToKeep(20)  // 摘要后保留的最新消息数
    .build();

// 使用
ReactAgent agent = ReactAgent.builder()
    .name("my_agent")
    .model(chatModel)
    .hooks(summarizationHook)
    .saver(new MemorySaver())  // 需要配置 Saver 来持久化状态
    .build();

参数选项:

model:用于生成摘要的 ChatModel(可以使用更便宜的模型来降低成本)

maxTokensBeforeSummary:触发摘要之前的最大 token 数,当消息历史达到此阈值时自动触发压缩

messagesToKeep:摘要后保留的最新消息数,确保最近的对话内容完整保留

规划

在执行工具之前强制执行一个规划步骤,以概述 Agent 将要采取的步骤。这对于需要执行复杂、多步骤任务的 Agent 特别有用,可以提高执行透明度,并便于调试错误。

就是这个一般都是使用思考模型来的,或者是我们去调用思考的迭代的mcp去执行,然后去写入Todolist里面

需要执行复杂、多步骤任务的 Agent

通过在执行前显示 Agent 的计划来提高透明度

通过检查建议的计划来调试错误

1
2
3
4
5
6
ReactAgent agent = ReactAgent.builder()
    .name("planning_agent")
    .model(chatModel)
    .tools(myTool)
    .interceptors(TodoListInterceptor.builder().build())
    .build();

模型调用限制

限制模型调用次数以防止无限循环或过度成本。这是生产环境中成本控制的重要手段。

适用场景

  • 防止失控的 Agent 进行太多 API 调用
  • 在生产部署中强制执行成本控制
  • 在特定调用预算内测试 Agent 行为
1
2
3
4
5
6
ReactAgent agent = ReactAgent.builder()
    .name("my_agent")
    .model(chatModel)
    .hooks(ModelCallLimitHook.builder().runLimit(5).build())  // 限制模型调用次数为5次
    .saver(new MemorySaver())
    .build();

工具重试

自动重试失败的工具调用,具有可配置的指数退避策略。这对于处理外部 API 调用中的瞬态故障特别有用,可以提高依赖网络的工具的可靠性。

适用场景

  • 处理外部 API 调用中的瞬态故障
  • 提高依赖网络的工具的可靠性
  • 构建优雅处理临时错误的弹性 Agent
1
2
3
4
5
6
7
8
9
ReactAgent agent = ReactAgent.builder()
    .name("resilient_agent")
    .model(chatModel)
    .tools(searchTool, databaseTool)
    .interceptors(ToolRetryInterceptor.builder()
        .maxRetries(2)
        .onFailure(ToolRetryInterceptor.OnFailureBehavior.RETURN_MESSAGE)
        .build())
    .build();

工具选择器

使用一个 LLM 来决定在多个可用工具之间选择哪个工具。当多个工具可以实现相似目标时,可以根据细微的上下文差异进行智能选择。

适用场景

  • 当多个工具可以实现相似目标时
  • 需要根据细微的上下文差异进行工具选择
  • 动态选择最适合特定输入的工具
1
2
3
4
5
6
ReactAgent agent = ReactAgent.builder()
    .name("smart_selector_agent")
    .model(chatModel)
    .tools(tool1, tool2)
    .interceptors(ToolSelectionInterceptor.builder().build())
    .build();

上下文编辑

在将上下文发送给 LLM 之前对其进行修改,以注入、删除或修改信息。这是上下文工程的核心能力之一,可以动态调整传递给模型的信息。

适用场景

  • 向 LLM 提供额外的上下文或指令
  • 从对话历史中删除不相关或冗余的信息
  • 动态修改上下文以引导 Agent 的行为
1
2
3
4
5
6
7
8
ReactAgent agent = ReactAgent.builder()
    .name("context_aware_agent")
    .model(chatModel)
    .interceptors(ContextEditingInterceptor.builder()
        .trigger(120000)      // 当上下文超过120000 tokens时触发
        .clearAtLeast(60000)  // 至少清理60000 tokens
        .build())
    .build();

hook&&Interceptors

Hooks (钩子) 和 Interceptors (拦截器) 是实现“上下文工程”的核心机制。它们允许您在 Agent 执行的每一步进行监控、修改、控制和强制执行。

比如

  • 监控: 通过日志、分析和调试跟踪 Agent 行为
  • 修改: 转换提示、工具选择和输出格式
  • 控制: 添加重试、回退和提前终止逻辑
  • 强制执行: 应用速率限制、护栏和 PII 检测

我们的好多功能都是基于Hooks和Interceptors实现的

  • Hooks (AgentHook, \ModelHook*)*:在 Agent 或模型生命周期的特定节点(如 beforeModel, afterModel)_插入_自定义逻辑,如日志记录或消息修剪。
  • Interceptors (ModelInterceptor, \ToolInterceptor*)*:_包裹_模型或工具的调用,允许您拦截和_修改_请求/响应,或实现重试、缓存、安全护栏等。

比如内容审核的拦截器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class ContentModerationInterceptor extends ModelInterceptor {

    private static final List<String> BLOCKED_WORDS =
        List.of("敏感词1""敏感词2""敏感词3");

    @Override
    public ModelResponse interceptModel(ModelRequest request, ModelCallHandler handler) {
        // 检查输入
        for (Message msg : request.getMessages()) {
            String content = msg.getText().toLowerCase();
            for (String blocked : BLOCKED_WORDS) {
                if (content.contains(blocked)) {
                    return ModelResponse.blocked(
                        "检测到不适当的内容,请修改您的输入"
                    );
                }
            }
        }

        // 执行模型调用
        ModelResponse response = handler.call(request);

        // 检查输出
        String output = response.getContent();
        for (String blocked : BLOCKED_WORDS) {
            if (output.contains(blocked)) {
                // 清理输出
                output = output.replaceAll(blocked, "[已过滤]");
                return response.withContent(output);
            }
        }

        return response;
    }

    @Override
    public String getName() {
        return "ContentModerationInterceptor";
    }
}

Tool

Tools 是 agents 调用来执行操作的组件。它们通过定义良好的输入和输出让模型与外部世界交互,从而扩展模型的能力。Tools 封装了一个可调用的函数及其输入模式。我们可以把工具定义传递给兼容的 models,允许模型决定是否调用工具以及使用什么参数。在这些场景中,工具调用使模型能够生成符合指定输入模式的请求。

工具定义:

你可以通过编程方式构建 FunctionToolCallback,将函数类型(FunctionSupplierConsumerBiFunction)转换为工具。

1
2
3
4
5
6
7
8
9
public class WeatherService implements Function<WeatherRequest, WeatherResponse> {
public WeatherResponse apply(WeatherRequest request) {
return new WeatherResponse(30.0, Unit.C);
}
}

public enum Unit { C, F }
public record WeatherRequest(String location, Unit unit) {}
public record WeatherResponse(double temp, Unit unit) {}
  • name: 工具的名称。AI 模型使用此名称在调用时识别工具。因此,在同一上下文中不允许有两个同名的工具。对于特定的聊天请求,名称在模型可用的所有工具中必须是唯一的。必需
  • toolFunction: 表示工具方法的函数对象(FunctionSupplierConsumerBiFunction)。必需
  • description: 工具的描述,模型可以使用它来了解何时以及如何调用工具。如果未提供,将使用方法名称作为工具描述。但是,强烈建议提供详细描述,因为这对于模型理解工具的目的和使用方式至关重要。如果未提供良好的描述,可能导致模型在应该使用工具时不使用,或者使用不正确。
  • inputType: 函数输入的类型。必需
  • inputSchema: 工具输入参数的 JSON schema。如果未提供,将根据 inputType 自动生成 schema。你可以使用 @ToolParam 注解提供有关输入参数的附加信息,例如描述或参数是必需还是可选。默认情况下,所有输入参数都被视为必需。
  • toolMetadata: 定义附加设置的 ToolMetadata 实例,例如是否应将结果直接返回给客户端,以及要使用的结果转换器。你可以使用 ToolMetadata.Builder 类构建它。
  • toolCallResultConverter: 用于将工具调用结果转换为字符串对象以发送回 AI 模型的 ToolCallResultConverter 实例。如果未提供,将使用默认转换器(DefaultToolCallResultConverter)。

FunctionToolCallback

1
2
3
4
5
6
7
8
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.function.FunctionToolCallback;

ToolCallback toolCallback = FunctionToolCallback
.builder("currentWeather", new WeatherService())
.description("Get the weather in location")
.inputType(WeatherRequest.class)
.build();

函数的输入和输出可以是 Void 或 POJO。输入和输出 POJO 必须是可序列化的,因为结果将被序列化并发送回模型。函数以及输入和输出类型必须是公共的。

重要提示:某些类型不受支持。有关更多详细信息,请参阅函数工具限制。

然后工具调用的时候直接使用toolCallbacks即可

Memory

Spring AI Alibaba 将短期记忆作为 Agent 状态的一部分进行管理。

通过将这些存储在 Graph 的状态中,Agent 可以访问给定对话的完整上下文,同时保持不同对话之间的分离。状态使用 checkpointer 持久化到数据库(或内存),以便可以随时恢复线程。短期记忆在调用 Agent 或完成步骤(如工具调用)时更新,并在每个步骤开始时读取状态。

其实就是我们的OverAllState用它的Map来存储我们的一部分短期记忆

保留所有对话历史是实现短期记忆最常见的形式。但较长的对话对历史可能会导致大模型 LLM 上下文窗口超限,导致上下文丢失或报错。

即使你在使用的大模型上下文长度足够大,大多数模型在处理较长上下文时的表现仍然很差。因为很多模型会被过时或偏离主题的内容”分散注意力”。同时,过长的上下文,还会带来响应时间变长、Token 成本增加等问题。

在 Spring AI ALibaba 中,ReactAgent 使用 messages 记录和传递上下文,其中包括指令(SystemMessage)和输入(UserMessage)。在 ReactAgent 中,消息(Message)在用户输入和模型响应之间交替,导致消息列表随着时间的推移变得越来越长。由于上下文窗口有限,许多应用程序可以从使用技术来移除或”忘记”过时信息中受益,即 “上下文工程”。

启用记忆:

要启用会话级持久化,您只需在创建 Agent 时指定一个 checkpointer (保存器)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1. 配置内存存储
ReactAgent agent = ReactAgent.builder()
    .name("chat_agent")
    .model(chatModel)
    .saver(new MemorySaver()) //
    .build();

// 2. 使用 thread_id 维护对话上下文
RunnableConfig config = RunnableConfig.builder()
    .threadId("user_123") //
    .build();

agent.call("我叫张三", config);
agent.call("我叫什么名字?", config);  // Agent 会回答: "你叫张三"在生产环境中,您可以轻松换成 RedisSaver 或 MongoSaver 等持久化存储。

多智能体协作

当单个 Agent 难以处理复杂任务时,多智能体架构允许您将任务分解为多个协同工作的专业化 Agent。Spring AI Alibaba 支持两种核心模式:

模式一:工具调用 (Agent as a Tool)

这是一种集中式控制流,一个“控制器” (Controller) Agent 将其他“子 Agent” (Sub-agent) 作为工具来调用。

子 Agent 独立执行任务并返回结果,但不直接与用户对话。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1. 创建子 Agent (作家)
ReactAgent writerAgent = ReactAgent.builder()
    .name("writer_agent")
    .model(chatModel)
    .description("擅长写作,可以写文章")
    .build();

// 2. 创建主 Agent (控制器)
ReactAgent blogAgent = ReactAgent.builder()
    .name("blog_agent")
    .model(chatModel)
    .instruction("使用写作工具来完成用户的文章创作请求。")
    // 将子 Agent 封装为工具
    .tools(AgentTool.getFunctionToolCallback(writerAgent)) //
    .build();

blogAgent.invoke("帮我写一篇关于西湖的散文");

工作流编排 (Handoffs / Flow):

这是一种去中心化控制流,控制权从一个 Agent “交接”给下一个 Agent。框架内置了多种流程型 Agent:

  • SequentialAgent:按预定义顺序依次执行 Agent (A -> B -> C)。每个 Agent 的输出(通过 outputKey 指定)会传递给下一个。
  • ParallelAgent:将相同的输入同时发送给所有子 Agent 并行处理,然后使用 MergeStrategy 合并结果。
  • LlmRoutingAgent:使用 LLM 根据用户输入和子 Agent 的 description,_智能_地选择一个最合适的子 Agent 来处理请求。

在路由模式中,使用大语言模型(LLM)动态决定将请求路由到哪个子 Agent。这种模式非常适合需要智能选择不同专家 Agent 的场景。LLM 会根据用户输入和子 Agent 的 description,智能地选择最合适的 Agent 来处理请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 创建多个专家 Agent
ReactAgent writerAgent = ReactAgent.builder()
    .name("writer_agent")
    .model(chatModel)
    .description("擅长创作各类文章,包括散文、诗歌等文学作品")
    .instruction("你是一个知名的作家,擅长写作和创作。请根据用户的提问进行回答。")
    .outputKey("writer_output")
    .build();

ReactAgent codeAgent = ReactAgent.builder()
    .name("code_agent")
    .model(chatModel)
    .description("专门处理编程相关问题,包括代码编写和调试")
    .instruction("你是一个资深的软件工程师,擅长编写和调试代码。")
    .outputKey("code_output")
    .build();

// 创建路由 Agent
LlmRoutingAgent routingAgent = LlmRoutingAgent.builder()
    .name("content_routing_agent")
    .description("根据用户需求智能路由到合适的专家Agent")
    .model(chatModel)  // 路由需要一个 LLM 来进行智能选择
    .subAgents(List.of(writerAgent, codeAgent))
    .build();

// 使用 - LLM 会自动选择最合适的 Agent
// LLM 会路由到 writerAgent
Optional<OverAllState> result1 = routingAgent.invoke("帮我写一篇关于春天的散文");

// LLM 会路由到 codeAgent
Optional<OverAllState> result2 = routingAgent.invoke("帮我写一个 Java 排序算法");

Agent as Workflow Node

在复杂的工作流场景中,可以将 ReactAgent 作为 Node 集成到 StateGraph 中,实现更强大的组合能力。Agent 作为 Node 可以利用其推理和工具调用能力,处理需要多步骤推理的任务。

ReactAgent 可以通过 asNode() 方法转换为可以嵌入到父 Graph 中的 Node:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class AgentWorkflowExample {

    public StateGraph buildWorkflowWithAgent(ChatModel chatModel) {
        // 创建专门的数据分析 Agent
        ReactAgent analysisAgent = ReactAgent.builder()
            .name("data_analyzer")
            .model(chatModel)
            .instruction("你是一个数据分析专家,负责分析数据并提供洞察")
            .tools(dataAnalysisTool, statisticsTool)
            .build();

        // 创建报告生成 Agent
        ReactAgent reportAgent = ReactAgent.builder()
            .name("report_generator")
            .model(chatModel)
            .instruction("你是一个报告生成专家,负责将分析结果转化为专业报告")
            .tools(formatTool, chartTool)
            .build();

        // 构建包含 Agent 的工作流
        StateGraph workflow = new StateGraph("multi_agent_workflow", keyStrategyFactory);

        // 将 Agent 作为 SubGraph Node 添加
        workflow.addNode("analysis", analysisAgent.asNode(
            true,                     // includeContents: 是否传递父图的消息历史
            false,                    // returnReasoningContents: 是否返回推理过程
            "analysis_result"         // outputKeyToParent: 输出键名
        ));

        workflow.addNode("reporting", reportAgent.asNode(
            true,
            false,
            "final_report"
        ));

        // 定义流程
        workflow.addEdge(StateGraph.START, "analysis");
        workflow.addEdge("analysis", "reporting");
        workflow.addEdge("reporting", StateGraph.END);

        return workflow;
    }
}