调用 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 与预期不一致。

排查清单:

  1. 在应用启动时打印 Key 的前 6 位和后 4 位(不要打印完整 Key),确认加载的是哪一份;
  2. 检查是否存在多个同名环境变量,例如 shell 中已 export 的变量覆盖了 .env;
  3. 容器场景下确认 env 注入发生在进程启动之前;
  4. 确认没有把 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 本身、环境变量或网络出口。

排查顺序建议

按以下顺序推进,避免在错误层面反复尝试:

  1. 打印完整 401 响应体,对照官方文档确认错误语义;
  2. 用最小脚本直连验证 Key 与端点;
  3. 在最终发送层打印 headers,确认 Authorization 未被覆盖;
  4. 核对 Key 加载来源与格式,排除重复 Bearer 前缀;
  5. 对比直连与代理请求头,定位网关丢失;
  6. 核对 Key 与 base_url 是否来自同一套配置来源。

401 的本质是凭证在到达服务端前被修改、丢失或本身无效。逐段隔离变量,比反复更换 Key 更有效。