资讯动态

Manus-Manus:手脑分离的Agent架构设计与实践

发布时间:2026/10/14 14:54:56 来源:尧图企业网站定制
简介「manus-manus」是一套以Python为核心的3D数据处理与模型构建项目围绕姿态相关三维场景展开面向计算机视觉、三维重建及Blender工具链开发者与研究人员。压缩包共167个文件大小24.19MB其中113个Python脚本构成主程序逻辑16个yaml负责参数与流程配置11个shell脚本便于环境部署9个Blender工程文件提供姿态场景参考另有pkl、json等格式贯穿数据预处理与结果存储。描述中提到的数据集说明文档、preprocess预处理目录、环境初始化脚本等模块体现出项目重视工程化与可复现性方便读者按模块搭建环境、复现完整数据处理与模型构建流程。已有293人学习适合想快速借鉴三维姿态数据组织方式、Python项目文件结构与Git子模块管理经验的中高级开发者。1. manus-manus 到底是什么一套给 Agent 当手的 Agent很多人跑自主 Agent觉得只要把规划和执行写进同一段 Prompt模型就会自己把活干完。真跑一次就知道一次超过五步的任务大概率在第 6 步开始原地打转工具返回了模型却不读结果读了又忘了自己原本要干什么。我试过能稳定跑完的方向反而不是更聪明的单 Agent而是这种叫 manus-manus 的调度结构。它不迷信某个具体的 Agent 产品而是把“干活的人”拆成两个角色一个负责想下一步做什么一个负责把手上的动作真正执行完。凡是涉及网页操作、文件读写、多轮工具调用并且希望最后有可追溯成果的任务都适合从这里入门。2. Manus 式 Agent 的核心运行机制从单 Agent 到手足分工2.1 为什么单 Agent 连跑 5 步就掉链子单 Agent 最常见的做法是把任务、工具说明、历史对话全部塞进同一个上下文让模型看完之后一次性输出下一步动作。这在两三个工具调用内是可行的但只要任务链条稍微变长问题就变得很具体。第一个问题是注意力被稀释。工具返回的结果往往很长比如网页正文一次性回传五千字模型真正需要记住的只是“页面左上角有一个可点击的按钮”但它在生成下一步时不得不在五千字里重新找这个结论。每经历一步上下文里都会多一段这样的原始结果原始结果之间还会互相干扰最后模型连“已经完成了什么”都会判断错。第二个问题是“以为做了”和“真的做了”无法区分。单 Agent 在生成文字时很容易把“我应该点击登录按钮”当成“我已经点击了登录按钮”写进历史。因为它的输出只是文本没有经过真实环境的校验。等到下一步需要基于点击后的页面状态继续操作时模型面对的还是旧页面于是开始用猜测填补缺口。这两个问题叠加在一起就形成了最常见的掉链子现场任务做到一半Agent 开始输出重复的、自相矛盾的步骤每一步都在消耗 token却没有改变任何真实状态。这也是我把手脚分工当作核心结构的原因。2.2 手脑分工后的主循环计划—执行—回写—再计划manus-manus 的设计思路是不要让同一个模型同时承担“思考下一步”和“操作工具”两件事。规划端只负责输出一个结构化的动作执行端只负责把这个动作变成真实的系统调用并把结果压缩后回写给规划端。主循环可以用下面的伪代码表达# 主循环骨架manus-manus 的核心调度逻辑 for step in range(max_steps): # 1. 规划端根据任务、已有历史、工具清单输出下一步动作 action planner.plan(task, history, tool_list) if action.kind complete: return action.answer # 2. 执行端真实执行这个动作结果可能是页面跳转、文件写入、报错 result executor.run(action) # 3. 回写把结果压缩成结构化摘要而不是把原始输出整个塞进历史 history.append(action, result.summary) return {status: timeout, history: history}这段代码里有三个容易被忽略的设计要点。第一planner 永远不直接调用工具。它看到的是经过压缩的 result.summary比如“点击后页面跳转到了登录页”“文件写入成功大小 214 字符”。这样规划端不会被偶然的报错细节牵着走。第二executor 只认结构化的 action。动作里必须写清楚 tool_name 和 params比如工具名 click_page参数为 selector。这样工具调用可以被复现出错了也可以定位。第三循环有一个硬性的 max_steps 限制。这不是为了限制能力而是为了让超时态能被明确感知。一个 Agent 跑 40 步还没收尾已经说明计划收敛性出了问题继续往下跑只会增加成本。这个循环看起来简单但它能把“模型幻觉”和“环境真实状态”隔离开。模型可以在文本里说假话executor 的真实结果会把假话纠正回来。2.3 工具注册与状态隔离让模型不猜参数工具注册表是规划端能感知到的唯一接口。每个工具不能只是一个函数它需要有三样东西名字、用途描述、参数说明。# 工具注册表的简化定义 TOOL_REGISTRY [ { name: write_file, description: 把内容写入指定路径返回文件大小。适合保存任务产物。, input_schema: { path: string, 相对路径不要用绝对路径, content: string, 要写入的完整文本 } }, { name: click_page, description: 点击当前页面中匹配 selector 的元素。如果页面跳转等待最多 8 秒。, input_schema: { selector: string, 必须是唯一的 CSS 选择器 } } ]如果你的工具没有这段描述模型就只能靠猜。猜参数的后果是执行端收到一个根本匹配不到任何元素的 selector或者一个路径写错位置的文件写入请求然后报错规划端再猜一次来回几次预算就耗完了。另一个容易犯的错是把页面对象、数据库连接这类真实资源直接传给规划端。这会让模型有机会输出一些它根本不理解的内部状态操作。常见做法是浏览器实例由执行端持有规划端永远只拿文本摘要。这样即使模型幻觉影响的也只是下一次计划而不是直接弄脏当前会话状态。3. 本地跑通 manus-manus 最小闭环Python 调度核与浏览器操作层3.1 项目最小目录和调度核代码我建议的目录非常小不要一开始就上框架保持文件级可见即可。manus_manus/ app.py # 调度主循环 planner.py # 规划端生成 action executor.py # 执行端执行 action返回压缩结果 tools.py # 工具注册与真实实现 history.py # 历史压缩与回放 logs/ # 每次任务的回放 JSON调度核放在 app.py 里。下面这段是我调过很多遍之后最稳定的一版骨架适合先跑通再改造。# app.py import json from planner import parse_action from executor import execute_action from history import History def run_agent(task, chat_once, planner_model, max_steps40): history History() tool_list history.load_tool_descriptions() page None # 浏览器实例由执行端懒加载 for step in range(max_steps): # 规划端只看三样东西任务、压缩后的历史、工具清单 messages history.to_planner_messages(task, tool_list) raw chat_once( modelplanner_model, messagesmessages, temperature0.2 ) try: action parse_action(raw) except json.JSONDecodeError: history.add_system_event(规划输出无法解析已跳过该步) continue if action[kind] complete: return {status: complete, answer: action[answer], step: step, history: history.dump()} # 执行端拿到结构化动作后真正去调用浏览器或文件系统 result execute_action(action, page) history.add_step(action, result) return {status: timeout, history: history.dump()}这段代码里chat_once 是我习惯用的统一模型调用封装它接受 model、messages、temperature 三个参数屏蔽了不同服务商的协议差异。parse_action 负责把模型的文本输出解析成 JSON 结构。temperature 设为 0.2是为了尽量稳定输出格式而不是为了让它更有创意。history.add_step 里面会做压缩——只保留“动作名、参数摘要、结果摘要、是否成功”四个字段不保留原始长文本。3.2 注册 Tool 的方法文件写入、网页访问、点击操作工具的数量不需要一开始就很多。我一般先用三个写文件、读取页面文本、点击元素。三个工具足以覆盖一大批真实任务。# tools.py def write_file(path, content): with open(path, w, encodingutf-8) as f: f.write(content) return {ok: True, summary: f已写入 {path}大小 {len(content)} 字符} def read_page_text(page, selectorbody): # 只拿文本不拿 HTML避免把样式和脚本塞进上下文 text page.eval_selector(selector, node node.innerText) return {ok: True, summary: text[:3000]} def click_page(page, selector): page.wait_visible(selector, timeout8000) page.click(selector) return {ok: True, summary: f已点击 {selector}} # 统一分发入口 def execute_action(action, page): name action[tool] fn_map { write_file: lambda p: write_file(p[path], p[content]), read_page_text: lambda p: read_page_text(page, p.get(selector, body)), click_page: lambda p: click_page(page, p[selector]), } if name not in fn_map: return {ok: False, summary: f未知工具 {name}} try: return fn_map[name](action[params]) except Exception as e: return {ok: False, summary: f执行异常: {str(e)[:200]}}read_page_text 里的截断是刻意的。一次只拿 3000 字符比拿整页更有利于规划端判断下一步。如果任务确实需要全文可以再增加一个 read_full_page 工具让模型明确知道自己需要长文本。click_page 里的 wait_visible 不能省。很多页面是异步加载的模型以为元素已经出现实际上还处于 loading 状态。提前点击的结果要么是超时要么是点到错误位置。工具描述里最好写明“不要用绝对路径”这类约束。模型如果看到相对路径示例就不会自己脑补一个盘符路径出来。3.3 能直接套用的规划 Prompt 与任务回放日志规划端的 Prompt 我保持得非常短只做三件事交代任务、给出工具清单、要求输出 JSON。PLANNER_PROMPT 你是任务规划端。你的目标是让执行端一步一步完成任务。 当前任务{task} 已完成的历史步骤 {history} 可用工具 {tool_list} 请只输出一个 JSON 对象格式如下 - 如果还需要执行动作{{kind: call, tool: 工具名, params: {{参数}}}} - 如果任务已经完成{{kind: complete, answer: 最终结果}} 不要输出任何多余的解释文字。 # 示例调用 messages [ {role: system, content: PLANNER_PROMPT.format(...)} ]为什么要强调“不要输出任何多余的表达文字”因为很多模型在输出 JSON 前会先来一句“我将按照以下步骤执行”。解析层如果没做好兼容这句废话会让整个 action 变成无效输入。任务回放日志是 manus-manus 里我最后悔没早点加的部分。每跑一步就把动作和结果追加到 JSON 文件里。# history.py import json, time class History: def __init__(self): self.steps [] def add_step(self, action, result): self.steps.append({ action: action, result_summary: result[summary], ok: result[ok], ts: time.time() }) def dump(self): return {steps: self.steps}日志里必须记录 result_summary 和 ok 两个字段。回放时只要看 ok 字段就能知道哪一步真正失败只要看 action就能知道模型是不是在重复调用同一个工具。没有这套日志排错就只能靠玄学。4. manus-manus 的三个必调参数temperature、max_steps、递归重试4.1 temperature 分层规划器低温执行器次低温同一个模型在不同位置应该用不同的 temperature。规划端负责决定下一步它应该尽量稳定、格式准确温度太高会让工具名和参数名漂移。执行端负责真实调用和结果判断同样需要低温度。角色建议温度主要风险规划端0.1 - 0.2温度过高会导致 JSON 输出变形执行端结果判断0.1 - 0.2温度过高会误判是否成功创意内容生成0.6 - 0.8只用于写文案、改标题等生成任务我自己一般把规划端固定在 0.2执行端固定在 0.1。如果某一次出现工具名拼写错误我第一反应不是换模型而是检查是不是把温度调高了。4.2 max_steps 步数预算用预算倒逼规划收敛max_steps 不要给一个固定值就完事。我通常在任务文本里附带“当前是第几轮剩余步数还剩多少”让模型在接近预算上限时自动切换策略。budget_note f 当前已经执行到第 {step} 步最多允许 {max_steps} 步。 如果剩余步数少于 5 步请只输出能直接收尾的动作。 .strip()当剩余步数变少时模型如果还在做探索性操作最后一定会超时。加上这条提醒之后模型会更早地进入收尾模式把未完成的部分写入文件而不是继续尝试完美解决。max_steps 的默认值我会看任务类型单纯读一个页面给 15 步需要多页面跳转并产文件的给 40 步。超过 40 步的任务我会先怀疑规划端是不是没有理解任务边界。4.3 递归重试给工具调用一次后悔药工具调用失败后不能直接让整个任务停下来也不能无脑重试同一个动作。正确的做法是把失败原因作为一个新的用户消息追加到对话末尾让规划端重新输出一个动作。# 失败的递归重试逻辑 for attempt in range(3): result execute_action(action, page) if result[ok]: break retry_message { role: user, content: ( f第 {attempt 1} 次执行失败{result[summary]}。 请换一个工具或修改参数重新尝试只输出 JSON。 ) } messages.append(retry_message) raw chat_once(modelplanner_model, messagesmessages, temperature0.1) action parse_action(raw)重试次数限制在 3 次以内。超过 3 次还能失败的基本不是参数问题而是规划端对这个任务的理解本身有偏差继续重试只是浪费 token。这里还有一个容易被忽略的问题重试时要把执行工具是不是改变了部分状态写清楚。比如“文件已经创建但内容写入失败”这种情况就不能直接再写一次文件否则可能产生重复内容。5. manus-manus 避坑四种最常翻车的场景和处理办法5.1 死循环同一个 tool 被反复调用结果不改现象日志里同一个 click_page 被调用了 7 次selector 完全一样每次返回结果都是“已点击”。但页面状态没有任何变化任务也一直没有进入 complete。原因模型在上一步判断里已经认为“点击成功后页面会跳转”但实际的页面可能根本没有响应。回写结果里又只写了“已点击”没有写“点击后页面标题还是原样”规划端没有收到状态未变更的信号于是继续点。解决我在 execute_action 里加了一个重复动作计数器。如果连续三次动作名和参数完全相同就把结果摘要改为“连续 3 次重复动作页面状态无变化”让规划端知道这条路已经走不通。if same_action_params(action, history.last_action_params, repeat3): return {ok: False, summary: 连续三次重复动作状态未改变请换策略}连续重复的检测必须在执行端做不能依赖规划端的自我觉察。5.2 JSON 解析失败模型输出了 Markdown 围栏和多余文字现象parse_action 直接抛 JSONDecodeError日志里能看到模型输出前加了“好的我来执行”或者把 JSON 包进了 json 代码块。原因很多模型在强指令环境下会自动套用代码块格式尤其是你给了它带 Markdown 的示例之后它会把格式当成内容的一部分。解决解析时先剥离多余部分。我写了一个兼容函数先尝试json.loads失败则用正则从第一个 { 提取到最后一个 }再尝试一次。def parse_action(raw): try: return json.loads(raw) except json.JSONDecodeError: start raw.find({) end raw.rfind(}) if start -1 or end -1: raise return json.loads(raw[start:end1])这个函数不能替代格式约束它只是兜底。最可靠的办法始终是规划 Prompt 里“只输出 JSON”的强调加解析兜底双保险。5.3 上下文被长页面内容撑爆token 消耗失控现象任务跑到第 20 步之后单次响应的速度明显变慢费用也突然增加。打开日志一看read_page_text 的返回被完整塞进了历史。原因我在最早的一版里偷懒把页面的长文本当作 summary 保存。结果每一步之后下一次规划都带着这五千字文本历史越长重复阅读的负担越重。解决所有回写内容都必须先截断再入库。页面文本默认截断到 3000 字符且要优先截取正文部分避免把侧边栏和底部的重复导航文本带进来。如果任务确实需要全页内容用一个独立的 read_full_page 工具并且只在规划端明确声明需要时才允许使用。还有一个更省预算的办法对页面文本做关键词摘要。比如提取页面前 200 个有信息量的字符或者只保留标题、链接文本、主要段落而不是直接说“这个页面很长”。5.4 浏览器点击失效元素等待和可见性没对齐现象click_page 抛出超时异常或者点击没有产生任何跳转任务卡死在这一步。原因页面是异步加载的元素可能已经出现在 DOM 里但还处于不可见或不可点状态。有些页面还会在点击动作执行后立刻重绘导致命中元素失效。解决点击前先做“可见且可交互”等待点击后再检测页面是否发生了预期变化。def click_page(page, selector, expect_url_changeFalse): before_url page.url() page.wait_visible(selector, timeout8000) page.click(selector) if expect_url_change: page.wait_url_change(before_url, timeout8000)expect_url_change 这个参数值得根据任务动态设置。模型判断“点击后应该跳转”时就要求等待 URL 变化如果只是展开一个菜单则不等待跳转避免误判超时。6. 验证与进阶把 manus-manus 从“能跑”推到“跑稳”6.1 固定验收任务集五个任务看完成率、步数和耗时不要用单一任务判断框架是否合格。我整理了一套固定任务集每个任务都同时覆盖工具调用、状态判断和历史回写能力。任务描述验收标准平均步数主要失败点打开一个示例页面提取标题和正文前 500 字写入 markdown文件存在且内容正确8 - 12页面加载等待事件读取本地 CSV计算指定列求和结果写入新文件结果文件数值正确6 - 10参数名拼写错误依次访问三个页面记录每个页面的 H1 标题三条标题都正确15 - 20第 2 页点击失效根据指定 JSON 配置创建三个不同内容的文件三个文件内容匹配10 - 16规划端截断过早多轮问答类任务最后把结论写进文件文件包含最终结论12 - 18历史压缩过于激进每个任务至少跑 3 次。因为模型有随机性单次成功可能是运气单次失败也可能是意外只有三次结果才算有点参考价值。6.2 进阶调度策略慢脑快手、会话摘要、自愈钩子第一版跑稳之后可以往三个方向走。慢脑快手规划端用能力更强但速度较慢的模型执行端用便宜且速度快的模型。手脑分离之后两个模型不用同时升级成本能降下来不少。会话摘要历史步数超过一定阈值时不直接追加所有步骤而是先把旧步骤压缩成三句摘要再接新一轮。这一步对长任务至关重要能避免上下文无限膨胀。自愈钩子在 executor 返回失败结果后先不急着让规划端重新规划而是用一段规则代码判断是否有明显的修复路径。比如“文件路径不存在”这类确定性错误规则可以直接修正路径不需要模型再次思考。我在近几个项目上的体会是这类两段式结构的稳定性取决于你对日志的耐心。每一条卡顿背后都藏着一个参数该调的信号参数调顺之后中间步骤的意外会少很多。跑顺一轮再谈规模而不是一上来就堆多个工具和复杂模型。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑