🌳 多 Agent:层级树 + Handoff
AD1 的 Supervisor 是一层路由。当系统更复杂时,需要多层级的 Agent 团队——顶层主管管组长,组长管工人。本章深入层级(Hierarchical)拓扑和 Handoff(控制权移交)语义,并精读 OpenCode task 工具如何用 parentID 建立会话树、防递归、回流结果。
本章目标
- 理解层级拓扑的适用场景与设计要点
- 掌握 Handoff(控制权移交)vs 结果回流 两种通信语义的区别
- 精读 OpenCode task 工具的父子会话树建立机制(parentID)
- 理解防递归设计(子 Agent 不能再派生子 Agent)
- 用 LangGraph 构建一个三层 Agent 团队
层级 vs Supervisor
AD1 的 Supervisor 是扁平的星形——一个中心管所有专家。层级(Hierarchical)是树形——每层都有自己的 Supervisor。区别:
| 维度 | Supervisor(AD1) | Hierarchical(本章) |
|---|---|---|
| 拓扑 | 星形(1 层) | 树形(多层) |
| 路由负担 | 全部在一个 Supervisor | 分摊到各层组长 |
| 可伸缩性 | 专家多了 Supervisor 决策变差 | 可加层处理更多 Agent |
| 延迟 | 低(1 跳) | 高(多跳) |
| 适用 | 专家数 ≤ 5 | 大型系统、明确分组 |
Handoff:控制权移交
多 Agent 协作有两种根本不同的通信语义,必须分清:
| 语义 | 控制权 | 典型实现 | 适合 |
|---|---|---|---|
| 结果回流 | 始终在主 Agent | 子图返回结果;工具调用返回值 | Supervisor/Hierarchical(本章) |
| Handoff 移交 | 从 A 转移到 B | Swarm 拓扑;create_handoff | 客服转接、专业接力 |
OpenCode 的子 Agent(task 工具)完成后,控制权回到主 Agent,子结果作为返回值回流(task.ts:199)。主 Agent 始终是协调者。这是结果回流语义,不是 Handoff。Swarm/Handoff 在独立 contrib 包(langgraph-swarm),本仓库不含。
精读:OpenCode task 工具的会话树
OpenCode 的层级多 Agent 是通过 task 工具 + parentID 会话树实现的。逐段精读:
① 建立父子关系
// task 工具执行时,创建一个【子会话】,绑定到当前会话
const nextSession = session ?? (yield* sessions.create({
parentID: ctx.sessionID, // ★ 父子关系建立点!parentID 指向当前会话
title: params.description + ` (@${next.name} subagent)`,
agent: next.name, // 用哪个子 agent(explore/general/...)
permission: [
...childPermission, // 派生的子权限(下面讲)
...childToolDenies, // 额外禁止的工具
],
}))
// 每个 task 调用 = 创建一个子会话节点,parentID 指向父
// 多次 task 调用 = 形成一棵会话树
② 防递归:子 Agent 不能再派生
export function deriveSubagentSessionPermission(input) {
// 合并父会话的 deny 规则 + external_directory 规则
// 并【额外禁止】:
return Permission.merge(parentRuleset, {
todowrite: "deny", // 子 agent 不改待办
task: "deny", // ★ 关键:禁止子 agent 再派生子 agent!防递归爆炸
})
}
// canTask/canTodo 检测:仅当子 agent 自己显式允许 task 才放行(默认 deny)
如果不禁止子 Agent 再派生,LLM 可能陷入 task → task → task → ... 的无限递归,每个递归都消耗 token 和上下文,瞬间爆炸。task: "deny" 是结构性防护——比运行时检测死循环(OC4 的 doom_loop)更彻底,直接从权限层切断递归可能。这是生产系统的必备设计。
③ 模型继承与结果回流
// 模型继承:子 agent 没指定模型就用父会话的
const model = next.model ?? {
modelID: msg.info.modelID,
providerID: msg.info.providerID,
}
// 同步回流:子 agent 跑完,取最后一条 text part 作为 task 工具返回值
const runTask = Effect.fn("TaskTool.runTask")(function* () {
const parts = yield* ops.resolvePromptParts(params.prompt)
const result = yield* ops.prompt({ messageID: ..., ... })
return result.parts.findLast((p) => p.type === "text").text // ★ 同步回流
})
// 主 agent 收到这段文本,作为 task 工具的结果,继续推理
还有异步回流(background 模式):injectBackgroundResult(task.ts:202)把后台子 Agent 完成后的结果,用 <task id=... state=...> XML 标签包裹,作为合成消息注入父会话。
实战:三层 Agent 团队
顶层主管 → 中层组长(研究组/开发组)→ 底层工人。每层都是 Supervisor 模式(AD1)的嵌套。
# pip install langgraph langchain-openai
import operator
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
model = ChatOpenAI(model="gpt-4o-mini")
class State(TypedDict):
messages: Annotated[list, operator.add]
next: str
# ============ 底层:工人节点(实际干活)============
def researcher(state: State) -> dict:
resp = model.invoke([SystemMessage(content="你是研究员,简洁回答。"), *state["messages"]])
return {"messages": [AIMessage(content=f"[研究] {resp.content}")]}
def coder(state: State) -> dict:
resp = model.invoke([SystemMessage(content="你是程序员,简洁回答。"), *state["messages"]])
return {"messages": [AIMessage(content=f"[编码] {resp.content}")]}
def tester(state: State) -> dict:
resp = model.invoke([SystemMessage(content="你是测试员,简洁回答。"), *state["messages"]])
return {"messages": [AIMessage(content=f"[测试] {resp.content}")]}
# ============ 中层:组长(管一组工人)============
def make_team_lead(members: dict, lead_name: str):
"""工厂:创建一个组长节点(mini-supervisor)。"""
def team_lead(state: State) -> dict:
resp = model.invoke([
SystemMessage(content=(
f"你是{lead_name}组长。根据进度决定下一步:"
f"可选 {list(members.keys())} 或 DONE。只回一个词。"
)),
*state["messages"],
])
choice = resp.content.strip()
next_node = members.get(choice, "DONE")
return {"next": next_node}
return team_lead
# 研究组:组长管 researcher
research_members = {"RESEARCH": "researcher", "DONE": "DONE"}
# 开发组:组长管 coder + tester
dev_members = {"CODE": "coder", "TEST": "tester", "DONE": "DONE"}
# ============ 顶层:主管(管组长)============
def top_supervisor(state: State) -> dict:
resp = model.invoke([
SystemMessage(content=(
"你是项目主管,协调【研究组长】和【开发组长】。"
"决定下一步:RESEARCH_LEAD(研究组)/ DEV_LEAD(开发组)/ DONE。只回一个词。"
)),
*state["messages"],
])
mapping = {"RESEARCH_LEAD": "research_lead", "DEV_LEAD": "dev_lead", "DONE": "DONE"}
return {"next": mapping.get(resp.content.strip(), "DONE")}
# ============ 构建三层图 ============
graph = StateGraph(State)
# 顶层
graph.add_node("top_supervisor", top_supervisor)
# 中层(组长)
graph.add_node("research_lead", make_team_lead(research_members, "研究"))
graph.add_node("dev_lead", make_team_lead(dev_members, "开发"))
# 底层(工人)
graph.add_node("researcher", researcher)
graph.add_node("coder", coder)
graph.add_node("tester", tester)
# 顶层路由
graph.add_edge(START, "top_supervisor")
graph.add_conditional_edges("top_supervisor", lambda s: s["next"],
{"research_lead": "research_lead", "dev_lead": "dev_lead", "DONE": END})
# 组长执行后回到顶层(形成层级循环)
graph.add_edge("research_lead", "top_supervisor")
graph.add_edge("dev_lead", "top_supervisor")
# 底层工人执行后回到各自的组长
graph.add_edge("researcher", "research_lead")
graph.add_edge("coder", "dev_lead")
graph.add_edge("tester", "dev_lead")
app = graph.compile()
# 注意:这个简化版组长没做条件边路由到工人(完整版需加)
# 实际生产中,每个组长内部也是一个 mini-supervisor 图(AD1 的结构)
result = app.invoke({"messages": [HumanMessage(content="做一个待办CLI工具")], "next": ""})
看代码结构:顶层主管 → 中层组长 → 底层工人,每一层都是 AD1 的 Supervisor 模式。层级 = Supervisor 的嵌套。关键设计:每层的 next 字段只在相邻层间传递,顶层不知道底层工人是谁(只管组长),这叫"关注点分离"。
层级设计的工程要点
1. 树深控制
层级越深,延迟越高、调试越难。OpenCode 用 task: deny 把树深限制在 2 层(主 + 子)。生产建议不超过 3 层。
2. 状态可见性
子层不应该看到父层的所有细节——用子图独立 schema(AD1 讲过)隔离。OpenCode 的子会话有独立的消息历史,主会话只看到回流的结果摘要。
3. 错误隔离
def safe_subtask(state):
"""子任务失败时,返回错误信息而非抛异常。"""
try:
result = sub_agent.invoke({"messages": state["messages"]})
return {"messages": [result["messages"][-1]]}
except Exception as e:
# 隔离错误:父 Agent 收到"子任务失败"消息,可决定重试或换策略
return {"messages": [AIMessage(content=f"[子任务失败: {e}],请重试或调整")]}
与生产实践对照
| 层级概念 | OpenCode 对应 | |
|---|---|---|
| 层级树(多层 Supervisor) | → | 主会话 + task 子会话(2 层,受限) |
| parentID 父子关系 | → | sessions.create({ parentID })(task.ts:144) |
| 结果回流(非 Handoff) | → | 子 agent 最后 text part 作为 task 返回值 |
| 树深控制 | → | 子 agent task 工具 deny(防递归) |
| 权限隔离 | → | deriveSubagentSessionPermission(继承 deny) |
| 异步回流 | → | background 模式 + injectBackgroundResult |
OpenCode 没有实现无限层级——它主动限制在 2 层(主 + 子)。这是务实的设计:大多数编码任务 2 层足够(主 agent 协调 + explore/general 子 agent 执行),更深的层级带来的复杂度不值得。这给我们的启示:不要为了"看起来强大"而过度设计层级,按实际任务复杂度选择最小够用的结构。
小结
- 层级拓扑 = Supervisor 的嵌套,适合大型系统、明确分组;专家多时比扁平 Supervisor 更可伸缩。
- 两种通信语义:结果回流(控制权回主,OpenCode 用此)vs Handoff 移交(控制权转移,Swarm 用此)。
- OpenCode 用 task 工具 + parentID 建立会话树,
task: deny防递归(结构性防护)。 - 层级设计要点:树深控制(≤3层)、状态隔离、错误隔离。
- 务实原则:按任务复杂度选择最小够用的结构,OpenCode 限 2 层。
多 Agent 讲完了。接下来 AD3 · 高级 RAG:检索策略——换个方向,深入 RAG。精读四种文档合并策略、多查询/混合检索、重排算法,构建生产级 RAG 管道。