高层编排模式:把 agent 编进工作流
30 秒导读: 前面几章讲了单个 agent 怎么跑(第 01 章)、怎么长手脚(第 02 章),以及底层那台"类型路由的 Pregel 图引擎"怎么转(第 03 章)。本章讲的是最上面那层:框架怎么把"多个 agent 协作"这件事,变成五种开箱即用的拓扑(顺序、并行、去中心路由、编排者主导、Magentic 自规划)。核心洞察只有一句——这五种模式不是各写一套引擎,而是各自在同一台图引擎上"生成一张特定形状的图"。理解了这句,五个 Builder 就都通了。
本章覆盖两块内容:
- 桥接层——让 agent 和 workflow 能互相包裹的三个适配器(
AgentExecutor、WorkflowAgent、WorkflowExecutor)。 - 编排包——
agent_framework_orchestrations里的五个高层 Builder,以及压轴的 Magentic 台账机制。
1. 先搞清楚一件事:两个世界要打通
框架里有两个"世界",词汇不一样:
| 世界 | 基本单位 | 怎么调用 | 输出 |
|---|---|---|---|
| Agent 世界 | Agent / 任何 SupportsAgentRun | agent.run(messages) | AgentResponse |
| Workflow 世界 | Executor(图里的节点) | 引擎按边把消息路由给它 | 靠 ctx.send_message / ctx.yield_output |
编排的本质,就是把 agent 塞进 workflow 的图里当节点跑。但 agent 的接口(run)和节点的接口(收消息、发消息)对不上。所以框架先造了一层适配器,把两个世界的接口互相翻译。
桥接层一共三个适配器,方向各不同:
桥接层三件套(谁包谁)
Agent ──包成──► AgentExecutor (agent 当图里一个节点)
└ 收 AgentExecutorRequest,发 AgentExecutorResponse
Workflow ──包成──► WorkflowAgent (整张图反过来当一个 agent)
└ 对外暴露 .run(),内部把 workflow 事件翻成 AgentResponse
Workflow ──包成──► WorkflowExecutor (子工作流当父图里一个节点)
└ 图套图,支持嵌套
先把这三个适配器讲透,后面五个 Builder 才有地基。
2. 桥接件一:AgentExecutor —— 把 agent 包成节点
它解决的小问题: 图引擎只认 Executor。你有一个 agent,想让它在图里当一个节点,谁来收发消息、谁来维护对话上下文?
思路: 写一个 Executor 子类,内部持有 agent;收到消息就攒 进缓存,该回复时调 agent.run(),把结果打包成一个标准信封发给下游。这个包装类就是 AgentExecutor(python/packages/core/agent_framework/_workflows/_agent_executor.py:119,class AgentExecutor)。
2.1 两个标准信封
整个编排包的节点之间,传的都是这两个 dataclass:
| 信封 | 方向 | 关键字段 | 源码 |
|---|---|---|---|
AgentExecutorRequest | 发给 agent 节点 | messages、should_respond(是否要它真的回复) | _agent_executor.py:32(class AgentExecutorRequest) |
AgentExecutorResponse | agent 节点发出 | executor_id、agent_response、full_conversation(到此为止的完整对话) | _agent_executor.py:46(class AgentExecutorResponse) |
should_respond=False 是个关键设计:它让编排者可以只把消息灌进某个 agent 的上下文缓存、但不让它现在开口(见 run handler,_agent_executor.py:197)。后面群聊/交接的"广播同步"全靠这个开关。
full_conversation 也不是摆设。它保证下游 agent 拿到的是完整对话历史而不是只有上一个 agent 的最后一句——否则链条越长,前面的用户提问越容易丢。
2.2 无缝链接:三种输入都能接
AgentExecutor 定义了一组 handler,靠输入类型自动分派(这正是第 03 章讲的类型路由):
| 收到的类型 | 走哪个 handler | 行为 |
|---|---|---|
AgentExecutorRequest | run | 标准路径,攒缓存后按需回复 |
AgentExecutorResponse | from_response | 上一个 agent 的输出直接喂进来,继续对话 |
str | from_str | 裸字符串当新用户输入 |
Message / list[Message] | from_message / from_messages | 单条/多条消息 |
from_response(_agent_executor.py:213)里藏着一个上下文策略开关 context_mode:
# 示意,非源码:from_response 里怎么决定"把多少历史喂给下一个 agent"
if context_mode == "full": # 默认:全量历史都带上
cache.extend(prior.full_conversation)
elif context_mode == "last_agent": # 只带上一个 agent 的回复
cache.extend(prior.agent_response.messages)
else: # custom:用户给的过滤函数说了算
cache.extend(context_filter(prior.full_conversation))
一个容易踩的坑(源码里专门警告了): 如果你写自定义 executor,想改写 agent 的输出文本,别直接
send_message一个裸str——那会命中下游的from_str,把完整对话历史丢光。要用AgentExecutorResponse.with_text(...)(_agent_executor.py:62),它保持信封类型不变,于是走from_response,历史得以保留。这个坑在from_str的 docstring(_agent_executor.py:244)里被明确点名。
3. 桥接件二:WorkflowAgent —— 把整张图反过来当 agent
它解决的小问题: 你辛辛苦苦编排了一张多 agent 的图,现在想把它当成一个普通 agent 塞进别人的系统(或者再嵌进另一张图)。可是图的接口是"跑起来吐一串事件",不是 run() -> AgentResponse。
思路: 反向包装。WorkflowAgent(python/packages/core/agent_framework/_workflows/_agent.py:52,class WorkflowAgent)继承 BaseAgent,对外长得就是个 agent——有 .run();内部把 workflow 跑出来的事件流,翻译回 AgentResponse / AgentResponseUpdate。
它在构造时会做一个类型校验:workflow 的起始节点必须能吃 list[Message],否则拒绝包装(_agent.py:117)——因为 agent 的输入就是消息列表,图的入口得对得上。
翻译规则很清晰,只放行两类事件(_agent.py:_convert_workflow_events_to_agent_response,起于 :483):
| workflow 事件类型 | 翻成什么 |
|---|---|
output(终态输出) | 追加进 AgentResponse.messages |
request_info(要人介入) | 翻成一个"函数审批请求"内容,交给上层处理 |
其它(生命周期、诊断、编排内部事件如 group_chat/handoff_sent/magentic_orchestrator) | 一律丢弃 |
这就是"图套 agent 套图"能无限嵌套的原因:每一层只暴露干净的 AgentResponse,内部噪音全被这层滤掉。