Skip to content
Unstructured Play
Go back

Agent 首字延迟(TTFT)优化:从原理到落地

回路 / The Loop — 提出有价值的问题,比直接获取答案更重要。

这个专题收集和整理我与 Gemini、ChatGPT、Claude 等 AI 的对话记录。每一篇对应一次完整的提问与回答过程。通过留存这些层层追问,还原日常思考的真实轨迹。


概要

这是一场关于 Agent 首字延迟(TTFT)优化 的工程对话。从”首字为什么慢”的底层原理出发,逐步追问到快慢多级路由的具体分工、Prompt Caching 与动态工具加载之间的冲突、Anthropic Managed Agents 架构解耦的工程意义,以及在 DeepSeek 公有云 API 上的缓存预热落地方案。每追问一层,就厘清一个”听起来正确但实操有坑”的工程假设。

核心结论:将静态前缀固化以命中缓存、让重度计算惰性后置、用小模型的即时响应填补主力模型的预热窗口——三者组合,是当前降低 Agent TTFT 的有效实践。


Q: Agent 的首字延迟(TTFT)到底卡在哪里?有哪些降低它的工程手段?

TTFT(Time to First Token,首字延迟)是指用户发出请求后到看见第一个输出字符之间的等待时间。降低它,本质上是在做两件事:缩短大模型的 Prefill 耗时,以及消除 Agent 编排层的同步阻塞

先解释一下 Prefill:大模型在输出第一个字之前,必须先并行处理完输入的所有 Token(提示词中的每个词汇片段),并为它们生成 KV 缓存(Key-Value Cache,一种中间计算结果的存储,后续生成每个新字时都要读取它)。输入的 Prompt 越长——包括 Agent 垫底的系统提示、工具定义、历史对话——这个阶段耗时就越长。

业内的优化手段可以归纳为四个维度:

优化维度核心手段TTFT 降低幅度实施复杂度
工程基础设施Prompt Caching(提示词缓存)50% - 90%
编排架构设计快慢多级路由 / 异步并发优化30% - 60%
交互与传输全链路 SSE 流式传输体感延迟接近 0
RAG 数据链路意图预测与并行检索20% - 40%

注:SSE(Server-Sent Events)是一种服务器向浏览器单向推送数据的技术,模型生成的每个字可以即时推送到前端,而不必等全部生成完再一次性返回。

关键落地要点:

  1. Prompt Caching:确保系统提示、工具定义和历史对话的前缀部分结构完全一致,让推理引擎直接复用已计算的 KV 缓存,跳过 Prefill 阶段。
  2. 动态上下文裁剪:不要把所有历史消息和 RAG(检索增强生成)召回的文本碎片全部塞入,应使用动态摘要或滑动窗口截断。
  3. 流式思维链输出:将模型的思考过程直接流式推送给前端(可折叠显示),让用户在 100-200ms 内就看到”模型已开始思考”。
  4. 全链路异步:Agent 内部各节点之间通过异步生成器流式对接,避免中间同步等待。

Q: 快慢多级路由(Multi-grade Routing)具体是怎么运转的?小模型和主力模型如何分工?

快慢多级路由的核心思想是:把”选择工具”与”执行工具/深度推理”分离。

一个轻量、高吞吐的小模型负责前置判断(类似医院的分诊台),只在确认需要深度推理时才把请求转交给主力大模型。

职责分工

角色典型模型核心职责特点
前置小模型(Fast Router)7B-14B 参数规模(如 Qwen-2.5-7B、Llama-3-8B)或专用分类器意图分类、闲聊拦截、工具初筛快(TTFT < 100ms),成本低
后端大模型(Slow Reasoner)Claude Sonnet、DeepSeek-V3 等旗舰模型复杂规划、多步工具调用、深度内容合成能力强,但 Prefill 较慢(300ms-1s+)

整体流程

[用户输入]


┌──────────────────────────────┐
│ Phase 1: 小模型意图分类        │  ← 极精简 Prompt,毫秒级响应
└──────────────────────────────┘

     ├─► 路径 A(极速):闲聊/简单FAQ → 小模型直接流式回复(结束)

     ├─► 路径 B(标准):明确命中某工具 → 并行触发 RAG 检索 + 主力模型精确执行

     └─► 路径 C(兜底):意图模糊/置信度低 → 透传给主力模型走完整链路

关键设计细则:


Q: 主力大模型虽然不用选工具,但还是要生成工具参数——速度到底提升在哪里?上下文的”干净”又体现在哪里?

既然主力模型最终还是要输出工具参数(Tool Call JSON),它该写的 Token 一个也没少,那这个架构到底是怎么让首字变快的?

速度提升的三个来源

1. 大幅缩短 Prefill 耗时

大模型在输出第一个字之前,必须先把输入 Prompt 全部计算一遍。

2. 消除主力模型的”选择过程”

工具选项多时,大模型在输出 Tool Call 之前,往往先生成一段内部推理:“A 工具适合…B 工具适合…所以决定用 A…”。有了路由后,系统提示可以直接指定”当前调用 A 工具,请输出参数”,模型跳过选择过程,直接输出参数。

3. 多步调用的并行提前量

小模型虽然不能生成第一步的具体参数,但能预判任务属于某个工作流(如”退款流程”),系统可在主力模型还在生成第一步参数的同时,提前异步加载相关的配置和缓存。

“干净上下文”的含义

这里的”干净”指的是对模型注意力机制的噪音过滤。过长的上下文和过多的工具定义会分散模型注意力,降低输出质量。

维度无路由(单模型 Agent)有路由(多级路由 Agent)
工具定义塞满所有 API 描述,名字和参数还很相似按需加载,只有 1-2 个相关工具定义
背景信息包含不相关的历史闲聊和广撒网的检索碎片意图已知,精准召回,去除大部分无关文本
模型任务”既要判断意图、又要选工具、又要生成参数”任务单一:“已知用工具 A,请生成 A 的参数”

小结:快慢路由不是缩短大模型”写出参数”的时间,而是把主力大模型需要处理的输入体积大幅缩减(可达 80% 以上),并让它在一个聚焦的上下文中工作。这不仅降低 TTFT,也提升了工具参数生成的准确率。


Q: 多轮对话中,如果工具按需加载、每轮动态变化,岂不是直接破坏 Prompt Caching 的前缀匹配?这和缓存机制在底层逻辑上矛盾吗?

这正是做 Agent 全链路性能优化时容易踩到的一个坑。

Prompt Caching 的匹配是严格自顶向下的前缀匹配——只要输入文本从头开始逐字节比对完全一致,缓存就能命中。主流服务商的序列化顺序通常是:

KV Cache 匹配顺序 = Tools 定义 → System Prompt → Messages 历史对话

如果第二轮对话把 Tools 数组里的工具 A 删掉、换成工具 B,整个前缀的哈希值改变,之前所有 System Prompt 和 History 的 KV 缓存全部失效。大模型必须重新做一次完整的 Prefill,TTFT 反而升高。

为解决这个问题,生产环境中有三种方案:

方案一:全量工具注入 + tool_choice 约束(推荐公有云 API 场景)

在 API 的 tools 参数里永远放入完整的、不变的工具库定义,不做运行时增删,确保前缀永远命中缓存。通过 tool_choice 参数限定当前轮允许调用的工具——这个参数不参与前缀缓存计算。

第一轮:tools=全量20个工具(命中缓存)+ tool_choice 指定 query_account
第二轮:tools=全量20个工具(继续命中缓存)+ tool_choice 指定 send_email

OpenAI 在 Prompt Caching 201 中明确推荐这种模式,并提供了 allowed_tools 参数来限制工具子集而不破坏缓存。

方案二:工具作为数据的渐进式披露(适合工具池极大的场景)

主力模型只绑定 1-2 个”元工具”(如 load_skill),系统提示只包含一个轻量的工具清单(名称+一句话描述)。当模型需要某工具时,框架把详细说明书作为工具执行结果(ToolMessage)追加到对话历史尾部。

为什么不破坏缓存? 对话历史是不断向后追加的。新加载的工具定义成为”已发生的对话内容”的一部分。只要不修改已有的历史消息,缓存就会随对话延伸自动向前平移。

但需要注意:这主要是为了解决工具数量过多无法一次性装入的问题(规模化),而非直接优化速度——因为多了一次模型推理的往返。

方案三:Prompt 结构重排(仅限自建模型部署)

如果使用 vLLM 或 llama.cpp 本地托管模型,可以自主控制 Token 序列化顺序:

自建模型 KV Cache 顺序 = 固定 System Prompt → 历史对话 → 当前轮按需工具定义 → 用户提问

把动态变化的工具定义挪到序列最尾部,前面的 System Prompt + 历史对话始终可以命中缓存。

兼顾速度与规模的组合方案

编排层前置预测 + 尾部动态注入——不让主力模型自己去选工具,编排层在请求到达主力模型之前就通过小模型/向量检索选好工具定义,注入到 Prompt 尾部:

┌────────────────────────────────────────────────────────┐
│ [1. 固定 System Prompt]                                │ → 命中缓存
├────────────────────────────────────────────────────────┤
│ [2. 历史多轮对话]                                      │ → 命中缓存
├────────────────────────────────────────────────────────┤
│ [3. 当前轮动态工具定义(编排层选好后注入)]              │ → 未命中(仅几百 Token)
├────────────────────────────────────────────────────────┤
│ [4. 用户当前轮输入]                                    │ → 未命中
└────────────────────────────────────────────────────────┘

前面的大段内容全部命中缓存(Prefill 跳过),只有尾部新增的少量 Token 需要计算,主力模型一次直出。


Q: Anthropic 在其 Managed Agents 工程文章中提到 TTFT 大幅下降(p50 约降 60%,p95 降超 90%),原因是什么?

核心原因:他们把模型推理及编排逻辑和代码执行环境进行了物理解耦,消除了容器启动的等待时间。

旧架构的问题

在旧设计里,Agent 的控制主进程(负责循环调用模型、解析响应、决策下一步的代码)直接运行在 Sandbox 容器(隔离的代码执行环境)内部。流程是线性阻塞的:

用户请求 → 分配云主机 → 下载并启动 Docker 容器
→ 容器内 git clone 拉取代码 → 安装依赖 → 启动 agent 主进程
→ 主进程第一次向大模型发起请求 → 开始 Prefill → 首字输出

在主进程启动之前,大模型处于完全空闲状态。如果代码仓库大或依赖安装耗时长,用户的等待全部花在了与模型推理无关的 IO 操作上。

新架构的做法

Managed Agents 架构中,编排进程被抽离出来放到常驻的云端服务中,不再依赖 Sandbox 容器的生命周期。Sandbox 被降级为一个纯粹的远程代码执行器——只有模型明确需要执行代码时才按需调用。

用户请求 → 常驻编排服务立刻响应,读取会话日志
         → 立刻向大模型发起请求 → 模型开始 Prefill → 首字输出
         
         (与此并行:后台按需启动 Sandbox、克隆代码、初始化环境)
         
         → 当模型输出 Tool Call(需要执行代码)时,Sandbox 刚好就绪

大模型在输出初始的规划文字时,后台 Sandbox 正在利用这段时间窗口完成初始化。等模型真正需要执行代码时,环境恰好准备好了。

结果:旧架构 TTFT = 容器启动 + 代码克隆 + 依赖安装 + 模型响应时间。新架构 TTFT ≈ 纯粹的模型响应时间。

工程启示:保持大模型首字输出的链路尽量轻量。把数据检索、环境初始化、重型工具实例化全部做成惰性加载(只在真正需要时才触发)或异步并行处理。


Q: 使用公有云 API(如 DeepSeek)时,第一次请求还没有缓存可命中,怎么降低首轮延迟?缓存预热怎么做?

第一轮请求面对的悖论是:为了让后续请求能命中缓存,首次就得把完整的工具定义全部发送给模型去计算,而这恰恰导致首次请求最慢。

业内有三种互补的解决方式:

1. 定时预热请求(Cron Warm-up)

OpenAI 明确说明其 Prompt Cache 在组织(Organization)级别共享。对于 DeepSeek,虽然官方文档没有明确标注缓存的共享范围,但其机制是:同一个 API Key 发送相同前缀的请求,会命中服务端已有的 KV 缓存。

做法:部署一个定时任务,每 10-15 分钟发送一个最小化请求,内容只包含完整的 System Prompt + 全量工具定义,max_tokens=1(只让模型输出 1 个 token,控制成本),用户输入写一个占位符即可。

import openai

client = openai.OpenAI(
    api_key="your_deepseek_api_key",
    base_url="https://api.deepseek.com"
)

def cron_warm_up():
    messages = [
        {"role": "system", "content": "你是一个AI助手。"},
        {"role": "system", "content": "这里是100个静态工具的定义..."},
        {"role": "user", "content": "warmup"}
    ]
    response = client.chat.completions.create(
        model="deepseek-v4-pro",
        messages=messages,
        max_tokens=1,
        stream=False
    )

当真实用户到来时,服务端发现相同前缀已有缓存,直接复用。

2. 小模型即时响应填充等待窗口

小模型 TTFT 很低(50-80ms),可以用它在主力模型预热期间先给用户反馈:

  1. 用户发送请求
  2. 小模型 50ms 内完成意图判定
  3. 小模型立刻向前端建立 SSE 流连接,输出过渡状态:“正在为您检索财务数据…”
  4. 用户看到即时反馈,不会感到系统无响应
  5. 与此同时主力模型在后台完成 Prefill,完成后前端无缝切换为主力模型的输出流

用小模型的毫秒级响应,填补了主力模型 Prefill 所需的数百毫秒窗口。

3. 工具分组(Tool Bucketing)

将 100 个工具按业务领域分成 5 个固定分组(每组 20 个)。小模型判定领域后,只把”基础组 + 对应领域组”(约 40 个)丢给主力模型。首次 Prefill 压力降低约一半,且同领域内后续对话持续命中缓存。


Q: DeepSeek 的 Prompt Caching 具体怎么工作?怎么保证请求命中缓存?它支持 allowed_tools 吗?

DeepSeek 缓存机制

DeepSeek 称之为”上下文硬盘缓存”(Context Disk Caching),其 API 调用是无状态的——你不需要显式传递缓存 ID 或指针。它的工作方式是隐式前缀匹配:服务端自动对请求的输入文本从头开始计算哈希,如果与已有缓存匹配则跳过对应部分的计算。

关键特性(官方文档):

保证前缀一致性

// ❌ 错误做法:动态内容在前面,每次请求都变,后面的缓存无法命中
[
  {"role": "system", "content": "你是AI助手。当前时间:2026-05-22。"},
  {"role": "system", "content": "100个工具定义..."}
]

// ✅ 正确做法:静态内容固定在头部,动态内容放在末尾
[
  {"role": "system", "content": "你是AI助手。"},
  {"role": "system", "content": "100个工具定义..."},
  {"role": "user", "content": "当前时间:2026-05-22。帮我查账"}
]

对前缀的一致性要求是字节级的——一个空格、一个换行的差异都会导致匹配失败。

验证是否命中缓存

检查 API 响应中 usage 字段的两个值:

"usage": {
    "prompt_cache_hit_tokens": 8000,
    "prompt_cache_miss_tokens": 150
}

prompt_cache_hit_tokens 占比越高,TTFT 越低。

DeepSeek 不支持 allowed_tools

DeepSeek 的 tool_choice 参数目前只支持:

它没有像 OpenAI GPT-5 系列那样的 allowed_tools 参数来指定一个允许调用的工具子集。

替代方案


参考资料


Share this post on:

Previous Post
AI 流式输出处理:从 Buffer 原理到 Agent 工具调用隐藏
Next Post
Claude Code 512,000 行代码里的秘密