news 2026/9/26 3:34:13

Manus逆向工程:用Python拆解AI智能体的ReAct与Plan-Execute

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Manus逆向工程:用Python拆解AI智能体的ReAct与Plan-Execute

1. 从 Manus 日志里能拆出什么:ReAct 与 Plan-Execute 的混合骨架

Manus 这类 AI 智能体最值得研究的地方,不是它“能做什么”,而是它“怎么想”。我把它公开的行为日志逐条拆开看,发现它并不是单纯跑一个 ReAct 循环,也不是一次性把计划列完就闷头执行,而是把 Plan-Execute 和 ReAct 叠在一起用:高层用计划-执行管方向,底层用思考-行动-观察管细节。这个结构用 Python 复现出来并不复杂,难的是把两条路线的边界划清楚。

先说结论:Plan-Execute 负责“把模糊需求拆成宏观步骤”,ReAct 负责“在单个步骤里反复和环境交互直到搞定”。比如“总结 DeepSeek 训练加速技巧”这个请求,Plan-Execute 层会拆成搜索、浏览、整理、验证、写报告五步;而“浏览某个 URL 并提取关键信息”这一步,内部会跑一个 Thought → Action → Observation 的小循环,可能反复打开多个页面、写多次 todo.md 才收敛。

适合谁看:已经会用 Python 调大模型 API、想自己搭一个能规划又能调工具的智能体骨架的开发者。如果你只想要一个现成产品,这篇不适合;如果你想搞清楚“思考”和“行动”到底怎么交替,下面这套骨架可以直接跑。

我试过把两条路线硬塞进一个 while 循环里,结果日志乱成一团,后来才想明白:Plan-Execute 是外层 for,ReAct 是内层 while,两者职责不同,不能混。下面按这个思路一步步搭。

2. 前置准备:用 TaoToken 统一 Key 接入,别在多个平台间来回切

复现智能体最烦的不是写循环,是模型接入。ReAct 循环里每一步都要调 LLM,如果规划用一个平台、思考用另一个平台,Key 管理和计费会把人逼疯。我的做法是用 TaoToken 做统一入口,一个 Key 覆盖规划、思考、工具参数生成这几类调用。

TaoToken 在这里的角色是模型调用的统一网关,官网是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 端点是 https://taotoken.net/api 。你需要在控制台创建一个 API Key,然后把它写进环境变量,代码里只认一个 base_url 和一个 key,切换模型时改 model 字段就行,不用动调用逻辑。

具体操作:进控制台 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite 创建 Key,然后在 API Keys 页面 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 复制出来。接入细节如果卡住,直接看接入文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有 base_url 和鉴权头的写法。

注意:Key 只放环境变量,别硬编码进 settings.json 或 config.toml 后提交到仓库。我见过有人把 Key 写进配置文件推到公开仓库,几分钟就被刷爆额度。

这一步做完,后面所有 LLM 调用都走同一个客户端,ReAct 循环里换模型只改一个字符串。

3. 可复制配置骨架:settings.json 与 config.toml

配置分两层:settings.json 管运行时参数(模型、超时、迭代上限),config.toml 管工具白名单和路径。分开的好处是调参不用改代码,工具权限也能单独审。

先看 settings.json:

{ "llm": { "base_url": "https://taotoken.net/api", "api_key_env": "TAOTOKEN_API_KEY", "planner_model": "claude-sonnet-4-5", "react_model": "claude-sonnet-4-5", "timeout_seconds": 60, "max_retries": 2 }, "agent": { "max_plan_steps": 8, "max_react_iterations": 6, "scratchpad_file": "todo.md", "log_level": "INFO" } }

planner_model 和 react_model 分开配,是因为规划需要更强的长上下文推理,ReAct 单步思考可以换更便宜的模型压成本。max_react_iterations 是防止死循环的保险丝,设 6 意味着单个子目标最多思考-行动 6 轮,超了就强制跳出并记录。

再看 config.toml:

[tools.browser] enabled = true allowed_actions = ["search", "open_url", "extract_text"] timeout_seconds = 30 [tools.python_interpreter] enabled = true allowed_actions = ["shell", "read_file", "write_file", "list_files"] workdir = "./workspace" allow_shell = true [memory] scratchpad_path = "./workspace/todo.md" artifact_dir = "./workspace/artifacts"

allow_shell = true 是双刃剑,本地跑通链路时方便,但生产环境建议关掉,只留 read_file/write_file。workdir 限定在 ./workspace,避免工具误操作到项目根目录。

提示:config.toml 里的 allowed_actions 是白名单,LLM 生成的 action 如果不在列表里,直接拒绝并作为 Observation 返回“工具不可用”,这样 ReAct 循环会自己换策略,而不是崩掉。

4. 用 Python 复现思考-行动循环:主循环与工具层

核心就两个类:Agent 主循环和 Tool 执行器。主循环负责 Plan-Execute 的外层 for 和 ReAct 的内层 while,工具层负责把 LLM 生成的 action 翻译成真实调用。

先写 LLM 客户端,统一走 TaoToken:

import os, json, requests class LLMClient: def __init__(self, cfg): self.base_url = cfg["llm"]["base_url"] self.api_key = os.environ[cfg["llm"]["api_key_env"]] self.timeout = cfg["llm"]["timeout_seconds"] def chat(self, model, messages, temperature=0.2): resp = requests.post( f"{self.base_url}/v1/chat/completions", headers={ "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", }, json={ "model": model, "messages": messages, "temperature": temperature, }, timeout=self.timeout, ) resp.raise_for_status() return resp.json()["choices"][0]["message"]["content"]

再写工具层,把 browser 和 python_interpreter 的动作分发出去:

import subprocess, pathlib class ToolBox: def __init__(self, cfg): self.cfg = cfg self.workdir = pathlib.Path(cfg["tools"]["python_interpreter"]["workdir"]) self.workdir.mkdir(parents=True, exist_ok=True) def execute(self, tool_name, action, params): if tool_name == "browser": return self._browser(action, params) if tool_name == "python_interpreter": return self._python(action, params) return f"Error: unknown tool {tool_name}" def _browser(self, action, params): if action == "search": return f"[mock] search results for: {params.get('query')}" if action == "open_url": return f"[mock] page content of {params.get('url')}" if action == "extract_text": return f"[mock] extracted text: {params.get('content', '')[:200]}" return "Error: browser action not allowed" def _python(self, action, params): if action == "write_file": p = self.workdir / params["path"] p.parent.mkdir(parents=True, exist_ok=True) p.write_text(params["content"], encoding="utf-8") return f"File written: {p}" if action == "read_file": p = self.workdir / params["path"] return p.read_text(encoding="utf-8") if p.exists() else "Error: file not found" if action == "shell": r = subprocess.run( params["command"], shell=True, cwd=self.workdir, capture_output=True, text=True, timeout=30, ) return f"rc={r.returncode}\nstdout={r.stdout}\nstderr={r.stderr}" return "Error: python action not allowed"

主循环把两层拼起来,关键是内层 while 的退出条件:LLM 返回 finish_sub_goal 或达到迭代上限:

class Agent: def __init__(self, cfg, llm, toolbox): self.cfg = cfg self.llm = llm self.toolbox = toolbox self.scratchpad = [] def run(self, user_request): plan = self._make_plan(user_request) for step in plan: self._run_react(step) def _make_plan(self, user_request): prompt = ( f"把任务拆成不超过{self.cfg['agent']['max_plan_steps']}个宏观步骤," f"每行一个,不要编号。任务:{user_request}" ) raw = self.llm.chat(self.cfg["llm"]["planner_model"], [{"role": "user", "content": prompt}]) return [s.strip() for s in raw.splitlines() if s.strip()] def _run_react(self, sub_goal): for i in range(self.cfg["agent"]["max_react_iterations"]): thought = self.llm.chat( self.cfg["llm"]["react_model"], [{"role": "user", "content": f"子目标:{sub_goal}\n历史:{self.scratchpad[-3:]}\n" f"输出JSON:{{\"thought\":\"...\",\"tool\":\"...\",\"action\":\"...\",\"params\":{{}}}}"}], ) step = json.loads(thought) if step.get("tool") == "finish": self.scratchpad.append(f"[DONE] {sub_goal}") return obs = self.toolbox.execute(step["tool"], step["action"], step.get("params", {})) self.scratchpad.append(f"Thought: {step['thought']}\nAction: {step['action']}\nObservation: {obs}")

跑起来后,日志里会看到 Thought 和 Action 交替出现,这正是 ReAct 循环在工作的证据。

5. 验证请求:本地跑通一次任务规划与工具调用链路

配置和代码就位后,用一个小任务验证整条链路。任务选“在 workspace 下创建 notes.md 并写入三条训练加速技巧”,这样能同时触发 write_file 和 shell 两个工具。

启动脚本:

import json, tomllib from agent import Agent, LLMClient, ToolBox with open("settings.json", encoding="utf-8") as f: cfg = json.load(f) with open("config.toml", "rb") as f: cfg.update(tomllib.load(f)) agent = Agent(cfg, LLMClient(cfg), ToolBox(cfg)) agent.run("在 workspace 下创建 notes.md,写入三条 DeepSeek 训练加速技巧")

预期日志形态:

INFO plan step 1: 创建 notes.md 文件 INFO Thought: 需要先确认文件不存在,再写入内容 INFO Action: write_file path=notes.md INFO Observation: File written: workspace/notes.md INFO Thought: 文件已创建,子目标完成 INFO [DONE] 创建 notes.md 文件

看到 Thought 和 Action 交替、Observation 紧跟 Action,说明 ReAct 循环正常。如果日志里只有 Action 没有 Thought,多半是 prompt 里没强制要求输出 thought 字段,LLM 偷懒了。

验证成功后,可以换更复杂的任务,比如“搜索三个网页并汇总成 report.md”,观察 Plan-Execute 是否拆出多个步骤、每个步骤内部是否跑了多轮 ReAct。这一步跑通,骨架就算立住了。

6. 本篇常见错排查

报错一:401 Unauthorized。九成是环境变量没生效。检查echo $TAOTOKEN_API_KEY是否有值,settings.json 里的 api_key_env 名字是否和实际环境变量一致。别把 Key 直接写进 json,那样换机器就失效。

报错二:JSONDecodeError。LLM 返回的 thought 不是合法 JSON,常见于模型在 JSON 前后加了说明文字。解决方式是在 prompt 里加“只输出 JSON,不要任何解释”,或者在解析前用正则截取第一个{到最后一个}。

报错三:ReAct 循环跑满 max_react_iterations 还没结束。说明子目标太大或工具返回的 Observation 信息量不够。把子目标拆细,或者在 Observation 里带上更多上下文(比如文件当前内容),让 LLM 有足够信息判断下一步。

报错四:shell 命令在 workdir 找不到文件。ToolBox 里 subprocess 的 cwd 设成了 workdir,但 LLM 生成的命令可能带绝对路径。统一约定所有路径相对 workdir,或者在执行前做一次路径归一化。

报错五:规划步骤为空。planner_model 返回了空行或纯编号。在 _make_plan 里加一层过滤,去掉纯数字和空行,再判断列表长度,为 0 就重试一次。

排查顺序建议:先确认 Key 和 base_url,再确认 JSON 解析,最后看循环退出条件。大部分问题出在前两步。

7. 下一步:把骨架接到真实模型对话与长期编码场景

骨架跑通后,下一步是把它接到真实场景。如果你只是想验证模型在 ReAct 循环里的表现,可以直接用模型对话 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite 手动试几轮 Thought-Action 提示,观察不同模型的输出稳定性,再决定 planner 和 react 分别用哪个模型。

如果你打算把这个骨架用于长期编码或 Agent 任务,比如让它自己读代码、改文件、跑测试,那单次调用成本会累积得很快。这种情况更适合用 Coding Plan https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ,把规划、思考、工具参数生成这几类高频调用统一管理,避免每个环节单独计费。

接入过程中如果遇到鉴权或端点问题,回到接入文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 对照一遍请求头格式,多数报错都能定位。Key 管理仍在 API Keys 页面 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite ,建议给智能体单独建一个 Key,方便按项目追踪用量。

最后留一个实用技巧:在 scratchpad 里给每条 Observation 打上时间戳和工具名,回看日志时能一眼看出哪一步耗时最长、哪个工具调用最频繁。这个习惯比任何性能分析工具都直接。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 3:34:12

Intel 在人工智能领域配 TaoToken:config.toml 骨架与报错排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 3:33:23

MySQL8.0定时删除数据实战:Event Scheduler与分批清理大表日志

先说个真实场景。我去年接手一套订单系统的时候,发现一张操作日志表在半年内从不到1GB涨到了近30GB。业务方最初说“日志不删也不影响主流程”,直到某天大促脚本在凌晨跑批卡了十几分钟,磁盘IO被日志表的数据文件打到接近100%,清理…

作者头像 李华
网站建设 2026/9/26 3:32:16

Debian 13 上修复 VS Code 中文输入法:fcitx5 与 Electron 排查实战

先说结论:这个问题不是玄学,也不是 VS Code 坏了。九成以上的情况,是输入法框架和 Electron 应用之间的“握手”没成功。我在 Debian 13 上装好 VS Code 后第一次遇到不能输入中文时,第一反应是换输入法,后来把 fcitx5…

作者头像 李华
网站建设 2026/9/26 3:31:50

易语言-基础-数据库应用

易语言的数据库应用创建易数据库:使用菜单创建易数据库:使用代码创建易数据库:.版本 2 .支持库 eAPI 如果需要调用API,需添加此支持库,或者使用下面的纯易语言逻辑.程序集 窗口程序集_启动窗口.子程序 __启动窗口_创建完毕子程序_建数据库 ().子程序 子…

作者头像 李华