搞 AI Agent 的都知道,单个 Agent 干活还行,一旦涉及多 Agent 协作,事情就变得复杂了:谁先谁后?怎么分工?出了问题谁负责?

我翻 OpenManus 源码的时候,发现它有个 run_flow.py 的入口,跟 main.py 不一样——这个是"先规划再执行"模式。研究了下背后的 Planning 架构,不得不说,这套设计有点东西。今天就来聊聊。


一、整体流程概览

先上个整体流程,看完心里就有数了:

Agent (Manus/DataAnalysis) LLM PlanningTool PlanningFlow FlowFactory run_flow.py User Agent (Manus/DataAnalysis) LLM PlanningTool PlanningFlow FlowFactory run_flow.py User 阶段一: LLM 生成计划 阶段二: 状态驱动执行 loop [遍历每个步骤] 阶段三: 执行总结 python run_flow.py 创建 agents 字典 FlowFactory.create_flow(PLANNING, agents) new PlanningFlow(agents) flow 实例 flow.execute(prompt) ask_tool(创建计划的提示词) 返回 planning 工具调用 execute(create, plan_id, steps) 计划创建成功 get(plan_id) 获取当前步骤 返回步骤信息 mark_step(in_progress) agent.run(step_prompt) think() → act() 步骤执行结果 mark_step(completed) ask(总结计划的提示词) 执行总结 最终结果

整个流程分三个阶段:LLM 生成计划 → 状态驱动执行 → 执行总结


二、入口代码分析

async def run_flow():
    agents = {
        "manus": Manus(),
    }
    if config.run_flow_config.use_data_analysis_agent:
        agents["data_analysis"] = DataAnalysis()

    flow = FlowFactory.create_flow(
        flow_type=FlowType.PLANNING,
        agents=agents,
    )

    result = await flow.execute(prompt)

看起来简单,但有几个值得注意的点:

1. agents 是字典结构

为什么用字典而不是列表?因为后面要根据步骤类型来选择对应的 Agent。比如 [DATA_ANALYSIS] 步骤可以路由到数据分析 Agent,[MANUS] 步骤路由到通用 Agent。

2. FlowFactory 工厂模式

目前只有 FlowType.PLANNING 一种类型,但设计上预留了扩展空间。以后加个 FlowType.CHAINFlowType.PARALLEL 都很容易。


三、核心模块设计(静态架构)

在深入执行流程之前,先了解三个核心模块的职责和设计。

3.1 PlanningTool:计划管理器

这是整个 Planning 架构的基石,提供了完整的计划 CRUD 操作。

支持的操作:

命令功能关键参数
create创建新计划plan_id, title, steps
update更新计划plan_id, title?, steps?
get获取计划详情plan_id
list列出所有计划
mark_step标记步骤状态plan_id, step_index, step_status
set_active设置活跃计划plan_id
delete删除计划plan_id

核心数据结构:

plan = {
    "plan_id": "plan_1234567890",
    "title": "分析数据并生成报告",
    "steps": [
        "[DATA_ANALYSIS] 读取 CSV 文件",
        "[MANUS] 生成可视化图表",
        "[MANUS] 输出分析报告"
    ],
    "step_statuses": ["completed", "in_progress", "not_started"],
    "step_notes": ["", "处理中...", ""]
}

划重点:step_statuses 和 step_notes 与 steps 一一对应,状态变更会同步更新。

状态流转:

not_started → in_progress → completed
                  ↓
               blocked

3.2 PlanningFlow:编排执行流程

这是"大脑",负责协调整个规划-执行过程。

核心属性:

class PlanningFlow(BaseFlow):
    llm: LLM                          # 独立的 LLM 实例
    planning_tool: PlanningTool        # 计划管理器
    executor_keys: List[str]           # 可执行 Agent 列表
    active_plan_id: str               # 当前活跃计划 ID
    current_step_index: Optional[int]  # 当前执行步骤索引

execute() 方法的骨架:

async def execute(self, input_text: str) -> str:
    # 阶段一:创建计划
    await self._create_initial_plan(input_text)

    # 阶段二:循环执行步骤
    while True:
        self.current_step_index, step_info = await self._get_current_step_info()
        if self.current_step_index is None:
            break
        executor = self.get_executor(step_type)
        await self._execute_step(executor, step_info)

    # 阶段三:执行总结
    return await self._finalize_plan()

3.3 BaseFlow:抽象基类

class BaseFlow(BaseModel, ABC):
    agents: Dict[str, BaseAgent]
    tools: Optional[List] = None
    primary_agent_key: Optional[str] = None

    @abstractmethod
    async def execute(self, input_text: str) -> str:
        """Execute the flow with given input"""

精妙之处:

  1. agents 参数的多态处理 - 可以传单个 Agent、列表、或字典:
# 这三种写法都合法
flow = PlanningFlow(Manus())
flow = PlanningFlow([Manus(), DataAnalysis()])
flow = PlanningFlow({"manus": Manus(), "data": DataAnalysis()})
  1. primary_agent 的自动推断 - 没指定就取第一个。

  2. 抽象方法强制子类实现 execute() - 保证了所有 Flow 都有统一的调用入口。


四、完整执行流程(动态流程)

接下来按时间顺序,详细分析三个阶段的执行过程。


4.1 阶段一:LLM 生成计划

这部分是整个架构最核心的地方:计划不是硬编码的,是 LLM 理解、决策并生成的

4.1.1 完整调用流程
async def _create_initial_plan(self, request: str) -> None:
    # 1. 构建 system message,告诉 LLM 有哪些 Agent 可用
    system_message_content = (
        "You are a planning assistant. Create a concise, actionable plan..."
    )

    # 2. 把 Agent 信息加进去
    agents_description = []
    for key in self.executor_keys:
        agents_description.append({
            "name": key.upper(),
            "description": self.agents[key].description,
        })

    system_message_content += f"\nNow we have {agents_description} agents..."

    # 3. 调用 LLM,把 PlanningTool 作为可用工具传进去
    response = await self.llm.ask_tool(
        messages=[user_message],
        system_msgs=[system_message],
        tools=[self.planning_tool.to_param()],  # 关键:工具定义
        tool_choice=ToolChoice.AUTO,
    )

    # 4. LLM 返回工具调用,解析并执行
    if response.tool_calls:
        args = json.loads(tool_call.function.arguments)
        args["plan_id"] = self.active_plan_id
        result = await self.planning_tool.execute(**args)
4.1.2 LLM 收到的信息

System Message(部分):

You are a planning assistant. Create a concise, actionable plan with clear steps.

Now we have [{'name': 'MANUS', 'description': 'A versatile agent...'},
             {'name': 'DATA_ANALYSIS', 'description': 'An agent for data...'}] agents.
When creating steps, please specify the agent names using the format '[agent_name]'.

User Message:

Create a reasonable plan with clear steps to accomplish the task: 帮我分析 sales.csv 并生成报告

工具定义(PlanningTool.to_param()):

{
    "type": "function",
    "function": {
        "name": "planning",
        "description": "A planning tool that allows the agent to create and manage plans...",
        "parameters": {
            "type": "object",
            "properties": {
                "command": {"enum": ["create", "update", "list", ...]},
                "plan_id": {"type": "string"},
                "title": {"type": "string"},
                "steps": {"type": "array", "items": {"type": "string"}},
                ...
            },
            "required": ["command"]
        }
    }
}
4.1.3 LLM 的响应

LLM 理解了意图后,会返回一个工具调用:

{
    "tool_calls": [{
        "id": "call_abc123",
        "type": "function",
        "function": {
            "name": "planning",
            "arguments": {
                "command": "create",
                "plan_id": "plan_1709876543",
                "title": "分析 sales.csv 并生成报告",
                "steps": [
                    "[DATA_ANALYSIS] 读取并分析 sales.csv 文件结构",
                    "[MANUS] 根据分析结果生成可视化图表",
                    "[MANUS] 输出完整的分析报告"
                ]
            }
        }
    }]
}
4.1.4 为什么这样设计是"智能"的?

1. 计划根据任务动态调整

同样是"分析数据",如果用户说"帮我分析 sales.csv",LLM 会生成一个简单的 3 步计划。如果用户说"帮我深入分析销售数据,找出异常,预测下季度趋势",LLM 会生成更复杂的计划(取决于 LLM 的"智能")。

2. Agent 选择是 LLM 决定的

LLM 看到 [DATA_ANALYSIS] 的描述是"数据分析师",它就会在数据相关的步骤里标记 [DATA_ANALYSIS]。这比写死规则灵活多了。

3. 步骤粒度可控

通过调整 system_message 里的提示词,可以让 LLM 生成粗粒度或细粒度的计划。比如加上 “Break tasks into detailed sub-steps”,LLM 就会拆得更细。


4.2 阶段二:状态驱动执行

计划创建好后,进入执行阶段。核心是一个 while 循环:

while True:
    # 1. 找到下一个要执行的步骤
    self.current_step_index, step_info = await self._get_current_step_info()

    # 2. 没有步骤了,退出
    if self.current_step_index is None:
        result += await self._finalize_plan()
        break

    # 3. 执行当前步骤
    executor = self.get_executor(step_type)
    step_result = await self._execute_step(executor, step_info)

    # 4. 检查是否要终止
    if executor.state == AgentState.FINISHED:
        break

下面按执行顺序,依次分析:步骤选择 → Agent 路由 → 步骤执行。


4.2.1 步骤选择:_get_current_step_info()

核心逻辑:

async def _get_current_step_info(self) -> tuple[Optional[int], Optional[dict]]:
    # 从 PlanningTool 读取计划
    plan_data = self.planning_tool.plans[self.active_plan_id]
    steps = plan_data.get("steps", [])
    step_statuses = plan_data.get("step_statuses", [])

    # 遍历 steps,找第一个状态为 not_started 或 in_progress 的步骤
    for i, step in enumerate(steps):
        status = step_statuses[i] if i < len(step_statuses) else "not_started"

        if status in ["not_started", "in_progress"]:
            step_info = {"text": step}

            # 用正则提取 Agent 标记
            type_match = re.search(r"\[([A-Z_]+)\]", step)
            if type_match:
                step_info["type"] = type_match.group(1).lower()

            # 标记为 in_progress
            await self.planning_tool.execute(
                command="mark_step",
                plan_id=self.active_plan_id,
                step_index=i,
                step_status="in_progress",
            )

            return i, step_info

    return None, None  # 没有未完成的步骤

关键:状态驱动

这样设计的好处:不需要维护执行队列,每次循环都从"计划状态"推导下一步。


4.2.2 Agent 路由:正则提取 + get_executor()

正则提取步骤类型:

# app/flow/planning.py:245
type_match = re.search(r"\[([A-Z_]+)\]", step)
if type_match:
    step_info["type"] = type_match.group(1).lower()

正则 r"\[([A-Z_]+)\]" 的含义:

  • \[\] - 匹配方括号本身
  • ([A-Z_]+) - 捕获组,匹配一个或多个大写字母或下划线
  • 整体匹配 [MANUS][DATA_ANALYSIS][SEARCH] 等格式

提取示例:

输入: "[DATA_ANALYSIS] 读取并分析 sales.csv 文件结构"
    ↓
re.search(r"\[([A-Z_]+)\]") → 匹配成功
    ↓
type_match.group(1) → "DATA_ANALYSIS"
    ↓
.lower() → "data_analysis"
    ↓
step_info["type"] = "data_analysis"

get_executor() 路由逻辑:

def get_executor(self, step_type: Optional[str] = None) -> BaseAgent:
    # 精确匹配
    if step_type and step_type in self.agents:
        return self.agents[step_type]

    # 默认返回第一个可用 Agent
    for key in self.executor_keys:
        if key in self.agents:
            return self.agents[key]

    return self.primary_agent  # 最终兜底

完整路由链路:

步骤文本: "[DATA_ANALYSIS] 分析数据"
    ↓
re.search(r"\[([A-Z_]+)\]") 匹配 → "DATA_ANALYSIS"
    ↓
.lower() → "data_analysis"
    ↓
step_info["type"] = "data_analysis"
    ↓
get_executor("data_analysis") → agents["data_analysis"]

边界情况处理:

  • 没有标记type_match 为 None,走默认逻辑,返回第一个可用 Agent
  • 标记的 Agent 不存在:精确匹配失败,同样走默认逻辑

4.2.3 步骤执行:_execute_step()

这个方法负责把计划中的每个步骤交给合适的 Agent 去执行。

源码分析:

async def _execute_step(self, executor: BaseAgent, step_info: dict) -> str:
    # 1. 获取当前计划的整体状态
    plan_status = await self._get_plan_text()

    # 2. 获取当前步骤的文本描述
    step_text = step_info.get("text", f"Step {self.current_step_index}")

    # 3. 构建发送给 Agent 的提示词
    step_prompt = f"""
    CURRENT PLAN STATUS:
    {plan_status}

    YOUR CURRENT TASK:
    You are now working on step {self.current_step_index}: "{step_text}"

    Please only execute this current step using the appropriate tools.
    When you're done, provide a summary of what you accomplished.
    """

    # 4. 调用 Agent 执行
    try:
        step_result = await executor.run(step_prompt)

        # 5. 标记步骤完成
        await self._mark_step_completed()

        return step_result
    except Exception as e:
        logger.error(f"Error executing step {self.current_step_index}: {e}")
        return f"Error executing step {self.current_step_index}: {str(e)}"

Agent 收到的提示词示例:

假设正在执行第二步"生成可视化图表":

CURRENT PLAN STATUS:
Plan: 分析 sales.csv 并生成报告 (ID: plan_1709876543)
================================================

Progress: 1/3 steps completed (33.3%)
Status: 1 completed, 1 in progress, 0 blocked, 1 not started

Steps:
0. [✓] [DATA_ANALYSIS] 读取并分析 sales.csv 文件结构
1. [→] [MANUS] 根据分析结果生成可视化图表
2. [ ] [MANUS] 输出完整的分析报告


YOUR CURRENT TASK:
You are now working on step 1: "[MANUS] 根据分析结果生成可视化图表"

Please only execute this current step using the appropriate tools.
When you're done, provide a summary of what you accomplished.

为什么要传整个计划状态?

让 Agent 知道:

  • 前面做了什么(已完成步骤)
  • 现在要做什么(当前步骤)
  • 后面还要做什么(待执行步骤)

这样 Agent 可以基于上下文做出更好的决策,而不是盲目执行。

Agent 内部执行过程:

当 Agent 收到 step_prompt 后,会进入自己的执行循环(上一篇文章已经介绍过:OpenManus 入口设计:37 行代码里的架构智慧):

# Agent.run() 内部逻辑(简化版)
async def run(self, prompt: str) -> str:
    self.update_memory("user", prompt)

    while self.state != AgentState.FINISHED and self.current_step < self.max_steps:
        await self.think()   # 调用 LLM 思考
        if self.tool_calls:
            await self.act()  # 执行工具
        self.current_step += 1

    return self.get_final_result()

步骤完成的判定:

两种方式:

  1. Agent 自己判断 - LLM 没有返回工具调用,而是返回总结性文字
  2. 显式调用 terminate 工具 - Agent 有一个 terminate 工具可以显式标记完成
class Terminate(BaseTool):
    name: str = "terminate"
    description: str = """Terminate the interaction when the request is met..."""
    parameters: dict = {
        "type": "object",
        "properties": {
            "status": {"type": "string", "enum": ["success", "failure"]}
        },
        "required": ["status"],
    }

状态标记的实现:

async def _mark_step_completed(self) -> None:
    try:
        # 通过 PlanningTool 更新状态
        await self.planning_tool.execute(
            command="mark_step",
            plan_id=self.active_plan_id,
            step_index=self.current_step_index,
            step_status="completed",
        )
    except Exception as e:
        # 降级处理:直接修改内存中的状态
        plan_data = self.planning_tool.plans[self.active_plan_id]
        plan_data["step_statuses"][self.current_step_index] = "completed"

完整的执行时序:

LLM Agent PlanningFlow LLM Agent PlanningFlow 任务完成 alt [有工具调用] [无工具调用] loop [think() → act() 循环] _execute_step() step_prompt (计划状态+当前任务) think() 调用 LLM tool_calls 或文字回复 act() 执行工具 step_result _mark_step_completed()

4.3 阶段三:执行总结

所有步骤执行完毕后,调用 _finalize_plan() 生成总结:

async def _finalize_plan(self) -> str:
    plan_text = await self._get_plan_text()

    system_message = Message.system_message(
        "You are a planning assistant. Your task is to summarize the completed plan."
    )

    user_message = Message.user_message(
        f"The plan has been completed. Here is the final plan status:\n\n{plan_text}\n\n"
        f"Please provide a summary of what was accomplished."
    )

    response = await self.llm.ask(
        messages=[user_message], system_msgs=[system_message]
    )

    return f"Plan completed:\n\n{response}"

五、容错设计

计划创建失败怎么办?

# 如果 LLM 没返回工具调用,创建一个默认计划
await self.planning_tool.execute(
    command="create",
    plan_id=self.active_plan_id,
    title=f"Plan for: {request[:50]}...",
    steps=["Analyze request", "Execute task", "Verify results"],
)

步骤执行失败怎么办?

try:
    step_result = await executor.run(step_prompt)
    await self._mark_step_completed()
except Exception as e:
    logger.error(f"Error executing step {self.current_step_index}: {e}")
    return f"Error executing step {self.current_step_index}: {str(e)}"

错误会被记录,但不会中断整个流程。PlanningTool 的状态可以追踪哪些步骤失败了。


六、实战示例

假设用户输入:“帮我分析 sales.csv 并生成报告”

✨ Creating initial plan with ID: plan_1709876543
📋 Plan creation result: Plan created successfully with ID: plan_1709876543

📍 Current step: 0 - [DATA_ANALYSIS] 读取并分析 sales.csv
🔧 Executing with agent: data_analysis
...
✓ Step 0 completed

📍 Current step: 1 - [MANUS] 生成可视化图表
🔧 Executing with agent: manus
...
✓ Step 1 completed

📍 Current step: 2 - [MANUS] 输出分析报告
🔧 Executing with agent: manus
...
✓ Step 2 completed

🎉 Plan completed. Generating summary...

七、架构亮点总结

折腾完这套代码,有几点设计确实值得学习:

1. 单一职责拆分

  • PlanningTool 只管计划的存储和状态
  • PlanningFlow 只管编排和调度
  • Agent 只管具体执行

各司其职,边界清晰。

2. 状态驱动执行

通过 step_statuses 自动判断下一个要执行的步骤,不用维护复杂的执行队列。

3. Agent 路由灵活

步骤里写 [AGENT_NAME] 就能自动路由,不用硬编码 if-else。

4. 容错机制

计划创建失败会自动 fallback 到默认计划;步骤执行失败会记录错误但不中断流程。

5. LLM 驱动的动态规划

计划不是写死的,是 LLM 根据任务动态生成的。同一个入口,不同任务产生不同计划,这才是"智能"的体现。


写在最后

从 run_flow.py 出发,整个 Planning 架构就清晰了:Flow 是调度器,Tool 是存储器,Agent 是执行器。

这套设计最让我欣赏的是扩展性——想加新类型的 Flow?继承 BaseFlow 实现 execute()。想加新 Agent?往 agents 字典里塞一个。想自定义步骤路由?改 get_executor() 逻辑就行。

有问题欢迎评论区交流。

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐