basic/sequential/concurrent 差在哪:微软 AI Agent 入门课第 8 课

2026-08-18

打开 08-multi-agent/code_samples/workflows-agent-framework/python/ 这个目录,第一眼会觉得三个 notebook 是同一份东西复制了三遍:都 load_dotenv()、都建 FoundryChatClient、都写一堆 agent 的 instructions、都用 WorkflowViz 打印 mermaid 图。翻到第三个的时候很容易开始跳着看,然后就漏掉了这一课真正要讲的东西。

其实这三个文件的差别只集中在两处:图是怎么连起来的,以及结果从哪几个节点收上来。把这两处对齐着看,第 8 课就讲完了。下面按仓库里的代码逐个拆,文件名一律用仓库内相对路径。

前置条件:这一步跳过去后面全是报错

00-course-setup/README.md 的 Requirements 一节写明的门槛:

  • Python 3.12+,并且特意提醒如果没装 3.12,要用 python3.12 去建 venv,好让 requirements.txt 里的版本装对;
  • Azure CLI,用来 az login——三个 notebook 都用 AzureCliCredential(),不走 API key;
  • 一个 Microsoft Foundry 项目,并在里面部署好模型。

环境变量只要两个,.env.example 里给的是 AZURE_AI_PROJECT_ENDPOINTAZURE_AI_MODEL_DEPLOYMENT_NAME(示例值写的是 gpt-5-mini,这只是仓库里的示例值,换成你自己部署的名字即可)。复制 .env 这一步 README 分了两种写法,Windows 侧不要照抄 bash 那行:

# zsh/bash
cp .env.example .env
# PowerShell
Copy-Item .env.example .env

激活 venv 同理,README 里 Windows 命令提示符那行是 venv\Scripts\activate,而 zsh/bash 是 source venv/bin/activate

还有一处很容易吃亏:三个 notebook 的 markdown 说明里都写着 pip install agent-framework -U,而仓库根目录的 requirements.txt 里,注释明确说明它是直接锁 agent-framework-core==1.10.0,理由写在注释里——1.11.0 引入了课程 notebook 用到的破坏性变更(移除了 ChatMessageHostedWebSearchTool、改了 Message 构造函数、去掉了 Agent.run() 上的 model= 参数),而且不用 agent-framework 这个 meta-package,是为了避开 [all] extras 拉进未锁版本的子包造成 pip 冲突。

好在 notebook 自己已经处理了这个矛盾:三个文件的第一个代码格里,那行 pip install被注释掉的,上面跟着一句「Already covered by repo-level requirements.txt; left for reference.」。所以真正照做的时候,别去执行 markdown 里那行 -Upip install -r requirements.txt 才是与 notebook 代码对得上的那条路。以上均为仓库当前内容,随版本可能变动,以仓库最新内容为准。

agent 本身反而是三份里最没差别的部分

拆图之前先把不变量摘出来。三个 notebook 建 agent 的写法完全一致,都是先建一个 client:

provider = FoundryChatClient(
    project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
    model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
    credential=AzureCliCredential(),
)

再用 provider.as_agent(name=..., instructions=...) 造 agent。这三个 notebook 调用 as_agent 时只传了 nameinstructions 两个参数,没有传工具、没有传 response_format(同目录 04 就传了 tools=response_format=,可以对照着看),也就是说这三例里的分工完全靠 prompt。所以你在三个文件之间来回比对时,那几大段 instructions 常量(REVIEWER_INSTRUCTIONSSalesAgentInstructionsResearcherAgentInstructions 之类)可以整块跳过,它们不影响编排结构。顺带一提,README 里那段示例代码把 client 变量叫 chat_client,notebook 里叫 provider,是同一个东西换了个名字。

import 那几行倒是值得看一眼,它比正文更早暴露每一课用到什么。01 和 02 从 agent_framework 里只取 WorkflowBuilderWorkflowEventWorkflowViz,03 则多取了 ExecutorWorkflowContexthandler——正好对应它要自定义节点。03 的 import 里还带着 Messagetyping.Any,但这两个符号在该 notebook 的代码格里再没出现过。

三个 notebook 的图分别怎么连

01 basic01.python-agent-framework-workflow-ghmodel-basic.ipynb 里两个 agent,front_desk_agent(instructions 让它简短、每次只给一条推荐)和 reviewer_agent(名字是 Concierge,负责判断推荐够不够本地化)。图就一条边:

workflow = (
    WorkflowBuilder(start_executor=front_desk_agent)
    .add_edge(front_desk_agent, reviewer_agent)
    .build()
)

WorkflowBuilder 的第一个参数是 start_executoradd_edge(a, b) 的语义是 a 的输出流向 b。这里没有显式指定输出节点,notebook 里那段注释写得很清楚:reviewer 是最后一级,也就是唯一的 output。

02 sequential02....-sequential.ipynb 把链拉长到三个——sales_agent(从描述里挑家具)、price_agent(拆价格区间)、quote_agent(输出 markdown 报价单)。连接方式没变,只是多写了一条 add_edge

workflow = (
    WorkflowBuilder(start_executor=sales_agent)
    .add_edge(sales_agent, price_agent)
    .add_edge(price_agent, quote_agent)
    .build()
)

所以 basic 和 sequential 在 API 层面根本是同一个模式,区别只是边的条数。这一点仓库 README 自己也是这么归的:Case 1 叫 Basic Sequential Workflow,Case 2 叫 Multi-Step Sequential Workflow。

03 concurrent:这个才是真正换了写法。03....-concurrent.ipynb 先自定义了一个透传节点:

class InputDispatcher(Executor):
    """Forward the user input unchanged to all participating agents."""

    @handler
    async def forward(self, text: str, ctx: WorkflowContext[str]) -> None:
        await ctx.send_message(text)


dispatcher = InputDispatcher(id="dispatcher")
agents = [research_agent, plan_agent]

workflow = (
    WorkflowBuilder(
        start_executor=dispatcher,
        output_executors=agents,
    )
    .add_fan_out_edges(dispatcher, agents)
    .build()
)

三个新面孔要看清:继承 Executor 并用 @handler 装饰的方法(形参带 WorkflowContext[str],靠 ctx.send_message 往下游发消息)、WorkflowBuilderoutput_executors 参数、以及 add_fan_out_edges(source, targets)。前面两个 notebook 一个都没用到。

结果是怎么收上来的

这是三者最实际的差别,也是最容易写错取值代码的地方。

01 和 02 的取结果写法一模一样(下面这段抄自 01,02 只是把传进去的字符串换成了那段家具需求描述):

events = await workflow.run("I would like to go to Paris.")
outputs = events.get_outputs()
result = outputs[0].text if outputs else ""

只有一个终点,所以 outputs[0] 就够了。

03 则不是。output_executors=agents 传进去的是一个列表get_outputs() 回来的也是列表,notebook 里的注释把顺序写死了:outputs 是一组 AgentResponse,每个输出 executor 一个,顺序就是传给 output_executors 的顺序(先 research_agentplan_agent)。所以它是这样遍历的:

if outputs:
    print("===== Final Aggregated Responses =====")
    for i, response in enumerate(outputs, start=1):
        print(f"{'-' * 60}\n\n{i:02d}:\n{response.text}")

换句话说,Python 这一版的并发没有写聚合节点,两路结果是并排返回、由调用方自己拼的。如果你按 basic 的习惯去写 outputs[0].text,代码不会报错,但你会静悄悄地只拿到 researcher 那一路。

几处对不上的地方,读之前先知道

这一课的坑不在代码本身,在于文档与代码不同步。以下都是把仓库里两处白纸黑字放在一起就能看出来的:

  1. README 讲的并发写法和 notebook 里的不是一个workflows-agent-framework/README.md 的 Case 3 里,Python 部分给的是 ConcurrentBuilder().participants([...]).build(),说 builder 会自动生成 fan-out / fan-in;而 03 notebook 里实际是自定义 InputDispatcheradd_fan_out_edges,没有 fan-in。照 README 抄会对不上 notebook。
  2. 02 的输入已经不是图片了。README 的 Case 2 写的是用 ChatMessageTextContent + DataContent(uri=image_uri, media_type="image/png") 传一张客厅照片;notebook 里虽然仍保留了读 ../imgs/home.png 并转 base64 的那格,但下一格的注释说明这次迁移改成传同一场景的文字描述,理由是让这一课聚焦在编排机制上,并注明 agent 接受纯字符串、与 basic/concurrent 两例一致。
  3. run_stream 的状态在同一目录里两说。01 和 02 取结果那一格的注释都写着 Workflow.run_stream 已不再属于公开 API,现在用 workflow.run(...)get_outputs()(03 没写这句注释,但代码同样是 run + get_outputs);而同目录的 04.python-agent-framework-workflow-aifoundry-condition.ipynb 仍然在用 async for event in workflow.run_stream(task)。04 用的 client 也不同(agent_framework.azureAzureAIAgentClient,不是 01–03 的 agent_framework.foundry.FoundryChatClient)。这两处在仓库里并存,我们不替作者解释原因。
  4. 文件名里的 ghmodel 已经名不副实。三个 notebook 顶部的 Migration note 都写明:这批示例原先引用 GitHub Models,因其已弃用(仓库注明将于 2026 年 7 月退役),现改用 Microsoft Foundry 的 FoundryChatClient,指向 Azure OpenAI 的 Responses API。文件名没跟着改。
  5. SVG 导出是可选项viz.export(format="svg") 在三个 notebook 里都被 try/except ImportError 包着,注释说明它需要额外的 graphviz extra 以及 graphviz 的系统二进制;装不上就跳过,不影响工作流本身。
  6. class DatabaseEvent(WorkflowEvent): ... 在 01 和 02 里各定义了一次,但这两个 notebook 后面没有再引用它;03 里没有这个定义。

另外提醒一句:concurrent 那个 notebook 的说明文字里有大量关于吞吐、加速、资源利用的表述。我们没有跑过这些代码,本文不复述也不背书任何运行表现方面的说法。

怎么确认自己连对了

最省事的验证不需要真的调模型。WorkflowViz 在 build 之后就能用:

viz = WorkflowViz(workflow)
print(viz.to_mermaid())
print(viz.to_digraph())

to_mermaid()to_digraph() 各输出一份图的文本描述。basic 应该是一条直线、sequential 是三节链、concurrent 是从 dispatcher 分出两条边且下游不再汇合——这一步就能把「我以为连了 fan-out 其实写成了串行」这类错误挡掉。

真正调用模型之后,再看一眼 len(events.get_outputs())。按 notebook 注释的口径,get_outputs() 每个 output executor 给一条 AgentResponse——01/02 只有一个终点,03 则对应 output_executors 里那份列表。这个数与你的预期对不上时,先回头看 output_executors 那一行和 add_fan_out_edges 的 targets,而不是去调 agent 的 instructions。

把这三种跑法吃透之后,同目录的 04 才好读:它换了一套连法,用 add_multi_selection_edge_group(source, [handle_review, save_draft], selection_func=select_targets) 做分支,select_targets 拿到解析后的 ReviewResult 与一组 target_ids,按 review.review_result == "Yes" 决定返回哪个 id。也就是说,从 basic 到 conditional,变的一直是 builder 上那几个方法名,agent 那一侧几乎没动过——这大概是第 8 课最值得记住的一条。

以上为按仓库代码中的接口语义组合的示例,未经实测,以仓库最新代码为准。另外,这三个 notebook 都会调用云端模型服务,az login 之后的凭据与你传进去的业务文本都会离开本机,动手前先看清楚数据边界。


本文依据 github.com/microsoft/ai-agents-for-beginners 仓库于 2026-08-18 的公开内容整理, 事实来自仓库内的课程正文与代码示例。我们没有跑过文中涉及的代码, 因此不涉及运行结果、耗时与 Agent 实际表现的任何描述。 该课程持续更新,文中涉及的文件路径、依赖与接口写法随版本变动,请以仓库最新内容为准。 文中涉及的云端服务调用会产生费用并可能上传数据,请自行评估密钥与数据边界。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。