#9、如何使用SpringAI实现自主规划智能体

Spring AI 实现拥有自主规划能力的智能体:从零手写一个 ReAct Agent(LoveManus)

本文基于 QianLiuLiang/QianLiuLiang_AI 中实现自主规划智能体的提交代码,代码以 com.qianliuliang.love.agent 包下的 5 个核心类为准:AgentStateBaseAgentReActAgentToolCallAgentLoveManus

技术栈:Spring Boot 3 + Java 21 + Spring AI + OpenAI 兼容大模型端点。 阅读建议:本文按"为什么 → 原理 → 设计 → 逐层代码 → 关键细节 → 总结"的路径,把整个实现思路完整还原一遍。


目录

  1. 为什么需要"自主规划"智能体
  2. [核心原理:CoT · Agent Loop · ReAct](#核心原理:CoT · Agent Loop · ReAct "#2-%E6%A0%B8%E5%BF%83%E5%8E%9F%E7%90%86cot--agent-loop--react")
  3. 整体实现思路:四层代理架构
  4. [数据模型设计:AgentState 状态机](#数据模型设计:AgentState 状态机 "#4-%E6%95%B0%E6%8D%AE%E6%A8%A1%E5%9E%8B%E8%AE%BE%E8%AE%A1agentstate-%E7%8A%B6%E6%80%81%E6%9C%BA")
  5. 逐层实现核心代码
  6. 终止工具与循环安全
  7. 关键实现细节与踩坑记录
  8. 完整运行示例
  9. 总结:自主规划能力的代价与取舍

1. 为什么需要"自主规划"智能体

1.1 一句话定义

自主规划智能体(Autonomous Planning Agent) ,也叫目标导向智能体,是指能够根据任务目标自主分解任务、制定计划、选择工具并一步步执行,直到完成任务的 AI 系统。

很多基于 Spring AI 实现的对话应用,本质上已经是一个"轻量智能体"------具备大模型决策、对话记忆、知识库检索、工具调用等能力。但它还不算真正的自主规划,因为它的执行路径是"一次提问 → 一次回答",缺少"持续自主执行"的能力。

两者的差异用一个对比就能看清:

能力维度 普通对话应用(反应式/有限规划) LoveManus(自主规划)
多轮推理 单次回答,最多一次工具调用 思考 → 行动 → 观察,循环往复
任务分解 依赖用户逐步引导 自主拆解为多步子任务
工具选择 模型一次性选好 每步动态决策,可跨工具接力
完成判定 回答完即结束 由模型通过 Terminate 工具自主结束
典型场景 问答、单步查询 "查资料 + 下载图片 + 生成 PDF"这类复合任务

1.2 为什么值得自己写一遍

市面上已经有 AutoGPT、Manus、OpenManus 这类成熟项目,为什么还要在 Spring AI 里手写一个?

  1. 理解本质:自主规划的核心不是某个框架的 API,而是"循环"这个机制。自己写一遍才能吃透 ReAct。
  2. 落地 Java 生态:OpenManus 是 Python 实现,Java 后端团队要接入,必须在 Spring AI 生态内做等价实现。
  3. 可控与可定制:框架托管的 Agent 循环是黑盒,自己实现才能做状态管理、循环检测、人工介入等定制。

关键洞察:自主规划 = 大模型决策能力 + Agent Loop 执行循环 + 工具调用能力。 大模型能力和工具调用我们通过 Spring AI 已经具备,缺的只是一个"让 AI 反复自主行动直到完成"的循环外壳------这正是本次提交要做的事。


2. 核心原理:CoT · Agent Loop · ReAct

在动手写代码前,先厘清三个支撑自主规划的技术原理。它们不是可选项,而是智能体的"大脑回路"。

2.1 CoT 思维链(Chain of Thought)

CoT 是让 AI 先思考后回答的提示技术。对于复杂任务,直接要答案容易出错,分步推理则准确率高得多。

它的实现极其简单------在系统提示词里要求模型按步骤思考:

text 复制代码
You are an assistant focused on Chain of Thought reasoning. For each question, please follow these steps:

1. Break down the problem: Divide complex problems into smaller, more manageable parts
2. Think step by step: Think through each part in detail, showing your reasoning process
3. Synthesize conclusions: Integrate the thinking from each part into a complete solution
4. Provide an answer: Give a final concise answer

在 LoveManus 中,CoT 的载体就是精心设计的 SYSTEM_PROMPTNEXT_STEP_PROMPT(后面 5.4 节会看到)。

2.2 Agent Loop 执行循环

Agent Loop 是智能体最核心的工作机制:在没有用户输入的情况下,自主重复执行"推理 + 工具调用"

传统聊天是一问一答就结束;而 Agent Loop 在 AI 回复后,可能继续自主执行下一个动作,形成循环,直到任务完成或达到最大步数。

2.3 ReAct 模式(Reasoning + Acting)

ReAct 是目前最主流、也是本次提交采用的智能体工作模式。它模仿人类解决问题的"思考 - 行动 - 观察"循环:

vbnet 复制代码
┌─────────────────────────────────────────────────────────────┐
│                     Agent Loop 主循环                          │
│                                                               │
│    ┌─────────────┐      ┌─────────────┐      ┌─────────────┐ │
│    │    Reason   │ ───▶ │     Act     │ ───▶ │   Observe   │ │
│    │   推理/思考  │      │    行动      │      │    观察      │ │
│    └─────────────┘      └─────────────┘      └─────────────┘ │
│         │ ① 拆解任务,决定下一步用什么工具                       │
│         │ ② 调用工具执行动作(搜索/抓取/下载/生成 PDF...)        │
│         │ ③ 把工具结果喂回模型,作为下一轮决策的依据              │
│         └─────────────────── 循环迭代 ◀──────────────────────┘ │
│                                                               │
│        当模型认为任务完成(调用 Terminate)→ 退出循环             │
└─────────────────────────────────────────────────────────────┘

三个动作缺一不可:

  1. 推理(Reason):把原始问题拆成多步任务,明确当前要做哪一步。
  2. 行动(Act):调用外部工具执行,比如网页搜索、打开网页、下载文件。
  3. 观察(Observe):拿到工具执行结果,作为下一步决策的输入。

大白话理解:就像人做一顿饭------先看菜谱决定做哪道菜(Reason),切菜下锅(Act),尝一口看看熟了没(Observe),没熟就再炒一会儿(循环),熟了就装盘上桌(Terminate)。


3. 整体实现思路:四层代理架构

理解了原理,接下来回答最关键的问题:整个实现是怎么设计出来的?

本次提交把"自主规划"拆成了四个层层递进的抽象,每一层只解决一个问题,越往下越具体:

scss 复制代码
┌─────────────────────────────────────────────────────────────┐
│                    BaseAgent(基类)                          │
│   职责:状态管理 + 执行循环(run/step/clean)                  │
│   ------ 定义"怎么循环",不知道"每步做什么"                        │
├─────────────────────────────────────────────────────────────┤
│                    ReActAgent(抽象类)                       │
│   职责:把 step() 拆成 think() + act() 两个模板方法            │
│   ------ 定义"思考-行动"骨架,不知道"怎么思考/怎么行动"             │
├─────────────────────────────────────────────────────────────┤
│                    ToolCallAgent(抽象类)                    │
│   职责:用 Spring AI 的工具调用能力实现 think() 和 act()       │
│   ------ 知道"怎么思考(问大模型)和怎么行动(执行工具)"           │
├─────────────────────────────────────────────────────────────┤
│                    LoveManus(具体实现)                      │
│   职责:装配工具 + 注入 Prompt + 配置模型,对外可直接调用       │
│   ------ 一个完整可用的自主规划智能体实例                          │
└─────────────────────────────────────────────────────────────┘

3.1 为什么这样分层?

这背后是模板方法设计模式(Template Method):父类定义算法的骨架与执行顺序,把某些步骤延迟到子类实现。

设计决策 理由
循环写在 BaseAgent 所有智能体的执行循环都一样,只需写一次
step() 设为抽象方法 不同的智能体"每一步做什么"不同
ReActAgent 再拆 think/act ReAct 是主流模式,把"思考"与"行动"解耦
ToolCallAgent 具体实现 工具调用是"行动"的通用实现,可复用
LoveManus 最底层 不同业务的差异收敛到工具与 Prompt,不改框架

3.2 工程结构

本次提交的核心代码位于 com.qianliuliang.love.agent 包下,职责一目了然:

bash 复制代码
com.qianliuliang.love.agent/
├── AgentState.java        # 状态枚举(IDLE/RUNNING/FINISHED/ERROR)
├── BaseAgent.java         # 基类:执行循环 + 状态管理 + 消息上下文
├── ReActAgent.java        # 思考-行动抽象类(模板方法)
├── ToolCallAgent.java     # 工具调用智能体(实现 think/act)
└── LoveManus.java         # 最终智能体实例(可注入使用)

com.qianliuliang.love.advisor/
└── MyLoggerAdvisor.java   # 自定义日志 Advisor(打印每次 LLM 调用)

从整体到局部地看代码,比一上来就啃单个类要高效得多。先把"谁继承谁、谁负责什么"画出来,再深入细节。


4. 数据模型设计:AgentState 状态机

自主规划需要一个"当前处于什么阶段"的明确标记,用来控制循环的进入、退出和异常处理。本次提交用枚举 AgentState 实现了一个轻量状态机:

java 复制代码
package com.qianliuliang.love.agent;

/**
 * 智能体状态
 */
public enum AgentState {

    /**
     * 空闲状态
     */
    IDLE,
    /**
     * 运行中
     */
    RUNNING,
    /**
     * 完成
     */
    FINISHED,
    /**
     * 异常/错误
     */
    ERROR,
    ;
}

四个状态的生命周期:

scss 复制代码
   IDLE ──run()──▶ RUNNING ──任务完成/达到最大步数──▶ FINISHED
                    │
                    └─────── 异常 ────────────────▶ ERROR
状态 含义 何时进入
IDLE 空闲,可接受新任务 初始状态
RUNNING 正在自主执行 调用 run()
FINISHED 任务完成 模型调用 Terminate 工具 / 达到最大步数
ERROR 执行出错 循环中抛出异常

设计要点:状态机让智能体的生命周期可观测、可控。这也是后面做"防死循环""人工介入"等增强功能的地基。


5. 逐层实现核心代码

下面从最底层开始,逐层还原核心代码。建议一边读一边想:这一层解决了上一层的什么问题?

5.1 BaseAgent ------ 执行循环的骨架

BaseAgent 是所有代理的基础,负责三件事:状态管理、消息上下文维护、多步执行循环。注释里明确写着"定义执行模板",是典型的模板方法。

java 复制代码
package com.qianliuliang.love.agent;

import cn.hutool.core.util.StrUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;

import java.util.ArrayList;
import java.util.List;

/**
 * 抽象基础代理类,用于管理代理状态和执行流程。
 * <p>
 * 提供状态转换、内存管理和基于步骤的执行循环的基础功能。
 * 子类必须实现step方法。
 */
@Data
@Slf4j
public abstract class BaseAgent {

    /**
     * 智能体名称
     */
    private String name;

    /**
     * 系统提示词
     */
    private String systemPrompt;

    /**
     * 下一步 提示词
     */
    private String nextStepPrompt;

    /**
     * 代理状态
     */
    private AgentState state = AgentState.IDLE;

    /**
     * 当前步骤
     */
    private int currentStep = 0;

    /**
     * 最大步骤上限
     */
    private int maxSteps = 10;

    /**
     * 对话客户端
     */
    private ChatClient chatClient;

    /**
     * 会话记忆 ,用户记录会话上下文
     */
    private List<Message> messageList = new ArrayList<>();

    /**
     * 运行代理
     * 定义执行模板
     *
     * @param userPrompt 用户提示词
     * @return 执行结果
     */
    public String run(String userPrompt) {
        // 1.基础校验
        if (this.state != AgentState.IDLE) {
            throw new RuntimeException("Cannot run agent from state: " + this.state);
        }
        if (StrUtil.isBlank(userPrompt)) {
            throw new RuntimeException("Cannot run agent with empty user prompt");
        }
        // 2.执行,更改agent状态
        this.state = AgentState.RUNNING;
        // 将用户提示词添加到上下文
        this.messageList.add(new UserMessage(userPrompt));
        // 初始化执行结果
        List<String> results = new ArrayList<>();
        try {
            while (currentStep < maxSteps && state != AgentState.FINISHED) {
                log.info("Executing step {}/{}", currentStep, maxSteps);
                // 单步执行
                String stepResult = step();
                String result = "Step " + currentStep + ": " + stepResult;
                // 将单步执行的执行结果存入执行结果里
                results.add(result);
                currentStep++;
            }
            // 检查是否超出步骤限制
            if (currentStep >= maxSteps) {
                state = AgentState.FINISHED;
                results.add("Terminated: Reached max steps (" + maxSteps + ")");
            }
            return String.join("\n", results);

        } catch (Exception e) {
            state = AgentState.ERROR;
            log.error("error executing agent", e);
            return "执行错误" + e.getMessage();
        } finally {
            // 3.清理资源
            this.clean();
        }
    }

    /**
     * 单个执行步骤
     *
     * @return
     */
    public abstract String step();

    /**
     * 清理资源
     */
    protected void clean() {
        // 子类可以重写此方法来清理资源
    }
}

这个类里有几个值得细品的设计:

  1. 注入 ChatClient 而非写死模型:由调用方传入具体的大模型客户端,智能体与具体模型解耦,可替换、可测试。
  2. messageList 自己维护上下文:不走 Spring AI 的 ChatMemory,因为 Agent 的循环需要精确控制每一步的消息,手动维护更透明。
  3. state 控制循环 :循环条件是 currentStep < maxSteps && state != FINISHED------只要任务没结束、步数没超,就继续自主执行。这是 Agent Loop 的 Java 形态。
  4. run() 是执行模板 :校验 → 置 RUNNING → 加入用户消息 → while 循环 → 单步 step() → 超限兜底 → 异常置 ERROR → finally clean()。整个流程在基类定死,子类只需实现 step()

小细节currentStep 从 0 开始计数,所以日志第一条是 Executing step 0/50clean() 方法(不是 cleanup())留给子类重写做资源释放。

5.2 ReActAgent ------ 思考-行动的模板方法

BaseAgent 定义了"循环",但没定义"每步做什么"。ReActAgent 把 step() 拆成了 think()act() 两个抽象方法,落实 ReAct 模式。

java 复制代码
package com.qianliuliang.love.agent;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;

/**
 * ReAct (Reasoning and Acting) 模式的代理抽象类
 * 实现了思考-行动的循环模式
 */
@EqualsAndHashCode(callSuper = true)
@Data
@Slf4j
public abstract class ReActAgent extends BaseAgent {

    /**
     * 思考下一步行动
     *
     * @return 是否需要执行行动, true表示要执行
     */
    public abstract boolean think();

    /**
     * 执行
     *
     * @return 执行的结果
     */
    public abstract String act();

    /**
     * 执行单个步骤
     *
     * @return
     */
    @Override
    public String step() {
        try {
            boolean shouldAct = think();
            if (shouldAct) {
                return act();
            }
            return "think finish, not do anything";
        } catch (Exception e) {
            log.error("单个操作执行失败");
            return "单个操作执行失败" + e.getMessage();
        }
    }
}

这里的模板方法模式非常典型:

scss 复制代码
step() 方法(模板,父类定义)
    ├── 1. think() ------ 问大模型"下一步该干什么"
    ├── 2. 判断 shouldAct ------ 需要行动才行动
    └── 3. act()   ------ 执行决定

think() 的返回值是个精妙设计:返回 boolean 表示"要不要行动" 。当模型觉得任务已经可以回答、不需要再调用工具时,think() 返回 falsestep() 就返回 "think finish, not do anything",循环不再执行 act()

5.3 ToolCallAgent ------ 把"工具调用"接进循环

这是最关键、也最"Spring AI"的一层。ToolCallAgent 继承 ReActAgent,真正实现了 think()act(),把 Spring AI 的工具调用能力接入了自主规划循环。

5.3.1 核心属性与构造方法
java 复制代码
package com.qianliuliang.love.agent;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.ToolResponseMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.tool.ToolCallingChatOptions;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.model.tool.ToolExecutionResult;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbackProvider;

import java.util.List;
import java.util.stream.Collectors;

/**
 * 处理工具调用的基础代理类,具体实现了 think 和 act 方法
 */
@EqualsAndHashCode(callSuper = true)
@Data
@Slf4j
public class ToolCallAgent extends ReActAgent {

    /**
     * 可用的工具集合
     */
    private final ToolCallback[] availableTools;

    /**
     * 工具回调提供者(用于动态/批量注册工具)
     */
    private final ToolCallbackProvider toolCallbackProvider;

    /**
     * 工具调用的响应结果
     */
    private ChatResponse toolCallChatResponse;

    /**
     * 工具调用管理器
     */
    private final ToolCallingManager toolCallingManager;

    /**
     * 聊天选项
     */
    private final ChatOptions chatOptions;

    /**
     * 构造器
     */
    public ToolCallAgent(ToolCallback[] availableTools, ToolCallbackProvider toolCallbackProvider) {
        super();
        this.availableTools = availableTools;
        this.toolCallbackProvider = toolCallbackProvider;
        this.toolCallingManager = ToolCallingManager.builder().build();
        this.chatOptions = ToolCallingChatOptions.builder()
                .toolCallbacks(this.availableTools)
                .internalToolExecutionEnabled(false) // 取消框架自动执行工具调用
                .build();
    }
    // ... think / act 见下文
}

注意这一行是本次实现最重要的决策

java 复制代码
ToolCallingChatOptions.builder()
        .toolCallbacks(this.availableTools)
        .internalToolExecutionEnabled(false) // 取消框架自动执行工具调用
        .build()

internalToolExecutionEnabled(false) 是 Spring AI 官方的禁用框架托管工具执行 开关。作用是:让大模型返回工具调用意图(ToolCall),但框架不自动执行,把执行权交给我们自己的 ReAct 循环。

为什么一定要禁掉框架托管?

Spring AI 的 ChatClient 默认会自动完成"LLM 请求工具 → 执行工具 → 结果回填 → 再请求 LLM"的整条循环。如果我们不禁掉它,外层再加一层 ReAct 循环,就会变成"套娃",think() 里一次调用可能内部就自动执行了多次工具,流程完全失控。自主实现 Agent 的第一步,就是夺回循环控制权

5.3.2 think() ------ 推理:问大模型"下一步干什么"
java 复制代码
    /**
     * 思考的实现
     * 大模型根据提示词(含消息历史)进行思考,思考需要调用哪些工具(并未实际调用工具)
     *
     * @return
     */
    @Override
    public boolean think() {
        //1.校验提示词
        if (StrUtil.isNotBlank(getNextStepPrompt())) {
            UserMessage userMessage = new UserMessage(getNextStepPrompt());
            getMessageList().add(userMessage);
        }
        //2.调用大模型
        Prompt prompt = new Prompt(getMessageList(), this.chatOptions);
        try {
            ChatResponse chatResponse = getChatClient().prompt(prompt)
                    .system(getSystemPrompt())
                    .tools(availableTools)
                    .tools(toolCallbackProvider)
                    .options(chatOptions)
                    .call()
                    .chatResponse();
            // 记录响应
            this.toolCallChatResponse = chatResponse;
            //3.解析工具调用结果
            AssistantMessage assistantMessage = chatResponse.getResult().getOutput();//助手消息
            // 获取要调用的工具列表
            List<AssistantMessage.ToolCall> toolCalls = assistantMessage.getToolCalls();
            String result = assistantMessage.getText();//助手消息
            log.info(getName() + "的思考:" + result);
            log.info(getName() + "选择了" + toolCalls.size() + "个工具来使用");
            String toolCallInfo = toolCalls.stream()
                    .map(toolCall -> String.format("工具名称:%s,参数:%s", toolCall.name(), toolCall.arguments()))
                    .collect(Collectors.joining("\n"));
            log.info(toolCallInfo);// 打印 具体选择了哪些工具
            // 如果不需要调用工具
            if (CollUtil.isEmpty(toolCalls)) {
                // 将助手消息添加到会话列表中
                getMessageList().add(assistantMessage);
                return false;
            } else {
                // 需要调用工具时,不需要记录助手消息,在调用工具时会自动记录
                log.info(getName() + "思考完成。》》》》》》》》》");
                return true;
            }
        } catch (Exception e) {
            log.error(getName() + "在思考过程中遇到了问题:" + e.getMessage());
            getMessageList().add(new AssistantMessage("处理时遇到了问题:" + e.getMessage()));
            return false;
        }
    }

think() 做了三件事:

  1. 注入下一步提示 :把 nextStepPrompt(引导模型继续行动的提示词)作为 UserMessage 加入上下文。
  2. 调用大模型并传入工具清单chatClient.prompt(prompt).system(systemPrompt).tools(availableTools).tools(toolCallbackProvider) ------ 把系统提示、历史上下文、全部可用工具 Schema 一起交给模型,问它"下一步怎么做"。注意这里同时注册了 ToolCallback[]ToolCallbackProvider 两个来源的工具
  3. 解析模型的工具调用意图 :从 AssistantMessage.getToolCalls() 拿到模型想要调用的工具列表。
    • 有工具调用 → 返回 true,进入 act() 执行工具;
    • 没有工具调用 → 说明模型准备直接回答了,把助手消息记入上下文,返回 false

设计细节 :注意消息上下文的记录时机------只有不调用工具时才手动记录助手消息 。因为一旦有工具调用,后面的 act() 通过 conversationHistory() 拿到的结果里已经包含了助手消息,再记就重复了(这条坑在 7.3 节还会重点讲)。

5.3.3 act() ------ 行动:执行工具并回填结果
java 复制代码
    /**
     * 行动/执行
     *
     * @return 执行的结果
     */
    @Override
    public String act() {
        if (!toolCallChatResponse.hasToolCalls()) {
            return "不需要调用工具";
        }
        // 执行工具
        Prompt prompt = new Prompt(getMessageList(), chatOptions);
        ToolExecutionResult toolExecutionResult = toolCallingManager.executeToolCalls(prompt, toolCallChatResponse);
        // 记录消息上下文,conversationHistory 已经包含了助手消息和工具调用返回的结果
        setMessageList(toolExecutionResult.conversationHistory());
        ToolResponseMessage toolResponseMessage = (ToolResponseMessage) CollUtil.getLast(toolExecutionResult.conversationHistory());
        // 判断是否执行了终止工具
        boolean terminateToolCalled = toolResponseMessage.getResponses().stream()
                .anyMatch(response -> response.name().equals("doTerminate"));
        if (terminateToolCalled) {
            // 任务结束,更改状态
            setState(AgentState.FINISHED);
        }
        String results = toolResponseMessage.getResponses().stream()
                .map(response -> "工具 " + response.name() + " 返回的结果:" + response.responseData())
                .collect(Collectors.joining("\n"));
        log.info(results);
        return results;
    }

act() 的核心是 ToolCallingManager.executeToolCalls() 。这是 Spring AI 暴露给开发者"手动控制工具执行"的入口:给它一份完整上下文 + 上一次模型返回的工具调用意图,它替你完成------执行工具、把工具响应封装成 ToolResponseMessage、并把"助手消息 + 工具响应"拼接成新的 conversationHistory

scss 复制代码
act() 执行流程
    │
    ▼
① 校验:toolCallChatResponse 里确实有工具调用
    │
    ▼
② toolCallingManager.executeToolCalls(prompt, toolCallChatResponse)
    │    ├── 执行每个被请求的工具
    │    ├── 把结果封装成 ToolResponseMessage
    │    └── 返回 conversationHistory(助手消息 + 工具响应)
    │
    ▼
③ setMessageList(conversationHistory) ------ 把新上下文接回智能体
    │
    ▼
④ 检查是否调用了 doTerminate 终止工具 → 是则置状态 FINISHED
    │
    ▼
⑤ 解析结果并记录日志

执行完后,智能体的 messageList 已经包含了这一轮的"思考 + 工具结果"。下一轮循环的 think() 会带着这些新信息重新问模型------观察(Observe)环节就这样完成了

5.3.4 终止循环的关键:识别 doTerminate

自主规划必须让模型自己决定什么时候收手 。本次提交在 act() 末尾检查工具响应里是否包含 doTerminate 工具,一旦命中就把智能体状态置为 FINISHED,下一轮 while 判断就会退出循环:

java 复制代码
// 判断是否执行了终止工具
boolean terminateToolCalled = toolResponseMessage.getResponses().stream()
        .anyMatch(response -> response.name().equals("doTerminate"));
if (terminateToolCalled) {
    // 任务结束,更改状态
    setState(AgentState.FINISHED);
}

为什么用"工具"而不是"特殊输出标记"来结束? 因为工具调用是模型最擅长的结构化表达方式。让"结束任务"成为一个可被模型自主选择调用的工具,比让模型在自由文本里输出某个暗号可靠得多。

5.4 LoveManus ------ 最终可用的超级智能体

有了前三层的骨架,最后一步就是装配。LoveManus 继承 ToolCallAgent,只需要配置:工具、模型、提示词、最大步数

java 复制代码
package com.qianliuliang.love.agent;

import com.qianliuliang.love.advisor.MyLoggerAdvisor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

/**
 * 超级智能体(拥有自主规划能力)
 */
@Component
public class LoveManus extends ToolCallAgent {

    /**
     * 构造器
     *
     * @param toolCallbacks
     * @param deepChatModel
     */
    public LoveManus(ToolCallback[] toolCallbacks,
                     ToolCallbackProvider toolCallbackProvider,
                     @Qualifier("openAiChatModel") ChatModel deepChatModel) {
        super(toolCallbacks, toolCallbackProvider);
        this.setName("LoveManus");
        String SYSTEM_PROMPT = """
                You are LoveManus, an all-capable AI assistant, aimed at solving any task presented by the user.
                You have various tools at your disposal that you can call upon to efficiently complete complex requests.
                """;
        this.setSystemPrompt(SYSTEM_PROMPT);
        String NEXT_STEP_PROMPT = """
                Based on user needs, proactively select the most appropriate tool or combination of tools.
                For complex tasks, you can break down the problem and use different tools step by step to solve it.
                After using each tool, clearly explain the execution results and suggest the next steps.
                If you want to stop the interaction at any point, use the `terminate` tool/function call.
                """;
        this.setNextStepPrompt(NEXT_STEP_PROMPT);
        this.setMaxSteps(50);
        // 初始化 AI 对话客户端
        ChatClient chatClient = ChatClient.builder(deepChatModel)
                .defaultAdvisors(new MyLoggerAdvisor())
                .build();
        this.setChatClient(chatClient);
    }
}

这里藏着几条关键实现思路:

配置 作用 实现思路
SYSTEM_PROMPT 定义智能体的身份与总目标 一句话让模型知道"你是全能助手,可以用工具解决任何任务"
NEXT_STEP_PROMPT 定义每一步的决策策略 引导模型"主动选工具、复杂任务拆步做、每步后解释结果、需要时调用 terminate"
setMaxSteps(50) 最大循环步数 防止模型陷入无限循环,是自主规划的安全阀
@Qualifier("openAiChatModel") 指定注入的模型 Bean 通过 Spring 的 Qualifier 明确选择 OpenAI 兼容模型端点
defaultAdvisors(new MyLoggerAdvisor()) 打印每次 LLM 调用的详细日志 便于开发期观察每一步的思考过程
super(toolCallbacks, toolCallbackProvider) 同时传入两路工具来源 静态工具数组 + 动态工具 Provider 双保险

关键洞察:自主规划智能体的"人格",其实全写在 Prompt 里。 代码骨架负责"循环和工具",而"怎么拆任务、怎么选工具、什么时候收手"这些智能行为,全靠 SYSTEM_PROMPT + NEXT_STEP_PROMPT 引导出来。Prompt 设计在自主规划智能体中的地位,不亚于代码本身。


6. 终止工具与循环安全

6.1 终止工具:自主规划的安全阀

ToolCallAgent 在 act() 中识别名为 doTerminate 的工具。对应的工具实现通常长这样:

java 复制代码
public class TerminateTool {

    @Tool(description = """
            Terminate the interaction when the request is met OR if the assistant cannot proceed further with the task.
            "When you have finished all the tasks, call this tool to end the work.
            """)
    public String doTerminate() {
        return "任务结束";
    }
}

这个工具的描述词值得学习:它明确告诉模型两个使用时机------"任务已满足时"以及"无法继续推进时"。这就把"何时结束"的决策权完整交给了模型。

6.2 循环安全:两道防线

自主规划最大的隐患是无限循环------模型可能反复调用同一个工具而不收手。本次提交用两道防线兜底:

防线 机制 兜底层级
maxSteps = 50 循环步数硬上限,超出即强制结束 代码级保险
doTerminate 工具 由模型自主判断何时收手 模型级决策

两道防线缺一不可:maxSteps 是最后一道物理闸门,doTerminate 则是让"结束"这件事成为模型能力的一部分,二者配合才能在"任务完成就停、失控也能停"之间取得平衡。

进阶思路:还可以参考 OpenManus 增加"死循环检测"------比较最近几步的回复内容是否重复,重复到一定阈值就注入提示词"请更换策略,避免重复无效路径"。这是把"防失控"从被动上限升级为主动干预。


7. 关键实现细节与踩坑记录

这一节是本次提交里最"值钱"的部分------很多问题不实际跑一遍根本发现不了。

7.1 禁用框架托管工具调用:internalToolExecutionEnabled(false)

:如果直接用 Spring AI 默认的 ChatClient 工具调用,框架会自动完成"请求工具 → 执行工具 → 结果回填 → 再次请求"的整条循环。在自主规划场景下,这会造成循环套娃 :外层 ReAct 循环调一次 think(),内部框架又自动执行了多次工具,流程完全不可控。

正解 :在 ToolCallingChatOptions 中显式关闭框架自动执行:

java 复制代码
this.chatOptions = ToolCallingChatOptions.builder()
        .toolCallbacks(this.availableTools)
        .internalToolExecutionEnabled(false) // 取消框架自动执行工具调用
        .build();
写法 效果
默认(开启托管) 框架自动执行工具,无法掌控循环 ❌
internalToolExecutionEnabled(false) 模型只返回工具调用意图,执行权交回自主循环 ✅

为什么? internalToolExecutionEnabled(false) 让大模型返回的是"调用意图"(ToolCall)而不是"执行结果"。这个意图被存在 toolCallChatResponse 里,交给后面的 act() 通过 ToolCallingManager 手动执行。

换模型供应商时注意:不同供应商的 ChatModel 对"禁用托管"的支持方式可能不同(部分厂商需要开启自己的"代理工具调用"开关)。务必确认你使用的模型对应的禁用方式。

7.2 为什么手动执行工具,而不是用 ChatClient 的自动循环

Spring AI 的 ChatClient 支持一行代码完成工具调用:

java 复制代码
String result = chatClient.prompt()
    .user("帮我查下北京的天气")
    .call()
    .content();  // 框架内部自动循环处理工具

那为什么还要绕一大圈用 ToolCallingManager

因为自动循环是个"黑盒"。它适合"工具调用"这个单一场景,但做不了自主规划需要的三件事:

  1. 每一步的中间状态可观测:我们需要在 think 和 act 之间插入日志、判断、甚至人工确认。
  2. 循环条件可控:自主规划的结束条件(doTerminate 工具)由模型决策,不是固定由框架决定。
  3. 上下文精确管理:消息的增删时机必须精确可控,黑盒循环无法做到。

所以本次实现拆成两段:think()ChatClient 拿模型决策(此时不执行工具),act()ToolCallingManager.executeToolCalls() 手动执行工具。

7.3 消息上下文:千万不要重复添加

ToolCallingManager.executeToolCalls() 返回的 conversationHistory() 已经包含

  • 本轮模型的助手消息(AssistantMessage)
  • 工具调用的响应消息(ToolResponseMessage)

因此 act() 里必须直接 setMessageList(...) 整体替换,而不是 addAll 追加------否则每轮循环消息都会翻倍,上下文迅速膨胀,Token 消耗爆炸,甚至导致模型"失忆"。

java 复制代码
// ✅ 正确:整体替换
setMessageList(toolExecutionResult.conversationHistory());

// ❌ 错误:重复追加(历史 + 新历史会叠加)
getMessageList().addAll(toolExecutionResult.conversationHistory());

同理,think() 里也做了对称处理:有工具调用时不提前记录助手消息 ,留给 act()conversationHistory() 统一处理;只有"不需要调用工具"(模型准备直接回答)时才手动记录。

7.4 两路工具来源:ToolCallback\[\] + ToolCallbackProvider

think() 里同时注册了两种工具来源:

java 复制代码
.tools(availableTools)            // 静态工具数组
.tools(toolCallbackProvider)      // 动态工具提供者

这种设计的价值在于:静态工具数组 适合在构造时确定、数量固定的工具(如文件操作、PDF 生成);ToolCallbackProvider 适合需要动态发现、按需加载的工具(如通过 Spring 容器注入的工具 Bean)。两者互补,扩展新工具时不需要改动核心循环。


8. 完整运行示例

8.1 编写测试

给 LoveManus 一个真实的复合任务,验证自主规划能力:

java 复制代码
@SpringBootTest
class LoveManusTest {

    @Resource
    private LoveManus loveManus;

    @Test
    void run() {
        String userPrompt = """
                我的另一半居住在上海静安区,请帮我找到 5 公里内合适的约会地点,
                并结合一些网络图片,制定一份详细的约会计划,
                并以 PDF 格式输出""";
        String answer = loveManus.run(userPrompt);
        Assertions.assertNotNull(answer);
    }
}

8.2 预期的执行轨迹

以 Debug 模式运行,配合 MyLoggerAdvisor 可以看到智能体一步步"思考-行动-观察":

arduino 复制代码
Executing step 0/50
思考:需要先搜索静安区 5 公里内的约会地点 → 选择了1个工具来使用
    工具名称:WebSearchTool,参数:{...}
工具 WebSearchTool 返回的结果:找到 8 个热门约会地点...
Executing step 1/50
思考:需要进一步了解候选地点的特色 → 选择了1个工具来使用
    工具名称:WebScrapingTool,参数:{...}
工具 WebScrapingTool 返回的结果:星巴克臻选烘焙工坊的特色是...
Executing step 2/50
思考:需要为约会计划配图 → 选择了1个工具来使用
    工具名称:ResourceDownloadTool,参数:{...}
工具 ResourceDownloadTool 返回的结果:图片已下载到 ...
Executing step 3/50
思考:现在整理成 PDF 约会计划 → 选择了1个工具来使用
    工具名称:PdfGenerationTool,参数:{...}
工具 PdfGenerationTool 返回的结果:PDF 已生成到 ...
Executing step 4/50
思考:任务已完成 → 选择了1个工具来使用
    工具名称:TerminateTool,参数:{...}
[状态置为 FINISHED,while 循环退出]

整个过程中没有用户参与,模型自主完成了"搜索 → 抓取 → 下载 → 生成 PDF → 结束"的完整链路------这就是自主规划智能体最直观的体现。

8.3 跑起来会发现的现实问题

现象 原因 对策
执行步数不固定 LLM 决策有随机性 正常现象,接受并设计兜底
非常耗 Token 每步都可能调用一次大模型 按需使用,慎用复杂任务
偶发无限循环 模型反复调用同一工具 靠 maxSteps + doTerminate 兜底,进阶可加循环检测

经过实际测试会发现:自主规划智能体虽然能完成复杂任务,但非常浪费 tokens,并且可能陷入无限循环。 所以一定要按需使用,并在生产环境加上完善的兜底与监控。


9. 总结:自主规划能力的代价与取舍

9.1 一张图回顾整个实现思路

scss 复制代码
用户提出复杂任务
       │
       ▼
┌─────────────────────────────────────────────────────────────┐
│ BaseAgent.run()                                             │
│   └── while (currentStep < maxSteps && state != FINISHED)   │
│         ├── 记录日志:Executing step {currentStep}/{maxSteps}│
│         └── ReActAgent.step()                               │
│               ├── ToolCallAgent.think()  → 问 LLM 下一步     │
│               │       ├── 有工具调用? → 记录意图,返回 true  │
│               │       └── 无工具调用? → 记录消息,返回 false │
│               │                                              │
│               └── ToolCallAgent.act()  → 手动执行工具        │
│                       ├── ToolCallingManager.executeToolCalls│
│                       ├── conversationHistory 回填上下文     │
│                       └── 调用了 doTerminate? → FINISHED     │
└─────────────────────────────────────────────────────────────┘
       │
       ▼
返回分步执行结果

9.2 本次提交教会我们的三件事

  1. 自主规划 = 循环 + 工具 + 提示词 。代码骨架(BaseAgent → ReActAgent → ToolCallAgent)解决"怎么循环"和"怎么用工具";而真正让模型"会规划"的,是 SYSTEM_PROMPTNEXT_STEP_PROMPT 和 Terminate 工具这三样提示设计。

  2. 用框架,但要知道何时绕过框架 。Spring AI 的自动工具调用很强大,但自主规划需要循环控制权,所以用 ToolCallingChatOptions.internalToolExecutionEnabled(false) + ToolCallingManager 手动接管。"什么时候用框架,什么时候夺回控制权"是 AI 应用开发的分水岭。

  3. 自主性必须用约束换安全 。无限循环、Token 失控是自主规划的天生代价。maxStepsdoTerminate 工具、消息去重......所有工程手段,本质都是给自由加安全边际

9.3 进阶方向

  • 防死循环:对比最近几步回复,重复即注入"更换策略"提示。
  • 人工介入:实现 AskHuman 工具,让模型在关键决策点询问用户。
  • 多智能体协作:参考 A2A 协议与 Spring AI Alibaba Graph,把多个专业 Agent 编排成工作流。
  • 状态持久化 :把 messageList 持久化到数据库,支持中断恢复与多轮长任务。

自主规划是 AI 应用从"问答工具"走向"数字员工"的关键一步。理解它的原理、成本与边界,才能在实际项目中做出正确的取舍。


附:核心类关系一览

java 复制代码
AgentState(枚举) ◀── BaseAgent.state
                          ▲
                          │ extends
                     ReActAgent(抽象)
                          ▲
                          │ extends
                   ToolCallAgent(抽象)
                          │  ├── ToolCallback[] availableTools
                          │  ├── ToolCallbackProvider toolCallbackProvider
                          │  ├── ToolCallingManager toolCallingManager
                          │  └── ChatOptions chatOptions
                          ▲
                          │ extends
                      LoveManus(@Component)
                          ├── ChatClient chatClient(openAiChatModel + MyLoggerAdvisor)
                          ├── SYSTEM_PROMPT
                          ├── NEXT_STEP_PROMPT
                          └── maxSteps = 50

参考

相关推荐
fhhdzw1 小时前
别让 AI 替你理解代码
ai编程
前端小付2 小时前
我做了一个多 Agent 智能协作软件:让 AI 不再单打独斗
ai编程
山间小僧12 小时前
「AI学习笔记」Loop Engineering 和 Graph Engineering
langchain·agent·ai编程
大侠Luffy13 小时前
我开源了一个 Agent Skill:一键把播客生成小红书帖子
agent·ai编程·vibecoding
Jackson__13 小时前
从 LLM 到 Agent:一篇文章搞懂 AI 圈热词!
前端·agent·ai编程
寅时码13 小时前
我的 AI 工作流写了两年,直到 Opus 4.8 才真正生效
openai·ai编程·claude
落子AI15 小时前
智谱GLM-4.5编程智能体深度实测:355B MoE架构如何重塑AI编程体验
大模型·ai编程·智能体·glm-4.5·ai工具推荐
晴天小庭16 小时前
介绍下本人开发的OpenCode开源多模态插件——analyze-image
openai·ai编程
唐老板17 小时前
Meta Muse Code 发布:低价杀入编程
ai编程