← 返回
IT技术

编码 Agent 的 TTFT 怎么测:前缀越长,首字延迟越不由硬件决定

✍️ zhirenhun 📅 2026/11/3 👁 7 阅读 ⏱ 50 分钟
编码 Agent 的 TTFT 怎么测:前缀越长,首字延迟越不由硬件决定

引言

部署后的编码 Agent 每一轮都要重新发送一段不断变长的前缀,包括系统提示词、工具定义和对话历史。前缀越长,预填充的工作量就越大,因此首 token 延迟(TTFT)更多取决于这段前缀有多少能从缓存中读取、而非重新计算,硬件本身的原始速度反而没那么关键。经过多轮对话和数千次工具调用,缓存命中与未命中的请求之间的差距会不断累积。本文在 DigitalOcean Serverless Inference 上对这一差距进行了实测。

2026 年 10 月 14 日至 16 日,我们在 DigitalOcean Serverless Inference 上测试了模拟编码会话,上下文长度分别为 20k、60k 和 120k token,模型为 Kimi K3、Qwen 3.8 Max 和 GLM 5.3。请求从一台笔记本电脑发出,因此结果中包含了客户端到服务端的网络路径。我们主要报告首个流式 token 的到达时间,结果部分还会给出首个可见内容 token 的时间——对推理模型而言,这一时间包含推理耗时。

三个模型上,缓存命中的 p50 TTFT 约为 1.0–1.9 秒,未命中时约为 1.9–9.3 秒。差距随上下文增长而扩大:20k token 时约为 1.5–1.8 倍,120k token 时约为 3.0–5.6 倍。在每组约 200 次缓存命中调用的大样本中,命中场景的 p95 约为 2.1–3.1 秒。前缀稳定时,缓存命中请求的命中率约为 98.5%–99.8%。而一旦在系统提示词中加入时间戳,或调整工具定义的顺序,三个模型的命中率都降到约 0%,TTFT 也回落到接近未命中的水平。

模型 20k 未命中 → 命中 p50(ms) 60k 未命中 → 命中 p50(ms) 120k 未命中 → 命中 p50(ms)
Kimi K3 2,238 → 1,392 4,670 → 1,942 9,329 → 1,862
Qwen 3.8 Max 2,048 → 1,340 3,098 → 1,580 5,477 → 1,814
GLM 5.3 1,926 → 1,047 4,183 → 1,353 8,172 → 1,450

首个流式 token 的到达时间,从笔记本电脑测得,每组 5 次未命中调用和 15 次命中调用。首个内容 token 的数据见下文结果部分。

这些数字只是示例,不是预测。同一测试连续两天各跑一次,部分单元格的结果相差可达约30%,所以你自己的数字也会不同。本文说明该测什么、如何用这套测试工具跑 DigitalOcean Serverless Inference,以及如何解读结果。测试工具放在公开仓库里,每次请求的原始结果在ttft_runs.csv中。

为什么编码 Agent 的表现不一样

聊天提示词很短,但编码 Agent 的上下文会随每一步增长,而且每次请求的大部分内容都是模型已经见过的前缀。前缀一长,prefill 就成了 TTFT 的大头,而从缓存中读取这段前缀能让模型省掉大部分计算。

Agent 每完成一个任务要发起很多次调用,成千上万次工具调用中,慢的那些会不断累积。Agent 要等每一步返回才继续行动,所以开发者注意到的是慢调用,而不是中位数。20 次调用里出现 1 次的 p95,在一个会话里会反复出现——这正是尾部延迟会随调用次数叠加的原因,也是本文报告 p95 而不只报中位数的原因。

如何测量首 token 时间:冷启动与缓存预热、p50 与 p95

TTFT 指从发出请求到收到第一个流式 token 的时间,不含输出生成。Agent 要等整步完成才行动,所以还要记录端到端步骤耗时(测试工具存为 total_s)。推理模型可能在输出任何可见内容之前先流式输出推理过程,因此你可以测任意类型的首个 token,也可以测首个可见内容 token。我们两者都报告,并以首个流式 token 作为主要指标,因为这是标准定义,而且它能单独反映提示词处理耗时——缓存改变的正是这部分。

对比缓存冷启动与缓存预热下的 TTFT。冷请求是某个前缀的第一次请求,模型要处理整个提示词;热请求复用已缓存的前缀,省下这部分计算。Agent 大多数轮次是热的,第一轮是冷的,所以两者都要报告。

用百分位数报告 TTFT。p50 代表典型调用:如果 p50 TTFT 是 1,000 ms,说明一半请求比这更快。p95 反映每 20 次调用中最慢的那次有多慢。p99 是罕见的极端情况,比如缓存未命中或副本繁忙。只有在样本量足够时才报告 p95 和 p99。测试框架在每格样本数达到 100 及以上时才会将其标记为可报告,而且我们不对任何格报告 p99。

缓存命中率等于 cached_tokens / prompt_tokens,它能解释一次调用为何快或慢。本该命中缓存的调用却出现低命中率,说明前缀变了、缓存被逐出,或者请求落到了另一个副本上——副本是运行在不同硬件上的模型独立拷贝。

DigitalOcean 托管的模型会自动启用缓存,缓存写入免费,缓存输入最高可便宜 90%(取决于模型),因此无需手动开启。稳定的前缀很重要,因为前缀中任何位置发生变化都会导致匹配失败。DigitalOcean 会把前缀匹配的请求路由到持有缓存的副本,所以在利用率极高时,请求可能被发往别处而错过缓存。

可选的 prompt_cache_key 参数会把相关请求归为一组,让它们一起路由,因此请在自己的流量上分别测试带与不带该参数的情况。缓存没有保证的过期时间,会逐出最近最少使用的条目,且属于尽力而为,所以要实际测量命中率而不是想当然,并与控制面板交叉核对。更多内容参见提示词缓存文档。

环境准备

要在 DigitalOcean 上测试 TTFT,首先需要一个 DigitalOcean 账户和一个模型访问密钥。还需要测试框架脚本。该脚本是用于测量你自己工作负载的示例代码,并非 DigitalOcean 产品,也不提供支持。克隆仓库以便跟随操作:

git clone https://github.com/adugan-do/coding-agent-ttft-benchmark.git
cd coding-agent-ttft-benchmark

harness 从环境变量 MODEL_ACCESS_KEY 读取密钥,所以请在终端里设置,不要写进脚本:

export MODEL_ACCESS_KEY="your-model-access-key"

这个测试框架用 Python 编写,并使用 openai SDK,因为 DigitalOcean Serverless Inference 兼容 OpenAI 接口。安装 SDK 可执行 pip install openai。基础 URL 是 https://inference.do-ai.run/v1。

接下来,确定要测试哪些模型。你可以用这个框架列出账户可用的模型,因为模型目录会随时间变化:

python3 harness/ttft_harness.py models

选3-4个支持工具调用的模型,因为编程Agent每次请求都会发送工具定义。我们测试了Kimi K3、Qwen 3.8 Max和GLM 5.3。把模型纳入测试之前,先查阅各自的文档,确认它支持工具调用。

正式跑完整实验前,先用probe命令发一次测试请求。它会发起两次完全相同的调用,你可以借此确认响应中包含usage.prompt_tokens_details.cached_tokens,并且第二次调用显示有缓存token。如果缺少这些字段,后续结果里的命中率就没有意义。

python3 harness/ttft_harness.py probe --model kimi-k3

最后,记录本次运行的条件:日期、时间窗口,以及你从何处运行测试框架,比如笔记本电脑,或与端点处于同一区域的服务器。TTFT 会随一天中的时段和客户端位置而变化。测试框架会在每一行保存一个 --region 标签,后续解读结果时需要用到它。

测试框架:回放一次编码 Agent 会话

测试框架已公开发布,可以直接按原样运行。它会回放一段合成的编码会话,包含一条系统提示词、四个工具定义(read_file、run_tests、grep 和 edit_file)、一个用户任务,以及一段工具调用与生成的类代码结果的历史记录,因此每个请求都带有很长的前缀,和真实 Agent 一样。最后一条消息只要求输出一句简短的话,且不调用任何工具,这样就能以流式文本的形式观察 TTFT。每个请求的 temperature 均为 0,输出上限默认为 256 个 token(--max-tokens)。以下四项设计让数据可信:

测量的核心是一次带计时器的流式调用。下面是一个简化版本:

def stream_call(client, model, messages, max_tokens, tools=None):
    start = time.perf_counter()
    first_chunk = first_content = cached = prompt = None
    stream = client.chat.completions.create(
        model=model, messages=messages, tools=tools, max_tokens=max_tokens,
        temperature=0, stream=True, stream_options={"include_usage": True},
    )
    for chunk in stream:
        now = time.perf_counter() - start
        if chunk.choices:
            delta = chunk.choices[0].delta
            has_content = bool(delta.content)
            has_other = bool(delta.tool_calls or getattr(delta, "reasoning_content", None))
            if first_chunk is None and (has_content or has_other):
                first_chunk = now
            if first_content is None and has_content:
                first_content = now
        if chunk.usage:
            prompt = chunk.usage.prompt_tokens
            cached = chunk.usage.prompt_tokens_details.cached_tokens
    return first_chunk, first_content, cached, prompt

这段代码同时记录 TTFT 的两种定义,并从 usage 块中读取 cached_tokens 和 prompt_tokens 来计算命中率。完整脚本还处理了一些额外细节,比如某些模型把缓存 token 放在别的字段里上报,以及把错误记录到对应行,这样单次调用失败不会中断整轮运行。

每个请求都保存为 CSV 的一行,包含模型、目标与实际 prompt token 数、缓存 token 数、两个 TTFT 值、总耗时、区域标签、时间戳以及错误信息(如有)。summarize 命令会把这些行汇总成 p50、p95、p99、p50 的置信区间、错误计数和中位命中率。

运行冷缓存与缓存预热对比实验及缓存破坏实验

我们用一条 run 命令跑了四个实验,每个请求都会追加写入同一个 CSV 文件,包括基线、多种上下文大小下的冷缓存与热缓存对比、缓存破坏测试,以及并行调用测试。

在消耗任何 token 之前,先用 --dry-run 跑一遍。它会打印计划使用的请求数和 prompt token 数,但不会实际发送任何请求。这个估算值是上限,因为它没有算上缓存 token 的折扣。可以用 --max-requests 设置硬性上限。

python3 harness/ttft_harness.py run \
  --models kimi-k3 \
  --sizes 20000 60000 120000 \
  --experiments baseline cold_warm cache_break \
  --cold-n 5 --warm-n 15 --cache-break-n 8 \
  --break-variants stable timestamp reordered_unique stable_key \
  --baseline-n 5 \
  --max-tokens 1024 \
  --max-requests 140 \
  --region "laptop" \
  --out data/run_kimi-k3.csv \
  --dry-run

去掉 --dry-run 就会开始正式运行。--region 标记只是给客户端运行位置打的一个标签。我们一次只跑一个模型,每个模型各存一个 CSV 文件。

--max-tokens 标记设定输出上限。这对推理模型很关键:它们可能把整个上限都花在推理上,还没开始流式输出内容就用完了。第一次运行时上限设为 256 token,Kimi K3 和 GLM 5.3 约有 5%-6% 的请求结束时没有输出任何内容 token。第二次运行把上限提到 1,024,主测试单元里每个请求都有内容 token。在更大的测试单元里,199 次 Kimi K3 调用中有两次没有内容 token,199 次 GLM 5.3 调用中也有两次,Qwen 3.8 Max 则一次都没有。检查一下你的数据里有多少行没有内容 token。

第一个实验是基线测试。它发送一个很小的未缓存提示词,再加一次列出模型的往返请求,用来测出客户端在涉及长上下文之前的最低延迟。之后测到的任何 TTFT 都包含这个最低值。

第二个实验是在各上下文大小下对比冷调用与热调用。测试工具会发起 --cold-n 次冷调用和 --warm-n 次热调用,外加一次预热调用。为控制成本,我们用了 5 次冷调用和 15 次热调用,这足够算 p50,但不够算 p95。为了看尾部情况,我们给每个模型跑了一个更大的测试单元,从 60k token 起步,包含 20 次冷调用和 200 次热调用。每次热调用都会增加一步,所以这些单元里的提示词增长了约 38k-50k token,就像一次长时间的 Agent 会话。

第三个实验是缓存破坏检查。它在一个上下文大小下运行(--break-size,默认 60k),比较不同的前缀变体。每个变体先预热一个会话,然后每次重复时多加一步。

用这个实验来测试你自己的提示词。如果你的 Agent 在提示词开头附近加了时间戳、请求 ID 或不断变化的工具列表,这项检查能告诉你代价有多大。测试工具还有一个 changing_key 变体,每次调用都发送一个新的 prompt_cache_key。

并行模式先预热一个前缀,然后发送多个并发请求,这些请求只在最末尾不同,用来检验并发调用是否仍能命中缓存。我们在 60k token 下跑了 8 个并发请求,共 5 轮。把 parallel 加进 --experiments,并设置 --parallel-width 和 --parallel-rounds。

测试框架还提供可选的 Inference Router 测试,但我们不报告其结果,因为路由器会为每个请求选择模型,测试结果无法单独体现它自身的开销。Inference Router 目前处于公开预览阶段,其当前行为请查阅文档。

最后,在采信某个数值之前,请在一天中的不同时段重复运行,因为 TTFT 取决于平台负载。用 summarize 把 CSV 转成百分位数、命中率和错误计数:

python3 harness/ttft_harness.py summarize --csv data/run_kimi-k3.csv

先看 n_errors 列。如果某个单元格有错误,百分位数只反映成功的调用。

编码 Agent 的冷启动与缓存预热 TTFT 对比(p50 和 p95)

第一张表列出各上下文长度下冷请求与热请求的 p50 TTFT,按两种口径测量:任意类型的首个流式 token,以及首个可见内容 token。比值是冷 p50 除以热 p50。每个单元格包含 5 次冷调用和 15 次热调用。

模型 上下文 冷启动首 token(ms) 热启动首 token(ms) 比值 冷启动内容(ms) 热启动内容(ms) 比值
Kimi K3 20k 2,238 1,392 1.6x 3,842 3,458 1.1x
Kimi K3 60k 4,670 1,942 2.4x 6,462 4,516 1.4x
Kimi K3 120k 9,329 1,862 5.0x 11,440 4,886 2.3x
Qwen 3.8 Max 20k 2,048 1,340 1.5x 2,048 1,340 1.5x
Qwen 3.8 Max 60k 3,098 1,580 2.0x 3,098 1,580 2.0x
Qwen 3.8 Max 120k 5,477 1,814 3.0x 5,477 1,814 3.0x
GLM 5.3 20k 1,926 1,047 1.8x 3,395 2,492 1.4x
GLM 5.3 60k 4,183 1,353 3.1x 6,317 3,032 2.1x
GLM 5.3 120k 8,172 1,450 5.6x 10,082 2,831 3.6x

冷启动 TTFT 随上下文增长得很快。以 Kimi K3 为例,首 token 从 20k 时的约 2.2 秒涨到 120k 时的约 9.3 秒,而热启动 TTFT 只从约 1.4 秒升到约 1.9 秒。正因如此,长时间运行的 Agent 最能从稳定的前缀中获益。

TTFT 的两个版本在流式输出推理过程的模型上结果不同。Qwen 3.8 Max 的两个数值相同,因为在我们发起的请求中它没有流式输出任何推理内容。在一次单次调用检查中,它的流里只有 content 增量,usage 块报告的推理 token 数为 0。对 Kimi K3 做同样的检查,可以看到内容之前先有 reasoning_content 增量,推理 token 数为 309。我们没有测试 Qwen 3.8 Max 是否存在某种思考模式、只是我们的请求没有开启,因此它的内容 TTFT 无法与会推理的模型的内容 TTFT 直接比较。在 Kimi K3 和 GLM 5.3 上,热调用时首个内容 token 比首个流式 token 晚 1.4–2.6 秒到达。我们只对 Kimi K3 确认了推理流,所以 GLM 5.3 的情况看起来像推理耗时,但尚未确认。缓存能消除预填充时间,但消除不了推理时间,因此从首个内容 token 看,缓存带来的差距显得更小。如果你的用户只看得到内容,那么这一列才符合他们的实际体验。

热请求几乎全部由缓存提供。各模型、各上下文规模的中位命中率都在 98.5% 到 99.8% 之间,冷请求则约为 0%。

模型 20k 60k 120k
Kimi K3 98.5% 99.5% 99.7%
Qwen 3.8 Max 98.5% 99.5% 99.7%
GLM 5.3 98.9% 99.6% 99.8%

即便命中率达到 99%,热请求的首 token TTFT 也没有降到零,仍维持在约 1.0–1.9 秒。部分原因是我们这套环境的最低延迟。在基线测试中,从笔记本上列出模型列表耗时约 500 ms,且不涉及推理;一个极小的未缓存提示词耗时约 0.8–0.95 秒。如果服务器与端点位于同一区域,最低延迟很可能会更低。

两次运行之间数值也有波动。两次运行在连续两天进行,除输出上限外设置相同。Qwen 3.8 Max 在 20k 下的冷请求 p50 从 1,607 ms 升至 2,048 ms,热请求 p50 从 1,038 ms 升至 1,340 ms(约 27%)。Kimi K3 在 60k 下的热请求从 1,719 ms 升至 1,942 ms(约 13%)。整体规律保持一致。

缓存失效测试说明前缀有多重要。在60k上下文下,我们对比了几种情况:稳定前缀、在系统提示词里放时间戳、每次调用都打乱工具定义顺序,以及稳定前缀加固定prompt_cache_key。这项测试来自第一轮运行,当时256 token的输出上限截断了一些推理回复,所以表格展示的是首token TTFT。每个变体各做八次热调用。

模型 变体 热调用首token p50(ms) 命中率中位数
Kimi K3 稳定 1,780 99.5%
Kimi K3 稳定 + prompt_cache_key 1,757 99.5%
Kimi K3 时间戳 4,796 0.0%
Kimi K3 工具重排 5,172 0.0%
Qwen 3.8 Max 稳定 1,476 99.5%
Qwen 3.8 Max 稳定 + prompt_cache_key 1,338 99.5%
Qwen 3.8 Max 时间戳 3,210 0.0%
Qwen 3.8 Max 工具重排 3,098 0.0%
GLM 5.3 稳定 1,440 99.6%
GLM 5.3 稳定 + prompt_cache_key 1,757 99.6%
GLM 5.3 时间戳 4,272 0.6%
GLM 5.3 工具重排 4,176 0.1%

三个模型上都一样:系统提示词里只要有一行发生变化,或者工具列表顺序被打乱,命中率就会掉到接近0%,TTFT也回到大致冷启动水平(60k上下文下,Kimi K3约4.8–5.2秒,Qwen 3.8 Max约3.1–3.2秒,GLM 5.3约4.2–4.3秒)。固定prompt_cache_key对命中率和TTFT没有可测量的影响,因为稳定前缀本来就已经匹配上了。稳定行与prompt_cache_key行之间的小差异,比如GLM 5.3的1,440 ms和1,757 ms,落在我们观察到的运行间波动范围内。你的实际流量表现可能不同,所以值得亲自测一测。

60k上下文下的并行调用测试中,我们同时发出八个共享前缀的请求,共五轮(每个模型40次热调用)。首token p50为:Kimi K3 1,790 ms,Qwen 3.8 Max 1,749 ms,GLM 5.3 1,531 ms,与60k下顺序热调用的数字接近。命中率方面,Kimi K3为99.8%,另外两个模型为100%,说明共享前缀的并发请求同样能命中缓存。由于只有40次调用,我们只报告p50。

最后,较大的单元格测量尾部延迟。每个模型从 60k token 的起点出发,各进行 20 次冷调用和 200 次热调用。由于样本量约为 200,这些单元格我们报告 p95。

模型 热调用(失败数) 首 token p50(ms) 首 token p95(ms) 内容 token p50(ms) 内容 token p95(ms) 缓存命中率中位数
Kimi K3 199(1) 1,826 2,445 3,718 6,142 99.6%
Qwen 3.8 Max 200(0) 1,767 3,100 1,767 3,100 99.6%
GLM 5.3 199(1) 1,405 2,126 3,189 6,340 99.7%

这些单元格中,冷调用首 token p50 分别为:Kimi K3 5,034 ms(20 次调用)、Qwen 3.8 Max 3,142 ms(20 次调用)、GLM 5.3 4,230 ms(20 次调用)。热调用首 token p50 的 95% 置信区间为:Kimi K3 1,784–1,871 ms,Qwen 3.8 Max 1,716–1,836 ms,GLM 5.3 1,355–1,431 ms。内容列使用有内容 token 的调用:Kimi K3 197 次、GLM 5.3 197 次。这些单元格并非固定 60k。每次热调用都会增加一步,因此提示词长度在 Kimi K3 上从约 64k 增长到约 114k token,在 Qwen 3.8 Max 上从约 61k 增长到约 109k,在 GLM 5.3 上从约 61k 增长到约 98k。在 Kimi K3 上,首 token p50 从前 50 次热调用的约 1.7 秒上升到后 50 次的约 2.0 秒;Qwen 3.8 Max 则从约 1.6 秒上升到约 1.9 秒。

尾部延迟远高于中位数。在这些单元格中,首 token p95 是 p50 的 1.3–1.8 倍,即多出 0.6–1.3 秒(Kimi K3 1.3 倍,GLM 5.3 1.5 倍,Qwen 3.8 Max 1.8 倍)。在内容 token 上,Kimi K3 和 GLM 5.3 的 p95 分别约为 6.1 秒和 6.3 秒,而 p50 分别约为 3.7 秒和 3.2 秒,原因是每次调用的推理长度不同。

如何解读你自己的 TTFT 结果

拿到自己的 CSV 后,先做几项检查,再决定是否相信任何 TTFT 数字。

首先看缓存命中率。如果热调用的命中率接近 0%,说明你测的其实是冷调用。这通常意味着提示词开头附近有内容在两次调用之间发生了变化,比如时间戳、请求 ID,或者工具列表顺序不同。如果命中率不稳定,有些调用接近 0%、有些接近 99%,要逐行查看,不能只看中位数。在我们早前的运行中,gpt-oss-120b 即使前缀稳定也出现了这种情况,不过最新几轮运行没有把它纳入。

接下来,看看热 TTFT 如何随上下文大小变化。在我们的结果中,随着上下文增长,热 TTFT 几乎保持不变,而冷 TTFT 持续攀升。如果你的热 TTFT 很高且不随上下文大小变化,那这段时间很可能不是 prefill,更可能是固定开销,比如网络路径或排队。在推理模型上,对比首 token 和首内容的时间数字,因为差距大意味着推理时间,而缓存无法消除这部分。还要统计没有内容 token 的行,因为输出上限过低可能在内容开始前就截断推理。

然后对比 p95 与 p50,并检查样本数。如果 p95 远高于 p50,说明存在离群值,而调用频繁的 Agent 必然会遇到它们。p95 只统计成功调用,会掩盖失败调用,所以还要检查 n_errors。尾部数字只有在样本足够时才可靠,因此 summarize 会把样本少于 100 的单元格的 tail_ok 标为“no”。如果 p50 的置信区间很宽,说明需要更多调用。

要把网络时间和模型时间分开,可以将基线行与 TTFT 数字对比,并从与 Agent 同区域的服务器运行同一套测试工具。如果数字大幅下降,说明网络占了你所测时间的很大一部分。

冷热之间的差距也能反映提示词设计。差距大说明缓存生效,稳定的前缀值得保护。如果缓存破坏检查显示,时间戳这类小改动就导致大幅下降,就把每次请求都会变化的内容移到提示词末尾、稳定部分之后。如果没有差距,检查提示词是否足够长以被缓存,以及前缀是否在变化。

即使提示词很好,也会出现一些未命中,所以要判断多次调用中的整体模式,而不是单次未命中。

如果已经修好提示词,仍需要更低或更可预测的延迟,可以看看 Dedicated Inference,它把模型部署在专用 GPU 上作为你自己的端点。决定前,用同一套测试工具测一测。

局限性与在自己的 trace 上运行

我们的所有结果都来自三天。我们没有在更长周期内重复,因此无法说明 TTFT 如何随平台负载变化,而且前两次运行结果就有差异,结果部分已经展示。我们只从一个客户端位置(一台笔记本电脑)运行,API 也不报告由哪个区域处理请求,所以无法记录端点区域。我们的 TTFT 有一部分来自那台笔记本电脑的网络路径。

这条 trace 是合成的,形态上贴近真实场景:有系统提示词、工具定义,还有不断增长的历史记录,但工具结果是生成的类代码文本,并非真实 Agent 会话。真实 trace 的 token 分布不同,轮次之间也有停顿。除并行测试外,测试框架一次只发一个请求,并且要求返回不含工具调用的简短文本回复,因此真实 Agent 的单步耗时会有差异。我们还将输出上限设为 1,024 token,所以推理时间更长的推理模型在我们的结果中不会达到其内容 token。

为控制成本,我们的样本量较小。每个单元格 5 次冷调用、15 次热调用,p50 可用,但尾部百分位不可用。我们只对三个较大的单元格报告 p95,且每个 p95 仅描述成功的调用。缓存失效结果来自第一次运行,每个变体 8 次调用。缓存是尽力而为,换一次运行可能看到更多或更少的未命中。

受这些限制影响,使用本文的最佳方式是在你自己的 trace 上运行测试框架。我们的合成会话位于 build_messages 函数中,系统提示词在 SYSTEM_PROMPT 里,工具定义在 TOOLS 里。请把它们替换为你自己 Agent 日志中的系统提示词、工具定义和消息历史。测试框架不会自行导入日志,因此需要对脚本做一点小改动。消息顺序要与 Agent 发送时一致,把稳定部分放在前面,因为缓存按前缀匹配。

挑选几个能代表真实流量的会话,比如一个短的、一个典型的、一个长的。第一个请求作为冷调用发送,然后按 Agent 的实际行为逐轮重放,让历史不断增长。重放前先清除日志中的隐私内容,例如密钥、客户数据和文件路径。

测试过程中,你可以把测得的命中率与 DigitalOcean 控制面板(Analyze 标签页)中的缓存指标做对比。如果对不上,检查你发送的请求是否与 Agent 发送的一致。TTFT 也会随时间变化,因此要定期重跑并保留 CSV 文件。结果发生变化,可能说明提示词改了,或者平台负载不同了——这能让你在用户察觉之前就发现问题。

常见问题

编程 Agent 的 TTFT 应该是多少?

这取决于你的工作负载、所在位置,以及提示词中有多少内容能从缓存命中。在我们用 DigitalOcean Serverless Inference 跑的示例中,上下文规模在 2 万到 12 万 token 之间时,缓存预热后 p50 的首个流式 token 到达时间约为 1.0-1.9 秒,冷启动 p50 约为 1.9-9.3 秒。这些数字来自一台笔记本电脑和一条合成轨迹,所以请在你自己的真实轨迹上运行测试工具,得出你自己的数据。

如何测量编码 Agent 的首 token 时间?

发送一个流式请求,发送时启动计时器,第一个 token 到达时停止计时。推理模型可能会在任何可见内容之前先流式输出推理过程,所以要分别记录任意类型的首个 token 和首个内容 token。按你的 Agent 实际达到的上下文规模回放一段真实会话,并分别测量冷请求和热请求。报告中位数,只有在样本量足够时才报告 p95。环境搭建和实验两节展示了完整流程。

提示词缓存能降低工具调用频繁的编码 Agent 的 TTFT 吗?

可以,前提是每次调用之间提示词的开头保持不变。编码 Agent 每次调用都会重新发送系统提示词、工具定义和历史记录,所以每个请求的大部分内容都是重复的前缀。在我们的结果中,热请求的命中率为 98.5%-99.8%,且与冷请求的差距随上下文增长而扩大:以首个流式 token 计,2 万 token 时约为 1.5-1.8 倍,12 万 token 时约为 3.0-5.6 倍。

为什么长时间 Agent 会话中缓存命中率会下降?

最常见的原因是提示词开头附近的内容在两次调用之间发生了变化。在我们的缓存破坏测试中,往系统提示词里加一个时间戳,或者调整工具定义的顺序,命中率就会掉到约 0%。其他原因包括最近最少使用(LRU)淘汰,以及在利用率极高时,请求被路由到不持有该缓存的副本。保持提示词开头逐字节一致,把每次请求都会变化的内容放到末尾,并在测试工具输出和控制面板中检查命中率。

如何让 Agent 的请求固定在同一模型上以利用缓存?

缓存与模型一一对应,只有发往同一模型的请求才能共享缓存。直接调用某个模型(例如 kimi-k3)时,请在整个会话中保持该 ID 不变。如果使用 Inference Router,模型可能在请求之间发生变化,因此请查阅 Inference Router 文档中的 X-Model-Affinity 请求头,了解如何让相关请求固定在一个模型上。我们的测试结果没有使用该路由器。你也可以发送固定的 prompt_cache_key 来对相关请求分组,不过在本次测试中,它并未改变命中率或 TTFT。

结论

对编码 Agent 而言,首 token 时间在很大程度上取决于每个请求的前缀有多少由缓存提供。在我们的示例运行中,随着上下文增长,冷请求明显变慢,而缓存已预热的请求则平缓得多;只要改动提示词开头附近的一行内容(例如时间戳),延迟就会回到接近冷请求的水平。具体毫秒数取决于你的提示词、模型、客户端位置和平台负载,因此最可靠的数字是你在自己工作负载上实测得到的那个。

具体做法是:对比缓存冷、缓存热两种状态下的 TTFT,检查命中率,排查提示词中任何会破坏前缀的内容,报告中位数,并且只有在样本量足够时才报告 p95。然后在不同时段重复运行,因为单个时间窗口只能反映一种情况。

你可以在 DigitalOcean Serverless Inference 上用已发布的测试工具运行同样的测试,它提供与 OpenAI 兼容的托管模型访问方式,并自动启用提示词缓存。

——

广告 广告
🧑‍💻

zhirenhun

一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。