Python 版和 .NET 版差在哪三处:微软 AI Agent 入门课同课对照

2026-08-18

团队里 Python 和 .NET 两拨人一起看 ai-agents-for-beginners,最容易出的事是:各自照着自己那份代码配好了环境,一开会发现连环境变量名都对不上,谁也说服不了谁「你那边配错了」。

其实两份代码没有谁配错。这个课程仓每一课基本都放了两份实现,例如 01-intro-to-ai-agents/code_samples/ 下同时有 01-python-agent-framework.ipynb01-dotnet-agent-framework.cs。把这两套从头到尾比一遍,会发现差异并不均匀铺开,而是集中在三个地方:客户端怎么构造、工具怎么注册、异步怎么写。下面逐处对照,只对照两边都白纸黑字写明的部分。

第一处:客户端构造,两边连的不是同一类端点

先看 Python 主线。01-python-agent-framework.ipynb 里的构造是这样的:

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

endpoint = os.getenv("AZURE_AI_PROJECT_ENDPOINT")
model = os.getenv("AZURE_AI_MODEL_DEPLOYMENT_NAME")

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

依赖由 notebook 首个代码单元装:%pip install agent-framework azure-ai-projects azure-identity -q

同一课的 .NET 版走的是另一条线:

var azureEndpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
    ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deployment = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT") ?? "gpt-5-mini";

var azureClient = new AzureOpenAIClient(new Uri(azureEndpoint), new AzureCliCredential());

AIAgent agent = azureClient
    .GetChatClient(deployment)
    .AsAIAgent(
        instructions: "You are a helpful AI Agent that can help plan vacations for customers at random destinations",
        tools: [AIFunctionFactory.Create(GetRandomDestination)]
    );

这里 ?? "gpt-5-mini" 是仓库当前代码里的兜底值,随版本可能变动,别把它当成你环境里必然存在的部署名。

把两段放一起,最扎眼的不是语法,是环境变量名:Python 侧读的是 AZURE_AI_PROJECT_ENDPOINTAZURE_AI_MODEL_DEPLOYMENT_NAME,.NET 侧读的是 AZURE_OPENAI_ENDPOINTAZURE_OPENAI_DEPLOYMENT。这不是命名风格问题——前者是 Microsoft Foundry 项目的 endpoint,后者是 Azure OpenAI 资源的 endpoint,.cs 文件里的注释逐字写明它走的是 Azure OpenAI 的 Responses API(stable v1 endpoint)。所以一份 .env 覆盖不了两边要读的变量名,这是最先咬到人的那个差异。

值得单独指出的是,Python 侧并非只有 Foundry 这一条路。02-explore-agentic-frameworks/code_samples/ 下除了 02-python-agent-framework.ipynb,还有一份 02-python-agent-framework-azure-openai.ipynb,它用的是:

from agent_framework.openai import OpenAIChatClient

chat_client = OpenAIChatClient(
    model=deployment,
    azure_endpoint=endpoint,
    credential=AzureCliCredential(),
)

读的环境变量正是 AZURE_OPENAI_ENDPOINTAZURE_OPENAI_DEPLOYMENT,依赖也换成了 agent-framework agent-framework-openai azure-identity。也就是说,如果你手上只有一个 Azure OpenAI 资源、没有 Foundry 项目,Python 侧要找的是这一份,而不是各课的主线 notebook。

凭据类的写法两边也不完全整齐:01-python-agent-framework.ipynbAzureCliCredential(),而 04-tool-use/code_samples/04-python-agent-framework.ipynb07-planning-design/code_samples/07-python-agent-framework.ipynb 用的是 DefaultAzureCredential();这几课的 .cs 里则都是 AzureCliCredential。这是两个不同的凭据类,换课照抄时容易忽略,具体差别以 azure-identity 官方文档为准。

前置条件与 Windows 侧

.NET 侧的前置条件写在 04-tool-use/code_samples/04-dotnet-agent-framework.md 里:.NET 10 SDK 或更高、一个带模型部署的 Azure OpenAI 资源、Azure CLI 并 az login。同一份文档给了两种环境变量设法,PowerShell 那份是:

$env:AZURE_OPENAI_ENDPOINT = "https://<your-resource>.openai.azure.com"
$env:AZURE_OPENAI_DEPLOYMENT = "gpt-5-mini"
az login

运行方式文档也给了两条:一条是 chmod +x ./04-dotnet-agent-framework.cs 之后直接执行该文件——这条依赖 .cs 首行的 #!/usr/bin/dotnet run,在 Windows 上不适用;另一条是 dotnet run ./04-dotnet-agent-framework.cs。Windows 上用第二条。这些 .cs 顶部的 #:package 行就是单文件应用声明依赖的方式,不需要单独建工程文件。

第二处:工具注册,一个靠装饰器,一个靠特性加工厂

Python 侧用 agent_framework 里的 tool 装饰器。04-python-agent-framework.ipynb 的写法:

from typing import Annotated
from agent_framework import tool

@tool(approval_mode="never_require")
def check_availability(
    destination: Annotated[str, "The destination to check"],
) -> str:
    """Check booking availability for a destination."""
    ...

这一课的正文明确写了两条约定:函数的 docstring 会成为模型看到的工具描述,类型注解(含 Annotated 里的描述串)用来生成工具 schema。注册时直接把函数对象放进列表:tools=travel_tools,其中 travel_tools = [get_destinations, check_availability, get_flight_info]

.NET 侧没有装饰器这一层,改成特性标注加显式包装:

[Description("Gets the current weather conditions for a specified location. Use this when planning activities or packing suggestions.")]
static string GetWeather(
    [Description("The city and country to get weather for, e.g., 'Paris, France'")] string location)

方法上的 [Description] 对应 Python 的 docstring,参数上的 [Description] 对应 Annotated 里那串说明。注册时必须过一道 AIFunctionFactory.Create

tools: [
    AIFunctionFactory.Create(GetRandomDestination),
    AIFunctionFactory.Create(GetWeather),
    AIFunctionFactory.Create(GetDestinationInfo),
    AIFunctionFactory.Create(EstimateTripCost)
]

到这里为止都还只是语言习惯的差别,翻译过去不难。真正需要留意的是下面这一点。

Python 的 @toolapproval_mode 参数,04-tool-use 正文列了两个取值:"never_require" 是自动执行,"always_require" 是每次调用都要用户批准,并建议给有副作用的操作(例如订机票、扣款)用后者。这一课还演示了工具对象上可以直接读到 book_flight.namebook_flight.approval_mode

这个机制在 .NET 示例里我们没有找到对应物。01、02、04、07 这几课的 .cs 文件里查不到与工具审批对应的写法(仓库里唯一出现 approval 字样的 .cs08-multi-agent/ 下,那是一段讲内容质量审核的注释,不是工具审批开关);另外,讲人工介入的 06-building-trustworthy-agents/code_samples/ 下只有 06-human-in-the-loop.ipynb06-system-message-framework.ipynb 两份 notebook,没有 .NET 版本。这不代表 .NET 侧的框架没有这个能力——我们只能说课程仓里没给示例,所以这一点不比。但如果你的团队正是冲着「工具调用前必须人工点头」来抄这套代码的,这个空缺会直接咬到你。

第三处:异步模型,一个写在方法名里,一个写在参数里

.NET 侧把异步语义放进方法名。流式是 RunStreamingAsync,非流式是 RunAsync07-planning-design/code_samples/07-dotnet-agent-framework.cs 里就是 await agent.RunAsync(...)),会话创建是 CreateSessionAsync

AgentSession session = await agent.CreateSessionAsync();

await foreach (var update in agent.RunStreamingAsync("What's the weather like in Tokyo?", session))
{
    await Task.Delay(10);
    Console.Write(update);
}

Python 侧不分方法名,靠参数切换。01-python-agent-framework.ipynb 里非流式是 response = await agent.run(...),流式是同一个 run 加一个开关:

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

会话也是一样的思路——02-python-agent-framework-azure-openai.ipynb 里是 session = agent.create_session()没有 await,随后把它作为具名参数传进去:agent.run(user_input, session=session, stream=True)。而 .NET 侧同一件事必须 await,且 session 是位置参数跟在提示词后面。

顺带一个仓库内部的小不一致,就出在同一课里:04-dotnet-agent-framework.md 文档正文贴的代码是 await using var session = await agent.CreateSessionAsync();,带释放语义;而同一课实际执行的那份 04-dotnet-agent-framework.cs 里写的是 AgentSession session = await agent.CreateSessionAsync();,不带(02-dotnet-agent-framework.cs 也是后一种写法)。抄哪一份取决于你打开的是 .md 还是 .cs,翻仓库时留个心。

还有一处形态差异容易被忽略:Python 侧的示例是 notebook,代码单元里直接写了顶层 await(例如 response = await agent.run(...));02-python-agent-framework-azure-openai.ipynb 是这些示例里最接近脚本形态的一份,它把两轮对话包进 async def main(),最后仍然是一句顶层的 await main()。仓库里没有给出把这些示例改写成独立 .py 脚本的入口示例,这部分要你自己补。.NET 侧则是另一套形态:单文件顶层语句,按文档是 dotnet run 直接执行单个 .cs,也不是长驻服务的形态。两边都需要你自己做那一步改造,只是缺口不同。

三处差异的连带效应:结构化输出挂在哪一层

上面三处不是各自独立的,它们会串起来影响第四件事——结构化输出配在哪一层。

Python 侧把它放在运行时。07-python-agent-framework.ipynb 里,agent 创建时只给 nameinstructions,Pydantic 模型是在调用时通过 options 传的:

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}
)

拿结果也多一层,是 result.value,取到的就是那个 Pydantic 对象,可以直接 plan.destination 这样访问。

.NET 侧则要把它前置到构造阶段。07-dotnet-agent-framework.cs 先组一个 ChatClientAgentOptions,把 schema 塞进 ChatOptions.ResponseFormat,再把这个 options 交给 AsAIAgent

ChatClientAgentOptions agentOptions = new()
{
    Name = AGENT_NAME,
    Description = AGENT_INSTRUCTIONS,
    ChatOptions = new()
    {
        ResponseFormat = ChatResponseFormatJson.ForJsonSchema(
            schema: AIJsonUtilities.CreateJsonSchema(typeof(TravelPlan)),
            schemaName: "TravelPlan",
            schemaDescription: "Travel Plan with main_task and subtasks")
    }
};

差别的实际后果是:Python 侧同一个 agent 可以这一次要结构化、下一次要自由文本;.NET 侧这个示例里格式是绑在 agent 上的,要换格式就得再造一个 agent。另外这个 .cs 文件顶部多了一行 #:property JsonSerializerIsReflectionEnabledByDefault=true,01、02、04 这几课的 .cs 里都没有这一行,抄的时候别落下。数据类那边也要手写 [JsonPropertyName("main_task")] 之类的映射,Python 侧靠 Pydantic 字段名直接对上。

顺带记一个仓库自身的不一致:04-python-agent-framework.ipynb 的正文一节讲「把 Pydantic 模型设给 response_format」,但紧跟着那个代码单元里的 structured_agent = client.as_agent(...) 只传了 nameinstructionstools,并没有出现 response_format。真要用这个能力,参照 07 那一课的写法更稳妥。

从你的处境倒推该看哪一份

  • 手上只有 Azure OpenAI 资源,没有 Foundry 项目:Python 走 02-python-agent-framework-azure-openai.ipynb 那条 OpenAIChatClient 路线,.NET 侧本来就是这条线,两边环境变量能统一。反过来,只配好各课主线 Python notebook 要的 AZURE_AI_PROJECT_ENDPOINT,并不能满足 .NET 那份——.cs 里对 AZURE_OPENAI_ENDPOINT 写的是 ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."),读不到就直接抛异常。
  • 需要工具调用前人工确认:Python 侧有 approval_mode="always_require" 的现成示例,.NET 侧在课程仓里没有对应示例,你得自己去框架文档里找,或者接受这块由自己实现。
  • 要把示例接进现有服务:两边都要改形态。Python 是 notebook,.NET 是单文件脚本,各自都不带服务壳。

有几个维度是没法在这个仓库里对照的:两侧的运行表现、包体、错误处理完备度,我们既没有跑过也没有依据,不比。另外提醒一句代际问题——05-agentic-rag/code_samples/05-dotnet-agent-framework.cs 顶部的 #:package 行里,Azure.AI.Agents.Persistent 钉的是 beta 包、Microsoft.Agents.AIMicrosoft.Agents.AI.AzureAI 钉的是 preview 包,会话对象也还是 AgentThread thread = agent.GetNewThread();,和 01–04 的 AgentSession / CreateSessionAsync() 不是同一代写法。beta 与 preview 是仓库里逐字标注的,接口随时可能变,别把它当稳定 API 抄进生产代码。

以上代码片段均原样取自仓库文件,未做改写。以上为按仓库代码中的接口语义组合的示例,未经实测,以仓库最新代码为准。


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

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

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