面试知识库
极高 基础

LLM API调用与流式输出#

一句话答案#

LLM API 本质是一个无状态的 HTTP 接口:每次把完整 messages(system/user/assistant/tool)连同 model、temperature、tools 等参数发过去,拿回 choices + finish_reason + usage;流式模式把一次响应拆成 SSE 的 data: 增量 chunk,客户端按 delta 拼接、以 [DONE] 结束。工程上真正拉开差距的是 token 预算管理(计数、裁剪历史)和可靠性(超时、429/5xx 退避重试、限流、多供应商兼容)。

核心要点

1. 消息结构与无状态#

Chat Completions 风格(OpenAI 定义、业界事实标准)的请求核心是 messages 数组,每条有 role + content:

role作用备注
system设定身份/规则,通常放第一条OpenAI 新模型用 developer 角色,语义相同;Responses API 改用顶层 instructions;Anthropic 的 system 是顶层参数,不在 messages 里
user用户输入,可含多模态(text + image_url)
assistant模型历史回复;含 tool_calls 时 content 可为空多轮时由客户端回填
tool工具执行结果,必须带 tool_call_id 与上一条 assistant 的调用配对见 [Function Calling与工具编排](/topics/ai-agent/Function Calling与工具编排)

服务端不保存会话:模型每次只”看到”本次请求里的 messages,所以多轮对话必须把历史全量重发——这就是为什么上下文越聊越长、越聊越贵,也是为什么 prompt caching 能省钱(前缀相同的部分命中缓存,usage.prompt_tokens_details.cached_tokens 会体现)。OpenAI 现在给新项目推荐的是 Responses API(Chat Completions 仍受支持;Assistants API 已于 2026-08-26 下线):请求用 input + instructions,输出是按类型区分的 item 数组(message、reasoning、function_call 等),可以用 previous_response_id 或 Conversations API 让服务端保存状态,响应默认会被存储(store: false 关闭)。即便如此,Chat Completions 形态仍是各家 OpenAI 兼容端点的公共格式,面试默认按无状态理解。

2. 核心参数(只点名,细节见 解码策略与采样参数)#

  • model:决定能力、价格、上下文窗口;temperature / top_p:随机性,官方建议二选一调;max_completion_tokens(旧参数 max_tokens 已标废弃、不兼容 o 系列推理模型):输出上限,包含推理 token,注意它和上下文窗口是两回事;stop:停止序列;n / seed:多候选与可复现;tools / tool_choice:工具声明与强制调用;response_format:json_object 或 json_schema(见 结构化输出与约束解码);stream / stream_options:是否流式、流式是否返回 usage;safety_identifier / prompt_cache_key:旧的 user 字段已标废弃,拆成这两个——前者是终端用户标识(建议传哈希值),供厂商做滥用追踪,后者用于提高缓存命中和按用户分开计缓存(见 [前缀缓存与KV Cache复用](/topics/llm-serving/前缀缓存与KV Cache复用))。

3. 返回结构与 finish_reason#

{ "id": "...", "model": "...",
  "choices": [{ "index": 0, "message": { "role": "assistant", "content": "...", "tool_calls": [...] }, "finish_reason": "stop" }],
  "usage": { "prompt_tokens": 120, "completion_tokens": 80, "total_tokens": 200,
             "prompt_tokens_details": { "cached_tokens": 64 }, "completion_tokens_details": { "reasoning_tokens": 0 } } }
plaintext

finish_reason 是代码必须分支处理的字段:stop(自然结束或命中 stop 序列)、length(撞到 max_tokens 或上下文上限,答案被截断——要么加大 max_tokens,要么续写)、tool_calls(模型要调工具,content 通常为空,进入工具执行循环)、content_filter(被厂商安全策略拦截)。Anthropic 对应字段叫 stop_reason:end_turn / max_tokens / stop_sequence / tool_use 与上面基本对应,另外还有 refusal(安全策略拒答)、pause_turn(服务端工具循环到了迭代上限,需要把响应原样发回继续)、model_context_window_exceeded(撞到上下文窗口);所有 stop_reason 都随 HTTP 200 正常返回,不是错误。usage 用来计费、做限流和成本归因(见 AI应用成本优化),cached_tokens 单价通常只有正常输入的几分之一,是长 system prompt 场景的主要省钱点。

4. 流式输出的底层#

stream=true 时响应头是 Content-Type: text/event-stream,走 SSE:

data: {"object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"你好"},"finish_reason":null}]}
data: {"choices":[{"index":0,"delta":{"content":","},"finish_reason":null}]}
data: {"choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: {"choices":[],"usage":{"prompt_tokens":12,"completion_tokens":5,"total_tokens":17}}   ← 仅 stream_options.include_usage=true
data: [DONE]
plaintext
  • 每帧是 data: <json>\n\n,对象类型变成 chat.completion.chunk,message 变成 delta(增量);客户端按 choices[i].index 把 delta.content 字符串追加即可
  • finish_reason 只在最后一个内容帧非 null,usage 只在末尾单独一帧(且 choices 为空)——所以流式下”用了多少 token”要等流结束才知道,客户端中途断开就拿不到
  • 工具调用也是增量的:delta.tool_calls[i] 第一帧给 id + function.name,后续帧 function.arguments 是 JSON 字符串碎片,按 index 拼完整后才能 parse
  • 其他格式:OpenAI Responses API 的流式是带类型的语义事件,如 response.created → 多个 response.output_text.delta / response.function_call_arguments.delta → response.completed,出错为 error 事件;Anthropic 是 event: + data: 成对出现,顺序为 message_start → 每个内容块 content_block_start / 若干 content_block_delta(text_delta、input_json_delta、thinking_delta)/ content_block_stop → message_delta(带 stop_reason 和累计 usage)→ message_stop,中间可能夹 ping。解析器要按事件类型分支,不能假设只有一种 chunk 格式
  • 流式的价值是把感知延迟从总时长降到首 token 时间(TTFT);前后端怎么传(SSE vs WebSocket、Nginx 缓冲、事件协议设计)属于应用层,见 流式输出与实时交互

5. Token 计数与上下文预算#

  • 计数:用与模型配套的 tokenizer 离线估算——OpenAI 用 tiktoken(GPT-4o、GPT-4.1、GPT-5、o 系列是 o200k_base,GPT-4/3.5 是 cl100k_base,新模型以 tiktoken 的模型映射表为准);Anthropic 等不公开 tokenizer 的厂商用官方 token 计数接口;Java 可用 jtokkit;国产模型用厂商提供的 tokenizer 或计数接口。每条 message 有几个 token 的格式开销,汉字通常比英文更”贵”且各 tokenizer 差异大(见 Tokenizer与分词原理),估算留 10–20% 余量
  • 预算关系:prompt_tokens + max_completion_tokens ≤ context_window,超出直接 400(context_length_exceeded),不会自动截断
  • 裁剪历史:固定保留 system + 最近 N 轮(滑动窗口);更长对话用”旧历史摘要 + 近几轮原文”;RAG 场景先压缩检索内容。裁剪时成对删除 user/assistant,且 tool 消息不能与对应 assistant 的 tool_calls 拆开,否则 400。工程化的记忆管理见 Agent记忆与上下文工程

6. 可靠性工程#

问题做法
超时连接超时短(几秒)、读超时按输出长度给(非流式几十秒到几分钟);流式改用帧间空闲超时而非总超时
429 / 5xx / 连接重置指数退避 + 抖动重试(如 1s→2s→4s,上限 3–5 次),优先遵守 Retry-After;400/401/403 不重试
限流厂商按 RPM(请求/分钟)和 TPM(token/分钟)双维度限;客户端用令牌桶/信号量限并发,批量任务排队;大流量申请提额或多 Key 分摊
幂等LLM 调用天然不幂等:重试可能重复计费、更糟的是重复执行有副作用的工具;应用层用 request_id 去重,工具侧做幂等
Key 管理只在服务端持有,走密钥管理/环境变量,前端一律经后端代理;按环境/服务拆 Key 便于审计与轮换
多供应商DashScope(通义)、DeepSeek、Moonshot、vLLM/Ollama 等都提供 OpenAI-compatible 端点,切 base_url + api_key 即可换模型;但 tools、response_format、stream_options 等细节支持度不一,要做能力探测;大规模用 LiteLLM/New API/Higress 这类网关统一入口并做故障转移。Java 侧 Spring AI 的 ChatClient(.call() / .stream() 返回 Flux)或官方 OpenAI Java SDK 已封装以上细节,见 [Spring AI核心概念与架构](/topics/ai-agent/Spring AI核心概念与架构)

面试回答(2分钟版)

LLM API 的基本形态是 Chat Completions:一个无状态 HTTP 接口,请求核心是 messages 数组,角色有 system、user、assistant、tool。服务端不存会话,多轮每次都要全量重发历史,这是上下文越聊越贵的根源,也是 prompt caching 能省钱的原因。参数主要是 model、temperature/top_p、max_completion_tokens(旧名 max_tokens)、stop、tools、response_format。OpenAI 现在给新项目推荐 Responses API,可以让服务端存状态,但各家兼容端点的公共格式仍是 Chat Completions。返回里最要紧的是 finish_reason:stop 正常结束,length 被截断,tool_calls 进工具循环,content_filter 被拦;usage 的 prompt/completion/cached tokens 用来计费。流式是 stream=true 走 SSE,每帧 data: 一个 chunk,message 变成 delta,按 index 拼 content,finish_reason 在最后一个内容帧,usage 要开 include_usage 才在末尾单独一帧,最后 [DONE];工具参数也是 JSON 碎片,拼完才能 parse。工程上第一管 token 预算:tiktoken 估算,prompt 加 max_tokens 超窗口直接 400,要滑动窗口或摘要裁剪历史。第二可靠性:429 和 5xx 指数退避加抖动重试、遵守 Retry-After,400 不重试;限流分 RPM 和 TPM;重试要考虑幂等,别把有副作用的工具跑两遍。第三 Key 只放服务端,多供应商用 OpenAI-compatible 端点切 base_url,DashScope、DeepSeek 都兼容,大规模上网关统一。

追问与易错

追问方向:

  • 为什么每轮都要把历史全发?不能只发新消息吗? → Chat Completions 是无状态的,服务端不存上下文,模型只看本次请求;要”只发增量”得用带服务端状态的接口(如 Responses API 的 previous_response_id)或自己在应用层存历史。全量重发的代价靠 prompt caching 摊薄,命中部分按 cached_tokens 折扣计费
  • 流式模式下怎么拿到 token 用量? → 请求里加 stream_options: {"include_usage": true},usage 会在最后一帧单独返回(choices 为空);客户端中途断开拿不到,只能用 tokenizer 估算已收到的内容
  • 429 了怎么办?是不是所有错误都重试? → 429 和 5xx、连接重置做指数退避 + 抖动重试,有 Retry-After 就照它来;400(参数错/上下文超限)、401/403 重试没意义。还要区分 429 是 RPM/TPM 限流还是额度用尽
  • 国内模型怎么接?换厂商要改多少代码? → 通义 DashScope、DeepSeek 等都提供 OpenAI 兼容端点,SDK 改 base_url 和 key 即可;差异在 tools/response_format/流式 usage 等细节支持度,要做能力探测和回退,生产建议网关统一

易错点:

  • ❌ “流式每一帧都有 usage 和 finish_reason” → finish_reason 只在最后一个内容帧非 null,usage 只在开启 include_usage 后的末尾单独一帧
  • ❌ “上下文超了模型会自动截断旧的” → 直接报 400,裁剪历史是客户端的责任
  • ❌ “重试是安全的,多试几次就行” → 重试会重复计费,更危险的是重复执行有副作用的工具调用,需要 request_id 去重与工具幂等