先说一个最近常被问到的问题:为什么模型选型、Prompt 编排、Agent 流程都做完了,线上用户还是觉得“卡”?这个问题的答案,基本都落在AI系统性能工程上。模型能力再强,如果系统扛不住真实并发,延迟和吞吐压不住,最终产品体验都会被拖垮。
这篇是AI系统性能工程系列的第二篇,我会把重心放在大模型推理优化、Agent 工作流编排、以及线上压测和问题排查上。第一篇聊过的基础方法,这篇会直接带过,重点讲可落地的优化手段和现场经验。适合做模型部署、写AI Agent 应用、或者正在被AI服务性能问题折磨的工程同学阅读。就算你刚入门,也能照着里面的步骤给自己服务做一次性能体检。
1. AI 系统性能工程为什么不能照搬传统思路
1.1 传统性能优化工具在这里经常失灵
做传统Web服务性能优化时,思路很清晰:加缓存、加连接池、调线程池、做负载均衡、优化慢SQL。因为请求相对独立,CPU 和内存是主要瓶颈,监控体系也很成熟,拿到CPU、内存、磁盘、网络指标就能定位大多数问题。
但AI系统的性能模型完全不一样。一个推理请求进来,不只是消耗CPU,还会占用GPU显存和算力;多个请求可以拼在一个batch里跑,请求之间互相影响;模型权重、KV Cache、临时激活值都在抢显存。于是你会遇到传统监控完全解释不了的现象:显存看着还剩不少,新请求却被拒绝;GPU利用率很高,但吞吐上不去;CPU一点都不忙,用户延迟却高得离谱。这些问题的根因,往往藏在推理框架的调度策略和显存分配逻辑里,靠老一套工具确实看不到。
1.2 从模型层到应用层,性能问题分布在四个层面
我习惯把AI系统的性能工程拆成四层看:
- 模型层:参数量、量化精度、上下文窗口、采样参数。这里决定单次推理的理论代价。
- 推理运行时层:显存管理、批处理策略、调度器、并行方式。这里是AI性能优化的主战场。
- 服务层:API框架、网关、队列、限流、鉴权。这里负责把推理能力对外开放并保护后端。
- 应用层/Agent层:Prompt长度、工具调用次数、上下文管理、多智能体协作方式。这一层经常是性能瓶颈最容易被忽略的地方。
性能问题往往是跨层的。比如模型层超长上下文导致prefill变慢,表象却是服务层的请求超时;再比如 Agent 应用层不断重复调用大模型,成本飙升,但监控图上模型延迟却很正常。只有先把问题映射到正确的层,才能找到合适的解法。
1.3 性能工程本质是目标取舍
延迟、吞吐、成本、稳定性这四个目标,在AI系统里天然互相拉扯。小并发场景下你希望TTFT足够低,用户体验好;高负载场景下你希望吞吐最大化,摊薄单次成本;但高吞吐往往意味着把更多请求塞进同一个batch,又会推高单请求延迟。所以AI系统性能工程的本质不是“越快越好”,而是“在可接受的延迟范围内,尽量压出吞吐、控住成本、稳住尾延迟”。
这也意味着性能工程必须建立在量化指标之上。没有基线就没有优化,拍脑袋调参数只会把问题调得更乱。下一节就先解决指标问题。
2. 先把指标定明白,没有基线就没有优化
2.1 大模型服务绕不开的六组关键指标
做AI系统性能工程,第一件事是统一语言。我常用的指标是这六组:
| 指标 | 含义 | 建议观测方式 | 常见参考范围 |
|---|---|---|---|
| TTFT | 首Token延迟,从请求发出到返回第一个Token的时间 | 服务端打点,观测p50/p95 | 交互式场景最好1秒以内 |
| TPOT | 每个输出Token的平均生成时间 | 用总生成时间除以Token数 | 低于50ms/token才有流畅体感 |
| 生成吞吐 | 每秒生成的Token数(tokens/s) | GPU或服务进程统计 | 与模型规模和硬件相关 |
| 请求吞吐 | 每秒处理的完成请求数(req/s) | 压测工具统计 | 与业务SLA绑定 |
| 尾延迟 | p95/p99的端到端延迟 | 压测日志分位统计 | 过高会显著感知为“卡顿” |
| 错误率与重试率 | 超时、拒绝服务的比例 | 网关和客户端双向统计 | 线上建议低于0.1% |
不要只盯“平均延迟”,平均延迟会掩盖大量问题。一个服务平均200ms,p99却到了5秒,说明长时间存在长尾请求拖后腿。性能调优的目标往往是先压p99,而不是让平均值更好看。
2.2 端到端延迟和模型延迟不是一回事
最常见的误判,是把模型推理耗时当成端到端延迟。实际一个AI请求在系统里走过的路径可能很长:客户端到网关鉴权、路由转发、服务进程接包、预处理、构建Prompt、检索召回、排序、模型prefill、模型decode、流式返回、客户端首包解析。
比如一个RAG问答系统,用户体感延迟 = 网络RTT + 网关耗时 + 向量检索耗时 + Prompt构造耗时 + 模型推理耗时 + 流式传输耗时。如果模型推理只占40%,那你优化模型半天,体感提升也不明显。所以排查性能问题前,先做链路分段打点。
代码层可以很简单地打点,不用引什么重型框架:
import time def handle_query(query): t0 = time.time() retrieved = retriever.retrieve(query) t1 = time.time() prompt = build_prompt(query, retrieved) t2 = time.time() result = model.generate(prompt) t3 = time.time() print(f"retrieve={t1-t0:.3f}s prompt={t2-t1:.3f}s generate={t3-t2:.3f}s")这样跑一次就能看出时间到底烧在哪个环节。实测中我见过不少“优化模型”的项目,最后发现检索库没加索引,一次检索占了总耗时一半以上。
2.3 指标采样的三个坑
第一,压测并发线程数没设对。用JMeter或Locust压测时,如果线程池配置过高,客户端自己先成为瓶颈,测出来的延迟曲线完全失真。先确认压测工具所在机器负载正常,再谈服务端指标。
第二,只看均值不看分位。AI推理的延迟分布通常不均匀,受批处理、抢占、显存换页影响,长尾非常明显。必须把p50、p95、p99三档日志都打出来,对比观察。
第三,忽略冷启动和预热。模型加载、内核缓存填充、显存预分配都需要时间。刚启动的服务前几十个请求性能会明显偏差,压测时先热一杯水,跑5到10分钟预热请求再正式开始记录。
3. 大模型推理优化的显存、批处理与多卡选型
3.1 先算一笔显存账,再决定并发策略
大模型推理的显存占用不是固定的。模型权重只是一部分,随着请求生成,KV Cache会不断膨胀。这一点如果不理解,后面做并发策略全是盲打。
KV Cache的估算公式不复杂:每个Token需要保存Key和Value两组向量,大小为 2 × 层数 × 隐藏层维度,再乘以精度字节数,最后乘上序列长度。以7B级模型为例,假设32层、隐藏层维度4096、FP16精度,每个Token的KV Cache约占用2×32×4096×2字节,算下来差不多0.5MB。如果序列长度2048,单请求的KV Cache就要吃掉约1GB显存。
所以显存账要这样算:可用显存 = 模型权重 + 激活值 + KV Cache预留 + 推理框架开销。模型权重是固定的,KV Cache却随请求并发和序列长度线性增长。你只调大并发数,忘了给KV Cache留够空间,系统就会在某个瞬间触顶显存,接下来要么OOM,要么疯狂抢占和换页,延迟爆炸。
3.2 连续批处理才是吞吐的发动机
传统动态批处理是等一批请求凑齐再统一推理,谁的请求短就先结束先返回,但批次必须整体结束才能释放资源,中间空转浪费很大。现在的推理框架普遍采用连续批处理:GPU空闲时立即插入新的请求,每个请求生成到自己的结束符就立刻退出,不用等整批结束。
这个机制对吞吐的提升非常直观。我曾经在同一个7B模型、同样硬件条件下做过对比:关闭连续批处理,稳定吞吐约400 tokens/s;开启后,同样的并发规模能跑到900 tokens/s以上。代价是单请求的延迟波动会大一些,所以要通过调度策略控制同一时刻的活跃序列数(比如max_num_seqs参数),避免一个批次塞进太多长上下文请求把批处理周期拖得太长。
配合连续批处理,很多框架还在KV Cache分配上用到了类似虚拟内存的分页机制,给每个请求按页分配缓存,而不是预先分配最大可能长度的连续显存。好处是显存碎片大幅减少,长请求短请求可以混跑,缓存利用率高不少。这也是为什么我建议优先用成熟推理框架而不是自己写批处理逻辑,调度算法远比想象中复杂。
3.3 多卡并行方式怎么选
模型太大单卡放不下,或者算力不够想多卡一起跑,常见的并行方式有三种:
- 数据并行:每张卡放完整模型副本,各处理不同请求,吞吐能线性扩展,但显存需求不变。适合模型能塞进单卡、主要缺吞吐的场景。
- 张量并行:把一层网络切到多卡,每张卡算一部分,适合单卡放不下的大模型,但通信开销大,跨机时延迟会明显增加。
- 流水线并行:按层切分,不同卡负责不同层,适合超大模型,但容易出现流水线气泡,利用率不容易打满。
选型逻辑并不复杂:7B、13B这种能单卡或双卡放下的小模型,优先数据并行加连续批处理,把吞吐压出来;70B以上大模型,节点内用张量并行,节点之间再用数据并行;只有模型在单节点都放不下时才需要认真考虑流水线并行。
3.4 一套我实测过的推理服务参考配置
拿一个13B模型部署在两卡环境的场景举例,我会用类似的启动参数:
python -m vllm.entrypoints.openai.api_server \ --model /models/llama-13b \ --tensor-parallel-size 2 \ --gpu-memory-utilization 0.9 \ --max-model-len 8192 \ --max-num-seqs 64 \ --enable-prefix-caching几个参数的意思要说透:
- tensor-parallel-size 设为2,是因为模型在单张80G卡上虽然能放下,但单卡算力有限,两卡张量并行能显著降低单Token延迟。
- gpu-memory-utilization 设为0.9,给CUDA、驱动、推理框架留出10%的显存余量,避免显存颠簸。别贪心设成0.99,实测很容易OOM。
- max-model-len 是支持的最大上下文长度。这个值越大,KV Cache上限占用越高。如果业务不需要8192,设成4096能省出大量显存给并发。
- max-num-seqs 控制同一时刻活跃的序列数。设得太大,一个批次里挤满长请求,端到端延迟会飙升;设得太小,GPU算力吃不饱。64是我在这个硬件组合下的折中值,业务不同需要自己压测调整。
- 开启prefix-caching,多个请求共享相同Prompt前缀时,可以复用KV Cache,尤其适合Agent场景里反复使用系统提示词的调用模式。
4. AI Agent 工作流的性能工程:慢往往不是模型一个人的问题
4.1 先拆一次 Agent 调用的时间账单
AI Agent系统性能问题的特征和纯模型服务很不一样。用户的一次请求,往往对应多次大模型调用,中间还可能穿插工具调用、代码执行、文档检索。模型单次响应不慢,但被编排逻辑一叠,端到端就慢得离谱。
我拆过一个典型的多智能体协作任务,运行时间分布大概是:
| 环节 | 调用次数 | 单次耗时 | 合计 |
|---|---|---|---|
| 任务规划LLM调用 | 1 | 3秒 | 3秒 |
| 子任务工具API | 5 | 2-6秒不等 | 约20秒 |
| 代码生成LLM调用 | 2 | 8秒 | 16秒 |
| 审查LLM调用 | 1 | 6秒 | 6秒 |
| 聚合总结LLM调用 | 1 | 5秒 | 5秒 |
这些环节如果全部串行执行,整体耗时接近50秒。但里面工具API调用是相互独立的,代码生成和审查之间也有冗余等待。经过优化,总耗时压到了18秒左右,体感差距非常大。
4.2 缓存一切能复用的结果
Agent工作流的耗时大头往往不是模型能力,而是重复劳动。同一个工具执行结果被反复取用,同一段上下文被反复塞进Prompt,这些都在浪费时间和Token。
做三件简单的缓存,收益往往立竿见影:
- 工具结果缓存:比如检索API、数据库查询、HTTP请求,加个内存或Redis缓存,相同输入直接返回。不要求长期有效,几分钟内复用就够了。
- LLM结果缓存:相同或高度相似的Prompt,直接命中历史结果。可以用语义哈希或嵌入相似度做近似匹配,但要注意业务对结果时效性的要求。
- Prompt片段缓存:现在主流推理框架支持前缀缓存或上下文缓存,如果业务里所有请求都带一大段一样的系统提示词,开启这个功能,每次请求的prefill耗时能省一大截。
4.3 并行化与流式输出要双管齐下
很多Agent框架默认把工具调用写成了串行:先调A工具,拿到结果,再调B工具。但实际业务里,几个独立的工具往往可以一次并发发起。在代码层面用异步并发来控制,几个工具同时跑,时间成本从“加和”变成“取最大值”。
具体实现时注意控制并发度,不要无限发请求。可以用信号量把同阶段工具并发数限制在5个左右,避免把下游API打挂。工具返回后,优先把结果流式输出给用户,而不是等全部链路跑完再一次性返回。至少能提升“首包体验”的感知,用户以为系统在思考,其实结果已经在路上了。
4.4 上下文管理是Agent性能的隐形杀手
Agent一多轮,历史上下文会越来越长。Prompt长度直接影响prefill时间,还同步扩大KV Cache占用。很多Agent变慢,不是模型推理变慢了,而是上下文太长了。
优化思路是压缩和选择性保留:
- 历史消息做摘要:把对话历史压缩成一段结构化摘要,而不是把原始对话全部灌进模型。
- 控制工具返回值长度:检索结果动辄几万字符,实际用到可能只有一小段。先做裁剪再进Prompt。
- 阶段性“清空”:一轮子任务结束后,把中间细节写进摘要文件,让主模型的上下文保持精简。
这类优化对Agent性能的影响,比调整模型采样参数明显得多。我在一个长会话机器人上试过,上下文从1万Token压缩到3千Token后,每次请求的prefill时间从2秒降到0.6秒。
4.5 多AI协作场景里容易被忽略的系统级开销
多智能体协作、多个模型并行处理任务,听起来强大,但系统开销很容易失控。每个Agent背后都可能是一个推理服务实例,每个推理服务都有调度队列和连接池。如果Agent编排层没有全局限流和连接池共享,流量峰值一到,各个模型服务互相争抢资源,整体雪崩。
这里要特别提醒:不要让每个Agent都维护独立的后端连接。公共模型网关连接池要复用;给不同Agent设置优先级和配额;对耗时长的任务设置超时和优雅降级。AI系统性能工程不只是调GPU参数,还包括编排层的流量治理。
4.6 一个可落地的Agent调优案例
我对一个代码生成类Agent做了三轮改造:
第一轮,去掉重复的上下文拼接逻辑,所有工具返回先裁减再入Prompt,单次调用的Prompt长度减少60%。第二轮,把独立工具调用改成并发执行,规划完成后一次发出5个检索请求,而不是循环等待。第三轮,给“审查”环节增加条件判断:只有测试失败或者代码规范检查不过时才触发二次生成,避免每次都白跑一轮。
三轮改完,端到端p50从42秒降到19秒,p95从68秒降到31秒,Token消耗也明显下降。这说明Agent性能工程的核心不是某一个高大上的参数,而是把调用链路里的每一段浪费找出来处理掉。
5. 线上表现怎么压测和排查
5.1 压测方案先定好,别上来就打
性能优化最忌讳没有基准的乱调。一个可信的压测方案至少要包含四件事。
第一,请求集要有代表性。不要只压单一短Prompt,要覆盖长短文本、工具调用、多轮对话、长回复生成。我习惯从线上日志里抽取真实请求做脱敏后组成测试样本,这样的结果才贴近真实。
第二,压力要阶梯式增加。从低并发开始,比如1、2、4、8、16逐步加,每档维持3到5分钟,观察系统在哪个阶段指标开始劣化。一上来就压高并发,只会得到一张什么都看不出来的曲线。
第三,压测时长要够。很多推理服务前5分钟表现正常,10分钟后显存碎片累积、队列积压,性能开始恶化。每次正式压测我至少跑15分钟,记录全过程的p50/p95/p99和错误率。
第四,软硬件状态要记录。模型版本、推理框架版本、量化方式、并发参数、GPU型号和显存,全部存进压测报告。没有这些上下文,以后你根本没法对比优化前后到底差了多少。
5.2 典型性能问题的快速排查手册
我把这几年最常见的线上问题整理成了一张速查表,遇到问题直接对着查:
| 现象 | 可能原因 | 优先排查项 | 常见解法 |
|---|---|---|---|
| TTFT很高 | prefill阶段慢、输入过长 | Prompt长度、检索耗时 | 压缩上下文、开启前缀缓存 |
| 生成阶段卡顿 | TPOT偏高,decode带宽不足 | GPU利用率、显存频率 | 减并发、降上下文长度、调量化 |
| 吞吐上不去 | batch被长请求拖死 | 平均生成长度、max-num-seqs | 调整批大小、限制最大长度 |
| 显存OOM | KV Cache预留不足、并发过高 | nvidia-smi观测显存曲线 | 调低gpu-memory-utilization和max-num-seqs |
| 部分请求超时 | 长尾调度问题、抢占频繁 | p99延迟、队列深度 | 加超时重试、做优先级调度 |
| GPU很忙但CPU也满 | 前处理、序列化、网关占资源 | CPU火焰图 | 优化预处理逻辑、数据走二进制协议 |
其中有一个我反复踩过的坑:GPU利用率明明很高,但吞吐就是上不去。最后定位到是API网关里的一次JSON序列化把CPU打满了,请求在进模型服务之前就已经排队。这类问题靠GPU监控看不出来,必须把全链路分段耗时打出来。
5.3 给AI测试开发的一点心得
性能工程和测试开发可以非常紧密地配合。我的习惯是把线上流量录制下来,脱敏后保存成性能回归样本集,每次模型迭代、框架升级、Prompt调整后,都用同一套样本跑基线。没有回归样本的性能优化,很容易出现一个问题:这次延迟降了10%,但另一个场景的长尾反而变差了,你却完全没有发觉。
测试用例也不能只覆盖标准输入。一定要包含极端场景:超长文本、超高并发、工具调用失败后重试、连续多轮对话。AI系统性能是数据敏感的,不同输入分布下性能差异极大,测试覆盖不全会给你一种“性能很好”的错觉。
6. 几个实践后才知道的性能细节
分享三个很土但很有效的经验。
第一个,把耗时日志埋到所有关键环节。别一上来就上链路追踪系统,先用最简单的时间戳打印,采样率百分之几就够了。我排查过很多“又卡又慢”的AI服务,最终都是靠这些原始日志定位到瓶颈。工具不怕土,好用就行。
第二个,线上优化永远先解决“跨层浪费”。不少团队的模型推理耗时只有200毫秒,但一次请求端到端要2秒,差距基本都被Agent编排、重复调用、无效上下文吃掉了。先做链路分析,再做参数调优,顺序不能颠倒。
第三个,每次只改一个变量。今天调并发数,明天换量化,后天改上下文长度,然后看整体指标,这是性能工程的大忌。改完之后你根本说不清哪个改动起了作用。我用成本最低的方式保证可信度:每次改动后只对比改动前后的全量指标,其他条件原封不动。
AI系统性能工程这项工作,技术和业务耦合很深。模型在变、数据在变、Prompt在变,性能基线需要持续维护。能跑通一个稳定、可控、有预算约束的AI性能体系,本身就是竞争力。