跳到正文
Elaine Blog
返回

怎样把模型用进应用:能力、显存、速度、成本与可靠性

更新于:
LLM 基础

假设你要给店铺做一个客服助手。它需要把消息分到合适的队列,提取订单号,阅读售后条款,有时还要辨认照片里的商品损坏。某个模型很擅长写文章,这能说明它适合这项工作吗?还不能。应用需要的是在这些具体输入上,稳定地产生能被使用的结果。

模型选择的起点是一组任务,而不是参数规模排行榜。先把“好”变成可观察的行为,再讨论速度和价格,否则比较到最后,往往只是挑出了最会展示自己的模型。

先确定一次成功到底是什么

对工单分类来说,“回答很礼貌”不是成功标准。我们希望“账号出现陌生登录”进入 security,“重复扣款”进入 billing,普通咨询进入 general;信息不足时保留 unknown,而不是硬猜。输出还要包含合法字段,不能用另一套类别名称。

如果任务变成售后问答,成功标准就不同了:检索到适用条款,保留三十天期限和核实质量问题的条件,引用原文,缺少证据时明确说明。照片任务又增加了图像输入、清晰度、OCR 和视觉判断的要求。同一个模型在三种任务上的优势未必相同。

实际比较时,可以准备一组经过人工核验的样本,记录输入、期待结果和理由。样本既要有日常高频问题,也要有容易弄错的边界,例如“忘记密码”与“密码被别人改了”。边界样本揭示失败机制,日常样本说明问题发生的频率,两者不能相互替代。

比较候选模型时保持任务定义、资料和输出契约一致。允许使用各自要求的聊天模板与接口,但应记录差异。如果给一个模型完整政策,给另一个只有问题,比较的是系统输入而不只是模型能力。

参数数量能影响容量和运行开销,却不能替代任务测试。训练数据、后续训练、量化方式、上下文利用和工具调用能力都会改变最终表现。我们要选的是“在当前系统里完成任务”的方案,而不是一个脱离输入与运行环境的名字。

参数、显存和量化之间是什么关系

如果要自己部署,先要知道机器装得下什么。权重大小可以粗略按“参数数量 × 每个参数的字节数”估算。假设一个教学模型有 70 亿参数,使用每参数 2 字节的表示,仅权重大约 140 亿字节,约 13.0 GiB;这里 GiB 按 1024 的三次方换算,不等于厂商十进制 GB。

这只是权重。运行时还有 KV Cache、临时激活、运算工作区和内存分配开销。第四篇的例子中,某组 32 层、32 个 KV 头的配置在 4096 长度时,单条序列 KV 就约 2 GiB。把请求并发加大时,每条独立序列还要保存各自计算状态,所以“权重能装下”不等于“能承受目标并发”。

FP32、FP16、BF16 都是数值格式。FP16 与 BF16 通常都用 2 字节,但指数范围与有效精度分配不同,不能只因内存相同就认为行为完全相同。模型支持的硬件与数值路径也会影响运行效果。

量化进一步用较少的位表示权重或部分计算状态。先用对称量化理解,设置比例 s,把浮点 x 近似为整数 q = round(x / s),使用时恢复为 x̂ = sq。例如 s=0.1,0.37 量化为 4,恢复成 0.4,误差是 0.03。真实实现还要做范围裁剪,可能按组设置比例,也可能有零点等额外数据。

假设理论上把 70 亿权重都存成 4 bit,纯权重位数约对应 3.5 GB。但分组比例、未量化模块和运行缓存都会增加开销;计算内核还可能临时反量化。不能直接把文件大小当峰值显存,也不能保证位数减半就快一倍。

量化误差会进入后续矩阵计算。一般文本上看起来差不多,不代表精确编号、边界分类和工具参数也保持相同表现。因此量化方案应与任务一起比较。量化、LoRA、GQA 又是不同的改造:一个改变精度,一个限制可训练增量,一个改变 KV 头的组织。

MoE 的总参数与活跃参数也必须分别看。即使一个 Token 只使用部分专家,服务仍需要把专家存放在可访问设备或内存里;跨设备调度可能增加通信。一个“活跃参数较少”的模型未必在你的机器上比稠密模型更快。

托管 API 与自部署,实际选择的是什么

托管服务替应用承担模型加载、设备调度等工作,应用主要面对请求协议、配额、数据处理约定和账单。自部署可以更直接控制模型版本与运行环境,但需要自己承担硬件、更新、容量规划和故障处理。

可以用店铺客服的流量理解:每天只有零散几十次调用,长期空置的设备可能比按调用付费更贵;持续高负载任务则需要比较设备利用率、运维成本和实际吞吐。这里没有通用胜者,价格与负载数据要来自当前方案,不能只比较一个宣传单价。

还要区分模型权重、部署与 API 能力。同一权重可以部署在不同推理引擎上,提供不同结构化输出、上下文或流式接口;一个 API 接受同名字段,不代表其模型支持完全一致的工具行为。更换后端后,至少要重新核对应用依赖的输入输出契约。

模型别名也不等于固定快照。如果服务让一个名字指向新版本,昨日记录的结果可能无法用今天的调用完全重现。保存服务返回的版本标识、调用日期、生成配置和资料版本,可以帮助定位变化,而不是把所有回归都归因于 Prompt。

为什么一次调用便宜,整个任务仍可能贵

最直接的文本费用来自输入和输出 Token。若输入数为 I、输出数为 O,每百万 Token 单价分别为 pᵢ、pₒ,那么本次费用是 (I × pᵢ + O × pₒ) / 1,000,000。缓存命中部分、额外推理计量、图像或音频的计费方式要按供应商口径单独纳入,不能强行套进这一条简化公式。

更接近业务的问题是:解决一个工单用了多少次调用?一个便宜模型如果经常返回错误字段,触发修复,再触发较强模型兜底,单次便宜不一定带来整体便宜。输入历史越积越长,也会让后续轮次付出更多成本。

下面用虚构价格做一个可运行的账本。A 一次较便宜但有重试;B 一次较贵但一次成功。数字只用来解释计算,不是实际模型报价,脚本也没有向模型发送请求。

# 每条记录表示一次完整任务;attempts 列出该任务的所有调用。
# 每次调用用“输入 Token、输出 Token”表示,失败尝试同样会消耗资源。
runs = [
    {"model": "A", "ok": True, "attempts": [(1000, 100), (1000, 100)]},
    {"model": "A", "ok": False, "attempts": [(1000, 100), (1000, 100)]},
    {"model": "B", "ok": True, "attempts": [(1000, 100)]},
    {"model": "B", "ok": True, "attempts": [(1000, 100)]},
]
# 教学单价:每百万 Token 的输入价和输出价,单位保持一致。
prices = {"A": (1.0, 3.0), "B": (2.0, 6.0)}
for model, (input_price, output_price) in prices.items():
    selected = [run for run in runs if run["model"] == model]
    # 统计所有尝试的支出,而不是只统计最终成功的那一次调用。
    total = sum(
        (inputs * input_price + outputs * output_price) / 1_000_000
        for run in selected
        for inputs, outputs in run["attempts"]
    )
    successes = sum(run["ok"] for run in selected)
    # 分母为零时不能报告“成功成本为零”,应明确显示不可计算。
    per_success = total / successes if successes else None
    print(model, "总费用", round(total, 6), "每成功任务费用", per_success)

运行得到两组总费用都为 0.0052,但 A 只解决一个任务,B 解决两个。这里的“每成功任务费用”包含失败任务支出,是一个比较口径,不意味着失败任务不重要。真正上线时还应同时报告失败率,不能只用一项平均数掩盖服务质量。

人工处理也有成本。如果十个失败里有一个会把订单误退,后果与十个失败都只是格式错误不同。选择模型时,把失败按影响分组,比单纯追求总体准确率更接近实际目标。

用户在等待的,究竟是哪段时间

一次请求有网络、排队、输入处理、逐步生成和结果处理。首 Token 延迟衡量开始看到输出要等多久,端到端延迟衡量完整结果何时可用。输出很长时,每个后续 Token 的生成速度会明显影响总时间;输入很长时,输入处理和缓存命中情况也会改变等待。

对聊天解释,较早出现第一段文字有价值。对分类接口,下游要等完整 JSON 才能工作,首字快并不等于业务完成快。对退款动作,可能还要等数据库校验或人工审批;模型部分再快,也不能代替整个链路的测量。

可以把一次教学请求标在时间轴上:0 ms 发出,200 ms 收到第一个 Token,最终在 1100 ms 收完第十个 Token。首 Token 延迟 TTFT 为 200 ms;若用“第一到最后 Token 的时间除以后续间隔数”定义 TPOT,则是 (1100−200)/(10−1) = 100 ms。不同工具口径可能不同,报告时应说明定义。

流式文本事件不一定对应一个 Token。若客户端只收到三个文本片段,不能把三个事件直接当作三个 Token 计算 TPOT;可以记录首个非空文本到达时间,并明确这是客户端观测值。服务端 Token 计数和时间戳更适合区分模型计算阶段。

吞吐量又是另一个分母。一个服务同时为十个用户生成,每个用户 10 Token/s,总输出可以是 100 Token/s,但单个用户仍只有 10 Token/s。增加 batch 可能提升总体资源利用率,同时因等待组批或共享资源影响单用户延迟。

测试时记录每个任务的总耗时,并报告中位数和高分位数,例如 p95。平均值可能掩盖少数很慢的请求,而用户往往恰好记住那些请求。还要区分冷启动与热启动、缓存命中与未命中、单请求与并发压力。不同条件混成一个数字,通常无法指导优化。

延迟和质量还会相互影响。为了快而限制输出长度,可能把 JSON 截断;为了少调用而合并太多任务,可能增加理解难度。每次优化都应回到同一组样本,确认省下的时间没有以关键错误为代价。

一个具体的实验可以分成两轮。第一轮只测试固定样本的正确性,把不支持必要输入类型、频繁违反契约或漏掉关键安全事件的方案排除。第二轮对剩余方案做延迟与费用测量。这样不会因为某个候选非常便宜,就把明显不合格的结果合理化。

举例说,候选 A 在普通咨询上表现很好,却漏掉三条陌生登录;候选 B 的普通咨询偶尔进入 unknown,但安全样本都正确。不能只根据两个总准确率相近就选 A。需要判断 unknown 是否会触发补充询问、安全漏判会进入什么流程,再决定实际损失。这个判断来自应用,而不是模型供应商能够替你决定的统一权重。

测试集较小时,一两个样本就能显著改变百分比。与其给出小数点后两位的排名,不如报告样本数、失败类型和不确定性。如果当前两种方案无法拉开差距,可以优先选接入简单的一种,同时继续收集真实失败;没有必要为了微小的离线差异立刻增加双模型路由。

缓存测试同样要接近真实流量。如果为了压测连续发送完全相同的提示,得到的是高度命中的表现;真实用户的问题和历史都不一样,效果可能不同。测量时记录命中状态,才能判断收益来自模型本身、公共前缀,还是测试数据的重复。

最后别忽略输出长度的可比性。一个模型用两百字完成任务,另一个写一千字,后者总耗时长不一定是每 Token 更慢。应分别看任务是否完成、生成了多少内容和耗时,避免把冗长误认为深入,或者把删掉必要解释误认为效率提升。

路由先排除不适用的模型,再比较代价

设有三个教学候选:A 只能处理文本,B 支持图文,C 支持文本但上下文更短。用户上传破损照片时,A 与 C 应先被排除,而不是因为便宜就得到更高分。若用户只是问一段已提供政策,三个候选中的文本能力才有可比性。

“简单问题交给小模型”并不是程序可以直接执行的规则。可以先用可观察条件:是否包含图片、输入长度、是否要求结构化输出、是否需要某种工具能力。若再用一个模型判断难度,那个判断也要计入时间、费用和错误。

下面只演示约束筛选,不进行模型调用。候选能力、容量和成本都是虚构值,成本排序也只用于表示已经在同类任务中估算出的代价。正式服务不能照抄这些数值。下载:routing_demo.py下载

# 教学候选:通过质量门槛后,才进入这里的能力与成本筛选。
models = [
    {"name": "A", "vision": False, "structured": True, "window": 8000, "cost": 1.0},
    {"name": "B", "vision": True, "structured": True, "window": 16000, "cost": 2.0},
    {"name": "C", "vision": False, "structured": False, "window": 4000, "cost": 0.6},
]

def choose_model(has_image, needs_structure, token_requirements):
    eligible = []
    for model in models:
        if has_image and not model["vision"]:
            continue
        if needs_structure and not model["structured"]:
            continue
        # 不同 Tokenizer 计数不同,因此预算按候选分别提供,并包含输出余量。
        required = token_requirements[model["name"]]
        if required > model["window"]:
            continue
        eligible.append(model)
    # 没有合格候选时明确失败,不偷偷丢掉图片或截断政策来强行调用。
    if not eligible:
        raise ValueError("没有满足本次任务约束的模型")
    return min(eligible, key=lambda model: model["cost"])["name"]

print(choose_model(True, True, {"A": 6000, "B": 6500, "C": 6200}))

这次只能选择 B,原因是图像输入属于硬约束。它没有证明 B 的视觉判断正确,也没有比较真实价格;它只是把“不能用”与“合格后选哪个”分成两个明确步骤。

路由与负载均衡也不同。前者在能力或配置不同的候选间选择,后者常在同类服务副本之间分配流量。把图像任务转到一个不支持图像的文本后端,不能用“另一个节点空闲”来解释为合理均衡。

超时之后,什么时候重试,什么时候停止

调用失败可能发生在不同阶段。参数不合法、权限不足,应先修正请求;短暂连接故障或限流,可能适合等待后有限重试;答案格式错误,需要输出校验与明确的修复流程。把所有失败统一重试三次,只会让一部分错误重复发生。

更危险的是不知道原请求是否成功。模型调用如果只是生成文字,重复调用主要增加费用;若随后执行退款或发送消息,超时可能发生在动作已完成、确认尚未返回之间。此时再次执行需要业务幂等与状态查询,不能由模型重新猜一次。

例如 SDK、网关、应用三层都允许最多 3 次尝试,包含各自的首次调用,最多可能把一次业务请求放大成 27 次尝试。应该明确哪一层负责重试,并让总截止时间与尝试次数约束整条链。例如用户最多等 8 秒,第一次已耗 6 秒,就不能再启动一个有 10 秒超时的后备请求并假装仍满足承诺。

退避是在重试之间增加等待,抖动让大量客户端不要同一时刻重发。熔断则在服务持续异常时暂时阻止新请求进入,之后用少量请求观察是否恢复。背压限制进入队列的速度或数量,隔舱为不同任务分开资源,避免大批离线工作耗尽实时客服容量。这些机制分别控制重复、故障扩散和资源争用,不能相互替代。

Fallback 也应保持任务语义。主视觉服务不可用时,可以转另一个合格视觉模型或让人工处理;转纯文本模型后继续声称已经检查照片,会把技术可用性伪装成任务成功。主模型拒绝越权查询时,更不能用后备模型绕过权限。

回答缓存同样要求语义保持不变。若缓存键只有“运费谁承担”,可能把另一位用户、另一政策版本的答案复用过来。通常需要考虑租户、权限范围、政策版本、任务配置以及关键业务状态;包含敏感字段的原始长字符串也不应直接作为公开日志内容。

例如 A100 昨天尚未核实,今天已核实为质量问题,问题文字完全相同,合理回答却发生变化。可以用状态版本参与缓存有效性判断,或对这种动态任务不缓存最终答案。高命中率本身不能说明回答正确。

批处理改变交付时机,不代表每项都成功

假设每天凌晨要整理一万条历史工单,业务并不要求两秒内逐项返回。可以把它们作为异步任务提交,保存稳定 ID,稍后取回结果。在线聊天关心单次等待,离线批处理更关心总完成时间、吞吐和失败项处理。

Background 通常表示单次请求在后台执行并稍后查询;Batch 通常把多条独立请求作为一批提交;应用任务队列管理自己的工作调度;工作流还描述步骤之间的依赖。它们可以组合,不能因为用了队列就推断模型服务也提供批量优惠或自动恢复。

一批请求可用下面的教学状态说明:

项目 ID输入版本结果状态下一步
ticket-001v3succeeded保存分类结果
ticket-002v3failed根据错误决定是否重试这一项
ticket-003v3pending等待或查询,不把缺失结果当空答案

结果返回顺序可能与输入不同,所以按 ID 对账,不能按列表位置直接 zip。提交成功也只是服务接受了任务,不代表三项都完成。重跑时记录新的尝试与原项目 ID,避免把旧版本输入的结果覆盖新版本。

应用若先写数据库,再发队列消息,中间崩溃可能造成“有任务记录却没人执行”;反过来可能造成“执行了却找不到记录”。生产系统可以用事务 Outbox 等机制,把待投递事件和业务记录一起持久化,再由投递器发送。这个工程机制解决交付一致性,不改善模型分类能力。

小型对账示例见 batch_reconcile.py下载,它用本地教学结果展示乱序、失败和缺失,不冒充实际调用了某个 Batch API。完整调用方式要按所选服务的任务协议接入。

把比较结果变成一个可重复的决定

最后可以得到一张很小的决策表:每种任务需要哪些输入能力,最低通过标准是什么,哪些错误不能接受,完整任务的耗时和费用各是多少。先排除不满足必要条件的方案,再在合格方案中选择,而不是把所有指标随意加权成一个看似精确的总分。

第一次测试的结论也不应永久有效。模型版本、提示、资料和应用流程变化后,系统表现可能变化。保存输入样本、参数配置和原始结果,才能知道是模型进步了,还是测试条件变宽松了。对无法固定版本的服务,至少记录调用时间与服务返回的版本标识。

对于刚开始做应用的读者,一组认真核对过的真实任务,通常比一套复杂路由架构更有价值。先证明简单方案在哪些地方失败,再引入缓存、后备或异步。更多重试边界、事件记录、批任务接入及 Java 配套入口见 LLM 接入与工程指南下载。正文已经解释了这些机制为什么需要,指南用于把它们落到程序里。

到这里,从文字编码、Transformer 计算、训练与采样,到上下文、图像和声音,再到应用选择,六篇已经连成一条完整链路。下一步是把任务本身说清楚,从同一个工单分类器学习 Prompt 为什么能改变模型回答


分享这篇文章:

上一篇
模型怎样看图和听声音:多模态与实时对话
下一篇
Prompt 为什么能改变模型回答:从生成原理到上下文组织