Transformer 与推理基础Transformer 与推理基础
从注意力与自回归解码出发,理解 Token 预算、KV Cache、采样、延迟指标与 Java 推理网关设计。从注意力与自回归解码出发,理解 Token 预算、KV Cache、采样、延迟指标与 Java 推理网关设计。
专题导读
Java 后端工程师不需要从零训练大模型,但必须理解推理服务为何不像普通 JSON RPC:输入长度会显著影响首 token 延迟,输出按 token 逐步生成,流式连接会长期占用资源,采样结果也不具备数据库查询式的确定性。
本文主要讨论当前大语言模型常见的 decoder-only 自回归 Transformer。Transformer 还包括 encoder-only(如用于表示学习的 BERT 类模型)和 encoder-decoder(如原始 Transformer、T5 类模型);不能把“预测下一个 token”当作所有 Transformer 的统一工作方式。
建议沿着一条请求链理解:
文本 → tokenizer → token ids → embedding/位置表示 → 多层 Transformer → logits → 解码策略 → 新 token → 重复解码 → 文本
后端系统真正要控制的是:上下文与输出预算、排队与取消、首 token 延迟、逐 token 吞吐、失败原因、成本和结果校验。
知识地图
| 层次 | 核心问题 | Java 后端关注点 |
|---|---|---|
| 模型结构 | 注意力、MLP、残差、归一化如何形成 logits | 明确能力边界,不把注意力权重当事实依据 |
| 输入表示 | 文本如何变成 token,窗口如何计量 | 使用目标 tokenizer 估算,给输出和协议开销留余量 |
| 推理阶段 | prefill 与 decode 有何差异 | 分开记录 TTFT、TPOT、总时延与排队时间 |
| 状态缓存 | KV Cache 节省了什么、占用什么 | 长上下文和高并发会争抢设备内存 |
| 解码策略 | temperature、top-k、top-p 如何改变分布 | 低温不代表正确或绝对可复现 |
| 服务治理 | 批处理、限流、超时、取消、背压 | 按 token 而非只按请求数估算负载 |
| 可靠性 | 截断、拒答、过滤、网络中断如何处理 | 检查 finish reason,不能把半截输出当成功 |
面试题详解
Q1:Transformer 是否都在做“下一个 token 预测”?
**核心回答:**不是。Transformer 是一类基于注意力的网络架构;“根据已有 token 自回归地产生下一个 token”主要描述 decoder-only 生成模型,以及 encoder-decoder 模型的解码阶段。encoder-only 模型通常一次处理双向上下文,用于分类、检索或表示学习。
原始 Transformer 由 encoder 和 decoder 组成。encoder 使用自注意力处理源序列;decoder 使用带因果掩码的自注意力,并通过交叉注意力读取 encoder 输出。decoder-only LLM 去掉独立 encoder,通过因果掩码保证位置 i 不能读取未来位置,训练目标通常是最大化序列条件概率:
P(x₁, …, xₙ) = ∏ P(xᵢ | x₁, …, xᵢ₋₁)
推理时,模型对词表产生 logits,经解码策略选出一个 token,再把它加入上下文继续计算。模型直接优化的是 token 条件分布,不是“事实正确率”或“业务规则满足率”。
面试追问:为什么聊天模型看起来能遵循指令? 预训练提供语言与知识能力,后训练阶段再通过指令数据、偏好优化等方式改变行为分布;但这些训练不会把模型变成确定性规则引擎。
常见错误回答:“Transformer 就是 decoder-only”“每层直接输出自然语言”。前者忽略 encoder-only 和 encoder-decoder;后者忽略最终 logits、解码和 tokenizer。
Q2:缩放点积注意力到底在计算什么?
**核心回答:**单头注意力可写为 Attention(Q,K,V) = softmax(QKᵀ / √dₖ)V。它先计算 query 与 key 的相似度,再将归一化权重作用于 value,从已有表示中聚合与当前位置相关的信息。
输入隐藏状态分别乘以可学习矩阵得到 Q、K、V。除以 √dₖ 是为了在维度较大时抑制点积方差,避免 softmax 过度饱和。多头注意力在多个投影子空间并行计算后拼接,使不同头可以学习不同关系;它并不保证每个头都具有可解释的人类语义。
模型还需要位置信息,否则纯注意力对输入排列缺少顺序感。具体实现可能使用绝对位置嵌入、旋转位置编码等,行为取决于模型架构。
标准全序列自注意力需要形成长度为 n 的 token 间关系,算术复杂度中的注意力项通常为 O(n²d),注意力矩阵空间为 O(n²);线性投影和 MLP 还包含与隐藏维度有关的成本。FlashAttention 一类方法通过分块和 IO 感知计算减少中间读写与显存占用,但不应被笼统描述成把标准注意力算术降为 O(n)。
面试追问:注意力权重能否作为引用证据? 不能。它是模型内部数值计算,不等同于数据库检索结果、来源归因或因果解释;引用必须由检索来源和应用层校验支撑。
常见错误回答:“attention 就是在知识库里搜索”“FlashAttention 消除了长上下文的二次成本”。
Q3:token 是什么,为什么字符数不能直接代替 token 数?
**核心回答:**token 是特定 tokenizer 的编码单位,可能是完整单词、子词、单个字符、字节片段或特殊控制符。不同模型的词表和切分规则不同,同一文本会得到不同 token 数。
聊天请求还可能包含角色标记、消息包装、工具定义、JSON Schema 和供应商内部特殊 token。Java 网关可以用目标模型公开的 tokenizer 做预算估算,但账单与硬限制应以服务端返回的 usage 为准;供应商还可能单独统计缓存 token、推理 token 或其他类别。
上下文预算至少满足:
系统/开发者指令 + 历史 + 当前请求 + RAG 证据 + 工具定义 + 协议开销 + 预留输出 ≤ 模型窗口
裁剪时应按消息或文档块等原子单元处理,不能简单截断 UTF-16 字符串,否则可能破坏 JSON、代码、引用边界,甚至切开代理对。
面试追问:没有官方 tokenizer 怎么办? 使用保守估算和安全余量,在预生产环境用真实 API 校准估算误差;接近硬上限时拒绝、摘要或减少证据,而不是赌供应商接受。
常见错误回答:“一个汉字就是一个 token”“本地估算值一定等于计费值”。
Q4:prefill 和 decode 有什么本质差异?
**核心回答:**prefill 一次处理已有输入并建立各层 KV Cache,输入 token 间计算可高度并行;decode 每一步只新增一个 token,读取历史 KV,存在严格的逐步依赖,难以在单条序列内部并行生成多个普通 token。
| 维度 | Prefill | Decode |
|---|---|---|
| 输入 | 完整 prompt | 上一步产生的新 token |
| 并行性 | token 维度较高 | 单序列逐步依赖 |
| 主要压力 | 长序列计算、显存带宽 | 反复读取权重与 KV Cache |
| 用户指标 | TTFT 的重要组成 | TPOT/ITL、输出 tokens/s |
| 常见优化 | prefix cache、分块 prefill、FlashAttention | continuous batching、量化、speculative decoding |
TTFT(time to first token)还包含排队、网络、tokenization、调度、冷启动等,不能只归因于 prefill。TPOT(time per output token)或 ITL(inter-token latency)反映流式输出节奏,但客户端观测值还会受网络缓冲影响。
面试追问:为什么输入很短仍可能 TTFT 高? 队列拥塞、实例冷启动、跨区网络、批调度等待或供应商限流都可能主导。
常见错误回答:“prefill 生成第一个 token,decode 一次生成全部剩余文本”“TTFT 完全由 prompt 长度决定”。
Q5:KV Cache 节省了什么,代价是什么?
**核心回答:**自回归解码时,历史 token 在每层得到的 K、V 不再变化。缓存它们可以避免每一步重新计算历史 token 的 K/V 投影;当前 query 仍需与历史 key 计算注意力,并读取对应 value,因此缓存不是零成本。
忽略对齐、分页和实现元数据时,单条序列 KV Cache 的近似字节数为:
2 × layers × sequenceLength × numKvHeads × headDim × bytesPerElement
其中 2 表示 K 和 V。实际容量还要乘批大小或并发序列数。MHA 通常每个 query head 有对应 KV head;GQA 让多组 query head 共享较少 KV head;MQA 进一步共享,因此 numKvHeads 不能总写成 attention heads。
对带 KV Cache 的单步 decode,当前 token 对长度为 n 的历史执行注意力,其相关工作量近似随 n 线性增长;连续生成 m 个 token 时还会逐步扩张。长窗口因此同时增加 prefill 计算、KV 内存和后续 decode 读取成本。
面试追问:prefix cache 与 KV Cache 是一回事吗? KV Cache 是单次序列解码状态;prefix cache 通常指跨请求复用相同前缀对应的缓存,命中条件、隔离和生命周期由推理服务实现。跨租户复用必须避免侧信道或数据泄漏。
常见错误回答:“有 KV Cache 后 decode 与上下文长度无关”“KV Cache 只占主机内存”。具体放置取决于实现,常见瓶颈是加速器内存。
Q6:上下文窗口越大,回答一定越好吗?
**核心回答:**不一定。更大窗口只是允许提交更多 token,不保证模型等量使用所有信息。无关、重复、冲突或位置不佳的内容会增加成本并可能降低质量。
工程上要同时考虑:
- 硬限制:输入与最大输出通常共享窗口,但精确定义以目标 API 为准。
- 质量边界:长上下文可能出现信息被忽略、冲突证据选择错误等现象。
- 性能边界:长输入提高 prefill 与 KV Cache 成本。
- 治理边界:不能因为窗口足够就绕过 ACL,把用户无权访问的数据塞给模型。
后端应先鉴权,再去重、排序、按原子块裁剪,为输出留显式预算,并记录被丢弃的来源。高风险事实应能回源,而非只保留生成式摘要。
面试追问:长上下文能替代 RAG 吗? 通常不能。RAG 解决知识选择、更新、来源和权限问题;长窗口只是容量能力。具体取舍需用任务评测验证。
常见错误回答:“窗口 128K 就应把所有历史都传入”“超限时从字符串头部直接删除一半”。
Q7:temperature、top-k、top-p 分别做什么?
**核心回答:**它们改变候选 token 的采样集合或概率分布,不改变模型已学到的事实,也不能替代业务校验。
- temperature:通常将 logits 除以温度后再 softmax。较低温度使分布更尖锐,较高温度使分布更平坦;具体参数边界由 API 定义。
- top-k:只保留概率最高的
k个候选,再归一化。 - top-p:按概率从高到低选择累计概率达到阈值的最小候选集合,再归一化;常称 nucleus sampling。
供应商对这些参数能否组合、处理顺序、默认值和极端值语义可能不同。抽取、分类、工具参数生成通常使用较低随机性;创意任务可提高多样性,但应通过评测而非经验常数决定。
即使 temperature=0 被 API 解释为贪心或近似贪心,也不能承诺绝对确定:模型版本、服务端路由、浮点并行归约、相同/极近 logits、隐藏提示和供应商实现都可能变化。seed 若受支持,也通常只是“提高可复现性”,不是跨版本保证。
面试追问:低温为什么仍会幻觉? 温度只影响从当前分布如何选 token;如果分布本身基于错误知识或缺失证据,选择最高概率项仍可能错误。
常见错误回答:“temperature=0 保证每次字节级相同”“top-p=0.9 就保留概率大于 0.9 的 token”。
Q8:推理请求为什么不能只看 HTTP 200?
**核心回答:**HTTP 成功只说明传输层/API 调用完成,不表示生成了完整、可用的业务结果。调用方还要检查供应商定义的完成原因、拒答、内容过滤、长度截断、工具调用状态和 usage。
典型终止状态包括:正常停止、命中 stop 序列、达到最大输出 token、模型拒绝、内容策略拦截、客户端取消、服务端超时、流中断。若 JSON 在长度限制处被截断,即使前半段可见,也不能执行副作用。
应用层应把响应建模为明确状态机:
RECEIVING → COMPLETED | TRUNCATED | REFUSED | FILTERED | CANCELLED | FAILED
只有 COMPLETED 且通过结构与业务校验后,结果才能进入下一步。供应商字段名并不统一,应由适配器映射为内部枚举。
面试追问:stop 序列能否阻止敏感内容? 不能。它只是生成终止机制,可能被 tokenization 或上下文变化影响,也不会撤回终止前已产生的内容。
常见错误回答:“只要流里收到字符就是成功”“max output tokens 是期望长度而非硬上限”。
Q9:TTFT、TPOT、吞吐和总延迟如何一起看?
**核心回答:**它们分别描述不同瓶颈,不能只保留一个总耗时。
- 排队时间:进入系统到模型开始处理;高值通常反映容量或调度问题。
- TTFT:客户端发起到收到首个有效 token;影响交互感受。
- TPOT/ITL:首 token 之后相邻 token 的平均/分位间隔;反映 decode 流畅度。
- 总延迟:直到完成、失败或取消。
- 吞吐:单位时间处理的输入/输出 token 或完成请求;只看 QPS 会忽略请求大小差异。
从客户端流式观测看,若输出共有 m 个 token,可用下面的近似式理解:
总时延 ≈ TTFT + (m - 1) × 平均 ITL
其中 TTFT 已包含首 token 之前的网络、排队、tokenization、调度、prefill 与首次 decode;ITL/TPOT 描述首 token 之后的生成间隔。若做服务端拆解,则应分别记录这些阶段,不能再把 TTFT 与 prefill 重复相加。这不是硬公式:连续批处理会让请求相互影响,speculative decoding 也可能一次接受多个草稿 token。监控至少按模型、区域、输入长度桶、输出长度桶和优先级分组,并记录 P50/P95/P99。
面试追问:如何优化 P95 TTFT 而不只优化平均值? 做交互/离线队列隔离、限制大 prompt、预热容量、设置排队截止时间,并观察批处理等待带来的尾延迟。
常见错误回答:“tokens/s 越高所有用户越快”“只用 QPS 做容量规划”。
Q10:动态批处理为什么提高吞吐却可能伤害延迟?
**核心回答:**批处理将多个序列放到同一轮设备计算中,提高矩阵运算利用率和权重读取复用;但等待凑批会增加排队,长短序列混合还会影响调度公平性和尾延迟。
静态批处理要求请求形状接近;continuous batching 会在运行中的批次里动态加入新请求、移除完成请求,更适合长度不一的在线生成。后端网关不应自行猜测设备批策略,但要提供优先级、截止时间、输入/输出上限和取消信号。
容量治理不能只有并发数。一个 200-token 分类请求和一个 50K 输入、4K 输出请求对 KV Cache 与占用时间的影响完全不同。更合理的是组合约束:并发槽位、每用户 token 速率、最大在途估算 token、超长请求独立队列。
面试追问:交互与离线任务如何隔离? 使用独立队列/配额,必要时独立模型池;交互请求优化 TTFT,离线任务可接受更大批和更长等待。
常见错误回答:“batch 越大越好”“网关的固定线程池大小可以直接等于 GPU 最大 batch”。
Q11:流式输出在 Java 服务中有哪些背压与取消问题?
**核心回答:**流式响应延长连接生命周期。若客户端读取慢或已断开,而服务端继续从模型拉取并缓冲 token,会占用内存、连接和模型配额;必须把取消向上游传播,并设置首 token 与整体截止时间。
需要区分:
- 首 token 超时:尚未向用户产生有效内容,可安全切换备用模型或返回错误。
- 空闲超时:流已开始但长时间无事件,需要关闭并标记不完整。
- 整体截止时间:防止超长生成无限占用资源。
- 客户端断开:尽快取消上游请求;是否立即停止计费取决于供应商实现。
- 背压:限制每连接缓冲区,不能无限累积;响应式框架也必须确认上游是否真正支持取消。
若已经向客户端发送部分自然语言,再透明切换模型可能造成语义重复或矛盾。结构化、副作用任务更不应消费半截流。
面试追问:重试应复用原 prompt 吗? 要按失败分类。网络未开始生成可有限重试;已经产生输出或工具副作用时,先查询状态并依赖幂等设计,不能盲重试。
常见错误回答:“SSE 自带背压和取消传播”“客户端断开后上游一定自动停止”。
Q12:推理优化技术如何判断适用范围?
**核心回答:**先定位瓶颈,再判断优化位于模型服务端还是应用网关。量化、FlashAttention、continuous batching、tensor parallelism 通常是推理平台职责;上下文裁剪、限流、缓存隔离、超时、结果校验是 Java 应用职责。
- 量化:用更低精度表示权重或激活,可降低内存/带宽并提升吞吐,但质量、硬件支持和实际速度要实测。
- speculative decoding:草稿模型先提出多个 token,目标模型并行验证;接受率高时可减少串行步骤,同时保持目标分布的算法需满足特定采样校正条件。
- prefix caching:复用完全相同或符合实现规则的稳定前缀;动态用户数据放在前缀会降低命中,缓存键和租户隔离不可省略。
- 并行化:可扩大可服务模型或吞吐,但跨设备通信也会增加延迟。
后端面试中不应声称某技术“必然提速”。应给出基线、目标指标、输入/输出分布、硬件和质量回归,再做灰度比较。
面试追问:为什么 speculative decoding 不一定有效? 草稿模型与目标模型分布差异大时接受率低,验证和草稿计算会抵消收益;短输出也可能不值得。
常见错误回答:“量化只省内存不影响质量”“prefix cache 可以跨所有用户无条件共享”。
Java 21 完整示例:预算、限流与截止时间
下面示例只使用 Java 21 标准库。外部模型 API 被抽象为 ModelClient 接口;示例关注网关职责,不假设任何供应商 SDK、finish reason 字段或 tokenizer 实现。
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.Semaphore;
import java.util.concurrent.StructuredTaskScope;
public final class InferenceGatewayDemo {
public record PromptPart(String role, String content, int estimatedTokens) {
public PromptPart {
Objects.requireNonNull(role);
Objects.requireNonNull(content);
if (estimatedTokens < 0) throw new IllegalArgumentException("estimatedTokens < 0");
}
}
public record ModelRequest(List<PromptPart> parts, int maxOutputTokens) {
public ModelRequest {
parts = List.copyOf(parts);
if (maxOutputTokens <= 0) throw new IllegalArgumentException("maxOutputTokens <= 0");
}
}
public enum FinishReason { STOP, LENGTH, REFUSED, FILTERED, CANCELLED, FAILED }
public record ModelResponse(String text, int inputTokens, int outputTokens,
FinishReason finishReason) {
public ModelResponse {
Objects.requireNonNull(text);
Objects.requireNonNull(finishReason);
}
}
/** 由 HTTP/SSE 适配器实现;必须把 Thread.interrupt 或取消映射到上游请求。 */
public interface ModelClient {
ModelResponse generate(ModelRequest request) throws Exception;
}
public static final class InferenceGateway {
private final ModelClient client;
private final Semaphore concurrentSlots;
private final int contextWindow;
private final int protocolReserve;
public InferenceGateway(ModelClient client, int maxConcurrency,
int contextWindow, int protocolReserve) {
this.client = Objects.requireNonNull(client);
this.concurrentSlots = new Semaphore(maxConcurrency, true);
this.contextWindow = contextWindow;
this.protocolReserve = protocolReserve;
}
public ModelResponse generate(ModelRequest request, Duration deadline) throws Exception {
validateBudget(request);
if (!concurrentSlots.tryAcquire()) {
throw new IllegalStateException("推理并发已满,请稍后重试");
}
try {
Instant expiresAt = Instant.now().plus(deadline);
Callable<ModelResponse> call = () -> client.generate(request);
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var task = scope.fork(call);
scope.joinUntil(expiresAt);
scope.throwIfFailed();
ModelResponse response = task.get();
if (response.finishReason() != FinishReason.STOP) {
throw new IllegalStateException(
"模型未正常完成: " + response.finishReason());
}
return response;
}
} finally {
concurrentSlots.release();
}
}
private void validateBudget(ModelRequest request) {
long inputEstimate = request.parts().stream()
.mapToLong(PromptPart::estimatedTokens)
.sum();
long total = inputEstimate + request.maxOutputTokens() + protocolReserve;
if (total > contextWindow) {
throw new IllegalArgumentException(
"上下文预算超限: estimated=" + total + ", window=" + contextWindow);
}
}
}
public static void main(String[] args) throws Exception {
ModelClient fakeClient = request ->
new ModelResponse("KV Cache 以显存换重复计算。", 18, 12, FinishReason.STOP);
var gateway = new InferenceGateway(fakeClient, 32, 8_192, 256);
var request = new ModelRequest(List.of(
new PromptPart("system", "你是 Java 后端面试助手。", 12),
new PromptPart("user", "解释 KV Cache。", 6)
), 256);
ModelResponse response = gateway.generate(request, Duration.ofSeconds(3));
System.out.println(response.text());
}
}
StructuredTaskScope在 Java 21 中是预览 API,编译和运行需加--enable-preview --release 21。不采用预览特性时,可改用ExecutorService+Future.get(timeout),但仍要确保取消真正传播到 HTTP 客户端。
**复杂度与成本模型:**预算校验遍历 p 个 prompt part,时间复杂度 O(p)、额外空间 O(1);信号量只限制并发槽位,不等价于 token 公平调度。真实网关还应增加租户级 token bucket、最大在途 token、排队截止时间、实际 usage 对账与流式背压。示例将非 STOP 一律拒绝是保守策略;生产中应根据内部状态机细分是否允许展示拒答文本、是否可重试以及是否进入人工流程。
场景设计题
场景:设计一个面向企业知识问答的 Java 推理网关
需求:支持 SSE 流式回答;模型窗口为可配置值;交互请求 P95 TTFT 有目标;同一租户有预算;离线总结不能拖慢在线问答;供应商可能限流、截断或返回拒答。
推荐回答框架:
- 请求准入:认证租户;限制正文、附件、历史轮数;用目标 tokenizer 或保守估算计算输入与预留输出。
- 队列隔离:在线与离线使用独立队列和配额;超长输入进入专用队列或提前拒绝。
- 并发治理:同时限制请求槽位、租户 token 速率和最大在途估算 token;重试消耗也计入预算。
- 调用适配:供应商差异收敛为内部 request/response 与 finish reason;保留 model id、参数、prompt version 和 usage。
- 流式生命周期:首 token、空闲、整体三类超时;客户端断开后取消上游;设置有界缓冲。
- 失败处理:未开始输出的瞬时故障可有限退避重试;已输出内容不透明切换;截断或过滤不得进入副作用。
- 可观测性:记录队列、TTFT、TPOT、输入/输出 token、取消率、截断率、各状态码、模型与长度桶;内容日志需脱敏并受访问控制。
- 容量评估:用真实输入/输出分布压测,不用平均 QPS 代替 token 吞吐和 KV 容量;分别验证 P95/P99 与成本。
**加分点:**指出模型服务端的 continuous batching 与应用网关队列是两层调度;切换模型可能改变 tokenizer、窗口、输出质量与合规行为,不能只替换 URL。
速记总结
- Transformer 是架构族,不是全部等同于 decoder-only;本文的下一 token 生成结论限定于自回归解码。
- 注意力聚合表示,不是事实检索或可审计引用;标准全序列注意力项随长度近似二次增长。
- prefill 处理已有上下文,decode 逐 token 生成;TTFT 与 TPOT 要分开观测。
- KV Cache 用内存换重复计算;容量取决于 K/V、层数、序列、KV heads、head dimension、精度和并发。
- token 数由目标 tokenizer 与协议包装决定;字符数只能用于粗估,实际计费看服务端 usage。
- 大窗口不是“全部塞入”的理由;鉴权、选择、去重、排序和输出预留仍然必要。
- temperature、top-k、top-p 控制采样,不保证正确;
temperature=0也不是绝对确定性承诺。 - HTTP 200 不等于生成成功;必须识别正常停止、截断、拒答、过滤、取消和流中断。
- 批处理提高吞吐但可能增加尾延迟;容量规划应看 token、序列长度和在途状态,而不只看 QPS。
- Java 网关负责预算、限流、超时、取消、背压、状态映射和校验;底层算子优化通常属于推理平台。
参考资料
- Attention Is All You Need(Transformer 原论文)
- BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding
- Language Models are Few-Shot Learners(GPT-3)
- FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness
- Fast Inference from Transformers via Speculative Decoding
- Hugging Face Transformers:Cache strategies
- Hugging Face Transformers:Generation strategies
- Java 21 API:StructuredTaskScope