微软 AI Agent 入门课第 11 课:WorkflowBuilder 怎么串起三个 Agent
手上有三个各管一摊的 agent,想让它们互相接力,第一反应通常是自己写个 for 循环把上一个的输出塞进下一个的输入。写着写着就会发现麻烦的不是循环,是「谁在说话」这件事在流式输出里彻底糊掉了——三段文字连成一片,分不清哪句是哪个 agent 说的。
ai-agents-for-beginners 第 11 课的 A2A 示例,正好把这一段处理干净了。这篇只拆一个文件:11-agentic-protocols/code_samples/11-a2a-agent-framework.ipynb。
前置条件:运行这份 notebook 需要什么
这一段最容易被跳过,但它决定了你在第三个 cell 就会不会被拦下来。
notebook 第一个代码 cell 是安装:
%pip install agent-framework azure-ai-projects azure-identity python-dotenv
依赖之外,00-course-setup/README.md 写明的前提是:一个 Microsoft Foundry 项目并且部署好了模型,装好 Azure CLI,然后 az login。课程正文特意解释过,多数 notebook 通过 AzureCliCredential 或 DefaultAzureCredential 拿凭据,所以 .env 里不放密钥。要填的是两个变量:
AZURE_AI_PROJECT_ENDPOINT=https://<your-project>.services.ai.azure.com/api/projects/<your-project-id>
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-5-mini
上面这个部署名是仓库示例里写的值,换成你自己的部署名。
Windows 侧别照抄 bash:课程 README 给的复制命令,PowerShell 是 Copy-Item .env.example .env,zsh/bash 才是 cp .env.example .env;虚拟环境激活在 Windows 命令提示符下是 venv\Scripts\activate。没有浏览器的远程环境(比如 Codespaces),README 给的是 az login --use-device-code。
有一处得提前说清楚:仓库根目录的 requirements.txt 里,agent-framework-core 是被钉住版本的,注释自述的理由是更高版本引入了课程用到的破坏性改动(移除了 ChatMessage 与 HostedWebSearchTool、改了 Message 构造、Agent.run() 去掉了 model= 参数),并且注释说明它是直接钉 agent-framework-core 而不是走 agent-framework 这个 meta-package,以免 extras 拉进未钉版本的子包造成 pip 冲突。而 notebook 里那行 %pip install 装的恰恰是 agent-framework。这是仓库当前两处内容的差异,我把它摆出来,你装依赖时心里有个数;具体以仓库最新的 requirements.txt 为准。
角色定义:三个 agent 的差别全在 instructions 里
三个 agent 用的是同一个 client、同一个模型部署,代码上唯一的区别是 name 和 instructions:
currency_agent = client.as_agent(
name="CurrencyExchangeAgent",
instructions="""You are a currency exchange specialist. You help travelers understand:
- Current exchange rates between currencies
...""",
)
(instructions 有截断,完整文本见仓库文件。)
另外两个是 ActivityPlannerAgent 和 TravelManagerAgent。值得注意的是 TravelManagerAgent 的 instructions 里写了带编号的三步:先从货币专家那里拿信息、再拿活动推荐、最后汇总成一份出行简报。也就是说,这个「协调者」的协调逻辑是写在自然语言提示里的,不是写在代码里的——代码层面它和另外两个 agent 是同一种对象。
name 也不只是个标签,后面事件循环要靠它分段。
通信构造:边定义在 WorkflowBuilder 上
真正把三个 agent 串起来的是这一段:
workflow = WorkflowBuilder(start_executor=currency_agent) \
.add_edge(currency_agent, activity_agent) \
.add_edge(activity_agent, travel_manager) \
.build()
start_executor 指定入口,两条 add_edge 定义传递方向,build() 收口。链路是货币 → 活动 → 汇总,一条直线,没有分支也没有回路。
然后是运行与消费事件:
events = workflow.run(
"Plan a week-long trip to Tokyo. I love food, temples, and technology.",
stream=True,
)
async for event in events:
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
update = event.data
author = update.author_name
开头说的「谁在说话」问题就解在这里:stream=True 之后拿到的是一串事件,代码先按 event.type == "output" 过滤,再用 isinstance(event.data, AgentResponseUpdate) 确认类型,最后从 update.author_name 取作者名。notebook 用一个 last_author 变量存住上一次的作者,作者变了才打印分隔线和标题,正文则是 print(update.text, end="", flush=True) 连续吐出。这套写法值得抄——它把「分段」这件事挂在 author_name 的变化上,而不是挂在你对轮次的假设上。
还有个细节:cell 里 from agent_framework import tool, AgentResponseUpdate, WorkflowBuilder 把 tool 也导进来了,但这份 notebook 从头到尾没有注册任何工具。
以上代码为仓库文件中的节选与组合,未经实测,以仓库最新代码为准。
和 MCP 的位置差别,在参数位上看得最清楚
同一课还有一份 11-agentic-protocols/code_samples/11-mcp-agent-framework.ipynb。把两边的 as_agent() 并排看,差别不用讲概念就摆在那儿:
MCP 那份里,能力是作为参数塞进单个 agent 的:
agent = client.as_agent(
tools=[search_accommodations, get_local_experiences],
name="AccommodationAgent",
...
)
两个工具函数用 @tool(approval_mode="never_require") 装饰,参数用 Annotated[str, "..."] 带描述。也就是说,MCP 的落点是 as_agent(tools=...) 这个参数位——能力长在一个 agent 内部。
这里要顺带说清楚,那份 notebook 同样声明了自己是模拟:它的 markdown cell 写明「真正的 MCP server 需要一个跑着的服务进程」,所以改用本地 @tool 函数来演示这套模式,生产环境里这些工具应该是从 MCP server 动态发现的、而不是像示例这样在本地定义。两份 notebook 在「示例是简化版」这一点上是一致的,别拿任何一份当协议实现来读。
A2A 那份里,as_agent() 完全没有 tools= 参数,连接关系落在 WorkflowBuilder.add_edge() 上——关系长在 agent 与 agent 之间。
第 11 课 README 对两者的分工也写得直白:MCP 面向 LLM 与工具的连接,A2A 面向不同 agent 之间的协作,并且写明下游的专职 agent 各自跑自己的 LLM、用自己的工具(这些工具本身可能就是 MCP server)。
边界:这份 notebook 不是 A2A 协议实现
这是读这一课最需要看清的一点,而且不用推断,notebook 自己就写了。
A2A 那节的正文原文说的是「我们模拟(simulate)A2A 风格的协作」,后面标题为「Understanding A2A in Production」的那个 markdown cell 又补了一句:上面构建的 workflow 是这套模式的一个简化的、进程内的版本;真实部署里每个 agent 会暴露 HTTP 端点、发布 Agent Card、通过 A2A 的 JSON-RPC 协议通信。
具体缺了什么,可以拿 README 的组件描述来对。README 说 Agent Card 上带的是:agent 的名称、它能完成的一般任务的描述、一份带描述的技能清单(好让别的 agent 甚至人类判断什么时候该叫它)、当前的 Endpoint URL,以及版本与能力(比如是否支持流式响应和推送通知)。Agent Executor 负责把用户会话的上下文传给远程 agent;远程 agent 干完活产出 Artifact,里面装着结果、完成了什么的描述以及在协议里传递的文本上下文,Artifact 送出之后连接就关掉,等下次需要再建;Event Queue 则用来处理更新与传消息,README 特意点明它在生产里的作用是防止任务还没做完连接就被关掉。notebook 的 markdown 里还列了 A2A 定义的任务状态:submitted、working、completed、failed。
这四个组件和这组状态,在这份 notebook 的代码里一个都没有出现。「发现」那一半尤其明显——三个 agent 是在同一个文件里用变量名直接引用的,不存在任何查询注册表、读取 Agent Card 再决定叫谁的环节。所以这一课能学到的是「怎么定义角色、怎么定义传递关系、怎么按作者切分流式输出」,学不到的是「跨进程、跨组织的发现与鉴权」。想找真正构造远程 agent 的写法,仓库里目前能查到的是第 14 课 README 的一个片段,用的是 A2AAgent(name=..., description=..., agent_card=..., url=...)。它是 README 里的代码片段,不是可运行的示例文件。
另有两处得对上号:requirements.txt 里列了 a2a-sdk,AGENTS.md 的依赖说明也把它标注为 A2A 协议支持,但第 11 课这份 notebook 并没有 import 它。以及同一课的 MCP 目录下确实有 mcp-agents/server/ 与 mcp-agents/client/ 这样的真实服务端与客户端代码,A2A 这边没有对应的目录。
还有个跨文件的写法差异:第 11 课 notebook 用的是 client.as_agent(...),第 14 课 README 的示例用的是 create_agent(...)。写代码时以你实际装到的那个版本为准。
怎么验证你配对了
第一道关卡在 notebook 自己手里。环境变量那个 cell 会把 AZURE_AI_PROJECT_ENDPOINT 和 AZURE_AI_MODEL_DEPLOYMENT_NAME 收集进一个 missing 列表,缺任意一个就 raise ValueError 并把缺的变量名列出来。走过这个 cell,说明 .env 至少被 dotenv.load_dotenv() 读到了。
第二道是凭据。课程 README 给的检查命令是 az account show,用来确认 az login 的会话在,并且订阅选的是包含 Foundry 项目的那个——DefaultAzureCredential() 拿的就是这个会话。
第三道是链路本身:按上面那段事件循环的写法,作者名每变一次就会打印一次分隔线和标题,所以分隔线出现的次数对应链路里实际发言过的 agent 段数。如果只出现了一段,说明边没有连上或者事件没走到后面的 executor。这里说的是代码写了什么,我们没有跑过这份 notebook。
课程仓持续更新,上面涉及的文件路径、依赖钉法与接口写法都可能随版本变动,以仓库最新内容为准。
本文依据 github.com/microsoft/ai-agents-for-beginners 仓库于 2026-08-18 的公开内容整理,
事实来自仓库内的课程正文与代码示例。我们没有跑过文中涉及的代码,
因此不涉及运行结果、耗时与 Agent 实际表现的任何描述。
该课程持续更新,文中涉及的文件路径、依赖与接口写法随版本变动,请以仓库最新内容为准。
文中涉及的云端服务调用会产生费用并可能上传数据,请自行评估密钥与数据边界。