概览
我们会研究并分析三个项目
- manus 首个通用 agent
- minimax agent ,minimax 团队推出的更加完善的通用 agent 智能体
- openmanus 所谓 manus 的开源方案
通过这三个项目来看看现在所谓的“通用” agent 的能力边界和底层实现原理。
正版 Manus
工作流程
和通常的 agnet 类似,manus 会根据提示词调用工具搜索相关的信息
还会调用 browser use 去访问实际的页面
然后会编写 markdown 文档
最后用自己的 manus-md-to-pdf 工具,将 markdown 转换成 pdf
部署网页
然后我让 manus 基于之前的研究,将结果编写成网页
manus 则调用命令,创建了一个 react 项目,并且编辑代码
,最后还会去使用浏览器去测试网页
需要注意的是这里的创建开发项目的命令为
manus-create-react-app deepseek-research-website 我查看了他的实际源码,他实际的技术栈为
vite + shadcn + tailwindcss 是一个目前前端网页开发非常流行且常见的技术栈,他为了减少后期需要去引入一些依赖或者编写多余的组件,先统一了这个脚手架
可以看到不管这里的组件有没有用到,都给你先装好了,避免还需要写代码
最后 manus 会把编译好的静态代码部署到公网
我试过让 manus 部署一个带有后端服务的项目,但是 manus 无法实现这个功能,也就是说只能部署一个静态的项目,不过如果实在私有的环境中,理论上完全可以让 agent 在虚拟环境中部署后端服务等
在 minimax 里面,创建前端项目直接调用了工具
看不到具体是什么命令,但是从项目文件来看,技术栈是差不多的,都是
vite + tailwind + shadcn 同样也把所有的组件都预制了,
不过在卖力程度上, minimax 显然更加的卖力,他额外编写的代码更多,并且主动去修改了样式,
这里估计也是对于 agent 的 prompt 的特殊调整
在完成之后 minimax 会使用 browser use 去检查网站的结果
这一点还是非常细致的
花费
minimax 免费送 1000 的积分,我用了不到三个提问,花完了,按月付费 16美刀/月,每个月 5000 积分,可能只能用 15 次。
可以看出来通用 Agent 的 花费是很高的,特别是 minimax 倾向于给你更多内容
相比之下 manus 非常的偷懒,倾向于减少输出。
问题
总体的报告太过于简单,不够规范,数据会留空,无法找到实际数据
会出现有些数据无法获取的情况,这时候会留空
总结 manus
manus 的前端体验是十分丝滑的,在 Agent 操作的可视化上做的很好,操作的每一个步骤,你都可以看到详细过程,执行的命令,打开的浏览器,任务的数量,进度,还可以用用远程 vscode 打开对应的环境,查看创建的文件,脚本等等。而且过程没有太多卡顿。
可以看到 manus 在工程化方面是做的比较出色的,用户体验和完成度相对来说也是比较高的。
Manus 相当于是一个拥有隔离沙盒环境的程序员,一上来打开的是一个命令行终端,在里面用文本编辑器写了个 todo list,干活的过程中不断写代码做自动化,最后的交付件(Artifact)也是一段代码(交互式网页和图表)。
但背后的核心能力其实还是依赖于大模型其所谓的“自主规划”能力,实则是基于现有大语言模型(如GPT-4)的调用,并未展示底层技术的突破。例如,在分析案例中,Manus仅通过调用雅虎金融API获取数据,再借助Python脚本生成可视化图表,本质仍是工具链的拼接。
我们可以学习的一点就是,沙盒环境下的 agent 自主执行能力。以及过程透明展示的用户交互方式
minimax agent
和 manus 类似,他也会去规划任务,然后搜索信息,整理信息
生成图表
在生成图表之前, minimax agent 会生成图表数据对应的 deepseek_charts_data.md markdown 文件,来暂存数据,这种方式可以减轻上下文压力,防止将整个上下文都包含进去消耗太多 token
然后会调用 python 来生成图表
最后会使用 python 脚本来生成 pdf
从结果来看 minmax agent 的 PDF 的内容和样式明显要比 manus 好不少,内容更多,样式更加规范完善
其中的原因主要也是 minimax 使用的生成 PDF 的方式是 python 而非 markdown 转 PDF, 所以在样式的控制上更加的灵活方便,所以最后的结果也是更加的合理
我自己也尝试过让 claude-4-sonnet max 版本也生成 python 代码来生成 pdf ,没有特别的提示词,结果相比 minimax agent 的输出结果,还是有比较大的差别,可以一定程度说明,对于生成 PDF 这种场景,应该是对提示词有特殊优化的,才能保证大部分情况下生成的 pdf 的格式是合理的
OpenManus
尝试
在尝试运行 openmanus 的过程中遇到很多问题,包括但不限于
1. browser-use 无法执行 extract_content 命令,导出文案
2. 一直在一个页面尝试滚动,一直到 20 次工具调用限制用完
3. 在沙盒环境中安装用于导出 pdf 的工具失败
4. 文件编辑后字符编码乱码
总而言之就是架构上模仿,实际细节中完全没有打磨,比如没有内置自己的 pdf 导出工具,没有自己的前端项目模板,导致看起来执行流程和原版差不多,但是实际交付内容就是一坨未经雕饰的泥巴
但这也说明了一件事情,所有通用 agent ,其背后的本质还是人工,如果没有人去优化打磨一个比较好的 pdf 导出格式的 prompt ,如果没有提供一份合适的前端项目模板来作为基地,所谓通用 agent 能做到的能力是十分有限的,因为目前大语言模型的能力就是十分有限的,必须经过 prompt 优化来确保其输出是相对优秀的。
总的来说 openmanus 项目并没有能达到 manus 能力的 1/3,而 manus 能力也是存在局限性,整个所谓通用 agent 概念目前来说还没有达到人们对他的期待, minimax agent 虽然在交付内容上的打磨足够精细,但是其高昂的价格也注定其实用性是比较差的。
那相比我们之前解析的开源 agent ,manus 相对有可以借鉴的地方就是沙盒环境。
项目架构
OpenManus/ ├── app/ # 核心应用代码 │ ├── agent/ # Agent 实现层 │ ├── flow/ # 工作流管理层 │ ├── tool/ # 工具层 │ ├── prompt/ # 提示词管理 │ ├── mcp/ # MCP 协议支持 │ └── sandbox/ # 沙箱执行环境 ├── config/ # 配置管理 ├── examples/ # 使用示例 └── tests/ # 测试代码
🏗️ 沙箱系统架构
1. 核心组件层次结构
app/sandbox/ ├── client.py # 客户端接口层 ├── core/ │ ├── sandbox.py # 核心沙箱实现 │ ├── manager.py # 沙箱管理器 │ ├── terminal.py # 终端交互接口 │ └── exceptions.py # 异常定义 └── __init__.py # 模块导出
2. 沙箱实现原理
基于 Docker 的容器化
class DockerSandbox: """Docker 沙箱环境实现""" def __init__(self, config, volume_bindings): self.config = config or SandboxSettings() self.volume_bindings = volume_bindings or {} self.client = docker.from_env() # Docker 客户端 self.container = None self.terminal = None
核心特性:
- 资源隔离 - 使用 Docker 容器提供完全隔离的执行环境
- 资源限制 - 可配置内存、CPU 等资源限制
- 网络控制 - 支持禁用网络或使用桥接网络
- 文件系统隔离 - 容器内文件系统与宿主机隔离
沙箱创建流程
async def create(self) -> "DockerSandbox": """创建并启动沙箱容器""" # 1. 准备容器配置 host_config = self.client.api.create_host_config( mem_limit=self.config.memory_limit, # 内存限制 cpu_period=100000, # CPU 调度周期 cpu_quota=int(100000 * self.config.cpu_limit), # CPU 配额 network_mode="none" if not self.config.network_enabled else "bridge", binds=self._prepare_volume_bindings(), # 卷挂载 ) # 2. 创建容器 container = await asyncio.to_thread( self.client.api.create_container, image=self.config.image, command="tail -f /dev/null", # 保持容器运行 working_dir=self.config.work_dir, host_config=host_config, name=f"sandbox_{uuid.uuid4().hex[:8]}", # 唯一名称 tty=True, detach=True, ) # 3. 启动容器并初始化终端 await asyncio.to_thread(self.container.start) self.terminal = AsyncDockerizedTerminal(container["Id"]) await self.terminal.init()
3. 主进程调度机制
SandboxManager 负责生命周期管理
class SandboxManager: """沙箱管理器 - 负责多个沙箱的调度和管理""" def __init__(self, max_sandboxes=100, idle_timeout=3600): self.max_sandboxes = max_sandboxes # 最大沙箱数量 self.idle_timeout = idle_timeout # 空闲超时时间 self._sandboxes = {} # 活跃沙箱映射 self._last_used = {} # 最后使用时间 self._locks = {} # 并发控制锁 self._global_lock = asyncio.Lock() # 全局锁
调度策略
- 资源池管理
async def create_sandbox(self, config, volume_bindings) -> str: """创建新沙箱实例""" async with self._global_lock: # 检查资源限制 if len(self._sandboxes) >= self.max_sandboxes: raise RuntimeError("达到最大沙箱数量限制") # 确保 Docker 镜像可用 if not await self.ensure_image(config.image): raise RuntimeError("Docker 镜像不可用") # 创建沙箱实例 sandbox_id = str(uuid.uuid4()) sandbox = DockerSandbox(config, volume_bindings) await sandbox.create() # 注册到管理器 self._sandboxes[sandbox_id] = sandbox self._last_used[sandbox_id] = time.time() self._locks[sandbox_id] = asyncio.Lock()
- 并发控制
@asynccontextmanager async def sandbox_operation(self, sandbox_id: str): """沙箱操作的上下文管理器""" async with self._locks[sandbox_id]: if sandbox_id not in self._sandboxes: raise KeyError(f"沙箱 {sandbox_id} 不存在") self._active_operations.add(sandbox_id) try: # 更新使用时间 self._last_used[sandbox_id] = time.time() yield self._sandboxes[sandbox_id] finally: self._active_operations.remove(sandbox_id)
- 自动清理机制
async def _cleanup_idle_sandboxes(self): """清理空闲沙箱""" current_time = time.time() to_cleanup = [] for sandbox_id, last_used in self._last_used.items(): if (sandbox_id not in self._active_operations and current_time - last_used > self.idle_timeout): to_cleanup.append(sandbox_id) for sandbox_id in to_cleanup: await self._safe_delete_sandbox(sandbox_id)
4. 终端交互机制
AsyncDockerizedTerminal 实现
class AsyncDockerizedTerminal: """异步 Docker 终端接口""" async def run_command(self, cmd: str, timeout=None) -> str: """在沙箱中执行命令""" if not self.session: raise RuntimeError("终端未初始化") try: # 执行命令并等待结果 result = await asyncio.wait_for( self.session.execute(cmd), timeout=timeout or self.default_timeout ) return result except asyncio.TimeoutError: raise TimeoutError(f"命令执行超时: {cmd}")
Socket 通信机制
class DockerSession: """Docker 会话管理""" async def execute(self, command: str) -> str: """通过 Socket 执行命令""" # 发送命令 full_command = f"{sanitized_command}\\necho $?\\n" self.socket.sendall(full_command.encode()) # 异步读取输出 async def read_output(): buffer = b"" while True: chunk = self.socket.recv(4096) if not chunk: break buffer += chunk # 检测命令结束标志 if buffer.endswith(b"$ "): break return buffer.decode("utf-8")
5. 文件操作机制
容器文件系统交互
async def read_file(self, path: str) -> str: """从容器读取文件""" try: # 获取文件归档 tar_stream, _ = await asyncio.to_thread( self.container.get_archive, path ) # 从 tar 流中读取内容 content = await self._read_from_tar(tar_stream) return content.decode("utf-8") except NotFound: raise FileNotFoundError(f"文件不存在: {path}") async def write_file(self, path: str, content: str) -> None: """向容器写入文件""" # 创建 tar 流 tar_stream = await self._create_tar_stream( os.path.basename(path), content.encode("utf-8") ) # 写入容器 await asyncio.to_thread( self.container.put_archive, os.path.dirname(path), tar_stream )
6. 配置与安全
沙箱配置
class SandboxSettings(BaseModel): use_sandbox: bool = False # 是否启用沙箱 image: str = "python:3.12-slim" # 基础镜像 work_dir: str = "/workspace" # 工作目录 memory_limit: str = "512m" # 内存限制 cpu_limit: float = 1.0 # CPU 限制 timeout: int = 300 # 默认超时 network_enabled: bool = True # 网络访问
安全隔离
- 容器隔离 - 每个沙箱运行在独立的 Docker 容器中
- 资源限制 - 严格的内存和 CPU 配额
- 网络控制 - 可选择禁用网络访问
- 文件系统隔离 - 容器内外文件系统完全隔离
- 权限控制 - 容器内 root 权限不影响宿主机
工作流程总结
- 创建阶段: 主进程通过
SandboxManager创建 Docker 容器
- 初始化阶段: 建立终端连接,配置工作环境
- 执行阶段: 通过 Socket 异步执行命令和文件操作
- 管理阶段: 并发控制、资源监控、超时处理
- 清理阶段: 自动清理空闲沙箱,释放资源
这种设计确保了安全性、隔离性和高性能,同时提供了灵活的资源管理和并发控制机制。
🏗️ 沙箱功能暴露机制
那么沙箱中的功能是如何暴露给 agent 的呢
1. 通过工具层抽象暴露
FileOperator 接口模式
# app/tool/file_operators.py @runtime_checkable class FileOperator(Protocol): """文件操作接口 - 统一本地和沙箱操作""" async def read_file(self, path: PathLike) -> str: ... async def write_file(self, path: PathLike, content: str) -> None: ... async def run_command(self, cmd: str, timeout: Optional[float] = 120.0) -> Tuple[int, str, str]: ... # 本地文件操作实现 class LocalFileOperator(FileOperator): async def read_file(self, path: PathLike) -> str: return Path(path).read_text(encoding=self.encoding) # 沙箱文件操作实现 class SandboxFileOperator(FileOperator): def __init__(self): self.sandbox_client = SANDBOX_CLIENT # 全局沙箱客户端 async def read_file(self, path: PathLike) -> str: await self._ensure_sandbox_initialized() return await self.sandbox_client.read_file(str(path))
2. 全局沙箱客户端
单例模式的沙箱客户端
# app/sandbox/client.py SANDBOX_CLIENT = create_sandbox_client() # 全局实例 # 在 BaseAgent 中使用 # app/agent/base.py from app.sandbox.client import SANDBOX_CLIENT class BaseAgent(BaseModel, ABC): async def run(self, request: Optional[str] = None) -> str: # 执行逻辑... await SANDBOX_CLIENT.cleanup() # 确保清理
3. 工具级别的沙箱集成
文件编辑工具的沙箱判断
# app/tool/str_replace_editor.py class StrReplaceEditor(BaseTool): _local_operator: LocalFileOperator = LocalFileOperator() _sandbox_operator: SandboxFileOperator = SandboxFileOperator() def _get_operator(self) -> FileOperator: """根据配置选择文件操作器""" return ( self._sandbox_operator if config.sandbox.use_sandbox # 🔑 核心判断逻辑 else self._local_operator ) async def execute(self, *, command: Command, path: str, **kwargs) -> str: # 获取适当的操作器 operator = self._get_operator() # 执行文件操作 if command == "view": result = await self.view(path, view_range, operator) elif command == "create": await operator.write_file(path, file_text) # ...
2. 工具层面的自动判断
动态操作器选择
# 沙箱文件操作器会自动初始化沙箱 class SandboxFileOperator(FileOperator): async def _ensure_sandbox_initialized(self): """确保沙箱已初始化""" if not self.sandbox_client.sandbox: await self.sandbox_client.create(config=SandboxSettings()) async def read_file(self, path: PathLike) -> str: await self._ensure_sandbox_initialized() # 🔑 自动初始化 return await self.sandbox_client.read_file(str(path))
3. Agent 不直接感知沙箱
透明的工具调用
# Agent 调用工具时不需要知道是否使用沙箱 class Manus(ToolCallAgent): available_tools: ToolCollection = Field( default_factory=lambda: ToolCollection( PythonExecute(), # Python 执行 (目前未使用沙箱) StrReplaceEditor(), # 文件编辑 (根据配置选择沙箱) BrowserUseTool(), # 浏览器工具 # ... ) ) # Agent 只调用工具,不关心底层实现 async def execute_tool(self, command: ToolCall) -> str: result = await self.available_tools.execute( name=command.function.name, tool_input=args )
架构图
graph TB subgraph "Agent 层" A[Manus Agent] --> B[ToolCollection] A1[BaseAgent] --> B A2[SWEAgent] --> B end subgraph "Tool 层" B --> C[StrReplaceEditor] B --> D[PythonExecute] B --> E[Bash] B --> F[BrowserUseTool] C --> G{config.sandbox.use_sandbox?} end subgraph "FileOperator 抽象层" G -->|true| H[SandboxFileOperator] G -->|false| I[LocalFileOperator] H --> J[SANDBOX_CLIENT] I --> K[本地文件系统] end subgraph "Sandbox 层" J --> L[LocalSandboxClient] L --> M[DockerSandbox] M --> N[AsyncDockerizedTerminal] N --> O[Docker Container] end subgraph "配置层" P[config.toml] --> Q[SandboxSettings] Q --> G Q --> L end style A fill:#e1f5fe style G fill:#fff3e0 style H fill:#f3e5f5 style J fill:#e8f5e8 style O fill:#ffebee
除了文件操作,沙箱还提供了
1. run_command 来在沙箱中运行任何命令行操作
2. python 运行时支持 ,可以使用 python 代码来操作环境,并且可以通过 pip 安装 python 依赖
总结
沙盒环境理论上对于 agent 能力会有非常大的提升,但是需要通过打磨细节来保证其运行的流畅性,尤其是如何给用户一个比较完善和美观的交付内容,是这类开源产品需要进一步打磨的
TODO
- 看一下其他的通用 agent 是否提供更加有用户场景的功能?更垂类和高频的实用领域
