调用 DeepSeek API 时收到 401,通常不是单一原因,而是鉴权链路中某一环断裂。本文按请求从客户端到服务端的实际路径,逐段给出排查方法和最小验证代码。
说明:本文涉及的具体端点、模型名、请求头格式与错误响应字段均为示例,实际行为请以 DeepSeek 官方文档和真实响应为准。
第一段:先打印完整响应体,不要只看状态码
401 只说明凭证未被接受,具体原因需要看响应内容。不同服务、不同版本的错误体结构可能不同,因此第一步是原样打印完整响应体和响应头,而不是根据记忆猜测错误分类。
import requests
resp = requests.post(
"https://api.deepseek.com/chat/completions", # 示例端点,以官方文档为准
headers={"Authorization": "Bearer " + api_key},
json={"model": "deepseek-chat", "messages": [{"role": "user", "content": "hi"}]},
)
print(resp.status_code)
print(resp.headers)
print(resp.text)
拿到完整响应体后,再对照官方文档判断错误字段含义。如果官方文档没有明确分类,就把它当作待验证信息,不要自行归纳成确定的产品行为。
第二段:检查 Authorization 头是否被中间件覆盖
这是后端框架中最隐蔽的一类问题。常见场景:
- 全局请求拦截器统一设置了
Authorization,覆盖了业务代码传入的 Key; - 日志脱敏中间件把
Bearer前缀截断后回写; - 重试逻辑重新构造请求时丢失了原始头。
排查方法:在真正发出 HTTP 请求的最后一层打印 headers,而不是在业务入口打印。以 Python 为例,可以用 httpx 的 event hooks 在发送前拦截:
import httpx
def log_request(request):
print("FINAL HEADERS:", dict(request.headers))
with httpx.Client(event_hooks={"request": [log_request]}) as client:
client.post(url, headers=headers, json=payload)
如果这里打印出的 Authorization 与预期不符,问题就在客户端中间件,而非服务端。
第三段:核对 API Key 加载顺序与环境变量
多环境部署时,Key 的来源可能有多层:.env 文件、系统环境变量、容器编排注入、配置中心。加载顺序不同会导致实际使用的 Key 与预期不一致。
排查清单:
- 在应用启动时打印 Key 的前 6 位和后 4 位(不要打印完整 Key),确认加载的是哪一份;
- 检查是否存在多个同名环境变量,例如 shell 中已 export 的变量覆盖了
.env; - 容器场景下确认
env注入发生在进程启动之前; - 确认没有把 Key 误写成
Bearer sk-xxx整体存入环境变量,导致拼接后变成Bearer Bearer sk-xxx。
import os
key = os.environ.get("DEEPSEEK_API_KEY", "")
print("key prefix:", key[:6], "suffix:", key[-4:], "len:", len(key))
assert not key.startswith("Bearer "), "环境变量中不应包含 Bearer 前缀"
第四段:代理与网关是否丢失 Bearer 前缀
经过 Nginx、API Gateway 或企业代理时,Authorization 头可能被重写或丢弃。典型表现:直连成功,走代理就 401。
验证方法:
- 用
curl -v直连目标端点,确认成功; - 再通过代理地址发起同样请求,对比
>发出的请求头中Authorization是否完整; - 检查代理配置中是否有
proxy_set_header Authorization "";之类的清空规则; - 确认代理没有把
Bearer前缀当作自定义字段剥离。
如果代理层做了鉴权转换,需要确认转换后的头仍然符合目标服务要求的格式。具体格式以官方文档为准。
第五段:核对 Key 与 base_url 的来源是否一致
如果你的架构中存在多个 base_url(例如自建网关、多环境配置、区域化部署),需要核对当前请求使用的 base_url 与 Key 是否来自同一套配置来源。
注意:DeepSeek API 本身是否对 Key 与 Endpoint 做区域或环境绑定,当前没有官方一手资料可以确认。因此这里只作为基于读者自有架构的检查项,而不是 DeepSeek 的产品行为断言。
排查时确认:
- 当前请求的 base_url 是从哪个配置项读取的;
- 当前 Key 是从哪个环境变量或配置中心读取的;
- 两者是否由同一套部署配置注入;
- 是否存在测试配置覆盖生产配置的情况。
建议在配置中把 base_url 和 Key 作为一组绑定管理,而不是分开注入。
最小验证脚本
当上述分段排查仍无法定位时,用一个不依赖任何框架的最小脚本做基线验证:
import os, httpx
key = os.environ["DEEPSEEK_API_KEY"]
base = os.environ.get("DEEPSEEK_BASE_URL", "https://api.deepseek.com") # 示例,以官方文档为准
with httpx.Client(timeout=30) as c:
r = c.post(
f"{base}/chat/completions",
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
json={"model": "deepseek-chat", "messages": [{"role": "user", "content": "ping"}]},
)
print(r.status_code)
print(r.text)
如果这个脚本成功,说明 Key 和端点本身可用,问题在业务代码的中间件或配置层;如果这个脚本也 401,则问题在 Key 本身、环境变量或网络出口。
排查顺序建议
按以下顺序推进,避免在错误层面反复尝试:
- 打印完整 401 响应体,对照官方文档确认错误语义;
- 用最小脚本直连验证 Key 与端点;
- 在最终发送层打印 headers,确认 Authorization 未被覆盖;
- 核对 Key 加载来源与格式,排除重复 Bearer 前缀;
- 对比直连与代理请求头,定位网关丢失;
- 核对 Key 与 base_url 是否来自同一套配置来源。
401 的本质是凭证在到达服务端前被修改、丢失或本身无效。逐段隔离变量,比反复更换 Key 更有效。