大模型从Token到训练与推理完整原理
大语言模型不是一个“存满标准答案的数据库”,也不是先理解整篇文章再一次性写出回答。它本质上是一个经过大规模数据训练的参数化函数:接收已有 Token 序列,计算下一个 Token 的概率分布,选出一个 Token,再把它放回输入继续预测。
这句定义很短,但真正学懂必须把整个过程串起来:原始文本怎样变成 Token,Token 怎样变成向量,Transformer 怎样交换上下文信息,训练时参数怎样更新,推理时为什么逐 Token 生成,Temperature 为什么改变随机性,KV Cache 为什么提速又占显存,模型为什么会幻觉,以及商业系统怎样控制质量、延迟和成本。
学习目标
完成本页后,你应该能够:
- 区分 AI、机器学习、深度学习、基础模型和大语言模型。
- 解释 Token、Token ID、Vocabulary、Tokenizer 和 Embedding 的关系。
- 解释预训练为什么通常使用“预测下一个 Token”的目标。
- 从输入文本一路讲到 Logits、Softmax 和下一个 Token。
- 用直觉和公式解释 Self-Attention 中的 Q、K、V。
- 解释 Causal Mask、Residual、Normalization、FFN 和多层堆叠的作用。
- 区分预训练、继续预训练、SFT、偏好对齐和推理。
- 区分 Greedy、Temperature、Top-k、Top-p 和 Beam Search。
- 解释 Prefill、Decode、TTFT、TPOT、吞吐量和 KV Cache。
- 判断幻觉、知识缺失、上下文缺失、检索失败和指令冲突的区别。
- 写出 Token 估算、采样和模型 API 调用 Demo。
- 在商业场景中完成模型选型、成本预算、监控和故障排查。
一、AI、大模型和应用系统是什么关系
flowchart TD
A["人工智能AI"] --> B["机器学习"]
B --> C["深度学习"]
C --> D["Transformer等神经网络架构"]
D --> E["基础模型"]
E --> F["大语言模型LLM"]
F --> G["Prompt、RAG、Agent和业务应用"]| 概念 | 含义 | 例子 |
|---|---|---|
| AI | 让机器表现出感知、决策或生成能力的总称 | 规则系统、搜索、推荐、视觉、LLM |
| 机器学习 | 从数据中学习参数,而不是把每条规则都手写 | 分类、回归、排序 |
| 深度学习 | 使用多层神经网络学习复杂表示 | CNN、Transformer |
| 基础模型 | 在大规模数据上训练、可适配很多任务的模型 | 语言、视觉、多模态基础模型 |
| LLM | 主要处理和生成语言 Token 的大规模模型 | 问答、摘要、代码、抽取 |
| AI应用 | 把模型与权限、数据、工具、评估和业务流程组合 | 企业知识库、客服、采集异常助手 |
商业系统不会因为接入 LLM 就删除规则、数据库和搜索引擎。金额计算、权限判断和事务状态应由确定性代码负责;模型适合语言理解、非结构化信息处理、解释和候选方案生成。
二、全过程总览:一句话怎样变成回答
flowchart TD
A["用户输入文本"] --> B["Tokenizer切分Token"]
B --> C["Token映射为整数ID"]
C --> D["Embedding查表得到向量"]
D --> E["加入位置信息"]
E --> F["多层Transformer Block"]
F --> G["最后位置的隐藏向量"]
G --> H["LM Head投影到词表维度"]
H --> I["得到每个候选Token的Logit"]
I --> J["Softmax与解码策略"]
J --> K["选出下一个Token"]
K --> L{"是否遇到停止条件"}
L -->|"否"| C
L -->|"是"| M["Detokenize为最终文本"]关键点:模型每轮通常只确定下一个 Token。你看到一句完整回答,是这个循环执行几十、几百甚至几千次的结果。
三、Tokenizer:文本怎样变成模型能处理的数字
3.1 Token不等于汉字或单词
模型只能接收有限词表中的整数 ID。Tokenizer 先把文本拆成词表中存在的片段:
原文:unbelievable
可能切分:un + believe + able
原文:数据库连接失败
可能切分:数据库 + 连接 + 失败
原文:order_id=1024
可能切分:order + _ + id + = + 102 + 4这只是示意。真实结果完全由目标模型配套的 Tokenizer、词表和规范化规则决定。不能用“一个汉字约等于一个 Token”作为精确计费依据。
3.2 为什么不用每个完整单词做词表
如果把每个完整单词都设为 Token:
- 词表会极大,Embedding 和输出层参数暴涨。
- 新人名、产品名、拼写变化和代码标识符无法覆盖。
- 多语言和组合词会制造大量低频项。
如果只按单个字符:
- 词表小,但序列过长。
- 模型需要更多层才能组合出词义。
- 上下文和推理成本增加。
子词 Tokenization 在“词表大小”和“序列长度”之间折中。常见思想包括 BPE、WordPiece 和 Unigram/SentencePiece;它们训练词表的方法不同,但目标都是用有限子词表示开放文本。
3.3 Vocabulary、Token和Token ID
| 名称 | 含义 |
|---|---|
| Vocabulary | 模型认识的全部 Token 集合 |
| Token | 文本被切出的一个词表单元 |
| Token ID | Token 在词表中的整数编号 |
| Special Token | BOS、EOS、PAD、角色边界等特殊标记 |
| Detokenize | 把生成的 Token ID 恢复成文本 |
同一段文本使用不同模型的 Tokenizer,Token 数和 ID 都可能不同。模型权重和 Tokenizer 必须配套;换错词表相当于把完全不同的编号含义交给模型。
3.4 可运行Demo:观察真实Tokenizer
安装:
python -m pip install transformersfrom transformers import AutoTokenizer
MODEL_DIR = "替换为本地模型目录或允许访问的模型标识"
tokenizer = AutoTokenizer.from_pretrained(MODEL_DIR)
texts = [
"Redis为什么快?",
"ConcurrentHashMap",
"order_id=1024",
]
for text in texts:
ids = tokenizer.encode(text, add_special_tokens=False)
tokens = tokenizer.convert_ids_to_tokens(ids)
restored = tokenizer.decode(ids)
print({
"text": text,
"token_count": len(ids),
"tokens": tokens,
"ids": ids,
"restored": restored,
})这个实验用于理解切分和预算,不应在每次线上请求中重新加载 Tokenizer。生产通常在进程启动时加载一次,或使用 Provider 返回的 usage 作为最终计费事实。
四、Embedding:整数ID为什么能表达语义
Token ID 只是索引,不带大小或语义关系。ID 1000 并不比 ID 10“更重要”。模型维护一个可训练矩阵:
Embedding矩阵形状 = 词表大小 V × 隐藏维度 d用 Token ID 查矩阵的一行,就得到该 Token 的 d 维向量。训练过程中,反向传播不断调整这些数字,让在相似上下文中发挥相似作用的 Token 获得可利用的表示。
flowchart TD
A["Token:数据库"] --> B["Token ID:示意为4812"]
B --> C["Embedding矩阵查第4812行"]
C --> D["d维初始向量"]
D --> E["经过多层上下文计算"]
E --> F["带有当前语境的隐藏表示"]输入 Embedding 只是起点。同一个“苹果”在“苹果很好吃”和“苹果发布手机”中的查表向量起初相同,但经过 Attention 与不同上下文交互后,最终隐藏表示不同。
这里的 Token Embedding 与 RAG 的句子/文档 Embedding 有联系但用途不同:前者是模型内部逐 Token 表示,后者通常把整段文本压缩成一个向量用于相似度检索。详见 Embedding与向量库。
五、为什么必须加入位置信息
纯 Self-Attention 只看元素之间的内容关系,本身没有天然顺序。如果没有位置:
我打你
你打我包含相同 Token 集合,却无法可靠区分主客体。模型需要位置编码或旋转位置表示等机制,让注意力计算感知顺序和相对距离。
位置方法会影响:
- 模型能支持的上下文长度。
- 长距离关系的表达。
- 超出训练长度后的外推表现。
- KV Cache 和推理实现。
“配置支持 128K 上下文”不等于模型在整个 128K 范围内都同样准确,也不等于把无关资料全部塞满窗口会提高质量。
六、Self-Attention:每个Token怎样读取上下文
6.1 Q、K、V的直觉
每个输入向量通过三个可训练矩阵变换为:
| 向量 | 直觉 |
|---|---|
| Query | 当前 Token 想寻找什么信息 |
| Key | 当前 Token 能被什么查询匹配 |
| Value | 当前 Token 真正提供的内容 |
以“订单支付失败,因为它超时了”为例,处理“它”时,Query 会与“订单”“支付”“失败”“超时”等位置的 Key 计算匹配程度,再按权重汇总对应 Value,使“它”的新表示带上相关上下文。
6.2 计算步骤
对输入矩阵 X:
Q = XWq
K = XWk
V = XWv
Scores = QKᵀ / √dk
Weights = Softmax(Scores + Mask)
Attention = WeightsV逐步理解:
XWq/XWk/XWv:用可学习矩阵从同一输入提取三种视角。QKᵀ:每个 Query 与每个 Key 做点积,得到相关性分数。- 除以
√dk:避免维度增大后点积数值过大,使 Softmax 梯度过度饱和。 - 加 Mask:禁止读取不应看到的位置。
- Softmax:把一行分数变成和为 1 的权重。
WeightsV:按相关性加权汇总 Value。
flowchart TD
A["输入隐藏向量X"] --> B["线性变换得到Q"]
A --> C["线性变换得到K"]
A --> D["线性变换得到V"]
B --> E["Q与K点积并缩放"]
C --> E
E --> F["加入Causal Mask"]
F --> G["Softmax得到注意力权重"]
G --> H["按权重汇总V"]
D --> H
H --> I["上下文化表示"]Attention 权重不是天然可靠的“人类解释”。它是模型计算的一部分,不能只展示一张权重图就断言模型的真实推理原因。
6.3 为什么需要Causal Mask
自回归模型训练时可以一次输入完整句子,但位置 i 不能偷看右侧未来 Token,否则训练时知道答案、推理时却没有未来内容,产生训练—推理不一致。
输入:今天 天气 很 好
预测“天气”时可看:今天
预测“很”时可看:今天 天气
预测“好”时可看:今天 天气 很Causal Mask 把未来位置的分数设为负无穷,Softmax 后权重接近 0。
6.4 多头注意力为什么存在
一个注意力头的表示空间有限。多个头使用不同投影矩阵,可同时学习不同关系,例如语法依赖、指代、局部搭配和长距离关联,再把各头结果拼接并投影。
多头不保证每个头都有清晰的人类标签,也不是简单把同一计算重复多次取平均。各头参数不同,会形成不同子空间。
七、一个Transformer Block还有什么
典型 Decoder Block 不只有 Attention:
flowchart TD
A["输入隐藏状态"] --> B["Normalization"]
B --> C["Masked Multi-Head Attention"]
C --> D["与残差相加"]
D --> E["Normalization"]
E --> F["前馈网络FFN"]
F --> G["与残差相加"]
G --> H["输出给下一层"]| 组件 | 作用 | 缺少后的问题 |
|---|---|---|
| Attention | 在不同位置间交换信息 | Token 难以利用上下文 |
| FFN | 对每个位置做非线性特征变换 | 表达能力不足 |
| Residual | 保留原信息并改善深层梯度传播 | 深层网络难训练、信息易损失 |
| Normalization | 稳定数值尺度和训练 | 梯度和激活更不稳定 |
| 多层堆叠 | 逐层形成更抽象表示 | 单层难表达复杂模式 |
不同模型可能使用 Pre-Norm、RMSNorm、SwiGLU、GQA、MQA、RoPE、MoE 等变体,核心链路相似但实现和性能边界不同。完整拆解见 Transformer原理。
八、LM Head、Logits和Softmax
经过最后一层后,取当前生成位置的隐藏向量,通过 LM Head 投影到词表维度。假设词表有 100000 个 Token,就得到 100000 个分数,这些未归一化分数叫 Logits。
Softmax 将 Logits 转为概率:
P(token_i) = exp(logit_i) / Σ exp(logit_j)示意:
| 候选Token | Logit | Softmax后概率 |
|---|---|---|
| 成功 | 4.2 | 0.62 |
| 失败 | 3.5 | 0.31 |
| 超时 | 1.9 | 0.06 |
| 香蕉 | -0.4 | 0.01 |
概率表示在当前模型、参数和上下文下的相对偏好,不等于事实正确率。模型可能非常自信地生成错误事实。
九、解码策略:模型怎样从概率中选Token
9.1 Greedy
每次选概率最高的 Token。稳定、简单,但可能重复、僵硬,并且局部最优不保证整段最优。
9.2 Temperature
在 Softmax 前将 Logit 除以温度 T:
P(token_i) = softmax(logit_i / T)T < 1:分布更尖锐,高概率候选更占优势。T > 1:分布更平坦,低概率候选更容易被采样。- 温度接近 0 的具体处理由 Provider 决定,通常接近确定性选择。
低温不等于事实正确,高温也不等于模型获得新知识。它只改变现有概率分布的采样方式。
9.3 Top-k
只保留概率最高的 k 个候选,再重新归一化采样。k 固定,无法随分布“很确定”或“很不确定”自适应。
9.4 Top-p
从高到低累加概率,保留累计概率达到 p 的最小候选集合。模型很确定时集合小,分布平坦时集合大。
9.5 Beam Search
保留若干条累计分数较高的候选序列,常用于翻译等更强调全局序列概率的任务。开放式聊天中可能导致输出过于保守或相似,也增加计算量。
9.6 可运行Demo:观察温度如何改变概率
import math
import random
TOKENS = ["成功", "失败", "超时", "香蕉"]
LOGITS = [4.2, 3.5, 1.9, -0.4]
def softmax(logits: list[float], temperature: float) -> list[float]:
if temperature <= 0:
raise ValueError("temperature必须大于0")
scaled = [value / temperature for value in logits]
maximum = max(scaled)
exps = [math.exp(value - maximum) for value in scaled]
total = sum(exps)
return [value / total for value in exps]
def sample_once(temperature: float) -> str:
probabilities = softmax(LOGITS, temperature)
return random.choices(TOKENS, weights=probabilities, k=1)[0]
for temperature in [0.2, 1.0, 2.0]:
probabilities = softmax(LOGITS, temperature)
counts = {token: 0 for token in TOKENS}
for _ in range(10_000):
counts[sample_once(temperature)] += 1
print("temperature=", temperature)
print("probabilities=", dict(zip(TOKENS, probabilities)))
print("sample counts=", counts)减去最大 Logit 是数值稳定技巧,避免 exp 溢出;它不会改变 Softmax 结果,因为所有项同时减去同一常数。
十、模型是怎样训练出来的
10.1 预训练数据链路
flowchart TD
A["原始网页、书籍、代码等语料"] --> B["授权、过滤和数据治理"]
B --> C["清洗、去重、质量分类"]
C --> D["隐私和有害内容处理"]
D --> E["Tokenizer编码"]
E --> F["切成固定长度训练样本"]
F --> G["批量送入模型"]
G --> H["计算下一个Token损失"]
H --> I["反向传播计算梯度"]
I --> J["优化器更新参数"]
J --> K{"是否完成训练计划"}
K -->|"否"| G
K -->|"是"| L["评估和保存Checkpoint"]训练数据不是越多越好。重复、污染、隐私、错误事实和低质量模板会进入模型行为。数据溯源和许可还涉及版权、合规与可追溯性。
10.2 下一个Token训练目标
例如训练序列:
输入:Redis 是 一个
目标:是 一个 内存模型在每个位置输出词表概率,训练用正确 Token 的负对数概率计算交叉熵损失。若正确 Token 概率低,损失大;反向传播计算每个参数应向哪个方向调整,优化器执行更新。
模型不是把训练文本逐条写进数据库。知识和模式分布式地编码在大量参数中,但模型也可能记忆部分训练片段;因此不能认为“参数化”天然消除隐私和版权风险。
10.3 为什么预测Token能产生通用能力
要降低海量语料上的预测误差,模型必须学习词法、语法、上下文关系、常见事实、代码结构和一定的问题求解模式。规模、数据和训练使这些能力组合出来。
但训练目标仍是预测序列,不是内置事实数据库或形式化证明器。这解释了它为什么语言流畅,也解释了它为什么可能把“统计上合理”误当成“事实正确”。
10.4 预训练、SFT和对齐的区别
| 阶段 | 输入 | 目标 | 典型作用 |
|---|---|---|---|
| 预训练 | 大规模通用语料 | 下一个Token预测 | 获得语言和通用模式 |
| 继续预训练 | 领域原始语料 | 保持语言建模目标 | 适应领域术语和分布 |
| SFT | 指令—回答样本 | 学习按指令输出 | 形成助手行为和任务格式 |
| 偏好对齐 | 多个回答及偏好/奖励 | 更符合人类或安全偏好 | 改善有用性和安全性 |
| 推理 | 用户当前输入 | 不更新基础参数 | 生成实际结果 |
RAG 不属于训练:它在推理时把检索资料放进上下文。微调会更新参数或适配器权重,适合行为、格式和领域模式,不适合替代频繁更新且要求可引用的事实库。
十一、训练为什么能并行,生成为什么仍逐Token
训练样本的正确后续 Token 已经存在。配合 Causal Mask,GPU 可以一次计算序列中多个位置的预测损失。
推理时下一个 Token 尚未产生:
必须先生成token 1
才能把token 1作为上下文生成token 2
再用token 1、2生成token 3同一请求的 Decode 具有串行依赖,但不同请求、不同样本和矩阵运算仍可批处理并行。这也是推理引擎持续批处理和调度优化的重要来源。
十二、Prefill、Decode和KV Cache
12.1 Prefill
模型先处理系统提示词、历史、用户问题和 RAG 资料等全部输入 Token,生成各层隐藏状态以及后续需要的 Key/Value。输入越长,Prefill 计算通常越重。
12.2 Decode
之后逐步生成输出 Token。每一步读取已有上下文的缓存并计算新 Token。Decode 常受显存带宽、批处理和序列长度影响。
12.3 KV Cache为什么存在
如果每生成一个 Token 都重新计算之前所有 Token 的 K、V,会重复大量工作。KV Cache 保存每层历史 Token 的 Key 和 Value,新一步只计算新 Token 并读取历史缓存。
flowchart TD
A["Prefill处理全部输入"] --> B["保存每层历史K和V"]
B --> C["生成第一个输出Token"]
C --> D["只计算新Token的Q、K、V"]
D --> E["Q读取历史KV Cache"]
E --> F["选出下一个Token"]
F --> G["把新K、V追加到Cache"]
G --> H{"是否结束"}
H -->|"否"| D
H -->|"是"| I["释放请求Cache"]KV Cache 用空间换计算:上下文越长、层数越多、并发越高,占用显存越多。模型权重能放进显存,不代表高并发长上下文一定能运行。
简化估算思路:
KV Cache约与以下因素成正比:
层数 × KV头数 × 每头维度 × 序列长度 × 2(K和V) × 每元素字节 × 并发具体模型可能使用 MQA/GQA、分页 KV Cache、量化 Cache 等优化,必须按模型配置和推理引擎公式计算。
十三、延迟指标到底看什么
| 指标 | 含义 | 用户感受 |
|---|---|---|
| 排队时间 | 等待 GPU/配额 | 请求尚未处理 |
| TTFT | 从请求到首个Token | 多久开始看到回答 |
| TPOT | 后续每个Token平均时间 | 打字速度快慢 |
| 总耗时 | 从请求到结束 | 完整任务多久完成 |
| 吞吐量 | 单位时间处理Token/请求数 | 系统整体能力 |
| 并发 | 同时在途请求 | 影响排队和显存 |
流式输出主要改善用户感知的 TTFT,不自动降低 Prefill、总 Decode 时间或 Token 成本。只监控平均耗时会掩盖 P95/P99 长尾。
请求慢的分层判断:
flowchart TD
A["AI请求慢"] --> B{"首Token是否慢"}
B -->|"是"| C["检查排队、输入长度、RAG、网络和Prefill"]
B -->|"否"| D{"后续生成是否慢"}
D -->|"是"| E["检查模型大小、批处理、显存带宽和输出长度"]
D -->|"否"| F["检查网关缓冲和前端渲染"]
C --> G["按阶段耗时定位"]
E --> G
F --> G十四、上下文窗口不是无限记忆
一次请求的上下文通常包含:
System Prompt
+ 开发者规则
+ 历史对话
+ 当前用户问题
+ RAG片段
+ 工具定义
+ 工具结果
+ 已生成输出必须满足模型和 Provider 的窗口限制。输入占得越多,可生成输出空间通常越少。
预算示例:
def check_token_budget(
context_limit: int,
system_tokens: int,
history_tokens: int,
rag_tokens: int,
tool_tokens: int,
user_tokens: int,
max_output_tokens: int,
safety_margin: int = 512,
) -> dict:
input_tokens = (
system_tokens
+ history_tokens
+ rag_tokens
+ tool_tokens
+ user_tokens
)
required = input_tokens + max_output_tokens + safety_margin
return {
"input_tokens": input_tokens,
"required_tokens": required,
"context_limit": context_limit,
"allowed": required <= context_limit,
"remaining": context_limit - required,
}
print(check_token_budget(
context_limit=32_768,
system_tokens=800,
history_tokens=6_000,
rag_tokens=12_000,
tool_tokens=2_000,
user_tokens=500,
max_output_tokens=3_000,
))超限不能只从最前面机械截断,否则可能删掉系统规则或最近问题。应按类型设计裁剪:保留安全规则;摘要旧历史;RAG 去重和重排;工具返回只保留必要字段;为输出预留明确空间。
十五、参数量、精度、显存和成本
模型参数权重的粗略存储:
权重显存约等于 参数量 × 每参数字节例如 7B 参数仅权重粗估:
| 精度 | 每参数理论字节 | 仅权重粗估 |
|---|---|---|
| FP32 | 4 | 约28GB |
| FP16/BF16 | 2 | 约14GB |
| INT8 | 1 | 约7GB |
| 4-bit | 0.5 | 约3.5GB |
真实运行还要增加:
- 量化元数据和分组尺度。
- KV Cache。
- 激活和临时工作区。
- 推理引擎、CUDA Context和通信缓冲。
- 并发请求。
- 多卡切分开销。
量化降低权重和带宽压力,但可能损失精度,且不同层和任务敏感度不同。不能只看模型成功加载,必须用业务评估集验证质量。
训练显存远高于推理,因为还要保存梯度、优化器状态和大量中间激活。LoRA/QLoRA 通过冻结基础权重、训练低秩适配器等方式降低成本,详见 LoRA与QLoRA。
十六、幻觉为什么发生
幻觉不是一个单一故障,至少要区分:
| 类型 | 例子 | 根因方向 |
|---|---|---|
| 参数知识错误 | 编造不存在的版本或论文 | 训练知识不足、冲突或陈旧 |
| 上下文无依据 | 材料没写却给出具体结论 | 生成目标偏向流畅回答 |
| RAG检索错误 | 找到相似但不适用的制度 | 切分、召回、权限、重排问题 |
| 引用幻觉 | 引用了不存在的文件和页码 | 让模型自由生成引用标识 |
| 工具结果误读 | 把失败状态解释为成功 | Schema、Prompt或结果裁剪错误 |
| 指令冲突 | 用户诱导忽略系统规则 | 信任边界和提示注入问题 |
模型优化的是条件概率,不是数据库约束中的事实一致性。面对信息不足时,“生成一个常见且语言合理的后续”可能比“拒绝回答”概率更高。
治理流程:
flowchart TD
A["用户问题"] --> B["判断是否需要实时或私有事实"]
B -->|"需要"| C["受权限控制的RAG或工具"]
B -->|"不需要"| D["模型参数知识"]
C --> E["返回可追溯资料"]
D --> F["标注知识时效和不确定性"]
E --> G["要求基于资料回答"]
F --> G
G --> H["结构、引用和规则校验"]
H --> I{"证据是否充分"}
I -->|"否"| J["拒答、澄清或人工处理"]
I -->|"是"| K["返回并记录版本证据"]降低温度只能减少采样发散,不能给模型补充缺失事实。高风险医疗、支付、法律和生产变更必须使用确定性校验与人工审批。
十七、模型记忆、会话历史和RAG的区别
| 能力 | 数据在哪里 | 是否实时 | 适合什么 |
|---|---|---|---|
| 参数知识 | 模型权重 | 通常不是 | 通用语言和稳定模式 |
| 当前上下文 | 本次Prompt | 是 | 当前任务、短期规则和资料 |
| Chat Memory | 应用保存后重新放入Prompt | 取决于应用 | 多轮会话连续性 |
| RAG | 外部知识库检索 | 可更新 | 私有、可引用、频繁变化知识 |
| Tool Calling | 实时业务系统 | 是 | 订单状态、计算、执行动作 |
模型 API 不会因为你上一次调用过就天然记住用户。所谓 Memory 通常是应用保存历史并在下一次请求重新发送,因此会占上下文和成本,也必须做租户隔离、过期和隐私删除。
十八、云API调用Demo与工程边界
import os
import time
from typing import Any
import requests
API_URL = os.environ["MODEL_API_URL"]
API_KEY = os.environ["MODEL_API_KEY"]
MODEL_NAME = os.environ["MODEL_NAME"]
class ModelCallError(RuntimeError):
pass
def ask_llm(question: str, request_id: str) -> dict[str, Any]:
if not question.strip():
raise ValueError("question不能为空")
if len(question) > 4_000:
raise ValueError("question过长")
payload = {
"model": MODEL_NAME,
"messages": [
{
"role": "system",
"content": "你是企业技术助手。不确定时必须明确说明。",
},
{"role": "user", "content": question},
],
"temperature": 0.2,
"max_tokens": 800,
}
started = time.perf_counter()
try:
response = requests.post(
API_URL,
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
"X-Request-Id": request_id,
},
json=payload,
timeout=(3.0, 30.0),
)
except requests.Timeout as exc:
raise ModelCallError(f"模型调用超时 requestId={request_id}") from exc
except requests.RequestException as exc:
raise ModelCallError(f"模型网络调用失败 requestId={request_id}") from exc
elapsed_ms = int((time.perf_counter() - started) * 1000)
if response.status_code == 429:
retry_after = response.headers.get("Retry-After")
raise ModelCallError(
f"模型限流 requestId={request_id} retryAfter={retry_after}"
)
if response.status_code in {401, 403}:
raise ModelCallError(f"模型认证或权限失败 requestId={request_id}")
if response.status_code >= 500:
raise ModelCallError(f"模型服务异常 requestId={request_id}")
response.raise_for_status()
data = response.json()
choices = data.get("choices") or []
if not choices:
raise ModelCallError(f"模型没有返回候选结果 requestId={request_id}")
answer = choices[0]["message"]["content"]
return {
"answer": answer,
"usage": data.get("usage", {}),
"model": data.get("model", MODEL_NAME),
"elapsed_ms": elapsed_ms,
"request_id": request_id,
}这个 Demo 比“直接 requests.post”多做了参数边界、连接/读取超时、错误分类、空候选检查、usage 和 requestId,但仍不是完整生产实现。生产还需要身份权限、请求/并发/Token/金额限额、Retry-After、退避抖动、熔断、脱敏日志、评估和降级。
十九、商业模型选型
不要只看排行榜总分。模型必须按实际业务评估:
| 维度 | 要问的问题 |
|---|---|
| 任务质量 | 分类、抽取、问答、代码或推理是否达到阈值 |
| 上下文 | 实际有效长上下文表现如何,而不只是标称长度 |
| 结构化输出 | JSON Schema和枚举约束是否稳定 |
| Tool Calling | 参数生成、并行工具和错误恢复是否可靠 |
| 多模态 | 支持哪些图片、音频、文件格式和大小 |
| 延迟 | TTFT、TPOT、P95/P99是否满足SLO |
| 成本 | 输入、输出、缓存、图片和失败重试如何计费 |
| 安全合规 | 数据驻留、训练使用、删除、审计和合同边界 |
| 可用性 | 配额、Region、SLA、限流和故障历史 |
| 可迁移性 | API兼容是否覆盖真实使用能力 |
同一系统可以按任务路由:低风险分类走小模型,复杂推理走强模型,敏感数据走本地模型,高风险决策进入人工审核。模型路由必须记录实际模型、Prompt、RAG、工具和安全版本,便于复现与回滚。
二十、生产故障排查Runbook
20.1 回答突然变差
先固定一组失败样本并保存脱敏证据,然后对比:
实际模型及版本
Prompt模板和参数
历史对话裁剪
RAG召回与Rerank结果
工具输入输出
Temperature/Top-p/输出上限
finish reason和内容安全状态
应用发布和Provider变更时间不要只反复问同一问题看“这次好了没有”。使用评估集比较变更前后正确率、引用、拒答、安全、延迟和成本。
20.2 首Token突然变慢
检查排队时间、输入 Token、RAG/Rerank、网络、Provider Region、模型负载和 Prefill。若输入增长,继续拆 System、历史、RAG 和工具定义分别占多少 Token。
20.3 生成速度慢
检查 TPOT、输出长度、模型大小、量化、批处理、GPU 利用率、显存带宽、KV Cache 和并发。TTFT 正常但 TPOT 变差时,不要优先优化向量检索。
20.4 显存OOM
判断是加载模型时 OOM,还是并发增长后 OOM:
- 启动即 OOM:权重、精度、并行切分和工作区问题。
- 长上下文才 OOM:KV Cache 随序列增长。
- 并发后 OOM:每个请求 Cache 与批处理叠加。
- 运行一段时间后持续增长:请求未释放、引擎或应用泄漏。
临时降低并发和上下文可以止血,但必须结合显存时间线、请求长度分布和引擎指标确认根因。
20.5 成本突然上升
公式化拆分:
总成本 = 成功请求成本
+ 失败请求已消耗成本
+ 重试成本
+ RAG/Embedding/Rerank成本
+ 工具和基础设施成本检查请求量、输入/输出 Token 分布、缓存命中、历史长度、RAG TopK、重试次数、模型路由和异常流量。流式输出不等于成本更低。
20.6 回答出现敏感信息
立即限制传播并保存受控证据,定位敏感内容来自用户输入、系统 Prompt、历史、RAG、工具、模型参数知识还是日志回放。撤销泄露密钥、修复权限过滤、清理缓存和评估集,并审计受影响租户。只修改一句 Prompt 不能代替数据权限修复。
二十一、常见误区
大模型是搜索引擎
错误。模型参数不提供可枚举、实时和可追溯文档集合。实时事实用搜索、RAG或工具。
参数越大一定越适合
错误。任务质量还受数据、训练、上下文、推理策略影响;大模型通常延迟和成本更高。
上下文越长回答越好
错误。无关内容会稀释信号、增加成本和攻击面,长上下文有效利用也可能下降。
模型概率就是答案正确率
错误。下一个 Token 的条件概率不是事实置信度。
Temperature设为0就不会幻觉
错误。它只降低采样随机性,不修复错误知识和缺失上下文。
模型支持JSON就能直接入库
错误。语法正确不等于字段、金额、权限和业务规则正确,必须 Schema 和业务校验。
二十二、面试标准回答
大模型怎样生成一句话
文本先经Tokenizer切成Token并映射为ID,通过Embedding和位置信息得到向量;多层Transformer用Masked Self-Attention和FFN形成上下文表示;最后位置经过LM Head得到整个词表的Logits,Softmax和解码策略选出下一个Token。新Token追加到上下文后重复计算,直到EOS、停止词或长度上限。
Q、K、V是什么
Q表示当前位置想找什么,K表示每个位置可被怎样匹配,V表示实际贡献的信息。Q和K点积、缩放、Mask、Softmax后得到权重,再按权重汇总V。多头使用不同投影学习不同子空间关系。
训练和推理有什么区别
训练有已知目标Token,计算交叉熵损失并反向传播更新参数,配合Causal Mask可并行计算多个位置;推理不更新基础参数,而且下一个Token未知,必须生成后再用于下一步,因此单请求Decode具有逐Token依赖。
KV Cache为什么提速又占显存
自回归生成时历史Token的K和V不会变化。KV Cache保存每层历史K/V,新一步只计算新Token,避免重复计算;但Cache随层数、KV头、序列长度、精度和并发增长,因此长上下文和高并发会显著占用显存。
为什么大模型会幻觉
模型训练目标是生成在上下文中概率较高的后续,不是查询受约束的事实数据库。训练知识缺失或过期、上下文不足、RAG错误、指令冲突都可能让流畅但错误的文本概率更高。治理要结合RAG或工具、权限、引用校验、拒答、评估和高风险人工审核,不能只调低Temperature。
二十三、学习验收
不看答案完成:
- 用目标模型 Tokenizer 比较中文、英文和代码的 Token 数。
- 解释为什么 Token ID 大小不表示语义远近。
- 手算三个 Logit 的 Softmax,并解释减最大值的作用。
- 画出 Q、K、V、Mask、Softmax 和加权求和链路。
- 解释训练时为什么能同时算多个位置,推理时为什么逐Token。
- 运行温度采样 Demo,比较三组分布和计数。
- 为32K窗口设计System、历史、RAG、工具和输出预算。
- 估算一个模型仅权重显存,并列出遗漏开销。
- 区分TTFT变慢和TPOT变慢的排查方向。
- 对一个幻觉案例判断根因属于参数、上下文、RAG、工具还是指令。
- 设计一个小模型、强模型、本地模型和人工审核的路由规则。
- 给模型API Demo补请求限流、Token限额和脱敏审计。
