芥末
发布于 2026-02-13 / 0 阅读
0
0

用 Spring AI Alibaba 构建 ReAct 与 Plan-Execute Multi-Agent 系统

大模型应用做到一定复杂度后,问题会从“怎么调用模型”变成“怎么组织模型完成任务”。一次简单问答只需要拼提示词;一个业务流程可能需要多个步骤;更复杂的任务还需要模型自己判断要调用哪些工具、什么时候终止、不同智能体之间怎样传递上下文。

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 的执行过程拆成三个动作:

  1. Reasoning:分析当前状态,判断下一步应该做什么。
  2. Acting:调用工具或直接生成回答。
  3. 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 CallingSupervisor 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[最终回答]

三个阶段职责不同:

阶段组件职责
PlanningPlanningAgent + PlanActTool根据用户需求生成结构化计划,给每个步骤分配 Agent
ExecuteExecutionNode读取计划,逐步调用对应的 ReactAgent,并传递上下文
SummarySummaryAgent汇总每个步骤结果,生成面向用户的最终回答

这个模式适合目标明确但路径不固定的任务,例如:

  • “帮我规划一场去苏州的周末旅行,需要考虑天气、交通和预算。”
  • “分析一个商品是否适合上架,需要看价格、评价、库存和风险。”
  • “根据多个资料源生成调研报告,并列出引用依据。”

如果业务流程本来就是固定的,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任务
1weather_agent查询苏州周末天气
2traffic_agent查询上海到苏州的交通选择
3weather_agent根据天气给出穿衣和户外活动建议
4summary_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 系统时,可以按这条路径推进:

  1. 先把每个工具做好,保证描述、入参、返回值和异常结构稳定。
  2. ReactAgent 封装单个专家能力,例如天气、翻译、检索、总结。
  3. 对固定流程使用 SequentialAgent,对独立子任务使用 ParallelAgent
  4. 对复杂目标再引入 Plan-Execute,不要一开始就把所有请求都规划化。
  5. 用 Hook 和 Interceptor 做日志、监控、限流、内容审核和上下文注入。
  6. 明确状态字段,例如 planningstep_resultsexecution_summarymessages
  7. 需要多轮和恢复能力时,提前配置 CheckpointSaver。
  8. 对大结果使用外部存储,避免把完整工具输出塞进模型上下文。

在已有工具和业务接口可以复用的前提下,Spring AI Alibaba 能把大量基础设施代码收敛到框架层:ReAct 循环、工具接入、记忆、Graph 状态、Multi-Agent 编排都有统一抽象。真正需要投入精力的部分,会变成 Agent 职责划分、工具设计、上下文工程和异常策略。对于 Java 团队来说,这比从零复刻一套 Multi-Agent 框架更容易维护,也更适合多人协作。


评论