Spring AI Alibaba Agent 开发入门与实战

作者:袖梨 2026-09-12

在 Spring 应用中接入大模型并不难,真正复杂的是如何让模型调用工具、维护状态,并按照可控流程持续完成任务。Spring AI Alibaba 在 Spring AI 之上提供 Agent Framework 与 Graph 运行时,将 ReAct、多智能体协作和工作流编排纳入统一开发体系。接下来将从关键概念和环境配置入手,逐步理解核心 API 与实际构建方式。

从 Agent 到 Graph,用阿里云百炼(DashScope)模型构建智能体工作流

  • 框架版本:Spring AI Alibaba 1.1.2.2
  • Spring Boot 版本3.5.9
  • 运行环境:JDK 21+、Maven 3.6+

第一部分:基础与概念

第 0 章 导读与准备

0.1 本文要做什么

Spring AI Alibaba 是一个面向 Java 开发者的企业级 AI 应用开发框架,它基于 Spring AI 构建, 深度集成阿里云百炼平台,用于快速构建 Agentic(智能体)Workflow(工作流)Multi-agent(多智能体) 应用。

在开始之前,需要先建立一条最重要的认知主线:

Graph 是 Agent Framework 的底层运行时。

  • Agent Framework 是「高层用法」,提供 ReactAgentSequentialAgentParallelAgent 等开箱即用的抽象;
  • Graph 是「底层引擎」,把智能体工作流建模为一张图(节点 + 边),提供状态管理、持久化、中断恢复、流式执行等能力。

官方建议开发者优先使用 Agent Framework;但当需要更细粒度的编排控制时,直接使用 Graph API 也完全可行

0.2 环境准备

项目要求
JDK21+
构建工具Maven 3.6+
模型平台阿里云百炼(DashScope)
API Key在百炼控制台申请

获取 API Key 后,建议以环境变量的方式注入(避免硬编码到代码):

export AI_DASHSCOPE_API_KEY=sk-xxxxxx

0.3 依赖引入

pom.xml 中引入以下依赖(版本统一由 Spring AI Alibaba 的 BOM 管理):

<properties>
    <spring-ai.version>1.1.2</spring-ai.version>
    <spring-ai-alibaba.version>1.1.2.2</spring-ai-alibaba.version>
    <spring-ai-alibaba-extensions.version>1.1.2.2</spring-ai-alibaba-extensions.version>
</properties>

<dependencyManagement>
    <dependencies>
        <!-- Spring AI 基础(消息、ChatModel、工具等) -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>${spring-ai.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!-- Agent Framework / Graph 核心 -->
        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-bom</artifactId>
            <version>${spring-ai-alibaba.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!-- DashScope / 百炼 等扩展 -->
        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-extensions-bom</artifactId>
            <version>${spring-ai-alibaba-extensions.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <!-- 百炼 / DashScope 模型接入 -->
    <dependency>
        <groupId>com.alibaba.cloud.ai</groupId>
        <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
    </dependency>

    <!-- Agent Framework(高层抽象) -->
    <dependency>
        <groupId>com.alibaba.cloud.ai</groupId>
        <artifactId>spring-ai-alibaba-agent-framework</artifactId>
    </dependency>

    <!-- Graph Core(底层运行时,Agent Framework 已传递依赖,可按需显式引入) -->
    <dependency>
        <groupId>com.alibaba.cloud.ai</groupId>
        <artifactId>spring-ai-alibaba-graph-core</artifactId>
    </dependency>
</dependencies>

说明:spring-ai-alibaba-agent-framework 本身依赖 spring-ai-alibaba-graph-core, 因此引入前者即可获得 Graph 能力。为便于讲解 Graph 章节,这里把两者都显式列出。

另外,spring-ai-alibaba-starter-dashscope(百炼模型接入)属于独立的扩展库, 版本由 spring-ai-alibaba-extensions-bom 管理(同样对齐 1.1.2.2)。


第 1 章 概念篇:Agentic / Workflow / ReAct / Plan-and-Execute

在写代码之前,先把四个最容易混淆的概念讲清楚。它们是理解整个框架的钥匙。

1.1 从 LLM 到 Agent:什么是 Agentic(智能体范式)

传统的 LLM 调用是「一问一答」:你发一条消息,模型返回一条回复,仅此而已。

Agentic(智能体范式) 的核心区别在于:模型可以自主决定「下一步做什么」

一个智能体通常具备:

  1. 推理能力:理解任务,拆解出执行步骤;
  2. 行动能力:调用工具(查天气、查数据库、发请求……)去改变外部世界;
  3. 观察能力:读取工具返回的结果,并据此调整下一步。

「推理 → 行动 → 观察 → 再推理」这个循环,就是智能体的灵魂。它不再是一条直线, 而是一个由模型主导的循环——走到哪一步、调用哪个工具、什么时候停下来,都由模型自己判断。

一句话:Agentic = 把「决策权」交给模型,让它在循环中自主推进任务。

1.2 Workflow(固定工作流)

Workflow(固定工作流) 则相反:执行路径是预先编排好的,模型只负责在某个节点内完成推理。

一句话:Workflow 工作流 Agent = 开发者「预先画好流程图」,模型只在节点内干活。

举个例子,一个「文章生成」工作流可能是:

输入 → [标题生成节点] → [正文生成节点] → [校对节点] → 输出

这条路径是开发者画死的,数据按固定顺序流过每个节点。每个节点内部当然会调用模型, 但**「下一步去哪个节点」不由模型临时决定,而是由图的边(Edge)决定**。

维度Agentic(智能体)Workflow(工作流)
路径决定者模型(自主)开发者(预先编排)
灵活性高,可动态调整低,但可控、可预测
可观测性相对难路径清晰,易调试
典型场景开放任务、需多轮工具调用流程固定、步骤明确的任务

两者并非对立关系。实际上在本框架里,Agent 本身就是用 Graph(工作流)实现的——一个 ReactAgent 内部就是一张小图(LLM 节点 ↔ 工具节点循环)。而复杂业务则可以用 Graph 把多个 Agent、多个步骤编排成更大的工作流。

1.3 ReAct(Reasoning + Acting)

ReAct 是「Reasoning(推理)+ Acting(行动)」的缩写,是智能体最经典、也是本框架最核心的范式。 它的执行流程是一个四段循环:

┌─────────────┐      ┌─────────────┐
│  Reasoning  │ ───▶ │   Acting    │
│   思考       │      │ 调用工具     │
└─────────────┘      └─────────────┘
       ▲                     │
       │                     ▼
┌─────────────┐      ┌─────────────┐
│  Observing  │ ◀─── │   Tool 结果  │
│   观察结果   │      │             │
└─────────────┘      └─────────────┘
  • Reasoning:模型分析当前状态,决定下一步动作(是否调用工具、调用哪个);
  • Acting:执行工具调用;
  • Observing:把工具返回结果作为新的上下文,喂回给模型;
  • 循环:重复上述过程,直到模型认为任务完成、不再发起工具调用。

在 Spring AI Alibaba 中,ReactAgent 就是这一范式的生产级实现:它内部用一张 StateGraph 把「LLM 节点」和「工具节点」串成循环,模型每发起一次工具调用,就路由到工具节点执行, 执行完再回到 LLM 节点继续推理。

1.4 Plan-and-Execute(计划-执行)

Plan-and-Execute 是另一种智能体范式,思路是「先规划,再执行」:

  1. Plan(规划):模型先把任务拆解成一个步骤清单;
  2. Execute(执行):按照清单逐步执行每个步骤;
  3. (可选)Reflect(反思):执行完后评估结果,必要时修正计划重新执行。

一句话:Plan-and-Execute = 模型「现场写任务清单」,然后按清单跑。

它与 ReAct 的区别在于「粒度」:

  • ReAct 是边走边想:每一步都重新推理下一步,适合短链路的交互式任务;
  • Plan-and-Execute 是先想好再走:先制定整体计划,再按部就班执行,适合步骤多、结构清晰的复杂任务。

在本框架里,Plan-and-Execute 可以用 Graph(StateGraph) 来落地——这正是第 9 章要做的实战。

1.5 概念 → 框架映射

学完概念,先给一张「概念对应到本框架的哪个类」的速查表,后面章节会逐一展开:

概念对应实现
Agentic / ReActReactAgentcom.alibaba.cloud.ai.graph.agent.ReactAgent
Workflow / 图编排StateGraphcom.alibaba.cloud.ai.graph.StateGraph
多智能体编排SequentialAgentParallelAgentLlmRoutingAgentLoopAgent
短期记忆 / 持久化CheckpointSaver(Memory / Redis / File / Mongo 等)+ RunnableConfig.threadId
人工介入HumanInTheLoopHook / InterruptableAction
Plan-and-ExecuteStateGraph 编排「规划 → 执行 → 反思」

第 2 章 基础组件:Models / Messages / Tools

在写智能体之前,先认识三个最基础的组件。官方文档对它们的定义非常精炼:

  • Models(模型)ChatModel API 提供将 AI 驱动的聊天补全能力集成进应用的能力;
  • Messages(消息):模型交互的基本单元,代表模型的输入与输出,携带对话状态所需的内容与元数据;
  • Tools(工具):Agents 调用来执行操作的组件。

2.1 Models:接入百炼模型

Spring AI Alibaba 通过 spring-ai-alibaba-starter-dashscope 对接百炼平台。 引入依赖后,在 application.yml 中配置 API Key 与模型名:

spring:
  ai:
    dashscope:
      api-key: ${AI_DASHSCOPE_API_KEY}   # 百炼 API Key
      ch@t:
        options:
          model: qwen-plus               # 通义千问模型名
          temperature: 0.7

常用模型名:qwen-turbo(快而省)、qwen-plus(均衡)、qwen-max(最强)、 qwen-long(超长上下文)。按需替换。

最小的可运行示例——注入 ChatModel 直接对话:

import [email protected];
import [email protected];
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    private final ChatClient ch@tClient;

    public HelloController(ChatModel ch@tModel) {
        this.ch@tClient = ChatClient.builder(ch@tModel).build();
    }

    @GetMapping("/ch@t")
    public String ch@t(@RequestParam String q) {
        return [email protected](q).call().content();
    }
}

启动后访问 http://localhost:8080/ch@t?q=你好,若能返回回复,说明百炼模型已接通。

除「yml 自动配置 + 注入 ChatModel」外,也可以手动构建具体的 DashScopeChatModel

import com.alibaba.cloud.ai.dashscope.api.DashScopeApi;
import [email protected];
import [email protected];

DashScopeApi api = DashScopeApi.builder()
        .apiKey(System.getenv("AI_DASHSCOPE_API_KEY"))
        .build();

DashScopeChatModel ch@tModel = DashScopeChatModel.builder()
        .dashScopeApi(api)
        .defaultOptions(DashScopeChatOptions.builder()
                .model("qwen-plus")
                .temperature(0.7)
                .build())
        .build();

DashScopeChatOptions 支持 model / temperature / topP / maxToken 等参数, 对应百炼模型的采样与长度控制。日常开发推荐用 yml 自动配置,需要动态切换模型或精细调参时再手动构建。

2.2 Messages:模型交互的基本单元

消息是模型交互的载体,Spring AI Alibaba 沿用 Spring AI 的消息类型。核心的几种:

角色说明
SystemMessagesystem系统提示词,设定模型人设/规则
UserMessageuser用户输入
AssistantMessageassistant模型回复(含可能的工具调用)
ToolResponseMessagetool工具执行结果,回传给模型

它们都在 [email protected] 包下,共同实现 Message 接口。

手工构造一段对话:

import [email protected].*;

List<Message> messages = List.of(
    new SystemMessage("你是一个中文助手"),
    new UserMessage("你好"),
    new AssistantMessage("你好,有什么可以帮你?")
);

在智能体场景下,这些消息会被追加进状态(state)中的 messages 列表,作为模型与工具循环的上下文。

2.3 Tools:工具(Function Calling)

工具是智能体「行动能力」的来源。在 Spring AI Alibaba 中,用 @Tool 注解一个方法, 框架会自动把它暴露给模型(Function Calling),模型在需要时发起调用。

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;

@Component
public class WeatherTools {

    @Tool(description = "查询指定城市的实时天气")
    public String getWeather(@ToolParam(description = "城市名称,例如:杭州") String city) {
        // 真实场景:调用天气 API。这里用固定返回值演示。
        return city + " 今天晴,气温 25℃,微风";
    }
}

要点:

  • @Tool(description = "..."):工具描述,模型据此判断「什么时候该用这个工具」,务必写清楚;
  • @ToolParam(description = "..."):参数描述,帮助模型正确传参;
  • 方法返回的字符串会被作为 ToolResponseMessage 回传给模型,成为下一轮推理的「观察」输入。

这些 @Tool 方法会被 Spring AI Alibaba 自动注册为 ToolCallbackorg.springframework.ai.tool.ToolCallback)。 在第 3 章你会看到如何把它们挂载到 ReactAgent 上。


第二部分:单智能体与扩展能力

第 3 章 Agents:ReactAgent

3.1 ReactAgent 与 ReAct 循环原理

ReactAgentcom.alibaba.cloud.ai.graph.agent.ReactAgent)是 Spring AI Alibaba 提供的生产级 Agent 实现, 它把第 1 章讲的 ReAct 范式固化了下来。

从源码角度看,一个 ReactAgent 内部就是一张 StateGraph,它有两个核心节点:

  • _AGENT_MODEL_(模型节点,AgentLlmNode:调用大模型,产出推理结果或工具调用请求;
  • _AGENT_TOOL_(工具节点,AgentToolNode:执行模型请求的工具调用。

两者之间通过条件边(Conditional Edge)连接成循环:

  1. 入口进入模型节点 → 模型推理;
  2. 若模型产出了 tool_callAssistantMessage.hasToolCalls() == true)→ 路由到工具节点执行;
  3. 工具执行结果(ToolResponseMessage)回传给模型节点 → 模型继续推理;
  4. 若模型不再发起工具调用 → 路由到 END,循环结束。

这套「推理 → 行动 → 观察 → 循环」正是 ReAct 的机器实现。

3.2 构建 ReactAgent:Builder API

ReactAgent 采用 Builder 模式构建。把第 2 章的模型与工具串起来:

import com.alibaba.cloud.ai.graph.agent.ReactAgent;
import [email protected];
import org.springframework.ai.tool.ToolCallback;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;

@Configuration
public class AgentConfig {

    @Bean
    public ReactAgent weatherAgent(ChatModel ch@tModel, ToolCallback[] toolCallbacks) {
        return ReactAgent.builder()
                .name("weather_agent")                                   // 必填:图中唯一标识
                .description("天气查询助手,可查询指定城市天气")            // 描述
                .model(ch@tModel)                                        // 模型(model / ch@tClient 至少其一)
                .instruction("你是一个天气助手,用户问天气时调用 getWeather 工具。")
                .tools(List.of(toolCallbacks))                           // 挂载工具
                .build();
    }
}

Builder 常用方法逐项说明:

方法是否必填说明
name(String)✅ 必填Agent 名称,图中唯一标识
description(String)描述;在多智能体路由中,Router 依据它选择子 Agent
instruction(String)任务指令模板,支持 {占位符} 引用状态变量
systemPrompt(String)系统提示词(system 角色),固定人设/规则
model(ChatModel)二选一指定 ChatModel
ch@tClient(ChatClient)二选一或指定 ChatClient
tools(...)挂载已包装好的 ToolCallback 工具
methodTools(...)传入带 @Tool 方法的对象,框架自动包装成工具
outputKey(String)把最终结果写入 state 的指定 key
hooks(...)挂载 Hook(见第 5 章)

toolsmethodTools 的区别

  • tools(...):传入已经包装好的 ToolCallback(如 FunctionToolCallback、MCP 工具等);
  • methodTools(...):传入任意带 @Tool 注解方法的对象(如 @Component Bean),框架会通过 ToolCallbacks.from(...) 反射扫描其 @Tool 方法,自动包装成 MethodToolCallback 并注册。
WeatherTools weatherTools = new WeatherTools();   // 第 2 章里那个带 @Tool 方法的类

ReactAgent agent = ReactAgent.builder()
        .name("weather_agent")
        .model(ch@tModel)
        // 等价于 .tools(ToolCallbacks.from(weatherTools))
        .methodTools(weatherTools)
        .build();

注意:name 必须唯一且不可省略;modelch@tClient 至少要提供一个。 若需限制推理/工具调用轮数(防止死循环),可通过 ModelCallLimitHook / ToolCallLimitHook 等 Hook 实现。

指令、系统提示词与用户提示词的区别

ReactAgent 里容易混淆三个「提示词」,它们的角色各不相同:

概念设置方式消息角色是否支持占位符作用
系统提示词.systemPrompt(...)system固定人设 / 全局规则
指令(任务模板).instruction(...)user是(如 {input}描述「拿到输入后做什么」
用户提示词agent.call("...") 传入user每次对话的动态输入
  • systemPrompt:会作为 SystemMessage(system 角色)放进模型请求,不参与模板替换, 适合写「你是一个中文天气助手」这类固定人设。
  • instruction:由框架默认注入的 InstructionAgentHook 在每次运行前处理,是一个带占位符的模板 (user 角色)。发送给模型前,{input}{article} 等占位符会被替换成状态里的实际值, 适合写「写一篇关于 {input} 的文章」这类任务模板。
  • 用户提示词:即每次 call(...) 传入的 UserMessage,是动态输入。框架会把最后一条用户消息的文本 同时写入 input 状态键——它正是 instruction{input} 占位符的取值来源。

占位符替换的具体机制:instruction 先被 InstructionAgentHook 注入成 AgentInstructionMessage, 随后 AgentLlmNode 在组装请求时用 Spring AI 的 PromptTemplate(底层 StringTemplate)执行 render(params), 把 {input} 替换成状态里 input 键的值(即最后一条用户消息的文本);渲染完成后打上标记,避免每轮重复替换。

占位符的写法就是 {keyName}(StringTemplate 语法),keyName 必须是图状态里的一个键:

  • 内置键 {input}:用 call("...") / call(UserMessage) 时,框架自动把最后一条用户消息文本写入 input 键;
  • 自定义键:用 call(Map) 重载传入——messagesinput 是保留键,其余任意键都会作为状态键, 可在 instruction 里用 {keyName} 引用。
// 1) 内置键 {input}
agent.call("杭州今天天气怎么样?");   // 自动写入 input 键

// 2) 自定义键 {topic} / {tone}
Map<String, Object> inputs = new HashMap<>();
inputs.put("input", "请开始");                    // 保留键:提供输入
inputs.put("topic", "Spring AI Alibaba");         // 自定义键
inputs.put("tone", "通俗易懂");                   // 自定义键
agent.call(inputs);
// instruction 里可写:「用 {tone} 的语气,写一篇关于 {topic} 的文章」

3.3 使用工具的四种方式

ReactAgent 有四种接入工具的方式,按「业务最常用 → 最灵活」排列:

@Tool 注解(声明式,推荐) —— 在方法上直接加注解,框架自动生成工具定义,业务开发首选:

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;

@Component
public class WeatherTools {
    @Tool(description = "查询指定城市的实时天气")
    public String getWeather(@ToolParam(description = "城市名称,例如:杭州") String city) {
        return city + " 今天晴,25℃";
    }
}
// 挂载:.tools(toolCallbacks) 或 .methodTools(weatherTools)

FunctionToolCallback(函数式) —— 直接包装一个 Lambda/Function,轻量快速构建简单工具。它提供成对的两个 builder 重载

  • Function<I, O>(单参数):工具只关心输入 I、返回 O,最简洁;
  • BiFunction<I, ToolContext, O>(带工具上下文):额外拿到 ToolContext,可读取上下文与工具调用历史。
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.function.FunctionToolCallback;
import [email protected];

// 单参数 Function:只关心输入
ToolCallback weather = FunctionToolCallback.builder("getWeather", (String city) -> {
        return "晴朗,25℃";
    })
    .description("获取指定城市的天气")
    .inputType(String.class)
    .build();

// 带工具上下文的 BiFunction:额外拿到 ToolContext
ToolCallback weatherWithContext = FunctionToolCallback.builder("getWeather",
        (String city, ToolContext context) -> {
            // context.getContext():上下文 Map
            // context.getToolCallHistory():工具调用历史(List<Message>)
            return "晴朗,25℃";
        })
    .description("获取指定城市的天气(带上下文)")
    .inputType(String.class)
    .build();

// 挂载:.tools(weather) 或 .tools(weather, weatherWithContext)

框架内部会把单参数 Function 自动适配成 BiFunction(忽略 ToolContext)。 当工具需要读取工具调用历史或上下文元数据时,就用 BiFunction 版本。

MethodToolCallback(编程式) —— 用反射手动构造 ToolCallback,可精细自定义工具元数据。三个参数的来源如下:

import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.definition.ToolDefinition;
import org.springframework.ai.tool.method.MethodToolCallback;
import org.springframework.ai.tool.support.ToolDefinitions;

import java.lang.reflect.Method;

// 目标方法所在类(可以是任意普通类,不必加 @Tool 注解)
public class WeatherService {
    public String getWeather(String city) {
        return city + " 今天晴,25℃";
    }
}

// ① toolMethod:用反射拿到 java.lang.reflect.Method
Method method = WeatherService.class.getMethod("getWeather", String.class);
// 或 Spring 的 ReflectionUtils.findMethod(WeatherService.class, "getWeather", String.class)

// ② toolDefinition:描述工具的名称、描述、入参 Schema
//    方式一:从方法推断(有 @Tool 注解时读取,否则用方法名兜底),再按需覆盖
ToolDefinition toolDefinition = ToolDefinitions.builder(method)
        .name("getWeather")
        .description("查询指定城市的实时天气")
        .build();
//    方式二:完全手动指定
ToolDefinition manualDef = ToolDefinition.builder()
        .name("getWeather")
        .description("查询指定城市的实时天气")
        .inputSchema("{"type":"object","properties":{"city":{"type":"string"}}}")
        .build();

// ③ toolObject:方法所属的对象实例(new 出来的,或注入的 Spring Bean)
WeatherService instance = new WeatherService();

// 组装
ToolCallback tool = MethodToolCallback.builder()
        .toolDefinition(toolDefinition)
        .toolMethod(method)
        .toolObject(instance)
        .build();

// 挂载:.tools(tool)

三个参数:toolMethod 是反射拿到的 MethodtoolObject 是方法所属实例;toolDefinition 描述工具元数据, 可用 ToolDefinitions.builder(method) 从方法推断,也可用 ToolDefinition.builder() 手动指定 name / description / inputSchema

ToolCallbackProvider / AgentTool(动态提供者 / 智能体即工具) —— 运行时动态提供工具,或把子 Agent 封装成工具,用于动态工具集、多 Agent 编排:

// 动态提供者:实现 ToolCallbackProvider(getToolCallbacks()),运行时返回工具集合
ReactAgent.builder()
        .toolCallbackProviders(myProvider)
        .build();

// 智能体即工具:把子 Agent 包装成一个 ToolCallback,供上层 Agent 调用
import com.alibaba.cloud.ai.graph.agent.AgentTool;
ToolCallback subAgentTool = AgentTool.create(subAgent);

// 挂载:上层 Agent 通过 .tools(subAgentTool) 使用这个子 Agent 工具

小结:①③ 都基于「方法」(注解 vs 反射),② 基于 Lambda/Function,④ 面向「动态工具集 / 多 Agent」场景。 挂载方式:②③④ 产出的都是 ToolCallback,统一用 .tools(...) 挂载;① 的注解对象可用 .methodTools(obj) 或先转成 ToolCallback.tools(...);④ 的 provider 用 .toolCallbackProviders(...)

3.4 调用 Agent

ReactAgentcall(...) 有一组重载,覆盖了最常见的入参形式,同步返回 AssistantMessage

// 1) 直接传字符串
AssistantMessage r1 = agent.call("杭州今天天气怎么样?");

// 2) 传 UserMessage
AssistantMessage r2 = agent.call(new UserMessage("杭州今天天气怎么样?"));

// 3) 传消息列表(多轮上下文)
AssistantMessage r3 = agent.call(List.of(
        new UserMessage("你好"),
        new AssistantMessage("你好,请问有什么可以帮您?"),
        new UserMessage("杭州今天天气怎么样?")
));

// 4) 传字符串 + 运行时配置(threadId 用于关联会话/持久化)
AssistantMessage r4 = agent.call(
        "杭州今天天气怎么样?",
        RunnableConfig.builder().threadId("user-1001").build()
);

System.out.println(r4.getText());
  • 同步调用返回 AssistantMessage,可通过 .getText() 取文本内容;
  • 带上 RunnableConfig.threadId(...) 后,同一 threadId 的多次调用会共享会话上下文(见第 6 章)。

AssistantMessage.metadata 常用 Key

call(...) 返回的 AssistantMessage 除了文本,还通过 getMetadata() 携带一份 Map<String, Object>。 它是厂商扩展字段——并非所有模型都返回全部 key,不同服务商返回的 key 也不一样。

metadata 由两部分构成:

  • Spring AI 统一封装(响应层 ChatResponseMetadata):idmodelusage(token 用量)、rateLimit(限流)、promptMetadata
  • 各厂商原生返回(消息层 AssistantMessage.metadata),key 名随厂商而异。

常见的重要 key(以 1.1.2 实测源码为准):

key含义DashScopeOpenAIDeepSeek
finishReason生成结束原因(如 stop / length / tool_calls)
reasoningContent思考/推理内容(thinking 模型)
usagetoken 用量
requestId平台请求 ID
refusal安全拒绝原因
index候选结果序号

读取时务必判空:message.getMetadata().get("finishReason") 可能为 null, 因为不是所有模型/所有调用都会返回同一个 key。

3.5 Structured Output(结构化输出)

默认情况下,模型返回的是自由文本。结构化输出则要求模型把结果组织成符合指定结构的数据(如一个 Java POJO), 便于程序直接反序列化、后续节点直接消费。

ReactAgent 的 Builder 在源码中持有 outputSchema / outputType(输出结构)与 inputSchema / inputType(输入结构)等字段, 用于约束模型的输入/输出格式。使用时,通常是声明一个目标类型,让模型把答案填充到该结构的字段中, 从而避免「让模型返回 JSON 字符串再手工解析」的脆弱做法。

方式一:outputType —— 直接传目标 POJO 类型:

import com.alibaba.cloud.ai.graph.agent.ReactAgent;
import [email protected];
import com.fasterxml.jackson.databind.ObjectMapper;

// 目标结构
public class ContactInfo {
    private String name;
    private String email;
    private String phone;
    // 省略 getter / setter
}

ReactAgent agent = ReactAgent.builder()
        .name("contact_extractor")
        .model(ch@tModel)
        .outputType(ContactInfo.class)   // 按该类型约束输出结构
        .build();

AssistantMessage result = agent.call(
        "从以下信息提取联系方式:张三,[email protected],(555) 123-4567");

// 结果文本是 JSON,用 Jackson 反序列化
ContactInfo info = new ObjectMapper().readValue(result.getText(), ContactInfo.class);

方式二:outputSchema —— 用 BeanOutputConverter 生成 JSON Schema 再传入:

import org.springframework.ai.converter.BeanOutputConverter;

BeanOutputConverter<ContactInfo> converter = new BeanOutputConverter<>(ContactInfo.class);
String schema = converter.getFormat();   // 生成 JSON Schema 字符串

ReactAgent agent = ReactAgent.builder()
        .name("contact_extractor")
        .model(ch@tModel)
        .outputSchema(schema)            // 传入 JSON Schema
        .build();

两者都约束模型返回结构化 JSON:outputType 更省事(直接给类型),outputSchema 更灵活(可自定义 Schema)。 取回结果后用 result.getText() + Jackson 反序列化即可得到 POJO 对象。


第 4 章 Skills(技能)

Skill 是「可复用的能力说明」——它把一个领域的操作步骤写成一份 Markdown 文件(SKILL.md), 模型在需要时加载并按其指令行事,从而「学会」如何完成某项任务。

渐进式披露(Progressive Disclosure)

Skill 采用渐进式披露策略,避免把所有技能内容一次性塞进上下文:

  1. 系统提示里只注入技能列表(每个技能的 namedescription、路径等轻量信息);
  2. 模型判断某个技能与当前任务相关时,调用 read_skill(skill_name) 按需加载完整的 SKILL.md
  3. 再按 SKILL.md 里的说明,按需访问技能目录下的脚本、参考资料等资源,或使用与该技能绑定的工具。

一句话:先让模型知道「有哪些技能」,用到时再加载完整说明,从而大幅节省上下文。

Skill 目录结构

每个技能是一个独立子目录,其中 SKILL.md 必需:

skill-name/
├── SKILL.md        # 必需:技能说明
├── references/     # 可选:参考资料
├── examples/       # 可选:示例
└── scripts/        # 可选:脚本等辅助资源

框架扫描技能目录时,只认含 SKILL.md 的子目录;SKILL.md 里可用绝对路径引用同目录下的脚本/资料。

SKILL.md 格式规范

SKILL.md 由 YAML frontmatter + Markdown 正文组成:

---
name: pdf-extractor
description: 提取 PDF 中的文本、表格与表单数据。当用户要求提取、解析或分析 PDF 时使用。
---

# PDF Extractor Skill

1. 确认 PDF 文件路径存在
2. 执行脚本提取内容
3. 解析并整理输出 JSON

必需字段:

字段规则
name小写字母、数字、单连字符(a-z 0-9 -),最长 64 字符;不能以连字符开头/结尾,不能连续连字符
description最长 1024 字符,超出会被截断

接入方式:先创建一个 FileSystemSkillRegistry 指向技能目录,再用 SkillsAgentHook 挂到 Agent 上:

import com.alibaba.cloud.ai.graph.agent.hook.skills.SkillsAgentHook;
import com.alibaba.cloud.ai.graph.skills.registry.FileSystemSkillRegistry;

FileSystemSkillRegistry registry = FileSystemSkillRegistry.builder()
        .userSkillsDirectory("~/saa/skills")   // 用户级技能目录
        .projectSkillsDirectory("./skills")    // 项目级技能目录
        .build();

SkillsAgentHook skillsHook = SkillsAgentHook.builder()
        .skillRegistry(registry)
        .autoReload(true)                       // 每次调用前重新加载技能
        .build();

ReactAgent agent = ReactAgent.builder()
        .name("skill_agent")
        .model(ch@tModel)
        .hooks(List.of(skillsHook))             // 通过 hooks 挂载技能
        .build();

挂载后,框架会:把技能列表注入系统提示(通过 SkillsInterceptor),并注册 read_skill 工具; 当模型判断某个技能相关时,会调用 read_skill 读取对应 SKILL.md 内容,再按其中指令行事。


第 5 章 Hooks 和 Interceptors

Hook 是插入到 Agent 执行生命周期中的自定义逻辑,Agent 的运行过程被划分为若干位置(Position):

HookPosition时机
BEFORE_AGENTAgent 开始前
AFTER_AGENTAgent 结束后
BEFORE_MODEL每次调用模型前
AFTER_MODEL每次调用模型后

通过 @HookPositions(...) 注解声明 Hook 生效的位置(可指定多个)。

5.1 能做什么?

Hooks 和 Interceptors 覆盖四类能力:

类别说明典型场景
坚控记录日志、分析、调试跟踪 Agent 行为耗时统计、链路追踪
修改转换提示、工具选择和输出格式改写请求、格式化结果
控制添加重试、回退和提前终止逻辑重试、熔断、提前终止
强制执行应用速率限制、护栏和 PII 检测限流、脱敏、安全护栏

5.2 内置实现

框架内置了大量 Hooks(生命周期级)与 Interceptors(模型/工具调用级):

Hooks(生命周期级)

Hook作用
SummarizationHook消息压缩 / 上下文摘要
HumanInTheLoopHook人机协同(人工审批,见 5.7)
ModelCallLimitHook / ToolCallLimitHook模型 / 工具调用次数限制
PIIDetectionHookPII 敏感信息检测
SkillsAgentHook技能加载(见第 4 章)
InstructionAgentHook指令注入(框架默认)
InterruptionHook中断反馈
ReturnDirectModelHook工具结果直接返回

使用示例(构造 + 挂载):

// 消息压缩 / 上下文摘要
SummarizationHook summarization = SummarizationHook.builder()
        .model(ch@tModel)
        .maxTokensBeforeSummary(4000)   // 超过 4000 token 触发摘要
        .messagesToKeep(10)             // 保留最近 10 条消息
        .build();

// 模型调用次数限制
ModelCallLimitHook callLimit = ModelCallLimitHook.builder()
        .runLimit(5)                    // 单次运行最多 5 次模型调用
        .build();

// PII 检测(邮箱脱敏)
PIIDetectionHook pii = PIIDetectionHook.builder()
        .piiType(PIIType.EMAIL)
        .strategy(RedactionStrategy.MASK)
        .build();

// 人机协同(需人工审批)
HumanInTheLoopHook hitl = HumanInTheLoopHook.builder()
        .approvalOn("transfer_money", "操作需人工确认")
        .build();

// 挂载:Hooks 统一用 .hooks(...) 注册
ReactAgent agent = ReactAgent.builder()
        .name("agent")
        .model(ch@tModel)
        .hooks(List.of(summarization, callLimit, pii, hitl))
        .build();

Hook 类分别位于 com.alibaba.cloud.ai.graph.agent.hook.summarization / modelcalllimit / pii / hip 等子包。

Interceptors(模型/工具调用级)

Interceptor作用
ModelRetryInterceptor模型调用重试
ToolRetryInterceptor工具调用重试(支持特定异常 + 指数退避)
TodoListInterceptorPlanning(规划):引导模型用 write_todos 拆解任务
ToolSelectionInterceptorLLM 工具选择器:先用 LLM 筛出相关工具
ToolEmulatorInterceptorLLM 工具模拟器:用 LLM 模拟工具而非真实执行
SkillsInterceptor把技能列表注入系统提示

使用示例(构造 + 挂载):

// 工具重试(指数退避)
ToolRetryInterceptor toolRetry = ToolRetryInterceptor.builder()
        .maxRetries(3)
        .toolName("getWeather")
        .initialDelay(100)              // 首次重试延迟 100ms
        .backoffFactor(2.0)             // 每次重试延迟翻倍
        .build();

// Planning(规划):引导模型用 write_todos 拆解任务
TodoListInterceptor planning = TodoListInterceptor.builder().build();

// LLM 工具选择器:先用模型筛出相关工具
ToolSelectionInterceptor selector = ToolSelectionInterceptor.builder()
        .selectionModel(ch@tModel)
        .maxTools(5)
        .build();

// LLM 工具模拟器:用 LLM 模拟工具而非真实执行
ToolEmulatorInterceptor emulator = ToolEmulatorInterceptor.builder()
        .model(ch@tModel)
        .emulateAllTools(true)
        .build();

// 挂载:拦截器统一用 .interceptors(...) 注册(框架按类型自动分流)
ReactAgent agent = ReactAgent.builder()
        .name("agent")
        .model(ch@tModel)
        .interceptors(toolRetry, planning, selector, emulator)
        .build();

拦截器类位于 com.alibaba.cloud.ai.graph.agent.interceptor.toolretry / todolist / toolselection / toolemulator 等子包。 注意:拦截器用 .interceptors(...) 注册(而非 .hooks(...)),框架内部会自动按 ModelInterceptor / ToolInterceptor 类型分流。

5.3 自定义 Hook

自定义 Hook 只需继承 AgentHook(或 ModelHook),用 @HookPositions 标注位置、覆写对应方法。

① 耗时统计 —— 记录 Agent 整体耗时:

import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.RunnableConfig;
import com.alibaba.cloud.ai.graph.agent.hook.AgentHook;
import com.alibaba.cloud.ai.graph.agent.hook.HookPosition;
import com.alibaba.cloud.ai.graph.agent.hook.HookPositions;

import java.util.Map;
import java.util.concurrent.CompletableFuture;

@HookPositions({HookPosition.BEFORE_AGENT, HookPosition.AFTER_AGENT})
public class TimingHook extends AgentHook {

    private long start;

    @Override
    public CompletableFuture<Map<String, Object>> beforeAgent(OverAllState state, RunnableConfig config) {
        start = System.currentTimeMillis();
        return CompletableFuture.completedFuture(Map.of());
    }

    @Override
    public CompletableFuture<Map<String, Object>> afterAgent(OverAllState state, RunnableConfig config) {
        System.out.println("Agent 耗时:" + (System.currentTimeMillis() - start) + "ms");
        return CompletableFuture.completedFuture(Map.of());
    }

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

② 消息上限限制 —— 继承 MessagesAgentHook 编辑消息列表(上下文裁剪):

import com.alibaba.cloud.ai.graph.RunnableConfig;
import com.alibaba.cloud.ai.graph.agent.hook.HookPosition;
import com.alibaba.cloud.ai.graph.agent.hook.HookPositions;
import com.alibaba.cloud.ai.graph.agent.hook.messages.AgentCommand;
import com.alibaba.cloud.ai.graph.agent.hook.messages.MessagesAgentHook;

import [email protected];

import java.util.ArrayList;
import java.util.List;

@HookPositions(HookPosition.BEFORE_AGENT)
public class MessageLimitHook extends MessagesAgentHook {

    private final int maxMessages;

    public MessageLimitHook(int maxMessages) { this.maxMessages = maxMessages; }

    @Override
    public AgentCommand beforeAgent(List<Message> previousMessages, RunnableConfig config) {
        // 超过上限:只保留最近 N 条
        List<Message> trimmed = previousMessages.size() > maxMessages
                ? new ArrayList<>(previousMessages.subList(previousMessages.size() - maxMessages, previousMessages.size()))
                : previousMessages;
        return new AgentCommand(trimmed);
    }

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

提前终止ModelCallLimitHook 在调用次数超限时抛出 ModelCallLimitExceededException 终止执行; 更通用地,Hook 可通过 canJumpTo() 声明可跳转目标,配合 jump_to 状态把流程导向 JumpTo.end 提前结束。

5.4 自定义 Interceptor

Interceptor 采用装饰器模式interceptXxx(request, handler) 里通过 handler.call(request) 调用下一层, 可自由决定调用前后做什么、是否调用、调用几次。类型都在 com.alibaba.cloud.ai.graph.agent.interceptor 包下。

ModelInterceptor —— 模型调用耗时 + 敏感词过滤

import com.alibaba.cloud.ai.graph.agent.interceptor.ModelCallHandler;
import com.alibaba.cloud.ai.graph.agent.interceptor.ModelInterceptor;
import com.alibaba.cloud.ai.graph.agent.interceptor.ModelRequest;
import com.alibaba.cloud.ai.graph.agent.interceptor.ModelResponse;

public class ModelGuardInterceptor extends ModelInterceptor {

    @Override
    public ModelResponse interceptModel(ModelRequest request, ModelCallHandler handler) {
        long start = System.currentTimeMillis();
        // 敏感词过滤:调用前改写 request(示意)
        ModelResponse response = handler.call(request);   // 真实调用模型
        long cost = System.currentTimeMillis() - start;
        System.out.println("模型调用耗时:" + cost + "ms");
        // 也可对 response 做敏感词过滤后再返回
        return response;
    }

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

ToolInterceptor —— 工具耗时记录 + 缓存

import com.alibaba.cloud.ai.graph.agent.interceptor.ToolCallHandler;
import com.alibaba.cloud.ai.graph.agent.interceptor.ToolCallRequest;
import com.alibaba.cloud.ai.graph.agent.interceptor.ToolCallResponse;
import com.alibaba.cloud.ai.graph.agent.interceptor.ToolInterceptor;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ToolCacheInterceptor extends ToolInterceptor {

    private final Map<String, ToolCallResponse> cache = new ConcurrentHashMap<>();

    @Override
    public ToolCallResponse interceptToolCall(ToolCallRequest request, ToolCallHandler handler) {
        String key = request.toString();   // 示意:按请求内容做缓存键
        if (cache.containsKey(key)) {
            return cache.get(key);          // 命中缓存,跳过真实调用
        }
        long start = System.currentTimeMillis();
        ToolCallResponse response = handler.call(request);   // 真实调用工具
        long cost = System.currentTimeMillis() - start;
        System.out.println("工具耗时:" + cost + "ms");
        cache.put(key, response);
        return response;
    }

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

5.5 Hook 和 Interceptor 的选择与调用顺序

选择

  • 需要按「执行阶段」插入逻辑(Agent 开始/结束、每次调模型前后)→ 用 Hook
  • 需要包裹「模型调用」或「工具调用」本身(重试、缓存、改写请求/结果)→ 用 Interceptor

调用顺序

  • Hook:按 Position 分阶段执行(BEFORE_AGENT → BEFORE_MODEL →(模型/工具循环)→ AFTER_MODEL → AFTER_AGENT); 同一 Position 内的多个 Hook 按 Prioritized.getOrder() 升序执行(InstructionAgentHook 的 order 为 -100,最先)。
  • Interceptor:以装饰器链(InterceptorChain)的形式包裹,每个拦截器调用 handler.call(...) 转发到下一层, 因此按注册顺序「外层先入、内层后入」,重试/缓存等逻辑在链上叠加。

5.6 Context Editing(上下文编辑)

Context Editing 指在每次调用前/后直接增删改对话历史(List<Message>)。它通过 MessagesAgentHook / MessagesModelHook 实现——覆写 beforeAgent(List<Message>, config) 返回一个 AgentCommand(携带修改后的消息列表),框架会用其替换当前消息。

5.3 的「消息上限限制」就是 Context Editing 的一个例子(裁剪旧消息)。其他典型用途:

  • 注入 / 更新指令消息;
  • 脱敏后重写历史消息(配合 PII 检测);
  • 裁剪过长的上下文,控制 token 消耗。

示例(注入系统提示 + 裁剪消息):

import com.alibaba.cloud.ai.graph.RunnableConfig;
import com.alibaba.cloud.ai.graph.agent.hook.HookPosition;
import com.alibaba.cloud.ai.graph.agent.hook.HookPositions;
import com.alibaba.cloud.ai.graph.agent.hook.messages.AgentCommand;
import com.alibaba.cloud.ai.graph.agent.hook.messages.MessagesAgentHook;

import [email protected];
import [email protected];

import java.util.ArrayList;
import java.util.List;

@HookPositions(HookPosition.BEFORE_AGENT)
public class ContextEditingHook extends MessagesAgentHook {

    private final int maxMessages;

    public ContextEditingHook(int maxMessages) { this.maxMessages = maxMessages; }

    @Override
    public AgentCommand beforeAgent(List<Message> previousMessages, RunnableConfig config) {
        List<Message> edited = new ArrayList<>();

        // ① 注入系统提示(若尚未有)
        boolean hasSystem = previousMessages.stream().anyMatch(m -> m instanceof SystemMessage);
        if (!hasSystem) {
            edited.add(new SystemMessage("你是一个中文助手"));
        }

        // ② 裁剪:超过上限只保留最近 N 条
        int start = Math.max(0, previousMessages.size() - maxMessages);
        edited.addAll(previousMessages.subList(start, previousMessages.size()));

        // 返回修改后的列表(默认 UpdatePolicy.REPLACE 替换整个消息列表)
        return new AgentCommand(edited);
    }

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

MessagesModelHook 与之类似,但作用在每次模型调用前后(beforeModel / afterModel),粒度更细; AgentCommand 还支持 UpdatePolicy(如 REPLACE)与 JumpTo,可控制替换策略与跳转目标。

5.7 Human-in-the-Loop(人工介入)

Human-in-the-Loop 指在 Agent 执行的关键节点「停下来,等人类确认/修改后再继续」。 官方文档明确指出:Agent 使用持久化机制来实现长期记忆与人工介入——HITL 正是建立在第 6 章讲的检查点机制之上的。

最典型的场景是「敏感操作需审批」,例如。Spring AI Alibaba 提供了开箱即用的 HumanInTheLoopHook,指定哪些工具需要人工审批:

import com.alibaba.cloud.ai.graph.agent.hook.hip.HumanInTheLoopHook;

HumanInTheLoopHook hitlHook = HumanInTheLoopHook.builder()
        .approvalOn("transfer_money", "操作需人工确认")
        .build();

ReactAgent agent = ReactAgent.builder()
        .name("bank_agent")
        .model(ch@tModel)
        .tools(toolCallbacks)
        .hooks(List.of(hitlHook))
        .build();

工作流程:

  1. Agent 推理后,决定调用 transfer_money
  2. HumanInTheLoopHook(在 AFTER_MODEL 位置)拦截,产生中断(InterruptionMetadata),暂停执行;
  3. 人类对每个工具调用给出反馈:APPROVED(同意) / EDITED(修改参数) / REJECTED(拒绝)
  4. 框架根据反馈恢复执行(resume),继续后续推理。

在更底层的 Graph 里,对应的是 InterruptableAction 接口——它提供两个钩子点:

public interface InterruptableAction {
    // 节点执行前调用,可阻止执行
    Optional<InterruptionMetadata> interrupt(String nodeId, OverAllState state, RunnableConfig config);

    // 节点执行后调用,可检查结果并中断
    default Optional<InterruptionMetadata> interruptAfter(String nodeId, OverAllState state,
            Map<String, Object> actionResult, RunnableConfig config) {
        return Optional.empty();
    }
}

配合检查点与 resume 机制,即可实现审批流、多轮对话式交互等「人机协同」场景(详见第 8 章的中断恢复)。


第 6 章 Memory(记忆)

6.1 三层记忆:短期 / 中期 / 长期

Agent 的「记忆」通常分三层:

层级内容存储 / 技术在本框架中的实现
短期记忆最近 10–20 条对话记录Redis / DB 等CheckpointSaver + threadId(内置)
中期记忆提取与当前对话相关联的聊天记录RAG(检索增强)结合向量库做检索
长期记忆核心结构化数据(用户画像、经验信息)结构化存储持久化事实,如「我男性、35 岁」
  • 短期记忆:保存当前会话的近期对话记录,靠 checkpointer 实现(框架内置);
  • 中期记忆:从历史聊天记录中检索出与当前对话相关的内容,用 RAG(向量检索)实现;
  • 长期记忆:持久化用户画像、经验等结构化事实(如「我男性、35 岁」),跨会话长期复用。

注意:框架原生提供的是短期记忆(checkpointer);中期(RAG)与长期(结构化画像)是你在其上构建的模式。

6.2 短期记忆:checkpointer

模型本身是无状态的:你不主动把历史带进去,它就「记不住」上一轮说过什么。

在 Spring AI Alibaba 中,短期记忆(会话级持久化)的实现方式是——在创建 Agent 时指定 checkpointer(检查点器)。 有了 checkpointer,框架会在每轮执行后保存状态(检查点),下次调用时用同一个会话 ID 即可恢复上下文。

会话 ID 通过 RunnableConfig.threadId(...) 指定:

RunnableConfig config = RunnableConfig.builder()
        .threadId("user-1001")   // 会话 ID
        .build();

agent.call("我叫小明", config);          // 第 1 轮
agent.call("我叫什么名字?", config);      // 第 2 轮:能记住上一轮

同一个 threadId 的多次调用共享状态;不同 threadId 之间相互隔离。

6.3 内置 Saver 与保存策略

框架提供了多种 Saver 实现,对应不同的持久化后端:

Saver存储位置适用场景
MemorySaver内存开发调试;进程重启即丢
VersionedMemorySaver内存(带版本)同一会话多次执行、需要版本回溯
FileSystemSaver文件系统单机持久化
RedisSaverRedis分布式部署(生产常用)
MongoSaver / MysqlSaver / PostgresSaver / OracleSaver对应数据库需要与既有存储统一

在 Graph 层,通过 SaverConfig 注册存档器,并在编译图时传入(StateGraph.compile() 默认就使用 MemorySaver):

import com.alibaba.cloud.ai.graph.CompileConfig;
import com.alibaba.cloud.ai.graph.CompiledGraph;
import com.alibaba.cloud.ai.graph.StateGraph;
import com.alibaba.cloud.ai.graph.checkpoint.config.SaverConfig;
import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver;

SaverConfig saverConfig = SaverConfig.builder()
        .register(new MemorySaver())
        .build();

CompiledGraph graph = stateGraph.compile(
        CompileConfig.builder()
                .saverConfig(saverConfig)
                .build()
);

ReactAgent 本身就是一张编译好的图,其 Builder 同样支持通过 .saver(...) 配置 checkpointer。 以 Redis(Redisson)为例:

import com.alibaba.cloud.ai.graph.agent.ReactAgent;
import com.alibaba.cloud.ai.graph.checkpoint.savers.redis.RedisSaver;
import org.redisson.api.RedissonClient;

// 1) 拿到 RedissonClient(Spring Boot 自动装配或手动构建)
// 2) 构建 RedisSaver
RedisSaver redisSaver = RedisSaver.builder()
        .redisson(redissonClient)
        .build();

// 3) 在创建 Agent 时指定 saver
ReactAgent agent = ReactAgent.builder()
        .name("agent")
        .model(ch@tModel)
        .saver(redisSaver)          // 指定 checkpointer
        .build();

状态合并策略(KeyStrategy):同名 key 如何合并——ReplaceStrategy(覆盖,适合单值结果)、 AppendStrategy(追加,适合 messages 这类需累积的列表)。

6.4 记忆带来的上下文过长问题

启用短期记忆后,长对话会不断累积消息,最终可能超过 LLM 的上下文窗口。常见解决方案:

方案做法对应实现
修剪消息调用 LLM 前移除前 N 条或后 N 条消息MessagesAgentHook 裁剪(见 5.6)
删除消息从 Graph 状态中永久删除消息直接操作 state 的 messages
总结消息把较早的消息总结成摘要替换SummarizationHook
自定义策略按需自定义(如消息过滤、按类型保留)自定义 Hook / Interceptor

示例(总结 + 裁剪):

// 总结:超过 4000 token 时把早期消息压缩成摘要
SummarizationHook summarize = SummarizationHook.builder()
        .model(ch@tModel)
        .maxTokensBeforeSummary(4000)
        .messagesToKeep(10)
        .build();

ReactAgent agent = ReactAgent.builder()
        .name("agent")
        .model(ch@tModel)
        .hooks(List.of(summarize))
        .build();

6.5 访问和修改短期记忆(状态)

短期记忆(状态)可以在工具或 Hook 中读取和修改。

在工具中读取 —— 使用 ToolContext 参数访问状态。toolContext 参数从工具签名中隐藏(模型看不到它), 但工具方法可以通过它访问状态(如 RunnableConfig 及其 metadata):

import com.alibaba.cloud.ai.graph.RunnableConfig;
import com.alibaba.cloud.ai.graph.agent.ReactAgent;
import com.alibaba.cloud.ai.graph.agent.tools.ToolContextHelper;
import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver;
import [email protected];
import [email protected];
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.function.FunctionToolCallback;

import java.util.function.BiFunction;

// 第二个参数 ToolContext 由框架注入,模型看不到
public class UserInfoTool implements BiFunction<String, ToolContext, String> {
    @Override
    public String apply(String query, ToolContext toolContext) {
        // 从上下文获取 RunnableConfig
        RunnableConfig config = ToolContextHelper.getConfig(toolContext).orElseThrow();
        String userId = (String) config.metadata("user_id").orElse("");
        if ("user_123".equals(userId)) {
            return "用户是 John Smith";
        }
        return "未知用户";
    }
}

// 创建工具
ToolCallback getUserInfoTool = FunctionToolCallback
        .builder("get_user_info", new UserInfoTool())
        .description("查找用户信息")
        .inputType(String.class)
        .build();

// 挂载并使用(通过 addMetadata 注入 user_id)
ReactAgent agent = ReactAgent.builder()
        .name("my_agent")
        .model(ch@tModel)
        .tools(getUserInfoTool)
        .saver(new MemorySaver())
        .build();

RunnableConfig config = RunnableConfig.builder()
        .threadId("1")
        .addMetadata("user_id", "user_123")
        .build();

AssistantMessage response = agent.call("获取用户信息", config);
System.out.println(response.getText());

底层说明:ToolContext 里 config 存于 "_AGENT_CONFIG_" 键、state 存于 "_AGENT_STATE_" 键 (见 ToolContextConstants);ToolContextHelper 提供了 getConfig / getState / getMetadata 等便捷方法。

从工具写入 —— 要在执行期间修改短期记忆(状态),有两种途径:

  1. 在 Hook 中更新状态:Hook 的 beforeXxx / afterXxx 返回的 Map<String, Object> 会合并进图状态;
  2. 工具返回的信息更新状态:工具返回的结果会作为 ToolResponseMessage 回写 messages 状态。
// 在 Hook 中写入自定义状态字段(持久化中间结果)
@HookPositions(HookPosition.AFTER_AGENT)
public class SaveStateHook extends AgentHook {
    @Override
    public CompletableFuture<Map<String, Object>> afterAgent(OverAllState state, RunnableConfig config) {
        return CompletableFuture.completedFuture(Map.of("last_result", "..."));  // 示意
    }
}

这对持久化中间结果、或让信息对后续工具/提示可访问很有用。

在 beforeModel / afterModel Hook 中处理消息 —— 继承 MessagesModelHook,直接拿到 List<Message>

  • beforeModel(List<Message>, config):在模型调用之前处理消息(裁剪、注入、脱敏);
  • afterModel(List<Message>, config):在模型调用之后处理消息(记录、改写结果)。
import com.alibaba.cloud.ai.graph.RunnableConfig;
import com.alibaba.cloud.ai.graph.agent.hook.HookPosition;
import com.alibaba.cloud.ai.graph.agent.hook.HookPositions;
import com.alibaba.cloud.ai.graph.agent.hook.messages.AgentCommand;
import com.alibaba.cloud.ai.graph.agent.hook.messages.MessagesModelHook;

import [email protected];

import java.util.List;

@HookPositions({HookPosition.BEFORE_MODEL, HookPosition.AFTER_MODEL})
public class MessageProcessHook extends MessagesModelHook {

    @Override
    public AgentCommand beforeModel(List<Message> previousMessages, RunnableConfig config) {
        // 模型调用前:处理消息(裁剪 / 注入 / 脱敏)
        return new AgentCommand(previousMessages);  // 示意
    }

    @Override
    public AgentCommand afterModel(List<Message> previousMessages, RunnableConfig config) {
        // 模型调用后:处理消息(记录 / 改写结果)
        return new AgentCommand(previousMessages);  // 示意
    }

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

MessagesModelHook(操作 List<Message>,返回 AgentCommand)比 ModelHook(操作 OverAllState,返回 Map) 更适合消息级处理;ModelHook 则适合读写任意状态字段

ModelInterceptor 中基于状态创建动态提示 —— 除 Hook 外,ModelInterceptor 也能读取状态并改写提示:

import com.alibaba.cloud.ai.graph.agent.interceptor.ModelCallHandler;
import com.alibaba.cloud.ai.graph.agent.interceptor.ModelInterceptor;
import com.alibaba.cloud.ai.graph.agent.interceptor.ModelRequest;
import com.alibaba.cloud.ai.graph.agent.interceptor.ModelResponse;

import [email protected];

import java.util.Map;

public class DynamicPromptInterceptor extends ModelInterceptor {

    @Override
    public ModelResponse interceptModel(ModelRequest request, ModelCallHandler handler) {
        // 从 context 读取状态(ReactAgent 会把 state.data() 放进 context)
        Map<String, Object> context = request.getContext();

        // 基于对话历史或自定义状态字段创建动态提示(示意)
        SystemMessage dynamicPrompt = new SystemMessage("你是用户 " + context.get("user_id") + " 的助手");

        // 用改写后的请求调用下一层
        ModelRequest newRequest = ModelRequest.builder(request)
                .systemMessage(dynamicPrompt)
                .build();
        return handler.call(newRequest);
    }

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

ModelRequest.getContext() 里就是 Agent 的状态数据(对话历史、自定义字段),ModelInterceptor 可据此改写 systemMessage / messages 生成动态提示;拦截器通过 .interceptors(...) 注册(见 5.2)。

直接查询当前会话状态

StateSnapshot snapshot = agent.getCurrentState(
        RunnableConfig.builder().threadId("user-1001").build()
);

第三部分:多智能体与图编排

第 7 章 多智能体(Multi-agent)与 Agent Tool

单个 Agent 能力有限,复杂任务往往需要多个 Agent 分工协作。Spring AI Alibaba 内置了四种编排模式, 它们都在 com.alibaba.cloud.ai.graph.agent.flow.agent 包下。

7.1 Instruction 占位符

子 Agent 的 instruction 支持 {占位符},会被替换为图状态里对应键的值。这是多智能体之间传递数据的核心机制。

① 支持的占位符

占位符来源说明
{input}内置自动填入最后一条用户消息文本
{outputKey}前序 Agent 的 outputKey{article}{reviewed},把上游结果传给下游
{自定义键}call(Map) 传入{topic}{tone},运行时注入的任意状态键

不支持的占位符(替换时会被剔除):{messages}(消息列表被显式排除)、List 类型的状态值 (Message 类型会转成 getText())。

② 占位符工作原理

  1. InstructionAgentHookBEFORE_AGENT)把 instruction 注入成 AgentInstructionMessage(此时 {占位符} 尚未替换);
  2. AgentLlmNode 组装请求时,renderTemplatedUserMessage 把当前图状态 state.data() 处理成参数 Map (剔除 messagesList 值,Message 转文本);
  3. 用 Spring AI 的 PromptTemplate(底层 StringTemplate)执行 render(params),把 {key} 替换成对应值;
  4. 渲染后打上 rendered 标记,避免每轮重复替换。

③ 使用示例

// 例 1:前序 outputKey 传给后序(SequentialAgent)
ReactAgent writer = ReactAgent.builder()
        .name("writer")
        .model(ch@tModel)
        .instruction("写一篇关于 {input} 的文章")
        .outputKey("article")                       // 结果写回 state 的 article 键
        .build();

ReactAgent reviewer = ReactAgent.builder()
        .name("reviewer")
        .model(ch@tModel)
        .instruction("校对该文章:{article}")        // {article} 引用上游 outputKey
        .outputKey("reviewed")
        .build();

// 例 2:自定义键(call(Map) 注入运行时参数)
Map<String, Object> inputs = new HashMap<>();
inputs.put("input", "Spring AI Alibaba");    // 内置键
inputs.put("tone", "通俗易懂");              // 自定义键
agent.call(inputs);                          // instruction 可写「用 {tone} 的语气…」

7.2 SequentialAgent(顺序执行)

多个子 Agent 依次执行,前一个的输出(outputKey)会自动映射为后一个的模板变量:

import com.alibaba.cloud.ai.graph.agent.flow.agent.SequentialAgent;
import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.RunnableConfig;
import [email protected];

import java.util.Optional;

// 写作 Agent:产出 {article}
ReactAgent writer = ReactAgent.builder()
        .name("writer")
        .model(ch@tModel)
        .instruction("写一篇关于 {input} 的文章")
        .outputKey("article")
        .build();

// 校对 Agent:读取 {article},产出 {reviewed}
ReactAgent reviewer = ReactAgent.builder()
        .name("reviewer")
        .model(ch@tModel)
        .instruction("校对该文章:{article}")
        .outputKey("reviewed")
        .build();

SequentialAgent workflow = SequentialAgent.builder()
        .name("blog_workflow")
        .subAgents(List.of(writer, reviewer))
        .build();

Optional<OverAllState> result = workflow.invoke(
        UserMessage.builder().text("Spring AI Alibaba").build(),
        RunnableConfig.builder().threadId("user-1").build()
);

// 使用结果:从 Optional<OverAllState> 中取出 outputKey 对应的值
result.ifPresent(state -> {
    String article = (String) state.value("article").orElse("");
    String reviewed = (String) state.value("reviewed").orElse("");
    System.out.println("终稿:" + reviewed);
});

前一个 Agent 的 outputKey(如 article)会自动成为后一个 Agent instruction{article} 占位符的值—— 这正是上一节(7.1)讲的占位符机制在多智能体下的应用。

适用场景:流水线任务(生成 → 校对 → 润色)。

7.3 ParallelAgent(并行执行)

多个子 Agent 并发处理同一输入,结果合并:

import com.alibaba.cloud.ai.graph.agent.flow.agent.ParallelAgent;

import java.util.List;

ParallelAgent parallel = ParallelAgent.builder()
        .name("parallel")
        .subAgents(List.of(agentA, agentB, agentC))
        .mergeOutputKey("merged_result")   // 合并结果写入的 state key
        .build();

执行并获取合并结果:

import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.RunnableConfig;
import [email protected];

import java.util.Optional;

Optional<OverAllState> result = parallel.invoke(
        UserMessage.builder().text("同时分析三个数据源").build(),
        RunnableConfig.builder().threadId("user-1").build()
);

// 从 mergeOutputKey 指定的 key 读取合并结果
result.ifPresent(state -> {
    Object merged = state.value("merged_result").orElse(null);
    System.out.println("合并结果:" + merged);
});

适用场景:互不依赖的子任务并发处理(如同时从多个数据源取数)。

默认用 DefaultMergeStrategy 把各子 Agent 的结果合并成 Map;你也可以实现自定义的 MergeStrategy,控制如何组合多个 Agent 的输出:

import com.alibaba.cloud.ai.graph.agent.flow.agent.ParallelAgent.MergeStrategy;

// 自定义合并策略:例如把各子 Agent 的结果拼接成字符串
MergeStrategy concatStrategy = (subAgentResults, overallState) -> {
    StringBuilder sb = new StringBuilder();
    subAgentResults.forEach((k, v) -> sb.append(v).append("n"));
    return sb.toString();
};

ParallelAgent parallel2 = ParallelAgent.builder()
        .name("parallel2")
        .subAgents(List.of(agentA, agentB, agentC))
        .mergeOutputKey("merged_result")
        .mergeStrategy(concatStrategy)     // 自定义合并策略
        .build();

7.4 LlmRoutingAgent(LLM 路由)

路由模式:由一个 Router LLM 根据各子 Agent 的 description,动态决定将请求路由到哪个子 Agent。 这种模式非常适合需要智能选择不同专家 Agent 的场景:

import com.alibaba.cloud.ai.graph.agent.flow.agent.LlmRoutingAgent;

LlmRoutingAgent router = LlmRoutingAgent.builder()
        .name("router")
        .model(ch@tModel)                  // 路由决策所用的模型
        .subAgents(weatherAgent, travelAgent, financeAgent)
        .build();

优化路由准确性LlmRoutingAgent 支持通过 systemPromptinstruction 自定义路由决策行为,提供更精确的路由控制:

LlmRoutingAgent router = LlmRoutingAgent.builder()
        .name("router")
        .model(ch@tModel)
        .systemPrompt("你是智能客服路由,根据用户问题选择最合适的专家")
        .instruction("可用的专家:n- weather_agent:天气n- travel_agent:旅游n- finance_agent:")
        .subAgents(weatherAgent, travelAgent, financeAgent)
        .build();

执行并获取路由结果:

import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.RunnableConfig;
import [email protected];
import [email protected];

import java.util.List;
import java.util.Optional;

Optional<OverAllState> result = router.invoke(
        UserMessage.builder().text("帮我查一下杭州的天气").build(),
        RunnableConfig.builder().threadId("user-1").build()
);

result.ifPresent(state -> {
    // 读取被路由到的子 Agent 的最终回复(messages 最后一条)
    List<Message> messages = (List<Message>) state.value("messages").orElse(List.of());
    if (!messages.isEmpty()) {
        System.out.println("路由结果:" + messages.get(messages.size() - 1).getText());
    }
});

适用场景:一个入口、多个专业 Agent,按意图分发(类似「智能客服分流」)。

注意类名是 LlmRoutingAgent(LLM 驱动的路由),不要与不带前缀的名称混淆。

7.5 LoopAgent(循环执行)

重复执行某个子 Agent,直到 LoopStrategy 判定满足退出条件:

import com.alibaba.cloud.ai.graph.agent.flow.agent.LoopAgent;

import java.util.List;

LoopAgent loop = LoopAgent.builder()
        .name("loop")
        .subAgents(List.of(subAgent))
        .loopStrategy(new CountLoopStrategy(5))   // 例如:固定循环 5 次
        .build();

执行并获取循环结果:

import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.RunnableConfig;
import [email protected];
import [email protected];

import java.util.List;
import java.util.Optional;

Optional<OverAllState> result = loop.invoke(
        UserMessage.builder().text("帮我优化这份方案").build(),
        RunnableConfig.builder().threadId("user-1").build()
);

result.ifPresent(state -> {
    // 循环结束后,读取最终的 messages(或子 Agent 的 outputKey)
    List<Message> messages = (List<Message>) state.value("messages").orElse(List.of());
    if (!messages.isEmpty()) {
        System.out.println("循环结果:" + messages.get(messages.size() - 1).getText());
    }
});

适用场景:需要反复迭代直至收敛的任务(如「反复优化方案」)。

7.6 Supervisor(监督者)模式

Supervisor(监督者) 模式:一个「监督者」LLM 节点负责把任务路由给多个「执行者(worker)」, 每个 worker 完成自己的任务后回到监督者,监督者再决定下一步或结束(FINISH)。它适合复杂、动态的任务分解场景。

注意:与前面四种不同,Supervisor 没有专用的类(不像 SequentialAgent),而是用 Graph(StateGraph) 手工编排:一个 supervisor 节点 + 若干 worker 节点 + 条件路由循环。官方示例(MultiAgentSupervisorExample)即如此。

import com.alibaba.cloud.ai.graph.CompiledGraph;
import com.alibaba.cloud.ai.graph.KeyStrategyFactory;
import com.alibaba.cloud.ai.graph.StateGraph;
import com.alibaba.cloud.ai.graph.state.strategy.AppendStrategy;
import com.alibaba.cloud.ai.graph.state.strategy.ReplaceStrategy;

import static com.alibaba.cloud.ai.graph.StateGraph.END;
import static com.alibaba.cloud.ai.graph.StateGraph.START;
import static com.alibaba.cloud.ai.graph.action.AsyncEdgeAction.edge_async;
import static com.alibaba.cloud.ai.graph.action.AsyncNodeAction.node_async;

import java.util.Map;

// 1) 状态策略:messages 追加、next 覆盖
KeyStrategyFactory keyStrategyFactory = () -> Map.of(
        "messages", new AppendStrategy(),
        "next", new ReplaceStrategy()
);

// 2) 节点:supervisor 负责路由,researcher/coder 负责干活
String[] members = {"researcher", "coder"};
SupervisorNode supervisor = new SupervisorNode(ch@tModel, members);
ResearcherNode researcher = new ResearcherNode(ch@tModelWithTool);
CoderNode coder = new CoderNode(ch@tModelWithTool);

// 3) 构图:supervisor 条件路由到 worker,worker 干完回到 supervisor
StateGraph workflow = new StateGraph(keyStrategyFactory)
        .addNode("supervisor", node_async(supervisor))
        .addNode("researcher", node_async(researcher))
        .addNode("coder", node_async(coder))
        .addEdge(START, "supervisor")
        .addConditionalEdges(
                "supervisor",
                edge_async(state -> (String) state.value("next").orElse("FINISH")),
                Map.of("FINISH", END, "researcher", "researcher", "coder", "coder")
        )
        .addEdge("researcher", "supervisor")
        .addEdge("coder", "supervisor");

CompiledGraph graph = workflow.compile();

SupervisorNode 核心逻辑——用 LLM 决定下一步交给哪个 worker:

public static class SupervisorNode implements NodeAction {
    private final ChatClient ch@tClient;
    private final String[] members;

    public SupervisorNode(ChatModel model, String[] members) {
        this.ch@tClient = ChatClient.builder(model).build();
        this.members = members;
    }

    @Override
    public Map<String, Object> apply(OverAllState state) {
        // 读取最后一条消息
        List<Object> messages = (List<Object>) state.value("messages").orElse(List.of());
        String lastText = messages.get(messages.size() - 1).toString();

        // 让 LLM 决定路由
        String membersList = String.join(", ", members);
        String result = [email protected]()
                .system("你是 supervisor,负责在以下 worker 间分配任务:" + membersList
                        + "。只返回 worker 名称或 FINISH。")
                .user("用户消息:" + lastText)
                .call()
                .content();

        // 归一化:只保留 worker 名称或 FINISH
        return Map.of("next", normalizeRoute(result, members));
    }

    private String normalizeRoute(String result, String[] members) {
        if (result == null) return "FINISH";
        String r = result.trim().toLowerCase();
        if (r.contains("finish")) return "FINISH";
        for (String m : members) {
            if (r.equals(m.toLowerCase()) || r.contains(m.toLowerCase())) return m;
        }
        return members.length > 0 ? members[0] : "FINISH";
    }
}

执行后:supervisor 通过 state.value("next") 决定流转,worker 干完 addEdge 回 supervisor 循环, 直到 supervisor 返回 FINISH 才路由到 END;最终 messages 里是 supervisor 与各 worker 的完整对话记录。

7.7 智能体作为工具(Agent Tool)

多智能体协作的另一种方式是「把一个 Agent 包装成工具,供另一个 Agent 调用」。Spring AI Alibaba 提供了 AgentTool.create(...),把子 Agent 封装成 ToolCallback——工具名取子 Agent 的 name、描述取子 Agent 的 description,上层 Agent 通过 Function Calling 触发,被调用时内部运行子 Agent 并返回其最终回复。

import com.alibaba.cloud.ai.graph.agent.AgentTool;
import com.alibaba.cloud.ai.graph.agent.ReactAgent;
import [email protected];
import org.springframework.ai.tool.ToolCallback;

// 1) 定义子 Agent(天气专家)
ReactAgent weatherAgent = ReactAgent.builder()
        .name("weather_agent")
        .description("查询指定城市的天气")   // 会作为工具的 description
        .model(ch@tModel)
        .build();

// 2) 把子 Agent 包装成工具
ToolCallback weatherAsTool = AgentTool.create(weatherAgent);

// 3) 上层 Agent 挂载这个「Agent 工具」
ReactAgent orchestrator = ReactAgent.builder()
        .name("orchestrator")
        .model(ch@tModel)
        .tools(weatherAsTool)
        .build();

// 4) 上层 Agent 遇到天气问题会调用 weather_agent 这个工具
AssistantMessage reply = orchestrator.call("北京今天天气怎么样?");
System.out.println(reply.getText());

这与「把 Agent 作为子图节点嵌入」一脉相承:ReactAgent.asNode(...) 可把 Agent 转成图节点, StateGraph.addNode(id, stateGraph) 也支持子图嵌入。AgentTool 则是「子 Agent 即工具」的封装, 内部用 MethodToolCallback 反射调用 executeAgent(String, ToolContext),把输入传给子 Agent 并返回其 AssistantMessage

小结:SequentialAgent / ParallelAgent / LlmRoutingAgent / LoopAgent 是框架内置的高层编排; 「Agent 作为工具/节点」则是更灵活的组装方式,本质都依赖 Graph 的子图能力。

7.8 自定义 Agent 上下文(Context Engineering)

Multi-agent 设计的核心是上下文工程——决定每个 Agent 看到什么信息。系统的质量在很大程度上取决于此: 既要让每个 Agent 拿到执行任务所需的正确数据,又要避免无关信息淹没它。

① 传递哪些部分:includeContents —— 决定子 Agent(作为子图节点)是否继承父图的对话历史(messages):

ReactAgent child = ReactAgent.builder()
        .name("child")
        .model(ch@tModel)
        .includeContents(false)   // 不继承父图 messages,只处理自己的输入
        .build();
  • includeContents(true)(默认):子 Agent 拿到父图的完整 messages
  • includeContents(false):子 Agent 只拿其他状态字段,不继承对话历史——适合「独立子任务」场景。

② 包含 / 排除中间推理:returnReasoningContents —— 决定子 Agent 完成后,向上返回「全部消息」还是「只返回最终回复」:

ReactAgent child = ReactAgent.builder()
        .name("child")
        .model(ch@tModel)
        .returnReasoningContents(false)   // 默认:只返回最终回复,隐藏中间推理
        .build();
  • returnReasoningContents(false)(默认):只返回最后一条 AssistantMessage(最终答案);
  • returnReasoningContents(true):返回全部消息,包含中间思考 / 工具调用过程。

③ 定制提示:instruction / systemPrompt —— 为每个子 Agent 定制专门的提示(详见 3.2):

ReactAgent writer = ReactAgent.builder()
        .name("writer")
        .model(ch@tModel)
        .systemPrompt("你是一位资深技术写作者")     // 固定人设(system 角色)
        .instruction("写一篇关于 {input} 的文章")    // 任务模板(带占位符)
        .build();

④ 自定义输入 / 输出格式:inputSchema / outputSchemainputType / outputType —— 约束每个 Agent 的输入输出结构(详见 3.5):

ReactAgent extractor = ReactAgent.builder()
        .name("extractor")
        .model(ch@tModel)
        .outputType(ContactInfo.class)     // 输出约束为 ContactInfo 结构
        .build();

相关机制:outputKey 决定子 Agent 把结果写回 state 的哪个键;instruction 里的 {占位符} 决定子 Agent 拿到上游的哪些 outputKey(见 7.2);description 用于路由选择(见 7.4)。

7.9 自定义工作流:FlowAgent

除了内置的四种编排,Spring AI Alibaba 还提供了 FlowAgent 抽象类,允许你创建自定义的 Agent 工作流模式。 通过继承 FlowAgent 并实现 buildSpecificGraph(...),你可以实现任意复杂的多 Agent 协作模式:

import com.alibaba.cloud.ai.graph.CompileConfig;
import com.alibaba.cloud.ai.graph.StateGraph;
import com.alibaba.cloud.ai.graph.agent.Agent;
import com.alibaba.cloud.ai.graph.agent.flow.agent.FlowAgent;
import com.alibaba.cloud.ai.graph.agent.flow.builder.FlowGraphBuilder;
import com.alibaba.cloud.ai.graph.exception.GraphStateException;

import java.util.List;

public class MyCustomWorkflow extends FlowAgent {

    protected MyCustomWorkflow(String name, String description,
            CompileConfig compileConfig, List<Agent> subAgents) {
        super(name, description, compileConfig, subAgents);
    }

    @Override
    protected StateGraph buildSpecificGraph(FlowGraphBuilder.FlowGraphConfig config) throws GraphStateException {
        // 自定义图结构:addNode / addEdge 定义任意多 Agent 协作
        StateGraph graph = new StateGraph();
        // ...
        return graph;
    }
}

FlowAgentSequentialAgent / ParallelAgent / LlmRoutingAgent / LoopAgent 的共同父类, 它们内部都是「继承 FlowAgent + 实现 buildSpecificGraph」实现的;你可用同样的方式扩展自己的编排模式 (完整落地还需配套一个继承 FlowAgentBuilder 的自定义 Builder)。


第 8 章 Workflow 与 Graph

8.1 定位:把工作流建模为图

如第 0 章所述,Graph 是 Agent Framework 的底层运行时,也是一个低级的工作流与多智能体编排框架。 它把「智能体工作流」抽象成一张有向图(DAG)

  • 节点(Node):一个具体的操作(一次模型调用、一段业务逻辑、一个子图);
  • 边(Edge):节点之间的流转;
  • 状态(State):在整张图中流动的共享数据。

核心类(com.alibaba.cloud.ai.graph 包):

职责
StateGraph定义节点与边的工作流主类
OverAllState全局状态,承载流转中的共享数据
CompiledGraphStateGraph 编译后的可执行形态
KeyStrategy状态合并策略(覆盖 / 追加)

8.2 StateGraph 与状态

创建图时,可以为每个状态 key 指定合并策略ReplaceStrategy(覆盖)或 AppendStrategy(追加)。

import com.alibaba.cloud.ai.graph.KeyStrategy;
import com.alibaba.cloud.ai.graph.KeyStrategyFactory;
import com.alibaba.cloud.ai.graph.StateGraph;
import com.alibaba.cloud.ai.graph.state.strategy.AppendStrategy;
import com.alibaba.cloud.ai.graph.state.strategy.ReplaceStrategy;

import java.util.Map;

KeyStrategyFactory keyStrategy = () -> Map.of(
        "result", new ReplaceStrategy(),   // 每次覆盖
        "logs",   new AppendStrategy()     // 每次追加
);

StateGraph graph = new StateGraph(keyStrategy);

OverAllState 提供 value(key)(返回 Optional)与 data()(返回整个 Map)来读写状态。

8.3 节点(Node)

addNode(id, action) 添加节点。节点动作可以是同步或异步,借助 node_async 工厂方法简化书写:

import static com.alibaba.cloud.ai.graph.action.AsyncNodeActionWithConfig.node_async;

graph.addNode("step_a", node_async(state -> Map.of("result", "A 完成")));
graph.addNode("step_b", node_async(state -> Map.of("result", "B 完成")));

节点的返回值会被合并进全局状态(按 key 的合并策略)。节点也可以嵌入子图: addNode(id, stateGraph)addNode(id, compiledGraph)

8.4 边(Edge)与条件边

普通边表示固定流转,条件边则根据当前状态动态决定去向:

import static com.alibaba.cloud.ai.graph.StateGraph.START;
import static com.alibaba.cloud.ai.graph.StateGraph.END;
import static com.alibaba.cloud.ai.graph.action.AsyncEdgeAction.edge_async;

// 普通边
graph.addEdge(START, "step_a");
graph.addEdge("step_a", "step_b");
graph.addEdge("step_b", END);

// 条件边:EdgeAction 返回目标节点名
graph.addConditionalEdges(
        "step_b",
        edge_async(state -> {
            String flag = state.value("result").orElse("").toString();
            return flag.contains("A") ? "step_a" : END;
        }),
        Map.of("step_a", "step_a", END, END)
);

START / ENDStateGraph 提供的常量,表示图的入口与出口。

8.5 编译与执行

StateGraph 需要编译成 CompiledGraph 才能执行:

import com.alibaba.cloud.ai.graph.CompiledGraph;
import com.alibaba.cloud.ai.graph.OverAllState;

import java.util.Optional;

CompiledGraph compiled = graph.compile();   // 默认使用 MemorySaver

Optional<OverAllState> output = compiled.invoke(Map.of("input", "hello"));
  • compile():用默认配置(内存存档器)编译;
  • compile(CompileConfig):自定义编译配置(存档器、中断点等);
  • invoke(...):同步执行;另有 stream(...) 支持流式返回。

8.6 持久化、中断与恢复

在第 6 章的基础上,Graph 层的中断/恢复能力更显式。通过 CompileConfig 指定存档器与中断点:

import com.alibaba.cloud.ai.graph.CompileConfig;
import com.alibaba.cloud.ai.graph.checkpoint.config.SaverConfig;
import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver;

CompiledGraph compiled = graph.compile(
        CompileConfig.builder()
                .saverConfig(SaverConfig.builder().register(new MemorySaver()).build())
                .interruptAfter("review")    // 在 review 节点执行后中断
                .build()
);

配合检查点,可以在中断后查询、修改状态,并从断点恢复执行:

  • compiled.getState(RunnableConfig):查询会话当前状态;
  • compiled.updateState(RunnableConfig, values):修改状态;
  • 恢复执行:以相同 threadId 从最近的检查点继续。

这就是第 5 章「人工介入」(5.7)在 Graph 层的机制基础。

8.7 并行聚合

当一个节点通过并行条件边addParallelConditionalEdges)路由到多个分支时,需要约定「何时算完成」。 框架提供两种聚合策略:

  • AllOf:所有并行分支都成功才算完成;
  • AnyOf:任一分支成功即可继续。

这让「多分支并行、结果汇聚」的逻辑从业务代码下沉为图 DSL 的内置能力。

8.8 可视化导出

StateGraph 支持把图导出为 PlantUMLMermaid 图,便于分享与排查:

import com.alibaba.cloud.ai.graph.GraphRepresentation;

GraphRepresentation mermaid = graph.getGraph(GraphRepresentation.Type.MERMAID, "我的工作流");
System.out.println(mermaid.content());

把导出的 Mermaid/PlantUML 代码粘贴到对应渲染器,即可得到工作流的可视化流程图。


第 9 章 Plan-and-Execute 实战

现在用 StateGraph 落地第 1 章的 Plan-and-Execute(计划-执行) 范式:先规划、再逐步执行。

import com.alibaba.cloud.ai.graph.*;
import com.alibaba.cloud.ai.graph.state.strategy.ReplaceStrategy;

import static com.alibaba.cloud.ai.graph.StateGraph.START;
import static com.alibaba.cloud.ai.graph.StateGraph.END;
import static com.alibaba.cloud.ai.graph.action.AsyncNodeActionWithConfig.node_async;
import static com.alibaba.cloud.ai.graph.action.AsyncEdgeAction.edge_async;

import java.util.List;
import java.util.Map;

// 1. 状态策略
KeyStrategyFactory keyStrategy = () -> Map.of(
        "plan", new ReplaceStrategy(),
        "stepIndex", new ReplaceStrategy()
);

StateGraph graph = new StateGraph(keyStrategy);

// 2. 规划节点:把任务拆成步骤清单
graph.addNode("planner", node_async(state -> {
    String task = state.value("task").orElse("").toString();
    List<String> plan = List.of("步骤1:收集资料", "步骤2:撰写初稿", "步骤3:校对定稿");
    return Map.of("plan", plan, "stepIndex", 0);
}));

// 3. 执行节点:执行当前步骤
graph.addNode("executor", node_async(state -> {
    List<String> plan = (List<String>) state.value("plan").orElse(List.of());
    int index = (Integer) state.value("stepIndex").orElse(0);
    System.out.println("执行:" + plan.get(index));
    return Map.of("stepIndex", index + 1);
}));

// 4. 连线:START -> planner -> executor,executor 根据进度决定继续或结束
graph.addEdge(START, "planner");
graph.addEdge("planner", "executor");
graph.addConditionalEdges(
        "executor",
        edge_async(state -> {
            List<String> plan = (List<String>) state.value("plan").orElse(List.of());
            int index = (Integer) state.value("stepIndex").orElse(0);
            return index < plan.size() ? "executor" : END;
        }),
        Map.of("executor", "executor", END, END)
);

// 5. 编译并执行
CompiledGraph compiled = graph.compile();
compiled.invoke(
        Map.of("task", "写一篇 Spring AI Alibaba 教程"),
        RunnableConfig.builder().threadId("plan-execute-1").build()
);

执行输出:

执行:步骤1:收集资料
执行:步骤2:撰写初稿
执行:步骤3:校对定稿

对照第 1 章的概念:

  • planner 节点 = Plan(规划)
  • executor 节点 = Execute(执行)
  • addConditionalEdgesedge_async = 判断「是否还有未执行步骤」的路由逻辑(循环控制)。

若要加入 Reflect(反思),可在 executor 之后再接一个 reflector 节点,评估执行结果, 用条件边决定「返回 executor 修正」还是「进入 END」。这正是 Plan-Act-Reflect 的图实现。

对比:ReactAgent 适合「边走边想」的交互式任务;而用 StateGraph 编排的 Plan-and-Execute 更适合步骤清晰、需要整体规划的复杂任务。两者底层都是同一套图机制。


第四部分:进阶与调试

第 10 章 分布式智能体(A2A Agent)

A2A(Agent-to-Agent) 是 Google 提出的智能体间通信协议。Spring AI Alibaba 对其提供了支持, 用于构建分布式多智能体场景:不同的 Agent 部署在不同的服务/节点上,通过注册中心互相发现、互相调用。

其落地方案是 Nacos 作为注册中心,对应 starter:

<dependency>
    <groupId>com.alibaba.cloud.ai</groupId>
    <artifactId>spring-ai-alibaba-starter-a2a-nacos</artifactId>
</dependency>

工作方式大致为:

  1. 各 Agent 服务启动后向 Nacos 注册自己;
  2. 需要协作时,Agent 通过 Nacos 发现目标 Agent 的地址;
  3. 通过 A2A 协议发起跨服务调用。

本文对 A2A 仅作概念介绍。它适合把 Agent 拆成独立部署的微服务、跨团队/跨系统协作的场景; 若你暂时不需要分布式,可先跳过本章。


第 11 章 spring-ai-alibaba-studio 调试

11.1 Studio 是什么

spring‑ai‑alibaba‑studio 是 Spring AI Alibaba 配套嵌入式 Web 可视化调试工具,用于本地开发阶段调试 LLM ChatBot、Agent、Graph 工作流与 RAG 应用。可直接嵌入 Spring Boot 服务,无需单独部署前端。该工具定位为开发调试工具,和面向生产运维的 spring‑ai‑alibaba‑admin 有所区分;Studio 持久化能力较弱,不建议暴露到公网生产环境。支持 Agent 多轮对话、链路追踪、模型参数可视化调参、Graph 工作流可视化、RAG 检索调试等功能。

11.2 引入依赖

pom.xml 中加入(版本与框架保持一致):

<dependency>
    <groupId>com.alibaba.cloud.ai</groupId>
    <artifactId>spring-ai-alibaba-studio</artifactId>
    <version>1.1.2.2</version>
</dependency>

11.3 嵌入式模式(推荐)

引入依赖后,直接启动你的 Spring Boot 应用,然后浏览器访问:

http://localhost:{your-port}/ch@tui/index.html

即可在页面中选择并调试你的 Agent,进行多轮对话、查看工具调用链路。

11.4 独立模式

也可以把 Studio 的前端(agent-ch@t-ui)单独跑起来,对接后端:

git clone https://github.com/alibaba/spring-ai-alibaba.git
cd spring-ai-alibaba/spring-ai-alibaba-studio/agent-ch@t-ui

pnpm install   # 或 npm install
pnpm dev       # 或 npm run dev

前端默认运行在 http://localhost:3000,在 .env.development 中配置后端地址与目标 Agent:

NEXT_PUBLIC_API_URL=http://localhost:8080
NEXT_PUBLIC_APP_NAME=research_agent
NEXT_PUBLIC_USER_ID=user-001

11.5 开启 DEBUG 日志

调试时打开日志,可观察完整的 tool_calls 交互过程(模型是否发起调用、参数是否符合 Schema、工具是否执行):

logging:
  level:
    org.springframework.ai: DEBUG
    com.alibaba.cloud.ai: DEBUG

11.6 常见调试场景

  • Agent 对话:在 Chat UI 里直接对话,观察多轮上下文是否正确;
  • 链路追踪:查看每一步的思考、工具调用入参与返回,定位「为什么调用了错误的工具」;
  • Graph 可视化:直观查看分支路由、并行节点、中断与恢复;
  • 参数调参:可视化调整 temperaturetopPmaxTokens 等,无需改代码重启。

11.7 常见问题:UI 列表为空

如果你的 Spring Boot 项目没有把 ReactAgent / CompiledGraph 注册暴露给 Studio,UI 的列表会是空的。

Agent / Graph 没有注册为 Spring Bean(最高频)

错误写法:在 main 方法内部 new 出来的 ReactAgent,不是 Spring Bean,Studio 扫描不到:

// main 里直接 new,不会被 Spring 容器管理,Studio 无法发现
ReactAgent agent = ReactAgent.builder()
        .model(ch@tModel)
        .tools(weatherTool)
        .build();

正确写法:用 @Bean 注入到 Spring 上下文:

@Bean
public ReactAgent reactAgent(ChatModel ch@tModel) {
    return ReactAgent.builder()
            .model(ch@tModel)
            .tools(weatherTool())
            .build();
}

CompiledGraph 同理,也要 return@Bean,Studio 才会识别 Graph。


附录:核心 API 速查

本附录集中列出贯穿全文的三个核心数据类的常用方法(版本 1.1.2.2)。

A.1 RunnableConfig(运行时配置)

com.alibaba.cloud.ai.graph.RunnableConfig —— 运行时配置,通过 RunnableConfig.builder()...build() 创建, 传给 agent.call(...) / graph.invoke(...),控制执行(会话 ID、检查点、元数据等)。

分类方法说明
会话threadId() / Builder threadId(String)获取 / 设置会话 ID(短期记忆的关键)
检查点checkPointId() / Builder checkPointId(String)获取 / 设置检查点 ID
恢复nextNode() / Builder nextNode(String)从指定节点恢复
元数据metadata() / metadata(String key)读取元数据 Map / 指定 key(返回 Optional
元数据Builder addMetadata(key, value)添加自定义元数据(工具 / 提示可读)
上下文context() / clearContext()读取 / 清空上下文
中断Builder addHumanFeedback(InterruptionMetadata)提交人工反馈(HITL)
中断Builder resume()恢复执行
其他Builder mergeReasoningContent(boolean)是否合并思考内容
其他Builder addStateUpdate(map)追加状态更新

A.2 OverAllState(图状态)

com.alibaba.cloud.ai.graph.OverAllState —— 图状态,节点间流转的共享数据容器。

分类方法说明
读取value(key)返回 Optional<T>
读取value(key, Class<T>)按类型读取
读取value(key, default)带默认值读取
读取data()返回整个状态 Map<String, Object>
写入updateState(map)合并更新状态
写入updateStateWithKeyStrategies(map, strategies)按合并策略更新
写入registerKeyAndStrategy(key, strategy)注册 key 的合并策略
其他keyStrategies() / input(map) / getStore()合并策略 / 输入 / 存储
其他clear() / reset() / snapShot() / cover(other)清理 / 重置 / 快照 / 覆盖
常量DEFAULT_INPUT_KEY / MARK_FOR_REMOVAL默认输入键 / 删除标记

A.3 ToolContext(工具上下文)

[email protected] —— 工具上下文,工具方法通过它访问状态与历史。

方法说明
getContext()返回上下文 Map<String, Object>
getToolCallHistory()返回工具调用历史 List<Message>
常量 TOOL_CALL_HISTORY历史记录的 key

配套工具类 ToolContextHelpercom.alibaba.cloud.ai.graph.agent.tools)提供更便捷的访问: getConfig(toolContext)Optional<RunnableConfig>getState(toolContext)Optional<OverAllState>getMetadata(toolContext, key, type)Optional<T>(详见第 6 章 6.5)。

相关文章

精彩推荐