跑微软生成式 AI 入门课示例被限流:仓库自带的重试封装做了什么
跑 generative-ai-for-beginners 的 Python 示例,连着几次请求之后开始报错,很多人的第一反应是「课程里应该自带重试吧」——毕竟 shared/python/api_utils.py 里明明白白有个叫 make_safe_request 的函数,docstring 第一句就写着提供 “proper timeout, error handling, and retry logic”。
问题是,这个函数和你正在跑的那个示例,很可能一次都没打过照面。
一、先确认报错是从哪条路径出来的
判定动作很简单,在仓库根目录 grep 一下这个模块被谁引用:
grep -rn "api_utils\|make_safe_request" . --exclude-dir=translations --exclude-dir=.git
(translations/ 下是官方各语种译本,里面的命中只是同一份文档的翻译副本,排掉它们结果才看得清。)
在 2026-08-18 这个时间点的仓库里,命中的位置只有五处:shared/python/api_utils.py 自身、shared/python/__init__.py(它从 api_utils 里再导出 make_safe_request、create_openai_client、create_azure_openai_client 进 __all__,同文件里的 download_image 不在导出列表内)、tests/test_api_utils.py,以及 CHANGELOG.md 与 docs/ENHANCED_FEATURES_ROADMAP.md 两处文字提及。课程各章的 .py 与 .ipynb 示例里没有它。
反过来看你手上的示例。06-text-generation-apps/python/aoai-app.py 开头是 from openai import OpenAI,直接构造 client 然后 client.responses.create(...);同目录的 githubmodels-app.py 用的是 azure.ai.inference 的 ChatCompletionsClient 加 AzureKeyCredential。两条路径都不经过 requests,自然也不经过 make_safe_request。
所以第一步结论就出来了:你被限流时抛出来的那个异常,是 SDK 抛的,不是 api_utils.py 里那段循环抛的。 至于这两个 SDK 各自的重试参数怎么设,课程仓库里没有找到相关说明,得去各自 SDK 的文档看,不要指望课程正文告诉你。
第二个判定动作是看 traceback 最内层那一帧属于哪个包。如果栈底出现的是 openai 或 azure.ai.inference 里的模块,那就和 shared/python/ 这套工具完全无关;如果栈底是 requests,才轮到下面这一节要讲的内容。这一步花不了十秒,却能省掉一整轮改错地方的功夫——照着 api_utils.py 调 retries 而示例根本不走它,改多少次都不会有变化。
课程仓里唯一一处正面提到限流的地方,是 00-course-setup/02-setup-local.md 第 5 节的排查表,其中一行把症状写成 OpenAI 401 / 429 errors,给出的处置是 Check OPENAI_API_KEY value / request rate limits.。把 401 和 429 并在一行,是提醒你先分清楚是凭证问题还是速率问题——这两者的处置完全相反,重试对 401 没有任何帮助。
二、再读一遍这个封装到底做了什么
即便课程示例没用它,make_safe_request 仍然值得逐行读一遍,因为很多人会照着它写自己的调用层。它的签名是:
def make_safe_request(
url: str, method: str = "GET", timeout: int = 30, retries: int = 3, **kwargs: Any
) -> requests.Response:
timeout 与 retries 的默认值是仓库当前代码里的默认值,随版本可能变动,别把它当成契约。核心循环是这样:
for attempt in range(retries):
try:
response = requests.request(method=method, url=url, timeout=timeout, **kwargs)
response.raise_for_status()
return response
except RequestException as e:
last_exception = e
if attempt < retries - 1:
# Exponential backoff could be added here
continue
raise
有几处必须看清楚:
第一,retries 是总调用次数,不是「失败之后再额外试几次」。 循环写的是 range(retries),tests/test_api_utils.py 的 test_retries_then_raises 用一个永远抛异常的假 requests.request 把这层语义钉死了:传 retries=3,断言底层被调用 3 次(这个 3 是测试里的参数值,不是什么固定上限)。
第二,raise_for_status() 抛出的错误也走重试分支。 它抛的是 HTTPError,属于 requests.exceptions.RequestException 的子类,而 except 捕的正是 RequestException。这意味着 429 会被重试——同时 401、403、404 这类重试多少次都不会变的错误,也一样会被完整地重试满次数。这个函数不看状态码,对它来说所有请求异常都是一个类别。
第三,退避没有实现。 那行注释逐字写的是 Exponential backoff could be added here——“could be added”,也就是当前没有。注释下面紧接着就是 continue,中间没有任何 sleep。对 429 而言,这等于把请求原速再打一遍。响应头里的 Retry-After 也没有被读取,整个文件里没有任何读取响应头的代码。
第四,中间几次的错误信息确实丢了。 last_exception = e 每轮覆盖一次,但最后一轮走的是裸 raise(重抛当前异常),函数末尾那句 raise last_exception or RequestException("Request failed") 上面写着注释 This should never be reached, but just in case。所以前面几次失败既没有日志、也没有被聚合进最终异常,你在栈里只会看到最后一次的样子——被限流时看起来就像「偶发失败一次」。
另外两个细节容易在照抄时踩到:method 的默认值是 "GET",你要发 POST 得显式传进去,否则重试三次发的都是 GET;其余参数走 **kwargs 原样透传给 requests.request,所以 headers、json、params 这些照常可用,但也意味着这层封装不会替你检查它们。同文件里的 download_image 就是这么调的——只传了 timeout,retries 走默认值,然后 os.makedirs(os.path.dirname(save_path) or ".", exist_ok=True) 建目录再写文件。它是这个函数在仓库里唯一的真实调用点,做的是把图片下载下来存盘,和模型请求无关。
三、要补的东西补在哪一层
先说位置:补在调用点,不是把模型请求塞进 make_safe_request。 那个函数是给 requests 发的普通 HTTP 请求用的(download_image 就是这个用途),模型调用走的是 SDK 客户端,套不进去。
真要在自己的封装里做,按仓库代码里已有的语义,缺的是三件事:按状态码分流(401 之类不重试)、失败之间加退避、读取 Retry-After。写成代码大致是:
import time
from requests.exceptions import HTTPError, RequestException
def request_with_backoff(url, retries=3, base_delay=1.0, **kwargs):
for attempt in range(retries):
try:
resp = requests.request(method="GET", url=url, timeout=30, **kwargs)
resp.raise_for_status()
return resp
except HTTPError as e:
status = e.response.status_code if e.response is not None else None
if status in (401, 403, 404) or attempt == retries - 1:
raise
delay = float(e.response.headers.get("Retry-After", base_delay * (2 ** attempt)))
time.sleep(delay)
except RequestException:
if attempt == retries - 1:
raise
time.sleep(base_delay * (2 ** attempt))
以上为按仓库代码中的接口语义组合的示例,未经实测,以仓库最新代码为准。这段是通用做法,不是课程仓库的官方内容——仓库里并没有提供这样的实现,别把它当成课程的一部分引用。
还有两条容易把限流排查带偏的岔路,都在同一批文件里写着:
其一,06-text-generation-apps/README.md 写明,当前 Microsoft Foundry 上未废弃的模型是 reasoning 模型(GPT-5 家族、o 系列),它们不支持 temperature / top_p,也不支持 max_tokens(改用 max_output_tokens),传了会得到「参数不支持」的报错。如果你在加重试的同时顺手把采样参数也调了,一个限流问题会变成两个错误交叠。
其二,githubmodels- 这个文件名前缀已经名不副实。00-course-setup/03-providers.md 与 06-text-generation-apps/python/githubmodels-app.py 的注释都逐字写明,GitHub Models 于 2026 年 7 月底退役,直接替代者是 Microsoft Foundry Models;样例读的环境变量也已经是 AZURE_INFERENCE_ENDPOINT 与 AZURE_INFERENCE_CREDENTIAL。所以「三条路线怎么选」的第三条,现在实际是 Microsoft Foundry Models(原 GitHub Models 路线)。如果你还在按旧写法配 GITHUB_TOKEN,那报错根本不是限流。
四、改完怎么验证
验证重试逻辑不需要真的去打限流,tests/test_api_utils.py 已经给了范式:用 monkeypatch.setattr("shared.python.api_utils.requests.request", fake_request) 把底层请求换成计数器,再断言调用次数。你自己的封装照这个思路写一份即可——把假 request 换成按次序返回不同状态码的版本,断言 401 只被调用一次、429 被调用到上限。
Linux / macOS 侧照常 pip install -r requirements.txt 即可。Windows 侧多一步:00-course-setup/02-setup-local.md 的排查表里单列了一行 Windows 专属症状——pip 无法构建 wheel 时,先 pip install --upgrade pip setuptools wheel 再重试。另外 Windows 上 .env 与系统环境变量重名时容易搞混,验证时建议在调用点直接打印你实际读到的 endpoint 主机名(别打印 key),确认走的是你以为的那个 provider。
五、什么情况说明不是限流
最后一步别省,下面几种报错和速率没关系:
- 抛的是
ValueError,信息里有Missing required environment variable—— 这是shared/python/env_utils.py的get_required_env抛的,环境变量没配。 create_azure_openai_client抛ValueError,信息里是 endpoint 或 API key 缺失 ——tests/test_api_utils.py里两条用例分别钉住了AZURE_OPENAI_ENDPOINT与AZURE_OPENAI_API_KEY缺失这两种情形。顺带记一下这个函数的行为:它把base_url拼成<endpoint>/openai/v1/,docstring 里写明因为用的是 v1 endpoint,所以不需要api_version。- 抛的是
ImportError,信息是The 'openai' package is required—— 依赖没装,和网络无关。 - 报的是参数不支持 —— 大概率是上面说的 reasoning 模型采样参数那一条。
- 401 而不是 429 —— 凭证问题,重试再多次也是同一个结果。
- 请求一直挂到超时才抛出、而不是很快带回一个 HTTP 状态码 —— 那是连接层面的事(网络、代理、证书),
timeout参数管的就是这一段,和服务端限流不是一回事。
分清这几类之后再决定要不要加退避,比一上来就把 retries 调大有用得多。
本文依据 github.com/microsoft/generative-ai-for-beginners 仓库于 2026-08-18 的公开内容整理,
事实来自仓库内的课程正文与代码示例。我们没有跑过文中涉及的代码,
因此不涉及运行结果、耗时与生成质量的任何描述。
该课程持续更新,文中涉及的文件路径、依赖与接口写法随版本变动,请以仓库最新内容为准。
文中涉及的云端服务调用会产生费用并可能上传数据,请自行评估密钥与数据边界。
安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。