中文内容
编程智能体正开始大规模编写生产代码。Stripe 的智能体每周生成 1,300 多个 PR。Ramp 将 30% 的已合并 PR 归因于智能体。Spotify 报告称每月有 650 多个由智能体生成的 PR。Claude Code 和 Codex 等工具在每个编程会话中会发起数百次 API 调用,每次调用都携带完整的对话历史。这些工作流背后都依赖一个承受显著 KV 缓存压力的推理栈。

以 Claude Code 为例。在第一次 API 调用将对话前缀写入 KV 缓存之后,后续每次发送到同一工作节点的调用都会达到 85–97% 的缓存命中率。智能体团队(或群组)进一步提升了这一水平,4 个 Opus 队友的总体缓存命中率达到 97.2%。11.7 倍的读写比意味着系统每写入一个 token,就会从缓存中读取近 12 次。这是一种一次写入、多次读取(WORM)的访问模式:系统提示和不断增长的对话前缀只计算一次,随后在每次调用中都从缓存提供服务。最大化所有工作节点之间的缓存复用率,并让 KV 块保持热态且可路由,是智能体式推理的核心优化目标。
这些数字来自托管式 API 基础设施,在其中提供方控制前缀匹配、缓存放置和驱逐。对于在自有 GPU 上运行开源模型的团队来说,这些能力并不会开箱即用。我们一直在构建 Dynamo,以弥合这一差距。本文将介绍我们如何在三个层面让 Dynamo 原生适配智能体:前端 API、路由器以及 KV 缓存管理。
在本文中,我们始终使用以下三个术语:
- Harness:驱动工作流的智能体框架(Claude Code、Codex、OpenClaw、OpenCode 等)
- Orchestrator:Dynamo 的路由、调度和缓存管理层
- 运行时:执行模型并拥有 KV 缓存管理器的推理引擎(SGLang、vLLM、TRT-LLM)
第 1 层:前端
多协议支持
智能体框架正越来越多地采用 v1/responses 和 v1/messages,而不是 v1/chat/completions,以便清晰地处理包括交错式思考和工具调用在内的新模式。这些 API 的关键区别在于结构。在 v1/chat/completions 中,消息内容是一个扁平字符串,工具调用则作为单独字段附加其上。举例来说,请注意 GLM 和 MiniMax API 在将其模型托管在 v1/chat/completions 端点后面时,如何以不同方式处理交错式思考。v1/responses 和 v1/messages API 使用类型化内容块,因此单个助手轮次可以将思考、工具调用和文本作为不同对象包含在内。这对推理很重要,因为编排器可以看到块边界、执行提示词优化,并按块类型应用不同的缓存和调度策略。Dynamo 通过共同的内部表示提供全部三个端点,因此单个 d
我们还投入资源,为各种开源模型提供首日可用的工具调用和推理解析支持。如果你发现某个模型尚不受支持,请提交 issue,或使用 tool-call-parser-generator 技能,结合你选择的测试框架来生成它。
智能体提示:Harness Orchestrator 接口
如今,推理服务器接收的是匿名的令牌化请求。但智能体编排框架拥有基础设施从未看到的全局上下文:哪些智能体因工具调用而阻塞、哪些刚刚生成、一个会话还剩多少轮,以及当前调用是快速查询还是长篇综合。使用编码智能体时,用户等待的是最终结果,而不是单个令牌流,因此编排器可以在不影响最终用户体验的情况下,对不同智能体的请求进行重新排序并确定优先级。会话会持续数分钟,甚至数天,并伴随长时间的工具调用暂停。这足以用传统服务无法实现的方式优化推理调度。
Dynamo 新的智能体提示扩展旨在弥合这一差距。它允许任何编排框架在全部三个 API 端点上为请求附加结构化提示,为路由器和运行时提供所需上下文,以做出具备智能体感知能力的调度和缓存决策。这是一个 v1 API,我们正在与社区积极共同设计,也非常希望从构建智能体编排框架的团队那里获得反馈,了解哪些信号最有用。如果你有任何想法或反馈,请联系我们。
{
"model": "MiniMaxAI/MiniMax-M2.5",
"messages": [...],
"tools": [...],
"nvext": {
"agent_hints": {
"osl": 256,
"speculative_prefill": true,
"priority": 10
},
"cache_control": {
"type": "ephemeral",
"ttl": "1h"
}
}
}
agent_hints 字段:
- priority 控制路由器和引擎两者的调度。值越高,表示在 Dynamo API 层面“更重要”;Dynamo 会将其转换为路由器队列排序和后端特定的引擎优先级。
- osl(output sequence length,输出序列长度)是测试框架对该请求将生成多少 token 的估计。路由器使用它来判断某个 worker 将被占用多长时间,从而改善负载均衡。测试框架可以通过按工具调用类型跟踪平均输出长度,随时间学习这一点。
- speculative_prefill 会通知编排器在完整请求准备好之前,先在一个可能的 worker 上开始缓存该请求的前缀。当测试框架知道某个工具调用即将返回,并希望提前预热缓存时,这会很有用。
cache_control 字段对使用过 Anthropic prompt caching API 的人来说会很熟悉。它告诉编排器在指定的 TTL 内将已计算的前缀固定在 worker 上,防止其在工具调用间隙期间被逐出。目前,ephemeral 是唯一受支持的类型(以匹配 Anthropic 的 API)。我们将在下面的缓存保留部分讨论其工作方式。你可以在此处找到有关 agent hints 的完整文档。
第 2 层:路由器
编码智能体遵循一种顺序模式:长预填充、工具调用、扩展前缀,然后重复。多智能体框架会将工作扇出到并行的子智能体上,这些子智能体具有短且相互独立的上下文。默认的轮询路由对这两种模式都是盲目的——它无法考虑缓存局部性、请求优先级或会话结构。Dynamo 的路由器通过三种机制弥合了这一差距:KV 感知放置、优先级调度和可扩展的路由策略。
KV 感知放置
如果没有缓存感知路由,对话第 2 轮落在与第 1 轮相同 worker 上的概率约为 1/N。每次未命中都意味着完整的前缀重新计算,这是一个显著的性能瓶颈,对最终用户而言成本极高。Dynamo 的路由器维护一个全局索引,记录哪些 KV 缓存块存在于哪些 worker 上。Flash Indexer 文章介绍了使该索引器达到 170M ops/s(行星级 KV 路由)的六次迭代。对于每个请求,路由器都会查询索引以获取每个 worker 的重叠得分,并选择能够最小化缓存未命中与当前解码负载综合成本的 worker。这个成本函数是可调的,我们将在下文展示团队如何在其基础上构建自定义的智能体感知路由策略。
优先级调度
优先级是面向用户的唯一调度旋钮。在 Dynamo API 层面,数值越高表示“越重要”。Dynamo 在两个层面都使用这一提示:
- 在路由器层,当启用 --router-queue-threshold 时,较高优先级的请求会在队列中被提前。
- 在引擎层,Dynamo 会规范化后端特定的极性,并转发请求,用于队列排序、抢占和 KV 缓存淘汰。
在路由器处,传入请求进入一个按有效到达时间排序的 BinaryHeap<QueueEntry>。更高的优先级会使请求看起来像是更早到达,从而将其排在较低优先级任务之前。只有当所有工作节点都超过可配置的负载阈值时,请求才会进入队列。低于该阈值时,请求会完全绕过队列,直接进入工作节点选择流程。当容量释放时(预填充完成或请求结束),队列会优先取出最高优先级的条目。
请求一经分发,SGLang、vLLM 和 TRT-LLM 可能会以不同方式解释引擎优先级,因此 Dynamo 会按后端对面向引擎的值进行归一化。SGLang 等引擎还可以使用基于优先级的 radix 缓存淘汰机制,在内存压力下优先淘汰较低优先级的块。
一个具有 200K 上下文窗口的研究代理需要具备足够空闲 KV 容量的工作节点来保存其完整状态。路由器的默认成本函数(重叠得分 + 解码负载)可以处理常见情况,但拥有特定领域工作负载的团队可以使用该路由器的 Python 绑定来实现自定义路由策略。核心 KvRouter 类提供 best_worker() 用于查询路由决策,get_potential_loads() 用于检查每个工作节点的负载,generate() 用于在一次调用中完成路由 + 分发。自定义路由器会与默认组件注册到同一服务网格,并且可以按请求覆盖路由配置:
# Query per-worker load and overlap for custom routing logic
loads = await router.get_potential_loads(token_ids)
# Override routing config based on request properties
# Long contexts benefit from heavier overlap weighting
config = {"overlap_score_weight": 2.0} if len(token_ids) > 8192 else {}
worker_id, dp_rank, overlap = await router.best_worker(
token_ids,
request_id="req-123",
update_indexer=True,
router_config_override=config
)
# Or bypass the default selector entirely when the harness
# has its own worker selection logic (e.g., session affinity)
stream = await router.generate(
token_ids, model=model, worker_id=chosen_worker
)
NeMo Agent Toolkit(NAT)团队使用这些 API 构建了一个自定义的在线学习代理式路由器。他们的路由器从 nvext 注解中提取会话元数据,并将其输入到一种 Thompson Sampling bandit 风格的成本函数中,该函数会学习在负载下哪些工作节点对哪些前缀模式表现最佳。与 Dynamo 的默认路由相比,他们测得 p50 TTFT 降低了 4 倍,p50 tokens-per-second 提高了 1.5 倍。对延迟敏感请求进行优先级标记,在中等内存压力下实现了最高 63% 的 p50 TTFT 降低。有关实现细节,请参见 NAT Dynamo 集成示例。我们很快会将其作为 Dynamo 中的一种路由策略提供。
第 3 层:KV 缓存管理
智能体型工作负载会产生复用价值差异巨大的块——系统提示词会在每一轮中复用,推理 token 则再也不会被复用——但默认的 LRU 淘汰策略会同等对待所有块。一次 2–30 秒的工具调用暂停可能会使智能体的整个前缀因老化而被淘汰,导致其恢复运行时必须完整重新计算。缓存需要理解块的价值,支持跨 worker 共享,并遵守智能体生命周期边界。
统一淘汰策略的问题
LRU 只关注最近使用情况。在高流量环境中,等待被调用工具完成(智能体等待外部 API 的 2–30 秒)可能会导致智能体的块因老化而被淘汰;当智能体恢复运行时,整个前缀必须重新计算。为了解决这个问题,我们需要向编排器提供 API,以控制哪些块应被保留、它们应存放在何处,以及应保留多长时间。
如今,KV cache 被视为每个 worker 上的本地、临时资源。一个 agent 约 32K token 的系统提示词和工具定义,会在为其请求提供服务的每个 worker 上独立计算。当一个主 agent 派生出 4 个 subagent,且它们具有重叠的工具定义时,如果这些 subagent 落在不同的 worker 上,这段共享前缀就会被重复计算 4 次。在我们对 Claude Code 团队会话的分析中,我们直接测量到了这一点:队友的平均缓存命中率为 79.4%,而主 agent 的探索型 subagent 为 91.3%(读/写比为 5.0x 对 11.7x),这一差距几乎完全由每位队友首次调用时的冷启动写入造成。目标是让高价值 KV cache 块可供集群中的所有 worker 使用。本质上,它们在冷启动期间只写入一次,之后可随时由任何 worker 读取。
SGLang 的 HiCache 和 Dynamo 的 KV Block Manager(KVBM)等解决方案,正在朝着 4 层内存层级结构发展:
块遵循写穿透路径:当某个 worker 为一个前缀计算 KV 时,这些块会自动从 GPU 流向 CPU,再流向磁盘。每个块都会在全局注册表中按序列哈希进行去重。一旦某个块完成注册,它就是不可变的,并且可由任何能够访问该存储层的 worker 寻址。
这直接解决了子代理的冷启动问题。当主代理计算工具定义和系统提示词时,这些块会写穿透到共享存储中。当子代理 1 在另一个 worker 上启动时,路由器会查询 Flash Indexer,在共享存储中找到这些块,然后该 worker 通过 NIXL(RDMA 读取)加载它们,而不是从头重新计算。子代理 2 也会执行相同操作。四次冗余的预填充计算变成了一次计算和三次加载。同一机制也解决了分离式预填充-解码服务中的缓存一致性问题。在分离式模式下,预填充 worker 会计算 KV,并通过 NIXL 将其传输给解码 worker。解码 worker 生成 token,产生新的 KV 状态。在下一轮中,某个预填充 worker 既需要原始前缀,也需要第 1 轮生成的 token,但这些只存在于解码 worker 上。借助共享存储,解码 worker 会写入
多层存储解决了共享和持久化问题,但块仍然只有在请求到达 worker 之后才会被送到 GPU。对于智能体系统而言,缺失的一环是预取:harness 可以利用历史时序数据来预测智能体的工具调用可能何时返回,这意味着它知道哪些块会在何时被需要。我们正在构建预取钩子,使 harness 能够发出信号:“在下一次请求之前,将这些块从存储带到 GPU。”结合保留 API(见下文),这使 harness 能够获得完整的生命周期控制:固定块以防止被驱逐,设置优先级以控制驱逐顺序,并在需要之前主动预取块。
选择性缓存保留
使块在全局范围内可用解决了共享问题,但并没有解决驱逐问题。SGLang 和 vLLM 都通过优先级堆支持基于优先级的驱逐,其中 harness 为每个请求分配一个数字优先级,较低优先级的块会先被驱逐。TensorRT-LLM 借助 TokenRangeRetentionConfig(由 Dynamo 团队成员 @jthomson04 设计并实现)进一步推进了这一点,它允许在单个请求内进行按区域控制。
一个请求携带零个或多个指令。没有指令的块沿用默认的 LRU 路径,且没有额外开销。逐出器变成一个双结构系统:用于无优先级块的 LRU 空闲列表(O(1),保持不变),以及用于带注释块的优先级队列。该测试框架可以表达“系统提示块最后被逐出(priority: 100);对话上下文在 30 秒的工具调用期间保留(duration: 45s);解码 token 最先被移除(priority: 1)”,而引擎无需理解其原因。
Anthropic 的提示缓存允许你在其基础设施上将前缀标记为可缓存。Dynamo 的 cache_control API 将相同的语义带到自托管推理中。当请求包含 cache_control: { type: "ephemeral", ttl: "1h" } 时,路由器会在该 TTL 期间将匹配的前缀节点固定在 worker 的基数树中,保护它们免于从 worker 的 L2 存储中被逐出。
下一步是将保留机制与分布式缓存连接起来。目前,保留指令适用于单个工作节点的本地缓存。当某个块固定在工作节点 A 上,但下一次请求被路由到工作节点 B 时,该固定状态不会随之转移。将保留语义扩展到 HiCache/KVBM 的共享存储层,意味着测试框架可以只固定一次某个块,并使其在不同工作节点之间持续存在:优先级和 TTL 元数据会通过直写路径随该块一起传递,任何从共享存储加载该块的工作节点都会继承该保留策略。结合上述预取钩子,这使测试框架能够在完整内存层级中实现端到端的生命周期控制。
Agent 生命周期感知
考虑一个典型的 Claude Code 会话。主代理运行 20 多轮,积累出不断增长的对话前缀。在此过程中,它会生成探索型子代理,每个子代理运行 1–3 轮后终止。它可能会生成一个由 4 名专家组成的团队,并行处理不同的子任务后终止。进行到中途时,该代理达到上下文限制,于是总结其历史记录,将约 175K 个 token 压缩到约 40K。每一个此类事件都会产生临时 KV:这些块将永远不会再被引用。子代理终止、上下文总结以及闭合的推理循环都会生成临时 KV,而这些 KV 占用的内存与系统提示等高价值块相同。推理模型会放大这一点:<think>...</think> 块约占生成 token 的 40%,但在推理循环闭合的那一刻就会变成临时内容。如果没有生命周期感知,缓存就会将所有这些块一视同仁

上述保留原语(优先级、TTL、token 范围)为我们提供了基础构件。缺少的是将它们与会话关联起来的能力。如果调度框架能够将某个子代理的请求标记为属于一个会话,并将该会话的 KV 标记为临时的,那么驱逐器就可以优先针对这些块,并完全跳过将它们写入共享存储。当子代理终止时,其会话的块将最先被回收。同样的机制也适用于思考 token:引擎可以在生成过程中检测 <think> 边界,并在插入时将这些块标记为临时的,这样它们就会跳过 L2 回写,并在普通块之前被驱逐,而无需任何外部信号。这里的设计空间很广:由调度框架驱动的会话标记、引擎原生的语义检测,以及结合两者的混合方法。我们正在积极探索多个方向,并预计正确答案将会 v
弥合差距
智能体推理中最大的优化空间,在于运行框架所知道的信息与基础设施所能看到的信息之间的差距。哪些智能体被阻塞,哪些即将恢复,哪些 KV 值得保留,哪些可以丢弃——所有这些上下文都存在于运行框架层,但从未跨过 API 边界。nvext.agent_hints 是我们弥合这一差距的首次尝试:一小组结构化信号,使编排器能够做出有依据的路由、调度和缓存管理决策,而不是把每个请求都当作匿名 token 来处理。这是一个 v1 API,我们正在积极推进其演进。如果你正在构建智能体运行框架、为智能体工作负载运行开源模型,或在思考具备缓存感知能力的推理,我们希望了解哪些信号对你的用例最重要。请通过 GitHub 联系我们,或在 X 上标记我们:@0xishand、@KranenKyle、@flowpow123。
标签















