简介:「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_steps=40): 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( model=planner_model, messages=messages, temperature=0.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", encoding="utf-8") as f: f.write(content) return {"ok": True, "summary": f"已写入 {path},大小 {len(content)} 字符"} def read_page_text(page, selector="body"): # 只拿文本,不拿 HTML,避免把样式和脚本塞进上下文 text = page.eval_selector(selector, "node => node.innerText") return {"ok": True, "summary": text[:3000]} def click_page(page, selector): page.wait_visible(selector, timeout=8000) 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(model=planner_model, messages=messages, temperature=0.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, repeat=3): 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:end+1])这个函数不能替代格式约束,它只是兜底。最可靠的办法始终是规划 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_change=False): before_url = page.url() page.wait_visible(selector, timeout=8000) page.click(selector) if expect_url_change: page.wait_url_change(before_url, timeout=8000)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 返回失败结果后,先不急着让规划端重新规划,而是用一段规则代码判断是否有明显的修复路径。比如“文件路径不存在”这类确定性错误,规则可以直接修正路径,不需要模型再次思考。
我在近几个项目上的体会是,这类两段式结构的稳定性,取决于你对日志的耐心。每一条卡顿背后都藏着一个参数该调的信号,参数调顺之后,中间步骤的意外会少很多。跑顺一轮,再谈规模,而不是一上来就堆多个工具和复杂模型。希望帮到你。
本文还有配套的精品资源,点击获取