ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

一次 LLM 推理的完整旅程:从你按下回车到最后一个字吐出,TaoToken 统一 Key 通道下的 Prefill/Decode 全链路拆解

一次 LLM 推理的完整旅程:从你按下回车到最后一个字吐出,TaoToken 统一 Key 通道下的 Prefill/Decode 全链路拆解 1. 按下回车之后一次 LLM 请求到底经历了什么你在聊天框里敲下一段 2000 token 的提示词按下回车然后盯着屏幕。大概一两秒后第一个字蹦出来接着像打字机一样一个字一个字往外吐直到 500 个 token 全部生成完。整个过程看起来很简单但服务端其实跑完了一条相当长的流水线。这条流水线可以拆成四步Tokenize、Prefill、Decode、终止与释放。其中真正决定你体验的是 Prefill 和 Decode 这两个阶段——一个决定你等多久才看到第一个字另一个决定后面吐字有多快。而 KV Cache 和 Continuous Batching 这两个词则是理解显存占用和并发吞吐的关键。这篇文章不讲抽象概念而是带你从一次真实请求出发把每个阶段的耗时来源、显存占用来源都拆开看。我会给出可复制的请求配置示例以及逐阶段打点验证的具体动作帮你在自己的环境里定位首字延迟TTFT和吐字速度TPOT的瓶颈到底出在哪。适合谁看正在调用大模型 API、被首字延迟或输出速度困扰的开发者想搞懂为什么输出 token 比输入 token 贵好几倍的工程师以及准备做推理优化、需要知道该从哪个环节下手的同学。读完你至少能回答三个问题我的请求慢在哪、显存被谁吃了、以及怎么用统一 Key 通道把这条链路跑通并观测起来。2. 用 TaoToken 统一 Key 通道跑通这条链路在拆解链路之前得先有一个能稳定发请求、并且能观测到耗时和 token 用量的入口。我试过直接对接各家模型厂商的原生接口问题在于每家的鉴权方式、请求格式、流式返回的字段名都不一样想横向对比 Prefill 和 Decode 的表现光适配就要花掉大半天。TaoToken 在这里的作用是提供一个统一的 Key 通道你只需要一个 API Key、一个 Base URL就能用同一套请求格式调用不同模型。这对我们做链路观测特别有用——因为请求格式统一了打点代码写一次就能复用换模型只需要改一个 model 字段。它的接入地址是 https://taotoken.net/api兼容 OpenAI 的请求协议。也就是说你原来用 openai 的 SDK 写的代码只需要把 base_url 和 api_key 换掉其余逻辑几乎不用动。对于本文这种需要反复发请求、对比不同参数下 Prefill/Decode 表现的场景这一点能省掉大量重复劳动。具体来说统一通道帮我们解决了三件事。第一是鉴权统一不用为每个模型单独管理一套密钥。第二是请求体统一messages、stream、max_tokens 这些字段的含义在所有模型上保持一致打点逻辑不用分支。第三是返回结构统一尤其是流式返回里每个 chunk 的字段位置一致我们才能用同一段代码去计算 TTFT 和 TPOT。需要说明的是TaoToken 是请求入口和通道不改变模型本身的推理过程。Prefill 和 Decode 发生在模型服务端的 GPU 上我们能观测到的是端到端的延迟和 token 流。但正因为有了统一入口我们才能把网络传输耗时和模型计算耗时大致区分开——这一点在排查首字延迟时非常关键。如果你还没有 Key可以先去控制台创建一个。拿到 Key 之后把它放进环境变量后面所有示例都从环境变量读取避免硬编码。这一步做完我们就可以进入具体的配置环节了。3. 可复制的请求配置与逐阶段打点这一节是全文最核心的部分我会给出完整的配置片段和打点代码。你可以直接复制到自己的项目里跑。先看配置。推荐用环境变量管理避免把 Key 写进代码。创建一个.env文件TAOTOKEN_API_KEYsk-你的key TAOTOKEN_BASE_URLhttps://taotoken.net/api然后在 Python 里读取。下面这段代码用 OpenAI SDK 发起一个流式请求并在每个 chunk 到达时打点import os import time from openai import OpenAI from dotenv import load_dotenv load_dotenv() client OpenAI( api_keyos.getenv(TAOTOKEN_API_KEY), base_urlos.getenv(TAOTOKEN_BASE_URL), ) prompt 请用 300 字解释什么是 KV Cache。 * 20 # 构造一个较长的输入 start time.perf_counter() first_token_time None token_count 0 stream client.chat.completions.create( modelgpt-4o-mini, # 换成你要测的模型 ID messages[{role: user, content: prompt}], streamTrue, temperature0.7, max_tokens500, ) for chunk in stream: delta chunk.choices[0].delta if delta and delta.content: token_count 1 if first_token_time is None: first_token_time time.perf_counter() ttft first_token_time - start print(fTTFT首字延迟: {ttft*1000:.1f} ms) end time.perf_counter() total end - start decode_time end - first_token_time tpot decode_time / max(token_count - 1, 1) print(f总耗时: {total*1000:.1f} ms) print(f输出 token 数: {token_count}) print(fTPOT每 token 耗时: {tpot*1000:.1f} ms) print(f吐字速度: {1/tpot:.1f} token/s)这段代码做了三件事记录请求发出到第一个 token 到达的时间TTFT主要对应 Prefill 阶段记录第一个 token 到最后一个 token 的时间Decode 阶段以及用总输出 token 数算出 TPOT。如果你想用配置文件的方式管理多个模型可以写一个 JSON{ base_url: https://taotoken.net/api, models: { fast: gpt-4o-mini, strong: gpt-4o, reasoning: o3-mini }, default_params: { stream: true, temperature: 0.7, max_tokens: 500 } }这样切换模型只需要改一个 key打点代码完全不用动。对于要对比不同模型 Prefill/Decode 表现的场景这个结构能让你少写很多重复代码。配置里还有几个参数直接影响链路表现值得单独说。streamTrue是必须的否则你拿不到 TTFT只能等整个响应结束。max_tokens决定了 Decode 循环的上限设得越大最坏情况下的输出耗时越长。temperature影响采样但不影响 Prefill 和 Decode 的计算量所以对延迟的影响可以忽略。把配置和打点代码准备好之后下一步就是实际发请求、看结果并对照不同输入长度下的表现。4. 验证请求与成功结果解读配置写好后跑一次上面的脚本你会看到类似这样的输出TTFT首字延迟: 820.3 ms 总耗时: 6420.7 ms 输出 token 数: 312 TPOT每 token 耗时: 18.0 ms 吐字速度: 55.6 token/s这组数字怎么读TTFT 是 820 毫秒说明 Prefill 阶段加上网络往返花了大约 0.8 秒。总耗时 6.4 秒减去 TTFTDecode 阶段大约 5.6 秒生成了 312 个 token平均每个 token 18 毫秒也就是每秒约 55 个 token。现在做一组对照实验把输入长度翻倍其他不变。你会观察到 TTFT 明显上升但 TPOT 基本不变。这正是 Prefill 和 Decode 性质不同的直接体现Prefill 的计算量随输入长度增长注意力部分还是平方增长所以输入越长首字越慢而 Decode 每步只处理一个 token单步成本相对固定所以吐字速度对输入长度不敏感。再换一个更大的模型比如从 mini 换成完整版。你会看到 TTFT 和 TPOT 都上升因为模型权重更大Prefill 的矩阵乘更重Decode 每步要搬运的权重也更多。这印证了 Decode 是内存带宽受限的——权重体积越大每步搬运时间越长。还有一个值得验证的点把同样的 prompt 连续发两次第二次的 TTFT 往往会明显下降。这就是 Prefix Caching 在起作用。第一次请求的 KV Cache 被保留第二次相同前缀直接命中跳过了大部分 Prefill 计算。你可以用同一个 prompt 连发三次观察 TTFT 的变化曲线通常第一次最高后面几次显著降低。成功跑通之后你应该能拿到三个关键指标TTFT、TPOT、以及总输出 token 数。这三个数字就是你后续排查瓶颈的基准线。任何优化动作都是围绕降低 TTFT 或提升吐字速度展开的。5. 常见报错与排查对照实际跑的时候大概率会遇到几个典型报错。这一节按报错信息对照排查都是真实会碰到的。401 Unauthorized。最常见的原因是 Key 没读到或者写错了。先确认.env文件里的变量名和代码里os.getenv的参数完全一致大小写敏感。如果用的是 shell 环境变量确认已经export过。还有一种情况是 Key 本身失效或额度用尽去控制台确认一下状态。注意不要把 Key 直接写进代码再提交到仓库这是很多 401 的隐藏来源——本地能跑CI 里就挂。local proxy failed / connection error。这类报错通常是网络层的问题。先确认base_url写的是https://taotoken.net/api不要多加或少加路径。然后用 curl 单独测一下连通性curl -s -o /dev/null -w %{http_code}\n https://taotoken.net/api/models \ -H Authorization: Bearer $TAOTOKEN_API_KEY如果返回 200说明通道是通的问题在客户端代码如果超时或返回其他状态码检查本地网络配置和 DNS 解析。reading choices / KeyError: choices。这个报错说明返回结构和你预期的不一样。常见于流式请求里某些 chunk 的choices为空数组——比如最后一个 chunk 只带 usage 信息。正确写法是先判断chunk.choices是否非空再取deltaif chunk.choices and chunk.choices[0].delta.content: ...另外如果请求本身出错返回体里可能是error字段而不是choices直接取choices就会报错。加一层判断能避免大部分这类问题。OAuth / 鉴权方式不匹配。有些模型或工具默认走 OAuth 流程而统一通道用的是 Bearer Token。如果你在某个客户端里配置确认鉴权方式选的是 API Key 而不是 OAuth。以 Claude Code 这类工具为例接入时需要同时确认三件套Base URL 填https://taotoken.net/apiKey 填你的 TaoToken KeyModel ID 填你要用的模型标识。三者缺一不可任何一个填错都会表现为鉴权失败或模型不存在。模型不存在 / model not found。检查 model 字段拼写以及该模型是否在你的可用列表里。不同通道支持的模型 ID 可能不同去文档里核对一下准确的标识符。排查的基本顺序是先确认鉴权401 类再确认网络连接类最后确认请求体和返回结构解析类。按这个顺序走大部分问题能在几分钟内定位。6. 把这条链路用起来从观测到优化跑通并观测之后你会发现优化方向其实很清晰。如果 TTFT 高重点看输入长度和前缀缓存——把 system prompt、few-shot 示例、固定文档放在 prompt 开头并保持逐字节稳定让 Prefix Caching 尽量命中。如果 TPOT 高、吐字慢重点看模型大小和输出长度——能要 JSON 就别要散文设置合理的 max_tokens避免让模型生成大量无用内容。对于延迟敏感的场景流式输出是必选项。用户在 TTFT 之后立刻开始阅读感知延迟会大幅下降哪怕总耗时没变。这一点在交互式产品里尤其重要。如果你要长期做编码类或 Agent 类任务可以考虑用 Coding Plan 这类方案把请求通道和额度管理统一起来省去反复配置的麻烦。想先验证模型效果可以直接在模型对话里试需要创建和管理 Key去 API Keys 页面接入细节和参数说明文档里有完整对照。把打点代码留在你的项目里每次调整参数或换模型都跑一遍TTFT 和 TPOT 的变化会告诉你优化有没有生效。这条链路一旦观测起来后面所有的性能决策就都有了依据。
返回列表