OpenManus 开发实战:从架构解析到浏览器自动化踩坑指南
本文基于一次 OpenManus 开发实战课程整理,内容涵盖:OpenManus 六层架构、Daytona 沙箱使用、核心源码逐行剖析(主循环 / think / act / 内存结构)、BrowserUseTool 浏览器状态管理机制,以及一个完整的踩坑案例——让 Agent 在携程查询机票,并分别用 RAG 策略 和 GUI 视觉策略 两条路线解决日期选择器难题。
一、OpenManus 是什么
OpenManus 是 MetaGPT 团队于 2025 年 7 月推出的开源通用 AI 智能体框架,定位"无堡垒、纯开放",目标是零成本复刻闭源 Manus 的"手脑并用"能力。
它是一个开箱即用的多智能体协作平台:把自然语言需求直接变成可执行的工作流,支持浏览器自动化、Python 代码执行、文件处理、数据可视化等 12 类工具,且全部可插拔替换。
说到 Manus 本身:它是 2025 年 3 月上线的通用 AI Agent,出自蝴蝶效应团队,可自主完成市场研究、编码等任务,上线 8 个月 ARR 破 1 亿美元;2025 年 12 月,Meta 以约 20 亿美元收购,创始人肖弘出任 Meta 副总裁,团队留驻新加坡,产品保持独立运营。OpenManus 正是对这一能力的开源复刻。
典型应用场景:
- 数据采集与可视化:自然语言指令 → 自动写爬虫 → 清洗数据 → 生成可视化报告并导出 HTML/PPT;
- 电商运营:输入"给三款新品写多平台文案"→ 市场分析、竞品价格抓取、本地化文案生成、合规检测全自动完成。
如果不喜欢命令行,还可以搭配社区的可视化界面 openmanus-ui:将 assets、static、templates 文件夹以及 app.py 拷贝到 OpenManus 项目根目录,运行 python app.py 即可。
二、核心结构:六层架构
OpenManus 的整体架构可以拆成六层:
| 层级 | 内容 |
|---|---|
| 用户交互层 | CLI、Web API、可视化界面三选一 |
| 应用入口层 | 单智能体(main.py)、多智能体协作(run_flow.py)、远程工具接入(run_mcp.py) |
| 智能体层 | BaseAgent → ReActAgent → ToolCallAgent → Manus 四级继承,逐级叠加"推理—行动—工具调用"能力 |
| 工具层 | 统一 BaseTool 接口,内置浏览器、搜索、代码执行、文件保存等工具,可通过 MCP 协议扩展外部服务 |
| 流程控制层 | PlanningFlow 自动把复杂任务拆成线性或 DAG 步骤,支持多智能体按角色分工(项目经理 → 产品经理 → 执行专员) |
| 基础设施层 | Docker 一键部署、Redis 缓存、FAISS 向量长期记忆,LLM 适配器同时支持 DeepSeek、Claude、Qwen 等 10+ 模型 |
其中最值得初学者关注的就是智能体层的四级继承:
BaseAgent:状态管理、内存管理、执行循环;ReActAgent:引入"思考—行动"(Think-Act)模式;ToolCallAgent:处理工具调用决策与执行;Manus:面向通用任务的完整 Agent,叠加浏览器上下文管理等能力。
三、Daytona:给 AI 生成的代码一个安全沙箱
3.1 Daytona 是什么
Daytona 的作用是安全沙盒执行器,负责把 AI 生成的代码放到一个隔离的容器里运行,防止恶意操作:
LLM 想跑代码 → OpenManus 把代码丢进 Daytona 容器
→ Daytona 在封闭环境里执行 → 只把结果返回给 LLM,宿主机不受影响
使用前需要到 Daytona 官网 申请 API Key。
3.2 最小使用示例
# Step1:安装 pip install daytona
# Step2:创建 API KEY
# Step3:创建 Sandbox 并执行代码
from daytona import Daytona, DaytonaConfig
# 定义配置
config = DaytonaConfig(api_key="dtn_XXX")
# 初始化 Daytona 客户端
daytona = Daytona(config)
# 创建 Sandbox 实例
sandbox = daytona.create()
# 在沙箱中安全地运行代码
response = sandbox.process.code_run('print("Hello World from code!")')
if response.exit_code != 0:
print(f"Error: {response.exit_code} {response.result}")
else:
print(response.result) # 输出:Hello World from code!
3.3 Daytona 在 OpenManus 中怎么用
相关代码分布:
app/daytona/
├── sandbox.py # Daytona 沙箱管理(创建、启动、删除)
├── tool_base.py # 沙箱工具基类(SandboxToolsBase)
└── README.md # 使用说明
app/tool/sandbox/
├── sb_browser_tool.py # 沙箱浏览器工具
├── sb_files_tool.py # 沙箱文件工具
├── sb_shell_tool.py # 沙箱 Shell 工具
└── sb_vision_tool.py # 沙箱视觉工具
app/agent/
└── sandbox_agent.py # SandboxManus Agent
Daytona 提供的是一个隔离的云端执行环境:
- 远程沙箱容器:基于 Docker 的隔离环境;
- 预配置环境:包含浏览器、文件系统、Shell 等工具;
- VNC 远程访问:可以实时查看浏览器操作过程(VNC 是一种远程桌面协议,允许你远程访问和控制另一台计算机的图形界面);
- 自动资源管理:自动停止和归档,节省资源。
注意:使用 SandboxManus Agent 时,浏览器在远程云端沙箱中运行,而不是在你的本地电脑上。
通过 Daytona 沙箱,Agent 可以使用四个专用工具:
| 工具 | 说明 |
|---|---|
SandboxBrowserTool |
在远程沙箱中运行浏览器,支持浏览器自动化操作,可通过 VNC 实时查看 |
SandboxFilesTool |
在沙箱中读写文件,管理 /workspace 目录下的文件 |
SandboxShellTool |
在沙箱中执行 Shell 命令,支持命令行操作 |
SandboxVisionTool |
在沙箱中进行视觉识别和分析 |
3.4 专用 Agent:SandboxManus
SandboxManus 是一个专门使用沙箱工具的 Agent:
# app/agent/sandbox_agent.py
class SandboxManus(ToolCallAgent):
"""使用沙箱工具的专用 Agent"""
available_tools = [
SandboxBrowserTool(), # 沙箱浏览器
SandboxFilesTool(), # 沙箱文件操作
SandboxShellTool(), # 沙箱 Shell
SandboxVisionTool(), # 沙箱视觉
AskHuman(),
Terminate(),
]
它的完整工作流程是:
- 用户启动 SandboxManus Agent;
- Agent 初始化时调用
create_sandbox(); - Daytona SDK 创建远程沙箱容器;
- 沙箱启动 supervisord 服务(supervisord 是一个用 Python 写的进程管理工具,作用是 24 小时盯着子进程,挂了就重启);
- Agent 获得 VNC URL 和 Website URL;
- Agent 使用沙箱工具执行任务;
- 用户可通过 VNC 实时查看浏览器操作;
- 任务完成后,沙箱自动停止或归档。
使用方式也非常简洁:
# 创建并初始化 SandboxManus(沙箱的 API Key 需要在 config.toml 中配置)
agent = await SandboxManus.create()
# 运行任务
await agent.run("你的任务提示词")
# 清理资源
await agent.cleanup()
初始化流程内部做了五件事:创建沙箱 → 启动 supervisord 并等待浏览器自动化服务就绪(端口 8003,最多等 60 秒)→ 生成 VNC URL 和 Website URL → 将沙箱工具添加到可用工具列表 → 初始化 MCP 服务器连接(如果配置)。
踩坑提示:如果用 Daytona,tier1 和 tier2 的用户没法上网,需要升级到 tier3 或 tier4。
四、两个快速上手的实战案例
4.1 案例一:网站 SEO 建议
直接对 Manus 输入:
对 http://class.wucai.com 进行 SEO 审查,提供详细的优化报告和可操作的建议
Agent 会自动打开浏览器访问目标站点、提取页面内容、分析 SEO 问题并生成报告。
这里有一个容易混淆的问题:网页是在 Daytona 沙箱中打开的,还是在本地打开的?
答案是:本地。因为我们使用的是 Manus Agent(不是 SandboxManus),Manus 使用 BrowserUseTool(),这是基于 Playwright 的本地浏览器工具,会在本地打开浏览器窗口。两者对比:
| 特性 | BrowserUseTool(本地) | SandboxBrowserTool(沙箱) |
|---|---|---|
| 运行位置 | 本地电脑 | Daytona 云端沙箱 |
| 使用的 Agent | Manus | SandboxManus |
| 可视化方式 | 直接看到浏览器窗口 | 通过 VNC URL 查看 |
| 资源占用 | 占用本地资源 | 不占用本地资源 |
如果想在 Daytona 沙箱中运行,需要使用 SandboxManus Agent(入口 python .\sandbox_main.py),它会创建 Daytona 沙箱、在沙箱中运行浏览器,并提供 VNC URL 和 Website URL 供查看。
4.2 案例二:竞品分析
帮我写一份关于 2025 年中国新能源汽车出海欧洲的竞品分析报告,重点关注比亚迪和小鹏。
Agent 会自动搜索资料、访问网页、提取信息,最终产出一份完整报告。这个案例背后有一个值得注意的机制:代码里给单任务设置了 20 步硬上限,而 OpenManus 会把每一步再拆成若干子任务——比如"收信息"会被拆成"搜索 A 产品功能""搜索 A 定价""搜索 A 用户评价"……一个环节就可能占 3~6 步。所以复杂任务很容易把 20 步用完,后文的机票案例就踩到了这个坑。
五、源码剖析:从 main.py 到工具执行
5.1 主逻辑总览
用户输入
↓
Agent.run(request) —— 主循环
↓
循环执行 step()(最多 max_steps=20 次)
├─ think() —— 思考阶段:LLM 决策使用哪些工具
└─ act() —— 行动阶段:执行选定的工具
↓
工具执行结果 → 添加到内存 → 下一轮循环
5.2 Step1:初始化阶段
# main.py 或 app.py
agent = Manus(
name="Manus",
description="A versatile agent that can solve various tasks using multiple tools"
)
result = await agent.run(prompt)
初始化做了四件事:
- 创建 Manus Agent 实例;
- 初始化可用工具:
BrowserUseTool、WebSearch、PythonExecute、StrReplaceEditor、AskHuman、Terminate; - 初始化 LLM(默认 qwen-max);
- 初始化内存(Memory)为空。
5.3 Step2:主循环 BaseAgent.run()
# app/agent/base.py:116-154
async def run(self, request: Optional[str] = None) -> str:
if request:
self.update_memory("user", request) # 添加用户请求到内存
while (self.current_step < self.max_steps and self.state != AgentState.FINISHED):
self.current_step += 1
step_result = await self.step() # 执行单步:think + act
results.append(f"Step {self.current_step}: {step_result}")
关键参数:max_steps = 20(最多执行 20 步),current_step 为当前步数(从 1 开始)。
5.4 Step3:单步执行 ReActAgent.step()
# app/agent/react.py:33-38
async def step(self) -> str:
should_act = await self.think() # 思考:决定下一步行动
if not should_act:
return "Thinking complete - no action needed"
return await self.act() # 行动:执行工具调用
5.5 Step4:思考阶段 think()
Manus 的 think() 会先检查浏览器工具是否可用,如果可用,就通过 browser_context_helper 获取浏览器状态并格式化进 prompt,然后再调用父类的 think():
# app/agent/manus.py:144-221
async def think(self) -> bool:
# 1. 检查浏览器工具是否可用
if browser_tool_available:
# 2. 使用 browser_context_helper 格式化 prompt
# 浏览器状态包括:URL、标题、可交互元素列表(格式:[index]<type>text</type>)
self.next_step_prompt = await self.browser_context_helper.format_next_step_prompt()
# 3. 调用父类的 think() 方法
result = await super().think()
return result
父类 ToolCallAgent.think() 是实际的决策核心:
# app/agent/toolcall.py:40-129
async def think(self) -> bool:
# 1. 构建消息列表(包含历史消息 + 当前 prompt)
if self.next_step_prompt:
user_msg = Message.user_message(self.next_step_prompt)
self.messages += [user_msg]
# 2. 调用 LLM,传入工具列表
response = await self.llm.ask_tool(
messages=self.messages,
system_msgs=[Message.system_message(self.system_prompt)],
tools=self.available_tools.to_params(), # 工具列表
tool_choice=self.tool_choices, # AUTO
)
# 3. 解析 LLM 响应
self.tool_calls = response.tool_calls # 工具调用列表
content = response.content # LLM 的思考内容
# 4. 记录日志
logger.info(f"{self.name}'s thoughts: {content}")
logger.info(f"{self.name} selected {len(tool_calls)} tools to use")
# 5. 将响应添加到内存
assistant_msg = Message.from_tool_calls(content=content, tool_calls=self.tool_calls)
self.memory.add_message(assistant_msg)
return bool(self.tool_calls) # 返回是否有工具需要执行
概括一下 LLM 的输入输出:
- 输入:系统提示
SYSTEM_PROMPT(来自app/prompt/manus.py)、历史消息(之前的对话和工具执行结果)、当前 prompt(next_step_prompt,可能包含浏览器状态)、工具列表(所有可用工具的描述,JSON Schema 格式); - 输出:思考内容(Agent 的推理过程,例如"我需要先搜索比亚迪在欧洲市场的信息……")、工具调用(选定的工具和参数,例如
web_search(query="比亚迪 欧洲市场"))。
5.6 Step5:行动阶段 act()
# app/agent/toolcall.py:131-164
async def act(self) -> str:
if not self.tool_calls:
return "No content or commands to execute"
results = []
for command in self.tool_calls:
# 1. 执行工具
result = await self.execute_tool(command)
# 2. 记录日志
logger.info(f"Tool '{command.function.name}' completed its mission! Result: {result}")
# 3. 将工具结果添加到内存
tool_msg = Message.tool_message(
content=result,
tool_call_id=command.id,
name=command.function.name,
base64_image=self._current_base64_image, # 如有截图
)
self.memory.add_message(tool_msg)
results.append(result)
return "\n\n".join(results)
单个工具的执行在 execute_tool() 中:
# app/agent/toolcall.py:166-219
async def execute_tool(self, command: ToolCall) -> str:
name = command.function.name
args = json.loads(command.function.arguments or "{}")
# 1. 执行工具
logger.info(f"Activating tool: '{name}'...")
result = await self.available_tools.execute(name=name, tool_input=args)
# 2. 检查是否有截图返回
if hasattr(result, "base64_image") and result.base64_image:
self._current_base64_image = result.base64_image
# 3. 返回结果
return str(result)
一句话总结:Think 阶段是 Agent 的大脑(智能决策、上下文理解、工具选择),Act 阶段是 Agent 的执行器(工具执行、结果收集、错误处理)。
5.7 内存结构
内存是 Agent 的记忆,存储了完整的对话历史和工具执行结果:
| 消息类型 | 说明 | 包含内容 |
|---|---|---|
| user | 用户输入 | 任务请求文本 |
| assistant | LLM 响应 | 思考内容 + 工具调用列表 |
| tool | 工具执行结果 | 执行结果文本 + 截图(可选) |
| system | 系统提示 | Agent 行为规范 |
对应的数据模型非常精简:
# app/schema.py
class Memory(BaseModel):
messages: List[Message] = Field(default_factory=list)
class Message(BaseModel):
role: str # "user", "assistant", "tool", "system"
content: str
tool_calls: Optional[List[ToolCall]] = None
base64_image: Optional[str] = None # 截图(如果有)
5.8 一次真实的执行流程(竞品分析案例)
以"新能源汽车出海欧洲竞品分析"为例,实际日志长这样:
Step 1:初始思考
Manus's thoughts: 我需要先搜索比亚迪和小鹏汽车在欧洲市场的信息...
Manus selected 1 tools to use
Tools being prepared: ['web_search']
Tool arguments: {"query": "比亚迪 欧洲市场 销量", "num_results": 5}
执行流程:think() → LLM 决定使用 web_search 工具 → act() 执行搜索 → 结果添加到内存。
Step 2-8:浏览器操作
Manus's thoughts: 根据搜索结果,我需要访问具体网页获取详细信息...
Tools being prepared: ['browser_use']
Tool arguments: {"action": "go_to_url", "url": "http://..."}
执行流程:think() 决定访问网页 → browser_context_helper.format_next_step_prompt() 获取浏览器状态(URL、标题、可交互元素列表)→ act() 执行 browser_use.go_to_url(url) → 浏览器工具返回文本结果和 base64 截图(存到 _current_base64_image)→ 结果(含截图)添加到内存。
Step 9-20:持续迭代
每次循环:think() 基于历史消息和当前浏览器状态决定下一步(可能选择 click_element、scroll_down、extract_content、web_search 等)→ act() 执行 → 所有结果累积到内存,供下一轮思考使用。
内存中的数据流大致是:
Memory.messages = [
Message(user, "帮我写一份关于2025年..."),
]
↓ think() → LLM 分析 → 决定使用 web_search
↓ act() → 执行 web_search → 返回搜索结果
Memory.messages = [
Message(user, "帮我写一份关于2025年..."),
Message(assistant, "我需要先搜索...", tool_calls=[web_search(...)]),
Message(tool, "Search results: 1. 比亚迪在欧洲市场..."),
]
↓ think() → LLM 分析搜索结果 → 决定使用 browser_use
↓ ...(循环继续)
5.9 代码结构速览
OpenManus/
├── app/ # 核心应用代码
│ ├── agent/ # Agent 实现层
│ │ ├── base.py # Agent 基类(状态管理、内存管理、执行循环)
│ │ ├── react.py # ReAct Agent(思考-行动模式)
│ │ ├── toolcall.py # 工具调用 Agent(处理工具调用决策)
│ │ ├── manus.py # Manus Agent(主要通用 Agent)
│ │ ├── sandbox_agent.py # SandboxManus Agent(沙箱环境 Agent)
│ │ ├── browser.py # 浏览器 Agent(浏览器上下文管理)
│ │ ├── data_analysis.py # 数据分析 Agent
│ │ ├── swe.py # SWE Agent(软件工程 Agent)
│ │ └── mcp.py # MCP Agent(MCP 协议 Agent)
│ ├── tool/ # 工具集合层
│ │ ├── base.py # 工具基类(BaseTool、ToolResult)
│ │ ├── tool_collection.py # 工具集合管理(注册、执行、管理)
│ │ ├── python_execute.py # Python 代码执行工具
│ │ ├── browser_use_tool.py # 浏览器自动化工具(Playwright)
│ │ ├── web_search.py # 网络搜索工具(多引擎支持)
│ │ ├── str_replace_editor.py
│ │ ├── file_operators.py # 文件操作工具(本地/沙箱)
│ │ ├── planning.py # 规划工具
│ │ ├── bash.py # Shell 命令执行工具
│ │ ├── crawl4ai.py # 网页爬虫工具
│ │ ├── terminate.py / ask_human.py / mcp.py ...
│ │ ├── search/ # 搜索引擎实现(Baidu/Google/Bing/DuckDuckGo)
│ │ ├── sandbox/ # 沙箱工具(Daytona 沙箱)
│ │ └── chart_visualization/ # 图表可视化工具
│ ├── flow/ # 流程管理层(PlanningFlow 等)
│ ├── daytona/ # Daytona 沙箱集成
│ ├── sandbox/ # Docker 沙箱系统
│ ├── mcp/ # MCP 协议支持
│ ├── prompt/ # Prompt 模板(manus/browser/toolcall/planning/swe/mcp...)
│ ├── llm.py # LLM 封装(OpenAI 兼容接口)
│ ├── config.py # 配置管理(TOML 配置、环境变量)
│ ├── schema.py # 数据模型(Message、Memory、ToolCall 等)
│ └── logger.py # 日志配置(loguru)
几个值得记住的设计细节:
tool/目录是 Manus 的核心,包括常用工具、搜索工具、沙箱工具、可视化工具等;WebSearch支持多引擎回退(Baidu → DuckDuckGo → Google → Bing),确保搜索成功率;prompt/把 Agent 的角色定义模块化管理,方便维护和调整。
六、BrowserUseTool:浏览器自动化的核心
6.1 浏览器状态管理
浏览器状态管理是浏览器自动化的核心,OpenManus 的设计有四个要点:
- 元素识别:自动识别页面上的可交互元素(按钮、输入框、链接等);
- 文本描述:为每个元素生成文本描述,LLM 根据文本匹配选择元素;
- 成本优化:使用文本描述而非截图,节省 API 调用成本;
- 状态同步:每次操作后自动更新浏览器状态,确保 LLM 始终了解当前页面。
浏览器元素的格式如下:
[0]<button>搜索</button>
[1]<input>出发地</input>
[2]<input>目的地</input>
[3]<button>查询</button>
[4]<link>登录</link>
LLM 根据这些文本描述选择元素,不需要视觉模型,这是 OpenManus 控制成本的关键设计。
6.2 工具执行代码
# app/tool/browser_use_tool.py
async def execute(self, action: str, url: str = None, index: int = None, ...) -> ToolResult:
# 1. 确保浏览器已初始化
context = await self._ensure_browser_initialized()
# 2. 执行浏览器操作
if action == "go_to_url":
await context.goto(url)
elif action == "click_element":
await context.click(index)
elif action == "extract_content":
content = await context.extract_content(goal)
...
# 3. 获取当前状态(包含截图和元素列表)
browser_state = await self.get_current_state()
# 4. 返回结果(包含 base64_image)
return ToolResult(
content=result_text,
base64_image=browser_state.get('screenshot') # base64 编码的截图
)
BrowserUseTool 基于 browser_use 库实现,支持的操作包括:go_to_url(导航)、click_element(点击指定索引元素)、input_text(输入文本)、scroll_down/up(滚动)、send_keys(发送键盘按键)等。工具执行链路是:
ToolCallAgent.execute_tool()
→ available_tools.execute(name=name, tool_input=args)
→ BrowserUseTool.execute() 处理具体操作
→ context.get_dom_element_by_index(index) # 通过索引获取元素
→ context._click_element_node(element) # 执行点击
6.3 BrowserContextHelper:连接 Agent 与浏览器
BrowserContextHelper 是浏览器上下文辅助类,负责获取浏览器当前状态、格式化浏览器相关的提示词、管理浏览器截图:
class BrowserContextHelper:
def __init__(self, agent):
self.agent = agent
self._current_base64_image = None
async def format_next_step_prompt(self):
# 获取浏览器状态
browser_state = await self.get_browser_state()
url = browser_state.get('url')
title = browser_state.get('title')
interactive_elements = browser_state.get('interactive_elements')
# 构建 prompt,包含 URL、标签页、滚动信息
prompt = NEXT_STEP_PROMPT.format(
url_placeholder=f"\n URL: {url}\n Title: {title}",
...
)
# 添加元素列表
prompt += "\n\n[Current state starts here]\n"
prompt += "Interactive Elements:\n"
prompt += interactive_elements
return prompt
6.4 关键函数速查表
Agent 层:
| 函数 | 位置 | 作用 |
|---|---|---|
run() |
base.py | 主循环,执行 step 直到完成 |
step() |
react.py | 单步执行:think() + act() |
think() |
toolcall.py | LLM 决策,选择工具 |
act() |
toolcall.py | 执行选定的工具 |
execute_tool() |
toolcall.py | 执行单个工具调用 |
浏览器层:
| 函数 | 位置 | 作用 |
|---|---|---|
get_browser_state() |
browser.py | 获取浏览器当前状态 |
format_next_step_prompt() |
browser.py | 格式化浏览器提示词 |
cleanup_browser() |
browser.py | 清理浏览器资源 |
execute() |
browser_use_tool.py | 执行浏览器操作 |
get_current_state() |
browser_use_tool.py | 获取页面状态和元素 |
_ensure_browser_initialized() |
browser_use_tool.py | 初始化浏览器实例 |
cleanup() |
browser_use_tool.py | 关闭浏览器 |
6.5 DOM 树与元素识别机制
DOM(Document Object Model)是浏览器将 HTML 文档解析后生成的树形结构,每个 HTML 标签都是树中的一个节点,具有父子关系:
html
├── head
│ ├── title
│ ├── meta
│ └── script
└── body
├── div (header)
│ ├── button (酒店)
│ ├── button (机票)
│ └── button (火车票)
└── div (content)
├── input (出发地)
├── input (目的地)
└── button (搜索)
browser_use 库通过 DomService 遍历 DOM 树,识别可点击/可交互元素(button、input、a 等),并通过 JavaScript 在页面上渲染索引标签。以携程机票频道为例:
URL: https://flights.ctrip.com/online/channel/domestic
Title: 【携程机票】飞机票查询,机票预订,机票价格查询,打折特价机票
Element Count: 133
=== All Interactive Elements ===
[0]<button 酒店/>
[1]<button 机票/>
[2]<button 火车票/>
[3]<button 旅游/>
...
[37]<input /> <- 出发地输入框
[39]<input /> <- 目的地输入框
[40]<input /> <- 出发日期输入框
[43]<button 搜索/> <- 搜索按钮
页面 DOM 与编号的对应关系:
<div class="header">
<button>酒店</button> <- 被识别为 [0]
<button>机票</button> <- 被识别为 [1]
<span>文字</span> <- 不可交互,跳过
<button>火车票</button> <- 被识别为 [2]
</div>
同时 browser_use 维护一张索引映射表(0 -> DOM节点(button:酒店)、1 -> DOM节点(button:机票)……),确保点击准确。
回答一个常见疑问:只能操作有编号的元素吗? 不是。主要通过索引操作(click_element(index=2) 点击 [2] 号元素),但也支持坐标点击 click_coordinates(x, y) 和滚动到文本 scroll_to_text(text)。
这套机制的关键设计总结:
- 文本描述替代视觉:使用元素文本描述而非截图,节省成本;
- 状态持久化:浏览器会话在步骤间保持;
- 索引映射:browser_use 维护索引到 DOM 元素的映射,确保点击准确。
七、Prompt 详解
Prompt 定义了 Agent 的角色和行为边界,OpenManus 将其模块化管理在 app/prompt/ 目录下。
Manus SYSTEM_PROMPT(系统提示):
你是 OpenManus,一个全能的 AI 助手,旨在解决用户提出的任何任务。
你拥有各种工具可以使用,能够高效地完成复杂的请求。
无论是编程、信息检索、文件处理、网页浏览,还是人机交互,你都能处理。
初始目录是:{directory}
重要提示:对于需要实时信息的任务(如机票价格、当前天气、股票价格、新闻等),
你必须使用浏览器工具访问实时网站。永远不要编造或猜测信息。
请使用中文回复用户。
Manus NEXT_STEP_PROMPT(下一步提示):
根据用户需求,主动选择最合适的工具或工具组合。
对于复杂任务,你可以分解问题并逐步使用不同工具来解决。
使用每个工具后,清楚地解释执行结果并建议下一步。
对于需要实时或当前信息的任务(机票价格、天气、新闻等),
你必须使用浏览器工具搜索并从网站检索实际数据。不要提供编造的信息。
如果你想在任何时候停止交互,请使用 terminate 工具/函数调用。
Browser SYSTEM_PROMPT(核心规则):
输入格式包含:任务、之前的步骤、当前 URL、打开的标签页、交互元素([index]<type>text</type>,index 为数字标识符,type 为 HTML 元素类型,text 为元素描述)。
响应规则:
- 响应格式:必须始终以 JSON 格式响应;
- 操作:可以指定多个操作按顺序执行;
- 元素交互:只使用交互元素的索引;
- 导航和错误处理:如果卡住,尝试替代方法;
- 任务完成:完成后使用 done 操作。
Browser NEXT_STEP_PROMPT(操作指引):
下一步我应该做什么来实现目标?
当你看到 [Current state starts here] 时,请关注以下内容:
- 当前 URL 和页面标题 {url_placeholder}
- 可用标签页 {tabs_placeholder}
- 交互元素及其索引
- 视口上方 {content_above_placeholder} 或下方 {content_below_placeholder} 的内容
浏览器交互操作:
- 导航:使用 browser_use,action="go_to_url", url="..."
- 点击:使用 browser_use,action="click_element", index=N
- 输入:使用 browser_use,action="input_text", index=N, text="..."
- 提取:使用 browser_use,action="extract_content", goal="..."
- 滚动:使用 browser_use,action="scroll_down" 或 "scroll_up"
如果你想在任何时候停止交互,请使用 terminate 工具/函数调用。
八、实战踩坑:让 Manus 在携程查机票
这是本次实战最有价值的部分。任务很简单:
查询 1月30日 从上海到北京的机票
运行 python main.py 后,任务没有成功。复盘整个过程,几乎把浏览器自动化的典型坑都踩了一遍。
8.1 问题一:Agent 陷入重复操作
查看日志发现 Agent 一直在重复:没有找到正确的按钮——本来应该"选择单程 → 选择日期 1 月 30 日 → 出发地上海 → 目的地北京 → 查询",但它始终定位不到正确的元素。
调试手段:固定 prompt 输入,增加调试日志,把网页 HTML 保存下来,检查为什么没有找到日期选择器——实际上点击"出发日期"后是有日历选择弹窗的,但日历格子是 JavaScript 动态渲染的,很难通过常规 DOM 遍历定位和编号。
8.2 问题二:20 步上限耗尽
修复定位问题后,Agent 终于找到了日期,而且正确选择了"单程"——但 20 个 step 已经用完了。
解决方法:把 max_steps 改成 25。注意 max_steps 不止一处定义,browser.py 中的 max_steps 也要同步修改,否则运行还是 20 步就结束了。
经验:复杂网页任务的步数消耗远超直觉,调大
max_steps时记得全局搜索所有定义点。
8.3 问题三:成功不稳定
即使能跑通,1 月 30 日的设置也是"有时候成功",并不稳定。这就引出了两条系统性的优化路线。
九、优化路线一:RAG 策略(知识库 + 工程化加固)
第一条路线的思路是:既然日历选择器难交互,那就尽量绕开它;把"怎么绕"的经验沉淀成知识库,让 Agent 启动时就能读到。
最终落地的改造有四项:
9.1 元素分类器 ElementClassifier
新增 app/tool/element_classifier.py,为浏览器交互元素添加智能分类和置信度评分:
- 支持 14 种元素分类:CALENDAR、DATE、INPUT、BUTTON、LINK、SELECT、TAB 等;
- 每个元素有 0-100 的置信度评分;
- 特别优化日历日期元素识别(1-31 数字自动标记为 CALENDAR)。
这样编号不再是一个扁平的大列表,而是按 HTML 标签和语义分了大类,LLM 选择时更有的放矢。
9.2 知识库 RAG 系统
新增 knowledge/travel_tools.txt,并在 app/prompt/manus.py 中实现知识库自动加载机制:
- Agent 启动时自动读取
knowledge/*.txt文件; - 知识库内容注入到系统提示词中;
- 首个场景:机票查询使用 URL 参数直接跳转,避免日历选择器交互。
携程机票 URL 格式:
https://flights.ctrip.com/online/list/oneway-{出发}-{到达}?depdate={日期}
也就是说,查"1 月 30 日上海到北京",与其在页面上点十几下,不如直接构造 URL 一步到位。知识库采用纯文本、按"情况 1、情况 2……"逐条补充的方式,简单但有效,而且天然可泛化——以后遇到新的场景就往 txt 里加一条。
9.3 系统时间注入
在系统提示中添加当前时间,帮助 Agent 正确处理日期:
当前系统时间:2026年01月21日 22:20:25 星期三
没有这个信息,LLM 很容易把"1 月 30 日"理解成错误的年份(实际踩坑中 Agent 就构造过 2023 年的 URL)。
9.4 任务完成策略优化
优化 app/prompt/browser.py 和 app/prompt/manus.py 中的任务完成逻辑:
- 优先从元素列表获取信息,减少
extract_content调用; - 对于"查询"类任务,获取信息后立即返回结果;
- 不再反复询问用户确认,提升效率。
改之前的问题很典型:Agent 明明已经从页面元素列表里看到了航班信息,还在不停地迭代、提取、滚动。改之后,日志里可以看到它直接基于元素列表给出了结果:
Manus's thoughts: 从页面内容来看,我们已经滚动并加载了航班列表。
尽管提取操作没有成功抓取到具体的航班信息,但我们可以直接从原始元素列表中
观察到航班的详细情况。以下是1月30日从上海到北京的一些航班选项:
- 四川航空 3U5163:空客330-300,共享,07:00出发(虹桥国际机场T2),
09:20到达(首都国际机场T2),¥750起,经济舱3.5折。
- 东方航空 MU5099:空客330-300,07:00出发(虹桥国际机场T2),
09:20到达(首都国际机场T2),赠接送机最高8折券,¥750起,经济舱3.5折。
- 厦门航空 MF3515:空客330-300,共享,07:00出发...,¥760起,经济舱3.6折。
...
十、优化路线二:GUI 策略(视觉模型定位)
第二条路线的思路更激进:既然 DOM 编号的方式一直有问题,干脆换成视觉模型,让模型看着截图直接给坐标。
10.1 gui-plus 模型调用方式
以阿里云百炼的 gui-plus 模型为例:
import os
from openai import OpenAI
client = OpenAI(
# 若没有配置环境变量,请用阿里云百炼 API Key 替换为:api_key="sk-xxx"
api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)
messages = [
{
"role": "system",
"content": """1. 核心角色 (Core Role)
你是一个顶级的AI视觉操作代理。你的任务是分析电脑屏幕截图,理解用户的指令,
然后将任务分解为单一、精确的GUI原子操作。
2. [CRITICAL] JSON Schema & 绝对规则
你的输出必须是一个严格符合以下规则的JSON对象。任何偏差都将导致失败。
[R1] 严格的JSON: 你的回复必须是且只能是一个JSON对象。禁止在JSON代码块前后添加任何文本、注释或解释。
[R2] thought对象的结构: "在这里用一句话简要描述你的思考过程。"
[R3] 精确的Action值: action字段的值必须是工具集中定义的一个大写字符串(例如 "CLICK", "TYPE")。
[R4] 严格的Parameters结构: parameters对象的结构必须与所选Action的模板完全一致。
3. 工具集 (Available Actions)
#CLICK 功能: 单击屏幕。Parameters: {"x": <integer>, "y": <integer>, "description": "<string, optional>"}
#TYPE 功能: 输入文本。Parameters: {"text": "<string>", "needs_enter": <boolean>}
#SCROLL 功能: 滚动窗口。Parameters: {"direction": "<'up' or 'down'>", "amount": "<'small', 'medium', or 'large'>"}
#KEY_PRESS 功能: 按下功能键。Parameters: {"key": "<string: e.g., 'enter', 'esc', 'alt+f4'>"}
#FINISH 功能: 任务成功完成。Parameters: {"message": "<string: 总结任务完成情况>"}
#FAILE 功能: 任务无法完成。Parameters: {"reason": "<string: 清晰解释失败原因>"}
4. 思维与决策框架
目标分析 → 屏幕观察(必须基于截图中存在的视觉证据,看不见的元素不能交互)
→ 行动决策 → 构建输出 → 最终验证(是否纯JSON?action是否大写无空格?parameters结构是否100%一致?)
"""
},
{
"role": "user",
"content": [
{"type": "image_url", "image_url": {"url": "https://.../screenshot.png"}},
{"type": "text", "text": "帮我打开浏览器。"}
]
},
]
completion = client.chat.completions.create(
model="gui-plus",
messages=messages
)
print(completion.choices[0].message.content)
模型返回的就是一个带坐标的原子操作:
{
"thought": "用户想打开浏览器,我看到了屏幕上的Google Chrome图标。因此,下一步是点击该图标来启动浏览器。",
"action": "CLICK",
"parameters": {
"x": 1086,
"y": 127
}
}
10.2 用 gui-plus 解决日历选择
把携程页面截图发给模型,问"帮我选择 1 月 30 日的出发日期",模型直接返回:
{
"thought": "用户想要选择1月30日作为出发日期。在截图中,我可以看到日历界面,并且找到了1月30日这个日期。因此,下一步操作应该是点击该日期。",
"action": "CLICK",
"parameters": {
"x": 652,
"y": 537
}
}
DOM 编号搞不定的日历格子,视觉模型一次就点中了。
10.3 但纯 GUI 路线也有自己的坑
实际接入后并不顺利,典型问题包括:
- 日志发现程序仍在走旧的编号路径,需要加调试日志确认 GUI 路径真正生效;
- 程序运行时间过长,出发城市识别错误(还停留在新加坡),日期也没选对;
- 输入框输入文字不成功(输入"上海""北京"失败);
- 写 prompt 时不应该给具体的数值示例和城市名称示例,否则模型会过拟合到示例,失去泛化能力。
10.4 最终方案:简化的 click + type 接口 + 三层定位
反复权衡后,最终没有把宝全押在任何一条路线上,而是把复杂的元素索引操作简化为两个核心接口:
click:智能点击,通过element_description描述目标元素;type:智能输入,通过element_description和text完成输入。
内部实现三层定位策略,逐级回退:
- Playwright Locator 精确/包含匹配——最快最稳,优先使用;
- JavaScript 动态元素查找——处理动态渲染元素;
- GUI-Plus 视觉模型回退——新增
vision_click和vision_type操作,使用视觉模型(qwen-vl-plus)识别页面元素坐标进行操作,解决动态日历选择等传统方法难以处理的场景。
同时配套两项加固:
- 日期处理优化:Prompt 增强(在 SYSTEM_PROMPT 中明确当前年份和日期规则)+ 工具层兜底(在
go_to_url中自动检测并修正携程 URL 中的过期日期); - 调试能力增强:自动保存每次操作的网页 HTML 和元素信息到
debug_html/目录,视觉操作保存截图并标记点击坐标,便于问题排查。
十一、总结
这次实战可以浓缩成几句话:
- OpenManus 的主干非常清晰:
run()主循环驱动step(),每步就是think()(LLM 决策)+act()(工具执行),结果全部沉淀到 Memory,如此往复直到任务完成或步数耗尽。读懂base.py → react.py → toolcall.py → manus.py这条继承链,就读懂了整个框架。 - 本地与沙箱两条路线按需选择:Manus + BrowserUseTool 在本地跑、直观省事儿;SandboxManus + Daytona 提供完全隔离的云端环境,还能通过 VNC 围观 Agent 操作,适合对安全和资源隔离有要求的场景。
- 浏览器自动化的本质是"让 LLM 看懂页面":OpenManus 默认用
[index]<type>text</type>的文本描述替代截图,成本低、效果好,但面对 JavaScript 动态渲染的复杂控件(如日历选择器)会失效。 - 复杂页面没有银弹:RAG 策略(知识库 + URL 直达 + 元素分类 + 时间注入 + 完成策略优化)相对稳定可控;GUI 视觉策略更通用,但不确定性更高、速度更慢。工程上最务实的做法是分层组合——Playwright 定位优先,JS 查找兜底,视觉模型救场。
- 调试手段决定迭代速度:保存 HTML、保存截图、标记点击坐标、精简日志,这些看似琐碎的工作,是让 Agent 从"偶尔能跑"走向"稳定可用"的关键。
参考链接:
- OpenManus:https://github.com/FoundationAgents/OpenManus
- openmanus-ui:https://github.com/aliasmaya/openmanus-ui
- Daytona:https://app.daytona.io/
- Manus:https://manus.im/

浙公网安备 33010602011771号