A08.手把手教你实现 ReAct 智能体编程范式.md

一灰灰blogSpringAISpringSpringAI约 5564 字大约 19 分钟

Spring AI 实战:手把手教你实现 ReAct 智能体编程范式

A08-01.webp
A08-01.webp

摘要:本文通过一个完整的 Spring AI 项目实例,深入浅出地讲解 ReAct(Reasoning + Acting)智能体编程范式的核心工作原理。我们将从零开始实现一个轻量级的 ReAct Agent,并通过流式处理优化用户体验,让你彻底掌握智能体开发的核心技术。


📖 目录


什么是 ReAct 范式?

ReAct(Reasoning + Acting)是一种先进的智能体编程范式,它将推理(Reasoning)和行动(Acting)有机结合,让 AI 系统能够像人类一样思考并执行任务。

A08-2.webp
A08-2.webp

图 1: ReAct 核心循环流程图 - Thinking → Acting → Observing 顺时针闭环,展示智能体的基础运行模型

ReAct的核心循环

┌─────────────┐
│   Thinking  │ ← 分析当前情况,决定下一步行动
└──────┬──────┘
       │
       ▼
┌─────────────┐
│    Acting   │ ← 调用工具执行具体操作
└──────┬──────┘
       │
       ▼
┌─────────────┐
│  Observing  │ ← 观察工具执行结果
└──────┬──────┘
       │
       └──────→ 返回 Thinking,继续下一轮循环

为什么需要 ReAct?

传统的 AI 对话系统存在以下局限:

  • ❌ 无法主动获取外部信息
  • ❌ 无法执行实际操作
  • ❌ 缺乏逻辑推理链条
  • ❌ 难以处理复杂多步任务

而 ReAct 范式通过思考 - 行动 - 观察的循环机制,完美解决了这些问题。


项目架构总览

让我们先看看项目的整体结构:

├── A06Application.java          # Spring Boot 主应用
├── advisor/
│   └── MyLoggingAdvisor.java    # 自定义日志顾问
└── react/
    ├── service/
    │   └── LlmService.java      # LLM 服务封装
    ├── simple/
    │   ├── CalculatorTools.java # 计算器工具集
    │   ├── SimpleReActAgent.java # 轻量级 ReAct 实现
    │   └── SimpleReActRunner.java # 演示运行器
    └── stream/
        ├── StreamReActAgent.java # 流式 ReAct 实现
        └── StreamReActRunner.java # 流式演示运行器

技术栈

  • 框架: Spring Boot + Spring AI
  • 模型: 支持多种大模型(默认使用 Qwen2.5-7B-Instruct)
  • 工具: 基于 ToolCallback 机制的工具调用系统
  • 响应式: Reactor(流式处理)

核心组件详解

1️⃣ 工具定义:CalculatorTools

首先,我们需要定义一些工具供 AI 调用(也用于后续的测试示例)。 这里提供了几个基本的计算和一个模拟的天气查询工具,通过SpringAI的@Tool进行工具声明定义

package com.git.hui.springai.app.react.simple;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;

import java.util.List;

public class CalculatorTools {
    private static final Logger log = LoggerFactory.getLogger(CalculatorTools.class);

    @Tool(description = "执行加法运算,返回两个数的和")
    public double add(@ToolParam(description = "第一个加数") double a,
                      @ToolParam(description = "第二个加数") double b) {
        log.debug("[🔨] 执行加法:{} + {}", a, b);
        return a + b;
    }

    @Tool(description = "执行减法运算,返回两个数的差")
    public double subtract(@ToolParam(description = "被减数") double a,
                           @ToolParam(description = "减数") double b) {
        log.debug("[🔨] 执行减法:{} - {}", a, b);
        return a - b;
    }

    @Tool(description = "执行乘法运算,返回两个数的积")
    public double multiply(@ToolParam(description = "第一个乘数") double a,
                           @ToolParam(description = "第二个乘数") double b) {
        log.debug("[🔨] 执行乘法:{} * {}", a, b);
        return a * b;
    }

    @Tool(description = "执行除法运算,返回两个数的商")
    public double divide(@ToolParam(description = "被除数") double a,
                         @ToolParam(description = "除数") double b) {
        log.debug("[🔨] 执行除法:{} / {}", a, b);
        if (b == 0) {
            throw new ArithmeticException("除数不能为零");
        }
        return a / b;
    }

    @Tool(description = "查询天气信息")
    public String weather(@ToolParam(description = "城市名称") String city) {
        log.debug("[🔨] 执行天气查询:{}", city);
        List<String> temperatures = List.of("25°C", "27°C", "23°C", "21°C", "19°C");
        List<String> weathers = List.of("晴天", "阴天", "雨天", "雷雨", "雪天");
        return "当前" + city + "的天气为:" + 
               weathers.get((int) (Math.random() * weathers.size())) + 
               " ,气温为:" + temperatures.get((int) (Math.random() * temperatures.size()));
    }

    /**
     * 获取所有工具回调
     */
    public List<ToolCallback> getTools() {
        ToolCallback[] toolCallbacks = MethodToolCallbackProvider.builder()
                .toolObjects(this)
                .build()
                .getToolCallbacks();
        return List.of(toolCallbacks);
    }
}

关键点解析

  1. @Tool 注解:标记这是一个工具方法,description 会被大模型理解
  2. @ToolParam 注解:描述参数含义,帮助大模型正确使用
  3. MethodToolCallbackProvider:自动将方法转换为工具回调
  4. 工具设计原则:单一职责、明确描述、类型安全

2️⃣ 自定义日志顾问:MyLoggingAdvisor

为了更好地观察 ReAct 过程,我们实现了一个自定义的日志顾问,用于请求前后交互信息

完整的代码实现请到文末的项目源码进行查看

public class MyLoggingAdvisor implements BaseAdvisor {
	@Override
    public ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain) {
        StringBuilder sb = new StringBuilder("\n[log] USER INPUT⬇️:");

        if (this.showSystemMessage && chatClientRequest.prompt().getSystemMessage() != null) {
            sb.append("\n [log] SYSTEM: ").append(first(chatClientRequest.prompt().getSystemMessage().getText(), 300));
        }

        if (this.showAvailableTools) {
            Object tools = "No Tools";

            if (chatClientRequest.prompt().getOptions() instanceof ToolCallingChatOptions toolOptions) {
                tools = toolOptions.getToolCallbacks().stream().map(tc -> tc.getToolDefinition().name()).toList();
            }

            sb.append("\n [log] TOOLS: ").append(ModelOptionsUtils.toJsonString(tools));
        }

        List<Message> msgList = chatClientRequest.prompt().getInstructions();
        Message lastMessage = null;
        for (int i = msgList.size() - 1; i >= 0; i--) {
            Message message = msgList.get(i);
            if (message instanceof UserMessage || message instanceof ToolResponseMessage) {
                lastMessage = message;
                break;
            }
        }
        if (lastMessage == null) {
            lastMessage = new UserMessage("");
        }

        if (lastMessage.getMessageType() == MessageType.TOOL) {
            ToolResponseMessage toolResponseMessage = (ToolResponseMessage) lastMessage;
            for (var toolResponse : toolResponseMessage.getResponses()) {
                var tr = toolResponse.name() + ": " + first(toolResponse.responseData(), 1000);
                sb.append("\n [log] TOOL-RESPONSE: ").append(tr);
            }
        } else if (lastMessage.getMessageType() == MessageType.USER) {
            if (StringUtils.hasText(lastMessage.getText())) {
                sb.append("\n [log] TEXT: ").append(first(lastMessage.getText(), 1000));
            }
        }

        log.debug("[log] before: {}", sb);
        return chatClientRequest;
    }

    @Override
    public ChatClientResponse after(ChatClientResponse chatClientResponse, AdvisorChain advisorChain) {
        StringBuilder sb = new StringBuilder("\nASSISTANT: ");

        if (chatClientResponse.chatResponse() == null || chatClientResponse.chatResponse().getResults() == null) {
            sb.append(" [log] No chat response ");
            log.debug("[log] after: {}", sb);
            return chatClientResponse;
        }

        for (var generation : chatClientResponse.chatResponse().getResults()) {
            var message = generation.getOutput();
            if (message.getToolCalls() != null) {
                for (var toolCall : message.getToolCalls()) {
                    sb.append("\n [log] TOOL-CALL: ")
                            .append(toolCall.name())
                            .append(" (")
                            .append(toolCall.arguments())
                            .append(")");
                }
            }

            if (message.getText() != null) {
                if (StringUtils.hasText(message.getText())) {
                    sb.append("\n [log] TEXT: ").append(first(message.getText(), 1200));
                }
            }
        }

        log.debug("[log] after: {}", sb.toString().replaceAll("\n", "\t"));
        return chatClientResponse;
    }
}

作用

  • ✅ 记录每次请求的完整上下文
  • ✅ 追踪工具调用链
  • ✅ 调试和排查问题

3️⃣ LLM 服务封装:LlmService

统一管理 ChatClient 的创建和配置(支持根据切换使用不同的模型进行响应)

@Service
public class LlmService {
    @Autowired
    private ChatModel chatModel;

    private Map<String, ChatClient> chatClientMap = new ConcurrentHashMap<>();

    public ChatClient getChatClient(String modelName) {
        if (StringUtils.isBlank(modelName)) {
            modelName = "Qwen/Qwen2.5-7B-Instruct";
        }

        return chatClientMap.computeIfAbsent(modelName, name -> 
            ChatClient.builder(chatModel)
                .defaultOptions(ChatOptions.builder().model(modelName).build())
                .defaultAdvisors(
                    MyLoggingAdvisor.builder()
                        .showAvailableTools(true)
                        .showSystemMessage(true)
                        .build())
                .build()
        );
    }
}

实现轻量级 ReAct Agent

这是整个项目的核心部分!让我们逐行分析 SimpleReActAgent 的实现。也希望大家可以基于这个简单的实现来理解ReAct的核心理念

核心数据结构

public class SimpleReActAgent {
    private static final int MAX_ITERATIONS = 10;  // 最大迭代次数
    private final ChatClient chatClient;           // 聊天客户端
    private final List<ToolCallback> tools;        // 可用工具列表
    
    public SimpleReActAgent(ChatClient chatClient, List<ToolCallback> tools) {
        this.chatClient = chatClient;
        this.tools = tools != null ? tools : new ArrayList<>();
    }
}

主流程:ReAct 循环

public String run(String question) {
    // 1. 初始化对话历史
    List<Message> messages = new ArrayList<>();
    messages.add(new UserMessage(question));

    // 2. ReAct 循环
    for (int iteration = 1; iteration <= MAX_ITERATIONS; iteration++) {
        log.info("🔁 第 {} 轮思考", iteration);

        try {
            // Thinking: 让大模型思考下一步该做什么
            ChatResponse response = think(messages);
            AssistantMessage assistantMessage = response.getResult().getOutput();

            // Act & Observe: 检查是否需要调用工具
            if (hasToolCalls(assistantMessage)) {
                // 执行工具调用
                String toolResult = executeTools(assistantMessage);
                
                // 将工具结果添加到对话历史
                var toolCall = getToolCall(assistantMessage);
                messages.add(ToolResponseMessage.builder()
                    .responses(List.of(new ToolResponseMessage.ToolResponse(
                        toolCall.id(), toolCall.name(), toolResult)))
                    .build());
                
                log.info("工具执行结果 {} => {}", toolCall.name(), toolResult);
            } else {
                // 没有工具调用,说明已给出最终答案
                String finalAnswer = assistantMessage.getText();
                log.info("✅ 无工具调用,直接返回结果:{}", finalAnswer);
                return finalAnswer;
            }
        } catch (Exception e) {
            log.error("ReAct 循环出错:{}", e.getMessage(), e);
            break;
        }
    }

    return "达到最大迭代次数 (" + MAX_ITERATIONS + "),无法完成任务。";
}

流程解析

  1. 初始化:将用户问题加入对话历史
  2. 循环开始:设置最大迭代次数防止无限循环
  3. Thinking:调用大模型,让它思考下一步
  4. 判断:检查是否有工具调用
    • ✅ 有 → 执行工具,记录结果,继续下一轮
    • ❌ 无 → 返回最终答案
  5. 异常处理:捕获错误,安全退出

请注意,上面这里判断是否结束是根据是否有工具调用来的,在真实的业务场景中,这样的方式显然不太友好,理应设计一套更符合实际场景的Observe(比如让大模型审查构建的返回是否满足需要,再多一轮对话让大模型整理输出结果等)

Thinking 阶段:构建系统提示

这是智能体的“内心独白”。它会分析当前情况、分解任务、制定下一步计划,或者反思上一步的结果。

private ChatResponse think(List<Message> messages) {
    // 构建系统提示
    String systemPrompt = """
        你是一个智能助手,使用 ReAct 范式解决问题。
                        
        请按以下步骤思考:
        1. 分析当前问题和已有信息
        2. 判断是否需要使用工具获取更多信息
        3. 如果需要工具,调用一个合适的工具
        4. 【重要】如果已有足够信息,直接给出最终答案
                        
        响应规则:
        - 你必须一次只调用一个工具,不允许同时调用多个工具
        - 关键:调用工具时,你必须使用工具定义中完整的工具名称
        - 【重要】如果之前的工具执行已经提供了足够的信息来回答原始问题,
          不要再调用另一个工具。相反,综合结果并直接提供你的最终答案
                        
        可用工具列表:\n""" + getToolDescriptions();

    Message systemMessage = new SystemMessage(systemPrompt);
    List<Message> allMessages = new ArrayList<>();
    allMessages.add(systemMessage);
    allMessages.addAll(messages);

    // 设置工具调用选项(禁用自动执行)
    ToolCallingChatOptions options = ToolCallingChatOptions.builder()
            .internalToolExecutionEnabled(false)  // 关键:手动控制工具执行
            .build();

    Prompt prompt = new Prompt(allMessages, options);

    return chatClient.prompt(prompt)
            .toolCallbacks(tools)
            .call()
            .chatResponse();
}

关键技巧

  1. 明确的指令:告诉 AI 如何使用 ReAct 范式
  2. 限制条件:一次只调用一个工具,避免混乱
  3. 终止条件:信息足够时直接给出答案
  4. 禁用自动执行internalToolExecutionEnabled(false) 让我们手动控制

Act & Observe 阶段:执行工具

这是智能体决定采取的具体动作,通常是调用一个工具,然后观察工具的执行结果,判断是否可以终止循环

private String executeTools(AssistantMessage message) throws Exception {
    var toolCall = getToolCall(message);  // 获取第一个工具调用

    log.debug("🔧 执行工具调用");
    log.debug("  工具名称:{}", toolCall.name());
    log.debug("  工具参数:{}", toolCall.arguments());

    // 查找并执行匹配的工具
    for (ToolCallback tool : tools) {
        if (tool.getToolDefinition().name().equals(toolCall.name())) {
            Object result = tool.call(toolCall.arguments());
            String resultText = result != null ? result.toString() : "null";
            log.debug("  执行结果:{}", resultText);
            return resultText;
        }
    }

    throw new RuntimeException("未找到工具:" + toolCall.name());
}

执行流程

  1. 提取工具名称和参数
  2. 遍历工具列表找到匹配的
  3. 调用工具并获取结果
  4. 返回结果用于下一轮思考

流式处理优化

基础的 ReAct Agent 功能已经实现,但用户体验不够好——用户需要等待很长时间才能看到结果。让我们用流式处理优化它!

A08-6.webp
A08-6.webp

图 3: 流式处理 vs 非流式处理对比 - 左侧红色调展示传统方式的不足,右侧绿色调展示流式处理的优势,底部表格总结关键差异

StreamReActAgent 核心实现

public class StreamReActAgent {
    
    public String run(String question) {
        List<Message> messages = new ArrayList<>();
        messages.add(new UserMessage(question));

        // ReAct 循环
        for (int iteration = 1; iteration <= MAX_ITERATIONS; iteration++) {
            // Thinking: 流式获取大模型的思考过程
            AssistantMessage assistantMessage = thinkStreaming(messages);

            if (hasToolCalls(assistantMessage)) {
                String toolResult = executeTools(assistantMessage);
                var toolCall = getToolCall(assistantMessage);
                messages.add(ToolResponseMessage.builder()
                    .responses(List.of(new ToolResponseMessage.ToolResponse(
                        toolCall.id(), toolCall.name(), toolResult)))
                    .build());
            } else {
                return assistantMessage.getText();
            }
        }
        return "达到最大迭代次数,无法完成任务。";
    }
}

流式 Thinking 实现

private AssistantMessage thinkStreaming(List<Message> messages) {
    String systemPrompt = /* ... 同上 ... */;
    
    Message systemMessage = new SystemMessage(systemPrompt);
    List<Message> allMessages = new ArrayList<>();
    allMessages.add(systemMessage);
    allMessages.addAll(messages);

    ToolCallingChatOptions options = ToolCallingChatOptions.builder()
            .internalToolExecutionEnabled(false)
            .build();

    Prompt prompt = new Prompt(allMessages, options);

    // 使用流式响应
    AtomicReference<AssistantMessage> lastMessage = new AtomicReference<>();
    StringBuilder fullText = new StringBuilder();

    Flux<ChatResponse> responseFlux = chatClient.prompt(prompt)
            .toolCallbacks(tools)
            .stream()
            .chatResponse();

    // 处理流式响应
    responseFlux.doOnNext(response -> {
        if (response.getResult() != null && response.getResult().getOutput() != null) {
            AssistantMessage output = response.getResult().getOutput();

            // 累积文本内容
            String text = output.getText();
            if (text != null && !text.isEmpty()) {
                fullText.append(text);
                log.info("【流式文本】{}", text);
            }

            // 检测工具调用
            if (output.getToolCalls() != null && !output.getToolCalls().isEmpty()) {
                log.info("【检测到工具调用】");
            }

            // 保存最后一个消息
            lastMessage.set(output);
        }
    }).blockLast();

    // 构建完整消息
    if (lastMessage.get() != null) {
        AssistantMessage currentMessage = lastMessage.get();
        if ((currentMessage.getToolCalls() == null || currentMessage.getToolCalls().isEmpty())
                && fullText.length() > 0) {
            return AssistantMessage.builder()
                    .content(fullText.toString())
                    .build();
        }
    }

    return lastMessage.get();
}

流式处理的优势

  • 实时反馈:用户可以看到 AI 的思考过程
  • 更好体验:减少等待焦虑
  • 调试友好:清晰展示每一步执行

实战演示

接下来我们写个demo来看看这个基础的ReAct具体表现如何

@Component
public class SimpleReActRunner implements CommandLineRunner {

    private final LlmService llmService;
    private final ChatClient chatClient;

    public SimpleReActRunner(LlmService llmService) {
        this.llmService = llmService;
        this.chatClient = llmService.getChatClient(null);
    }

    @Override
    public void run(String... args) throws Exception {
        // 1. 准备工具
        CalculatorTools calculatorTools = new CalculatorTools();
        List<ToolCallback> tools = calculatorTools.getTools();

        // 2. 创建 ReAct Agent
        SimpleReActAgent agent = new SimpleReActAgent(chatClient, tools);

        // 3. 运行示例
        System.out.println("\n========== 示例 1: 简单加法 ==========");
        String answer1 = agent.run("计算 25 + 37 等于多少?");
        System.out.println("最终结果:" + answer1);

        System.out.println("\n========== 示例 2: 多步计算 ==========");
        String answer2 = agent.run("先计算 100 + 50,然后将结果乘以 2,最后除以 3;\n最终根据上面这个计算返回的结果,是奇数就查询武汉天气、是偶数则查询北京天气");
        System.out.println("最终结果:" + answer2);
    }
}

示例 1:简单加法

其中示例1,比较简单,两轮对话即可返回最终的结果,执行记录如下

输入计算 25 + 37 等于多少?

执行日志

========== 示例 1: 简单加法 ==========
╔════════════════════════════════════════╗
║       🚀 ReAct Agent 启动              ║
╚════════════════════════════════════════╝
💬 问题:计算 25 + 37 等于多少?
┌────────────────────────────────────────
│ 🔁 第 1 轮思考
└────────────────────────────────────────
[🔨] 执行加法:25.0 + 37.0
工具执行结果 add => 62.0
┌────────────────────────────────────────
│ 🔁 第 2 轮思考
└────────────────────────────────────────
 ✅ 无工具调用,直接返回结果:25 + 37 等于 62。
╔════════════════════════════════════════╗
║       ✨ ReAct 任务完成 ✨              ║
╚════════════════════════════════════════╝

最终结果:25 + 37 等于 62。

分析

  • AI 识别到需要加法运算
  • 调用 add 工具
  • 返回最终答案
A08-3.png
A08-3.png

示例 2:多步混合运算

输入先计算 100 + 50,然后将结果乘以 2,最后除以 3;根据计算结果,是奇数就查询武汉天气、是偶数则查询北京天气

执行日志

========== 示例 2: 多步计算 ==========
╔════════════════════════════════════════╗
║       🚀 ReAct Agent 启动              ║
╚════════════════════════════════════════╝
💬 问题:先计算 100 + 50,然后将结果乘以 2,最后除以 3;最终根据上面这个计算返回的结果,是奇数就查询武汉天气、是偶数则查询北京天气
┌────────────────────────────────────────
│ 🔁 第 1 轮思考
└────────────────────────────────────────
[🔨] 执行加法:100.0 + 50.0
工具执行结果 add => 150.0
┌────────────────────────────────────────
│ 🔁 第 2 轮思考
└────────────────────────────────────────
[🔨] 执行乘法:150.0 * 2.0
工具执行结果 multiply => 300.0
┌────────────────────────────────────────
│ 🔁 第 3 轮思考
└────────────────────────────────────────
[🔨] 执行除法:300.0 / 3.0
工具执行结果 divide => 100.0
┌────────────────────────────────────────
│ 🔁 第 4 轮思考
└────────────────────────────────────────
 ✅ 无工具调用,直接返回结果:根据计算结果,100 + 50 = 150,然后 150 × 2 = 300,最后 300 ÷ 3 = 100。100 是偶数,因此需要查询北京天气。
╔════════════════════════════════════════╗
║       ✨ ReAct 任务完成 ✨              ║
╚════════════════════════════════════════╝

最终结果:根据计算结果,100 + 50 = 150,然后 150 × 2 = 300,最后 300 ÷ 3 = 100。100 是偶数,因此需要查询北京天气。

分析

  • AI 正确分解了多步任务
  • 按顺序执行:加 → 乘 → 除 → 判断奇偶 → 查询天气
  • 展示了强大的逻辑推理能力

示例2相对来说更复杂一些,除了计算之外,还有天气查询的工具调用,相应的循环次数会更多一些

A08-4.png
A08-4.png

示例 3:复杂应用题

然后我们再扩展一下,使用一个需要使用计算的算钱的应用场景,看下这套ReAct的表现如何

System.out.println("\n========== 示例 3: 应用场景 ==========");
String answer3 = agent.run("小明有 50 元钱,买了 3 本书,每本书 12 元,花了 2.5 元买了一个包子,还剩多少钱?");
System.out.println("最终结果:" + answer3);

执行日志

========== 示例 3: 应用场景 ==========
╔════════════════════════════════════════╗
║       🚀 ReAct Agent 启动              ║
╚════════════════════════════════════════╝
💬 问题:小明有 50 元钱,买了 3 本书,每本书 12 元,花了 2.5 元买了一个包子,还剩多少钱?
┌────────────────────────────────────────
│ 🔁 第 1 轮思考
└────────────────────────────────────────
[🔨] 执行乘法:3.0 * 12.0
工具执行结果 multiply => 36.0
┌────────────────────────────────────────
│ 🔁 第 2 轮思考
└────────────────────────────────────────
[🔨] 执行减法:50.0 - 36.0
工具执行结果 subtract => 14.0
┌────────────────────────────────────────
│ 🔁 第 3 轮思考
└────────────────────────────────────────
 ✅ 无工具调用,直接返回结果:小明一开始有 50 元钱。他买了 3 本书,每本书 12 元,总共花费了 $3 \times 12 = 36$ 元。他还花了 2.5 元买了一个包子。因此,他总共花费了 $36 + 2.5 = 38.5$ 元。

小明剩下的钱为 $50 - 38.5 = 11.5$ 元。

最终答案:小明还剩 11.5 元。
╔════════════════════════════════════════╗
║       ✨ ReAct 任务完成 ✨              ║
╚════════════════════════════════════════╝

最终结果:小明一开始有 50 元钱。他买了 3 本书,每本书 12 元,总共花费了 $3 \times 12 = 36$ 元。他还花了 2.5 元买了一个包子。因此,他总共花费了 $36 + 2.5 = 38.5$ 元。

小明剩下的钱为 $50 - 38.5 = 11.5$ 元。

最终答案:小明还剩 11.5 元。

分析

  • AI 理解了应用题的场景
  • 第一步:计算总花费(3 × 12 = 36)
  • 第二步:计算剩余(50 - 36 = 14)
  • 第三步:返回结果,不调用工具进行结果返回
A08-5.webp
A08-5.webp

虽然上面的截图结果是准确的,但是在实际测试时,发现不同模型的表现差距还是很明显的🤣


关键技术要点总结

1. ReAct 循环三要素

阶段作用实现方式
Thinking分析情况,决定下一步调用大模型,传入对话历史和工具列表
Acting执行具体操作查找并调用匹配的工具
Observing获取操作结果捕获工具返回值,加入对话历史

2. 系统提示设计技巧

✅ 必须做的:
- 明确说明使用 ReAct 范式
- 列出可用工具
- 强调一次只调用一个工具
- 说明何时应该停止(信息足够时直接回答)

❌ 避免的:
- 模糊的指令
- 允许多工具并行调用
- 没有明确的终止条件

3. 工具调用控制

禁用自动执行是关键:

ToolCallingChatOptions options = ToolCallingChatOptions.builder()
    .internalToolExecutionEnabled(false)  // 👈 手动控制
    .build();

这样做的好处:

  • ✅ 完全掌控执行流程
  • ✅ 可以记录和调试
  • ✅ 灵活处理异常情况

4. 对话历史管理

List<Message> messages = new ArrayList<>();
messages.add(new UserMessage(question));           // 用户问题
messages.add(systemMessage);                       // 系统提示
messages.add(assistantMessage);                    // AI 思考
messages.add(toolResponseMessage);                 // 工具结果
// ... 不断追加,形成完整上下文

5. 流式处理核心价值

对比项非流式流式
用户等待时间短(实时可见)
调试友好度一般优秀
实现复杂度简单稍复杂
推荐场景快速原型生产环境

总结与展望

本文核心收获

本文通过一个实现了一个基础的ReActAgent来演示现在Agent中广为使用的ReAct范式,其核心思想是模仿人类解决问题的方式,将推理和行动结合起来,然后通过代码的方式进行演绎。虽然我们实现的还比较初级,但通过这个项目,至少会有一些几个收获:

  1. 理解了 ReAct 范式:Thinking → Acting → Observing 循环
  2. 掌握了实现方法:基于Spring AI 手动控制工具调用
  3. 学会了流式优化:提升用户体验的关键技巧
  4. 获得了实战经验:完整的代码示例和调试日志

进一步学习方向

如果我们想跟进一步的打造一个生产可用的ReActAgent,那么下面这些是我们不能绕过的点

  1. 更复杂的工具集:集成真实 API(天气、地图、支付等)
  2. 多 Agent 协作:多个 ReAct Agent 协同完成复杂任务
  3. 记忆系统:实现长期记忆和上下文学习
  4. 自我反思:让 Agent 能够评估和优化自己的行为
  5. 视觉能力:结合图像识别等多模态能力

思考题

尝试扩展这个项目:

  1. 添加一个 CalendarTools,支持日期计算和提醒设置
  2. 实现一个简单的搜索引擎工具,查询网络信息
  3. 设计一个数据库查询工具,支持 SQL 查询
  4. 创建一个 Web 自动化测试工具集

参考资源

项目源码

相关文档

零基础入门:

实战

推荐阅读


作者:一灰
日期:2026 年 3 月 4 日
标签:#SpringAI #ReAct #AI-Agent #Java #智能体编程


💬 互动环节:你在实践中遇到了什么问题?欢迎在评论区留言讨论!如果觉得这篇文章有帮助,请点赞 👍 收藏 ⭐ 转发 🔄 支持一下!

Loading...