微软 AI Agent 入门课第 16 课:自带 skill 写了哪些部署步骤

2026-08-18

你在 notebook 里把一个 agent 跑通了,工具能调、检索能查、多轮也记得住。然后有人问:什么时候能上线?这一步通常卡的不是模型,是你手里没有一份「上线要做哪些事」的清单。

ai-agents-for-beginners 这个课程仓里,第 16 课除了 README 和 notebook,还多带了一份东西:.agents/skills/deploying-scalable-agents/SKILL.md。它是写给 AI 助手读的技能文件,但把它当成一页部署检查表来读,比通读整篇课程正文更省事——它已经把课程内容压成了带优先级的条目。本文就沿着这份文件走一遍,并顺手对照 16-deploying-scalable-agents/code_samples/16-python-agent-framework.ipynb 里的实现,看看哪些地方对得上、哪些地方对不上。

前置条件:这份 skill 本身不干活

先说清楚它是什么。.agents/skills/deploying-scalable-agents/ 目录下只有 SKILL.md 一个文件,没有脚本、没有模板、没有可执行入口。它的 YAML frontmatter 只有三个字段:namedescriptionlicense(写的是 MIT)。description 里用 USE FOR:DO NOT USE FOR: 两段划边界,DO NOT USE FOR 明确排除了「构建你的第一个 agent(去第 1 课)」「在本地设备上跑 agent(去第 17 课 / local-ai-agents skill)」以及「非 Foundry 的部署目标」。所以它不是通用部署指南,它只覆盖 Microsoft Foundry 这一条路。

真正要跑代码,前置条件在课程 README 与仓库根的 AGENTS.md 里:Python 3.12+、Azure 订阅、一个已经部署了聊天模型的 Microsoft Foundry 项目、Azure CLI 完成 az login,以及仓库根的 requirements.txt。环境变量至少两个:AZURE_AI_PROJECT_ENDPOINTAZURE_AI_MODEL_DEPLOYMENT_NAME。RAG 那节还会读 AZURE_SEARCH_SERVICE_ENDPOINTAZURE_SEARCH_API_KEY,notebook 里的 USE_AZURE_SEARCH 是这两个变量的 bool 与运算,只要有一个没配齐,search_policies 就走 _in_memory_search 那条内存版分支。

Windows 这边有个细节别漏:AGENTS.md 的初始化步骤给的是 python3 -m venv venvsource venv/bin/activate,行末注释写明 Windows 上改用 venv\Scripts\activate。另外 requirements.txt 里那段注释值得读一遍,它把 agent-framework-core 钉在了 1.10.0,注释逐字写明 1.11.0 引入了课程 notebook 用到的破坏性变更,其中一条是「dropped the model= argument on Agent.run()」。这是仓库当前的钉版,随版本可能变动,但它解释了后面一处代码差异。

skill 列的步骤:五块,各自对应一个动作

第一块是心智模型表。 七行,每行一个关注点:Hosting、Identity、State、Failure、Cost、Quality、Trust,左边是原型形态,右边是生产形态。比如 Identity 一行写的是从「你自己的 az login」变成「managed identity + scoped RBAC」,State 一行是从进程内内存变成外置的 thread / memory store。skill 要求把每一条建议都映射回这七行里的某一行——这条要求本身就是清单的用法说明。

第二块是三种部署形态:client-hosted(推理循环跑在你自己的进程里,控制力最大,扩容与状态自己扛)、hosted agent(Foundry Agent Service 托管循环、存 thread、管 RBAC 与内容安全,你的应用退化成薄客户端)、agent workflow(多个 agent 与工具组成图,带分支、审批节点和可持久化的 checkpoint)。skill 写的是「pick one, or combine」,不排优劣。

第三块是生命周期链,写成一行:create → version → evaluate (gate) → deploy hosted → observe online → collect failures → repeat。六个动作之后回到起点。链条下面那句是 skill 里加粗的原话:offline evaluation is a gate, not an afterthought——过不了阈值线,这个版本就不出去。同一句在课程 README 里也出现了一次。

第四块是伸缩与成本杠杆,明确标了优先级顺序:先把模型选小(用能通过评估门的最小模型),再按复杂度路由,再做缓存,最后才是无状态设计加有界并发。顺序是这份文件里少见的强断言,值得照抄进你自己的检查表。

第五块是「Key patterns to reproduce」,直接把你指向 notebook 里的四段代码:请求处理器(cache → route → trace span → run → cache)、评估门(跑离线测试集,返回 pass_rate >= threshold,为真才部署)、人工审批(@tool(approval_mode="always_require"))、tracing(tracer.start_as_current_span(...) 里设 routed.modelcustomer.id 这类属性)。

伸缩相关的配置项,都在 notebook 的那两个 cell 里

skill 只给名字,具体的旋钮在 notebook。路由与缓存这一节的配置项是这样的:

SMALL_MODEL = os.getenv("AZURE_AI_SMALL_MODEL", model)   # e.g. gpt-5-nano
LARGE_MODEL = os.getenv("AZURE_AI_LARGE_MODEL", model)   # e.g. gpt-5-mini

response_cache: dict[str, str] = {}
route_counters = {"small": 0, "large": 0, "cache": 0}

两个模型档位各自读一个环境变量,读不到就回落到 AZURE_AI_MODEL_DEPLOYMENT_NAME 那个 model——也就是说你不设这两个变量时,路由逻辑照跑,但两档指向同一个部署,等于空转。注释里的模型名只是仓库写的示例值。

分流判据是 is_simple(),它先看查询里有没有命中 COMPLEX_SIGNALS 这个元组里的信号词(refundcancelcomplaintescalatebrokenwrongwhy),命中就直接判为不简单;否则按分词长度阈值判断。normalize() 只做小写加空白折叠,缓存键就是它的返回值——这意味着标点不同的同一个问题不会命中同一条缓存,想提高命中率得自己改这个函数。课后作业第 3 条让你按 small / large / cache 三档记录每次请求的估算成本并打一份报告,route_counters 这三个计数器正好是现成的起点。

人工审批这一档的配置项是工具装饰器上的 approval_mode。notebook 里 get_order_statusopen_ticketsearch_policies 用的是 approval_mode="never_require",只有 issue_refundapproval_mode="always_require"。金额阈值单独放在 REFUND_APPROVAL_THRESHOLD = 50.0refund_needs_approval() 里——注意这两者在这份 notebook 里并没有接进 issue_refund 的装饰器判断,装饰器是无条件 always_require

评估门的旋钮是 evaluation_gate(test_cases, threshold=0.8) 的默认参数,这是仓库当前代码里的默认值,随版本可能变动。

部署产物:一个 hosted agent 名字,加一条 smoke-test 流水线

skill 的「Smoke-testing a deployed agent」这节指向的是仓库里真实存在的三样东西:

  • .github/workflows/smoke-test.yml,触发方式是 workflow_dispatch,三个输入分别是 tests_file(下拉选课程目录)、project_endpointagent_name(默认值 ContosoSupportAgent);permissionsid-token: write 是给 azure/login 的 OIDC 用的。
  • tests/lesson-16-smoke-tests.json,顶层是一个 tests 数组。tests/README.md 的字段速查表列了可用的断言:assertions.statuscontains_anycontains_allcontains_none,多轮靠 save_response_id_asuse_previous_response_id 串起来;第 16 课这份 catalog 实际只用到了前两类加 contains_none。同一张表写明这些断言是大小写不敏感的子串检查。
  • tests/README.md 里那张 catalog → 课程 → agent 名字的对应表。这张表就是「部署产物」的契约:第 16 课这一行要求你把 agent 部署成 ContosoSupportAgent,名字对不上,catalog 就找不到人。

有一条信息只有 skill 写全了:课程 README 只说 runner 会把每个 prompt POST 到 agent 的 Responses 端点,而 skill 把完整路径写了出来——POST {project_endpoint}/agents/{agent_name}/endpoint/protocols/openai/responses,并且 token 的 audience 必须是 https://ai.azure.com/。身份需要 Foundry 项目范围上的 Azure AI User 角色,这一条 README、workflow 注释和 skill 三处都写了。

- name: Smoke-test hosted agent
  uses: JFolberth/ai-smoketest@v1
  with:
    project_endpoint: ${{ inputs.project_endpoint }}
    agent_name: ContosoSupportAgent
    tests_file: tests/lesson-16-smoke-tests.json

这一段是课程 README 里的原文片段,注意它把 agent_name 写死成了 ContosoSupportAgent,而 workflow 文件本身走的是 ${{ inputs.agent_name }},由你在触发时填。以仓库最新内容为准。这里用的是 GitHub Marketplace 上的第三方 Action,不是仓库自带的脚本,接入前按你自己的供应链要求评估。

边界:清单到这儿就断了

notebook 不会真的部署。 最后那个 release() 函数干的事是跑评估门,通过就 print 一句「promoting agent version to the Foundry Agent Service」。notebook 里这一节的标题直接写着「Putting It Together: A Simulated Release」,正文也说这是你会在 CI 里跑的那套流程的示意。第 16 课的 code_samples/ 目录下只有这一个 notebook,我们没有在课程目录里找到实际执行部署的脚本、IaC 模板或 CLI 命令。有意思的是 tests/README.md 的第一步写着「Deploy the lesson’s agent to Microsoft Foundry as a hosted agent(see Lesson 16 for the deployment workflow)」——但第 16 课正文给的是几张 mermaid 架构图加一个模拟发布,具体怎么把 agent 注册成 hosted agent,得去它「Additional Resources」里链的 Foundry Agent Service 文档。这一步的落差要提前知道。

README 的示意片段和 notebook 的实现对不上,有两处。 一是 README 正文里的处理器写的是 await support_agent.run(query, model=model);notebook 里没有这个 model= 参数,而是用 agent_for(model_name) 给每个档位各建一个 FoundryChatClientas_agent(...),代码注释解释「当前 agent-framework 在 client 上选模型」。这与 requirements.txt 里那条钉版注释是同一件事的两个侧面。二是 README 的评估门片段写 threshold: float = 0.8 且逐条判 >= 0.8,notebook 里逐条判的是 s >= 0.5,只有整体通过率仍对 threshold 比较。照 README 抄和照 notebook 抄,得到的门是两回事。

skill 自己的 guardrail 也有个小落差:它写「Prefer the canonical FoundryChatClient(...) + provider.as_agent(...) pattern」,而 notebook 里那个叫 provider 的客户端建完之后,真正 as_agent(...) 的是 agent_for() 内部新建的 client。方法名一致,对象不是同一个。

smoke test 不是到处能用。 tests/README.md 写明第 17 课完全跑在本机、不暴露 Foundry Responses 端点,这个 Action 不适用;纯设计模式与理论课也没有可部署的 agent。

MCP 那条要照实读。 skill 的企业控制一节把 MCP server 定义为不可信边界:钉版本、给受限身份、校验输出、限流、不给它任何密钥。这是仓库文档的立场,不是「加了就安全」的保证。

怎么验证你配对了

按 skill 给的层级顺序,从便宜的往贵的走。

第一层,smoke test:把 agent 部署成 ContosoSupportAgent,在仓库 Settings 里配好 AZURE_CLIENT_IDAZURE_TENANT_IDAZURE_SUBSCRIPTION_ID 三个 secret,联合身份拿到 Foundry 项目范围的 Azure AI User 角色,然后从 Actions 页手动触发「Smoke-test hosted agents」,tests_file 选第 16 课那份。catalog 里那条 stays-on-topic 用的是 contains_none,专门验偏题;两条 thread-turn-* 验多轮串联是否真的生效。跑绿只能说明端点可达、回复符合几条子串断言,说明不了质量。

第二层,离线评估门:本地跑 notebook 的 release(TEST_CASES),确认门在该拦的时候真拦得住——把 TEST_CASES 里某条的 expected 改成明显答不上来的内容,看通过率是否跌破 threshold。课后作业第 2 条要的就是这件事:把 TEST_CASES 扩到覆盖退款审批场景,确认这道门真能拦住回归。

第三层,线上观测:确认 span 属性真的写进去了。handle_support_request 里设的是 customer.idrouted.model,没有这两个属性,你的 trace 就只是一堵墙。注意 notebook 里 tracing 那段包了 try/exceptagent_framework.observability 导入失败时会退化成一个 _NoopTracerset_attribute 是空实现——本地看不到 trace 时先确认自己不是在跑这个 no-op 分支。

最后提醒一句配置层面的自检:如果 AZURE_AI_SMALL_MODELAZURE_AI_LARGE_MODEL 都没设,route_counters 里 small 与 large 的计数依然会分开涨,但两边打的是同一个部署。计数器好看不等于成本下来了。


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

安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。

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