大模型应用做到一定复杂度后,问题会从“怎么调用模型”变成“怎么组织模型完成任务”。一次简单问答只需要拼提示词;一个业务流程可能需要多个步骤;更复杂的任务还需要模型自己判断要调用哪些工具、什么时候终止、不同智能体之间怎样传递上下文。
Spring AI Alibaba 的价值就在这里:它把常见的 Agent 模式抽成了可复用的框架能力,尤其是 ReAct Agent、Graph 编排和 Multi-Agent 组合。对于 Java 技术栈来说,这能少写不少基础设施代码,把精力更多放在工具设计、上下文管理和业务流程上。
从组件、工作流到自主 Agent
大模型应用通常会经历三个阶段。
| 阶段 | 核心问题 | 常见形态 | 局限 |
|---|---|---|---|
| 组件阶段 | 告诉模型“能做什么” | 问答、摘要、分类、代码生成 | 单次调用为主,缺少任务过程管理 |
| 工作流阶段 | 告诉模型“该怎么做” | 固定步骤串联,例如检索、分析、生成、审核 | 流程固定,中间结果异常时不够灵活 |
| 自主 Agent 阶段 | 告诉模型“目标是什么” | ReAct、Planning、Multi-Agent 协作 | 需要处理工具、记忆、循环、上下文和状态 |
工作流阶段通常长这样:
flowchart LR
A[用户输入] --> B[步骤1: 解析需求]
B --> C[步骤2: 查询数据]
C --> D[步骤3: 生成结果]
D --> E[步骤4: 审核或格式化]
E --> F[返回答案]
这种模式适合流程确定的业务,比如审批、订单处理、内容发布链路。它的问题也很明显:每个节点都由开发者提前定义,模型无法根据中间结果自主改变路线。
自主 Agent 则把更多决策权交给模型。一个常见定义是:
Agent = LLM + Planning + Memory + Tool
其中:
- LLM(Large Language Model,大语言模型)负责理解、推理和生成。
- Planning 负责拆解任务、制定步骤。
- Memory 负责保存短期会话状态或长期用户偏好。
- Tool 负责和外部环境交互,例如查天气、查数据库、访问业务接口。
当一个 Agent 无法覆盖所有能力时,就会进入 Multi-Agent(多智能体)协作:翻译 Agent、天气 Agent、交通 Agent、总结 Agent 分别处理自己擅长的部分,再由编排层把它们组织起来。
ReAct:让 Agent 在“思考”和“行动”之间循环
ReAct 来自 Reasoning + Acting,它把 Agent 的执行过程拆成三个动作:
- Reasoning:分析当前状态,判断下一步应该做什么。
- Acting:调用工具或直接生成回答。
- Observing:读取工具结果,把结果加入上下文,再进入下一轮判断。
流程可以表示为:
flowchart TD
A[接收用户任务] --> B[Reasoning: 分析状态和目标]
B --> C{是否需要调用工具}
C -- 否 --> D[生成最终回答]
C -- 是 --> E[Acting: 调用工具]
E --> F[Observing: 获取工具结果]
F --> G{任务是否完成}
G -- 是 --> D
G -- 否 --> B
手写一个最小化 ReAct Agent,大致会包含这样的主循环:
public abstract class AbstractReactAgent extends AbstractAgent {
private final int maxLoopCount = 5;
@Override
public Result execute(Context context) {
for (int loop = 0; loop < maxLoopCount; loop++) {
ReasoningResult reasoningResult = reasoning(context);
if (!reasoningResult.hasValidOutput()) {
continue;
}
Result actResult = act(context, reasoningResult);
if (actResult.isCompleted()) {
return actResult;
}
updateMemory(context, actResult);
}
return Result.failed("Agent reached max loop count");
}
private ReasoningResult reasoning(Context context) {
Prompt prompt = buildPrompt(context);
LlmResponse response = callModel(prompt, getAvailableTools());
return ReasoningResult.builder()
.text(response.getText())
.toolCalls(response.getToolCalls())
.build();
}
private Result act(Context context, ReasoningResult reasoningResult) {
if (reasoningResult.getToolCalls().isEmpty()) {
return Result.completed(reasoningResult.getText());
}
try {
List<ToolResult> toolResults = executeTools(reasoningResult.getToolCalls());
return Result.inProgress(toolResults);
}
catch (Exception ex) {
return Result.failed(ex.getMessage());
}
}
protected abstract Prompt buildPrompt(Context context);
protected abstract void updateMemory(Context context, Result result);
}
这个代码能跑通基本思路,但真正放到业务系统里,很快会遇到一组工程问题:
| 问题 | 具体表现 |
|---|---|
| 消息历史管理 | 哪些消息要保留,哪些要压缩,工具结果是否进入上下文 |
| 工具注册 | 工具描述、入参 Schema、异常处理、权限控制都要统一 |
| RAG 集成 | RAG(Retrieval-Augmented Generation,检索增强生成)需要切分、向量化、检索和注入上下文 |
| 循环控制 | 模型可能重复调用同一个工具,也可能迟迟不给最终答案 |
| 错误恢复 | 工具失败后是重试、跳过、降级,还是直接终止 |
| 扩展点 | 日志、审计、监控、内容安全、提示词动态修改不能侵入主链路 |
这些问题不难理解,但每一个都需要代码和规范。框架的价值不是让 ReAct 变得神秘,而是把这些重复基础设施沉淀下来。
Spring AI Alibaba 的 ReactAgent
Spring AI Alibaba 提供了开箱即用的 ReactAgent,核心配置包括模型、系统提示词、工具、记忆、拦截器和钩子。
ReactAgent reactAgent = ReactAgent.builder()
.name("weather_agent")
.model(chatModel)
.systemPrompt("""
你是一个天气助手。
当用户询问天气时,优先使用天气查询工具获取实时结果。
""")
.tools(List.of(weatherTool))
.interceptors(List.of(modelLimitInterceptor))
.hooks(List.of(loggingHook))
.saver(new MemorySaver())
.outputType(WeatherAnswer.class)
.build();
AssistantMessage response = reactAgent.call("今天苏州天气怎么样?");
String answer = response.getText();
几个核心概念需要分清。
Model:统一模型调用接口
ReactAgent 接收的是 Spring AI 的 ChatModel。业务代码面向统一接口编程,底层模型可以替换成不同厂商或不同部署方式,只要适配 ChatModel 即可。
ReactAgent agent = ReactAgent.builder()
.name("assistant")
.model(chatModel)
.systemPrompt("你是一个通用助手")
.build();
这种设计降低了模型切换成本,尤其适合需要同时接入多个模型或后续保留替换空间的系统。
Tools:用 ToolCallback 封装外部能力
Agent 的行动能力来自工具。Spring AI 使用 ToolCallback 描述工具,通常会配合函数调用(Function Calling)让模型生成结构化参数。
ToolCallback weatherTool = FunctionToolCallback
.builder("queryWeather", new WeatherTool())
.description("查询指定城市的实时天气")
.inputType(WeatherRequest.class)
.build();
工具设计时要把描述写清楚,尤其是:
- 工具适合解决什么问题。
- 入参字段含义。
- 什么时候不应该调用。
- 返回结果是什么格式。
- 出错时会返回什么信息。
模型选择工具时高度依赖这些描述。描述含糊,Agent 就容易误调用。
Memory:短期记忆和长期记忆分开
记忆管理可以拆成两类:
| 类型 | 作用 | 常见存储 |
|---|---|---|
| 短期记忆 | 保存一次会话内的上下文,例如多轮对话和工具执行结果 | MemorySaver、RedisSaver、MongoSaver |
| 长期记忆 | 跨会话保存用户偏好、历史事实或业务状态 | RedisStore、MongoStore、自定义 Store |
短期记忆更接近 Checkpoint,用来恢复和延续一次执行过程;长期记忆更像可检索的知识或状态,需要考虑权限、过期、更新和冲突。
RAG:让 Agent 动态补充知识
Spring AI Alibaba 提供了文本切分、向量化、向量存储、检索等 RAG 能力。Agent 可以把检索封装成工具,也可以在模型调用前通过 Hook 或 Interceptor 注入上下文。
一个常见流程是:
flowchart LR
A[用户问题] --> B[生成检索查询]
B --> C[(向量数据库)]
C --> D[召回相关片段]
D --> E[重排和过滤]
E --> F[注入 Agent 上下文]
F --> G[模型生成答案]
RAG 不只是“查知识库”。在 Agent 场景里,它还可以用来查文件、查工具执行历史、查用户长期记忆,甚至查某个业务对象的最新状态。
Hooks 和 Interceptors:把横切逻辑放到扩展点
Agent 执行过程中有很多横切需求,例如:
- 模型调用前修改提示词。
- 工具调用前做权限检查。
- 模型调用后记录日志。
- 消息过长时做摘要压缩。
- 限制模型调用次数,避免无限循环。
- 限制工具调用次数,控制成本和风险。
- 对输入输出做内容审核。
这些逻辑如果都写进 Agent 主流程,代码会很快变乱。Hooks 和 Interceptors 的作用就是把这些扩展放到框架预留位置。
flowchart TD
A[用户输入] --> B[Agent Hook: 执行前]
B --> C[Model Interceptor: 调用前]
C --> D[ChatModel]
D --> E[Model Interceptor: 调用后]
E --> F{是否调用工具}
F -- 是 --> G[Tool Interceptor: 调用前]
G --> H[Tool]
H --> I[Tool Interceptor: 调用后]
I --> C
F -- 否 --> J[Agent Hook: 执行后]
J --> K[返回结果]
这类扩展机制对于上下文工程很关键。单纯调提示词只能解决一部分问题,复杂 Agent 更需要动态检索、上下文裁剪、状态传递、调用限制和结果规范化。
Spring AI Alibaba 的分层结构
Spring AI Alibaba 的 Agent 能力可以理解成三层:
flowchart TD
A[Spring AI: 模型、工具、消息、RAG 基础接口]
B[Graph: 节点、边、状态、Checkpoint]
C[Agent Framework: ReactAgent、FlowAgent、Multi-Agent]
A --> B
B --> C
Graph 是轻量级工作流编排层。它用节点和边描述执行过程,用 OverAllState 在节点之间传递数据。
以 ReAct Agent 为例,框架内部可以看成一张图:
flowchart LR
S[__START__] --> R[Reasoning Node]
R --> C{是否需要工具}
C -- 是 --> T[Tool Node]
T --> O[Observation / State Update]
O --> R
C -- 否 --> E[__END__]
在更复杂的 Multi-Agent 场景里,Graph 的意义会更明显:每个 Agent 可以是一个节点,普通 Java 逻辑也可以是一个节点,不同节点通过状态对象共享必要信息。
Multi-Agent 的几种常见模式
Spring AI Alibaba 支持两类常见 Multi-Agent 组织方式。
| 模式 | 工作方式 | 适合场景 |
|---|---|---|
| Tool Calling | Supervisor Agent 把其他 Agent 当作工具调用 | 一个总控智能体需要动态选择专家能力 |
| Handoffs | 当前 Agent 或编排层把控制权交给另一个 Agent | 流程明确,或者需要顺序、并行、路由执行 |
| Sequential Agent | 多个 Agent 按顺序执行 | 翻译后总结、检索后生成、审核后发布 |
| Parallel Agent | 多个 Agent 并行执行后合并结果 | 多路分析、多源检索、多角色评审 |
| LLM Routing Agent | 由模型决定请求路由到哪个 Agent | 用户意图不固定,需要动态分发 |
顺序执行:SequentialAgent
顺序执行适合强依赖链路。后一个 Agent 可以使用前一个 Agent 的结果。
flowchart LR
A[用户输入] --> B[Agent 1]
B --> C[Agent 2]
C --> D[Agent 3]
D --> E[最终结果]
示例代码:
SequentialAgent sequentialAgent = SequentialAgent.builder()
.name("travel_sequential_agent")
.description("按顺序完成翻译、天气查询和出行建议")
.subAgents(List.of(translationAgent, weatherAgent, trafficAgent))
.build();
Optional<OverAllState> result = sequentialAgent.invoke("帮我规划苏州一日游");
并行执行:ParallelAgent
并行执行适合互不依赖的子任务。多个 Agent 同时执行,再由合并策略汇总。
flowchart TD
A[用户输入] --> B[Agent 1]
A --> C[Agent 2]
A --> D[Agent 3]
B --> E[Merge Strategy]
C --> E
D --> E
E --> F[最终结果]
示例代码:
ParallelAgent parallelAgent = ParallelAgent.builder()
.name("analysis_parallel_agent")
.description("并行执行多个角度的分析")
.subAgents(List.of(priceAgent, reviewAgent, riskAgent))
.mergeOutputKey("merged_results")
.mergeStrategy(new ParallelAgent.DefaultMergeStrategy())
.build();
Optional<OverAllState> result = parallelAgent.invoke("分析这个商品是否值得采购");
Object mergedResults = result
.flatMap(state -> state.value("merged_results"))
.orElse(null);
把 Agent 封装成工具
Tool Calling 模式下,可以把一个 ReactAgent 包装成工具,然后交给 Supervisor Agent 调用。
ReactAgent weatherAgent = ReactAgent.builder()
.name("weather_agent")
.model(chatModel)
.systemPrompt("你负责查询天气并给出天气建议")
.tools(List.of(weatherTool))
.build();
ToolCallback weatherAgentTool = AgentTool.from(weatherAgent);
ReactAgent supervisorAgent = ReactAgent.builder()
.name("supervisor_agent")
.model(chatModel)
.systemPrompt("你负责理解用户需求,并选择合适的专家工具完成任务")
.tools(List.of(weatherAgentTool, trafficAgentTool, translationAgentTool))
.build();
这种模式的重点是“选择权在 Supervisor”。如果任务边界不清晰,但专家能力比较明确,Supervisor 可以根据用户请求动态挑选 Agent。
Plan-Execute:先规划,再执行,再汇总
Plan-Execute 是 Multi-Agent 中很常用的架构。它把复杂任务拆成三个阶段:
flowchart LR
A[用户请求] --> B[PlanningAgent: 生成计划]
B --> C[ExecutionNode: 分步骤执行]
C --> D[SummaryAgent: 汇总结果]
D --> E[最终回答]
三个阶段职责不同:
| 阶段 | 组件 | 职责 |
|---|---|---|
| Planning | PlanningAgent + PlanActTool | 根据用户需求生成结构化计划,给每个步骤分配 Agent |
| Execute | ExecutionNode | 读取计划,逐步调用对应的 ReactAgent,并传递上下文 |
| Summary | SummaryAgent | 汇总每个步骤结果,生成面向用户的最终回答 |
这个模式适合目标明确但路径不固定的任务,例如:
- “帮我规划一场去苏州的周末旅行,需要考虑天气、交通和预算。”
- “分析一个商品是否适合上架,需要看价格、评价、库存和风险。”
- “根据多个资料源生成调研报告,并列出引用依据。”
如果业务流程本来就是固定的,Plan-Execute 反而可能增加不确定性。固定流程用顺序编排更可控。
手动编排 Plan-Execute
最直接的实现方式是写一个协调器,把三阶段串起来。
public class PlanningCoordinator {
private final ReactAgentFactory reactAgentFactory;
public Planning execute(Context context) {
Planning planning = generatePlanning(context);
executeSteps(planning, context);
summarize(planning, context);
return planning;
}
private Planning generatePlanning(Context context) {
ReactAgent planningAgent = reactAgentFactory.createReactAgent(
"planning_agent",
List.of(new PlanActTool())
);
String prompt = buildPlanningPrompt(context);
planningAgent.call(new UserMessage(prompt));
return context.getPlanning();
}
private void executeSteps(Planning planning, Context context) {
for (PlanningStep step : planning.getSteps()) {
ReactAgent agent = reactAgentFactory.getAgent(step.getAgentName());
String stepPrompt = buildStepPrompt(step, context);
AssistantMessage response = agent.call(new UserMessage(stepPrompt));
step.setResult(response.getText());
step.markCompleted();
context.appendStepResult(step);
}
}
private void summarize(Planning planning, Context context) {
ReactAgent summaryAgent = reactAgentFactory.getAgent("summary_agent");
String prompt = buildSummaryPrompt(planning, context);
AssistantMessage response = summaryAgent.call(new UserMessage(prompt));
context.setFinalResult(response.getText());
}
}
规划阶段最关键的是让模型输出结构化计划。可以通过工具约束模型生成结果:
public class PlanActTool implements ToolFunctionInterface<PlanActTool.PlanInput, String> {
@Override
public String getDescription() {
return "创建任务执行计划,并为每个步骤指定负责的 Agent";
}
@Override
public String apply(PlanInput input, ToolContext toolContext) {
Planning planning = new Planning();
planning.setTitle(input.getTitle());
for (StepInput stepInput : input.getSteps()) {
PlanningStep step = new PlanningStep();
step.setStepGuide(stepInput.getGuide());
step.setAgentName(stepInput.getAgent());
planning.addStep(step);
}
savePlanning(toolContext, planning);
return "执行计划已创建";
}
public static class PlanInput {
@ToolParam(description = "计划标题")
private String title;
@ToolParam(description = "计划步骤列表")
private List<StepInput> steps;
// getter / setter
}
public static class StepInput {
@ToolParam(description = "步骤任务描述,需要清晰、可执行")
private String guide;
@ToolParam(description = "执行该步骤的 Agent 名称,必须来自可用 Agent 列表")
private String agent;
// getter / setter
}
}
规划提示词需要包含可用 Agent 列表,否则模型不知道该把任务分配给谁。
private String buildPlanningPrompt(Context context, Map<String, ReactAgent> availableAgents) {
StringBuilder agentsInfo = new StringBuilder();
for (Map.Entry<String, ReactAgent> entry : availableAgents.entrySet()) {
agentsInfo.append("- ")
.append(entry.getKey())
.append(": ")
.append(entry.getValue().description())
.append("\n");
}
return """
你是任务规划专家,需要根据用户需求创建执行计划。
用户需求:
%s
可用 Agent:
%s
要求:
1. 将复杂任务拆解为多个可执行步骤;
2. 每个步骤只能选择一个可用 Agent;
3. 步骤之间的依赖关系要合理;
4. 使用 PlanActTool 创建计划。
""".formatted(context.getUserInput(), agentsInfo);
}
手动编排容易理解,也方便调试。缺点是抽象不统一:随着 Multi-Agent 类型增加,每种模式都可能写一套协调器,团队协作时也难形成统一规范。
用 Graph 编排 Plan-Execute
Graph 编排把 Plan-Execute 变成一个标准的 FlowAgent。拓扑结构很简单:
__START__ -> planning -> execution -> summary -> __END__
对应的 DAG(Directed Acyclic Graph,有向无环图)如下:
flowchart LR
S[__START__] --> P[PlanningNode<br/>ReactAgent]
P --> E[ExecutionNode<br/>NodeAction]
E --> M[SummaryNode<br/>ReactAgent]
M --> X[__END__]
类之间的关系可以整理成:
classDiagram
class FlowAgent
class PlanActAgent
class PlanActGraphBuildingStrategy
class StateGraph
class PlanActTool
class ExecutionNode
class ReactAgent
FlowAgent <|-- PlanActAgent
PlanActAgent --> PlanActGraphBuildingStrategy : uses
PlanActGraphBuildingStrategy --> StateGraph : creates
PlanActGraphBuildingStrategy --> ReactAgent : creates planning/summary
PlanActGraphBuildingStrategy --> ExecutionNode : creates
PlanActAgent --> PlanActTool : uses
PlanActAgent:对外暴露统一 Agent 接口
PlanActAgent 继承 FlowAgent,对调用方来说,它就是一个普通 Agent。
public class PlanActAgent extends FlowAgent {
private final ChatModel chatModel;
private final Map<String, ReactAgent> availableAgents;
private final PlanActTool planningTool;
protected PlanActAgent(PlanActAgentBuilder builder) throws GraphStateException {
super(builder.name, builder.description, builder.compileConfig, builder.subAgents);
this.chatModel = builder.chatModel;
this.availableAgents = builder.availableAgents;
this.planningTool = builder.planningTool;
}
public static PlanActAgentBuilder builder() {
return new PlanActAgentBuilder();
}
@Override
protected StateGraph buildSpecificGraph(FlowGraphBuilder.FlowGraphConfig config)
throws GraphStateException {
config.setChatModel(this.chatModel);
config.customProperty("availableAgents", this.availableAgents);
config.customProperty("planningTool", this.planningTool);
return FlowGraphBuilder.buildGraph(
PlanActGraphBuildingStrategy.STRATEGY_TYPE,
config
);
}
public static class PlanActAgentBuilder
extends FlowAgentBuilder<PlanActAgent, PlanActAgentBuilder> {
private ChatModel chatModel;
private Map<String, ReactAgent> availableAgents;
private PlanActTool planningTool;
public PlanActAgentBuilder chatModel(ChatModel chatModel) {
this.chatModel = chatModel;
return this;
}
public PlanActAgentBuilder availableAgents(Map<String, ReactAgent> availableAgents) {
this.availableAgents = availableAgents;
return this;
}
public PlanActAgentBuilder planningTool(PlanActTool planningTool) {
this.planningTool = planningTool;
return this;
}
@Override
protected PlanActAgentBuilder self() {
return this;
}
@Override
protected void validate() {
if (chatModel == null) {
throw new IllegalArgumentException("chatModel must not be null");
}
if (availableAgents == null || availableAgents.isEmpty()) {
throw new IllegalArgumentException("availableAgents must not be empty");
}
if (planningTool == null) {
throw new IllegalArgumentException("planningTool must not be null");
}
}
@Override
public PlanActAgent build() throws GraphStateException {
validate();
return new PlanActAgent(this);
}
}
}
PlanActGraphBuildingStrategy:创建节点和边
Graph 构建策略负责把 Planning、Execution、Summary 三个阶段组装起来。
public class PlanActGraphBuildingStrategy implements FlowGraphBuildingStrategy {
public static final String STRATEGY_TYPE = "PLAN_ACT";
private static final String PLANNING_NODE = "planning";
private static final String EXECUTION_NODE = "execution";
private static final String SUMMARY_NODE = "summary";
@Override
public StateGraph buildGraph(FlowGraphBuilder.FlowGraphConfig config)
throws GraphStateException {
validateConfig(config);
StateGraph graph = new StateGraph(
config.getName(),
config.getKeyStrategyFactory()
);
ChatModel chatModel = config.getChatModel();
PlanActTool planActTool = (PlanActTool) config.getCustomProperty("planningTool");
Map<String, ReactAgent> availableAgents =
(Map<String, ReactAgent>) config.getCustomProperty("availableAgents");
ToolCallback planningToolCallback = FunctionToolCallback
.builder(planActTool.getName(), planActTool)
.description(planActTool.getDescription())
.inputType(planActTool.getInputType())
.inputSchema(planActTool.getParameters())
.build();
ReactAgent planningAgent = ReactAgent.builder()
.name(PLANNING_NODE)
.model(chatModel)
.systemPrompt(buildPlanningSystemPrompt(availableAgents))
.tools(List.of(planningToolCallback))
.build();
ExecutionNode executionNode = new ExecutionNode(EXECUTION_NODE, availableAgents);
ReactAgent summaryAgent = ReactAgent.builder()
.name(SUMMARY_NODE)
.model(chatModel)
.systemPrompt(buildSummarySystemPrompt())
.build();
FlowGraphBuildingStrategy.addSubAgentNode(planningAgent, graph);
graph.addNode(EXECUTION_NODE, node_async(executionNode));
FlowGraphBuildingStrategy.addSubAgentNode(summaryAgent, graph);
graph.addEdge("__START__", PLANNING_NODE);
graph.addEdge(PLANNING_NODE, EXECUTION_NODE);
graph.addEdge(EXECUTION_NODE, SUMMARY_NODE);
graph.addEdge(SUMMARY_NODE, "__END__");
return graph;
}
@Override
public String getStrategyType() {
return STRATEGY_TYPE;
}
private String buildPlanningSystemPrompt(Map<String, ReactAgent> availableAgents) {
StringBuilder prompt = new StringBuilder();
prompt.append("你是任务规划专家,需要基于用户输入创建执行计划。")
.append("计划由多个步骤组成,每个步骤必须指定一个可用 Agent。")
.append("\n\n可用 Agent:\n");
for (Map.Entry<String, ReactAgent> entry : availableAgents.entrySet()) {
prompt.append("- ")
.append(entry.getKey())
.append(": ")
.append(entry.getValue().description())
.append("\n");
}
prompt.append("\n请使用 PlanActTool 创建执行计划。");
return prompt.toString();
}
private String buildSummarySystemPrompt() {
return """
你是结果汇总助手。
你需要根据分步骤执行结果提取关键信息,
合并重复内容,说明依据,并给出面向用户的最终回答。
""";
}
}
ExecutionNode:执行计划中的每个步骤
ExecutionNode 是普通 NodeAction,负责从状态里取出计划,再逐步调用对应 Agent。
public class ExecutionNode implements NodeAction {
private final String nodeId;
private final Map<String, ReactAgent> availableAgents;
public ExecutionNode(String nodeId, Map<String, ReactAgent> availableAgents) {
this.nodeId = nodeId;
this.availableAgents = availableAgents;
}
@Override
public Map<String, Object> apply(OverAllState state) throws Exception {
Planning planning = extractPlanningFromState(state);
String userInput = extractUserInput(state);
List<Map<String, String>> stepResults = new ArrayList<>();
StringBuilder accumulatedContext = new StringBuilder();
accumulatedContext
.append("用户请求:")
.append(userInput)
.append("\n\n");
for (PlanningStep step : planning.getSteps()) {
ReactAgent agent = availableAgents.get(step.getAgentName());
Map<String, String> stepResult = new LinkedHashMap<>();
stepResult.put("step_index", String.valueOf(step.getStepIndex()));
stepResult.put("agent", step.getAgentName());
stepResult.put("task", step.getStepGuide());
if (agent == null) {
stepResult.put("status", "skipped");
stepResult.put("error", "Agent not found: " + step.getAgentName());
stepResults.add(stepResult);
continue;
}
String stepPrompt = buildStepPrompt(step, accumulatedContext.toString());
try {
AssistantMessage response = agent.call(new UserMessage(stepPrompt));
String result = response.getText();
stepResult.put("status", "success");
stepResult.put("result", result);
accumulatedContext
.append("步骤 ")
.append(step.getStepIndex())
.append(",执行 Agent:")
.append(step.getAgentName())
.append("\n任务:")
.append(step.getStepGuide())
.append("\n结果:")
.append(result)
.append("\n\n");
}
catch (Exception ex) {
stepResult.put("status", "failed");
stepResult.put("error", ex.getMessage());
accumulatedContext
.append("步骤 ")
.append(step.getStepIndex())
.append(" 执行失败:")
.append(ex.getMessage())
.append("\n\n");
}
stepResults.add(stepResult);
}
List<Message> messages = new ArrayList<>(
(List<Message>) state.value("messages").orElse(new ArrayList<>())
);
messages.add(new UserMessage("""
按照计划执行后的结果如下:
%s
""".formatted(accumulatedContext)));
Map<String, Object> result = new HashMap<>();
result.put("step_results", stepResults);
result.put("execution_summary", accumulatedContext.toString());
result.put("messages", messages);
return result;
}
private String buildStepPrompt(PlanningStep step, String context) {
return """
请执行当前步骤。
当前任务:
%s
已有上下文:
%s
""".formatted(step.getStepGuide(), context);
}
}
这里有一个关键点:执行每个步骤时,不要只把当前步骤丢给 Agent。很多任务存在前后依赖,后续 Agent 需要知道用户原始请求、前序步骤结果、失败信息和可用约束。accumulatedContext 就是最简单的上下文传递方式。
使用 PlanActAgent
创建多个专家 Agent,然后交给 PlanActAgent 统一编排。
public class PlanActAgentExample {
private final ChatModel chatModel;
private final ToolCallback weatherTool;
private final ToolCallback trafficTool;
public AssistantMessage run(String userQuery) throws GraphStateException {
PlanActStrategyRegistrar.register();
Map<String, ReactAgent> availableAgents = new LinkedHashMap<>();
ReactAgent translationAgent = ReactAgent.builder()
.name("translation_agent")
.model(chatModel)
.systemPrompt("你是翻译专家,负责中英文互译。")
.description("负责中英文翻译")
.build();
ReactAgent weatherAgent = ReactAgent.builder()
.name("weather_agent")
.model(chatModel)
.systemPrompt("你是天气专家,负责查询天气并给出天气建议。")
.description("负责天气查询和天气建议")
.tools(List.of(weatherTool))
.build();
ReactAgent trafficAgent = ReactAgent.builder()
.name("traffic_agent")
.model(chatModel)
.systemPrompt("你是交通专家,负责查询交通信息并给出出行建议。")
.description("负责交通查询和出行建议")
.tools(List.of(trafficTool))
.build();
availableAgents.put("translation_agent", translationAgent);
availableAgents.put("weather_agent", weatherAgent);
availableAgents.put("traffic_agent", trafficAgent);
PlanActAgent planActAgent = PlanActAgent.builder()
.name("plan_act_agent")
.description("使用 Plan-Execute 模式完成复杂任务")
.chatModel(chatModel)
.availableAgents(availableAgents)
.planningTool(new PlanActTool())
.compileConfig(CompileConfig.builder().build())
.subAgents(new ArrayList<>(availableAgents.values()))
.build();
return planActAgent.invoke(userQuery);
}
}
调用时可以传入一个复杂目标:
AssistantMessage response = example.run("""
我周末想从上海去苏州玩一天。
请帮我看天气、交通,并给出适合的行程建议。
""");
规划阶段可能生成类似步骤:
| 步骤 | Agent | 任务 |
|---|---|---|
| 1 | weather_agent | 查询苏州周末天气 |
| 2 | traffic_agent | 查询上海到苏州的交通选择 |
| 3 | weather_agent | 根据天气给出穿衣和户外活动建议 |
| 4 | summary_agent | 汇总天气、交通和行程建议 |
执行阶段逐个调用专家 Agent,汇总阶段再把结果组织成最终回答。
Graph 编排时容易踩的坑
流式节点和状态更新差异
FlowGraphBuildingStrategy.addSubAgentNode(agent, graph) 添加的节点通常会包装成 AgentSubGraphNode。如果节点返回的是流式数据,框架更新 OverAllState 时可能只保留本轮 lastData。
影响是:Planning 节点的过程消息可能丢失,例如模型第一次返回、工具调用结果、工具观察结果等。
需要保留完整过程时,可以选择:
- 自己实现
NodeAction,显式控制返回状态。 - 在节点结果里放入过程数据,例如
planning_trace。 - 在 Hook 或 Interceptor 中记录执行轨迹。
- 将关键中间结果写入外部存储,再把索引放入状态。
子 Agent 状态不会自动完整进入父状态
ExecutionNode 内部会调用多个 ReactAgent。这些子 Agent 的内部消息、工具调用和中间状态不一定自动同步到父图的 OverAllState。
如果 Summary 阶段需要完整过程,需要在执行阶段主动返回:
result.put("step_results", stepResults);
result.put("execution_summary", accumulatedContext.toString());
result.put("tool_traces", toolTraces);
result.put("messages", messages);
不要只返回最终文本。否则 Summary Agent 能看到的上下文会很薄,回答质量也会受到影响。
Checkpoint 需要在 Agent 创建阶段配置
需要会话记忆时,BaseCheckpointSaver 要在 Agent 创建或 Graph 编译阶段配置好。对于 FlowAgent,可以通过 compileConfig 传入相关配置。
CompileConfig compileConfig = CompileConfig.builder()
.checkpointSaver(redisSaver)
.build();
PlanActAgent agent = PlanActAgent.builder()
.name("plan_act_agent")
.chatModel(chatModel)
.availableAgents(availableAgents)
.planningTool(planActTool)
.compileConfig(compileConfig)
.build();
没有 Checkpoint 时,多轮会话、任务恢复和执行追踪都会比较困难。
从提示词工程转向上下文工程
Agent 系统不是把提示词写长就能稳定运行。复杂任务里,真正影响结果的是上下文组织方式。
| 上下文问题 | 处理方式 |
|---|---|
| 工具返回太大 | 大结果写外部存储,只把摘要和引用 ID 放入上下文 |
| 工具返回格式不统一 | 定义统一响应结构,例如 status、data、error、source |
| 多轮消息过长 | 做摘要、裁剪、按优先级保留关键消息 |
| 需要业务知识 | 使用 RAG、文件检索工具或业务查询工具动态注入 |
| 多 Agent 信息混乱 | 明确共享信息和隔离信息,避免无关上下文污染 |
| 计划和执行偏离 | 每个步骤执行前带上原始目标、当前步骤和历史结果 |
| 成本不可控 | 限制模型调用次数、工具调用次数和最大循环数 |
一个实用的上下文结构可以分层:
flowchart TD
A[用户原始请求]
B[系统提示词]
C[可用工具和 Agent 描述]
D[短期会话记忆]
E[长期记忆检索结果]
F[RAG 召回内容]
G[工具执行结果]
H[步骤执行摘要]
A --> I[Agent Context]
B --> I
C --> I
D --> I
E --> I
F --> I
G --> I
H --> I
上下文越多不一定越好。无关内容会干扰模型判断,过长内容也会增加成本。更稳的做法是给上下文分优先级:目标、约束、当前步骤、关键结果优先保留;冗长过程、重复信息和低置信度召回内容可以摘要或丢弃。
架构选择:不是所有任务都需要 Planning
Planning 很适合开放式复杂任务,但不是银弹。架构选择应该看任务特征。
| 任务类型 | 推荐架构 | 原因 |
|---|---|---|
| 流程完全固定 | 固定工作流或 Sequential Agent | 可控、可测试、异常处理明确 |
| 单个 Agent 可完成,但需要工具 | ReAct Agent | 自主选择工具,复杂度适中 |
| 多个独立视角分析 | Parallel Agent | 并行执行后合并,节省等待时间 |
| 用户意图多样,需要分发 | Routing Agent 或 Supervisor Agent | 由模型判断进入哪个专家能力 |
| 目标复杂、路径不确定 | Plan-Execute | 先拆任务,再按步骤调度专家 |
| 强监管、高一致性场景 | 固定流程 + 局部 ReAct | 主链路可控,局部保留灵活性 |
很多业务系统更适合混合架构:主流程固定,局部节点使用 ReAct;少数复杂请求再进入 Plan-Execute。这样既能保留可控性,也能让模型在必要位置发挥决策能力。
工程落地建议
构建 Spring AI Alibaba Multi-Agent 系统时,可以按这条路径推进:
- 先把每个工具做好,保证描述、入参、返回值和异常结构稳定。
- 用
ReactAgent封装单个专家能力,例如天气、翻译、检索、总结。 - 对固定流程使用
SequentialAgent,对独立子任务使用ParallelAgent。 - 对复杂目标再引入 Plan-Execute,不要一开始就把所有请求都规划化。
- 用 Hook 和 Interceptor 做日志、监控、限流、内容审核和上下文注入。
- 明确状态字段,例如
planning、step_results、execution_summary、messages。 - 需要多轮和恢复能力时,提前配置 CheckpointSaver。
- 对大结果使用外部存储,避免把完整工具输出塞进模型上下文。
在已有工具和业务接口可以复用的前提下,Spring AI Alibaba 能把大量基础设施代码收敛到框架层:ReAct 循环、工具接入、记忆、Graph 状态、Multi-Agent 编排都有统一抽象。真正需要投入精力的部分,会变成 Agent 职责划分、工具设计、上下文工程和异常策略。对于 Java 团队来说,这比从零复刻一套 Multi-Agent 框架更容易维护,也更适合多人协作。