先说结论:Swarm 把控制流交给了模型,图式把控制流交给了代码。 前者写起来快、跑起来飘;后者写起来慢、跑起来稳。选哪个,取决于你能不能接受"这次请求走了哪条路"事后才知道。
1. Swarm 的核心:交接就是一个返回值
OpenAI 的 Swarm 是一个实验性、教学向的轻量框架(后来演化为 Agents SDK 的 handoff 机制)。它只有两个原语:Agent 和 handoff。没有图、没有节点、没有边,一个循环反复执行:拿当前 Agent 生成回复,如果回复里触发了 handoff,就把"当前 Agent"换成下一个,继续循环。
from swarm import Swarm, Agent
def to_billing():
"""处理账单、退款、发票相关问题。"""
return billing_agent
triage = Agent(
name="triage",
instructions="你是客服分流。判断用户问题属于账单、技术还是其他,需要时转交。",
functions=[to_billing, to_tech],
)
billing_agent = Agent(name="billing", instructions="只处理账单与退款,不聊技术。")
client = Swarm()
resp = client.run(agent=triage, messages=[{"role": "user", "content": "我上个月被多扣了钱"}])
print(resp.agent.name) # 交接后变成 billing
注意 handoff 的实现方式:它就是一个返回 Agent 对象的函数。模型通过 function calling 调用它,框架看到返回值是个 Agent,就切换当前执行者。没有协议、没有状态机,只有一个"下一步谁来说话"的指针。
2. 与图式编排的根本差异
| 维度 | Swarm 式交接 | 图式编排(LangGraph 等) |
|---|---|---|
| 控制流写在哪 | 模型的 function call 里 | 代码的边与条件函数里 |
| 转移是否可枚举 | 否,取决于模型当次判断 | 是,边是有限集合 |
| 状态存哪 | 调用栈 + 消息列表 | 显式 State + checkpoint |
| 一段对话能走多远 | 由模型自行决定 | 由边与终止条件决定 |
| 中断/审批 | 需自己加 | 原生 interrupt |
| 失败重放 | 从头再来 | 断点续跑 |
一句话概括:Swarm 的转移是隐式的(你看代码看不出可能走向哪),图的转移是显式的(读 add_conditional_edges 就知道全部出口)。
3. 状态保存在哪:这是最大的工程差异
| 方案 | 状态位置 | 恢复能力 | 代价 |
|---|---|---|---|
| Swarm 内存对象 | 进程内存里的 messages 列表 | 无,进程挂了就没了 | 零成本 |
| Swarm + 自己落盘 | 每轮把 messages 序列化 | 可恢复到"某一轮之后" | 要自己设计断点 |
| Agents SDK + Session | 会话存储 | 按会话恢复 | 依赖 SDK 的存储抽象 |
| 图 + checkpointer | 数据库里的 State 快照 | 任意节点恢复、可改状态再继续 | 要设计 State 与 reducer |
Swarm 的 messages 列表是线性追加的,这意味着"回到第 3 轮重来"这件事在它里面没有语义——你只能把消息截断到第 3 轮,然后指望模型重新走一遍相同的路径。图式编排里这是一次 update_state 的事。
4. 什么时候 Swarm 是对的
| 场景 | 为什么合适 |
|---|---|
| 客服分流 | 问题类型决定去哪个专席,路径本身就是"看情况" |
| 意图路由 | 一个入口分发给多个下游能力,每次只走一条 |
| 多 persona 切换 | 同一个会话里换"说话的人",不换流程 |
| 原型验证 | 几十行就能跑起来,验证"分工是否有效" |
这类场景的共同点:一次请求只走一条路径,且路径选择本身就是模糊判断。 用图来写,你会得到一堆 if 和不必要的状态字段。
5. 什么时候必须用图
| 场景 | 为什么不能只靠交接 |
|---|---|
| 需要严格顺序(先校验再执行) | 顺序必须由代码保证,不能由模型"记得" |
| 需要审计每一步 | 图有显式节点,日志天然按步分片 |
| 需要人工审批 | 审批点必须是可暂停、可恢复的 |
| 需要失败重试与补偿 | 断点续跑依赖 checkpoint |
| 有并行分支 | 交接模型里没有"并行"这个概念 |
一个实用的判断:如果出了事故你要向别人解释"为什么走了这一步",而你说不出规则,那就该用图。
6. 同一件事的两种写法
图式版本里,交接变成了一条显式边:
from langgraph.graph import StateGraph, START, END
def route(state) -> str:
intent = classify(state["messages"][-1])
return {"billing": "billing", "tech": "tech"}.get(intent, "general")
g = StateGraph(State)
g.add_node("triage", triage_node)
g.add_node("billing", billing_node)
g.add_node("tech", tech_node)
g.add_edge(START, "triage")
g.add_conditional_edges("triage", route,
{"billing": "billing", "tech": "tech", "general": END})
g.add_edge("billing", END)
app = g.compile(checkpointer=checkpointer)
区别不在于功能,而在于出口是有限集合:读这段代码你能列出所有可能的走向;而在 Swarm 里,to_billing 和 to_tech 什么时候被调用,只有跑起来才知道。
Agents SDK 里的 handoff 更接近工程可用形态,转移仍然是工具调用,但转移目标被声明为类型:
from agents import Agent, handoff
billing = Agent(name="billing", instructions="处理账单与退款")
triage = Agent(
name="triage",
instructions="按问题类型转交,不要自己回答账单问题",
handoffs=[handoff(billing, tool_name_override="转交账单组")],
)
tool_name_override 值得注意:交接工具的名字就是模型看到的选项。写成 transfer_to_billing 不如写"转交账单组",后者在中文语境下被正确触发的概率明显更高。
7. 迁移到生产要补的四件事
| 补充项 | 做法 | 不加的后果 |
|---|---|---|
| 审计日志 | 每次交接记录 from、to、触发理由、消息摘要 |
出事无法归因 |
| 权限边界 | 每个 Agent 独立工具集与凭证 | 分流到低级 Agent 却有高级权限 |
| 成本上限 | 每会话交接次数上限 + 预算阈值 | 在两个 Agent 之间来回弹 |
| 终止判据 | 显式 end 条件,不让模型自己收尾 |
会话永不结束 |
MAX_HANDOFFS = 5
def guarded_handoff(state, target):
if state["handoffs"] >= MAX_HANDOFFS:
return {"messages": [("ai", "本次已转交多次,请描述具体问题以便直接处理")],
"handoffs": state["handoffs"]}
log.info("handoff from=%s to=%s reason=%s",
state["current"], target, state["last_reason"])
return {"current": target, "handoffs": state["handoffs"] + 1}
交接日志应该长成这样,一行一次转移(示意格式):
2025-11-03T09:12:41Z from=triage to=billing reason="用户提到退款" handoffs=1 ms=410
2025-11-03T09:13:20Z from=billing to=tech reason="账单已确认,但问到接口报错" handoffs=2 ms=380
2025-11-03T09:13:52Z from=tech to=end reason="已给出排查步骤" handoffs=2 ms=295
有了这三行,"为什么最后是技术组在回话"这个问题就有答案了;没有它,只能把整段对话重放一遍再猜。
"无限互踢"是 Swarm 式系统最经典的失控:两个 Agent 都觉得该对方处理,来回交接,每一轮都烧一次完整推理。加一个计数器是最低成本的止损。
8. 排错
| 现象 | 原因 | 处理 |
|---|---|---|
| 该转交却没转 | 交接工具的名字/描述不清晰 | 用业务语言命名,描述里写触发条件 |
| 不该转交却转了 | 当前 Agent 的 instructions 没划清边界 | 明确"你不处理 X,遇到 X 必须转交" |
| 两个 Agent 互踢 | 职责重叠 + 无次数上限 | 计数器 + 单向交接 |
| 会话跑了几十轮 | 模型自己决定继续 | 加显式终止条件与步数上限 |
| 无法复现同一路径 | 路由由模型每次重判 | 记录每次分类结果,必要时固化规则 |
| 转交后上下文丢失 | 只传了最后一条消息 | 交接时显式携带必要的状态摘要 |
9. 边界
| 场景 | 选择 | 理由 |
|---|---|---|
| 客服分流、意图路由 | Swarm 式交接 | 路径短、每次只走一条 |
| 多步审批流程 | 图 | 需要暂停、恢复、审计 |
| 需要并行处理 | 图 | 交接模型没有并行语义 |
| 快速原型 | Swarm 式 | 数十行即可验证分工 |
| 强合规、要出报告 | 图 | 每一步都得能被引用 |
还有一条中间路线值得知道:用图管主干,用交接管局部。 主流程用图保证顺序与审计,某个"分流"节点内部用 handoff 决定谁来回话。两者不是互斥的技术选型,而是同一系统里不同粒度的工具。
10. 自测
- Swarm 里 handoff 的技术实现是什么?为什么它不需要"协议"?
- "转移是隐式的"具体指什么?它给调试带来什么困难?
- 为什么在 Swarm 里"回到第 3 轮重来"没有语义?
- 哪一类场景最适合交接式控制流?举两个。
- "无限互踢"的最低成本止损手段是什么?
- 图管主干、交接管分流,这种混合形态的分界线怎么划?