发出第一个大模型 API 请求:最小可用示例
发出第一个大模型 API 请求 只需三样东西:API key、端点 URL、一条 messages 数组。本文用最少代码帮你在 5 分钟内从零跑出成功响应,再介绍如何读懂返回值。
第一步:用 curl 验证连通性
拿到 key 之后,最快的验证方式是直接跑 curl,排除网络和 key 问题:
export OPENAI_API_KEY="sk-xxx"
export OPENAI_BASE_URL="https://api.lidayun.com/v1" # 你的端点
curl "$OPENAI_BASE_URL/chat/completions" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "hello"}],
"max_tokens": 32
}'
成功时会返回一段 JSON,找到 choices[0].message.content 就是模型的回复。
这一步不是走过场,是排查地基。SDK 封装了太多细节,一旦出错你很难分清是网络问题、key 问题还是代码写错了参数名。先用 curl 把「网络能不能通、key 对不对、模型名有没有拼错」这三件事确认清楚,后面写代码出错时至少能排除掉一半的可能性。
卡住没反应时,把命令改成带 -m 10 -v:
curl -m 10 -v "$OPENAI_BASE_URL/chat/completions" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hello"}]}'
-m 10 给请求设 10 秒超时,避免命令行卡死到你怀疑人生;-v 会把完整的请求头、TLS 握手过程、响应头都打印出来。我见过最典型的一个坑:本地开了系统代理,no_proxy 又没配置对,curl 悄悄把请求走了代理转发出去,结果连不上目标端点,报错信息却是很笼统的 Failed to connect to api.xxx.com。这条报错看着像 DNS 或者服务器的问题,实际上跟 key 对不对没有任何关系,你越检查 key 越查不出问题——这时候 -v 里的 Trying xxx.xxx.xxx.xxx 那一行会告诉你请求根本没走到正确的地址上,一眼就能定位。
不同平台的 base_url 写法也不完全一样,接入前先核对清楚,路径写错最常见的表现就是 404:
| 平台 | base_url 示例 | 备注 |
|---|---|---|
| OpenAI 官方 | https://api.openai.com/v1 | 需要能访问境外网络 |
| DeepSeek | https://api.deepseek.com/v1 | 国内可直连 |
| 阿里云百炼(Qwen) | https://dashscope.aliyuncs.com/compatible-mode/v1 | 兼容模式路径比想象中长,容易漏写 /compatible-mode |
| 力达云聚合入口 | https://api.lidayun.com/v1 | 统一多平台模型的接入地址 |
具体计费单价、并发限额以各平台官方文档和账单页为准,这里不写死数字,因为几乎每个月都在变。
读懂响应结构
{
"id": "chatcmpl-xxx",
"object": "chat.completion",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Hello! How can I help you today?"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 9,
"completion_tokens": 9,
"total_tokens": 18
}
}
| 字段 | 含义 |
|---|---|
choices[0].message.content | 模型回复文本 |
choices[0].finish_reason | stop=正常结束;length=达到 max_tokens 截断 |
usage.prompt_tokens | 输入消耗的 token 数 |
usage.completion_tokens | 输出消耗的 token 数 |
usage.total_tokens | 本次请求总 token(计费依据) |
finish_reason 除了 stop 和 length,还有两个新手容易忽略的值:content_filter(内容被安全策略拦截,content 可能是空的或者被截断,这不是你代码的 bug,是平台侧的审核策略生效了)、tool_calls(模型没有直接回答,而是决定调用你注册的工具函数,这时候 message.content 通常为 null,真正的信息在 message.tool_calls 里)。如果你的代码只判断 content 是否为空就报错,遇到 tool_calls 场景会误判成请求失败——这是我踩过的一个真实的坑,排查了小半天才发现根本不是接口挂了,是没处理这个分支。
流式响应(stream=True)的结构跟这里展示的非阻塞响应不一样:不是一次性拿到完整的 message,而是收到一连串只包含增量文本的 delta 片段,usage 字段也可能只在最后一个 chunk 里才给出,具体位置各平台实现略有差异,写代码前翻一下你接入的那个平台的文档确认。
Python 最小示例
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["OPENAI_API_KEY"],
base_url=os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1"),
)
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "你好"}],
max_tokens=64,
)
print(resp.choices[0].message.content)
三行核心逻辑:初始化 client → 调用 create → 读取 content。
注意这里 api_key 用的是 os.environ["OPENAI_API_KEY"](方括号取值),base_url 用的是 os.environ.get(...)(get 方法取值)。这两种写法不是随手换着写的,是故意的:key 是必须项,没配置直接让程序崩溃报 KeyError,比悄悄传个空值进去然后拿到一个语焉不详的 401 要好排查得多;base_url 有默认值兜底,用 .get() 配一个默认值更合理。如果你把两处写法搞反了,最容易出现的现象是:本地忘了 export 环境变量,代码没报错也没崩溃,但请求全部返回 401,你会怀疑是 key 本身失效了,其实是 os.environ.get("OPENAI_API_KEY") 悄悄拿到了 None,SDK 用 None 拼了一个 Authorization: Bearer None 的请求头发出去,服务端自然把它当成无效 key 拒绝。
进阶:流式输出
如果你在做类似聊天界面的场景,用户等待完整回复会觉得卡顿,这时候流式输出能明显改善体验:
stream = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "你好,介绍一下你自己"}],
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta.content
if delta:
print(delta, end="", flush=True)
流式的好处是首字延迟(TTFT,Time To First Token)低,用户能立刻看到文字一点点蹦出来,感知上比等 3 秒钟一次性出结果要快得多,即便两者总耗时其实差不多。但流式不是任何场景都该用:如果你要模型输出的是一段结构化 JSON,后续还要解析字段做后续逻辑,流式反而麻烦——你得自己拼接完整字符串再解析,还得处理半截 JSON 解析失败的情况;这类场景老老实实用非流式的 stream=False(默认值),拿到完整 content 再一次性 json.loads 更省心。判断标准很简单:给人看的、要即时反馈的场景用流式;给程序下一步处理的场景用非流式。
进阶:请求失败后的重试退避
线上环境请求量一大,偶尔撞上 429(限流)或者短暂的网络抖动是常态,别指望永远不失败,要写好重试逻辑:
import time
import random
def call_with_retry(client, **kwargs):
for attempt in range(5):
try:
return client.chat.completions.create(**kwargs)
except Exception as e:
msg = str(e)
if "429" not in msg and "rate" not in msg.lower():
raise
wait = (2 ** attempt) + random.random()
time.sleep(wait)
raise RuntimeError("重试 5 次仍失败,请检查配额或稍后再试")
这里用的是指数退避(exponential backoff)加抖动(jitter):等待时间是 1、2、4、8、16 秒依次翻倍,再叠加一个 0-1 秒的随机数。为什么不直接固定 sleep(1) 重试?因为限流场景下如果你的服务同时有几十个请求都在 429,大家都固定等 1 秒后再一起重试,等于集体又撞了一次限流,越重试越拥堵;加上随机抖动能把重试请求错开时间点,退避的间隔也随着失败次数拉长,给服务端足够的恢复窗口。另外要注意判断条件——只对 429/限流类错误重试,像 401(key 错误)、400(参数错误)这种重试再多次结果也不会变,应该让它直接抛出来,不然会白白浪费几十秒时间还掩盖了真实问题。
Node.js 最小示例
// node 18+,先 npm install openai
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1",
});
const resp = await client.chat.completions.create({
model: "gpt-4o-mini",
messages: [{ role: "user", content: "你好" }],
max_tokens: 64,
});
console.log(resp.choices[0].message.content);
Node 版同理,baseURL 用了 ?? 空值合并运算符兜底默认地址,跟 Python 里 .get() 的思路一致。如果你要做流式,把 create 的参数加上 stream: true,然后用 for await 遍历:
const stream = await client.chat.completions.create({
model: "gpt-4o-mini",
messages: [{ role: "user", content: "你好" }],
stream: true,
});
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta?.content;
if (delta) process.stdout.write(delta);
}
这里用了可选链 ?.,因为流式响应的某些 chunk(比如最后一个携带 usage 统计的 chunk)里 delta 字段可能不存在,不加可选链直接访问会抛 TypeError: Cannot read properties of undefined,这是 Node 里接流式接口最常见的一个报错,加上 ?. 就能安全跳过。
常见错误速查
| HTTP 状态码 | 原因 | 解决 |
|---|---|---|
| 401 | key 无效或未传 | 检查 Authorization: Bearer sk-xxx |
| 403 | key 无权限访问该模型 | 确认 key 所属平台支持该模型名 |
| 404 | 端点 URL 错误 | 检查 base_url 末尾是否带 /v1 |
| 429 | 超出速率限制 | 降低频率或申请更高配额 |
| 500 | 平台内部错误 | 稍后重试,或检查模型服务状态 |
除了这五个标准状态码,实际接入时还会碰到几个更隐蔽的报错,同样值得记下来:
| 报错现象 | 根因 | 排查方法 |
|---|---|---|
context_length_exceeded / 400 | 输入内容加上历史对话超过了模型的上下文窗口上限 | 裁剪历史消息,或换用上下文窗口更大的模型;不要把整段长文档硬塞进一次请求 |
ECONNRESET / 请求超时无响应 | 网络链路中断,或者服务端正在处理长任务响应慢 | 给客户端设置合理的超时时间(不要用默认的无限等待),超时后走重试逻辑而不是让程序卡死 |
| SSL/TLS 证书校验失败 | 本机系统时间不准,或者流量被代理软件中间劫持解密 | 校准系统时间;关闭或正确配置代理的证书信任链 |
| 返回内容是乱码或部分中文变问号 | 客户端按错误编码解析响应体,通常是没有显式按 UTF-8 处理 | 确认 HTTP 客户端读取响应时使用 UTF-8 解码,尤其是自己手写 HTTP 请求而不是用官方 SDK 时容易漏这一步 |
上线前算一笔账:这次调用到底花多少钱
不要拍脑袋估成本,用 usage.total_tokens 这个字段自己测出来。做法很简单:在测试环境里,用你产品真实会出现的那种输入(不是打个招呼的 “hello”,而是真实业务场景下用户可能输入的长度),跑上 100 次左右的请求,把每次返回的 usage.total_tokens 记下来求平均值,再套下面这个公式估算月成本:
月成本 ≈ 日均调用次数 × 平均 total_tokens × 单价(元/千 token) × 30
单价直接查你接入平台当前的官方计费页,不同模型档位差好几倍,一定要对应到你实际调用的那个 model 名称去查,不要用别的档位的价格估算,否则误差可能翻倍都不止。这套方法比上线前凭感觉估的准得多——我见过不少团队上线前完全没测过真实场景下的 token 消耗,结果上线第一周账单就远超预期,回头查才发现是因为业务里习惯把很长的历史对话原样带上,prompt_tokens 比测试时用的短对话高出好几倍。
常见问题
model 字段填什么? 填平台支持的模型名称,如 gpt-4o-mini、deepseek-chat、qwen-plus 等;不同平台的模型名不通用,请查阅对应文档。
max_tokens 设多少合适? 测试阶段设 64-256 节省费用;生产中根据业务需要设 1024-4096,不设或设太大会导致不必要的等待和费用。
curl 成功但 Python 代码 401,为什么? Python 从环境变量读取时注意 os.environ["KEY"](会抛异常)vs os.environ.get("KEY")(返回 None),后者传入 None 会导致鉴权失败;确认变量名拼写一致。
流式和非流式返回的 token 消耗会不一样吗? 不会,usage.total_tokens 反映的是模型实际生成的内容量,跟你选择流式还是非流式取回没有关系,流式只是把同样的结果拆成多个片段传输而已,计费口径以官方账单为准。
要不要每次请求都新建一个 client 实例? 不需要,也不建议。OpenAI(...) 初始化的 client 对象内部维护了连接池,正确做法是在应用启动时创建一次、全局复用,每次请求都重新 new 一个会导致 TCP 连接反复建立,在并发量上来之后会明显拖慢响应速度。
更多接入方案见大模型 API 接入完全指南与接入教程专题。了解如何构造多轮对话见messages 角色与多轮对话构造。需要统一多模型入口?申请力达云聚合 API 内测。