Manus设计思路
Manus智能体的核心:思考、行动、观察、再思考、再行动......直到完成任务。
通过阅读OpenManus的源码,得知它通过模板方法+面向抽象编程+三重继承实现,参照它的架构升级恋爱大师为自主规划型智能体。
由OpenManus的源码架构分析,其实现智能体的架构有4层
- BaseAgent:智能体的基类,定义基本信息与多步骤执行流程
- ReActAgent:实现ReAct模式,将代理执行过程分为思考和行动两部分
- ToolCallAgent:在ReAct模式基础上,增加工具调用能力,也可远程调用MCP
- Manus:OpenManus的核心实例,集成了各种工具和能力,直接供业务层调用
开发
基于Spring AI 开发一个简化版的Manus智能体,在根目录下新建agent.model包。创建枚举类,定义智能体的状态,用于控制智能体的执行。
public enum AgentState {
// 空闲
IDLE,
// 运行中
RUNNING,
// 已完成
FINISHED,
// 运行错误
ERROR
}
BaseAgent
BaseAgent是所有具体代理类的抽象基础类,核心作用是统一管理代理的生命周期、执行流程及核心资源,为子类提供标准化的运行框架。
开发思路:
首先需要定义好代理需要哪些属性:代理名称、系统提示词、下一步提示词、代理状态、最大执行次数、当前执行次数、对话客户端、对话上下文。
run()方法是给Manus实例调用的,这里需要编写Manus的执行流程,流程如下:
- 基础检查: 如果代理状态非空闲且用户没有输入提示词,不允许运行代理
- 添加消息: 代理状态更改为“运行中”,并将用户提示词添加到上下文
- 保存结果列表: 记录当前执行次数,调用一次单步执行,然后将执行结果添加到上下文
- 异常检查: 如果当前步数已到达最大执行次数,将代理状态更改为“已完成”并将“达到最大执行次数”添加到上下文;如果代理启动失败,将状态更改为“运行错误”,直接返回错误堆栈信息。
@Data
@Slf4j
public abstract class BaseAgent {
// 代理名称
private String name;
// 提示词
private String systemPrompt;
private String nextStepPrompt;
// 状态
private AgentState state = AgentState.IDLE;
// 执行控制
private int maxStep = 10;
private int currentStep = 0;
// 对话客户端
private ChatClient chatClient;
// 对话上下文
private List<Message> messageList = new ArrayList<>();
public String run(String userPrompt) {
// 基础检查
if (this.state != AgentState.IDLE) {
throw new RuntimeException("Cannot run agent from state: " + this.state);
}
if (StrUtil.isBlank(userPrompt)) {
throw new RuntimeException("User prompt cannot be empty.");
}
// 更改状态
state = AgentState.RUNNING;
// 记录消息上下文,将用户消息添加到消息列表中
messageList.add(new UserMessage(userPrompt));
// 保存结果列表
List<String> results = new ArrayList<>();
try {
for (int i = 0; i < maxStep && state != AgentState.FINISHED; i++) {
int stepNumber = i + 1;
currentStep = stepNumber;
log.info("Executing step " + stepNumber + " / " + maxStep);
// 单步执行
String stepResult = step();
String result = "Step " + stepNumber + " : " + stepResult;
results.add(result);
// 检查是否超出步骤限制
if (stepNumber >= maxStep) {
state = AgentState.FINISHED;
results.add("Maximum number of steps reached.");
}
}
return String.join("\n", results);
} catch (Exception e) {
state = AgentState.ERROR;
log.error("Error executing agent: ", e);
return "执行错误: " + e.getMessage();
} finally {
// 清理资源
this.cleanup();
}
}
/**
* 执行单个步骤
*
* @return 步骤执行结果
*/
public abstract String step();
/**
* 清理资源
*/
protected void cleanup(){};
}
step()方法具体由子类ReActAgent实现,cleanup()方法属于可选实现方法,可以开发为用户手动清理上下文的功能。
ReActAgent
ReActAgent用于定义单次步骤需要执行的逻辑,这个代理抽象类集成自BaseAgent,具体实现
step()方法。根据ReAct的特性可知,这里一个步骤需要“思考 + 行动”,所以定义两个抽象方法
think()和act(),代码如下:
@EqualsAndHashCode(callSuper = true)
@Data
@Slf4j
public abstract class ReActAgent extends BaseAgent{
public abstract boolean think();
public abstract String act();
@Override
public String step() {
try {
// 先思考
boolean thinkResult = think();
if (!thinkResult){
return "No need to think.";
}
// 再行动
return act();
} catch (Exception e) {
// 记录异常日志
return "Steps Executing Error: " + e.getMessage();
}
}
具体的思考和行动方法,由子类ToolCallAgent实现。
ToolCallAgent
ToolCallAgent是负责处理工具调用的基础代理类,作为Manus实例的父类,如果Manus需要工具调用就需要继承这个类。如何处理实例的工具调用逻辑,思路如下:
- 准备好工具调用所需的依赖属性:工具列表、工具管理器、模型配置
ChatOption(用于关闭Spring AI自动工具调用),使用构造函数初始化。 - 编写
think()、act()方法的逻辑
Q: 为什么要使用构造函数初始化除了 toolCallChatResponse 的属性?
这是用于临时存放工具调用的响应结果的变量,用于act()方法判断是否需要调用工具,以及调用什么工具,每一个步骤的工具调用响应都不相同,不应该在构造函数初始化。构造函数初始化的属性应该是整个工具调用逻辑运行时都需要用到的属性
1)关闭Spring AI的工具调用机制
如果要将自己设计的工具调用逻辑给千问大模型使用,就需要开启它的代理工具调用模式,开启后就会禁用Spring AI内置的工具调用模式。
this.chatOptions = DashScopeChatOptions.builder()
.withProxyToolCalls(true) //开启千问大模型代理工具调用模式
.build();
2) think()方法流程
think()方法负责大模型根据当前上下文,决定是否调用工具,调用什么工具,返回决策结果。
如果存在下一步引导提示词,就包装成用户消息,追加到对话历史,让模型基于最新上下文思考。
if (getNextStepPrompt() != null && !getNextStepPrompt().isEmpty()) {
UserMessage userMessage = new UserMessage(getNextStepPrompt());
getMessageList().add(userMessage);
}
构建完整的对话请求,包装为提示词对象Prompt。
List<Message> messageList = getMessageList();
Prompt prompt = new Prompt(messageList, chatOptions);
然后调用大模型进行思考,获取模型返回的思考结果+工具调用决策。
ChatResponse chatResponse = getChatClient().prompt(prompt)
.system(getSystemPrompt())
.tools(availableTools)
.call()
.chatResponse();
将大模型的响应保存到toolCallChatResponse,act()执行时会直接调用这个结果。提取助手消息,后续若判断不需要调用工具,将助手消息(大模型的回答)添加到对话历史。
this.toolCallChatResponse = chatResponse;
AssistantMessage assistantMessage = chatResponse.getResult().getOutput();
if (toolCallList.isEmpty()) {
// 不调用工具 → 直接把回答加入消息历史
getMessageList().add(assistantMessage);
return false;
} else {
// 需要调用工具 → 不添加消息,等待工具执行后再更新
return true;
}
3) act() 方法流程
检查是否存在工具调用,若没有,直接返回提示信息,终止执行。
if(!toolCallChatResponse.hasToolCalls()){
return "没有工具调用";
}
若需要调用,基于当前对话历史和大模型配置,构建提示词,并交给 toolCallingManager 工具调用管理器,此处真正执行模型指定的工具,返回工具执行后的统一结果。
Prompt prompt = new Prompt(getMessageList(), chatOptions);
ToolExecutionResult toolExecutionResult =
toolCallingManager.executeToolCalls(prompt, toolCallChatResponse);
获取对话上下文,并更新对话历史。用最新的对话上下文覆盖原有的消息列表,下一轮思考能基于工具执行结果继续思考。
setMessageList(toolExecutionResult.conversationHistory());
从最新对话历史提取工具返回的消息,解析执行结果,拼接成结果字符串,用于返回与后续对话
ToolResponseMessage toolResponseMessage = (ToolResponseMessage) CollUtil.getLast(toolExecutionResult.conversationHistory());
String results = toolResponseMessage.getResponses().stream()
.map(response -> "工具 " + response.name() +
" 完成了它的任务!结果: " + response.responseData())
.collect(Collectors.joining("\n"));
检查工具执行结果是否包含终止工具doTerminate,若执行,将智能体状态设置为FINISHED,不再进行下一步思考,最后返回执行结果。
boolean terminateToolCalled = toolResponseMessage.getResponses().stream()
.anyMatch(response -> "doTerminate".equals(response.name()));
if (terminateToolCalled) {
setState(AgentState.FINISHED);
}
return results;
Manus实例
YuluoLoveManus 是一个预配置好角色、行为、工具、模型和对话策略的业务智能体,它继承了 Agent 的思考与执行流程,拥有独立身份、系统指令、运行上限和专属聊天客户端,能够自主完成工具调用、多轮任务处理,是面向用户提供实际服务的核心单元。
@Component
public class YuluoLoveManus extends ToolCallAgent {
public YuluoLoveManus(ToolCallback[] allTool, ChatModel dashscopeChatModel) {
super(allTool);
this.setName("YuluoLoveManus");
String SYSTEM_PROMPT = """
You are YuluoLoveManus, 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 nextStepPrompt = """
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(nextStepPrompt);
this.setMaxStep(20);
// 初始化客户端
ChatClient chatClient = ChatClient.builder(dashscopeChatModel)
.defaultAdvisors(new MyLoggerAdvisor())
.build();
this.setChatClient(chatClient);
}
后续如果要新增新的Manus实例,只需要自定义Manus名称、系统提示词、ChatClient即可,属于典型的模板方法设计,不同的子类继承模板可以有多种不同的实现。
测试结果
用户提示词:
我想在上海的高档餐厅约会,要求能观看黄浦江的夜景,
请帮我找到黄浦区适合的地点,并生成markdown文件,命名“高档餐厅约会”
输出结果:
大模型已自主规划并调用高德地图、网页搜索、markdown转换、文件保存到本地的工具
总结
根据OpenManus的源码,其通过四层架构设计实现具有自主规划能力的智能体,即基础代理层+思考行动层+工具调用层+Manus实例层。前三层均采用模版方法设计模式+面向抽象编程,每一层负责的职责不同,又提供抽象方法供子类具体实现,实现各层之间的完全解耦,极大提高了扩展性。
本项目通过模仿OpenManus的架构设计模式,简易实现了Manus,实现对复杂任务的自主规划能力,将恋爱大师智能体升级从有限规划型升级为自主规划型。
相比于纯对话功能,自主规划功能更强大,能解决复杂任务,但消耗的token极大。所以按需使用智能体的功能对于节省token成本与服务器资源是至关重要的。