.env 写了却读不到:读一遍微软生成式 AI 入门课的 env 加载逻辑

2026-08-18

跟 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_envvalidate_env_varsget_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()

到这里,加载顺序就清楚了,一共三段,谁都不能少:

  1. 进程启动时继承的系统/终端环境变量;
  2. 调用方执行 load_dotenv(),把 .env 里的键值注入进程环境;
  3. 业务代码(或 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.pyaoai-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 Models00-course-setup/03-providers.md00-course-setup/02-setup-local.md.env.copy 以及 githubmodels-app.py 自己的注释里都写了)。所以这条路线实际要配的是 AZURE_INFERENCE_ENDPOINTAZURE_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.txtpyproject.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 里 cpCopy-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_validationapi_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 指控,只是你排查时得知道这条分岔。

处置之后怎么验证

按顺序过三关,每关都有明确的观察点:

  1. 在仓库根目录起一个 Python 交互式会话,跑 from dotenv import load_dotenv; load_dotenv(); import os; print(os.getenv("AZURE_INFERENCE_ENDPOINT"))。有值 → .env 的位置和格式没问题。
  2. 跑一遍公共工具的测试。pyproject.tomltestpaths 指向 tests,直接在仓库根执行 pytest 就行;addopts 里已经配了 -v --tb=short(这是仓库当前配置里的值,随版本可能变动)。测试通过说明你的 Python 环境和 shared 包的导入路径是通的——注意这一步验证的是环境,不是你的 key。
  3. 再跑目标课的脚本。

什么情况说明不是这个原因

这一节别跳过,下面几种情况再怎么折腾 .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 用作业文件名前缀来区分路线:aoaioaihfgithubmodels。写哪条路线就配哪一组变量,别把 AZURE_OPENAI_*AZURE_INFERENCE_* 混着填。

最后提一句:.env.gitignore 里,这是仓库有意为之(03-providers.md 明说这个文件被 gitignore 以保护密钥)。别为了图省事把它提交上去,也别把 key 直接硬编码回 .py 文件里。安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。


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

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