回路 / 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)是一种服务器向浏览器单向推送数据的技术,模型生成的每个字可以即时推送到前端,而不必等全部生成完再一次性返回。
关键落地要点:
- Prompt Caching:确保系统提示、工具定义和历史对话的前缀部分结构完全一致,让推理引擎直接复用已计算的 KV 缓存,跳过 Prefill 阶段。
- 动态上下文裁剪:不要把所有历史消息和 RAG(检索增强生成)召回的文本碎片全部塞入,应使用动态摘要或滑动窗口截断。
- 流式思维链输出:将模型的思考过程直接流式推送给前端(可折叠显示),让用户在 100-200ms 内就看到”模型已开始思考”。
- 全链路异步: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(兜底):意图模糊/置信度低 → 透传给主力模型走完整链路
关键设计细则:
- 小模型的系统提示必须极度精简——只给工具的名称和一句话描述,不塞详细参数定义,否则小模型自己的 Prefill 也会被拉长。
- 小模型采用结构化输出,强制返回 JSON 格式(含 intent、confidence、extracted_parameters)。
- 如果工具池很大(上百个),不要让小模型从全集中匹配——先用向量检索(Embedding,将文本转化为数学向量后计算语义相似度)找出最相关的 3-5 个工具,再交小模型做最终确认。
- 小模型输出 JSON 时可设置 stop_tokens(提前终止条件),一旦流式输出了工具名字段,代码层即刻截获,底层的数据检索就可以提前异步触发——无需等模型输出完整个 JSON。
Q: 主力大模型虽然不用选工具,但还是要生成工具参数——速度到底提升在哪里?上下文的”干净”又体现在哪里?
既然主力模型最终还是要输出工具参数(Tool Call JSON),它该写的 Token 一个也没少,那这个架构到底是怎么让首字变快的?
速度提升的三个来源
1. 大幅缩短 Prefill 耗时
大模型在输出第一个字之前,必须先把输入 Prompt 全部计算一遍。
- 没有路由时:50 个工具的完整参数定义全塞进 Prompt,可能高达 10k-20k Token。即便只输出一个字,TTFT 可达 800ms 甚至 1 秒。
- 有路由时:小模型 50ms 内判定只需 2 个工具,系统裁剪掉其余 48 个工具定义,输入缩减到 1k Token 左右,Prefill 降到 100-150ms。
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),可以用它在主力模型预热期间先给用户反馈:
- 用户发送请求
- 小模型 50ms 内完成意图判定
- 小模型立刻向前端建立 SSE 流连接,输出过渡状态:“正在为您检索财务数据…”
- 用户看到即时反馈,不会感到系统无响应
- 与此同时主力模型在后台完成 Prefill,完成后前端无缝切换为主力模型的输出流
用小模型的毫秒级响应,填补了主力模型 Prefill 所需的数百毫秒窗口。
3. 工具分组(Tool Bucketing)
将 100 个工具按业务领域分成 5 个固定分组(每组 20 个)。小模型判定领域后,只把”基础组 + 对应领域组”(约 40 个)丢给主力模型。首次 Prefill 压力降低约一半,且同领域内后续对话持续命中缓存。
Q: DeepSeek 的 Prompt Caching 具体怎么工作?怎么保证请求命中缓存?它支持 allowed_tools 吗?
DeepSeek 缓存机制
DeepSeek 称之为”上下文硬盘缓存”(Context Disk Caching),其 API 调用是无状态的——你不需要显式传递缓存 ID 或指针。它的工作方式是隐式前缀匹配:服务端自动对请求的输入文本从头开始计算哈希,如果与已有缓存匹配则跳过对应部分的计算。
关键特性(官方文档):
- 默认自动开启,无需代码修改
- 缓存存储在分布式硬盘上(非内存),成本低、容量大
- 匹配粒度为 64 Token 的块(前缀短于 64 Token 不会被缓存)
- 严格从前往后连续匹配——前缀中任何位置的变动都会导致该位置之后的缓存全部失效
- 缓存建立需要几秒,闲置数小时到数天后自动释放
保证前缀一致性
// ❌ 错误做法:动态内容在前面,每次请求都变,后面的缓存无法命中
[
{"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 参数目前只支持:
"none":不调用工具"auto":模型自行决定(默认)"required":强制至少调用一个工具- 指定单个工具:
{"type": "function", "function": {"name": "xxx"}}
它没有像 OpenAI GPT-5 系列那样的 allowed_tools 参数来指定一个允许调用的工具子集。
替代方案:
- 策略一(单工具锁定):如果小模型能预测出确切工具,传入全量工具定义(保证缓存命中)+
tool_choice指定该工具。主力模型无需选择,直接生成参数。 - 策略二(工具分组):将工具按领域分成若干固定分组,小模型判定领域后只传对应分组的静态定义。跨领域切换时会有一次缓存未命中,但同领域内多轮对话可持续命中。
参考资料
- DeepSeek Context Caching 官方文档 — 上下文硬盘缓存的完整说明
- OpenAI Prompt Caching 指南 — 组织级缓存共享机制与 1024 Token 触发规则
- OpenAI Prompt Caching 201 实战教程 — 多轮对话中缓存友好的 Prompt 设计模式,
allowed_tools用法 - Anthropic Prompt Caching 文档 — Tools → System → Messages 的缓存层级与 4 断点机制
- Anthropic Prompt Caching Cookbook — 多轮对话缓存滚动的 Python 代码示例
- Anthropic Managed Agents 工程文章 — 编排层与执行环境解耦,p50 TTFT 降约 60%,p95 降超 90%