数据截至 (上游 commit dad6f5196773)
从聊天到 7×24 运营:多 agent 编排、任务调度与对话树渲染
30 秒导读: 前五章讲的是「一个 agent 怎么跑完一轮」。这一章讲 LobeHub v2 在那之上加的一层:让一群 agent 互相调度、让 agent 在没人看着的时候按点上工、让它的进展变成可订阅的事件,最后让前端把一堆扁平消息还原成一棵对话树渲染出来。
1. 这是什么(零基础也能懂)
一句话定义: 这一层把「聊天窗口」升级成「agent 运营平台」——agent 从「你问它答」的工具,变成有排班、有工单、有交班简报 的员工。
它解决的是三个具体的落差:
| 聊天产品做得到 | 运营平台还需要什么 |
|---|---|
| 一个 agent 回一句话 | 一群 agent 里,谁该说话、谁该干活,由一个 supervisor 决定 |
| 你点一次它跑一次 | 它自己按 cron / 心跳跑,跑完给你一张简报 |
| 消息一条条往下排 | 消息其实是一棵树(分支、并行、子 agent、工具回调),得还原出来才能渲染 |
用起来什么样(场景化): 你建一个群,里面有「调研 agent」「写作 agent」「校对 agent」。你说一句「帮我把这周的竞品动态整理成周报」。群主管(supervisor)先派调研 agent 去查(异步任务),查完自动叫写作 agent 起草,再广播给校对 agent 和你自己评审。你关掉浏览器,它照跑;第二天早上你看到的是一张 brief 卡片:标题、摘要、以及几个可以点的按钮。
一句话直觉: 把第 1 章的 AgentRuntime 想成一个人的工作循环,这一章就是把同一套循环再套一层到一个团队上——只不过这次「大脑」不是模型对 tool 的选择,而是 supervisor 对「让谁上」的选择。
本节不出现代码。往下才进机制。
2. 顶层全景(它大概怎么转)
这一章覆盖五个彼此独立、但串成一条线的子系统。先看它们的位置:
┌──────────────────────────────────────────────┐
人 / 定时器 ──► │ ① 编排层 supervisor 状态机 + executor │
│ packages/agent-runtime/groupOrchestration │
└───────────────────┬──────────────────────┘
│ 派活
▼
┌──────────────────────────────────────────────┐
│ ② 员工层 builtin-agents / agent-manager │
│ 同构子 agent · 客户端子 agent · 异构 CLI │
└───────────────────┬──────────────────────────┘
│ 留痕
▼
┌──────────────────────────────────────────────┐
│ ③ 数据层 operations / tasks / briefs / cron │
│ packages/database + apps/server/services │
└───────────────────┬──────────────────────────┘
│ 进展变事件
▼
┌──────────────────────────────────────────────┐
│ ④ 信号层 packages/agent-signal │
└───────────────────┬────────────────── ────────┘
│ 消息落库后
▼
┌──────────────────────────────────────────────┐
│ ⑤ 渲染层 packages/conversation-flow │
│ 扁平 messages[] → ContextTree + FlatList │
└──────────────────────────────────────────────┘
各部件一句话职责:
| 部件 | 干什么 | 在哪个包/目录 |
|---|---|---|
| GroupOrchestrationSupervisor | 群体编排的纯状态机:收结果、出指令 | packages/agent-runtime/src/groupOrchestration/ |
| GroupOrchestrationRuntime | 把 supervisor 和 executor 拧成循环 | 同上 |
| builtin-agents | 12 个「内置岗位」的人设 + 工具包定义 | packages/builtin-agents/src/agents/ |
| AgentManagerRuntime | agent 的 HR 系统:增删改查、装插件、换模型 | packages/agent-manager-runtime/src/ |
| heterogeneous-agents | 接管 Claude Code / Codex 这类外部 CLI agent 的事件流 | packages/heterogeneous-agents/src/ |
| tasks / briefs / cron 表 | 工单、交班简报、排班表 | packages/database/src/schemas/ |
| task* 系列服务 | 跑工单、排依赖、定时唤醒、生成简报 | apps/server/src/services/ |
| agent-signal | source → signal → action 三段式事件总线 | packages/agent-signal/src/ |
| conversation-flow | 扁平消息 → 对话树 + 虚拟列表 | packages/conversation-flow/src/ |
主线走一遍(高层): 用户/定时器触发 → supervisor 决定派谁 → 被派的 agent 各自跑一次 AgentRuntime(第 1 章)→ 每次跑都在 agent_operations 记一笔账 → 完成时发 signal、写 brief → 消息落库 → 前端 parse() 把它们还原成树来渲染。
3. 群体编排:同一套「大脑/引擎」的二次套用
3.1 先说结论:这就是第 1 章的架构再来一遍
第 1 章讲过 LobeHub 的核心分工:Agent(大脑)只 做决策、返回一条 Instruction;Runtime(引擎)只负责执行、返回一个 Result。 两者靠一个 while 循环互喂。
群体编排把同一个形状换了一层语义:
| 第 1 章:单 agent | 本章:agent 群体 | |
|---|---|---|
| 大脑 | GeneralChatAgent.runner() | GroupOrchestrationSupervisor.decide() |
| 大脑吃什么 | AgentRuntimeContext(上一步 phase) | ExecutorResult(上一步结果) |
| 大脑吐什么 | AgentInstruction(call_llm / call_tool …) | SupervisorInstruction(call_agent / delegate …) |
| 引擎 | AgentRuntime 的 executors | GroupOrchestrationRuntime 的 executors |
| 终止条件 | type: 'finish' | type: 'finish' |
循环长这样:
ExecutorResult ──►┌────────────────────────────┐
│ Supervisor(大脑) │ 纯函数,不碰 I/O
│ decide(result, state) │
└─── ──────────┬──────────────┘
│ SupervisorInstruction
▼
┌────────────────────────────┐
◄─────────────────┤ Executor(引擎) │ 真的去调 agent / 写消息
ExecutorResult └────────────────────────────┘
循环直到 instruction.type === 'finish'
GroupOrchestrationRuntime.step() 就是这个循环的一步(GroupOrchestrationRuntime.ts:47):它先给 stepCount 加一并检查 maxSteps,再问 supervisor 要指令,遇到 finish 就把状态置 done,否则按 instruction.type 在 executors 表里查一个函数执行——查不到直接抛错(GroupOrchestrationRuntime.ts:89),没有兜底。
3.2 相位转移表(decide 的全部分支)
GroupOrchestrationSupervisor.decide()(GroupOrchestrationSupervisor.ts:48)是一个两层 switch。第一层按 result.type,第二层按 supervisor_decided 里的 decision。完整转移如下:
| 上一步结果 | 附加条件 | 下一条指令 | 说明 |
|---|---|---|---|
init | — | call_supervisor | 同时把 round 归零、skipCallSupervisor 归 false |
supervisor_decided | decision: 'speak' | call_agent | 让某一个 agent 发言 |
supervisor_decided | decision: 'broadcast' | parallel_call_agents | 并行发言,并强制 disableTools: true |
supervisor_decided | decision: 'delegate' | delegate | 把话语权交出去,supervisor 退场 |
supervisor_decided | decision: 'execute_task' + runInClient | exec_client_async_task | 任务需要本地文件/shell,跑在桌面端 |
supervisor_decided | decision: 'execute_task' | exec_async_task | 默认跑服务端 |
supervisor_decided | decision: 'execute_tasks' | batch_exec_async_tasks | 一批任务并行 |
supervisor_decided | decision: 'finish' | finish | reason 取 params.reason,缺省 supervisor_finished |
supervisor_decided | 未知 decision | finish | reason = unknown_decision: X |
agent_spoke / agents_broadcasted / task_completed / tasks_completed | skipCallSupervisor | finish | reason = skip_call_supervisor |
| 同上四种 | ++round >= maxRounds | finish | reason = max_rounds_exceeded |
| 同上四种 | 其余 | call_supervisor | 带上新的 round |
delegated | — | finish | reason = delegated_to_<agentId> |
| 其它 | — | finish | reason = unknown_result_type |
三个容易读漏的细节:
round只在「动作做完」时加一,不在 supervisor 做决策时加(GroupOrchestrationSupervisor.ts:161)。所以maxRounds数的是「派活轮次」,不是「LLM 调用次数」。broadcast硬编码禁工具。代码注释写得很直白:广播出去的 agent 默认不该调工具(GroupOrchestrationSupervisor.ts:82-83),因为那是「大家表个态」而不是「大家各干各的」。skipCallSupervisor是一次性开关:它在每次supervisor_decided时被覆写(GroupOrchestrationSupervisor.ts:65),在init时被清零。语义是「这一步做完就收工,别再回去问主管」——用在用户明确点名某个 agent 的场景。