搭建TravelManus旅游智能体,附完整代码+测试案例
智能体(Agent)是一种具备自主决策和执行能力的AI系统,能够感知环境、分解任务、调用工具并持续优化。它通过"思考-行动-观察"的循环机制(ReAct模式)完成任务,相比传统AI具有更强的规划能力。智能体可分为三类:反应式(简单应答)、有限规划(预设路径执行)和自主规划(完全自主分解任务)。实现智能体需要大模型、记忆系统、知识库和工具调用等技术支持,核心机制包括思维链(CoT
什么是智能体?
智能体(Agent)是一个能够感知环境、进行推理、制定计划、做出决策并自主采取行动以实现特定目标的 AI 系统。它以大语言模型为核心,集成 记忆、知识库和工具 等能力为一体,构造了完整的决策能力、执行能力和记忆能力,就像一个有主观能动性的人类一样。
与普通的 AI 大模型不同,智能体能够:
-
感知环境:通过各种输入渠道获取信息(多模态),理解用户需求和环境状态
-
自主规划任务步骤:将复杂任务分解为可执行的子任务,并设计执行顺序
-
主动调用工具完成任务:根据需要选择并使用各种外部工具和 API,扩展自身能力边界
-
进行多步推理:通过思维链(Chain of Thought)逐步分析问题并推导解决方案
-
持续学习和记忆过去的交互:保持上下文连贯性,利用历史交互改进决策
-
根据环境反馈调整行为:根据执行结果动态调整策略,实现闭环优化
大多数同学第一次感受到智能体应该是 “深度思考” 功能,这是 AI 逐步智能化的体现:
智能体的分类
跟人的生长阶段一样,智能体也是可以不断进化的。按照自主性和规划能力,智能体可以分为几个层次:
1)反应式智能体:仅根据当前输入和固定规则做出反应,类似简单的聊天机器人,没有真正的规划能力。23 年时的大多数 AI 聊天机器人应用,几乎都是反应式智能体。
2)有限规划智能体:能进行简单地多步骤执行,但执行路径通常是预设的或有严格限制的。鉴定为 “能干事、但干不了复杂的大事”。24 年流行的很多可联网搜索内容、调用知识库和工具的 AI 应用,都属于这类智能体。比如 ChatGPT + Plugins
3)自主规划智能体:也叫目标导向智能体,能够根据任务目标自主分解任务、制定计划、选择工具并一步步执行,直到完成任务。
比如 25 年初很火的 Manus 项目,它的核心亮点在于其 “自主执行” 能力。据官方介绍,Manus 能够在虚拟机中调用各种工具(如编写代码、爬取数据)完成任务。其应用场景覆盖旅行规划、股票分析、教育内容生成等 40 余个领域,所以在当时给了很多人震撼感。
但其实早在这之前,就有类似的项目了,比如 AutoGPT,所以 Manus 大火的同时也被人诟病 “会营销而已”。甚至没隔多久就有小团队开源了 Manus 的复刻版 —— OpenManus,这类智能体通过 “思考 - 行动 - 观察” 的循环模式工作,能够持续推进任务直至完成目标。
需要注意,自主规划能力是智能体发展的重要方向,但并非所有应用场景都需要完全的自主规划能力。在某些场景中,限制智能体的自主性反而能提高效率和安全性。
二、智能体实现关键技术
在自主开发智能体前,我们要先了解一下智能体的关键实现技术,也就是方案设计阶段做的事情。
CoT 思维链
CoT(Chain of Thought)思维链是一种让 AI 像人类一样 “思考” 的技术,帮助 AI 在处理复杂问题时能够按步骤思考。对于复杂的推理类问题,先思考后执行,效果往往更好。而且还可以让模型在生成答案时展示推理过程,便于我们理解和优化 AI。
CoT 的实现方式其实很简单,可以在输入 Prompt 时,给模型提供额外的提示或引导,比如 “让我们一步一步思考这个问题”,让模型以逐步推理的方式生成回答。还可以运用 Prompt 的优化技巧 few shot,给模型提供包含思维链的示例问题和答案,让模型学习如何构建自己的思维链。
在 OpenManus 早期版本中,可以看到实现 CoT 的系统提示词:
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 Your response should follow this format: Thinking: [Detailed thought process, including problem decomposition, reasoning for each step, and analysis] Answer: [Final answer based on the thought process, clear and concise] Remember, the thinking process is more important than the final answer, as it demonstrates how you reached your conclusion.
Agent Loop 执行循环
Agent Loop 是智能体最核心的工作机制,指智能体在没有用户输入的情况下,自主重复执行推理和工具调用的过程。
在传统的聊天模型中,每次用户提问后,AI 回复一次就结束了。但在智能体中,AI 回复后可能会继续自主执行后续动作(如调用工具、处理结果、继续推理),形成一个自主执行的循环,直到任务完成(或者超出预设的最大步骤数)。
Agent Loop 的实现很简单,参考代码如下:
public String execute() {
List<String> results = new ArrayList<>();
while (currentStep < MAX_STEPS && !isFinished) {
currentStep++;
String stepResult = executeStep();
results.add("步骤 " + currentStep + ": " + stepResult);
}
if (currentStep >= MAX_STEPS) {
results.add("达到最大步骤数: " + MAX_STEPS);
}
return String.join("\n", results);
}
ReAct 模式
ReAct(Reasoning + Acting)是一种结合推理和行动的智能体架构,它模仿人类解决问题时 ” 思考 - 行动 - 观察” 的循环,目的是通过交互式决策解决复杂任务,是目前最常用的智能体工作模式之一。
核心思想:
-
推理(Reason):将原始问题拆分为多步骤任务,明确当前要执行的步骤,比如 “第一步需要打开编程导航网站”。
-
行动(Act):调用外部工具执行动作,比如调用搜索引擎、打开浏览器访问网页等。
-
观察(Observe):获取工具返回的结果,反馈给智能体进行下一步决策。比如将打开的网页代码输入给 AI。
-
循环迭代:不断重复上述 3 个过程,直到任务完成或达到终止条件。
示例实现代码:
void executeReAct(String task) {
String state = "开始";
while (!state.equals("完成")) {
String thought = "思考下一步行动";
System.out.println("推理: " + thought);
String action = "执行具体操作";
System.out.println("行动: " + action);
String observation = "观察执行结果";
System.out.println("观察: " + observation);
state = "完成";
}
}
所需支持系统
除了基本的工作机制外,智能体的实现还依赖于很多支持系统。
1)首先是 AI 大模型,这个就不多说了,大模型提供了思考、推理和决策的核心能力,越强的 AI 大模型通常执行任务的效果越好。
2)记忆系统
智能体需要记忆系统来存储对话历史、中间结果和执行状态,这样它才能够进行连续对话并根据历史对话分析接下来的工作步骤。之前我们学习过如何使用 Spring AI 的 ChatMemory 实现对话记忆。
3)知识库
尽管大语言模型拥有丰富的参数知识,但针对特定领域的专业知识往往需要额外的知识库支持。之前我们学习过,通过 RAG 检索增强生成 + 向量数据库等技术,智能体可以检索并利用专业知识回答问题。
4)工具调用
工具是扩展智能体能力边界的关键,智能体通过工具调用可以访问搜索引擎、数据库、API 接口等外部服务,极大地增强了其解决实际问题的能力。当然,MCP 也可以算是工具调用的一种。
综合上面 4 类技术,并且结合 CoT、Agent Loop、ReAct 等机制(可以总称为 “规划执行机制”),我们就可以构建一个完整的、有自主规划能力的智能体系统啦。
使用SpringAI实现智能体
定义支持类型
定义AgentState枚举
首先我们在主项目travel-ai-agent新建包agent.enums用于存放我们智能体需要用到的相关枚举类型,并在此包创建AgentState枚举类型
public enum AgentState {
// 闲置、运行中、完成、错误
IDLE,
RUNNING,
FINISHED,
ERROR
}
实现核心架构
我们需要设计如下几个类型:
- BaseAgent:智能体的抽象基类
- ReActAgent:实现思考和行动两个步骤的智能体
- ToolCallAgent:实现工具调用能力的智能体
- TravelManus:最终生成旅游智能体Manus
开发BaseAgent抽象基类
在agent包下新建一个BaseAgent.java类
@Slf4j
@Data
public abstract class BaseAgent {
// Agent唯一标识
protected String chatId = UUID.randomUUID().toString();
// Agent名称
protected String name;
// 系统提示词
private String systemPrompt;
// 下一个步骤提示词
private String nextStepPrompt;
// 大模型客户端
protected ChatClient chatClient;
// Memory
protected ChatMemory chatMemory = new InMemoryChatMemory();
// Agent状态
protected AgentState state = AgentState.IDLE;
// 最大执行步骤
private int maxSteps = 10;
// 当前执行步骤
private int currentStep = 0;
// 执行第一个步骤,要求子类必须实现
public abstract String step();
// 运行agent智能体,要求用户输入提示词
public String run(String userPrompt){
if (this.state != AgentState.IDLE){
throw new RuntimeException("当前Agent正在运行中,请勿重复运行!");
}
if (StrUtil.isBlank(userPrompt)){
throw new RuntimeException("请输入用户提示词!");
}
// 开始记录上下文
chatMemory.add(chatId,new UserMessage(userPrompt));
// 保存结果列表
List<String> results = new ArrayList<>();
// 更改状态
this.state = AgentState.RUNNING;
try{
for (int i = 0; i < maxSteps && state != AgentState.FINISHED; i++) {
currentStep = i + 1;
log.info("开始执行第{}步...",currentStep);
// 执行当前步骤并记录结果
String stepResult = step();
String result = "Step " + currentStep + ": " + stepResult;
results.add(result);
}
if(currentStep >= maxSteps){
state = AgentState.FINISHED;
results.add("已执行最大步骤数,任务结束!,最大步数为"+maxSteps);
}
} catch (Exception e) {
state = AgentState.ERROR;
results.add("执行出错,请检查!");
log.error("执行出错,请检查!",e);
}finally {
cleanResources();
}
return StrUtil.join("\n",results);
}
private void cleanResources() {
}
}
开发ReActAgent类
在agent包下新建ReActAgent.java类
继承自BaseAgent,并且将step方法分解为think和act两个抽象方法。
@EqualsAndHashCode(callSuper = true)
@Data
@Slf4j
public abstract class ReActAgent extends BaseAgent {
/*
* 思考方法
* 根据当前状态和目标,决定下一步行动
* @return 是否执行act()
* */
public abstract boolean think();
/*
* 执行方法
* 根据think()的结果,执行下一步行动
* */
public abstract String act();
@Override
public String step() {
try{
// 先think思考,根据结果执行act()
if (think()){
return act();
}else {
return "思考完成 - 无需行动";
}
} catch (Exception e) {
log.error("执行步骤异常:{}", e.getMessage());
return "执行步骤异常:" + e.getMessage();
}
}
}
开发ToolCallAgent类
ToolCallAgent要复杂一些,负责实现工具调用的能力,继承自ReActAgent,具体实现了think和act两个抽象方法方法
由于代码比较复杂我们按照步骤来实现
1.首先我们搭建ToolCallAgent类的基本框架
@Slf4j
@Data
@EqualsAndHashCode(callSuper = true)
public class ToolCallAgent extends ReActAgent {
public ToolCallAgent(ToolCallback[] availableTools) {
super();
this.availableTools = availableTools;
// 创建工具调用管理者
this.toolCallingManager = ToolCallingManager.builder().build();
// 禁用DashScope内置的工具调用机制
this.chatOptions = DashScopeChatOptions.builder().withProxyToolCalls(true).build();
}
// 可用的工具集合
private final ToolCallback[] availableTools;
// 保存了工具调用信息的响应
private String toolCallChatResponse;
// 工具调用的管理者
private final ToolCallingManager toolCallingManager;
// 禁用内置的工具调用机制,自己维护上下文
private final ChatOptions chatOptions;
@Override
public boolean think() {
return false;
}
@Override
public String act() {
return "";
}
}
这里需要注意的是,ChatOptinons要用DashScopeChatOptions来指定withProxyToolCalls(true)来禁止自动工具调用机制
2.实现think方法
@Override
public boolean think() {
// 如果有下一步提示词,则添加为用户消息
String nextStepPrompt = getNextStepPrompt();
if(StrUtil.isBlank(nextStepPrompt)){
UserMessage userMessage = new UserMessage(nextStepPrompt);
chatMemory.add(chatId,userMessage);
}
// 获取所有消息
List<Message> messagesList = chatMemory.get(this.chatId,Integer.MAX_VALUE);
// 传入上下文和选项作为当前提示词
Prompt prompt = new Prompt(messagesList, chatOptions);
try{
ChatResponse chatResponse = getChatClient().prompt(prompt).system(getSystemPrompt())
.tools(availableTools) // 设置可用的工具
.advisors(spec -> {
spec.param(CHAT_MEMORY_CONVERSATION_ID_KEY,chatId)
}).toolContext(Map.of("chatId",chatId))
.call().chatResponse();
this.toolCallChatResponse = chatResponse;
AssistantMessage assistantMessage = chatResponse.getResult().getOutput();
// 输出提示信息
String result = assistantMessage.getText();
// 获取工具调用
List<AssistantMessage.ToolCall> toolCallList = assistantMessage.getToolCalls();
log.info("{}的思考:{}",getName(),result);
log.info("{}选择了{}个工具来使用,具体列表如下:",getName(),toolCallList);
// 获取工具调用信息
String toolCallInfo = toolCallList.stream()
.map(toolCall -> String.format(
"工具名称:%s,参数:%s",
toolCall.name(),
toolCall.arguments()
)).collect(Collectors.joining("\n"));
log.info(toolCallInfo);
// 如果没有调用工具,则记录助手消息
if(toolCallList.isEmpty()){
chatMemory.add(this.chatId,assistantMessage);
return false;
}else {
return true;
}
} catch (Exception e) {
log.error("{}的思考过程遇到了问题:{}",getName(),e.getMessage());
chatMemory.add(this.chatId,new AssistantMessage("处理时遇到错误"+e.getMessage()));
return false;
}
}
核心原理在于:ChatResponse中包含的助手信息可用通过调用assistantMessage.getToolCalls()方法,获得本次大模型将要调用的工具列表和参数信息如果这个集合非空,说明有工具调用需求
3.实现act方法
执行工具调用列表,得到返回结果,并将工具的响应添加到消息列表中:
@Override
public String act() {
if(!toolCallChatResponse.hasToolCalls()){
return "没有工具调用";
}
// 获取所有消息信息
List<Message> messages = chatMemory.get(this.chatId,Integer.MAX_VALUE);
// 需要调用工具,通过toolCallingManager执行
Prompt prompt = new Prompt(messages, chatOptions);
ToolExecutionResult toolExecutionResult = toolCallingManager.executeToolCalls(prompt, toolCallChatResponse);
// 记录消息上下文,conversationHistory以包含助手消息和工具调用返回的结果
// 先清空原来的消息,再追加全部消息
this.chatMemory.clear(this.chatId);
this.chatMemory.add(this.chatId,toolExecutionResult.conversationHistory());
// 获得最后一次工具调用的响应信息
ToolResponseMessage toolResponseMessage = (ToolResponseMessage) CollUtil.getLast(toolExecutionResult.conversationHistory());
String results = toolResponseMessage.getResponses().stream()
.map(response -> "工具"+response.name()+"完成了它的任务!结果:"+response.responseData()).collect(Collectors.joining("\n"));
// 判断是否调用了终止工具
boolean terminateToolCalled = toolResponseMessage.getResponses().stream()
.anyMatch(response -> "doTerminate".equals(response.name()));
if(terminateToolCalled){
// 如果调用了终止工具,则结束任务
setState(AgentState.FINISHED);
}
log.info(results);
return results;
}
通过 toolCallingManager.executeToolCalls 调用工具获得 ToolExecutionResult 工具调用结果,需要注意的是,ToolExecutionResult 中已经包含了所有的对话历史记录,添加时先直接清空(dkar)原本的记忆内容,然后添加 toolExecutionResult.conversationHistory)作为消息记忆上下文。另外,我们借鉴OpenManus 对于任务结束的逻辑,需要实现一个终止工具(TerminateTool).如果判断调用了这个终止工具(通过比对工具方法的名称),对智能体状态设置为完成(FINISHIED)。
4.实现并注册TerminateTool工具
最后一步,我们需要将上述步骤中需要的终止工具开发出来
在agent包中新建TerminateTool类代码如下:
@Component
public class TerminateTool {
@Tool(description = """
Terminate the interaction when the request is met
or if the assistant cannot proceed with the task.
When you have finished all the tasks, call this tool to end the interaction.""")
public String doTerminate(ToolContext context) {
return"任务结束";
}
}
最后将此工具注册到Tools4AIConfig中:
开发TravelManus类
TravelManus是最终提供给其他方法调用的AI智能体实例,继承自ToolCallAgent,需要给智能体设置各种参数,比如对话客户端chatClinet,工具调用列表等。
在agent包中新建TravelManus类:
@Component
// 每一次获取的都行新的智能体对象,避免不同用户用到同一个对象
@Scope("prototype")
public class TravelManus extends ToolCallAgent{
// 默认智能体名称
private static final String MANUS_NAME = "科泰旅游大师";
// 构造函数传递工具集合和ChatModel
public TravelManus(ToolCallback[] availableTools, ChatModel dashScopeChatModel) {
// 传入可用工具
super(availableTools);
// 设置智能体名称
this.setName(MANUS_NAME);
// 设置智能体角色
String SYSTEM_PROMPT = """
你是“%s”,一个全能型的AI助手,旨在解决用户提出的关于旅游的任何任务。
你拥有多种可用工具,可随时调用以完成复杂指令
""".formatted(MANUS_NAME);
// 设置系统提示词
this.setSystemPrompt(SYSTEM_PROMPT);
// 设置下一个步骤提示词
resetNextStepPrompt();
// 初始化客户端
ChatClient chatClient = ChatClient.builder(dashScopeChatModel)
.defaultAdvisors(new TokenCounterAdvisor()).build();
this.setChatClient(chatClient);
}
// 重写修改最大步数
@Override
public void setMaxSteps(int maxSteps) {
super.setMaxSteps(maxSteps);
resetNextStepPrompt();
}
// 重置下一个步骤提示词
private void resetNextStepPrompt() {
String NEXT_STEP_PROMPT = """
根据用户需求,主动选择最合适的工具或工具组合。对于复杂的任务,你可以将问题分解开来,
然后分步骤使用不同的工具来解决它。你应该尽量在%d 个步骤内完成任务,否则任务将可能提前结束。
在使用每种工具之后,要清晰地说明执行结果,并提出下一步的建议如果您想在任何时刻终止这种交互,请使用“终止”工具/函数调用、
""".formatted(getMaxSteps());
this.setNextStepPrompt(NEXT_STEP_PROMPT);
}
}
测试智能体
利用IDEA为TravelManus生成测试类TravelManusTest:
@Test
void testRun() {
String userPrompt = """
我想去湖南郴州旅游3天,
预算3000,帮我规划旅程,并安排附近景区5公里的酒店,
最后生成旅行计划,以PDF格式输出
""";
// 设置最大步骤数
travelManus.setMaxSteps(20);
String answer = travelManus.run(userPrompt);
System.out.println(answer);
}
接下来我们运行测试:
可以看到一共执行了12步完成了我们的任务
并且生成了一个比较精美的PDF
更多推荐


所有评论(0)