
当你在代码里敲下client.chat.completions.create()那行调用到屏幕上一个字一个字吐出回答中间到底经过了什么不少人把大模型 API 当成“黑盒”用参数填好、请求发出去、等结果。可真到了要做评测、要压测、要排查线上超时、或者想把模型部署到自己的 GPU 上时光会调接口远远不够。你得知道请求在服务端是怎么被调度、怎么排队、怎么被 GPU 吃进去的甚至要明白 KV Cache 为什么能直接决定你的并发上限和上下文长度上限。这篇文章就围绕“一次大模型 API 请求的完整生命周期”来聊重点拆四个东西Harness、GPU、并发和KV Cache。我自己在本地部署、评测框架接入、以及排查线上调用问题时踩过不少坑下面这些内容都是我实操过、验证过的希望能帮你在面对“请求跑不起来”“日志看不懂”“并发一高就超时”这类问题时有一套清晰的排查思路。1. 请求的第一站Harness 到底在扮演什么角色1.1 不只是“发请求”那么简单很多第一次接触“deepseek harness”“codex harness”这些概念的人都会懵Harness 是什么是不是一个 API 网关还是一种测试工具我的理解是在 AI 应用和大模型之间Harness 是负责“包装、调度、约束”的一层框架。英文里 Harness 本身有“马具、挽具”的意思引申过来就是“把模型的力量套上缰绳让它按你的规则去跑”。实际场景中Harness 可以是推理服务框架接收上游 API 请求做鉴权、限流、负载均衡再把请求转发到具体的模型实例上评测/测试框架比如给模型跑 benchmark 时用一套标准化的 Harness 来加载数据集、构造 prompt、调用模型、收集结果并计算指标Agent 编排框架管理大模型和外部工具之间的调用流程比如让模型能调代码解释器、能搜索、能读写文件。对于“一次 API 请求”来说它经过 Harness 时干的活是实实在在的token 化、上下文组装、路由选择、显存和并发状态检查。这也是为什么你会发现同样的模型裸调用和走 Harness 调用首字延迟TTFT可能不一样——因为 Harness 自己还要花一点时间做预处理。1.2 Harness 和 Agent 的区别这也是很多人在选型时容易混淆的地方。我用一句话来区分Agent 是“让模型主动去决策”Harness 是“约束模型按流程执行”。举个例子。你用 Harness 跑一个代码生成任务的批量评测它会固定好 prompt 模板、固定好超参temperature、max_tokens把 1000 条测试用例逐条喂给模型然后把输出统一收集起来。这个过程中模型是“被测试的对象”没有自主选择工具的权利。但 Agent 不一样比如一个 авто代码修复 Agent 接到一个 issue 后它会自己决定先读哪个文件再改哪个文件要不要跑测试测试失败了怎么调整。此刻模型是“决策者”而外界只给它提供工具和反馈。所以如果有人说“harness 和 agent 是同一个东西”千万别信。更准确的说法是Harness 是框架Agent 是模式一个 Harness 完全可以承载不同类型的 Agent 应用。回到 API 请求链路上你要理解的是Harness 是第一道关卡它决定了请求以什么格式进来、往哪个模型路由、以及要不要放行。1.3 请求在 Harness 中的构建流程我以本地部署一个大模型并用 Harness 评测为例把请求构建流程拆开来看接收原始请求HTTP 请求进来可能是 OpenAI 兼容格式也可能是自定义格式鉴权与校验检查 API token 是否有效。这里很常见的一个报错就是login failed. check api token or gitlab version。这不是模型的问题是请求方用的 token 跟服务端不匹配或者服务端版本太老、不支持当前的鉴权协议路由与适配把请求映射到特定的模型 ID 上。比如modeldeepseek-v4-proHarness 就要查一下这个模型的 LoRA 权重、部署实例、显存余量然后决定是转发给哪个后端上下文组装如果是带历史消息的对话请求Harness 会把 system prompt、历史消息、用户输入拼成完整的上下文序列调用后端推理引擎这是真正送到 GPU 之前的最后一步之后请求就进入推理引擎的调度队列了。如果你是自己搭服务我建议 Harness 层最好单独拆出来做不要和推理引擎混在一个进程里。这样你换模型、做灰度、加限流都方便得多不会动不动就要重启 GPU worker。2. GPU 上的真正推理从 Prefill 到 Decode 的完整过程2.1 文本怎么变成 Token请求到了推理引擎后第一件事就是 token 化。英文文本大概一个词对应一个 token中文则复杂一些可能一个字或一个词对应一个或多个 token。这也是为什么模型的上下文窗口永远按 token 算而不是按字数算。Tokenizer 做的工作可以理解成“切词 映射”把输入文本切成一个一个 token ID每个 ID 对应词表中的某一项。这个映射表是训练时定死的不同模型的词表大小不一样常见的大模型词表在 3 万到 20 万之间。顺带一提今天模型动辄支持 128K、1048576 tokens 的上下文但 token 多不代表你都能用好。你发一个超长文档进去如果 prompt 构造不当、系统提示词占了几千 token留给真正“思考”的空间和显存都会受影响这是后面 KV Cache 部分要细说的核心矛盾。2.2 Prefill 阶段一次性吃下所有输入Token 化之后模型进入Prefill预填充阶段。这个阶段要一次性处理所有输入 token经过 embedding 层转成向量经过多层 transformer 计算把每个 token 的中间状态Key 和 Value存下来然后根据最后一个位置的隐藏状态预测第一个输出 token 的概率分布。Prefill 的特点是计算密集GPU 的算力在这个阶段被大量利用起来。你可以把它想象成“老师把你交上去的整篇论文一下子都读完了”而不是一个字一个字读。因此 Prefill 花的时间和输入长度基本成正比输入越长首次出字越慢。评测时你会发现 TTFTTime To First Token首 token 延迟高多半就是输入太长或者 GPU 算力不够。2.3 Decode 阶段一个字一个字往外蹦Prefill 完成后模型进入Decode解码阶段。这个阶段就变成了“逐 token 生成”每生成一个 token就要把最新位置的输出拼到序列尾巴上再重新计算一次 self-attention然后得到下一个 token。注意这里的循环依赖决定了它是内存密集且串行的。GPU 虽然并行能力强但 Decode 过程因为每一步都依赖上一步的结果没法完全并行化。这也是为什么大模型“生成速度”远低于“阅读理解速度”你体感上觉得一个字一个字往外蹦就是 Decode 阶段的天然特性。我在做 Codex 这类代码生成模型评测时深有体会代码长、逻辑复杂、输出动辄几百上千 tokenDecode 时间远大于 Prefill 时间。如果你测一个只有 50 token 输入、却要求生成 2048 token 输出的任务99% 的时间都花在 Decode 上。2.4 GPU 的算力和显存都在忙什么很多人好奇一次请求而已GPU 为什么会跑得那么满原因在于 transformer 的矩阵乘法。Prefill 阶段大矩阵乘大矩阵算力拉满Decode 阶段变成了大矩阵乘小矩阵但每个 token 都要把权重矩阵过一遍所以主要瓶颈是显存带宽算力反而不那么饱和。这就是为什么你在nvidia-smi里看到 GPU 利用率Volatile GPU-Util跳来跳去不需要太惊慌——它很多时候反映的是算力利用不代表模型没在干活。实操建议监控 GPU 时别只看利用率要重点盯显存占用、显存带宽、温度、功耗以及是否有 ECC 错误。我自己在 Manjaro 上就踩过 NVIDIA 驱动监控工具的坑后来干脆用nvidia-smi dmon和nvtop配合才把问题看明白。3. 并发与批处理为什么高并发请求不会立刻打爆 GPU3.1 同步阻塞和异步事件驱动的差别如果你的代码里写的是同步调用response client.chat.completions.create(...) print(response.choices[0].message.content)那在等待模型返回期间线程就卡住了。单线程发 10 个并发请求会退化成串行——第一个请求不返回第二个就不会开始。这不是服务端的问题是你客户端太天真。服务端推理引擎通常会用异步事件驱动模型来处理请求请求进来先进入队列引擎里有一个调度器持续从队列里取请求能并行处理的就并行处理。客户端如果是异步的比如用AsyncOpenAI一个进程就能同时发成百上千个请求而不会被阻塞住。3.2 Continuous Batching 是并发的灵魂早期大模型服务一次只能处理一个请求GPU 利用率惨不忍睹。后来业界搞出了Continuous Batching连续批处理调度器像拼车一样把同一时刻到达的多个请求拼到一个 batch 里共享 GPU 算力和显存。更牛的是Decode 阶段有请求生成完了可以随时“下车”新请求能随时“上车”不用等整个 batch 结束。这就像餐厅拼桌有人吃完先走新的客人马上补位而不是等所有客人都吃完再翻桌。所以高并发下不会“立刻打爆 GPU”是因为调度器会自动合并请求。但合并也有限度——每个请求都要占显存都要占算力。并发超过某个阈值后每个请求的速度会变慢甚至直接 OOM显存溢出。3.3 并发上限是怎么算出来的这里给出一个粗略估算方法。假设用 FP16 精度部署一个 70B 模型模型权重显存70B × 2 bytes ≈ 140 GBKV Cache 部分根据输入/输出长度和并发数动态增长假设每个请求平均 8K token 上下文KV Cache 大约占 4 GB具体计算下一章讲运行时开销激活值、CUDA context、中间缓存至少预留 10%~20%单卡扛不住 70B通常用多卡张量并行。如果你部署的是 7B 模型单卡 24 GB 显存就能跑但并发还要看 KV Cache 能分多少出去。实际压测下来显存够不代表并发能无限加——调度器本身的 CPU 开销、显卡带宽抢用、以及生成速度的下降会先一步让你受不了。我在给客户做 GPU 服务器运维时见过太多“显存够还超时”的案例。本质就是并发开太满每个请求的 Decode 速度被严重拉长导致客户端等不起。压测的目的不是找并发上限而是在某个可接受延迟范围内找吞吐最优值。3.4 部署环节的几个坑部署大模型到 GPU 上环境问题常常比模型本身更折腾。我之前在 CentOS 7.9 上装 GPU 驱动、装 PyTorch 的 CUDA 版本前前后后折腾了好几轮。几个关键注意点驱动版本要和 CUDA 版本匹配nvidia-smi能显示 CUDA 版本不代表你代码里那个 PyTorch 能用PyTorch 的 CUDA 版本要和驱动的驱动 API 兼容最好直接看官方pip install torch对应的 CUDA 版本安装完一定要验证torch.cuda.is_available()必须返回True然后建一个小的张量测试放到 GPU 上多卡机器记得跑一下nvidia-smi topo -m确认 NVLink 拓扑避免卡间通信走 PCIe 导致性能崩塌。4. KV Cache大模型高效推理的生命线4.1 为什么要存 Key 和 Value在自注意力机制中每个 token 都要产出 Query、Key、Value 三组向量。计算当前 token 的 attention 时需要用它的 Query 去跟历史上所有 token 的 Key 做点积再跟对应 Value 加权求和。如果不做任何缓存那么每生成一个新 token就要重新算一遍所有历史 token 的 Key 和 Value等于一个长度为 N 的序列要重复计算 N 次时间复杂度直接爆炸。所以推理引擎会把已经算好的 Key 和 Value 存在显存里后续直接读取。这就是KV Cache。你可以把它理解成“做过的题不用重新算翻笔记就能找到答案”。KV Cache 对 Decode 阶段的加速是决定性的没有它的模型输出慢得根本没法用。4.2 KV Cache 大小计算KV Cache 的显存占用公式是KV Cache 显存 2K 和 V 两份× 层数 × 注意力头数 × 每头维度 × 序列长度 × 精度字节数 × batch 大小举一个真实例子。假设一个 7B 模型层数 32注意力头数 32每头维度 128KV Cache 为每个 token 需要存的字节数就是2 × 32 × 32 × 128 × 2 bytes 512 KB。这还只是一个 token如果上下文长度是 8192单个序列的 KV Cache 就是512 KB × 8192 ≈ 4 GB。也就是说在 24 GB 显存上部署 7B 模型光一个长上下文请求的 KV Cache 就能吃掉 4 GB模型权重再占 14 GB 左右剩下的显存才算真正能匀给并发请求。这也是为什么你会看到api error: 400 this models maximum context length is 1048576 tokens这种报错。模型架构上支持那么长的上下文不代表你的部署实例真的能把那么多 token 的 KV Cache 全装进显存。实际调用中超过支持长度就会直接报 400 错误很多本地部署的模型甚至把 max 长度调得更低就是为了防止显存 OOM。4.3 为什么显存越大能支持的上下文越长一句话因为 KV Cache 是按 token 数线性增长的。你允许的上下文越大、并发请求越多KV Cache 占的显存就越大。两者近似成反比关系要么少并发跑长文本要么多并发跑短文本。实践中推理引擎都会提供各种 KV Cache 量化手段比如把 Key 和 Value 从 FP16 压到 INT8 甚至 INT4。代价是精度轻微下降但显存能省一半甚至更多。我做过测试INT8 量化 KV Cache 在绝大多数任务上对输出质量的影响可以忽略不计但并发能力能提升近一倍。如果你的服务对延迟和并发要求高这招值得试。4.4 PagedAttention 与显存碎片很多人听说过 vLLM 的 PagedAttention它模仿操作系统虚拟内存的分页机制把 KV Cache 按固定大小的块来分配而不是给每个请求提前分配完整长度的连续显存空间。这样能大幅减少显存碎片化也能支持更大规模的并发。从用户视角看PagedAttention 带来的最直观好处是同样的显存能容纳的并发请求变多了。原来一个 32K 上下文的请求可能预分配 8 GB 显存如果实际只用到 2K token剩下全浪费PagedAttention 则按需分配请求越长才占越多。如果你自己写推理服务、不想引入 vLLM 这么重的框架至少要注意一点不要为每个请求预留最大长度的 KV Cache否则并发一多直接 OOM这属于新手最容易踩的坑。5. 一个请求的完整生命周期与常见问题排查5.1 从客户端到 GPU 再到客户端把前面所有环节串起来一次请求的完整路径是这样的客户端构造请求模型 ID、messages、temperature、max_tokensHTTP 请求到达 Harness / API 网关Harness 做鉴权、限流、路由、上下文组装推理引擎接收处理好的 promptTokenizer 把文本切成 token IDPrefill 一次性处理输入 token生成并存储 KV CacheDecode 逐 token 生成每生成一个 token 就读取并更新 KV Cache响应通过流式或一次性返回给客户端Tokenizer 把输出 token 解码回文本其中任何一个环节出问题表现都不一样。常见的表象和根因对照如下现象可能原因请求秒回但内容是报错Harness 层查了 token 或路由还没到 GPU长时间无响应、卡住请求排队过长、显存不足、Decode 速度过慢请求返回 400提示超过上下文长度输入 输出超过部署实例支持的 token 上限并发一高就超时KV Cache 吃满显存、连续批处理队列堆积报错 login failed / API token 无效token 过期、服务端版本不匹配、鉴权协议不同报错 API scope is not declared网关侧权限配置不足请求方没有获得对应接口的授权5.2 典型问题排查流程我自己的排查顺序一般是这样第一步看日志和响应码。400、401、429、500 分别对应参数错误、鉴权失败、限流、服务端内部错误。响应体里通常会带上具体原因先看清楚再动手。第二步看服务端。登录 GPU 服务器nvidia-smi看显存占用是不是已经跑满。如果显存还剩很多但请求还是慢多半是算力或带宽瓶颈或者调度器配置有问题比如并发数上限设置过低。第三步看队列。推理引擎的监控页面或者日志里通常有排队数量。排队数居高不下说明并发已经饱和。第四步复现。用最小化的请求先测通比如发一个hello这种短请求确认链路是好的再逐步加大上下文、加大并发。这个方法排查问题效率最高。5.3 关于超长上下文请求的一个建议如果你遇到 max context length 触及上限的问题先别急着骂模型按照下面几步来压缩输入去掉冗余的系统提示词、精简历史消息、用摘要代替完整文档启用上下文压缩很多框架支持自动压缩历史消息提示用户分轮提问本质上就是“别一次塞太多”升级硬件提升显存容量或换用 KV Cache 压缩更好的推理引擎。如果是 1048576 tokens 这种百万级上下文基本意味着底层走的是稀疏注意力或者特殊的长文本优化架构对显存要求非常高本地部署前务必先查 KV Cache 的显存估算值别想当然。5.4 排查经验里的最后几个提醒日志里出现 API 相关报错时第一件事确认是不是甲方/平台侧权限问题而不是一上来就怀疑 GPU 或模型不要忽略 tokenizer 的差异同样的文本不同模型切出来的 token 数相差很大这会直接影响你的计费、上下文上限和 KV Cache 占用上线前一定要压测而且压测时用的 prompt 分布要贴近真实数据。我见过上线前用短 prompt 压测上线后被长文档用户瞬间打爆的案例问题就出在 KV Cache 占用被严重低估了。写在最后的一点体会把这整条链路摸清楚之后你会发现大模型 API 的“黑盒感”会明显消失。遇到请求慢、超时、并发上不去你能更理性地判断瓶颈到底出在哪个环节是 Harness 层的路由不对是 GPU 算力不够还是 KV Cache 显存吃满了。反过来说如果你正在部署自己的服务设计时也尽量按“请求入口 → 调度层 → 推理引擎 → 显存管理”这样的层次去拆每个环节做成可监控、可独立扩容的状态后续维护会省心很多。我自己最大的一个感受是很多人盯着一味追求更大的上下文窗口、更高的并发数却忽略了 KV Cache 带来的显存约束。硬件资源永远是有限的理解请求在 GPU 上的真实消耗方式往往比堆参数更能解决实际问题。希望这篇文章能帮你在实战中少走些弯路如果后续有具体的部署案例再跟大家继续细聊。