AzureSearch.md 的 Python 与 .NET 两份代码:微软 AI Agent 入门课
如果你在跟 ai-agents-for-beginners 这门课,大概率会在某一刻卡在同一个地方:notebook 跑起来了,但它用的是内存里的知识库,你想把它换成一个真的索引;或者你在 .NET 那边复刻同一件事,发现字段定义的写法跟 Python 那份对不上,不确定两边建出来的索引是不是同一个东西。
课程把这块单独抽成了 00-course-setup/AzureSearch.md,同一件事给了 Python 和 .NET 两份代码,另外还有一个独立的 00-course-setup/AzureSearch.cs 文件。这篇就把这两份对着看一遍。
前置条件:别跳过这一段
AzureSearch.md 的 Prerequisites 只写了一条:一个 Azure 订阅。但往下读会发现真正要准备的不止这些。
Step 1 是建存储账户,文档里带了一句加粗的 NOTE:存储账户类型要选 Standard General Purpose V2。这一步在正文里排在建搜索服务之前。
权限。 Step 3 写明这份指南里的示例会创建/更新索引并上传文档,因此需要 Search Service Contributor 和 Search Index Data Contributor 两个角色;如果走基于密钥的方式,要用的是 primary admin key,文档特意补了一句「不是 query key」。00-course-setup/README.md 里还解释了为什么推荐 keyless(仓库文档自述):admin key 对搜索服务是完整写权限,而且容易随 .env 文件泄漏。
Python 侧的依赖只有一行:
pip install azure-search-documents azure-identity
仓库根目录的 requirements.txt 里也列了 azure-search-documents,所以如果你已经按课程主流程装过依赖,这个包通常已经在了,azure-identity 同样在列表里。
.NET 侧要注意几点。一是 AzureSearch.cs 用的是文件级指令,开头三行分别引了 Azure.Search.Documents、Azure.Identity 两个包并设了 PublishAot=false,文档给的运行命令直接指向这个 .cs 文件本身——dotnet run ./AzureSearch.cs,仓库的 00-course-setup/ 目录下并没有为这份示例配套的 .csproj 工程文件;这三行里的包版本是仓库当前代码里的写法,随版本变动。二是仓库根目录有 global.json,里面 pin 了 SDK 版本并设了 "rollForward": "latestFeature",具体版本值以仓库当前内容为准。三是 .agents/skills/testing-course-samples/SKILL.md 里有一条容易忽略的说明:*-dotnet-* 那批 notebook 需要 .NET Interactive kernel,默认是被排除在批量测试之外的,要用 -IncludeDotnet 才带上。
Azure CLI 和登录态。 文档要求先装 Azure CLI 并执行 az login。这一步不只是为了跑那几条 az 命令:AzureSearch.md 里明确写了,Python 与 .NET 两份示例都用 DefaultAzureCredential,本地开发时它会复用你 az login 的会话,因此不需要 admin key。换句话说,登录态本身就是这两份示例的凭据来源,忘了登录跟少配一个环境变量是同一类问题。
连接配置:endpoint 是拼出来的
这一步有个仓库自己踩过的坑,注释就写在 AzureSearch.md 的代码块里:az search service show 没有 endpoint 字段,URL 要用服务名自己拼。所以文档给的是这样一行:
export AZURE_SEARCH_SERVICE_ENDPOINT="https://<service-name>.search.windows.net"
启用 RBAC 的三条命令也在同一节里:
az search service update --name <service-name> --resource-group <resource-group> --auth-options aadOrApiKey
az role assignment create --assignee <your-user-or-principal-id> --role "Search Service Contributor" --scope $(az search service show -g <resource-group> -n <service-name> --query id -o tsv)
az role assignment create --assignee <your-user-or-principal-id> --role "Search Index Data Contributor" --scope $(az search service show -g <resource-group> -n <service-name> --query id -o tsv)
Windows 用户看这里。 上面这段是文档里标了 # zsh/bash 的那份。同一个文件里给了 PowerShell 的对应写法,别照抄 export:
# PowerShell
# az search service show has no "endpoint" field; build the URL from the service name.
$env:AZURE_SEARCH_SERVICE_ENDPOINT = "https://<service-name>.search.windows.net"
$env:AZURE_SEARCH_API_KEY = $(az search admin-key show -g <resource-group> --service-name <service-name> --query "primaryKey" -o tsv)
注意 $env: 这种设法只在当前 PowerShell 会话里有效,关掉窗口就没了。课程主流程是通过 .env 文件读变量的,.env.example 里给出的两个键就是 AZURE_SEARCH_SERVICE_ENDPOINT 和 AZURE_SEARCH_API_KEY,所以更稳妥的做法是把值写进 .env,命令行那套只用来临时验证。
至于这两个值分别从哪儿抄,00-course-setup/README.md 有一张对照表:endpoint 去 Azure portal 里搜索服务资源的 Overview 页看 URL;API key 走 Settings → Keys,取 primary admin key。AzureSearch.md 的 Step 3 也说了同一件事——部署完成后进搜索服务的 overview 面板,复制形如 https://<service-name>.search.windows.net 的 URL。两处说法一致,portal 和命令行拼出来的是同一个地址。
两份代码逐项对上
先说结论:两份代码建的索引名都是 sample-index,都是两个字段 id 和 content,id 做 key,content 可搜索。但写法不是一一镜像的。
| 这一步 | Python | .NET |
|---|---|---|
| 凭据 | DefaultAzureCredential() | new DefaultAzureCredential() |
| 索引客户端 | SearchIndexClient(service_endpoint, credential) | new SearchIndexClient(serviceEndpoint, credential) |
| endpoint 类型 | os.getenv(...) 拿到的字符串 | new Uri(Environment.GetEnvironmentVariable(...)!) |
| key 字段 | SimpleField(name="id", type=edm.String, key=True) | new SimpleField("id", SearchFieldDataType.String) { IsKey = true } |
| 可搜索字段 | SimpleField(name="content", type=edm.String, searchable=True) | new SearchableField("content") |
| 建索引 | index_client.create_index(index) | await indexClient.CreateOrUpdateIndexAsync(index) |
| 上传 | search_client.upload_documents(documents) | await searchClient.UploadDocumentsAsync(documents) |
有两处差异值得单独拎出来。
第一处,可搜索字段的表达方式不同。 Python 那份是给 SimpleField 加 searchable=True,.NET 那份直接换了一个类 SearchableField。两边描述的是同一个索引意图,但你在两种语言之间迁移代码时,不能指望把参数名直译过去。
第二处,建索引的方法语义不同。 Python 调的是 create_index,.NET 调的是 CreateOrUpdateIndexAsync——从方法名上看,后者带 create-or-update 语义,前者没有。这两处白纸黑字就在同一个文档的两段代码里,你重复执行时的行为差别会从这里来。仓库里没有对这个差异给出说明,我这里只是把两处放在一起,具体行为以两个 SDK 的官方文档为准。
顺带一提,同一个仓库里 Python 的字段定义还有第二种写法。11-agentic-protocols/code_samples/github-mcp/app.py 用的是 SearchableField(name="content", type=SearchFieldDataType.String),而不是 setup 指南里的 SimpleField(..., searchable=True);类型枚举也是 SearchFieldDataType.String 而非 edm.String。两处都在这个仓库里,写法不统一。我们没有跑过其中任何一份,不替你判断哪种写法更该照抄,但你至少要知道同一个仓库里存在这两种写法,别以为自己看错了。
密钥回退的写法两边都是注释掉的。Python 是 AzureKeyCredential(os.getenv("AZURE_SEARCH_API_KEY")),需要额外 from azure.core.credentials import AzureKeyCredential;.NET 的注释里特意说明 using Azure; 这行已经把 AzureKeyCredential 引进来了,直接替换那一行 credential 即可。
以上代码片段均原样取自仓库文件,为按仓库代码中的接口语义组合的示例,未经实测,以仓库最新代码为准。
边界:哪些地方文档说得很死
第 16 课不吃 RBAC。 00-course-setup/README.md 写得很直白:第 16 课的 notebook 目前用的是基于密钥的认证,只有当 AZURE_SEARCH_SERVICE_ENDPOINT 和 AZURE_SEARCH_API_KEY 两个都设置时才从内存检索切到 Azure AI Search,否则一直走内存;并且明说上面那套 RBAC 步骤适用于 setup 指南里的示例和你自己的代码,不会让第 16 课变成 keyless。所以你只配了 endpoint 却发现它没走真索引,不是配错了,是这一课就这么写的。
索引名在仓库里有三个不同的值。 setup 指南是 sample-index;github-mcp/app.py 硬编码成 event-descriptions;第 16 课的 notebook 读 AZURE_SEARCH_INDEX_NAME,取不到时回落到 contoso-policies——这是仓库当前代码里的默认值,随版本可能变动。你按 setup 指南建好索引再去跑第 16 课,名字对不上是正常的。
第 5 课和第 16 课本来就不需要搜索服务。 README 的 Optional Setup 一节写明这两课开箱走内存知识库。.agents/skills/testing-course-samples/SKILL.md 的资源表里也把第 5 课标注为「有 in-memory fallback path」。
这份 setup 指南里没有向量检索的示例。 字段定义只涉及 SimpleField / SearchableField 和字符串类型,我们在 AzureSearch.md 与 AzureSearch.cs 里没有找到向量字段、语义排序或 embedding 相关的配置说明。要做这类事情得去看 Azure AI Search 自己的文档,课程仓里这一篇不覆盖。
怎么确认配对了
.NET 那份自带验证输出。 代码末尾两行 Console.WriteLine 会分别打出索引名就绪和上传的文档数——注意这里的数量是从 result.Value.Results.Count 取的,是这次上传的结果条数,不是索引里的总量。
Python 那份没有任何打印语句。 create_index 和 upload_documents 的返回值在示例里都没有接,代码里也没有 print。所以这份示例本身不提供任何自我验证的手段,要确认得自己去 portal 看索引,或者参考 github-mcp/app.py 的做法:它用 index_client.get_index(index_name) 包在 try/except 里,取到就打印「已存在,复用」,抛异常就走 create_index。这个模式可以直接借来当检查手段。
跑第 16 课时看那一行状态输出。 notebook 里有 USE_AZURE_SEARCH = bool(os.getenv("AZURE_SEARCH_SERVICE_ENDPOINT") and os.getenv("AZURE_SEARCH_API_KEY")),最后会打印一句告诉你这次用的是 Azure AI Search 还是 in-memory search。这是判断环境变量到底有没有被读到的最直接办法——比去猜 .env 有没有加载可靠得多。
最后提醒一句:这些环境变量对应的是会计费的云端服务,示例代码会把文档内容上传到你的搜索服务,密钥写进 .env 之前先确认它没被纳入版本控制。安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。
本文依据 github.com/microsoft/ai-agents-for-beginners 仓库于 2026-08-18 的公开内容整理,
事实来自仓库内的课程正文与代码示例。我们没有跑过文中涉及的代码,
因此不涉及运行结果、耗时与 Agent 实际表现的任何描述。
该课程持续更新,文中涉及的文件路径、依赖与接口写法随版本变动,请以仓库最新内容为准。
文中涉及的云端服务调用会产生费用并可能上传数据,请自行评估密钥与数据边界。