.env 写了却读不到:读一遍微软生成式 AI 入门课的 env 加载逻辑
跟 generative-ai-for-beginners 这门课的人,十有八九在同一个地方卡过:.env 明明按 00-course-setup/03-providers.md 抄好了,key 也贴进去了,跑某一课的 Python 脚本还是甩一句缺少环境变量。然后开始怀疑是不是引号写错了、是不是有 BOM、是不是 key 失效了。
这类问题不用猜。这门课的 env 读取逻辑全在几个很短的文件里,读一遍就知道该往哪查。
现象长什么样
报错的形态不止一种,这一点本身就是线索:
ValueError: Missing required environment variable: XXX. Please set it in your .env file or environment.KeyError: 'AZURE_INFERENCE_CREDENTIAL'ModuleNotFoundError: No module named 'shared'
第一种来自 shared/python/env_utils.py,第二种来自示例脚本里直接写的 os.environ[...] 下标访问,第三种压根不是环境变量的问题。分不清这三种,排查方向从第一步就偏了。
第一刀:先确认到底是「没加载」还是「没读到」
先看这门课的公共工具模块。shared/python/env_utils.py 一共定义了三个函数:get_required_env、validate_env_vars、get_env_with_default。把这个文件从头翻到尾,你会发现一件挺反直觉的事:
整个文件只 import os,没有 import dotenv,也没有任何一处调用 load_dotenv()。
三个函数底下清一色是 os.getenv(...)。也就是说,这个模块只负责「从当前进程的环境变量里取值 + 取不到就抛个说得清楚的错」,它不负责把 .env 文件的内容读进进程。
而 get_required_env 抛出的那句错误提示,原文是 Please set it in your .env file or environment. —— 把这两处放在一起看:提示里提到了 .env,但这个模块自己从来不读 .env。 谁来读?调用方。你在 06-text-generation-apps/python/oai-app.py 里能看到标准写法:
from openai import OpenAI
import os
from dotenv import load_dotenv
# load environment variables from .env file
load_dotenv()
到这里,加载顺序就清楚了,一共三段,谁都不能少:
- 进程启动时继承的系统/终端环境变量;
- 调用方执行
load_dotenv(),把.env里的键值注入进程环境; - 业务代码(或
env_utils.py里的函数)用os.getenv/os.environ去取。
第二段是你自己代码里的一行,不是这门课的公共模块替你做的。 你写了个新脚本、from shared.python.env_utils import get_required_env 直接开用,忘了自己先 load_dotenv(),那它当然报缺失——.env 写得再对也没进程知道。
判定动作很简单,在报错的脚本里加两行看一眼:
import os
print(os.getenv("AZURE_INFERENCE_ENDPOINT"))
如果这里就是 None,问题在第一、二段(没加载);如果这里有值、后面还是报错,问题在第三段(变量名不一致或值不合法)。
顺带一个容易踩的不一致
同一课的 06-text-generation-apps/python/ 目录下,oai-app.py、aoai-app.py 这些都调了 load_dotenv(),但 githubmodels-app.py 全文没有调用 load_dotenv(),它开头就是 os.environ["AZURE_INFERENCE_CREDENTIAL"] 和 os.environ["AZURE_INFERENCE_ENDPOINT"] 的下标访问。这就是前面那个 KeyError 的来源:下标访问取不到会直接 KeyError,不会给你「请检查 .env」这种友好提示。
顺带说清一件事,免得配错方向:githubmodels- 这个前缀现在已经名不副实了。仓库在多处逐字写明 GitHub Models 于 2026 年 7 月底退役,直接替代者是 Microsoft Foundry Models(00-course-setup/03-providers.md、00-course-setup/02-setup-local.md、.env.copy 以及 githubmodels-app.py 自己的注释里都写了)。所以这条路线实际要配的是 AZURE_INFERENCE_ENDPOINT 与 AZURE_INFERENCE_CREDENTIAL,不是 GITHUB_TOKEN。如果你手里的老笔记还写着配 GITHUB_TOKEN,那是另一个「读不到」的原因。
查找路径:.env 该放哪,以及仓库没说的那部分
03-providers.md 的「Create .env file」一节写得很明确:.env.copy 在仓库根目录,第二步给的命令是
cp .env.copy .env
也就是说,.env 的位置是仓库根,而各课的脚本躺在 NN-xxx/python/ 这样的子目录里。这一层落差是很多「读不到」的现场原因。
需要老实交代一句:load_dotenv() 无参调用时按什么规则去找 .env 文件,仓库文档里没有找到相关说明——那是 python-dotenv 这个第三方库自己的行为,不属于这门课的内容,我不在这里替它下结论。仓库能给你的确定信息只有两条:.env 放仓库根、以及 requirements.txt 与 pyproject.toml 的依赖里都列了 python-dotenv。所以最稳的判定动作是在仓库根目录下启动 Python 进程(python 06-text-generation-apps/python/oai-app.py 这样跑,而不是先 cd 进那个子目录),先把变量排除掉,再谈别的。
Windows 侧多两个坑,都不在仓库文档范围内、属于通用排查经验,标注一下并非该项目官方内容:
02-setup-local.md的 Windows 分支给的建文件命令是echo . > .env,Unix 侧是touch .env。两条都只是「把文件建出来」,随后文档第 3 步要求你用编辑器打开去填内容——所以建完别忘了把里面原有的内容清掉。更省事的做法是直接复制模板:PowerShell 里cp是Copy-Item的别名,cp .env.copy .env可以照抄;cmd 下写copy .env.copy .env。- 用记事本「另存为」建的文件很容易变成
.env.txt。资源管理器默认不显示已知扩展名,肉眼看不出来。在 PowerShell 里Get-ChildItem -Force看一眼真实文件名,比盯着资源管理器强。
另一条查找路径:shared 这个包的 import
前面第三种报错 ModuleNotFoundError: No module named 'shared' 跟 .env 一点关系都没有,是 Python 的模块查找路径问题。tests/conftest.py 里干的就是这件事:
REPO_ROOT = Path(__file__).resolve().parent.parent
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
配合 pyproject.toml 里的 [tool.pytest.ini_options](testpaths = ["tests"]),跑测试时无论你在哪个工作目录,shared.python 都能解析到。但这个 conftest 只对 tests/ 下的测试生效——你在别的目录写脚本 import shared.python,没人替你插 sys.path。
还有一个更细的:shared/python/__init__.py 的导入语句是 from .env_utils import get_required_env, validate_env_vars,它的 __all__ 里还列了 input_validation、api_utils 那几个函数,但来自 env_utils 的同样只有上面这两个。get_env_with_default 没有被 __init__.py 导出。 所以 from shared.python import get_env_with_default 是取不到的,得像测试文件那样写全路径:from shared.python.env_utils import get_env_with_default。
测试文件覆盖了哪些边界,以及漏了哪一格
tests/test_env_utils.py 值得单独读一遍,它把这三个函数的边界语义钉死了,比读实现更快。测试全部用 pytest 的 monkeypatch 夹具(setenv / delenv)来造环境,不依赖任何 .env 文件——顺便说明一点:真实环境里已存在的同名变量会影响你的判断,测试用 monkeypatch.delenv("MISSING_VAR", raising=False) 就是为了先把它清干净。
覆盖到的边界:
| 函数 | 覆盖的边界 | 语义结论 |
|---|---|---|
get_required_env | 变量已设置 | 原样返回值 |
get_required_env | 变量未设置 | 抛 ValueError,消息里带变量名 |
get_required_env | 变量设为空字符串 | 同样抛 ValueError |
get_required_env | 传了 description 参数 | 描述文字被拼进错误消息 |
validate_env_vars | 多个变量都在 | 返回 名→值 的 dict |
validate_env_vars | 多个变量都缺 | 抛错,消息里同时列出所有缺失项 |
get_env_with_default | 变量未设置 | 返回传入的 default |
get_env_with_default | 变量已设置 | 返回环境里的值,忽略 default |
这张表里最该记住的是第三行和第六行。
第三行对应实现里的 if not value: —— get_required_env 把空字符串当作缺失,.env 里写了 AZURE_INFERENCE_CREDENTIAL= 后面什么都没跟,效果等于没写。测试 test_get_required_env_empty_raises 明确锁死了这个行为。
第六行是 validate_env_vars 存在的理由:它先把所有变量扫一遍收集 missing 列表,最后一次性抛出,所以一屏就能看全缺哪几个;而 get_required_env 一次只报一个,你得改一次跑一次。配一套新 provider 时用前者省事:
from shared.python.env_utils import validate_env_vars
env = validate_env_vars("AZURE_INFERENCE_ENDPOINT", "AZURE_INFERENCE_CREDENTIAL")
以上为按仓库代码中的接口语义组合的示例,未经实测,以仓库最新代码为准。
漏掉的那一格:get_env_with_default 只覆盖了「未设置」和「已设置为非空值」两种,没有覆盖「设置为空字符串」。而它的实现是 return os.getenv(var_name, default) —— os.getenv 的第二个参数只在键不存在时生效。把这两处放在一起就能得出:.env 里写了个空值时,get_required_env 会当成缺失报错,get_env_with_default 则会把空串原样返回、不会回落到 default。同一份 .env,两个函数对空值的判定并不一致。这不是 bug 指控,只是你排查时得知道这条分岔。
处置之后怎么验证
按顺序过三关,每关都有明确的观察点:
- 在仓库根目录起一个 Python 交互式会话,跑
from dotenv import load_dotenv; load_dotenv(); import os; print(os.getenv("AZURE_INFERENCE_ENDPOINT"))。有值 →.env的位置和格式没问题。 - 跑一遍公共工具的测试。
pyproject.toml里testpaths指向tests,直接在仓库根执行pytest就行;addopts里已经配了-v --tb=short(这是仓库当前配置里的值,随版本可能变动)。测试通过说明你的 Python 环境和shared包的导入路径是通的——注意这一步验证的是环境,不是你的 key。 - 再跑目标课的脚本。
什么情况说明不是这个原因
这一节别跳过,下面几种情况再怎么折腾 .env 都没用:
- 值读到了,但读到的是占位符。
.env.copy里所有值都是形如'<add your Microsoft Foundry Models API key here>'的占位串,而get_required_env只判空不判内容。你cp完忘了替换,它一路放行,错误会推迟到调用云端服务时才炸——那时的报错是认证失败,不是缺少环境变量。 - 报的是
KeyError而不是ValueError。 说明走的是os.environ[...]下标访问那条路(例如githubmodels-app.py),而那个脚本本身不调load_dotenv(),得靠进程环境或你自己补上加载。 - 报的是
ModuleNotFoundError,而缺的模块是dotenv。02-setup-local.md的 Troubleshooting 表里就有这一行(表里写作ModuleNotFoundError: dotenv),给的处置是pip install -r requirements.txt,属于依赖没装,不是配置问题。 - 在 GitHub Codespaces 里。
03-providers.md写明可以把变量存成 Codespaces secrets,这种情况下不需要本地.env;但同一段也写明这条路只对 Codespaces 有效,改用 Docker Desktop 仍然要配.env。 - 报错来自模型参数而不是环境变量。
06-text-generation-apps/README.md写明当前 Microsoft Foundry 上未废弃的是 reasoning 模型(GPT-5 家族、o 系列),它们不支持temperature/top_p,也不支持max_tokens(改用max_output_tokens),传了会得到「参数不支持」的错误。这跟.env无关,别往那个方向查。 - 变量名对不上。
03-providers.md用作业文件名前缀来区分路线:aoai、oai、hf、githubmodels。写哪条路线就配哪一组变量,别把AZURE_OPENAI_*和AZURE_INFERENCE_*混着填。
最后提一句:.env 在 .gitignore 里,这是仓库有意为之(03-providers.md 明说这个文件被 gitignore 以保护密钥)。别为了图省事把它提交上去,也别把 key 直接硬编码回 .py 文件里。安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。
本文依据 github.com/microsoft/generative-ai-for-beginners 仓库于 2026-08-18 的公开内容整理,
事实来自仓库内的课程正文与代码示例。我们没有跑过文中涉及的代码,
因此不涉及运行结果、耗时与生成质量的任何描述。
该课程持续更新,文中涉及的文件路径、依赖与接口写法随版本变动,请以仓库最新内容为准。
文中涉及的云端服务调用会产生费用并可能上传数据,请自行评估密钥与数据边界。