微软 AI Agent 入门课第 7 课的规划:TravelPlan 在哪一步被推翻

2026-08-18

看规划类的课程最容易犯的毛病,是读完满脑子「先拆解、再分派、有必要就重规划」,回头写代码却不知道第一行该敲什么。ai-agents-for-beginners 的第 7 课 07-planning-design/ 好就好在它把「计划」这个抽象词落成了一个 Pydantic 类——你能指着字段问:这个字段谁填、填完谁读、读完谁执行。

那我们就从这个类开始翻。

同一课里有两个 TravelPlan,字段不一样

先说一件读的时候容易懵的事:07-planning-design/README.md 正文里的模型,和配套 notebook 07-planning-design/code_samples/07-python-agent-framework.ipynb 里的模型,不是同一个。

README 正文里的定义是这样的:TravelSubTask 只有 task_detailsassigned_agent 两个字段,而 assigned_agent 的类型是 AgentEnum——一个继承 str, Enum 的枚举,取值是 flight_bookinghotel_bookingcar_rentalactivities_bookingdestination_infodefault_agentgroup_chat_manager 这一组。外层 TravelPlan 则是 main_tasksubtasksis_greeting 三个字段。

notebook 里定义数据模型的那个单元换了一套:TravelSubTask 变成 task_id: intdescriptionassigned_agent: strpriority: strdependencies: list[int] = [];外层 TravelPlan 变成 destinationtrip_duration_dayssubtaskstotal_estimated_budget_usdnotes

把这两处并排放,能看出取舍在哪一边:README 那版用枚举把 assigned_agent 锁死,模型只能从固定名单里挑一个,但整个结构里没有任何地方能表达「订完机票才能订酒店」;notebook 那版补上了 task_iddependencies,让子任务之间的先后关系有地方写,代价是 assigned_agent 退回成裸 str,代码层面不再校验它是不是一个真实存在的 agent,注释里只用 # "flight_agent", "hotel_agent", "activity_agent" 提示了一下。这两版哪个更接近你的场景,取决于你是更怕分派错人,还是更怕顺序错乱。仓库把两版都留着,没有说明谁替代谁。

notebook 在这两个模型上方那段 Task Decomposition 说明里,把拆解的收益列成了四条:每个子任务单一职责、互不依赖的子任务可以并发、失败被隔离在单个子任务里、成本按子任务估算再汇总(仓库文档自述)。最后一条正好对上结构里那个 total_estimated_budget_usd——字段不是随手加的,它对应着自述里的一条诉求。反过来看 README 那版没有预算字段,也就谈不上这条。你自己定结构时可以照这个路子倒着推:先写下你要靠计划回答的问题,再看哪个字段能承载它。

README 那版里还有个 is_greeting: bool 挂在 TravelPlan 上,以及 AgentEnum 里那个和其它取值明显不同类的 group_chat_manager。后者在正文的 Planning Agent with Multi-Agent Orchestration 一节能找到落点:那一节把 planner 的动作拆成四步——接住用户消息并依据系统提示词生成结构化计划、对照 agent 注册表(正文写明这个注册表持有各 agent 及其提供的函数或工具)、按子任务数量决定是把消息直接发给某个专职 agent 还是交给 group chat manager 去协调多 agent 协作、最后汇总结果。所以那个枚举值不是一个「会订机票的 agent」,而是一条协作路径的名字。至于 is_greeting,仓库里我们没有找到对它的说明,从名字看像是给「用户只是打了个招呼」这种不需要拆解的输入留的开关,但正文没写,这里不替它下结论。

顺带一提,.NET 那份 07-planning-design/code_samples/07-dotnet-agent-framework.cs 用的是第三套:TravelPlan 只有 Main_taskSubtasks,子项类叫 Plan,字段是 Assigned_agentTask_details,靠 [JsonPropertyName] 映射回蛇形命名。它连环境变量名都和 Python 侧不同,走的是 AZURE_OPENAI_ENDPOINTAZURE_OPENAI_DEPLOYMENT。看 Python 就别去参考它的写法,两边不通用。

结构怎么从「提示词里的一段话」变成硬约束

README 正文那段代码,让模型吐 JSON 的方式是把格式写进 system_prompt——提示词里直接贴了一段 {'main_task': ..., 'subtasks': [...]} 的样例,然后调用 client.create_response(input=user_message, instructions=system_prompt),拿到 response.output_textjson.loads 一把。也就是说,那几个 Pydantic 类在这条路径上其实没参与解析,它们只是文档意义上的约定。

notebook 走的是另一条:

planning_agent = client.as_agent(
    name="TravelPlanner",
    instructions="""You are a travel planning agent. When given a travel request:
1. Break it into specific subtasks (flights, hotels, activities, logistics)
2. Assign each subtask to the appropriate specialist agent
3. Set priorities and identify dependencies between tasks
4. Estimate the total budget""",
)

result = await planning_agent.run(
    "Plan a 7-day trip to Paris for a couple interested in art, cuisine, and history. Budget around $5000.",
    options={"response_format": TravelPlan}
)
plan = result.value

差别在 options={"response_format": TravelPlan} 这一处:类被交给了框架,返回值从 result.value 取出来就是对象,后面 plan.subtasks 直接当列表遍历,不用再手工 json.loads。上面那段行程描述与预算数字是仓库示例里的具体输入值,换成你自己的请求即可。

这里有个抄代码会踩的坑,直接说破:README 正文那段里创建客户端时变量名写的是 provider = FoundryChatClient(...),往下调用的却是 client.create_response(...)——同一段里 client 从没被赋过值。README 的两处代码块都是这个写法。它是课程正文里用来说明结构的片段,不是能整段跑通的脚本;要跑,以 notebook 里那份为准。

执行层:@tool 装的是什么,dependencies 又由谁来遵守

notebook 在 Executing a Plan with Specialist Tools 一节下的代码单元里定义了三个工具函数,用 agent_framework@tool 装饰器注册,参数类型全部套 Annotated,把参数说明写在类型里:

@tool
def book_flight(
    destination: Annotated[str, "The destination city"],
    departure_date: Annotated[str, "Departure date (YYYY-MM-DD)"],
    return_date: Annotated[str, "Return date (YYYY-MM-DD)"],
) -> str:
    """Search and book flights for the trip."""
    return f"Flight booked to {destination}: ..."

另外两个是 reserve_hotel(参数 citycheck_incheck_outguests)和 book_activity(参数 activity_namedateparticipants)。三个函数体都只是拼一个假的确认号字符串返回,没有任何真实预订逻辑——这是教学桩,别当成可用的接单代码。

执行方是 concierge_agent,用同一个 client.as_agent(...) 建,多传一个 tools=[book_flight, reserve_hotel, book_activity]

关键的一处在于计划怎么传给它。notebook 的做法是把 plan.subtasks 拼成文本:

subtask_lines = "\n".join(
    f"- [{t.priority}] {t.task_id}. {t.description} (agent: {t.assigned_agent}, deps: {t.dependencies})"
    for t in plan.subtasks
)

然后连同目的地、天数、预算拼成 execution_prompt,交给 concierge_agent.run(...)concierge_agentinstructions 里写着 Work through the subtasks in order, respecting dependencies

把这两处放在一起就清楚了:dependencies 这个字段最终是以一段 deps: [...] 的文本进入提示词的,遵守它的是模型,不是代码里的调度器——这一课的 notebook 里没有拓扑排序,也没有任何按依赖分批执行的循环。如果你的场景里执行顺序错了会真的出事(下单、扣款、发消息),那这层顺序保证得由你自己在外面写,课程给的是结构,不是执行引擎。

(以上为按仓库代码中的接口语义组合的示例,未经实测,以仓库最新代码为准。)

重规划的触发点写在哪一节

README 的 Iterative Planning 一节给了两个触发来源,都是文字描述的场景:一是某个子任务的产出会影响下一个,正文举的例子是订机票时遇到预期之外的数据格式,需要在进入订酒店之前调整策略;二是人的反馈,比如用户临时想换更早的航班,会触发一次局部重规划。

代码上对应的只有一个动作——把旧计划塞进上下文再问一次:

response = client.create_response(
    input=user_message,
    instructions=system_prompt,
    context=f"Previous travel plan - {TravelPlan}",
)

这段在 README 里被标注为 e.g sample code,插值进去的是 TravelPlan 这个类本身而不是某次生成的计划实例,照抄不会得到你想要的上下文。真正要传的是上一轮的计划内容,这一步得你自己接。

至于「谁来判断该重规划了」,README 在 Additional Resources 里把这件事指向了 Magentic One,并自述其 orchestrator 除了制定计划还带一套跟踪机制来监控任务进度、按需重规划。第 7 课本身没有实现这个跟踪层。

跑之前要有的两样东西

notebook 开头 Setup 一节的依赖安装单元写的是 %pip install agent-framework azure-ai-projects azure-identity python-dotenv -q

配置只需要两个环境变量:AZURE_AI_PROJECT_ENDPOINTAZURE_AI_MODEL_DEPLOYMENT_NAME。紧接着的导入单元会先 dotenv.load_dotenv(),再检查这两个是否为空,缺了直接抛 ValueError 并把缺失的变量名列出来——所以配错了不会等到调用模型才报错。00-course-setup/README.md 说明了这两个值分别在 Foundry 门户的项目 Overview 页与 Models + Endpoints 页拿。

复制配置模板的命令,仓库分两种壳给:

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

Windows 上用 PowerShell 的读者直接用后一条,别把 cp 那条硬套。

凭据这块两处也不同:README 正文用 AzureCliCredential,notebook 用 DefaultAzureCredential00-course-setup/README.md 自述,用这套凭据是因为登录态由 Azure CLI 会话提供,.env 里不放 API key。两个类都来自 azure-identity,前提都是你先在本机登录过 Azure CLI;这一步没做,前面的环境变量校验会通过,失败会推迟到真正建客户端或发请求的时候。


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

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