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


先问一个问题

你在用 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 对比,一次性讲透。


目录

  1. 什么是 ReAct 模式
  2. Spring AI vs Spring AI Alibaba:Agent 能力对比
  3. 业务场景:智能出行规划助手
  4. 系统架构设计
  5. ReAct 核心循环实现
  6. 多工具编排实战
  7. Agent 执行追踪与调试

一、什么是 ReAct 模式

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

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

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

 单步工具调用做不到:需要同时调用机票查询、酒店查询、天气查询,还要综合结果给出规划建议
 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 CallingReAct 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 则在此之上多建了一层

原生 Spring AI:
┌──────────────────────────────┐
   ChatClient / ChatModel        基础抽象
└──────────────────────────────┘

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

2.2 核心能力对比

能力维度原生 Spring AISpring 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 服务发现

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

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

依赖引入:

<dependency>
    <groupId>com.alibaba.cloud.ai</groupId>
    <artifactId>spring-ai-alibaba-starter-agent</artifactId>
    <version>1.0.0.2</version>
</dependency>

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

@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();
    }
}

调用方式:

// 同步调用
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(差距最大的地方)

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

3.1 场景描述

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

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

3.2 用户对话示例

用户输入:
"帮我规划下周三(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:上海312-14日,晴,15-22℃

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

3.3 核心挑战

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

四、系统架构设计

4.1 整体架构

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

4.2 项目依赖与配置

<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>
# application.yml
spring:
  ai:
    ollama:
      base-url: 
      chat:
        model: qwen2.5:7b
        options:
          temperature: 0.1    # 降低随机性,让 Agent 规划更稳定
          num-ctx: 8192        # 扩大上下文,支持多轮 ReAct 循环

五、ReAct 核心循环实现

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

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

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

@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 的"短期记忆"

/**
 * 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 的"发动机"

@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() + "nn请开始规划,先输出你的 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;
    }
}

六、多工具编排实战

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

@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 航班搜索工具

@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 | 时长:%sn",
                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 → %sn机型:%sn余票:%d 张n餐食:%s",
            detail.getFlightNo(),
            detail.getDepartAirport(),
            detail.getArriveAirport(),
            detail.getAircraftType(),
            detail.getAvailableSeats(),
            detail.getMealService()
        );
    }
}

6.3 酒店搜索工具

@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 | 位置:%sn",
                hotel.getName(),
                hotel.getStarLevel(),
                hotel.getPricePerNight(),
                hotel.getRating(),
                hotel.getLocation()
            ));
        }
        return result.toString();
    }
}

6.4 天气查询工具

@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 并行工具调用优化 —— 让无依赖的工具同时执行

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

@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 + 并行工具的混合策略

/**
 * 混合执行器:继承基础 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("nn");
        });

        return combined.toString();
    }
}

七、Agent 执行追踪与调试

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

7.1 执行链追踪

@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

@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,传入:

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

返回结果:

{
  "sessionId": "a3f2c1d4-...",
  "finalAnswer": "为您规划如下出行方案:nn️ 推荐航班:CA1532,08:00出发,10:05到达,¥680n 推荐酒店:全季酒店·上海外滩,¥388/晚,评分4.8n 天气:3月12-14日,晴,16-22℃,适合出行,建议带件薄外套nn 总费用预估:机票¥680 + 酒店¥776 = ¥1456n   出发前记得提前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 ThreadsParallelAgent 原生支持
工具重试手写 try-catchToolRetryInterceptor.builder().maxRetries(3)
执行步数限制maxSteps 字段手动判断ToolCallLimitInterceptor
执行追踪ReactExecutionTracer 自定义日志.enableLogging(true) + Studio 可视化
流式输出Flux + 自定义 SSEagent.stream() 内置,事件类型更丰富

选型速查表

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

系列导航

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

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

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com