🌳 多 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(层级树,本章) 顶层 组长A 组长B 多层路由,可伸缩,每层专注自己的范围
图 AD2.1 · 扁平 Supervisor vs 层级树:层级把"管理负担"分摊到中间层
维度Supervisor(AD1)Hierarchical(本章)
拓扑星形(1 层)树形(多层)
路由负担全部在一个 Supervisor分摊到各层组长
可伸缩性专家多了 Supervisor 决策变差可加层处理更多 Agent
延迟低(1 跳)高(多跳)
适用专家数 ≤ 5大型系统、明确分组

Handoff:控制权移交

多 Agent 协作有两种根本不同的通信语义,必须分清:

① 结果回流(Return) 主Agent 子Agent 派任务 回结果,控制权回主 主 Agent 始终是协调者 ② Handoff(移交) Agent A Agent B 移交控制权 B 接管 B 成为新的主,A 退出
图 AD2.2 · 结果回流 vs Handoff:控制权是否回到原 Agent
语义控制权典型实现适合
结果回流始终在主 Agent子图返回结果;工具调用返回值Supervisor/Hierarchical(本章)
Handoff 移交从 A 转移到 BSwarm 拓扑;create_handoff客服转接、专业接力
⚠️ OpenCode 用的是"结果回流",不是 Handoff

OpenCode 的子 Agent(task 工具)完成后,控制权回到主 Agent,子结果作为返回值回流(task.ts:199)。主 Agent 始终是协调者。这是结果回流语义,不是 Handoff。Swarm/Handoff 在独立 contrib 包(langgraph-swarm),本仓库不含。

精读:OpenCode task 工具的会话树

OpenCode 的层级多 Agent 是通过 task 工具 + parentID 会话树实现的。逐段精读:

① 建立父子关系

📄 tool/task.ts:144 · 创建子会话,绑定 parentID typescript
// 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 调用 = 形成一棵会话树
OpenCode 的会话树(parentID 建立的层级) 主会话 build explore 子会话 general 子会话 explore 子会话2 parentID parentID parentID 子会话默认禁止再派生(防递归)→ 树深受限
图 AD2.3 · parentID 建立的层级会话树——OpenCode 的层级多 Agent 实现

② 防递归:子 Agent 不能再派生

📄 agent/subagent-permissions.ts:14 · 派生子 Agent 权限 typescript
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)更彻底,直接从权限层切断递归可能。这是生产系统的必备设计。

③ 模型继承与结果回流

📄 task.ts:167/199 · 模型继承 + 同步回流 typescript
// 模型继承:子 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 模式):injectBackgroundResulttask.ts:202)把后台子 Agent 完成后的结果,用 <task id=... state=...> XML 标签包裹,作为合成消息注入父会话。

实战:三层 Agent 团队

🚀 用 LangGraph 构建层级团队

顶层主管 → 中层组长(研究组/开发组)→ 底层工人。每层都是 Supervisor 模式(AD1)的嵌套。

📄 ad2_hierarchical.py · 三层 Agent 团队(核心结构) python
# 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. 错误隔离

📄 子 Agent 失败不影响父 python
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 的务实选择

OpenCode 没有实现无限层级——它主动限制在 2 层(主 + 子)。这是务实的设计:大多数编码任务 2 层足够(主 agent 协调 + explore/general 子 agent 执行),更深的层级带来的复杂度不值得。这给我们的启示:不要为了"看起来强大"而过度设计层级,按实际任务复杂度选择最小够用的结构

小结

下一主题 · 高级 RAG

多 Agent 讲完了。接下来 AD3 · 高级 RAG:检索策略——换个方向,深入 RAG。精读四种文档合并策略、多查询/混合检索、重排算法,构建生产级 RAG 管道。