图像生成请求被内容策略挡下:微软生成式 AI 入门课示例怎么处理
第 09 课 09-building-image-applications/ 是这门课里少数几处「示例代码本身留了坑」的地方。你把 .env 配好、把 prompt 换成自己的描述,跑完程序,控制台干干净净地打印一行 completed!,images/ 目录里却什么都没有。这时候大多数人的第一反应是路径写错了或者权限不对,但更常见的原因是请求根本没有通过服务端的内容策略,而示例代码恰好把错误吞掉了。
下面按「现象 → 怎么确认 → 怎么处置 → 怎么验证 → 什么情况不是它」走一遍,依据全部落在仓库里能翻到的具体文件上。
现象长什么样
课程的 Azure 路线示例是 09-building-image-applications/python/aoai-solution.py 与同目录的 aoai-app.py。两个文件的骨架一样:try: 里调 client.images.generate(...),把 b64_json 解码写盘,最后用 Pillow 打开图片。
问题出在末尾。这两个文件的第一行都写着 from openai import AzureOpenAI, BadRequestError,把异常类导进来了;但真正的捕获分支是注释掉的:
# catch exceptions
#except BadRequestError as err:
# print(err)
finally:
print("completed!")
也就是说,try 块只跟了一个 finally,没有 except。异常会照常向外抛,但 finally 里那句 completed! 一定会先执行。如果你是在 notebook 或者某个包了一层的运行环境里跑,输出被截断、traceback 被折叠,肉眼看到的就只剩下 completed! 这四个字加一个感叹号——它是「这段代码执行完毕」的意思,不是「图片生成成功」的意思。
导入了 BadRequestError 却没有用它,这是同一个文件里两处白纸黑字放在一起就能看出来的不一致,值得先记住。
怎么确认是内容策略挡下的
第一个动作:把捕获分支恢复出来。 不用自己发明写法,仓库里同目录的 notebook 给了完整版。aoai-assignment.ipynb 的教学代码块里是这样写的:
# catch exceptions
except BadRequestError as err:
print(err)
OpenAI 平台那条路线的 oai-assignment.ipynb 对应写法是 except openai.BadRequestError as err: print(err),区别只在导入方式(notebook 里用的是 import openai 而不是 from openai import ...)。把 .py 文件里被注释掉的两行取消注释,就和 notebook 对齐了。异常正文一旦打印出来,是内容策略拒绝还是参数不合法,服务端返回的消息里会写。
第二个动作:看异常类型落在哪一层。 BadRequestError 表示这次请求本身被判定为不合法,内容策略拒绝是其中一类。它和网络超时、鉴权失败不是一回事。同仓库的 08-building-search-applications/scripts/transcript_enrich_summaries.py 把这条语义写得很直白,它给调用函数加的 tenacity 装饰器里有一项是 retry=retry_if_not_exception_type(BadRequestError)——重试策略明确把 BadRequestError 排除在外。同一个文件在调用点又单独写了 except BadRequestError as invalid_request_error:,记一条 warning 然后降级。
这两处合起来给出的判据是:这类错误不是瞬时故障,重试没有意义,要么改请求,要么走降级分支。你在第 09 课遇到 BadRequestError 时,思路应该是同一套。
第三个动作:确认成功判据。 gpt-image 系列返回的是 base64,不是图片 URL。README 在「Important」提示里逐字写明这一点,同页示例代码里对应的一行是 base64.b64decode(result.data[0].b64_json);aoai-solution.py 里则是先 json.loads(result.model_dump_json()) 转成字典,再取 generation_response["data"][0]["b64_json"] 去解码,两种写法拿的是同一个字段。所以判断成功与否的唯一可靠依据是 images/ 目录下有没有 PNG 落盘,而不是控制台打印了什么。
仓库给出的处置:metaprompt 与它的边界
README 的 Setting boundaries with metaprompts 一节给的处置是:在用户 prompt 前面拼一段约束文本。aoai-solution.py 里的完整实现是先定义 disallow_list 字符串,再用 f-string 把它插进 meta_prompt,最后拼出真正发送的 prompt:
prompt = f"""{meta_prompt}
Generate monument of the Arc of Triumph in Paris, France, in the evening light with a small child holding a Teddy looks on.
"""
上面这段中的具体描述文字只是仓库里的示例值,作业要求是自己换成别的地标。
这里必须把边界说清楚,否则很容易误解成「加了 metaprompt 就不会被拒」:
- 它只是字符串拼接,客户端没有任何过滤逻辑。
disallow_list里那串词最终是作为 prompt 正文一起发给服务端的,不是本地黑名单。改写提示词能影响模型生成什么,不能决定服务端放不放行。 - README 自己也没把它当成唯一手段,那一节末尾写明要与 Microsoft Foundry 内置的 content filters 配合,做纵深防御。
- 它属于分层缓解里的一层。 第 03 课
03-using-generative-ai-responsibly/README.md的 Mitigate Potential Harms 一节把缓解手段分成模型、Safety System、Metaprompt、User Experience 几层来讲,其中平台侧的内容过滤系统被归在 Safety System 那一层,Metaprompt 是另外一层。两层不互相替代。
顺带说一个容易串味的点:仓库 docs/SECURITY_GUIDELINES.md 里有个 sanitize_prompt_input 函数,用正则剥掉 {{ }} 和 ${ } 这类模板占位。那是防模板注入的,和内容安全不是一回事,别拿它当内容过滤用。同一份文档里关于内容过滤的建议只有一句:用 provider 内置的能力。另外 shared/python/input_validation.py 提供了 validate_text_input,签名里有 max_length、min_length、allow_empty、field_name 几个参数,默认值分别是 500、1、False 和 "input"——这是仓库当前代码里的默认值,随版本可能变动,且它做的是长度与空值校验,同样不承担内容判定。
处置后怎么验证
- 取消注释后重跑原 prompt,确认控制台能打印出服务端返回的错误正文,而不再只有
completed!。这一步是为了验证「错误可见」这条链路通了。 - 换回课程给的示例 prompt 再跑一次,确认
images/目录里出现 PNG 文件。落盘路径在aoai-solution.py里是os.path.join(image_dir, 'ch9-sol-generated-image.png')。 - 如果要验证 metaprompt 生效,只能通过对比生成结果的构图与风格来看——课程没有提供任何可以在本地断言的内容判定接口,我们也没有跑过这些代码,不对生成结果做任何描述。
Windows 侧有一个细节值得单独提:README 的安装步骤把激活命令写成 source venv/bin/activate,行尾注释是 # Windows: venv\Scripts\activate;而 aoai-assignment.ipynb 的 Windows 提示块里写的是 venv\Scripts\activate.bat。两处不完全一致,PowerShell 下这两种写法的行为也不同,照抄前先看清自己用的是哪个 shell。仓库的 notebook 代码单元格里那句 ! source venv/bin/activate 在 Windows 上是跑不通的,只能当作 Linux/macOS 侧的示意。
什么情况说明不是内容策略的问题
这一步不能省,否则很容易把所有失败都归到内容策略上。
- 报的是
NameError,提示image_path未定义。 看 OpenAI 路线的oai-app.py:它的except OpenAIError as err:只包住了生成与写盘那段,而文件末尾还有一次独立的client.images.create_variation(...)调用在try之外。一旦前面的调用抛错被吞掉,image_path从未被赋值,末尾这次调用就会先炸在变量上。而且 README 已经写明create_variation只在 DALL·E 2 时代存在,DALL·E 2、DALL·E 3 都属于 legacy,DALL·E 3 不再支持新建部署。这段代码本身就是历史遗留。 - 报的是参数不支持,而你传了
temperature。 README 与两个 notebook 都写明图像模型不接受temperature,那是文本生成的采样控制项;想要多样性就再调一次,想收敛就把 prompt 写得更具体。顺带一提,第 06 课06-text-generation-apps/README.md写明当前 Foundry 上未废弃的是 reasoning 模型(GPT-5 家族、o 系列),它们同样不支持temperature/top_p,也不支持max_tokens(改用max_output_tokens),传了会报参数不支持。所以看到「参数不支持」先查参数表,别往内容策略上想。 - 报的是部署名或 endpoint 找不到。
.env三项是AZURE_OPENAI_ENDPOINT、AZURE_OPENAI_API_KEY、AZURE_OPENAI_DEPLOYMENT,代码里api_version的取值是示例里写死的"2025-04-01-preview",同时注释提醒要按 Foundry 文档确认你的模型需要哪个版本。这类错误属于资源配置,和内容判定无关。 - 错误被
except OpenAIError一把兜住,看不出类型。oai-app.py捕获的是范围更大的OpenAIError,限流、网络、鉴权会和BadRequestError混在一起打印。想分清楚,就把捕获分支按 notebook 的写法收窄到BadRequestError,其余单独兜底。
最后提一句路线选择。第 09 课在仓库里只给了两条 provider 路线的示例:aoai-* 走 Azure OpenAI,oai-* 走 OpenAI 平台,没有第三条的图像示例。课程其它章节里那批 githubmodels-* 前缀的示例,前缀名如今已经和实际接入目标脱节了——00-course-setup/03-providers.md 逐字写明 GitHub Models 于 2026 年 7 月底退役,直接替代者是 Microsoft Foundry Models,环境变量也换成了 AZURE_INFERENCE_ENDPOINT 与 AZURE_INFERENCE_CREDENTIAL 这一组。今天这个时间点已经过去了,看到那个前缀别再照着 GITHUB_TOKEN 去配。
以上代码片段均原样取自仓库对应文件,未做改写。该课程持续更新,文中涉及的文件路径、依赖与接口写法随版本变动,请以仓库最新内容为准。
本文依据 github.com/microsoft/generative-ai-for-beginners 仓库于 2026-08-18 的公开内容整理,
事实来自仓库内的课程正文与代码示例。我们没有跑过文中涉及的代码,
因此不涉及运行结果、耗时与生成质量的任何描述。
该课程持续更新,文中涉及的文件路径、依赖与接口写法随版本变动,请以仓库最新内容为准。
文中涉及的云端服务调用会产生费用并可能上传数据,请自行评估密钥与数据边界。
安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。