工程化
本文总结模型怎么选、LoRA 怎么省显存、KV Cache 和延迟怎么算、每千次请求花多少钱、高峰怎么扛:把这些数一个个算出来。
主线第 7 站 · 上一站:评测 / 安全 · 下一站:Coding / Harness · 全程:主线
本站在主线里回答什么:员工那句「我上周出差打车超标了,还能报吗?能的话帮我提报销工单。」走到这里,前六站已经让助手能答对、能建对单,也能拿数据证明它可靠。第 6 站交过来一套可重复执行的报销评测集(超标可报、超标不可报、缺发票、重复发票、工单字段错误、发票文本里藏注入等用例),以及结果、过程、安全、代价四类基线和发布门槛。本站的问题更现实:正确率一样的前提下,用什么模型,花多少钱,多快,能扛多少人。
落到这个请求上:意图识别、"要不要查制度"这类轻判断交给便宜的小模型;读《差旅费管理办法》2026 版第 4.2 条,判断 INV-102 的 118.5 元比 80 元标准超了 38.5 元、有没有例外审批,并且带引用,这一步交给强模型;从 query_invoices 的返回里抽字段、拼 create_reimbursement_ticket 的参数,用开了结构化输出的中档模型,或者微调过的小模型;制度原文始终走 RAG,不进微调数据。我们还要算清一次成功报销平均用多少 token、花多少钱,首 token 延迟和端到端耗时各是多少,月底集中报销时峰值并发有多高,出了故障怎么降级(比如发票系统挂了,就只给制度依据,建单排队)。每一项优化都要先过第 6 站的门槛。本站交给第 8 站的是一份可执行的运行时配置:模型路由表,每一步的 token、步数和超时预算,限流与熔断阈值,降级路径,监控看板。Harness 再把它做成产品。
先给结论
工程化的题目最后都落到四个数上:效果、成本、延迟、稳定性。面试官想听你怎么选模型、怎么估显存、怎么降本、怎么扛并发。只说一句"vLLM 很快"拿不到分;能说出"7B 模型 FP16 权重 14 GB,8k 上下文每条请求的 KV 还要 4 GB 左右,所以一张 24 GB 的卡只能同时跑两路",才算答到点上。
1. 模型选型
榜单只能回答"效果"这一项里的一小部分。选型至少要看六项:
| 维度 | 要问的问题 |
|---|---|
| 效果 | 任务成功率、事实性、指令遵循 |
| 成本 | 输入/输出单价、上下文长度、重试 |
| 延迟 | 首 token、总时长、并发下的 P95 |
| 上下文 | 多长、长文效果、Lost in the Middle |
| 工具调用 | Function Calling/JSON 稳定性 |
| 部署 | 开源/闭源、私有化、合规、中文 |
做法是先用强模型打样,同时把评估集建起来;然后逐步试小模型、路由、微调、量化,看能不能在效果可接受的前提下把钱省下来。举个示意的例子:报销助手先用最强的模型把 50 条评测用例跑到 90% 成功率,这个数就是基线;接着只把"意图识别"这一步换成小模型重跑,成功率还在门槛以上就换,掉下去就退回。一开始就为了省钱上小模型,你连"掉了多少"都没法知道;反过来,简单步骤一直用最贵的模型,钱就白花了。
2. 成本怎么算
一次模型调用的钱,先看输入和输出各多少 token,再加上工具和重试:
| 符号 | 含义 | 单位 |
|---|---|---|
| 一次请求的总成本 | 元 | |
| 、 | 输入、输出 token 数(推理模型的思考 token 一般按输出计费) | token |
| 、 | 输入、输出单价,输出通常比输入贵好几倍 | 元 / token |
| 检索、rerank、外部接口等按次收费的部分 | 元 | |
| 失败重试多花的钱,按重试比例摊到每次请求上 | 元 |
代入一组示意数字算每千次请求:输入单价 2 元 / 百万 token,输出 8 元 / 百万 token;每次输入 6000 token(系统提示、工具定义、制度条款、发票结果),输出 400 token。
- 输入:1000 次 × 6000 = 600 万 token,× 2 元 / 百万 = 12 元;
- 输出:1000 次 × 400 = 40 万 token,× 8 元 / 百万 = 3.2 元;
- 小计 15.2 元,也就是每次 0.0152 元;
- 10% 的请求因为 JSON 不合法重试一次:多 100 次 × 0.0152 = 1.52 元;
- 每次调一次 rerank,示意按 0.001 元 / 次:1 元。
合计每千次 17.72 元。输出单价是输入的 4 倍,可输入 token 是输出的 15 倍,所以 12 元的输入才是大头。Agent 场景几乎都这样:输入长、输出短。所以下面的优化顺序把减上下文和缓存放在最前面。这个式子没算的东西也要记得:命中提示缓存的输入通常有折扣,人工兜底的成本不在 token 账里。
Agent 会把成本放大:多轮调用,每轮重复带上下文,加上工具结果、记忆检索、rerank,都是钱。优化顺序大致是:
- 减上下文:压缩、裁剪、摘要,只带相关证据。制度只带命中的第 4.2 条,不带整本《差旅费管理办法》;
- 缓存:系统提示、稳定前缀、只读工具结果;
- 路由:简单任务走小模型;
- 结构化输出:减少因为格式错误引起的重试;
- 微调或量化:把行为固化下来,降低推理成本;
- 推理优化:batching、KV Cache、PagedAttention。
3. LoRA 与微调
全量微调要更新一个 4096×4096 的矩阵,就得给 1677 万个数都存梯度和优化器状态。LoRA 的做法是:原权重冻住不动,只额外训练一个增量,并且规定这个增量必须能写成两个瘦长矩阵的乘积:
- 是预训练好的原权重,形状 ,训练时冻结; 是微调后实际使用的权重。
- 读作"d 行 r 列的实数矩阵", 读作"属于"。所以 B 是 d 行 r 列,A 是 r 行 d 列。
- 是秩(rank),可以理解为"这个增量最多包含 r 个独立的方向"。 读作"r 远小于 d",常见取值 8、16,而 d 是几千。
- 是矩阵乘法,结果是 ,和 W 形状一样,所以能直接相加。
用一个 3×3、r = 1 的小例子看 BA 长什么样:
结果是一个 3×3 矩阵,9 个数,但只用了 B 的 3 个数加 A 的 3 个数,共 6 个参数。而且你会发现第二行正好是第一行的 2 倍,第三行全是 0:每一行都是 A 那一行乘上一个系数,这就是"秩为 1"的意思。r = 2 时,相当于两个这样的秩 1 矩阵相加,能表达的变化多一些。
前向计算时,一般不把 BA 先乘出来,而是先算 再乘 B。取输入 (上标 表示转置,这里把一行数竖过来当列向量):
- ,得到一个数(r 维向量,这里 r = 1);
- 。
直接用上面乘好的 BA 去乘 x,第一行 0.5×2 = 1,第二行 1×2 = 2,第三行 0,结果一样。先算 Ax 只要 3 + 3 = 6 次乘法;拿乘好的 BA 去乘 x 要 9 次,还没算乘出 BA 本身的 9 次。一般地,前者是 次,后者是 次,d = 4096、r = 8 时差 256 倍。训练时 adapter 是旁路,按这种顺序算;部署时如果把 BA 合并进 W,就没有这条旁路了。
换成真实尺寸:d = 4096、r = 8 时,原矩阵 4096 × 4096 = 16,777,216 个参数;LoRA 只有 A 的 8 × 4096 = 32,768 个加 B 的 4096 × 8 = 32,768 个,共 65,536 个,是原来的 1/256,约 0.39%。
这个设计赌的是"微调需要的改动本来就是低秩的"。如果任务真要大改模型(比如灌入大量新领域语料),r 太小会学不动,这时要调大 r 或者改用全量微调。其余几个要点:
- r 增大,容量和开销都增加,效果是变好还是过拟合,要在验证集上看;
- alpha 控制缩放,实际乘到增量上的是 alpha/r,下文"扩展"一节有算例;
- target modules 通常选 attention 的 q、k、v、o,也可以加上 FFN。
QLoRA:把基座量化到 4-bit,再在上面训练 LoRA。显存省了不少,但可能损失一点精度,速度也未必更快(显存算例见下文 LoRA / QLoRA 一题)。
微调适合固定格式、风格、行为,领域术语,稳定的工具调用;不适合实时事实和经常变更的知识。比如报销工单的 JSON 字段顺序可以靠微调学,"市内交通标准 80 元"这种数字就不行:制度一改版,模型里记住的还是旧数。经验是事实用 RAG,行为用微调,简单约束写进 Prompt。
SFT、DPO、RLHF 的区别:
- SFT:用成对的输入和理想输出做示范,教格式和行为;
- DPO:用"好回答 vs 差回答"的偏好对来优化,不需要奖励模型;
- RLHF/PPO:先训练奖励模型,再做强化学习,成本高;
- GRPO:同一题采样一组回答做组内比较,省掉 critic(价值模型),适合答案能自动判对错的推理任务。
4. 推理与显存
权重显存就是"参数量 × 每个参数占几个字节":
FP16:参数量 × 2 bytes
INT8:参数量 × 1 byte
INT4:参数量 × 0.5 byte
7B 模型 FP16:7×10⁹ × 2 字节 = 14×10⁹ 字节,约 14 GB(换成 GiB 约 13 GiB);70B 约 140 GB。这只是权重,还要加 KV Cache、激活和框架开销。
KV Cache(逐项解释和算例见下文 KV Cache 一题):
2 × layers × kv_heads × head_dim × seq × batch × bytes
常见的优化手段:
- MQA、GQA 减少 KV 头数;MLA 用低维潜变量压缩缓存;
- PagedAttention 按页管理 KV,减少碎片;
- Continuous Batching 动态拼批,提高吞吐;
- Prefix Caching 复用相同前缀;
- 量化 KV Cache(可能损失精度);
- Prefill 和 Decode 分离,按阶段分别扩缩容。
首 token 延迟主要由 prefill 和排队决定;总延迟还要算 decode 速度、工具调用和网络。
5. 高并发与稳定性
- 限流:按用户、租户、API Key 分别限;
- 排队:优先级队列,每个请求带超时;
- 熔断:下游故障时快速失败,比如
query_invoices连续超时就先不调它; - 降级:切小模型、用缓存,或者给固定回答;
- 多实例:服务本身无状态,状态放共享存储;
- 监控:QPS、P95/P99、错误率、token、GPU 利用率、队列长度;
- 压测:按峰值和故障场景压,只看平均值会漏掉尖峰。
6. 一个估算例子
教学算例:按 Llama-2-7B 这类 MHA 结构(32 层、32 个 KV 头、head_dim 128)、FP16、8k 上下文、batch = 8 来估:
- 权重约 14 GB;
- KV Cache 每 token:2 × 32 × 32 × 128 × 2 字节 = 524,288 字节 = 0.5 MiB;8k(8192)token 一条是 4 GiB,× 8 条 = 32 GiB;
- 再加激活和框架开销,一张 48 GB 的卡也未必放得下,需要量化、换 GQA 结构或者减小 batch。换成 8 个 KV 头的 GQA 结构,KV 直接降到四分之一,8 条只要 8 GiB。
这种估算比背一句"7B 要 14G"更能体现工程能力。
7. 60 秒回答模板
选型看效果、成本、延迟、上下文、工具调用和部署约束。成本按输入、输出 token 加工具和重试来算,Agent 里输入通常是大头;优化顺序是上下文压缩、缓存、路由、结构化输出、微调或量化、推理优化。微调用 LoRA 或 QLoRA,事实靠 RAG,行为靠微调。推理显存看权重和 KV Cache,7B FP16 权重约 14 GB,KV Cache 随序列长度和并发线性增长;vLLM 用 PagedAttention 和 continuous batching 提高吞吐。高并发靠限流、排队、熔断、降级和监控。
8. 常见追问(带答法)
7B/70B 要多少显存?怎么估?
简答:显存 = 权重 + KV Cache + 激活与临时缓冲 + 框架预留。推理时先按精度算权重,再按上下文长度和并发数算 KV;训练还要加梯度和优化器状态,量级完全不同。
细说:按四块来算。
第一块是权重,参数量乘每个参数的字节数:
| 精度 | 7B | 70B |
|---|---|---|
| FP16 / BF16 | 约 14 GB | 约 140 GB |
| INT8 / FP8 | 约 7 GB | 约 70 GB |
| INT4 | 约 3.5 GB(另有 scale 等元数据,实际略多) | 约 35 GB |
第二块是 KV Cache,每 token 字节数 × 上下文长度 × 并发数(公式和逐项解释见下文"KV Cache"一题)。教学算例:Llama-2-7B 这类 MHA 结构每 token 约 0.5 MB,8k 一条约 4 GB;Llama-2-70B 用 GQA(80 层、8 个 KV 头),每 token 约 320 KB,8k 一条约 2.5 GB。70B 的权重是 7B 的十倍,每 token 的 KV 反而比 MHA 结构的 7B 小,所以决定 KV 大小的是注意力结构,参数量只是次要因素。
第三块是激活和框架。推理时激活不大,但有临时 buffer、CUDA Graph、通信缓冲。vLLM 这类引擎会按 gpu_memory_utilization 先占住大部分显存,减掉权重后剩下的都做成 KV 块池。比如 80 GB 的卡设 0.9,引擎先占 72 GB,放 7B FP16 权重 14 GB 后,剩下五十多 GB 给 KV 和临时缓冲。
第四块只在训练时有。全参数加 AdamW 混合精度,常用经验是每参数约 16 字节:BF16 权重 2 + 梯度 2 + FP32 主权重 4 + Adam 两个动量 8。7B 就是 7×10⁹ × 16 = 112 GB,还没算激活,必须多卡加 ZeRO 或 FSDP 切分。LoRA 只给少量参数存梯度和优化器,显存主要是基座 14 GB 加激活;QLoRA 把基座压到 4 GB 左右。
面试里可以这样收尾:70B FP16 推理至少要两张 80 GB 的卡做张量并行(权重 140 GB,再加 KV);INT4 权重能放进单张 80 GB 卡,但剩下给 KV 的空间决定了并发上限。
容易踩的坑:只报权重数是最常见的不完整答案。KV 随并发线性增长,而并发恰恰是吞吐的来源,忘了它就会在压测时 OOM。INT4 能省显存,但要看有没有对应的 kernel、业务精度掉没掉。GB 和 GiB 差约 7%,14 GB 只有约 13 GiB。MoE 模型要按总参数备显存。
可能的追问:
- 一张 24GB 卡能跑 7B 吗?能跑,但 FP16 权重占掉 14 GB 后只剩约 10 GB 给 KV 和开销,按每条 8k 请求 4 GB 算,只能并发两路左右。短上下文、低并发可以;要长上下文或高并发,就量化权重或 KV,或者选 GQA 结构的模型(Llama-3-8B 类 8k 一条只要 1 GB 左右)。
- 显存够了为什么还慢?显存只决定放不放得下,速度要看 prefill 的算力和 decode 的显存带宽,见下文"Prefill 和 Decode"。
LoRA 为什么有效?r 怎么选?和全量微调怎么选?
简答:下游适配真正需要的权重改动维度很低。LoRA 只学一个低秩增量 ( 读作"W 的改变量"),冻结基座,可训练参数压到原来的零头。r 是容量旋钮,用验证集来定;数据量大、和预训练分布差得远、要改深层能力时,才考虑全量微调。
细说:先说为什么有效。有研究发现,预训练模型的微调可以在很低维的子空间里完成(intrinsic dimension 一类工作);LoRA 论文进一步观察到适配所需的 秩很低,部分任务上很小的 r 已经接近全量效果。直觉上,预训练已经学到了能力,微调多数是在重新组合、放大已有的方向,用不着学一整套新知识。比如让模型学会"输出工单 JSON 时先写 invoice_id 再写 amount",这种改动在几千维的权重空间里只占很少几个方向。
初始化时 A 随机、B 置零,所以训练开始那一刻 ,模型和原模型完全一样,训练从一个稳定的点出发。
部署也方便:可以把 BA 合并回 W,推理没有额外延迟;也可以不合并,一个基座挂多个 adapter,按请求热切换,适合多租户、多任务。
r 怎么选:从 8 或 16 起步。任务越复杂、数据越多、和预训练分布差得越远,就往 32、64 试。alpha 常设为 r 或 2r。改 r 时要注意,实际乘到增量上的是 alpha/r,它相当于一个学习率倍率:r 从 8 改成 16 而 alpha 还留在 16,倍率就从 2 掉到 1,等于偷偷把学习率减半(rsLoRA 改用 ,缓解大 r 时更新幅度被压得过小的问题,算例见下文"扩展"一节)。target modules 可以只挂 q、v,也可以挂所有线性层(q、k、v、o 加 FFN),后者通常更稳,开销也更大。判断依据是验证集曲线,训练 loss 降得好看不说明问题。
和全量微调怎么选:
| 条件 | 倾向 |
|---|---|
| 数据几百到几万条,学格式、风格、工具调用 | LoRA |
| 显存有限、要多个任务版本并存 | LoRA / QLoRA |
| 数据量大、要注入大量领域语料(类继续预训练) | 全量或高 r |
| 追求某项能力的上限,且算力充足 | 全量,同时防遗忘 |
容易踩的坑:LoRA 学进大量新知识的能力有限,但对原能力的破坏也更小(可参考论文 LoRA Learns Less and Forgets Less)。r 不是越大越好,几百条数据配 r = 64 很容易过拟合。LoRA 的学习率通常比全量微调大一个量级(比如全量用 1e-5,LoRA 用 1e-4 到 2e-4 比较常见),照搬全量的学习率会学不动。
可能的追问:
- A、B 为什么一个随机一个置零?都置零行不行?不行。B 的梯度依赖 ,A 的梯度依赖 B,两者都为零时梯度恒为零,训练动不了。都随机的话,初始 ,一上来就扰动了原模型。一随机一置零时,第一步 B 先拿到非零梯度,A 随后也就有梯度了。
- 还有哪些变体?DoRA 把权重拆成幅度和方向分别更新;QLoRA 在量化基座上训练。它们都在"可训练参数少"和"接近全量效果"之间找更好的点。
vLLM 为什么快?PagedAttention 解决什么问题?
简答:解码阶段受显存带宽限制,同一张卡的吞吐主要看能同时塞进多少个请求。vLLM 用 PagedAttention 把 KV 显存的浪费压到很低,用 continuous batching 让 GPU 不空转,于是同样的卡能跑更大的有效 batch。它优化的是显存管理和调度,单条请求未必更快。
细说:把吞吐的来龙去脉按顺序捋一遍。
- decode 每一步都要把权重从显存读一遍。batch 越大,这次读取就被越多请求分摊。7B FP16 读一遍是 14 GB,batch = 1 时这 14 GB 只产出 1 个 token,batch = 32 时产出 32 个。
- batch 能开多大,受 KV Cache 显存的限制。
- 传统做法按最大长度给每个请求预留一段连续显存。比如最大长度 2048,实际只生成了 300 个 token,剩下的预留就空着;请求之间的空洞、长度不一造成的碎片,也都是浪费。
- PagedAttention 把 KV 分成固定大小的块,按需分配,浪费只剩每个序列最后一块没填满的部分。块大小 16 时,每个序列最多浪费 15 个 token 的位置,同样的显存能放下更多请求。
- continuous batching 在每个解码步重新组批,完成的请求立即让位,新请求立即加入。
- 在这之上再叠加 prefix caching、chunked prefill、量化 kernel、CUDA Graph、投机解码、张量并行等。
PagedAttention 具体解决三件事。一是预留浪费和碎片,靠按块分配。二是共享:并行采样、beam search 的多个输出共享同一段 prompt 的 KV 块,谁要改写谁复制(写时复制)。三是弹性:显存不够时可以按块换出或重算,不必直接 OOM。参见 vLLM 论文。
容易踩的坑:低并发时优势不明显。最大并发数开得越大,吞吐越高,但每 token 延迟会变差,要按 SLO 调。显存紧张时频繁抢占(重算或换出)会拉高尾延迟。"vLLM 让模型变快"这种说法是错的,它让的是同一张卡服务更多人。
可能的追问:
- vLLM、SGLang、TensorRT-LLM 怎么选?前两者都支持分页 KV 和连续批处理,SGLang 的 RadixAttention 对多轮对话、共享前缀和结构化生成友好;TensorRT-LLM 在 NVIDIA 硬件上优化得深,但部署流程重。最终要拿自己的负载(输入输出长度分布、并发、前缀复用率)压测来定。
线上 QPS 上来后,先优化模型还是先优化工程?
简答:先工程、后模型。先用数据定位瓶颈,再按"改动小、风险低、见效快"排序:限流排队保证不崩,缓存和裁上下文降负载,调推理参数和扩实例提效率,最后才动模型(量化、蒸馏、换小模型)。每一步都用评测集守住效果。
细说:
| 步骤 | 做什么 | 为什么排在这 |
|---|---|---|
| 0. 定位 | 拆 trace:排队、TTFT、decode、工具耗时、重试各占多少;看瓶颈是 GPU 打满、API 的 RPM/TPM 限额,还是下游工具慢 | 不定位就优化,常常优化错地方 |
| 1. 保稳 | 限流、带截止时间的排队、超时、熔断、降级 | 先不崩,再谈快 |
| 2. 减负载 | 前缀缓存、结果缓存、合并重复请求、裁上下文、减少 Agent 步数、确定性部分改 workflow | 不碰模型,效果风险最低 |
| 3. 提效率 | 自部署:调批处理参数、开 prefix caching、加副本;用 API:多 key、多区域、多供应商、预留吞吐 | 主要花的是钱和运维 |
| 4. 动模型 | 小模型路由、量化、蒸馏或微调小模型、投机解码 | 效果风险最高,必须过评测门槛 |
举个示意的定位结果:一次报销任务 P95 是 12 秒,拆开 trace 发现排队 4 秒、四次模型调用的 prefill 合计 3 秒、decode 2 秒、query_invoices 2.5 秒、其他 0.5 秒。这时候换小模型最多省 decode 那 2 秒的一部分;先解决排队(加副本或优先级队列)和前缀缓存(压 prefill),收益大得多。
估算并发用 Little 定律(在途数 = 到达率 × 平均停留时间),见下文"高并发"一题。
容易踩的坑:一上来就换小模型,成功率下降、重试增多,可能反而更贵更慢。GPU 扩容慢,加载权重往往要分钟级,不能指望临时加机器扛尖峰。优化工程也不只是加机器。
可能的追问:
- 什么时候必须先动模型?单次推理本身就超出 SLO 的时候,例如长输入的 prefill 已经超过首 token 预算;或者单位成本远超预算。这时工程手段只能缓解,必须换更小的、量化过的或结构更省的模型。
放到报销案例里:月底报销高峰,先让交互请求优先、对账类批量任务延后,制度条款检索按制度版本缓存,再考虑把意图识别迁到更小的模型。
扩展:算清低秩更新与系统代价
前面 是为了好懂写的简化版,假设矩阵是方的、也没有缩放。实际的线性层输入输出维度可以不同,LoRA 还会给增量乘一个系数:
- :冻结的原权重,形状 。下标 0 表示"训练开始前的那份"。
- 、:这一层的输入、输出维度。Llama-2-7B 的 q 投影从 4096 维到 4096 维,FFN 的 up 投影从 4096 维到 11008 维。
- :把 维输入压到 r 维;:再从 r 维放回 维。
- (alpha):人为设定的超参数; 是真正乘到增量上的倍率。
可训练参数量是 ,原矩阵是 。两者之比是
代入 4096×4096、r = 8:分子 8 × (4096 + 4096) = 65,536,分母 4096 × 4096 = 16,777,216,比值 1/256 ≈ 0.39%。这是单个矩阵的比例,不是整模型显存减少的比例:还要计入基座权重、优化器状态和激活。参见 LoRA,§4.1。
再按整个 7B 模型算一次(教学算例,Llama-2-7B 结构,32 层,每层挂全部 7 个线性层,r = 16):
- attention 的 q、k、v、o 都是 4096 维到 4096 维,每个 16 × 8192 = 131,072,四个共 524,288;
- FFN 的 gate、up 是 4096 维到 11008 维,down 是 11008 维到 4096 维,每个 16 × (4096 + 11008) = 241,664,三个共 724,992;
- 每层 1,249,280,乘 32 层约 4000 万(39,976,960),占 Llama-2-7B 约 67 亿参数的 0.6% 左右。
这 4000 万参数按每个 16 字节存权重、梯度和 Adam 状态,约 0.64 GB。下文 LoRA / QLoRA 一题里"几百 MB"就是这么来的。
再说 这个倍率。接着用第 3 节的 3×3 小例子:设 (示意),前面已经算出 。
- 取 r = 8、α = 16(真实 r 不是 1,这里只借用倍率),倍率 16 / 8 = 2,;
- 只把 r 改成 16、α 不动,倍率变成 1,同样的 BA 只贡献 ,结果是 ;
- 想保持倍率 2,就要把 α 同步改成 32。
为什么要除以 r? 可以看成 r 个秩 1 矩阵相加,r 翻倍,能叠上去的项也翻倍。除以 r 是为了让更新的量级不随 r 一起涨,改 r 时不用从头重调学习率。rsLoRA 的分析认为,r 大的时候除以 r 压得过头了,更新几乎学不动,所以改成除以 :r = 64、α = 16 时,原版倍率 0.25,rsLoRA 是 16 / 8 = 2。
提高 r 会增加容量和训练开销,效果是变好还是过拟合,要用验证集判断。微调、RAG 和 Prompt 可以组合:它们分别影响行为、外部证据和当前任务的约束,组合后的效果要实测。
系统侧看 可观测性:追踪一条任务的代价:把重试算进实际调用成本,把首 token 时间和任务完成时间分开记,比较的是成功任务的成本。缓存命中、并行和量化是否划算,也要在任务质量不变的前提下判断。
逐题作答
模型选型
Qwen、Llama、DeepSeek、GPT、GLM 这些主流模型怎么对比?你们业务怎么选?(216)
简答:不背榜单。先用几道硬筛子缩小范围:能不能私有化、许可证允不允许、中文和工具调用够不够用、尺寸和手里的硬件匹不匹配;再用自己的评测集打分定版。这个领域的版本按月迭代,面试里讲选择框架就好,具体型号以官方最新发布为准。
细说:截至 2026 年各家族的大致定位如下,这不是排名,具体能力随版本变化:
| 家族 | 形态 | 常被提到的特点 | 选型时要核对 |
|---|---|---|---|
| Qwen(阿里) | 开放权重 + API | 尺寸梯度全(从端侧小模型到大 MoE),中文强,微调与推理框架支持完善 | 不同型号许可证不同,逐个看 |
| Llama(Meta) | 开放权重 | 英文社区与工具链成熟,衍生模型多 | 自定义社区许可证有附加条款;中文和领域能力要实测 |
| DeepSeek | 开放权重 + API | MoE + MLA 结构,推理模型路线,API 价格有竞争力 | 全量模型总参数大,私有化需要多卡;按总参数备显存 |
| GPT(OpenAI) | 以闭源 API 为主,另有开放权重型号 | 综合能力、工具调用、结构化输出成熟 | 数据合规与跨境、无法改权重、供应商绑定 |
| GLM(智谱) | 开放权重 + API | 中文、Agent 与工具调用方向投入多 | 同样按具体版本评测 |
此外还有 Claude、Gemini 等闭源模型,选型逻辑一样。落地分六步:
- 硬约束过滤:数据能不能出域、是不是必须私有化、许可证、手里有几张什么卡。比如公司规定员工发票不能出境,境外 API 在这一步就被筛掉了。
- 能力初筛:公开榜单只当参考,要考虑数据污染,以及榜单题目和你的业务分布不一样。
- 自有评测集:按 Agent 的每一步分别测,只测最终回答会把问题藏起来。
- 成本与延迟压测:用真实的输入输出长度分布来压。
- 定版、灰度、留好回退。
- 定期复评:通过模型网关抽象接口,新模型出来能低成本地对比。
第 3 步的"按步骤测"可以是这样一张表(示意数字):
| 步骤 | 候选 A(小模型) | 候选 B(强模型) |
|---|---|---|
| 意图识别准确率 | 97% | 98% |
| 制度判断带正确引用 | 71% | 93% |
| 建单参数 Schema 合法率 | 99% | 99% |
这张表一出来,结论很自然:意图识别和建单参数用 A,制度判断用 B。只看最终回答的总分,你会得到一个"两者都差不多"的模糊结论。
容易踩的坑:榜单分高不代表在你的任务上好。"中文强"要拿领域术语和真实口语去测,比如"打车超了能报不""滴滴发票抬头写错了"。推理模型(长思考)效果好,但延迟高、思考 token 也计费,不适合每一步都用。只选一个模型,通常不如按步骤分层路由。
可能的追问:
- 新模型发布了要不要换?有评测集就跑一遍,对比效果、每成功任务成本和 P95 延迟,过了门槛再灰度。没有评测集,换模型就是赌。
- 为什么不直接选最强的?强模型单价高、延迟高,简单步骤用它是浪费。选型的目标是在效果门槛之上让成本最低,效果最高的那个未必是答案。
放到报销案例里:报销助手可以做成三档组合:小模型做意图识别和路由,强模型做制度判断和带引用的回答("根据《差旅费管理办法》2026 版第 4.2 条,市内交通标准 80 元,INV-102 超标 38.5 元……"),中档模型加结构化输出做建单参数抽取。
选型除了效果还看什么?效果、成本、延迟、上下文、工具调用、中文怎么权衡?(71)
简答:把六个维度都换成能验收的指标。先设硬门槛,不达标直接淘汰;再在剩下的候选里看"效果和成本"的取舍曲线。别用一个加权总分把短板掩盖掉。
细说:
| 维度 | 怎么量 | 门槛示意 |
|---|---|---|
| 效果 | 评测集任务成功率、引用正确率 | 不低于基线模型若干点 |
| 成本 | 每成功任务成本 | 低于业务可接受值 |
| 延迟 | TTFT P95、端到端 P95 | 交互场景首 token 要短 |
| 上下文 | 有效长度(长文中间位置的检索与多跳测试),不看标称窗口 | 覆盖业务最长输入 |
| 工具调用 | 工具选择准确率、参数 Schema 合法率、多轮一致性 | 合法率接近 100%,否则重试成本高 |
| 中文 | 领域术语、口语、繁简、中英混写 | 业务样本上实测 |
为什么不用加权总分?举个示意的例子:候选 C 效果 95 分、工具 Schema 合法率只有 80%,加权后总分照样最高;可上线后每五次建单就有一次参数不合法要重试,成本和延迟都会被拖垮。所以合法率这种项要当门槛,不该当加分项。
成本一定要按成功的任务折算,因为便宜的模型失败、重试多:
- : 是求和号,读作"sigma"。 是第 i 个任务实际花的钱,失败的任务、重试的调用都算进去。
- :成功完成的任务数。
- 整个式子就是:总共花的钱 ÷ 真正办成的事。
示意算例,各跑 1000 个报销任务:
| 模型 | 每个任务平均花费 | 成功率 | 总花费 | 成功数 | 每成功任务成本 |
|---|---|---|---|---|---|
| 便宜模型 A | 0.05 元 | 50% | 50 元 | 500 | 0.100 元 |
| 贵模型 B | 0.08 元 | 95% | 80 元 | 950 | 0.084 元 |
A 的单价便宜 37.5%,但按办成一件事算反而贵了约 19%。而且失败的那 500 单多半要转人工,人工成本还没进这张表。
分母要和分子用同一批任务,"成功"的判定标准也要在不同模型之间一致,比如都按第 6 站的评测器判。分母很小的时候(成功只有个位数),这个数会剧烈波动,不能拿来做决策。
容易踩的坑:单价便宜不等于真便宜,成功率低、步数多、重试多都会把账做大。推理模型的思考 token 也计费。上下文窗口大不等于长文效果好,128k 窗口的模型可能在 30k 处就开始漏信息。延迟还受部署区域和供应商负载影响,要在目标环境测。
可能的追问:
- 维度冲突时怎么拍板?先问业务哪个是硬约束(合规、实时性、预算上限)。其余的用每成功任务成本和 P95 延迟画一张散点图,把几个候选点交给业务方选。工程师不该替业务做这个取舍。
放到报销案例里:报销场景的硬门槛可以这样定(示意):建单参数 Schema 合法率 ≥ 99%,制度判断引用正确率 ≥ 90%,首 token P95 ≤ 1.5 秒。过了门槛的候选里,再挑每成功报销成本最低的那个。
开源还是闭源?自研底座还是接第三方 API?
简答:应用团队几乎都不该自研底座。开源和闭源的差别主要在三件事:数据合规、规模化以后的成本、可控性。常见的落地方式是闭源 API 打样,开源模型承接量大、稳定或敏感的场景,中间用模型网关解耦。
细说:
| 维度 | 第三方 API | 自部署开源 |
|---|---|---|
| 起步成本 | 低,按量付费 | 高,GPU、运维、团队 |
| 边际成本 | 线性随用量增长 | 利用率高时很低,空闲时也在烧钱 |
| 数据 | 出域,依赖供应商合规条款 | 可完全私有 |
| 定制 | Prompt、有限的托管微调 | 可改权重、量化、任意微调 |
| 能力上限 | 通常更高 | 视型号,差距在缩小 |
| 运维与可用性 | 供应商负责,但受限流与下线影响 | 自己负责扩缩容、故障 |
自部署是否划算,主要看利用率。每个 token 的成本可以这样估(示意公式):
- :每月的 GPU 费用(租金或折旧),单位元 / 月。
- :每月的运维、电费、人力分摊,单位元 / 月。
- :机器满载时每月能产出的 token 数,要在你自己的输入输出长度分布、并且满足延迟 SLO 的条件下压测出来。
- :实际利用率,0 到 1 之间。
- :摊到每个 token 上的成本,单位元 / token。
代入一组示意数字:GPU 每月 30,000 元,运维 10,000 元,合计 40,000 元;满载吞吐 5000 token/s,一个月 30 天共 2,592,000 秒, token。
- 利用率 60%:40,000 ÷ (0.6 × 1.296×10¹⁰) = 40,000 ÷ 7.78×10⁹ ≈ 5.1×10⁻⁶ 元 / token,也就是每百万 token 约 5.1 元;
- 利用率 10%:40,000 ÷ 1.296×10⁹ ≈ 3.1×10⁻⁵ 元 / token,每百万 token 约 30.9 元。
假设同档 API 每百万 token 8 元(示意),盈亏平衡点是 。利用率低于 39% 时,自部署每 token 反而更贵。白天忙、夜里闲的业务,日均利用率很容易掉到 20% 以下,"开源免费"的错觉就出在这里。这个式子也有简化:API 的输入、输出单价不同,要按你的输入输出比折算; 必须在满足延迟 SLO 的 batch 下测,否则会高估。
自研底座(从头预训练)需要海量数据、算力和长期团队,只有把模型本身当战略的公司才做。应用团队能选的中间态有:领域继续预训练(少见)、微调开源模型、从强模型蒸馏小模型。
容易踩的坑:闭源有涨价、模型下线、限流的风险。开源要核对许可证,包括商用条件、用户规模上限、衍生模型的命名要求。私有化也不等于安全,权限和审计照样要做。
可能的追问:
- 怎么避免被一家供应商绑定?统一模型网关接口,工具 Schema 用中性格式,Prompt 按模型版本管理,评测集常备。切换模型时按新模型重调 Prompt 并回归,别指望无缝替换。
放到报销案例里:报销涉及员工个人信息和发票。如果公司要求数据不出域,就选私有化开源模型或合规的境内云服务;如果允许,可以先用闭源 API 跑通评测,再把调用量最大的意图识别迁到自部署小模型,那一步每天几万次调用,最容易把利用率撑起来。
微调
LoRA / QLoRA 的原理是什么?r、alpha 怎么理解?显存是怎么省下来的?(165)
简答:LoRA 冻结基座,只在目标线性层旁边训练一对低秩矩阵;QLoRA 在此基础上把冻结的基座量化到 4-bit,再训练 LoRA。LoRA 省的是梯度和优化器状态,QLoRA 额外省了基座权重本身。为什么有效、r 怎么选,见上文常见追问。
细说:前向计算是
- :这一层的输入向量,长度 ;:输出向量,长度 。
- :原模型本来就会算的那部分, 冻结、不求梯度。
- :旁路。先用 A 把 x 压到 r 维,再用 B 放回来,乘上倍率 后加到主路上。只有 A、B 参与反向更新。
还是用前面那组 3 维小数字:,,α = 16、r = 8,所以 。训练刚开始时 B 全是 0,旁路贡献是 0,,和原模型一模一样;随着 B 被更新,旁路才慢慢把输出往任务需要的方向推。
显存账(7B 教学算例,未计激活):
| 方案 | 基座权重 | 梯度 + 优化器 | 量级 |
|---|---|---|---|
| 全参数(AdamW 混合精度) | BF16 约 14 GB | 梯度 2 + 主权重与两个动量 12,约 14 字节/参数,约 98 GB | 百 GB 级,需多卡 |
| LoRA(全线性层,r=16) | BF16 约 14 GB,冻结 | 只为约四千万参数存,几百 MB | 单张大显存卡可行 |
| QLoRA | NF4 约 4 GB 级 | 同 LoRA | 消费级卡可行 |
QLoRA 那一行的 4 GB 是怎么来的?把它写成一个粗估式:
- :基座参数量。每个参数用 4 bit 的 NF4 存,再加量化常数摊下来的 0.127 bit;除以 8 是把 bit 换成字节。
- 0.127 的来历:每 64 个权重共用一个 FP32 缩放常数,摊到每个权重是 32 / 64 = 0.5 bit。双重量化把这些常数再压成 8 bit(每 256 个常数再共用一个 FP32),变成 8 / 64 + 32 / (64 × 256) ≈ 0.125 + 0.002 = 0.127 bit。
- :LoRA 参数的权重、梯度和 Adam 两个动量,按每参数 16 字节算。
- :激活,取决于序列长度、batch,以及有没有开 gradient checkpointing。
代入 7B:
- 基座:7×10⁹ × 4.127 / 8 ≈ 3.61×10⁹ 字节,约 3.6 GB;
- LoRA:4000 万 × 16 字节 = 0.64 GB;
- 合计约 4.3 GB,再加几 GB 激活(示意,序列越长越多)。
同样的式子代入 65B:65×10⁹ × 4.127 / 8 ≈ 33.5 GB,加上 LoRA 和激活,还在 48 GB 以内,这和 QLoRA 论文"单张 48GB 卡微调 65B"对得上。如果没有双重量化,常数要占 0.5 bit,65B 会多出约 3 GB。
激活这时往往成了最大的开销,所以 QLoRA 常配合 gradient checkpointing(用重算换显存)和较短的序列长度。
QLoRA 有三个关键设计(QLoRA 论文):NF4 数据类型(针对正态分布权重设计的 4-bit 格式);双重量化,也就是上面把量化常数本身再量化一次;分页优化器,显存尖峰时把优化器状态换页到 CPU 内存。计算时 4-bit 权重要先反量化到 BF16 再做矩阵乘,所以它省显存但不提速,通常还更慢。
容易踩的坑:QLoRA 的 adapter 是在量化基座上训出来的,合并回 FP16 基座后行为可能略有偏差,部署前要按实际的部署形态重测。"QLoRA 更快"是错的。LoRA 只省训练显存,推理显存不变,合并后和原模型一样大。
可能的追问:
- 多个 LoRA 怎么部署?不合并,共享一份基座,按请求加载不同的 adapter。主流推理引擎都支持多 LoRA 服务,适合多租户或多任务;只有一个任务时就合并,省掉旁路开销。按上面的算例,r = 16 的一个 adapter 只有约 4000 万参数,BF16 存约 80 MB,一张卡上挂几十个也放得下。
放到报销案例里:假设要训一个专门抽建单参数的小模型,基座 7B,只有一张 24 GB 的卡。全量微调要 112 GB 起,直接排除;LoRA 要 14 GB 基座加激活,序列一长就紧;QLoRA 只要 4 GB 多加激活,开 gradient checkpointing 后剩下的十几 GB 足够留给激活。训完以后在 create_reimbursement_ticket 的参数合法率上,和第 6 站的基线对比。
SFT、RLHF、DPO、GRPO 是什么关系?什么时候用哪个?(192/115/97)
简答:它们都是后训练手段。SFT 用示范教"怎么答";RLHF 和 DPO 用偏好教"哪个答得更好";GRPO 是一种省掉价值模型的强化学习算法,特别适合答案能被自动判对错的推理任务。应用团队多数停在 SFT,遇到偏好类问题再加 DPO。
细说:
| 方法 | 数据 | 怎么训 | 成本 | 典型用途 |
|---|---|---|---|---|
| SFT | (指令, 理想回答) | 最大似然,只对回答部分算 loss | 低 | 格式、风格、工具调用模板 |
| RLHF(PPO) | 偏好对,先用来训奖励模型 | 训奖励模型,再用 PPO 优化策略并用 KL 约束不偏离参考模型;训练时策略、参考、奖励、价值四个模型都要在场 | 高,调参难 | 通用对齐 |
| DPO | (prompt, 好回答, 差回答) | 直接在偏好对上优化,不训显式奖励模型、不在线采样 | 中 | 风格偏好、拒答、减少啰嗦 |
| GRPO | 题目 + 可自动判分的奖励 | 每题采样一组回答,用组内相对得分作优势,不要价值模型 | 中高 | 数学、代码等可验证推理 |
三种数据长什么样,用报销场景各举一条(每行一条样本):
{"sft": {"prompt": "INV-102 打车 118.5 元能报吗?", "response": "根据第 4.2 条,市内交通标准 80 元,超标 38.5 元……"}}
{"dpo": {"prompt": "没有发票能先建单吗?", "chosen": "需要先上传发票,我可以先帮你保存草稿。", "rejected": "可以,已为你建单 R-55821。"}}
{"grpo": {"prompt": "金额 118.5 元,标准 80 元,超标多少?", "reward": "答案等于 38.5 记 1,否则记 0"}}
DPO 的损失。大白话:对每一对偏好数据,看新模型相对参考模型"把好回答的概率抬高了多少、把差回答的概率压低了多少",两者之差越大,损失越小。
| 符号 | 含义 |
|---|---|
| prompt | |
| 、 | 好回答(w 是 win)、差回答(l 是 lose) |
| 正在训练的模型在给定 x 时生成 y 的概率。竖线读作"在……条件下",这是条件概率 | |
| 参考模型(通常就是 SFT 后的模型,训练中冻结)给出的同一个概率 | |
| 对数比值:大于 0 表示新模型比参考模型更倾向于生成这个回答,小于 0 表示更不倾向。log 是自然对数 | |
| 温度系数,常见取 0.1 左右,控制"允许偏离参考模型多远" | |
| sigmoid 函数 ,把任意实数压到 0 和 1 之间, | |
| 期望,这里就是对数据集里所有偏好对取平均 | |
| 前面的负号 | log σ 小于 0,加负号变成正的损失,越小越好 |
手算一对(示意概率,真实的整句概率是每个 token 概率连乘,小得多,代码里直接用对数概率相加)。取 β = 0.1:
- 好回答:参考模型给 0.02,新模型给 0.04,比值 2,;
- 差回答:参考模型给 0.03,新模型给 0.015,比值 0.5,;
- 括号里:0.1 × (0.693 − (−0.693)) = 0.1 × 1.386 = 0.1386;
- ;
- 损失 。
训练刚开始时新模型就是参考模型,两个比值都是 1、对数都是 0,括号里是 0,,损失 。上面那一步把损失从 0.693 降到了 0.626。
同样的概率换成 β = 0.5:括号里 0.5 × 1.386 = 0.693,,损失 ≈ 0.405。β 越大,同样的偏离换来的损失下降越多,模型只要离参考模型一点点就能把损失压低,所以它也就不会跑太远。β 越小,模型要偏离得更多才能拿到同样的损失,约束更松。
这个损失只看"好回答的对数比值减去差回答的对数比值"这个差,所以会出一种怪事:新模型把好回答的概率从 0.02 降到 0.01(对数比值 −0.693),差回答从 0.03 降到 0.0015(对数比值 −3.0),括号里是 0.1 × (−0.693 + 3.0) ≈ 0.230,损失 ≈ 0.585,比起点还低。好回答的概率减半了,损失却说"进步了"。这就是后面坑里说的"好回答的概率也一起下降"。
PPO 的 clip。RLHF 里用 PPO 更新策略时,每一步不能改得太猛,PPO 的办法是把"新旧策略的概率比"限制在一个小区间里:
- :第 t 个 token 位置; 是到这里为止的上下文, 是这一步生成的 token。
- :新策略和采样时旧策略对同一个 token 的概率之比。等于 1 表示没变,1.5 表示新策略生成它的概率是原来的 1.5 倍。
- :优势,表示这个 token "比平均水平好多少"。正数表示该多做,负数表示该少做。PPO 里它由价值模型(critic)估计,拿实际回报减去价值模型预测的平均水平。
- :把 ρ 截在 之间,小于下界取下界,大于上界取上界。ε 常取 0.2,区间就是 [0.8, 1.2]。
- :两者取小。这个目标是要最大化的,代码里取负号当损失。
取 ε = 0.2,算三种情况:
| 情况 | ρ | Â | ρ × Â | clip 后 × Â | 取 min | 含义 |
|---|---|---|---|---|---|---|
| 好 token,已经抬得很高 | 1.5 | +2 | 3.0 | 1.2 × 2 = 2.4 | 2.4 | 被截住,不再继续往上推 |
| 好 token,还没抬起来 | 0.9 | +2 | 1.8 | 0.9 × 2 = 1.8 | 1.8 | 正常更新 |
| 坏 token,概率反而升高了 | 1.5 | −2 | −3.0 | 1.2 × (−2) = −2.4 | −3.0 | 不截,全力把它压回去 |
第一行取到的是截断后的常数 2.4,对 θ 没有梯度,这一步就不会把这个 token 的概率推得更高;第三行取的是没截断的 −3.0,梯度照常,错误方向的改动会被纠正。min 的作用就是"好处封顶、坏处不打折"。不加 clip 的话,一批数据上就可能把策略改得面目全非,之后采样出来的数据分布也跟着崩。
GRPO 的优势。GRPO 沿用上面的 clip 目标,但把 的来源换掉了:不训价值模型,对同一道题采样 G 个回答,用组内的相对得分当优势:
- :每道题采样的回答数,比如 4 或 8。
- :第 i 个回答的奖励,比如答对记 1、答错记 0。
- mean、std:这 G 个奖励的平均值和标准差。
- :第 i 个回答的优势,这个回答里每个 token 都用同一个值。
拿"118.5 元、标准 80 元,超标多少"这道题采样 4 个回答:38.5、38、118.5、40,只有第一个对,奖励是 [1, 0, 0, 0]。
- 平均值 = 0.25;
- 偏差 = [0.75, −0.25, −0.25, −0.25],平方 = [0.5625, 0.0625, 0.0625, 0.0625],平均 = 0.75 / 4 = 0.1875;
- 标准差 = √0.1875 ≈ 0.433(这里除以 G;有的实现除以 G − 1,结果是 0.5,数值会略有不同);
- 优势 ≈ [0.75 / 0.433, −0.25 / 0.433, …] = [1.73, −0.58, −0.58, −0.58]。
四个里只有一个答对时,这个答对的回答拿到很大的正优势 1.73。反过来如果奖励是 [1, 1, 1, 0],优势就是 [0.58, 0.58, 0.58, −1.73],那个唯一答错的被重罚。四个全对或全错时,标准差是 0,实现里分母会加一个很小的数防止除零,优势全是 0,这道题对训练没有贡献。所以题目难度要配好:太简单或太难的题都白采样。
典型流水线是先预训练,再 SFT,然后做偏好对齐(RLHF 或 DPO),最后针对推理能力做强化学习。RLHF 三阶段见 InstructGPT,DPO 见 原论文,GRPO 最早在 DeepSeekMath 中提出,后来用于 DeepSeek-R1 的推理强化训练。
容易踩的坑:只做 SFT,模型只见过正例,学不会"不要这样答",还会照抄示范里的错误。DPO 对数据质量和 β 敏感,可能出现上面算过的"好回答概率也一起下降",以及输出越来越长等现象。强化学习有 reward hacking 的风险:GRPO 的效果上限就是 verifier 的可靠程度,比如奖励只检查"答案里有没有 38.5",模型可能学会把好几个数都写上。
可能的追问:
- 为什么 SFT 之后还要 RLHF?只用 SFT 行不行?SFT 只告诉模型一个好答案长什么样;偏好训练利用"A 比 B 好"这种比写示范便宜得多的信号,还能压制具体的坏行为。简单的格式类任务只做 SFT 往往就够。
- DPO 相比 PPO 改进了什么?它把"KL 约束下的最优策略"和奖励之间的解析关系代入目标,消掉了显式的奖励模型,变成对偏好对的分类式损失:离线、稳定、省显存。代价是只用离线数据,没有在线探索。
放到报销案例里:报销场景里,SFT 用来教工单 JSON 格式和"缺发票时先追问再建单"。如果评测发现模型在证据不足时爱下结论,就用 DPO 偏好对调偏好,让"带引用的拒答"胜过"臆断",上面 JSON 里的 dpo 那条就是一例。GRPO 一般用不上,除非有能自动判分的计算类子任务,比如超标金额计算。
微调数据怎么构造、清洗、配比?(82)
简答:微调能做到什么程度,主要看数据。先定义"要模型学会的行为"并留出评测集;再从真实日志、专家撰写、强模型蒸馏三路取数据,经过去重、校验、脱敏、质检和防泄漏;最后按"任务数据 + 通用数据 + 负例与边界例"配比,用错例分析驱动迭代。
细说:
- 先定目标与评测。最先划出 held-out 测试集并冻结,后续所有数据都要和它去重。
- 数据来源有四类:
- 线上日志:分布最真实,需要授权和脱敏;
- 专家撰写:质量高、成本高,适合覆盖难例;
- 强模型合成或蒸馏:规模大,但要确认服务条款允不允许用它的输出训练模型;
- 拒绝采样:一条输入生成多个回答,用规则或 verifier 只留正确的。
- 清洗:精确去重加近似去重(MinHash 或向量相似度);格式校验(JSON 能解析,工具参数能通过 Schema);质量打分(规则 + LLM 评分 + 人工抽检);PII 脱敏;和评测集做 n-gram 或语义去污染。
- 配比:业务任务数据为主,混入一定比例的通用指令数据防止遗忘;难例、边界例上采样;必须有负例,比如该拒答的、该追问的、工具报错后怎么恢复的;多轮对话和工具轨迹要保持完整,别只截最后一轮。
- 格式对齐:训练时的 chat template、工具描述格式要和推理时完全一致;只对 assistant 部分计算 loss。
- 迭代:训练、评测、错例归类、针对性补数据,一味加量没用。
举个清洗前后的对比(示意)。原始日志里有这样一条:
改之前:用户 张三 13812345678:打车 118.5 能报吗
助手:可以报,标准是 100 元。
改之后:用户 [姓名] [手机号]:打车 118.5 能报吗
助手:根据第 4.2 条,市内交通标准 80 元,INV-102 超标 38.5 元,需要例外审批。
这一条要同时做三件事:脱敏,改正旧版制度的错误金额,再把回答改成带依据的格式。如果不改,模型会把"100 元"当成正确答案学进去。
配比也可以给个起点(示意,要靠消融调):业务任务 70%,通用指令 20%,负例和边界例 10%。
质量往往比数量重要:LIMA 用约一千条精选样本,就让强基座获得了不错的指令遵循能力。
容易踩的坑:合成数据同质化,会把教师模型的错误放大。配比没有通用公式,只能做消融。只有成功轨迹的话,模型就学不会出错后的恢复。数据里混进过时内容(比如旧版制度金额),微调后就固化成了错误。
可能的追问:
- 数据要多少才够?看任务。格式和风格类,几百到几千条高质量样本常能见效;复杂能力需要更多。用学习曲线判断:拿一半数据和全部数据各训一次,提升还明显就继续加。比如 1000 条时合法率 92%,2000 条时 97%,那就值得再补;2000 到 4000 只涨到 97.5%,再加就不划算了(示意)。
- 怎么防灾难性遗忘?混入通用数据,LoRA 配低学习率、少轮次,并且在通用基准和业务集上同时回归。
放到报销案例里:建单参数抽取的数据,从历史工单脱敏构造,必须覆盖缺字段、金额和发票不一致、重复发票等样本。具体报销额度属于制度,不进训练数据,由 RAG 提供。
微调、Prompt、RAG 怎么选?(21)
简答:按"缺的是什么"来选。缺当前任务的约束,用 Prompt;缺外部的、会变的知识,用 RAG;缺稳定的行为模式、格式或领域表达能力,才微调。三者经常叠加,一般先试 Prompt,再上 RAG,最后才微调。
细说:
| 症状 | 首选 | 原因 |
|---|---|---|
| 不知道私有或最新事实 | RAG | 可更新、可删除、可引用 |
| 需要可追溯的出处 | RAG | 微调没有出处 |
| 需要按用户权限给不同答案 | RAG(检索时过滤) | 微调无法做权限隔离 |
| 格式不稳 | Prompt + 结构化输出,仍不稳再 SFT | 先用便宜手段 |
| 风格、语气、术语用法 | few-shot,不够再微调 | 行为类问题适合微调 |
| 大模型 + 长 Prompt 太贵太慢 | 微调小模型,把长指令学进权重 | 降本降延迟 |
最后一行可以算一笔账(示意):系统提示里有 3000 token 的建单规则和 few-shot,每天调用 2 万次,就是每天 6000 万输入 token。把这些规则用 SFT 内化到小模型以后,提示缩到 500 token,每天省下 5000 万 token。
判断顺序可以写成:
if 缺的是事实/知识: RAG(必要时配合长上下文)
elif 缺的是本轮约束: Prompt / few-shot / 结构化输出
elif 行为在大量样本上仍不稳 or 成本延迟压力大: 微调
容易踩的坑:用微调灌知识,难更新、难删除、无法溯源,还容易一本正经地编。用 RAG 修行为,检索再准,模型也不会因此学会某种格式。"长上下文能替代 RAG"也不成立:成本、长文中间的信息丢失、权限过滤都是问题。
可能的追问:
- RAG 和微调能一起用吗?能。典型做法是微调模型"如何使用检索到的证据"(按格式引用、证据不足时拒答),知识本身仍由 RAG 提供,RAFT 就是这个思路。
放到报销案例里:报销制度条款走 RAG;"先查制度、再查发票、再建单"的流程由 Agent 编排和 Prompt 约束;工单 JSON 先靠结构化输出,仍然不稳再考虑微调。
推理部署
KV Cache 为什么能加速?显存怎么算?(64)
简答:自回归生成每一步只新增一个 token,前面 token 的 K、V 不会再变。把它们缓存下来,每一步就只需算新 token 的 Q、K、V,再和历史做注意力,不用把整段前缀重算一遍。代价是显存随层数、KV 头数、上下文长度和并发线性增长。
细说:
先看没有缓存时:生成第 t 个 token,要把前 t 个 token 重新过一遍所有层来得到 K、V。生成 n 个 token,投影的计算量是 1 + 2 + … + n,随 n 平方增长。比如 prompt 有 1000 个 token、要生成 100 个,没有缓存时第 100 步要重算 1099 个 token 的投影,有缓存只算 1 个。
有缓存时:每一步只对新 token 做投影,K、V 追加进缓存。注意力仍然要读全部历史的 KV,所以 decode 的瓶颈从算力变成了"读 KV 和权重的显存带宽"。
KV 占多少显存:
| 符号 | 含义 | Llama-2-7B 取值 |
|---|---|---|
| 2 | K 和 V 各存一份 | 2 |
| 层数 | 32 | |
| KV 头数(MHA 等于注意力头数,GQA 更少) | 32 | |
| 每个头的维度 | 128 | |
| 上下文长度,单位 token(输入加已生成) | 8192 | |
| 同时在跑的请求数 | 1 | |
| bytes | 每个数占几个字节,FP16 是 2 | 2 |
代入:2 × 32 × 32 × 128 = 262,144 个数每 token,× 2 字节 = 524,288 字节,即 0.5 MiB 每 token;× 8192 token = 4 GiB 一条请求。并发 8 条就是 32 GiB,已经比 14 GB 的权重大一倍多。
不同结构的教学算例(FP16,单条 8k 上下文):
| 结构 | 层数 / KV 头 / head_dim | 每 token | 8k 一条 |
|---|---|---|---|
| Llama-2-7B 类(MHA) | 32 / 32 / 128 | 约 0.5 MB | 约 4 GB |
| Llama-3-8B 类(GQA) | 32 / 8 / 128 | 约 128 KB | 约 1 GB |
| Llama-2-70B 类(GQA) | 80 / 8 / 128 | 约 320 KB | 约 2.5 GB |
GQA 那两行就是把 从 32 改成 8,KV 直接变成四分之一。这个公式是线性的:任何一项翻倍,KV 就翻倍,所以长上下文和高并发叠在一起时最先爆的往往是 KV。公式没算的部分有:PagedAttention 每个序列最后一块的空位、MLA 这类压缩结构(要按潜向量维度重算),以及滑动窗口注意力( 封顶在窗口大小)。
减 KV 的手段:MQA / GQA 减少 KV 头;MLA(DeepSeek-V2 起)缓存低维潜向量;KV 量化到 FP8 / INT8,bytes 从 2 变成 1,直接减半;滑动窗口注意力;跨请求共享前缀;把冷的 KV 卸载到 CPU 内存或 SSD。
容易踩的坑:长上下文、高并发时 KV 会超过权重本身。KV 量化可能影响长文精度。"KV Cache 让每步计算量变成常数"不准确:投影是常数,注意力读取的历史仍随长度增长。整体显存预算见上文常见追问。
可能的追问:
- KV Cache 和 API 的"提示缓存"是什么关系?前者是单个请求内部复用已生成 token 的 K、V;后者是跨请求复用相同前缀已经算好的 KV,省掉重复的 prefill。多数主流 API 对命中缓存的输入按折扣计费,规则各家不同,以官方文档为准。
放到报销案例里:报销助手每次调用大约 6000 token 输入。用 Llama-3-8B 类结构,每条约 6000 × 128 KB ≈ 0.73 GiB;如果卡上留了 40 GB(约 37 GiB)给 KV,大约能同时跑 50 路。换成 MHA 结构的 7B,每条约 2.9 GiB,只能跑 12 路左右。月底高峰时,这个数直接决定要几张卡。
vLLM 的 PagedAttention 和 Continuous Batching 具体怎么工作?(51)
简答:PagedAttention 借鉴操作系统的分页:把每个请求的 KV 切成固定大小的块,用块表映射到不连续的物理显存。Continuous Batching 在每个解码步重新组批,完成的请求立刻退出,新请求立刻加入。前者让更多请求放得下,后者让 GPU 一直有活干。为什么因此变快,见上文常见追问。
细说:
PagedAttention 的做法:
- 每个块存固定数量 token 的 KV,vLLM 默认 16 个 token 一块;
- 每个序列维护一张块表,记录每个逻辑块号对应哪个物理块号,生成到哪分配到哪;
- 注意力 kernel 按块表去不连续的物理块里取 KV;
- 多个输出共享同一段 prompt 时,物理块用引用计数共享,谁要改写谁复制(写时复制);
- 块池耗尽时调度器抢占部分请求:把它们的块换出到 CPU,或者直接释放,之后重算。
看一个具体的块表。一个请求已经有 40 个 token,块大小 16,需要 3 个块(16 + 16 + 8),最后一块空着 8 个位置:
序列 A 的块表 物理显存
逻辑块 0 → 物理块 7 [A 的 token 0-15]
逻辑块 1 → 物理块 2 [A 的 token 16-31]
逻辑块 2 → 物理块 11 [A 的 token 32-39,空 8 格]
物理块 7、2、11 不需要挨着。等 A 生成到第 48 个 token,再从空闲池里随便拿一块挂到逻辑块 3 上。按老办法,要按最大长度(比如 2048)一次性预留连续空间,A 只用了 40 个位置,剩下两千多个都闲着。
Continuous Batching(迭代级调度,最早见 Orca 的工作):
while serving:
batch = running + admit(waiting, free_blocks) # 有空块就接新请求
step(batch) # 整批前向一步
for seq in batch:
if seq.finished:
free(seq.blocks); respond(seq) # 立即让位
对比静态批:整批要等最长的那条生成完,短请求白等,新请求只能排队,还得按最长长度预留 KV。举个示意的例子:一批 4 条,要生成的长度分别是 20、30、40、500 个 token。静态批要跑 500 步,前三条早就结束了,它们占着的位置空转了四百多步;连续批处理下,第 20 步那条一结束,空出来的位置马上给排队的新请求。
Chunked prefill:把长 prompt 切成多段,和其他请求的 decode 混在同一步里调度,免得一个超长输入卡住所有人的吐字。
容易踩的坑:块太小,块表和 kernel 的开销增加;块太大,最后一块的浪费增加。最大并发序列数和每步最大 token 数是调吞吐与延迟的旋钮。抢占后重算浪费算力,换出则占 PCIe 带宽。
可能的追问:
- 为什么不直接把静态 batch 开大?请求长度差异大,静态批的 padding 和等待浪费很严重,而且必须按最长序列预留显存。只有按步动态进出,才能真正把 GPU 吃满。
放到报销案例里:报销助手的请求长短差异很大:意图识别只输出几个 token,带引用的制度解释可能要两三百个。用静态批的话,意图识别请求要陪着长回答一起等;连续批处理下它几步就退出,把位置让给下一个员工。
量化怎么做?INT8 / INT4、AWQ / GPTQ 有什么区别?精度损失怎么控制?(23)
简答:量化就是用更少的比特表示权重(有时也包括激活和 KV),拿精度换显存和带宽。部署中最常见的是只量化权重的 W8 / W4。GPTQ 和 AWQ 都是训练后的权重量化,区别在于用什么信息来决定怎么量化误差最小。
细说:基本操作是按缩放因子 s 和零点 z 把浮点数映射到整数,用的时候再反量化回来:
| 符号 | 含义 |
|---|---|
| 原始浮点权重 | |
| 缩放因子,一个整数台阶对应多大的浮点值 | |
| 零点,让浮点的 0 落在某个整数上,用于处理不对称的分布 | |
| round | 四舍五入到最近的整数 |
| clip(v, a, b) | 小于 a 取 a,大于 b 取 b,保证结果在整数能表示的范围内 |
| 、 | 整数范围,无符号 4-bit 是 0 到 15,有符号 INT8 是 −128 到 127 |
| 存下来的整数 | |
| 反量化后的近似值,读作"x hat",误差就是 |
s 和 z 一般按这一组权重的最大最小值来定:
手算一组 4 个权重 [−0.8, −0.1, 0.35, 1.0],量化到无符号 4-bit(0 到 15):s = (1.0 − (−0.8)) / 15 = 1.8 / 15 = 0.12,z = round(0.8 / 0.12) = round(6.67) = 7。
| x | x / s | round | + z 得 q | x̂ = 0.12 × (q − 7) | 误差 |
|---|---|---|---|---|---|
| −0.8 | −6.67 | −7 | 0 | −0.84 | −0.04 |
| −0.1 | −0.83 | −1 | 6 | −0.12 | −0.02 |
| 0.35 | 2.92 | 3 | 10 | 0.36 | +0.01 |
| 1.0 | 8.33 | 8 | 15 | 0.96 | −0.04 |
误差都在 ±0.06(半个台阶 s / 2)以内,16 个整数台阶覆盖这 4 个数绰绰有余。
现在把最后一个权重换成离群值 10.0:s = (10.0 + 0.8) / 15 = 0.72,z = round(0.8 / 0.72) = round(1.11) = 1。−0.1 变成 round(−0.14) + 1 = 1,0.35 变成 round(0.49) + 1 = 1,反量化后两个都是 0.72 × (1 − 1) = 0。原来一个负一个正的两个权重,量化后变成了同一个 0,信息直接丢了。一个离群值把台阶拉宽了 6 倍,其他所有权重都跟着变粗。
这个例子能解释后面几种方案为什么这么设计:分组量化(每 64 或 128 个权重一组,各算各的 s 和 z)把离群值的影响限制在组内;AWQ 找出对激活影响大的通道单独保护;SmoothQuant 把激活里的离群值转移到权重上。分组也有代价,每组要额外存 s 和 z:组大小 128、两个 FP16 常数,摊到每个权重是 32 / 128 = 0.25 bit,INT4 实际是 4.25 bit 左右,7B 权重约 7×10⁹ × 4.25 / 8 ≈ 3.7 GB,比 3.5 GB 略多(示意,具体看格式)。
粒度可以是整个张量、每个输出通道,或者每组若干个权重:组越小越准,元数据也越多。常见方案:
| 方案 | 量化对象 | 核心思路 | 适用 |
|---|---|---|---|
| W8A8(如 SmoothQuant) | 权重 + 激活 | 激活有离群值难量化,按通道把量化难度转移到权重上,可用 INT8 矩阵乘 | 高吞吐服务 |
| FP8 | 权重 + 激活(可含 KV) | 新一代 GPU 原生支持,精度损失通常较小 | 硬件支持时首选之一 |
| GPTQ(W4A16) | 权重 | 逐层量化,用近似二阶信息把已量化权重的误差补偿到剩余权重,需少量校准数据 | 显存紧张的部署 |
| AWQ(W4A16) | 权重 | 根据激活幅度找出少量关键权重通道,用缩放保护它们,无需反向传播 | 同上,泛化通常稳 |
| GGUF 等 | 权重 | 多种比特格式,面向 CPU / 端侧 | 本地与边缘设备 |
W4A16 为什么能提速:decode 受带宽限制,读的字节少了,速度就上去了。但计算前要反量化,batch 很大、转为算力受限时,收益会变小。
控制精度损失的办法:校准数据贴近业务分布;embedding、输出层等敏感层保留高精度;先试 8-bit 再试 4-bit;在业务评测集上对比,尤其是长文、数学、代码和工具 JSON 这类对精度敏感的任务。
容易踩的坑:困惑度差不多,不代表业务任务没掉点。小模型比大模型对量化更敏感。没有对应 kernel 的硬件上,量化可能只省显存、不提速。
可能的追问:
- 量化后效果掉了怎么补?先换更细的分组或更高的比特,换贴近业务的校准集,对关键层用混合精度;还不够,再做量化感知训练,或者在量化基座上训一个 LoRA 做补偿。
放到报销案例里:建单参数里的金额必须精确。量化后要专门测 create_reimbursement_ticket 的 amount 字段,看 118.5 会不会被写成 118 或 118.50 以外的值,只看通用问答的分数发现不了这类退化。
Prefill 和 Decode 有什么区别?首 token 延迟和吞吐怎么优化?
简答:Prefill 一次并行处理整段输入,属于算力受限,决定首 token 延迟(TTFT,Time To First Token)。Decode 逐个生成 token,每一步都要读一遍权重和 KV,属于带宽受限,决定每 token 延迟(TPOT,Time Per Output Token)和吞吐。两个阶段瓶颈不同,优化手段也不同。
细说:
| 阶段 | 计算特征 | 对应指标 | 主要手段 |
|---|---|---|---|
| Prefill | 大矩阵乘,算力受限 | TTFT | 缩短输入、前缀缓存、chunked prefill、FlashAttention、张量并行 |
| Decode | 矩阵乘向量,带宽受限 | TPOT、吞吐 | 连续批处理加大 batch、权重与 KV 量化、GQA / MLA、投机解码 |
端到端时间可以粗拆为:
- :在服务端排队等待的时间。
- :处理完整段输入的时间。用户看到的 TTFT 约等于 。
- :输出 token 数。
- :每生成一个 token 的时间,就是 TPOT。压测工具里常按 来算,因为第一个 token 已经算在 TTFT 里了。
代入一次报销助手的模型调用(示意):排队 0.1 秒,prefill 0.4 秒,所以 TTFT = 0.5 秒;输出是一次工具调用,60 个 token,TPOT 30 毫秒,decode 用 60 × 0.03 = 1.8 秒;合计约 2.3 秒。一次报销任务要四次模型调用(路由、制度判断、抽参数、最终回复),就是 9.2 秒左右,再加上 search_policy、query_invoices、create_reimbursement_ticket 三次工具调用。式子没算网络和工具耗时,也没算多轮之间的串行等待,Agent 的端到端时间要按 trace 把每一段加起来。
decode 每步至少要把权重和这批请求的 KV 从显存读一遍,所以有一个下界:
- :权重字节数;:一条请求当前的 KV 字节数;:batch 里的请求数。
- :显存带宽,单位字节每秒。
- 读作"大于约等于":实际时间只会比这个下界长,因为 kernel 效率达不到 100%。
- 吞吐:整张卡每秒产出的 token 数。
教学算例(理论上限,示意):7B FP16 权重 14 GB,每条请求当前 2048 token 的 KV 约 1.07 GB(2048 × 0.5 MiB),带宽假设 1 TB/s。
| batch | 每步要读 | TPOT 下界 | 吞吐上限 |
|---|---|---|---|
| 1 | 14 + 1.07 = 15.1 GB | 15.1 ms | 1 / 0.0151 ≈ 66 token/s |
| 8 | 14 + 8.6 = 22.6 GB | 22.6 ms | 8 / 0.0226 ≈ 354 token/s |
| 32 | 14 + 34.4 = 48.4 GB | 48.4 ms | 32 / 0.0484 ≈ 661 token/s |
batch 从 1 涨到 32,吞吐涨了 10 倍,每个用户感受到的 TPOT 却慢了 3 倍多,吞吐和延迟天然矛盾,指的就是这个。同样 batch 32 换成 GQA 结构(KV 变成四分之一),每步读 14 + 8.6 = 22.6 GB,吞吐上限升到约 1416 token/s。只看权重时,batch = 1 的上限是 1 TB/s ÷ 14 GB ≈ 70 token/s;量化到 INT4 只读约 3.5 GB,上限约提高到 4 倍。实际受 kernel 效率等影响,达不到这些上限。
两个进阶手段:
- PD 分离:prefill 和 decode 放在不同的 GPU 池里,各自扩缩容,避免长输入的 prefill 打断别人的吐字。代价是 KV 要跨机传输。
- 投机解码:小模型先草拟几个 token,大模型一次并行验证。按标准的接受规则,输出分布和大模型自己生成的一致;草稿的接受率越高,越快。
容易踩的坑:吞吐和延迟天然矛盾,batch 越大吞吐越高、TPOT 越差,上面那张表就是。Agent 的每一步往往"输入很长、输出很短",一次工具调用只有几十个 token,prefill 占大头,所以前缀缓存比 decode 优化更值钱。
可能的追问:
- 用户说"慢",先看哪个指标?先分清是首字慢、吐字慢,还是步骤多。首字慢查排队和 prefill(输入太长、没命中缓存);吐字慢查 decode 的并发和带宽;整个任务慢,多半是 Agent 步数和工具耗时。
放到报销案例里:报销助手每一步都带着系统提示、工具定义和证据,输出却只是一次工具调用。把稳定内容固定在前缀、让前缀缓存命中,是降首 token 延迟最直接的一招:上面算例里 0.4 秒的 prefill,如果 6000 token 里有 5000 token 命中缓存,只需处理剩下的 1000 token,粗略估计能降到 0.1 秒左右(示意)。
成本与并发
Agent 怎么降本?模型侧和工程侧各有哪些手段?(35)
简答:先把口径定成"每次成功任务的成本",再从两侧下手。工程侧减少调用次数和 token,最便宜,先做;模型侧让每个 token 更便宜,靠路由、小模型、量化、蒸馏。每个动作都要在评测集上证明效果没掉。
细说:
| 侧 | 手段 | 原理 | 风险 |
|---|---|---|---|
| 工程 | 少调用 | 确定性步骤改成 workflow 或规则,独立步骤合并或并行,设步数上限 | 灵活性下降 |
| 工程 | 少 token | 见下文"token 优化" | 压缩丢信息 |
| 工程 | 缓存 | 前缀缓存、只读工具结果缓存、语义缓存 | 语义缓存误命中 |
| 工程 | 离线批处理 | 非实时任务走批量接口,不少供应商对此有折扣 | 延迟变长 |
| 工程 | 少重试 | 结构化输出、参数校验前置、失败快速终止 | 基本没有 |
| 模型 | 路由 / 级联 | 先给小模型,置信度低或校验失败再升级 | 路由误判 |
| 模型 | 小模型 + 微调 / 蒸馏 | 把大模型在特定任务上的行为迁到小模型 | 训练与维护成本 |
| 模型 | 量化、推理优化 | 自部署时降每 token 成本 | 精度损失 |
级联是最常问的一种。做法是每个请求先给小模型,小模型没把握或输出没过校验,再升级给大模型。平均下来一个请求花多少钱:
- :每个请求的期望成本,就是很多请求平均下来的成本( 同前文 DPO 处的期望)。
- :小模型调用一次的成本。每个请求都要先过小模型,所以这一项总是全额计入。
- :大模型调用一次的成本。
- :升级率,即被转给大模型的请求比例,取 0 到 1。
教学算例(示意):小模型每次 0.1 单位、大模型 1 单位、升级率 30%,期望成本 0.1 + 0.3 × 1 = 0.4。用 10 个请求核对一下:10 个都过小模型花 1.0,其中 3 个再过大模型花 3.0,合计 4.0,平均 0.4。全用大模型是 1.0,所以省了六成。
反过来算临界点:令 ,得 。升级率超过 90%,级联反而比直接用大模型贵,比如 95% 时是 0.1 + 0.95 = 1.05。式子还没算两件事:小模型答错却没被拦下的请求(成功率下降),以及误判导致的失败重试。所以最终还是要换算成每成功任务成本来比。
容易踩的坑:只比较单价、不看成功率。语义缓存把依赖用户身份、权限或时间的答案共享给了别人,这是事故。多个降本动作叠在一起后效果下滑,单个看都没问题,所以要整体回归。
可能的追问:
- 降本第一步做什么?先用 trace 做成本归因,按步骤、模型、工具、是否重试拆开。常见的情况是大头在重复携带的上下文和少数失败的长尾任务,先砍它们。
放到报销案例里:"能不能报"的结论不能按问题文本缓存,因为它依赖个人发票和职级:同样问"打车 118.5 元能报吗",P6 在上海超标 38.5 元,换一个职级或城市答案就可能不同。制度条款的检索结果则可以按"制度版本 + 查询"缓存,第 4.2 条在 2026 版生效期间不会变。
高并发下怎么保证 Agent 服务稳定?限流、排队、熔断、缓存、多实例怎么配合?(30)
简答:沿请求的调用链路分层设防。入口限流挡住超额流量,队列削峰并给每个请求带截止时间,下游出故障时靠熔断快速失败,降级给出能接受的次优结果,服务做成无状态以便水平扩展,缓存用来减少重复负载。
细说:
- 入口限流:按用户、租户、API Key 用令牌桶限流。LLM 场景要同时按请求数和 token 数限,供应商的配额也是按 RPM / TPM 给的。超限返回 429,并带上重试时间。
- 排队:用优先级队列,交互请求排在批量任务前面。每个请求带截止时间,过期直接丢弃。队列满了就拒绝新请求(背压),不要无限堆积。
- 重试:指数退避加随机抖动,只重试幂等操作。建工单这类写操作靠幂等键防重复,见 可靠执行:状态、幂等与恢复。
- 熔断:对每个下游(模型供应商、发票接口)维护"关闭、打开、半开"三种状态。失败率超过阈值就打开,后续请求直接快速失败;过一段时间进入半开,放少量探测请求,成功了再关闭。
- 降级:顺序是备用模型或备用供应商、小模型、缓存结果、只返回检索到的条款原文、转人工。写操作不能降级成"猜着建"。
- 隔离:舱壁模式,不同租户、不同任务类型用独立的资源池和连接池,一个慢工具拖不垮全部。
- 多实例:Agent 服务本身无状态,会话和任务状态放外部存储,长任务可以断点恢复。GPU 推理扩容慢,要保留热备实例,按队列长度提前扩。
连接池、并发配额要配多大,先估算同时在途的任务数。排队论里的 Little 定律给了一个很简单的关系:
- :系统里同时在途的任务数(平均值)。
- :到达率,读作"lambda",单位是任务每秒。
- :一个任务从进来到离开的平均停留时间,单位秒。
教学算例(示意):峰值每秒来 20 个任务,每个任务平均停留 15 秒,那么同时在途 L = 20 × 15 = 300 个。连接池、并发配额和内存要按这个数再乘一个安全系数,比如 1.5 倍,就是 450。
这个式子能说明慢下游为什么危险。到达率不变,如果发票接口变慢,让 W 从 15 秒涨到 30 秒,在途任务就变成 600 个,是原来的两倍;在途多了,排队更长,W 再变大,形成恶性循环。熔断和截止时间就是用来切断这个循环的。使用时注意两点:λ 和 W 的时间单位要一致(每分钟的到达率配秒级的停留时间会差 60 倍);它描述的是稳态平均值,突发尖峰要另外留余量。
容易踩的坑:重试风暴,所有客户端同时重试,会把一次小故障放大成雪崩。熔断阈值太敏感会误伤正常请求。降级给出的答案要明确告诉用户。流式输出的长连接会一直占着连接数,要单独规划。
可能的追问:
- 模型供应商限流了怎么办?通过模型网关在多个 key、多个区域、多个供应商之间负载均衡;客户端按 token 预算先限流,免得撞上供应商的墙;非实时任务转异步;关键路径准备一个提前评测过的备用模型。
放到报销案例里:月底报销截止日流量尖峰时,交互请求优先,批量对账延后。发票接口熔断时,助手回复"发票系统暂不可用,已保存草稿,恢复后提交",不去建一张缺字段的工单。
token 怎么优化?上下文压缩、工具裁剪、缓存命中分别怎么做?(29)
简答:Agent 的 token 大头通常在每轮重复携带的系统提示、工具定义、历史和工具结果上,用户那句话占不了多少。优化就是三件事:少带、带短、带得能命中缓存。
细说:
第一件是上下文压缩,详见 Prompt / Context。常用的做法有滑动窗口加历史摘要;已完成的子任务只留结论;检索证据只带 top-k 并去重;长期记忆按需检索,不全量注入。
第二件是工具裁剪。每个工具的名称、描述和参数 Schema 都要占 token。假设接了 20 个工具、每个定义约 150 token(示意),光工具定义就是 3000 token,每次调用都要带。工具一多,就按意图或阶段只暴露相关的子集:先选类别,再加载这一类工具的定义。报销场景只暴露 4 个工具,就降到 600 token。工具返回也要裁:只留需要的字段,长结果存到外部,只给句柄或摘要。
第三件是缓存命中。前缀缓存要求前缀逐字节相同,所以 Prompt 的排布要讲究:
| 位置 | 放什么 | 稳定性 |
|---|---|---|
| 最前 | 系统提示、工具定义(顺序固定)、固定 few-shot | 跨请求不变 |
| 中间 | 会话历史(只追加、不改写) | 跨轮递增 |
| 最后 | 本轮检索结果、当前时间、用户问题 | 每轮变化 |
常见的反例是把当前时间戳或用户 ID 放在系统提示开头:
改之前:[当前时间 2026-11-02 09:31:07][用户 u_8812] 你是报销助手……(5000 token 固定内容)……[用户问题]
改之后:你是报销助手……(5000 token 固定内容)……[当前时间 2026-11-02 09:31:07][用户 u_8812][用户问题]
改之前,第一个 token 就每秒都在变,后面 5000 token 的固定内容一个也命中不了;改之后,这 5000 token 跨请求完全相同,每次都能命中(示意)。
最后是输出侧:限制最大输出长度,要求简短的结构化输出;推理模型设思考预算。
容易踩的坑:压缩可能丢掉关键约束,比如用户早先说过"先别建单",这类约束应该抽成结构化状态单独保留。动态工具集会改变前缀、破坏缓存,要在"少带工具"和"前缀稳定"之间权衡,一个折中是按阶段固定几套工具组合,每套各自能命中缓存。摘要本身也要花 token 和延迟。
可能的追问:
- 怎么知道优化有效?监控每个任务的输入、输出和命中缓存的 token(API 的用量返回里通常有缓存相关字段),同时在评测集上确认成功率没掉。
放到报销案例里:制度条款只带命中的两三条带编号的原文,比如第 4.2 条,不带整本制度。query_invoices 的结果只保留金额、日期、类型、状态四个字段,INV-102 这张就是"118.5 元、市内交通、未报销"这样一行,不把两千 token 的 OCR 全文塞进上下文。
线上要监控哪些指标?QPS、延迟、错误率、token 消耗怎么看?
简答:监控分四层。服务层看流量、延迟分位和错误率;模型层看 token、TTFT / TPOT、缓存命中和 GPU / KV 使用;工具层看每个工具的耗时与失败;业务层看任务成功率和每成功任务成本。指标负责告警,trace 负责定位。
细说:
| 层 | 核心指标 | 用来回答 |
|---|---|---|
| 服务 | QPS、并发、P50 / P95 / P99、按类型分的错误率(超时、429、5xx、解析失败)、队列长度与等待时间 | 稳不稳 |
| 模型 | 输入 / 输出 / 缓存 token、TTFT、TPOT、每任务步数、重试率、GPU 利用率、KV 块使用率、抢占次数 | 贵不贵、快不快 |
| 工具 | 每个工具的调用量、耗时、失败率、参数校验失败率 | 慢在谁 |
| 业务 | 任务成功率、人工接管率、用户反馈、每成功任务成本、安全拦截数 | 值不值 |
Trace 的做法是一个任务一个 trace,每次模型调用、检索、工具调用各一个 span,记录模型版本、Prompt 版本、token 数、耗时和结果状态。OpenTelemetry 有面向生成式 AI 的语义约定,仍在演进中。具体做法见 可观测性:追踪一条任务的代价。
告警用分位数和 SLO 消耗速度,不用平均值。举个例子:100 个请求里 98 个 1 秒返回,2 个卡了 20 秒,平均是 (98 × 1 + 2 × 20) / 100 = 1.38 秒,看着挺好;P99 却是 20 秒,那 2 个用户已经在骂了。成本类指标看趋势,比如日环比突增,不只看绝对值。
容易踩的坑:记录 Prompt 全文有隐私风险,要脱敏、采样、限权限。指标太多没人看,每层挑三到五个核心指标上看板就够。平均延迟会掩盖长尾,上面那个例子就是。
可能的追问:
- P99 突然升高怎么查?先按维度切分(模型、租户、工具、输入长度区间),看是不是集中在某一类请求;再抽几条慢 trace,看时间花在排队、prefill、decode、工具还是重试上。
放到报销案例里:报销看板至少有"建单成功率""缺发票追问率""每单成本""发票接口 P95"四个数。比如某天建单成功率从 95% 掉到 88%,切分后发现全集中在上海的打车票,再抽 trace 看到 query_invoices 返回的金额字段变成了字符串,问题就定位到发票接口的一次升级上(示意)。
旁枝串联
Agent 的成本为什么会被放大?一次任务的真实成本怎么算?
简答:单次调用的单价只是一个因子。Agent 一次任务要调用很多轮,每轮都要重新带上越来越长的上下文,再叠加检索、工具、重试和失败任务的分摊,所以累计输入 token 随步数超线性增长。
细说:设初始上下文 个 token,每步追加 k 个 token(工具结果、模型输出),一共 N 步。第 1 步输入 ,第 2 步 ,第 t 步 。把 N 步加起来:
- :整个任务累计计费的输入 token 数。
- :读作"对 t 从 1 到 N 求和",就是把每一步的输入加起来。
- :第一步的输入,系统提示、工具定义和用户问题。
- :每步新增的 token;:步数。
- 右边的 来自 0 + 1 + … + (N − 1),这一项是平方增长的来源。
先用 N = 3 核对:逐步加是 4000 + 4800 + 5600 = 14,400;用公式是 3 × 4000 + 3 × 800 = 14,400,对得上。教学算例(示意)取 、、:8 × 4000 = 32,000,加上 28 × 800 = 22,400,累计 54,400 token,是单次调用 4000 token 的 13.6 倍。按每百万输入 token 2 元(示意)算,光输入就是 0.109 元一个任务。
如果历史只追加、不改写,每一步除了新增的 k 个 token,前面都和上一步的输入完全相同,可以命中前缀缓存。没命中的部分只有 = 4000 + 7 × 800 = 9,600 token,其余 44,800 token 命中。假设命中部分按一成价格计费(示意,实际折扣以各家文档为准),等效计费 token 是 9,600 + 4,480 = 14,080,约为不缓存时的 26%。
这个式子假设每步追加量相同、上下文没有被截断或摘要;真实任务里 k 随工具不同而变化,有了压缩后 也不会一直平方增长。真实成本还要加上:
任务成本 = Σ 模型调用(输入 + 输出 + 思考 token)
+ 检索 / rerank + 外部工具费用
+ 重试 + 失败任务的分摊
每成功任务成本 = 总成本 / 成功任务数
对应三个杠杆:降 ,精简系统提示和工具定义并让它命中前缀缓存;降 k,裁剪工具结果;降 N,把确定性步骤改成 workflow,独立步骤并行。
容易踩的坑:历史只追加不改写时,二次项的大部分可以按缓存价计费,上面算过;一旦中途改写历史(例如重新摘要),从改写的位置往后缓存就断了,那一步要按全价重新付一次。
可能的追问:
- 多 Agent 会更贵吗?通常更贵:每个子 Agent 有自己的系统提示和上下文,协调消息也要 token。只有并行或上下文隔离带来的成功率、时延收益抵得过时才值得,见 Agent 核心。
放到报销案例里:报销任务一般是 search_policy、query_invoices、query_tickets、create_reimbursement_ticket 四次工具调用,加上最后的回复,大约 5 到 6 步。如果 query_invoices 返回 OCR 全文,k 从几百涨到两三千,二次项会跟着放大几倍;只回传 INV-102 的四个字段,k 就压下来了。
MoE 模型参数那么大,为什么推理不一定贵?
简答:MoE 每个 token 只经过路由选中的少数专家,计算量由激活参数决定;可所有专家都得放在显存里。可以这样记:算得像小模型,存得像大模型。
细说:以 DeepSeek-V3 技术报告 为例,总参数 671B,每个 token 激活约 37B。拿数字算一下两边:
- 显存按总参数备。FP8 下每个参数 1 字节,权重就是 671 GB,比 8 张 80 GB 卡的 640 GB 还多,还没算 KV;BF16 要翻倍到 1342 GB。私有化的门槛就在这里。
- 计算量按激活参数算。前向一次约每参数 2 次浮点运算,每个 token 约 2 × 37×10⁹ = 74 GFLOPs;同样 671B 的稠密模型是 1342 GFLOPs,差约 18 倍。单 token 的 FLOPs 接近 37B 的稠密模型。
- 带宽:batch 小时每步只读被选中的专家;batch 大时几乎所有专家都会被某些 token 用到,带宽优势变小。
- 部署:需要专家并行和负载均衡,跨卡通信成为新的开销来源。
容易踩的坑:低并发的小规模私有化部署,MoE 往往不划算,显存门槛高,利用率又低;大规模在线服务才能把它的计算优势兑现。
可能的追问:
- 卡不多时选 MoE 还是同等激活量的稠密模型?同等激活量下 MoE 效果通常更好,但显存需求是总参数级别。卡少、并发低选稠密;规模化服务、卡多选 MoE。
放到报销案例里:一家公司的报销助手,高峰也就每秒几个任务,为它私有化部署一个要十几张卡才放得下的 MoE,大部分时间显卡都在闲着。这种量级用 API,或者私有化一个 7B 到 14B 的稠密模型加 LoRA,更合算。
你说优化后又快又省,怎么证明效果没掉?
简答:把每次成本或延迟优化都当作一次模型变更来发布:同一评测集离线对比、影子流量、小流量灰度。效果设"不可退化"门槛,成本与延迟必须有实际改善才全量。
细说:
- 固定评测集:用第 6 站交来的黄金集,并按切片(难例、长文、工具密集、写操作、注入用例)分别统计,见 Agent 评测:从测试集到发布门槛。
- 离线对比:基线与新方案跑同一批用例。模型输出有随机性,要重复运行看方差,用同一批用例做配对比较。
- 影子流量:新方案并行处理真实请求但不返回给用户,对比输出差异。
- 灰度:小比例用户上线,盯业务指标,保留一键回退。
- 判定:效果差异在噪声范围内、高风险切片零退化、每成功任务成本或 P95 有显著改善,才全量。
样本量要心里有数。两个方案的成功率差了 2 个百分点,到底是真掉了还是运气?看差值的标准误:
- 、:基线和新方案的成功率, 表示从样本里算出来的值。
- 、:两边各跑了多少条用例。
- :一个比例的方差,p 越接近 0.5、n 越小,波动越大。
- SE:标准误,差值的波动幅度;1.96 是正态分布 95% 区间对应的倍数。
教学算例:成功率都在 80% 左右,各 1000 条独立样本。0.8 × 0.2 / 1000 = 0.00016,两边相加 0.00032,开根号得 0.0179,乘 1.96 是 0.035,所以区间是差值 ±3.5 个百分点。假如新方案是 78%,差值 −2 个百分点,区间是 [−5.5, +1.5],包含 0:你既不能说它掉了,也排除不了它掉了 5 个点。样本降到每边 300 条,SE 变成 0.0327,区间放宽到 ±6.4 个百分点,只能发现明显退化。
这个式子假设两边是独立样本。如果两个方案跑的是同一批用例,更好的做法是配对比较:只看"基线对、新方案错"和"基线错、新方案对"这两类用例各有多少,噪声会小得多。成功率接近 0 或 1、或者样本很少时(比如高风险切片只有 20 条),正态近似不可靠,直接看失败用例本身。
容易踩的坑:只看总分会掩盖某类任务大跌。LLM-as-judge 换了被评模型可能产生偏差,需要人工校准,见 评测 / 安全。"感觉快了"不算数,必须是同口径的 P95 和每成功任务成本。
可能的追问:
- 高风险用例怎么处理?写操作、权限、注入类用例单独设门槛,任何退化都阻断发布,不能被总体平均分稀释。
放到报销案例里:把意图识别换成更小的模型之前,先在报销评测集上确认"误把咨询当办事、误触发建单"的比例没有上升。员工只是问"打车超标能报吗",助手却调了 create_reimbursement_ticket,系统里就多出一张没人要的工单,这类错误一次都不该放过。
连接
LLM 基础 解释 KV Cache 和结构,RAG 是“知识注入”的另一条路,评测 决定优化是否真的有效。在主线里,本站接过第 6 站的评测门槛,把模型路由、预算、限流与降级交给 Coding / Harness;一条任务的代价怎么追踪,看练习 可观测性:追踪一条任务的代价。