该拆成多 Agent 吗:微软 AI Agent 入门课第 1 课与第 8 课对照

2026-08-18

很多人做第一个 agent 的顺序是这样的:先照着入门例子跑通一个带工具的 agent,然后看到”多智能体”这个词,觉得下一步理所当然是拆成三个角色。到底该不该拆,光看概念文章是看不出来的——概念文章只会告诉你”专业化分工质量更高”。

ai-agents-for-beginners 这个课程仓恰好提供了一个很干净的对照组:第 1 课 01-intro-to-ai-agents/code_samples/ 和第 8 课 08-multi-agent/code_samples/ 用的是同一个模型客户端、同一个旅行规划场景、同一套环境变量,唯一的差别就是编排层。把这两份文件并排放着读,你能非常具体地看到”多写了哪些代码”。

起手式几乎一样,分叉从 WorkflowBuilder 开始

Python 侧,01-python-agent-framework.ipynb 里建客户端是这样:

from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential

provider = FoundryChatClient(
    project_endpoint=endpoint,
    model=model,
    credential=AzureCliCredential()
)

08-python-agent-framework.ipynb 里这一段几乎逐字相同,差别只有两处:credential 换成了 DefaultAzureCredential,接收变量从 provider 改叫 client。两边读的环境变量都是 AZURE_AI_PROJECT_ENDPOINTAZURE_AI_MODEL_DEPLOYMENT_NAME,都在缺变量时直接 raise ValueError

造 agent 也一样,都是 .as_agent(name=..., instructions=...)。真正的分叉在下一行:第 1 课直接 await agent.run(...),第 8 课要先把 agent 接成图。

workflow = WorkflowBuilder(start_executor=planner_agent) \
    .add_edge(planner_agent, concierge_agent) \
    .build()

到这里为止,增量确实很小——三行。真正的成本在消费端。

第一笔成本:返回值从”回复”变成了”事件流”

第 1 课拿结果,非流式就是 print(response) 一行;流式是这样:

async for chunk in agent.run(
    "Tell me about Tokyo as a travel destination", stream=True
):
    print(chunk, end="", flush=True)

第 8 课同样是流式,代码变成了这样(抄自仓库 08-python-agent-framework.ipynb,中间打印分隔线的几行用 ... 略去):

async for event in events:
    if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
        update = event.data
        author = update.author_name
        if author != last_author:
            ...
            last_author = author
        print(update.text, end="", flush=True)

多出来的三件事值得逐个看清楚:一是 event.type 判断,流里不只有输出事件;二是 isinstance(event.data, AgentResponseUpdate),拿到 data 还要确认类型才能取 .text;三是 notebook 自己维护了一个 last_author 变量,靠比对 update.author_name 来判断”说话的人换了”。

这就是从单 agent 迁到多 agent 最先撞上的东西:输出不再是一个人的一段话,而是一条混合了多个作者的事件流,归属信息在数据里,但拆流得你自己写。 好消息是归属信息框架给了(author_name 是白给的),坏消息是任何一处要按角色分别落库、分别渲染的地方,你都得自己维护这个”当前作者”状态。

.NET 侧这笔账更明显。01-dotnet-agent-framework.cs 消费结果只有一个 await foreach

await foreach (var update in agent.RunStreamingAsync("Plan me a day trip"))
{
    await Task.Delay(10);
    Console.Write(update);
}

08-dotnet-agent-framework.cs 则是四步:InProcessExecution.RunStreamingAsync(workflow, new ChatMessage(ChatRole.User, "I would like to go to Paris.")) 拿到 StreamingRun,再 await run.TrySendMessageAsync(new TurnToken(emitEvents: true)),然后 await foreach (WorkflowEvent evt in run.WatchStreamAsync()...),循环体里判断 evt is AgentResponseUpdateEvent,用 executorComplete.ExecutorId 区分作者,并手动往 strResult 上累加拼最终结果。单 agent 那边没有 run 对象、没有 TurnToken、也没有”最终结果”要自己攒。

第二笔成本:一旦不是直线,消息类型得你自己定义

上面那个两段式流水线其实是最省事的形态——一条边,状态隐含在边里。真正的管理成本出现在扇出和条件分支上。

08-multi-agent/code_samples/workflows-agent-framework/python/ 下的并行示例里,为了把同一段用户输入广播给两个 agent,仓库专门写了一个 executor 类:

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)

然后 WorkflowBuilder(start_executor=dispatcher, output_executors=agents).add_fan_out_edges(dispatcher, agents).build(),结果通过 events.get_outputs() 取回。仓库在取结果那格的注释里写明:outputs 是一个列表,顺序按 output_executors 给定的顺序排。也就是说,并行之后结果的对应关系靠位置而不是靠名字,这个约定你得记住。

条件分支那份更能说明问题。为了让工作流按评审结果走两条不同的路,示例里出现了这些东西:一个 pydantic 的 ReviewAgent 模型、一个 @dataclassReviewResult、一个 @executor(id="to_reviewer_result") 的转换函数(把 AgentExecutorResponse 解析成 ReviewResult)、一个普通函数 select_targets(review, target_ids) 做目标选择,再由 add_multi_selection_edge_group(..., selection_func=select_targets) 接进图里;分支末端还要分清 ctx.yield_output(...)(产出给外部)和 ctx.send_message(...)(继续传给下一个 executor)。

这里有两处细节值得你在抄之前看清楚。第一,三个 agent 里只有 publisher 那个真的传了 response_format=PublisherAgent,另外两个的 response_format= 都是注释掉的,结构化输出实际上靠 instructions 里那句”return result as JSON with fields …”的口头约定,转换函数里则直接 ReviewAgent.model_validate_json(response.agent_run_response.text),没有包 try。第二,select_targets 上方的注释写的是 # Order: [handle_review, submit_to_email_assistant, summarize_email, handle_uncertain],而函数体只解包了 handle_review_id, save_draft_id 两个 id——注释和代码对不上。同一格里 save_draft 函数体上方还留着一句 # Only called for long NotSpam emails by selection_func,也带着邮件场景的字眼,与本示例的技术教程主题不相干。这两处都是仓库里白纸黑字的现状,指出来是为了提醒你别把它当成可以直接上生产的写法。

顺带一提,这个目录下的文件名还带着 ghmodel 字样,但并行那份 notebook 顶部的迁移说明已写明该示例改用 Microsoft Foundry 的 FoundryChatClient。文件名和实际接入目标已经脱节,按文件名去猜配置会走弯路。

一个常见的误会:多 Agent 不等于更能干

第 1 课的 agent 是带工具的——Python 侧用 @tool(approval_mode="never_require") 装饰 get_destinations(),.NET 侧用 [Description("Provides a random vacation destination.")] 标注静态方法再 AIFunctionFactory.Create(GetRandomDestination) 塞进 tools:

而第 8 课的两份主示例里,planner_agentconcierge_agentbudget_agent 三个 as_agent(...) 调用都只传了 name 和 instructions,一个 tool 都没有;.NET 那份也一样,ChatClientAgentOptions 里只有 NameDescription

所以这是两条正交的轴:工具决定 agent 能不能碰到外部世界,编排只决定谁先说谁后说。如果你现在的问题是”它查不到实时信息”,答案在第 1 课那个 @tool,不在 WorkflowBuilder

决策路径

按上面这些依据倒推,判断顺序大致是这样:

一次 run 就能拿到你要的全部输出吗? 能,就别建图。第 1 课的写法一共四步——建 client、as_agent 造 agent、await agent.run(...)print,出问题时你面对的只有一个 prompt 和一个 response。

你需要按角色分别拿到输出吗? 比如草稿存一处、评审意见存另一处。这种情况下 workflow 给的东西是对口的——author_name(Python)和 ExecutorId(.NET)是框架直接放在事件里的结构化字段,不需要你从模型输出的文本里再切一遍。

你需要并行或按结果分叉吗? 到这一步就得认账:要写 Executor 子类或 @executor 函数、要定义消息类型、要记住 output_executors 的顺序约定、要自己处理结构化输出的解析失败。这不是三行边的事。

你只是想让它更能干? 那是工具,回第 1 课。

有一个维度我们不比:单 agent 和多 agent 哪个效果好、哪个更省、哪个更快。仓库里没有给出任何可比数据,我们也没有跑过这些代码,任何这方面的结论都只能是编的。

抄之前先对齐的几处

两课的环境变量并不通用:Python 侧读的是 AZURE_AI_PROJECT_ENDPOINT / AZURE_AI_MODEL_DEPLOYMENT_NAME,.NET 侧读的是 AZURE_OPENAI_ENDPOINT / AZURE_OPENAI_DEPLOYMENT,且 .NET 侧对部署名写了一个兜底默认值 "gpt-5-mini"(这是仓库当前代码里的取值,随版本可能变动)。混着抄必然报缺变量。

依赖上也有落差。第 1 课的 .cs 文件头 #:package 全部钉到确定版本;第 8 课那份则出现了 @1.* 这类浮动写法,Microsoft.Agents.AI.OpenAI@1.*-* 还会带上预发布,并且引用了 #:package Azure.AI.Agents.Persistent@1.2.0-beta.10——这是标了 beta 的预发布包,接口随时可能变,这一点在动手前要有心理准备(该取值同样是仓库当前示例里的值)。第 8 课还多了一个 DotNetEnv,代码里是 Env.Load("../../.env"),写的是相对路径。仓库里没有说明这个相对路径以什么为基准,所以 Windows 下在 PowerShell 里从仓库根目录直接 dotnet run 08-multi-agent\code_samples\08-dotnet-agent-framework.cs,和先把当前目录切到 .cs 所在目录再运行,两种起手式解析出来的 .env 位置未必是同一个——报缺变量时这是值得先排除的一处。先切目录再运行属于通用做法,不是该课程官方给的步骤。 Linux/macOS 侧仓库文档给的是先 chmod +x 再直接执行 .cs 文件,Windows 上没有这一步,走 dotnet run 即可。

还有一处不一致值得知道:01-dotnet-agent-framework.md 的前置条件写的是 .NET 10 SDK 或更高,08-dotnet-agent-framework.md 写的是 .NET 9.0 SDK 或更高,两份文档口径不同。以哪个为准仓库里没有说明,装 SDK 前建议按较高的那个准备。

最后是指令的落点。第 1 课 .NET 侧走的是 AsAIAgent(instructions: "...", tools: [...]) 这个形参;第 8 课把 REVIEWER_INSTRUCTIONSFRONTDESK_INSTRUCTIONS 这两个常量赋给了 ChatClientAgentOptionsDescription 属性。两处对”给 agent 的指令放哪”的写法不一样,仓库里没有解释这个差异,照抄哪一份就照哪一份的字段名来,别互相套。

以上路径与字段均以仓库文件为准,该课程持续更新,接口写法随版本变动,以仓库最新内容为准。


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

本文对照的是同一项目内的两种用法,依据均为上述仓库内容,不对两种用法做优劣排名, 选型结论只在仓库文档写明的能力边界内成立。

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