这篇讲什么
拿到一把 OpenAI 兼容协议的 API Key 之后,代码侧真正要做的只有两件事:把 base_url 指到正确的网关地址,把鉴权头写对。听起来简单,但实际动手时最容易在路径拼接上翻车------十次报 404,九次是 /v1 多写或少写。
这篇按「环境准备 → 三种语言分别跑通 → 统一封装 → 404 定位」的顺序走一遍,每一步都有可运行代码和实际输出。
环境准备
只需要装官方 SDK,兼容协议的网关不需要额外依赖:
bash
pip install openai>=1.0.0 # Python
npm install openai # Node.js
另外准备两个环境变量,不要把密钥写死在代码里:
bash
# Linux / macOS
export LLM_API_KEY="sk-xxxxxxxxxxxxxxxx"
export LLM_BASE_URL="https://your-gateway.example.com/openai/v1"
powershell
# Windows PowerShell
$env:LLM_API_KEY = "sk-xxxxxxxxxxxxxxxx"
$env:LLM_BASE_URL = "https://your-gateway.example.com/openai/v1"
这里的 LLM_BASE_URL 请替换成你所用平台文档里给出的兼容协议地址。有一点要先分清:你登录管理密钥的站点域名,和实际发起调用的接口域名,往往不是同一个 。前者是控制台,后者才是要填进 base_url 的值。接入前务必在平台文档里核对这两个地址,直接把控制台域名填进代码是新手最常见的第一个错。
第一步:理解 base_url 该写到哪一层
OpenAI 兼容协议的路径结构是固定的:
<网关根地址>/v1/chat/completions
而不同工具对 base_url 的处理方式不一样,这是 404 的根源:
| 调用方式 | base_url 写到哪 | 说明 |
|---|---|---|
| openai Python SDK | .../openai/v1 |
SDK 自动补 /chat/completions |
| openai Node SDK | .../openai/v1 |
同上,字段名是 baseURL |
| curl / requests 手写 | 完整端点 .../openai/v1/chat/completions |
没人替你拼路径 |
| 部分三方框架 | 视文档,多数只到根地址 | 框架内部可能已带 /v1 |
记一句话就够了:SDK 写到 /v1,手写请求写完整端点。
第二步:鉴权怎么设置
鉴权是标准的 Bearer Token,放在 HTTP 请求头里:
Authorization: Bearer <你的Key>
Content-Type: application/json
因为和官方协议完全一致,所以官方 SDK 不需要任何改造,只换 base_url 就能跑。用 SDK 时你甚至不用手写这个头,传 api_key 参数即可,SDK 会自动组装。
第三步:Python 跑通第一个请求
python
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["LLM_API_KEY"],
base_url=os.environ["LLM_BASE_URL"], # 结尾到 /v1
)
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "你是一个简洁的技术助手,回答不超过两句话。"},
{"role": "user", "content": "用一句话解释什么是 Bearer Token"},
],
temperature=0.3,
)
print("模型:", resp.model)
print("回复:", resp.choices[0].message.content)
print("用量:", resp.usage.prompt_tokens, "+", resp.usage.completion_tokens)
实际输出(内容每次略有不同):
模型: gpt-4o-mini
回复: Bearer Token 是一种把令牌放在 HTTP Authorization 头里传递的鉴权方式,服务端凭这个令牌识别调用方身份。
用量: 42 + 38
跑到这一步说明三件事同时正确了:地址对、密钥有效、路径拼接没问题。
第四步:Node.js 版本
javascript
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.LLM_API_KEY,
baseURL: process.env.LLM_BASE_URL, // 注意是 baseURL,驼峰
});
const resp = await client.chat.completions.create({
model: "gpt-4o-mini",
messages: [{ role: "user", content: "写一句项目启动的欢迎语" }],
});
console.log(resp.choices[0].message.content);
Node 端有两个坑:字段名是 baseURL(大写 URL),不是 base_url;以及 await 顶层使用要求 package.json 里声明 "type": "module",否则改用 .mjs 后缀或包一层 async 函数。
第五步:curl 快速验活
调试阶段想确认「到底是我的代码有问题,还是密钥/地址有问题」,用 curl 隔离最快:
bash
curl "$LLM_BASE_URL/chat/completions" \
-H "Authorization: Bearer $LLM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "只回复两个字:收到"}]
}'
返回结构长这样(截取关键字段):
json
{
"id": "chatcmpl-xxxxxxxx",
"object": "chat.completion",
"model": "gpt-4o-mini",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "收到" },
"finish_reason": "stop"
}
],
"usage": { "prompt_tokens": 18, "completion_tokens": 2, "total_tokens": 20 }
}
注意 curl 这里拼的是 $LLM_BASE_URL/chat/completions------因为环境变量已经带了 /v1,所以只补后半段。
第六步:封装成可复用的客户端
真实项目里不会每处都 new 一个客户端。加上启动期校验和超时重试,写成一个模块:
python
# llm_client.py
import os
import sys
from openai import OpenAI
REQUIRED = ("LLM_API_KEY", "LLM_BASE_URL")
def _check_env() -> None:
missing = [k for k in REQUIRED if not os.environ.get(k)]
if missing:
print(f"[fatal] 缺少环境变量: {', '.join(missing)}", file=sys.stderr)
sys.exit(78) # 78 = EX_CONFIG,配置错误
_check_env()
client = OpenAI(
api_key=os.environ["LLM_API_KEY"],
base_url=os.environ["LLM_BASE_URL"],
timeout=30.0,
max_retries=2,
)
def ask(prompt: str, model: str = "gpt-4o-mini") -> str:
resp = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
)
return resp.choices[0].message.content
if __name__ == "__main__":
print(ask("自检通过就回复 OK"))
两个细节值得说明。一是启动期就校验环境变量 ,缺失直接退出,而不是等第一次调用时抛一个语义模糊的鉴权错误;退出码用 78 是沿用 sysexits 的约定,容器编排和 CI 能据此区分配置问题和运行时问题。二是 max_retries=2 让 SDK 自己处理瞬时网络抖动,业务代码不用套一层 try/except 重试。
404 定位清单
如果上面任一步返回 404,按这个顺序查,基本一遍就能定位:
1. 打印实际请求的完整 URL
python
import httpx, logging
logging.basicConfig(level=logging.DEBUG)
# 或者手动拼一遍确认
print(os.environ["LLM_BASE_URL"].rstrip("/") + "/chat/completions")
2. 数一下 /v1 出现了几次
出现两次(/v1/v1/chat/completions)说明 SDK 又补了一遍------把 base_url 里的 /v1 去掉,或确认 SDK 是否需要你带。出现零次说明手写请求漏了。
3. 确认没把控制台域名当接口域名
这一条单独列出来,因为它报的也是 404,很容易被误判成路径问题。管理密钥的站点通常没有 /v1/chat/completions 这个路由。
4. 用 -v 看真实响应头
bash
curl -v "$LLM_BASE_URL/chat/completions" -H "Authorization: Bearer $LLM_API_KEY" -d '{}'
如果返回的 content-type 是 text/html,说明请求根本没进到 API 层,八成是地址整个写错了;返回 JSON 且带 error.message 才是 API 在正常回你。
顺手记住另外两个常见状态码的区别,能省不少排查时间:401 是密钥错 (拼写、多余空格、引号被包进值里),404 是路径错 ,429 是频率限制。三者原因完全不重叠,别混着试。
小结
配置 OpenAI 兼容接口,本质就是两行:base_url 指向平台文档给出的网关地址,鉴权用 Bearer Token。真正需要肌肉记忆的是路径规则------SDK 写到 /v1,手写请求写完整端点;遇到 404 先数 /v1 的个数,再确认域名有没有把控制台和接口搞混。把本文的 llm_client.py 复制进项目,环境变量配好,剩下的调用逻辑和官方写法完全一致,不需要为兼容协议做任何额外适配。