手写 Spring AI Agent:让大模型自主规划任务,ReAct 模式全流程拆解

手写 Spring AI Agent:让大模型自主规划任务,ReAct 模式全流程拆解

Spring AI 实战指南 · 进阶篇 | 一文搞懂 ReAct 核心循环、多工具编排与 Spring AI Alibaba 能力对比


先问一个问题

你在用 Spring AI 写代码的时候,有没有想过这些问题:

  • 想让 AI 同时调用多个工具(比如查航班、查酒店、查天气一起执行),Tool Calling 怎么做到?
  • AI 每次调用工具后,结果怎么传回去,让它基于上一步的结果决定下一步?
  • 怎么防止 AI 死循环调用工具 ,或者调用次数失控
  • 原生 Spring AI 和 Spring AI Alibaba ,在 Agent 这件事上差距有多大

如果你用过 Spring AI 的 Tool Calling ,那你其实已经懂了 60%------ReAct 就是在工具调用的基础上加了一个「思考-行动-观察的循环」。

但有个关键区别,很多人没搞明白,导致写出来的 Agent 要么死循环,要么调用乱序,要么最后根本不知道怎么对比 Spring AI Alibaba。

今天这篇文章,我把 ReAct 的原理、手写实现、多工具编排、Spring AI Alibaba 对比,一次性讲透。

📖 本文是「Spring AI 实战指南」系列进阶篇第 4 篇。建议按顺序阅读效果更佳,当然直接看也完全没问题。 🔗 系列目录 | 完整代码关注同名公众号获取


📖 目录

  1. [什么是 ReAct 模式](#什么是 ReAct 模式 "#%E4%B8%80-%E4%BB%80%E4%B9%88%E6%98%AF-react-%E6%A8%A1%E5%BC%8F")
  2. [Spring AI vs Spring AI Alibaba:Agent 能力对比](#Spring AI vs Spring AI Alibaba:Agent 能力对比 "#%E4%BA%8C-spring-ai-vs-spring-ai-alibabaagent-%E8%83%BD%E5%8A%9B%E5%AF%B9%E6%AF%94")
  3. 业务场景:智能出行规划助手
  4. 系统架构设计
  5. [ReAct 核心循环实现](#ReAct 核心循环实现 "#%E4%BA%94-react-%E6%A0%B8%E5%BF%83%E5%BE%AA%E7%8E%AF%E5%AE%9E%E7%8E%B0")
  6. 多工具编排实战
  7. [Agent 执行追踪与调试](#Agent 执行追踪与调试 "#%E4%B8%83-agent-%E6%89%A7%E8%A1%8C%E8%BF%BD%E8%B8%AA%E4%B8%8E%E8%B0%83%E8%AF%95")

一、什么是 ReAct 模式

1.1 从"单步工具调用"到"自主规划"

传统的 Tool Calling 是单步执行:用户问 → AI 调工具 → 返回结果。面对复杂任务时,这个模式力不从心:

arduino 复制代码
用户:"帮我规划下周三去上海的出行,要预订机票、酒店,顺便查一下上海的天气。"

❌ 单步工具调用做不到:需要同时调用机票查询、酒店查询、天气查询,还要综合结果给出规划建议
✅ ReAct Agent 可以:自主拆解任务 → 依次调用工具 → 汇总结果 → 给出完整规划

1.2 ReAct 是什么?

ReAct = Reasoning(推理)+ Acting(行动),是一种让 AI 自主规划和执行多步任务的框架:

复制代码
┌─────────────────────────────────────────────────────────┐
│                    ReAct 执行循环                         │
│                                                         │
│  ┌─────────┐    ┌─────────┐    ┌─────────┐             │
│  │ Thought │ →  │ Action  │ →  │Observation│            │
│  │ (推理)  │    │ (行动)  │    │ (观察)   │            │
│  └─────────┘    └─────────┘    └─────────┘             │
│       ↑                                ↓                │
│       └────────────────────────────────┘                │
│                   循环直到完成任务                         │
└─────────────────────────────────────────────────────────┘
阶段 说明 示例
Thought(思考) AI 分析当前状态,规划下一步 "用户需要订机票,先查询可用航班"
Action(行动) 调用具体工具执行操作 searchFlights("北京", "上海", "2025-03-12")
Observation(观察) 接收工具执行结果 "查到 3 个航班:CA1234, MU5678..."

1.3 ReAct vs 传统 Tool Calling

对比项 传统 Tool Calling ReAct Agent
任务复杂度 单步,一问一答 多步,自主规划
工具数量 通常 1-2 个 可以编排 N 个工具
执行策略 用户指定 AI 自主决策
容错能力 工具失败则结束 失败可重试或换策略
适用场景 简单查询 复杂任务、跨系统操作

一句话总结:Tool Calling 是「遥控器」,按一下动一下;ReAct 是「自动驾驶」,告诉目的地,自己规划路线。


二、Spring AI vs Spring AI Alibaba:Agent 能力对比

在动手写代码之前,有必要先聊一个绕不开的问题------同样是 Spring AI 生态,原生 Spring AI 和阿里的 Spring AI Alibaba 在 Agent 这件事上差距有多大?

这对选型很重要,选错了可能要走不少弯路。

2.1 定位差异

框架 定位 Agent 支持
Spring AI 通用 AI 集成抽象层 ❌ 无内置 Agent,需手动实现 ReAct 循环
Spring AI Alibaba 生产级 Agentic 应用框架 ✅ 开箱即用的 ReactAgent + 多 Agent 编排

原生 Spring AI 的定位更偏向基础设施 ------它把 LLM、工具调用、记忆存储等能力标准化封装好,但没有替你把"Agent 循环"这层建起来。所以才有了本文第五章那一大段手写的 ReactAgentExecutor

Spring AI Alibaba 则在此之上多建了一层

yaml 复制代码
原生 Spring AI:
┌──────────────────────────────┐
│   ChatClient / ChatModel     │  ← 基础抽象
└──────────────────────────────┘

Spring AI Alibaba:
┌──────────────────────────────────────┐
│  ReactAgent / SequentialAgent ...   │  ← 高级 Agent 抽象(新增)
├──────────────────────────────────────┤
│         Graph Core 工作流运行时       │  ← 工作流引擎(新增)
├──────────────────────────────────────┤
│         Spring AI                    │  ← 基础抽象
└──────────────────────────────────────┘

2.2 核心能力对比

能力维度 原生 Spring AI Spring AI Alibaba
ReAct 循环 ❌ 需手动实现(本文就是在做这件事) ✅ 内置 ReactAgent,3 行完成
多 Agent 编排 ❌ 不支持 ✅ Sequential / Parallel / Routing / Loop
工具重试 ❌ 需自己写 try-catch ToolRetryInterceptor
工具调用限制 ❌ 需手动计数 ToolCallLimitInterceptor
上下文摘要压缩 ❌ 不支持 SummarizationHook,超长自动压缩
人在回路 ❌ 不支持 HumanInTheLoopHook
流式 Agent 输出 ✅ 支持(ChatClient stream) agent.stream() + 更细粒度的事件类型
结构化输出 .entity(Clazz) .outputType(Clazz)
状态持久化 ✅ ChatMemory(需手动集成) .saver(new MemorySaver()) 一行搞定
可视化调试 ❌ 无 ✅ Spring AI Alibaba Studio
MCP 协议工具 ⚠️ 支持但生态少 ✅ 原生集成,支持 Python 工具
A2A Agent 通信 ❌ 不支持 ✅ Agent-to-Agent + Nacos 服务发现

📊 差距一目了然:Spring AI Alibaba 在 Agent 能力上是碾压级的。但这不代表原生 Spring AI 没有价值------理解底层原理,才能在上层框架出问题时快速定位。

2.3 用 Spring AI Alibaba 重写出行规划 Agent

来直观感受一下差距------同样实现本文的智能出行规划 Agent,用 Spring AI Alibaba 的写法是这样的:

依赖引入:

xml 复制代码
<dependency>
    <groupId>com.alibaba.cloud.ai</groupId>
    <artifactId>spring-ai-alibaba-starter-agent</artifactId>
    <version>1.0.0.2</version>
</dependency>

Agent 构建(对比本文第五章手写的 200+ 行):

java 复制代码
@Service
public class TravelPlanAgentService {

    private final ChatModel chatModel;

    public TravelPlanAgentService(ChatModel chatModel) {
        this.chatModel = chatModel;
    }

    // 🔥 3 行关键代码 vs 手写 200+ 行
    public ReactAgent buildAgent() {
        return ReactAgent.builder()
            .name("travel-planner")
            .model(chatModel)
            .instruction("""
                你是一个智能出行规划助手。
                根据用户需求,依次查询航班、酒店和天气,给出完整出行规划。
                """)
            // 注册多个工具
            .tools(
                flightSearchTool(),
                hotelSearchTool(),
                weatherQueryTool()
            )
            // 工具失败自动重试(最多 3 次)
            .interceptors(ToolRetryInterceptor.builder().maxRetries(3).build())
            // 工具总调用次数上限(防止死循环)
            .interceptors(ToolCallLimitInterceptor.builder().maxToolCalls(10).build())
            // 对话状态持久化
            .saver(new MemorySaver())
            // 启用执行日志
            .enableLogging(true)
            .build();
    }
}

调用方式:

java 复制代码
// 同步调用
AssistantMessage response = agent.call("帮我规划3月12日从北京去上海的出行,住两晚,机票预算1000以内");

// 流式调用(逐步返回每一步思考和工具调用过程)
Flux<NodeOutput> stream = agent.stream("帮我规划出行");
stream.subscribe(output -> {
    if (output instanceof StreamingOutput s) {
        if (s.getOutputType() == OutputType.AGENT_MODEL_STREAMING) {
            System.out.print(s.message().getText());  // 实时打印推理过程
        }
    }
});

// 带记忆的多轮对话(相同 threadId 自动共享上下文)
RunnableConfig config = RunnableConfig.builder().threadId("user_001").build();
agent.call("帮我规划去上海的出行", config);
agent.call("把机票预算改到1500", config);  // 自动记住上文

2.4 那本文为什么还要手写?

既然 Spring AI Alibaba 封装得这么好,为什么本文还要从头手写 ReAct 循环?

原因很简单:理解底层,才能用好上层。

场景 推荐方案
学习 ReAct 原理、理解 Agent 执行机制 ✅ 手写(本文方式)
快速搭建生产级 Agent ✅ Spring AI Alibaba ReactAgent
需要定制执行逻辑(如特殊 Prompt 格式、私有协议) ✅ 手写或基于 Spring AI Alibaba Graph Core 扩展
团队已重度使用阿里云生态(DashScope、Nacos) ✅ Spring AI Alibaba,生态协同更顺畅
需要多 Agent 协作、工作流编排 ✅ Spring AI Alibaba(差距最大的地方)

💡 建议:先啃透本文的手写实现,再用 Spring AI Alibaba 的封装去做项目------你会对它每个配置项的背后逻辑都心知肚明,遇到问题不慌。


三、业务场景:智能出行规划助手

3.1 场景描述

构建一个智能出行规划助手,用户只需一句话,Agent 自动完成:

功能 说明
航班搜索 根据出发地、目的地、日期查询可用航班
价格比较 对比不同航班的价格和时长
酒店推荐 根据目的地和入住日期推荐酒店
天气查询 查询目的地未来天气
行程汇总 综合所有信息给出完整出行规划

3.2 用户对话示例

sql 复制代码
用户输入:
"帮我规划下周三(3月12日)去上海的出行,北京出发,住两晚,预算机票不超过1000元。"

Agent 自主执行过程:
Thought 1:需要查询北京→上海的航班信息
Action 1:searchFlights("北京", "上海", "2025-03-12")
Observation 1:找到5个航班,价格680-1200元

Thought 2:需要筛选1000元以内的航班,并查询酒店
Action 2:searchHotels("上海", "2025-03-12", "2025-03-14")
Observation 2:找到10家酒店,价格200-800元/晚

Thought 3:需要查询上海天气
Action 3:getWeather("上海", "2025-03-12")
Observation 3:上海3月12-14日,晴,15-22℃

Thought 4:已有足够信息,整理出行规划
Final Answer:为您整理出行规划如下...

3.3 核心挑战

挑战 解决方案
任务分解 ReAct Prompt 引导 AI 逐步规划
工具选择 Spring AI Tool 注册机制
上下文传递 将每步 Observation 追加到对话历史
执行超时 最大步数限制 + 超时兜底
结果汇总 Final Answer 触发条件判断

四、系统架构设计

4.1 整体架构

scss 复制代码
┌─────────────────────────────────────────────────────────────────┐
│                         用户输入                                  │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│                    ReactAgentExecutor                            │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │                   ReAct 执行循环                          │   │
│  │                                                          │   │
│  │  ┌──────────┐  ┌──────────┐  ┌──────────┐              │   │
│  │  │  Think   │→ │  Act     │→ │ Observe  │ ← 循环        │   │
│  │  │(LLM推理) │  │(工具调用) │  │(结果收集) │              │   │
│  │  └──────────┘  └──────────┘  └──────────┘              │   │
│  │                                                          │   │
│  │  ┌──────────────────────────────────────────────────┐   │   │
│  │  │  ExecutionContext(执行上下文)                    │   │   │
│  │  │  - 历史 Thought/Action/Observation                │   │   │
│  │  │  - 当前步数 / 最大步数                            │   │   │
│  │  │  - 工具调用记录                                   │   │   │
│  │  └──────────────────────────────────────────────────┘   │   │
│  └──────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│                      工具层(Tools)                              │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐       │
│  │ 航班搜索  │  │ 酒店查询  │  │ 天气查询  │  │ 行程生成  │       │
│  │FlightTool│  │HotelTool │  │WeatherTool│  │ItineraryT│       │
│  └──────────┘  └──────────┘  └──────────┘  └──────────┘       │
└─────────────────────────────────────────────────────────────────┘
                              ↓
                     ┌────────────────┐
                     │  最终出行规划   │
                     └────────────────┘

4.2 项目依赖与配置

xml 复制代码
<dependencies>
    <!-- Spring AI Ollama -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
    </dependency>

    <!-- Spring Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>
yaml 复制代码
# application.yml
spring:
  ai:
    ollama:
      base-url: http://localhost:11434
      chat:
        model: qwen2.5:7b
        options:
          temperature: 0.1    # 降低随机性,让 Agent 规划更稳定
          num-ctx: 8192        # 扩大上下文,支持多轮 ReAct 循环

开发环境备注:本文基于本地模型(Ollama + qwen2.5:7b)讲解,零成本可离线运行。生产环境建议替换为大参数量云端模型以获得更好的规划能力。后续示例源码使用智谱大模型验证通过。


五、ReAct 核心循环实现

这是全文最核心的部分------我们将手写一个完整的 ReAct 执行引擎。理解了这段代码,你就真正懂了 Agent 的底层运作机制。

5.1 ReAct System Prompt ------ Agent 的"大脑指令"

ReAct 的核心在于给 AI 一个清晰的"思考-行动-观察"提示词格式:

java 复制代码
@Component
public class ReactPromptTemplate {

    /**
     * ReAct 核心 System Prompt
     * 
     * 设计要点:
     * - 明确定义 Thought/Action/Observation 的输出格式
     * - 设置 Final Answer 作为终止条件
     * - 限制最大步数防止死循环
     */
    public static final String REACT_SYSTEM_PROMPT = """
        你是一个智能出行规划 Agent,能够自主规划和执行多步任务。
        
        你必须按照以下格式进行思考和行动:
        
        Thought: [分析当前状态,决定下一步做什么]
        Action: [调用的工具名称和参数]
        Observation: [工具返回的结果,由系统填入]
        
        重复上述步骤,直到你有足够的信息给出最终答案。
        
        当任务完成时,用以下格式输出:
        Final Answer: [完整的出行规划]
        
        注意事项:
        1. 每次只执行一个 Action
        2. 基于 Observation 调整下一步策略
        3. 避免重复调用相同参数的工具
        4. 最多执行 10 步,超出后直接给出现有信息的总结
        """;
}

5.2 执行上下文 ------ Agent 的"短期记忆"

java 复制代码
/**
 * ReAct 执行上下文,记录 Agent 每一步的思考、行动和观察结果
 * 类似于人类的"工作记忆",在单次任务执行过程中持续累积
 */
@Data
@Builder
public class ReactExecutionContext {

    private String sessionId;
    private String originalTask;          // 用户原始任务
    private List<ReactStep> steps;        // 执行历史
    private int maxSteps;                 // 最大步数限制
    private boolean completed;            // 是否完成
    private String finalAnswer;           // 最终答案

    public boolean isExceededMaxSteps() {
        return steps.size() >= maxSteps;
    }

    /**
     * 将执行历史转为对话格式,追加到下一次 LLM 调用
     * 这是 ReAct 能"记住"之前做了什么的关键机制
     */
    public String buildHistoryText() {
        StringBuilder sb = new StringBuilder();
        for (ReactStep step : steps) {
            sb.append("Thought: ").append(step.getThought()).append("\n");
            if (step.getAction() != null) {
                sb.append("Action: ").append(step.getAction()).append("\n");
                sb.append("Observation: ").append(step.getObservation()).append("\n");
            }
            sb.append("\n");
        }
        return sb.toString();
    }
}

/**
 * 单步执行记录
 */
@Data
@Builder
public class ReactStep {
    private String thought;       // AI 的推理过程
    private String action;        // 执行的工具调用
    private String observation;   // 工具返回的观察结果
    private long durationMs;      // 本步执行耗时
}

5.3 ReAct 执行器核心逻辑 ------ Agent 的"发动机"

java 复制代码
@Service
@Slf4j
public class ReactAgentExecutor {

    private final ChatClient chatClient;
    private final ToolRegistry toolRegistry;

    public ReactAgentExecutor(ChatClient.Builder builder, ToolRegistry toolRegistry) {
        this.toolRegistry = toolRegistry;
        this.chatClient = builder
            .defaultSystem(ReactPromptTemplate.REACT_SYSTEM_PROMPT)
            .build();
    }

    /**
     * ========== 主入口:执行 ReAct 循环 ==========
     * 
     * 核心流程:
     * 1. 初始化执行上下文
     * 2. 循环执行 Think → Act → Observe
     * 3. 检测 Final Answer 终止条件
     * 4. 超出最大步数则强制汇总
     */
    public ReactExecutionContext execute(String task) {
        // ① 初始化上下文
        ReactExecutionContext context = ReactExecutionContext.builder()
            .sessionId(UUID.randomUUID().toString())
            .originalTask(task)
            .steps(new ArrayList<>())
            .maxSteps(10)                    // 安全阀:最多执行 10 步
            .completed(false)
            .build();

        log.info("[ReAct] 开始执行任务:{}", task);

        // ② ReAct 主循环
        while (!context.isCompleted() && !context.isExceededMaxSteps()) {
            ReactStep step = executeOneStep(context);
            context.getSteps().add(step);

            // ③ 检测终止条件:LLM 输出了 Final Answer
            if (step.getThought() != null && step.getThought().contains("Final Answer:")) {
                context.setCompleted(true);
                context.setFinalAnswer(extractFinalAnswer(step.getThought()));
                log.info("[ReAct] 任务完成,共执行 {} 步", context.getSteps().size());
            }
        }

        // ④ 超出最大步数的兜底处理
        if (!context.isCompleted()) {
            context.setFinalAnswer(forceSummarize(context));
            log.warn("[ReAct] 已达最大步数 {},强制汇总", context.getMaxSteps());
        }

        return context;
    }

    /**
     * ========== 单步执行:Think → Act → Observe ==========
     */
    private ReactStep executeOneStep(ReactExecutionContext context) {
        long startTime = System.currentTimeMillis();

        // --------- Phase 1: Think ------ 让 LLM 进行推理 ---------
        String prompt = buildStepPrompt(context);
        String llmResponse = chatClient.prompt()
            .user(prompt)
            .tools(toolRegistry.getAllTools())   // 注册可用工具列表
            .call()
            .content();

        // 解析 LLM 输出中的 Thought 和 Action
        String thought = parseThought(llmResponse);
        String actionText = parseAction(llmResponse);

        // --------- Phase 2 & 3: Act + Observe ------ 执行工具调用并收集结果 ---------
        String observation = "";
        if (actionText != null && !actionText.isBlank()) {
            observation = executeAction(actionText);
        }

        return ReactStep.builder()
            .thought(thought)
            .action(actionText)
            .observation(observation)
            .durationMs(System.currentTimeMillis() - startTime)
            .build();
    }

    /**
     * 构建包含历史的 Prompt ------ 让 LLM "看到"之前做了什么
     */
    private String buildStepPrompt(ReactExecutionContext context) {
        String history = context.buildHistoryText();

        if (history.isBlank()) {
            // 第一次调用:只传原始任务
            return "任务:" + context.getOriginalTask() + "\n\n请开始规划,先输出你的 Thought:";
        }

        // 后续调用:带上完整历史,让 LLM 基于之前的决策继续思考
        return String.format("""
            任务:%s
            
            已执行的步骤:
            %s
            
            请继续,输出下一步的 Thought 和 Action,或输出 Final Answer:
            """,
            context.getOriginalTask(),
            history
        );
    }

    /**
     * 执行 Action(工具调用),带异常保护
     */
    private String executeAction(String actionText) {
        try {
            ToolCall toolCall = ToolCallParser.parse(actionText);
            log.info("[ReAct] 调用工具:{} 参数:{}", toolCall.getToolName(), toolCall.getParams());

            Tool tool = toolRegistry.getTool(toolCall.getToolName());
            if (tool == null) {
                return "错误:未找到工具 " + toolCall.getToolName();
            }

            return tool.execute(toolCall.getParams());

        } catch (Exception e) {
            log.error("[ReAct] 工具调用失败:{}", e.getMessage());
            // 返回错误信息给 LLM,让它自行决定下一步策略(重试 or 换思路)
            return "工具调用失败:" + e.getMessage() + ",请调整参数重试。";
        }
    }

    // ---------- 文本解析工具方法 ----------

    private String parseThought(String llmResponse) {
        int thoughtStart = llmResponse.indexOf("Thought:");
        int actionStart = llmResponse.indexOf("Action:");
        if (thoughtStart == -1) return llmResponse;
        if (actionStart == -1) return llmResponse.substring(thoughtStart + 8).trim();
        return llmResponse.substring(thoughtStart + 8, actionStart).trim();
    }

    private String parseAction(String llmResponse) {
        int actionStart = llmResponse.indexOf("Action:");
        int observationStart = llmResponse.indexOf("Observation:");
        if (actionStart == -1) return null;
        if (observationStart == -1) return llmResponse.substring(actionStart + 7).trim();
        return llmResponse.substring(actionStart + 7, observationStart).trim();
    }

    private String extractFinalAnswer(String thought) {
        int idx = thought.indexOf("Final Answer:");
        if (idx == -1) return thought;
        return thought.substring(idx + 13).trim();
    }

    /**
     * 兜底方案:达到最大步数后,让 LLM 基于已收集信息强制汇总
     */
    private String forceSummarize(ReactExecutionContext context) {
        String summary = chatClient.prompt()
            .user("已收集信息如下:\n" + context.buildHistoryText() +
                  "\n请基于以上信息,给出出行规划建议。")
            .call()
            .content();
        return summary;
    }
}

🔑 核心设计要点

  • 历史累积机制:每次循环将 Thought/Action/Observation 拼接进 Prompt,让 LLM 拥有完整上下文
  • 终止条件检测 :通过识别 Final Answer: 关键字判断任务完成
  • 安全兜底maxSteps 上限 + forceSummarize 强制汇总,避免无限循环
  • 容错设计:工具调用异常时返回错误信息给 LLM,而非直接抛异常中断

六、多工具编排实战

6.1 工具注册中心 ------ 统一管理所有可用工具

java 复制代码
@Component
public class ToolRegistry {

    /**
     * 使用 LinkedHashMap 保证注册顺序(影响 LLM 的工具选择)
     */
    private final Map<String, Tool> toolMap = new LinkedHashMap<>();

    /**
     * 通过构造器自动注入所有 Tool Bean,无需手动注册
     * 新增工具只需加 @Component 即可,符合开闭原则
     */
    public ToolRegistry(FlightSearchTool flightTool,
                        HotelSearchTool hotelTool,
                        WeatherQueryTool weatherTool,
                        ItineraryGeneratorTool itineraryTool) {
        register(flightTool);
        register(hotelTool);
        register(weatherTool);
        register(itineraryTool);
    }

    private void register(Tool tool) {
        toolMap.put(tool.getName(), tool);
    }

    public Tool getTool(String name) {
        return toolMap.get(name);
    }

    public List<Object> getAllTools() {
        return new ArrayList<>(toolMap.values());
    }
}

6.2 航班搜索工具

java 复制代码
@Component
public class FlightSearchTool {

    @Tool(description = "搜索指定日期的可用航班,返回航班号、价格、时长等信息")
    public String searchFlights(
            @ToolParam(description = "出发城市,如:北京") String from,
            @ToolParam(description = "到达城市,如:上海") String to,
            @ToolParam(description = "出发日期,格式:YYYY-MM-DD") String date) {

        log.info("[Tool] 搜索航班:{} → {},日期:{}", from, to, date);

        // 实际项目中对接真实机票 API(如去哪儿、携程开放平台)
        List<FlightInfo> flights = flightApiClient.search(from, to, date);

        if (flights.isEmpty()) {
            return String.format("未找到 %s 到 %s 在 %s 的航班", from, to, date);
        }

        // 格式化返回结果,便于 LLM 理解和引用
        StringBuilder result = new StringBuilder();
        result.append(String.format("找到 %d 个航班:\n", flights.size()));
        for (FlightInfo flight : flights) {
            result.append(String.format(
                "- %s | %s→%s | 价格:¥%d | 时长:%s\n",
                flight.getFlightNo(),
                flight.getDepartTime(),
                flight.getArriveTime(),
                flight.getPrice(),
                flight.getDuration()
            ));
        }
        return result.toString();
    }

    @Tool(description = "获取指定航班的详细信息和余票情况")
    public String getFlightDetail(
            @ToolParam(description = "航班号,如:CA1234") String flightNo) {

        FlightDetail detail = flightApiClient.getDetail(flightNo);
        return String.format(
            "航班 %s:%s → %s\n机型:%s\n余票:%d 张\n餐食:%s",
            detail.getFlightNo(),
            detail.getDepartAirport(),
            detail.getArriveAirport(),
            detail.getAircraftType(),
            detail.getAvailableSeats(),
            detail.getMealService()
        );
    }
}

6.3 酒店搜索工具

java 复制代码
@Component
public class HotelSearchTool {

    @Tool(description = "搜索指定城市、日期的酒店,支持按星级和价格过滤")
    public String searchHotels(
            @ToolParam(description = "城市名称") String city,
            @ToolParam(description = "入住日期,格式:YYYY-MM-DD") String checkIn,
            @ToolParam(description = "退房日期,格式:YYYY-MM-DD") String checkOut,
            @ToolParam(description = "最高价格(元/晚),不限填 0") int maxPrice) {

        log.info("[Tool] 搜索酒店:{},{} ~ {}", city, checkIn, checkOut);

        List<HotelInfo> hotels = hotelApiClient.search(city, checkIn, checkOut, maxPrice);

        if (hotels.isEmpty()) {
            return String.format("未找到 %s 符合条件的酒店", city);
        }

        StringBuilder result = new StringBuilder();
        result.append(String.format("找到 %d 家酒店:\n", hotels.size()));
        for (HotelInfo hotel : hotels) {
            result.append(String.format(
                "- %s | %s星 | ¥%d/晚 | 评分:%.1f | 位置:%s\n",
                hotel.getName(),
                hotel.getStarLevel(),
                hotel.getPricePerNight(),
                hotel.getRating(),
                hotel.getLocation()
            ));
        }
        return result.toString();
    }
}

6.4 天气查询工具

java 复制代码
@Component
public class WeatherQueryTool {

    @Tool(description = "查询指定城市未来 7 天的天气预报")
    public String getWeatherForecast(
            @ToolParam(description = "城市名称") String city,
            @ToolParam(description = "查询日期,格式:YYYY-MM-DD") String date) {

        log.info("[Tool] 查询天气:{},日期:{}", city, date);

        WeatherInfo weather = weatherApiClient.getForecast(city, date);

        return String.format(
            "%s %s 天气:%s,气温 %d~%d℃,风力:%s,%s",
            city,
            date,
            weather.getCondition(),
            weather.getTempMin(),
            weather.getTempMax(),
            weather.getWindLevel(),
            weather.getTravelTip()
        );
    }
}

6.5 并行工具调用优化 ------ 让无依赖的工具同时执行

有些工具之间没有依赖关系,可以并行执行来减少总耗时:

java 复制代码
@Service
public class ParallelToolExecutor {

    /**
     * 使用虚拟线程(Java 21+),轻量级并发
     * 相比 ThreadPoolExecutor,虚拟线程创建开销几乎为零
     */
    private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

    /**
     * 并行执行多个工具,减少等待时间
     * 适用场景:酒店搜索和天气查询可以同时发起,无需等待对方结果
     */
    public Map<String, String> executeParallel(List<ToolCallRequest> requests) {
        Map<String, CompletableFuture<String>> futures = new LinkedHashMap<>();

        for (ToolCallRequest request : requests) {
            CompletableFuture<String> future = CompletableFuture
                .supplyAsync(() -> executeOne(request), executor)
                .orTimeout(5, TimeUnit.SECONDS)           // 单工具超时 5 秒
                .exceptionally(e -> "工具调用超时或失败:" + e.getMessage());

            futures.put(request.getToolName(), future);
        }

        // 等待所有工具完成
        Map<String, String> results = new LinkedHashMap<>();
        futures.forEach((name, future) -> {
            try {
                results.put(name, future.get());
            } catch (Exception e) {
                results.put(name, "执行异常:" + e.getMessage());
            }
        });

        return results;
    }

    private String executeOne(ToolCallRequest request) {
        Tool tool = toolRegistry.getTool(request.getToolName());
        return tool.execute(request.getParams());
    }
}

6.6 ReAct + 并行工具的混合策略

java 复制代码
/**
 * 混合执行器:继承基础 ReAct 执行器,增强并行调用能力
 * 
 * 设计思路:
 * - 如果 LLM 返回单个 Action → 走原有串行逻辑
 * - 如果 LLM 返回多个 Action(用 || 分隔)→ 并行执行
 */
@Service
@Slf4j
public class HybridReactExecutor extends ReactAgentExecutor {

    private final ParallelToolExecutor parallelExecutor;

    /**
     * 重写 executeAction,增加并行分支判断
     */
    @Override
    protected String executeAction(String actionText) {
        // 如果 AI 返回了多个并行 Action(用 || 分隔)
        if (actionText.contains("||")) {
            return executeParallelActions(actionText);
        }
        // 否则走默认串行逻辑
        return super.executeAction(actionText);
    }

    private String executeParallelActions(String actionText) {
        String[] actions = actionText.split("\\|\\|");
        List<ToolCallRequest> requests = Arrays.stream(actions)
            .map(String::trim)
            .map(ToolCallParser::parse)
            .map(tc -> new ToolCallRequest(tc.getToolName(), tc.getParams()))
            .collect(Collectors.toList());

        log.info("[ReAct] 并行执行 {} 个工具", requests.size());

        Map<String, String> results = parallelExecutor.executeParallel(requests);

        // 合并所有工具结果,统一返回给 LLM
        StringBuilder combined = new StringBuilder();
        results.forEach((toolName, result) -> {
            combined.append("【").append(toolName).append("结果】\n");
            combined.append(result).append("\n\n");
        });

        return combined.toString();
    }
}

七、Agent 执行追踪与调试

生产环境中,能清晰看到 Agent 的每一步决策至关重要------否则出了问题根本不知道 LLM 在想什么。

7.1 执行链追踪

java 复制代码
@Component
@Slf4j
public class ReactExecutionTracer {

    /**
     * 打印完整的执行链路日志
     * 包含:任务信息、每步 Thought/Action/Observation、最终答案
     * 格式化输出,便于日志分析和问题排查
     */
    public void traceExecution(ReactExecutionContext context) {
        log.info("═══════════════════════════════════════════");
        log.info("任务:{}", context.getOriginalTask());
        log.info("会话 ID:{}", context.getSessionId());
        log.info("总步数:{}/{}", context.getSteps().size(), context.getMaxSteps());
        log.info("───────────────────────────────────────────");

        for (int i = 0; i < context.getSteps().size(); i++) {
            ReactStep step = context.getSteps().get(i);
            log.info("[Step {}] ({}ms)", i + 1, step.getDurationMs());
            log.info("  Thought   : {}", abbreviate(step.getThought(), 100));
            log.info("  Action    : {}", step.getAction());
            log.info("  Observation: {}", abbreviate(step.getObservation(), 100));
        }

        log.info("───────────────────────────────────────────");
        log.info("Final Answer: {}", abbreviate(context.getFinalAnswer(), 200));
        log.info("═══════════════════════════════════════════");
    }

    /** 截断过长文本,避免日志刷屏 */
    private String abbreviate(String text, int maxLen) {
        if (text == null) return "null";
        return text.length() <= maxLen ? text : text.substring(0, maxLen) + "...";
    }
}

7.2 对外暴露 API

java 复制代码
@RestController
@RequestMapping("/api/travel-agent")
public class TravelAgentController {

    private final HybridReactExecutor agentExecutor;
    private final ReactExecutionTracer tracer;

    /**
     * 同步接口:等待 Agent 全部执行完毕后返回结果
     * 适用场景:前端 loading 等待,对实时性要求不高
     */
    @PostMapping("/plan")
    public TravelPlanResponse planTravel(@RequestBody TravelPlanRequest request) {
        ReactExecutionContext context = agentExecutor.execute(request.getTask());
        tracer.traceExecution(context);

        return TravelPlanResponse.builder()
            .sessionId(context.getSessionId())
            .finalAnswer(context.getFinalAnswer())
            .steps(context.getSteps().size())
            .completed(context.isCompleted())
            .build();
    }

    /**
     * SSE 流式接口:逐步返回每一步执行过程
     * 适用场景:前端实时展示 Agent 的思考和行动过程,用户体验更好
     * 
     * 事件格式:step:N|thought:xxx|action:xxx|observation:xxx
     * 终止事件:final:xxx
     */
    @GetMapping(value = "/plan/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> planTravelStream(@RequestParam String task) {
        return Flux.create(sink -> {
            agentExecutor.executeWithCallback(task, new ReactStepCallback() {

                @Override
                public void onStep(ReactStep step, int stepIndex) {
                    String event = String.format(
                        "step:%d|thought:%s|action:%s|observation:%s",
                        stepIndex,
                        step.getThought(),
                        step.getAction(),
                        step.getObservation()
                    );
                    sink.next(event);
                }

                @Override
                public void onComplete(String finalAnswer) {
                    sink.next("final:" + finalAnswer);
                    sink.complete();
                }

                @Override
                public void onError(Throwable error) {
                    sink.error(error);
                }
            });
        });
    }
}

7.3 效果演示

调用 /api/travel-agent/plan,传入:

json 复制代码
{
  "task": "帮我规划3月12日从北京去上海的出行,住两晚,机票预算1000以内"
}

返回结果:

json 复制代码
{
  "sessionId": "a3f2c1d4-...",
  "finalAnswer": "为您规划如下出行方案:\n\n✈️ 推荐航班:CA1532,08:00出发,10:05到达,¥680\n🏨 推荐酒店:全季酒店·上海外滩,¥388/晚,评分4.8\n🌤 天气:3月12-14日,晴,16-22℃,适合出行,建议带件薄外套\n\n💡 总费用预估:机票¥680 + 酒店¥776 = ¥1456\n   出发前记得提前2小时到达机场!",
  "steps": 4,
  "completed": true
}

总结

本文通过"智能出行规划助手"场景,完整演示了 ReAct 模式 + 多工具编排的实现。下面一张表帮你快速回顾全文要点:

核心概念 手写实现(Spring AI) Spring AI Alibaba 等价方案
ReAct 循环 手写 ReactAgentExecutor (~200行) ReactAgent.builder().build() (3行)
多工具注册 ToolRegistry 统一管理 .tools(tool1, tool2, ...)
上下文传递 ExecutionContext 累积历史 内置,MemorySaver 自动持久化
并行工具 CompletableFuture + Virtual Threads ParallelAgent 原生支持
工具重试 手写 try-catch ToolRetryInterceptor.builder().maxRetries(3)
执行步数限制 maxSteps 字段手动判断 ToolCallLimitInterceptor
执行追踪 ReactExecutionTracer 自定义日志 .enableLogging(true) + Studio 可视化
流式输出 Flux + 自定义 SSE agent.stream() 内置,事件类型更丰富

选型速查表

你的场景 推荐
学习 ReAct 底层原理 👉 先看本文手写版
快速做生产级项目 👉 直接上 Spring AI Alibaba
需要自定义执行逻辑 👉 手写或基于 Graph Core 扩展
多 Agent 协作/工作流 👉 Spring AI Alibaba(优势最大)

📚 系列导航

本篇文章属于「Spring AI 系列」进阶篇,建议按顺序阅读:

主题 关键字
1 入门:环境搭建与第一个 AI 对话 Quick Start
2 Tool Calling 工具调用基础 Function Callback
★ 6 进阶:ReAct 模式与多工具编排 ReAct Agent(本文)
4 Advisor 拦截器链 责任链 / 自定义扩展
5 源码解析:ChatClient 内部机制 源码解读
6 Tool Calling 工具调用 Function Callback
7 VectorStore与RAG Pipeline 检索增强生成

👋 我是亦暖筑序,专注 Java 后端开发者的 AI 应用落地指南。

如果这篇 ReAct Agent 解析对你有帮助,欢迎 点赞 + 收藏 ,评论区说说你在项目中是如何处理多工具协作的? 我们下期见!🚀


相关推荐
万里鹏程转瞬至3 小时前
论文简读:Embarrassingly Simple Self-Distillation Improves Code Generation
人工智能·深度学习
敖正炀3 小时前
ReentrantLock 与 synchronized对比
java
空中湖3 小时前
大模型修炼秘籍
人工智能·agi
XiYang-DING3 小时前
【Java】二叉搜索树(BST)
java·开发语言·python
weixin_437957613 小时前
Mysql安装不成功
java
Lyyaoo.3 小时前
【JAVA基础面经】进程安全问题(synchronized and volatile)
java·开发语言·jvm
别或许3 小时前
4、高数----一元函数微分学的计算
人工智能·算法·机器学习
Andya_net4 小时前
Java | 基于 Feign 流式传输操作SFTP文件传输
java·开发语言·spring boot
_Evan_Yao4 小时前
别让“规范”困住你:前后端交互中的方法选择与认知突围
java·后端·交互·restful