知识库AgentLLM 与上下文LLM 与上下文Transformer 与推理基础Transformer 与推理基础
01 · LLM 与上下文LLM 与上下文
Roadmap 01基础Markdown58 min

Transformer 与推理基础Transformer 与推理基础

从注意力与自回归解码出发,理解 Token 预算、KV Cache、采样、延迟指标与 Java 推理网关设计。从注意力与自回归解码出发,理解 Token 预算、KV Cache、采样、延迟指标与 Java 推理网关设计。

#TransformerTransformer#LLM 推理LLM 推理#KV CacheKV Cache#TokenToken#Java 网关Java 网关更新于 2026-08-16

专题导读

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,不保证模型等量使用所有信息。无关、重复、冲突或位置不佳的内容会增加成本并可能降低质量。

工程上要同时考虑:

  1. 硬限制:输入与最大输出通常共享窗口,但精确定义以目标 API 为准。
  2. 质量边界:长上下文可能出现信息被忽略、冲突证据选择错误等现象。
  3. 性能边界:长输入提高 prefill 与 KV Cache 成本。
  4. 治理边界:不能因为窗口足够就绕过 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 实现。

JAVA
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 有目标;同一租户有预算;离线总结不能拖慢在线问答;供应商可能限流、截断或返回拒答。

推荐回答框架:

  1. 请求准入:认证租户;限制正文、附件、历史轮数;用目标 tokenizer 或保守估算计算输入与预留输出。
  2. 队列隔离:在线与离线使用独立队列和配额;超长输入进入专用队列或提前拒绝。
  3. 并发治理:同时限制请求槽位、租户 token 速率和最大在途估算 token;重试消耗也计入预算。
  4. 调用适配:供应商差异收敛为内部 request/response 与 finish reason;保留 model id、参数、prompt version 和 usage。
  5. 流式生命周期:首 token、空闲、整体三类超时;客户端断开后取消上游;设置有界缓冲。
  6. 失败处理:未开始输出的瞬时故障可有限退避重试;已输出内容不透明切换;截断或过滤不得进入副作用。
  7. 可观测性:记录队列、TTFT、TPOT、输入/输出 token、取消率、截断率、各状态码、模型与长度桶;内容日志需脱敏并受访问控制。
  8. 容量评估:用真实输入/输出分布压测,不用平均 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 网关负责预算、限流、超时、取消、背压、状态映射和校验;底层算子优化通常属于推理平台。

参考资料