调用 DeepSeek API 时持续收到 401,通常不是密钥本身失效,而是鉴权信息在传递链路的某个环节被丢失或改写。本文从工程配置层面梳理排查顺序,帮助定位问题。
一、先确认密钥是否真正加载
最常见的情况是代码读取了环境变量,但变量并未注入进程。排查时不要只看 .env 文件是否存在,而要在运行时打印变量长度和前后几位(不要打印完整密钥):
import os
key = os.getenv("DEEPSEEK_API_KEY")
print("loaded:", bool(key), "len:", len(key) if key else 0)
print("prefix:", key[:6] if key else None)
如果 loaded 为 False,检查:
- 启动命令是否在正确的 shell 或容器环境中执行;
.env是否被实际加载(许多框架不会自动读取);- 容器编排中环境变量是否注入到应用容器而非构建阶段;
- 变量名是否拼写一致,大小写敏感。
二、密钥格式校验
拿到密钥后先做基本格式检查。DeepSeek API Key 通常以固定前缀开头,若前缀明显不符,说明取到了错误的值(例如取到了其他服务的 key,或复制时带入了引号、空格、换行)。
key = key.strip().strip('"').strip("'")
assert key.startswith("sk-"), "key prefix mismatch"
注意:不要用 assert 在生产代码中做校验,这里仅用于本地排查。
三、Authorization 请求头拼装
DeepSeek API 使用 Bearer 鉴权,请求头格式为:
Authorization: Bearer
手动构造请求时容易犯两类错误:
- 漏掉
Bearer前缀,只写了密钥; - 前缀与密钥之间缺少空格,或多了空格。
headers = {
"Authorization": f"Bearer {key}",
"Content-Type": "application/json",
}
如果使用 requests,注意不要同时传入 auth= 参数和 Authorization 头,两者可能冲突导致最终头被覆盖。
四、代理与网关中的头覆盖
在服务端部署时,请求往往经过 Nginx、API 网关或公司内网代理。这些中间层可能:
- 丢弃
Authorization头; - 重写为其他鉴权头;
- 在转发时追加自己的鉴权信息,覆盖原始值。
排查方法是在代理层打印最终转发出去的头(注意脱敏),或临时绕过代理直连验证。如果直连正常、经代理失败,问题基本在中间层。
五、SDK 默认配置与手动 HTTP 调用的差异
使用官方 SDK 时,鉴权头由 SDK 自动注入,通常只需在初始化时传入 api_key。手动 HTTP 调用则需要自己拼装。两者行为不一致时,重点检查:
- SDK 初始化时传入的 key 是否为空或为占位符;
- SDK 是否读取了与手动调用不同的环境变量名;
- SDK 版本是否过旧,导致请求路径或头格式与当前 API 不匹配。
建议先用最小手动 HTTP 请求验证密钥有效性,再回到 SDK 排查配置。
import requests
resp = requests.post(
"https://api.deepseek.com/chat/completions",
headers={"Authorization": f"Bearer {key}"},
json={
"model": "deepseek-chat",
"messages": [{"role": "user", "content": "ping"}],
},
timeout=30,
)
print(resp.status_code, resp.text[:200])
六、可复用的鉴权配置检查清单
- 运行时确认环境变量已加载,长度与预期一致;
- 去除密钥首尾空白、引号、换行;
- 确认前缀符合预期;
- 请求头为
Authorization: Bearer,空格正确; - 未同时使用
auth=与手动Authorization头; - 代理/网关未丢弃或覆盖鉴权头;
- SDK 初始化参数与环境变量名一致;
- 先用最小手动请求验证,再排查 SDK;
- 401 响应体中的错误信息可用于区分“密钥无效”与“头缺失”;
- 更换密钥后重启进程,避免旧值缓存。
七、边界与失败模式
- 密钥轮换后未重启服务,进程仍持有旧密钥;
- 多环境共用同一变量名,测试环境覆盖了生产值;
- 日志中打印了完整密钥,导致泄露后被动失效;
- 代理层对
Authorization做了大小写归一化,某些实现可能不兼容。
按上述顺序排查,多数持续 401 都能定位到具体的配置环节。