Skip to content
LLM API 调用机制:从 HTTP 请求到模型响应生命周期
概述
一次 LLM API 调用可以看作一个请求生命周期,包含四个阶段:
- 客户端构造请求:确定 API 地址、认证信息、模型 ID、消息列表和生成参数。
- 服务端接收请求:API 网关处理鉴权、限流、负载均衡,并将请求路由到对应的模型推理服务。
- 模型推理:推理引擎将消息文本转换为 token 序列,执行 prefill 和 decode,生成输出 token。
- 响应返回:服务端将生成结果、用量统计和停止原因构造成 JSON,通过普通 HTTP 响应或 SSE 流式响应返回给客户端。
不同服务商提供的 API 路径、参数和错误格式有差异,但上述生命周期是通用的。下文以 OpenAI Chat Completions API 为主要示例,并在差异处对照 Anthropic Messages API。
基本概念
API 地址与鉴权
LLM API 是基于 HTTP/HTTPS 的 RESTful 接口。创建一次 chat completion 的资源路径是 /chat/completions,完整地址通常是:
text
https://api.openai.com/v1/chat/completionsAnthropic Messages API 的地址是:
text
https://api.anthropic.com/v1/messages两个 API 都要求 Content-Type: application/json。认证方式不同:
- OpenAI 使用
Authorization: Bearer <API_KEY>。 - Anthropic 使用
x-api-key: <API_KEY>,并且要求anthropic-version请求头。
注意:API Key 是敏感凭据,应保存在服务端环境变量或受保护的密钥管理服务中,不应出现在前端代码或公开仓库中。
请求体:模型、消息与生成参数
messages 结构与角色
messages 是一个数组,每个元素表示一条对话消息,至少包含 role 和 content 两个字段。
常见角色包括:
system:设定助手的行为、语气和边界,通常放在消息列表开头。user:表示用户输入。assistant:表示模型之前的回复。在多轮对话中,把历史回复放入messages可以让模型获得上下文。
示例:
javascript
const messages = [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: "What is the capital of France?" },
{ role: "assistant", content: "Paris." },
{ role: "user", content: "Is it larger than London?" },
];这个数组描述了两轮对话:用户先询问法国首都,助手回答“Paris”,用户接着追问巴黎是否比伦敦大。system 消息放在最前,用于设定助手行为。
注意:Anthropic Messages API 不使用 messages 数组中的 system 角色,而是在请求顶层传入 system 参数。
生成参数
请求体中除了 model 和 messages,还可以传出若干生成参数。常用的有:
temperature:控制采样随机性。值越低,模型越倾向选择高概率 token;值越高,输出多样性越强。取值范围通常是 0 到 2。top_p:核采样参数。模型只从累计概率达到top_p的最小 token 集合中采样。max_tokens或max_completion_tokens:限制模型最多生成的 token 数。stop:一个字符串列表。模型生成到列表中的任意字符串时停止生成。stream:设为true时,服务端使用 SSE(Server-Sent Events)流式返回生成结果。stream_options:与stream配合使用,例如{ "include_usage": true }可以让流式响应末尾包含 usage 统计。tools和tool_choice:声明模型可以调用的函数,以及是否强制调用某个函数。
注意:不同模型对参数的支持不同。OpenAI 文档指出,推理模型对 temperature、top_p 等采样参数的处理方式可能与普通模型不同;同时,非推理模型也不接受 reasoning_effort 这类推理控制参数。
工作原理
服务端处理链路
请求到达服务商网关后,不会直接进入推理引擎,而是先经过几层处理:
- 身份认证:网关校验 API Key 是否有效,并确定调用方身份。API Key 无效或缺失时返回
401;认证通过但权限不足时返回403。 - 限流与配额:根据调用方的订阅级别和速率限制,判断请求是否被拒绝或排队。超出限制时返回
429。 - 负载均衡:将请求分发到当前可用的一个或多个推理实例,避免单点过载。
- 模型路由:读取请求体中的
model字段,映射到具体模型的部署服务。
推理引擎(如 vLLM、TGI 等)收到请求后,会把它放入调度队列。整个服务端链路可以简化为:
text
HTTP request -> API Gateway -> authentication -> rate limiter -> router -> inference engine -> responseTokenization 与 Chat Template
模型不能直接处理原始字符串。服务端在把 messages 送入模型之前,需要先将文本切分为 token,再把 token 映射为模型可读取的 ID。
Tokenization 的切分粒度因模型和分词器而异。常见自然语言中的高频词可能是一个 token,生僻词或长词可能被拆成多个 token。因此,API 响应中的 prompt_tokens 和 completion_tokens 并不等于字符数,而是模型实际处理的 token 数。
messages 数组还需要经过 chat template 转换成模型训练时使用的文本格式。一个模板可能把 system、user、assistant 消息分别包裹在特殊标记中,也可能像 Anthropic 那样把系统提示放在请求顶层,再拼接用户和助手消息。
上下文窗口指模型能接受的最大 token 数量,包括输入 token 和输出 token。如果 messages 序列太长,超出上下文窗口,请求会被拒绝,或者需要先做截断、摘要等处理。不同模型的上下文窗口大小不同,具体数值以模型文档为准。
模型推理:Prefill 与 Decode
模型生成文本分为两个阶段:
- Prefill:模型读取输入 token 序列,并行计算每一层的隐藏状态,并生成 KV Cache。
- Decode:模型逐个生成新 token。每生成一个 token,就把这个 token 加入输入序列,继续预测下一个 token。
Decode 阶段是自回归的,生成第 n 个 token 时,需要基于之前所有 token 的信息。KV Cache 缓存了历史 token 的键和值,可以避免每次重复计算前缀部分,从而加快解码速度。KV Cache 会随序列长度增长,这也是上下文长度受 GPU 内存限制的原因之一。
采样与停止条件
模型在每步解码时,会为词表中的每个 token 计算一个概率。temperature 和 top_p 会调整这个概率分布,然后从分布中采样得到实际输出的 token。
- 调低
temperature:概率分布变得更尖锐,高概率 token 更容易被选中。 - 调高
temperature:分布更平滑,低概率 token 也有机会出现。 top_p:只保留累计概率达到阈值的一部分 token,其余 token 的概率被置零后重新归一化。
Decode 不会无限进行。模型遇到以下情况之一会停止:
- 生成模型专用的结束 token(end-of-sequence token)。
- 输出长度达到
max_tokens限制。 - 输出命中
stop列表中的字符串。 - 模型决定调用工具,并生成了完整的工具调用参数。
- 输出被内容过滤器拦截。
停止后,服务端会在响应中通过 finish_reason 字段说明具体停止原因,对应值为 stop、length、tool_calls、content_filter 等。
连续批处理与提示缓存
连续批处理是现代推理引擎的重要调度方式。相比等待整个 batch 完成后才调度下一批请求,连续批处理允许在每个解码步骤动态加入新请求、移出已完成请求。这样可以在混合不同长度请求时减少 GPU 空转,提高整体吞吐。
为了减少重复计算,服务端可能对相同前缀的输入启用提示缓存(prompt caching)。在 OpenAI 的 usage 字段中,prompt_tokens_details.cached_tokens 会报告本次请求中命中缓存的 token 数。
此外,一些推理引擎会提供推测解码(speculative decoding)等优化。该技术用一个较小的模型先草拟多个 token,再用主模型并行验证,从而减少自回归生成步骤的等待时间。
响应格式
非流式响应
当请求中 stream 为 false 或省略时,服务端会等生成结束后返回一个完整的 JSON 对象。以下是简化示例:
json
{
"id": "chatcmpl-xxx",
"object": "chat.completion",
"created": 1710000000,
"model": "your-model-id",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Paris is the capital of France."
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 15,
"completion_tokens": 8,
"total_tokens": 23
}
}需要关注以下几个字段:
choices:数组结构。服务端可能返回多个候选结果,因此使用数组。大多数情况下只取choices[0]。choices[0].message:完整的助手消息。如果模型执行工具调用,该对象会额外包含tool_calls字段,其中带有函数名和参数。choices[0].finish_reason:停止原因。常见值包括stop(自然结束)、length(达到长度上限)、tool_calls(模型生成工具调用后停止)、content_filter(输出被内容过滤器拦截)。usage:token 用量统计。包含prompt_tokens、completion_tokens、total_tokens,以及更细粒度的字段,如completion_tokens_details.reasoning_tokens、prompt_tokens_details.cached_tokens。
SSE 流式响应
当请求中 stream 为 true 时,响应不再是单个 JSON 对象,而是一个 SSE 流。OpenAI 兼容接口的每个事件通常是:
text
data: {json}最后一个事件是:
text
data: [DONE]每个数据事件的 JSON 结构类似:
json
{
"id": "chatcmpl-xxx",
"object": "chat.completion.chunk",
"created": 1710000000,
"model": "your-model-id",
"choices": [
{
"index": 0,
"delta": { "content": "Paris" },
"finish_reason": null
}
]
}流式响应中的 delta 字段是增量内容。客户端需要把多次 chunk 的 delta.content 拼接起来,才能得到完整文本。最后一个 chunk 的 delta 通常为空,finish_reason 为 stop。
如果设置了 stream_options: { "include_usage": true },流式响应末尾会有一个 choices 为空的 chunk,并在该 chunk 中返回 usage 信息。
Anthropic Messages API 的流式格式与 OpenAI 不同,它使用带类型的 SSE 事件:
message_start:包含输入侧的 usage。content_block_delta:包含增量内容,其中text_delta是输出文本增量。message_delta:包含输出侧的 usage。error:表示流式请求出错。
因此,客户端解析流式响应时,不能假设所有 SSE 事件都是同一个格式,需要按 API 协议分别处理。
平台差异
OpenAI Chat Completions API 与 Anthropic Messages API 的关键差异:
| 项目 | OpenAI | Anthropic |
|---|---|---|
| 请求路径 | /v1/chat/completions | /v1/messages |
| 认证请求头 | Authorization: Bearer | x-api-key |
| 系统提示 | messages 数组中的 system 角色 | 顶层 system 字段 |
| 最大输出 token 字段 | max_tokens / max_completion_tokens | max_tokens |
| 流式响应格式 | 统一 data: 事件 | 带类型的 message_start、content_block_delta、message_delta |
两个协议在请求生命周期上一致,差异集中在协议表达层面。
基本用法
构造请求
Node.js 中可以用全局 fetch 构造请求:
javascript
const apiKey = process.env.OPENAI_API_KEY;
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: "your-model-id",
messages: [
{ role: "user", content: "Explain AJAX in one sentence." },
],
}),
});这个示例向 Chat Completions API 发送一个 POST 请求,请求体中的 model 指定模型 ID,messages 包含用户输入。返回的 response 是一个 Response 对象:
- 状态码为 2xx 时,可以调用
response.json()读取非流式 JSON,或通过response.body.getReader()读取流式 body。 - 状态码不是 2xx 时,需要从响应体中读取错误信息。
model 字段需要替换为服务商提供的具体模型 ID。
流式解析
流式响应需要逐段读取 HTTP body,而不是直接解析 JSON。下面是一个针对 OpenAI 兼容接口的解析示例:
javascript
const response = await fetch(url, options);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API error: ${response.status} ${errorText}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const events = buffer.split("\n\n");
buffer = events.pop();
for (const event of events) {
for (const line of event.split("\n")) {
if (!line.startsWith("data:")) continue;
const data = line.slice(5).trim();
if (data === "[DONE]") continue;
const chunk = JSON.parse(data);
const content = chunk.choices?.[0]?.delta?.content;
if (content) {
process.stdout.write(content);
}
}
}
}这个示例以空行作为 SSE 事件分隔符。每个事件内可能有多行 data:,解析时会把它们依次交给事件处理逻辑。对于 OpenAI 兼容接口,每个 chunk 通常是一行 data: {json},随后跟一个空行。
如果使用 Anthropic Messages API,解析逻辑需要根据 event 类型分发,例如从 content_block_delta 事件中的 delta.text_delta 提取文本增量。
超时控制
LLM 推理比普通 HTTP API 更耗时。非流式请求可能需要几十秒,流式请求持续时间更长。客户端需要设置合理的超时时间,避免连接无限挂起。
Node.js 中可以用 AbortController 终止 fetch 请求:
javascript
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 60_000);
try {
const response = await fetch(url, {
method: "POST",
headers: headers,
body: JSON.stringify(payload),
signal: controller.signal,
});
// 处理 response
} finally {
clearTimeout(timer);
}如果请求在生成过程中超时,服务端可能仍在执行推理。客户端需要意识到,超时后重试可能会重复产生 token 消耗。
错误处理与状态码
网络错误和 HTTP 错误需要分开处理。fetch 只在网络层失败时抛出异常;HTTP 状态码不是 2xx 时需要手动检查。
javascript
const response = await fetch(url, options);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API error: ${response.status} ${errorText}`);
}
const data = await response.json();服务端返回的错误体通常包含错误类型、错误消息和请求标识。客户端可以记录这些信息,便于排查。
HTTP 状态码对应生命周期中的不同环节,重试策略也不同:
400:请求参数缺失或格式错误,例如messages结构不合法或输入超出上下文窗口。不应重试。401:身份认证失败。API Key 缺失或无效。不应重试,应检查密钥是否正确。403:认证通过但权限不足,例如订阅计划不允许访问所请求的模型。不应重试。429:限流或配额不足。如果返回Retry-After请求头,应按其指定时间等待,否则可在指数退避延迟后重试。5xx:网关或推理服务内部错误。可以重试,但每一次重试都会重新计费并重复生成相同 token。- 客户端超时:如果请求仍在服务端执行,重试会产生重复的 token 消耗。
重试
对于限流错误、服务端错误和网络错误,可以在延迟后重试。重试次数和延迟策略应当避免对服务端造成额外压力。以下是一个简单的指数退避重试实现:
javascript
async function requestWithRetry(fn, maxRetries = 3) {
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
const delay = 2 ** attempt * 200 + Math.random() * 200;
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw lastError;
}requestWithRetry 接受一个返回 Promise 的函数,在失败后按 200 ms、400 ms、800 ms 的指数退避延迟重试,并加入随机抖动。重试仅适合 429、5xx 和网络错误;400、401、403 不应重试,因为重试无法改变结果。
自托管服务与托管 API
托管 API 和自托管推理服务在请求生命周期上相似,但对客户端和运维方的影响不同。
托管 API:
- 服务商负责 API 网关、限流、负载均衡、模型部署和监控。
- 客户端只需要处理标准 HTTP 请求和响应。
- 服务商可能提供附加请求头或参数,例如 Anthropic 的
anthropic-version。 - 提示缓存、连续批处理等优化在服务端自动生效,调用方可以通过 usage 字段感知部分缓存效果。
自托管推理服务:
- 使用 vLLM、TGI 等推理引擎自行部署模型。这些引擎通常提供 OpenAI 兼容的 HTTP 接口。
- 需要自行为接口增加鉴权、限流和日志记录等能力。
- 可以选择开启或关闭连续批处理、提示缓存、推测解码等优化。
- 因为网络链路更短,请求延迟可能更低,但代价是需要自己管理 GPU 资源和处理故障。
无论是托管 API 还是自托管,客户端看到的请求构造方式、流式解析方式和错误处理流程都保持一致。区别主要体现在服务端组件和运维责任上。
