元鉴
返回中文阅读流

NVIDIA Developer Blog

流式 Token 和工具:NVIDIA Dynamo 中的多轮代理框架支持

代理交互必须保持结构化:助手回合将推理与一个或多个工具调用交错,随后的用户回合返回...

中文内容

已翻译official company source英文原文2026-05-26

代理式交互必须保留结构化的交互形式:assistant 轮次将推理与一次或多次工具调用交织在一起,随后的 user 轮次则将相应的工具结果返回到模型上下文中。推理重放取决于模型和轮次:有些推理应当保留,而有些应当丢弃。

推理引擎负责支持这种更具表达力的交互模型,并生成正确分段的 API 结果。工具调用解析和推理解析需要在所附的测试框架消费响应之前发生。诸如编码等高价值代理式工作流还依赖于响应迅速的测试框架体验:推理片段、工具调用事件和请求元数据需要随着轮次展开而流式返回,而不是只在最终文本响应之后才到达。

本文介绍了在 NVIDIA Dynamo 上运行真实代理式客户端所获得的经验:我们如何强化解析器和 API 覆盖范围、改进流式传输行为,并将这些解析器层提取为独立的可复用 crate。

这些变化建立在我们第一篇文章所概述的性能考量之上,该文章聚焦于代理式推理底层的服务架构:前端、路由器和 KV 缓存管理。这篇后续文章聚焦于正确性、用户体验等价性和性能。

智能体式框架仍在快速演进。Claude Code、Codex 和 OpenClaw 通过不同的 API 接口暴露出许多相同的压力点,因此以下示例聚焦于自定义服务栈需要复现的核心行为。

Side-by-side sequence diagrams compare a standard server and Dynamo over two agent turns. On the standard server path, each turn re-sends and re-parses the full prompt, including tool-call markup. On the Dynamo path, a stable prefix is cachSide-by-side sequence diagrams compare a standard server and Dynamo over two agent turns. On the standard server path, each turn re-sends and re-parses the full prompt, including tool-call markup. On the Dynamo path, a stable prefix is cach
图 1. 标准推理服务器与 Dynamo 在两个智能体轮次中的比较,展示了稳定前缀和已解析工具调度如何减少重复的提示词重建与解析开销。

面向框架的 Dynamo 设置

我们的实验使用了新近发布的 nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4 模型,不过同样的问题也适用于不同的模型、推理解析器和工具调用解析器。

要复现我们的结果,请使用 Anthropic 兼容 API 以及可保留提示词、推理和工具状态的标志来配置前端:

  • --enable-anthropic-api 会向测试框架暴露 Anthropic Messages API。许多测试框架可以回退到默认的 Messages API,但体验会有所下降。
  • --strip-anthropic-preamble 会移除 Anthropic 计费标头,该标头可能会破坏 KV 复用的稳定性。
  • --enable-streaming-tool-dispatch 允许完整的工具调用在解码完成后立即开始执行,而不是等待本轮结束。

综合以上内容:

python -m dynamo.frontend \
  --http-port 8000 \
  --enable-anthropic-api \
  --strip-anthropic-preamble \
  --enable-streaming-tool-dispatch

在工作节点侧,此部署中的重要设置包括:

  • --dyn-tool-call-parser <parser> 和 --dyn-reasoning-parser <parser> 会以测试框架所期望的模型特定格式重建工具调用和推理块。这些解析器还控制是否应保留、转换或丢弃之前轮次的推理内容。

提示词稳定性是缓存复用的关键

Claude Code 会发送数千个 token 的可复用提示词脚手架,其中很大一部分旨在在不同用户和会话之间保持一致。然而,每个提示词都以特定于会话的计费头开头;当请求被路由到不会将其剥离的自定义端点时,这会导致缓存未命中:

x-anthropic-billing-header: cc_version=0.2.93; cch=abc123def456==;
You are Claude Code, an interactive CLI tool...

这些头部会污染 KV 缓存并阻止其被复用,即使是同一用户的不同会话之间也是如此。位于第零位置的一行内容发生变化,意味着每个新会话都会以不同的 token 前缀开始,因此其后的稳定指令和工具定义永远无法干净地对齐以供复用。

为了恢复 KV 缓存复用,Dynamo 添加了 --strip-anthropic-preamble。这个修复在机制上很小,但在运维上很重要:在 tokenization 之前移除不稳定的计费头,使稳定提示词从第零个 token 开始。

测得的影响很大。在使用 52K-token 提示词的 Dynamo NVIDIA B200 部署上,稳定前缀的 TTFT 为 168ms。将每个会话都会变化的头部保留在前缀中,会使其升至 912ms。在 tokenization 之前移除计费头后,TTFT 回到 169ms。在这个工作负载上,不稳定头部使每个请求增加 744ms,并将可复用的系统提示词变成冷预填充。对于访问同一部署的新用户,或同一用户开启新会话的情况,这相当于将 TTFT 降低约 5 倍。

Plot of TTFT (ms) for a 52K-token prompt on Nemotron-3-Super-120B-A12B-NVFP4 (single B200 GPU). Stable prefix: 168 ms (no per-session metering). Stripped prefix: 169 ms (cache hits restored). Varying prefix: 911 ms (cache miss from changingPlot of TTFT (ms) for a 52K-token prompt on Nemotron-3-Super-120B-A12B-NVFP4 (single B200 GPU). Stable prefix: 168 ms (no per-session metering). Stripped prefix: 169 ms (cache hits restored). Varying prefix: 911 ms (cache miss from changing
图 2. 基准测试显示,移除每个会话中变化的头部后,会恢复前缀缓存,并将首个 token 生成时间缩短约 5 倍。

推理与工具解析的细微差别

将推理重放到下一轮并不存在一种通用的正确形式。有些模型会有意在普通助手轮次中丢弃先前的推理。带有交错工具调用的智能体轮次则不同:推理片段通常需要继续附着在它们所解释的工具调用上。真正的约定取决于具体模型和具体轮次。

Anthropic 4 月 23 日的 Claude Code 事后分析提供了这一策略的一个具体生产示例:在会话恢复时,可以清除前几轮的思考内容,以在缓存提示过期后降低预填充负担。

当代推理模型往往会生成两种不同类型的助手轮次:

  • 先进行推理,然后直接回应用户
  • 先进行推理,然后进行一次或多次工具调用

智能体模型尤其擅长生成这样的轮次:在单个响应中,许多推理片段和工具调用片段会按以下模式出现:

<think>reasoning_0</think> tool_call_0 <think>reasoning_1</think> tool_call_1

在下一轮中,每个推理片段都需要与其所解释的工具调用保持关联。Dynamo 现在已完全支持这种交错格式。此前,同一轮可能会被重构为:

<think>reasoning_0 reasoning_1</think> tool_call_0 tool_call_1

如果将助手的一轮回复重构为一个通用推理块后跟一整块工具调用,模型仍然拥有所有相同的 token,但会丢失使这些内容具有意义的顺序和分隔符。这种分组排序源自旧版模型,它们每轮只会输出一个推理片段和一次工具调用过程。

除了重排序 bug 之外,我们还发现,在进入下一轮之前,推理内容常常被过于激进地丢弃。对于某些模型,在没有工具调用的轮次中丢弃先前的思考是一种既定行为,也是模型微调的一部分(DeepSeek-R1 是最明显的例子)。但对于交错式智能体轮次而言,同样的行为是错误的,因为先前的推理解释了工具序列。这个问题很难发现,因为用户可以看到推理在传出响应中被正确解码,而在进入下一轮之前,它仍可能被悄然畸变或丢弃。

我们在一个 Dynamo 和 TRT-LLM 部署上对此进行了验证:Nemotron-3-Super-120B-A12B-NVFP4 运行在 4XB200 上,TP=4,并启用了 --enable-anthropic-api、--strip-anthropic-preamble、--enable-streaming-tool-dispatch,使用 nemotron_deci 推理解析器和 qwen3_coder 工具调用解析器。

结合推理与工具调用

在调用工具之前进行推理的模型会生成这样的响应:<think> 内容先输出,随后是 <tool_call> XML。对于 Nemotron,需要由两个不同的解析器——用于推理的 nemotron_deci 和用于工具调用的 qwen3_coder——将该流拆分为正确的 Anthropic Messages API 内容块,同时互不干扰。

我们通过 Anthropic Messages API 将同一个提示发送了五次:一个指示模型逐步思考的系统提示、两个工具定义(calculator 和 weather),以及用户消息:“仔细思考 15 * 23 等于多少,然后使用 calculator 进行验证。”某个代表性轮次的响应结构如下:

{
  "content": [
    {
      "type": "thinking",
      "thinking": "I need to calculate 15 * 23. Let me think: 15 * 20 = 300, and 15 * 3 = 45, so 300 + 45 = 345. I'll use the calculator to verify.\n"
    },
    {
      "type": "tool_use",
      "id": "call-a3364797-3160-4e84-b567-5c495694d502",
      "name": "calculator",
      "input": { "expression": "15 * 23" }
    }
  ],
  "stop_reason": "tool_use",
  "usage": { "input_tokens": 403, "output_tokens": 95 }
}

同时流式传输两个解析器

流式路径让解析器之间的交互更加可见。一次流式请求会产生一系列 SSE 事件,而事件类型序列会准确显示两个解析器如何切分 token 流:

   1ms  message_start
  82ms  content_block_start  type=thinking
  82ms  content_block_delta  (thinking tokens stream here, ~7ms apart)
   ...  (~70 thinking deltas over ~520ms)
 602ms  content_block_stop
 602ms  content_block_start  type=text
 602ms  content_block_delta
 800ms  content_block_stop
 800ms  content_block_start  type=tool_use
 800ms  content_block_delta
 800ms  content_block_stop
 814ms  message_delta        stop_reason=tool_use
 814ms  message_stop

thinking 块从 82ms 到 602ms 逐个 token 流式输出。随后出现一个简短的文本块(原始 token 流中 thinking 区域与工具调用区域之间的空白)。然后 tool_use 块在 800ms 作为一个单一的结构化单元到达。message_stop 随后在 814ms 到达。

这次往返直到 PR #7358 才生成正确的 Anthropic 事件序列。修复包含三个部分:

  1. 推理解析的单一归属方:推理解析过去会发生在多个相互竞争的层级。后端解析器可以将模型输出拆分为 reasoning_content 和普通 content,而 Anthropic 流式转换器在将同一数据流映射为 Anthropic 内容块时,仍会尝试推断 <think> 边界。PR #7358 明确了归属。如果某个后端路径已经生成了结构化推理增量,Anthropic 转换器就会信任这些增量,并且只将它们映射到响应格式中。
  2. 可用时采用模板原生推理:Dynamo 现在会检查当前启用的聊天模板是否知道如何读取 reasoning_content。像 Nemotron 和 Qwen3 这样的模板会直接读取该字段,因此 Dynamo 会保持它不变,并让模板决定保留多少先前的思考内容。如果模板只理解 content,Dynamo 会回退到旧版表示方式:通过在 content 中插入 <think> 块来保留推理,或者在模型/解析器策略认为先前思考不应带入下一轮时将其省略。Rust 预处理器路径(ModelInput::Tokens)和 Python worker 路径(ModelInput::Text)都使用这一相同的条件规则。
  3. 遵循每次请求的思考控制:许多模板默认使用 truncate_history_thinking=true 以节省上下文。这对普通聊天是合理的,但在智能体工作流中会移除先前工具调用背后的推理。Dynamo 现在仅在实际涉及推理的请求中改变这一行为:当配置了推理解析器且客户端未禁用思考时,Anthropic 路径会设置 enable_thinking=true 和 truncate_history_thinking=false。这会保留智能体在下一轮上下文中所需的内容,同时不改变那些应在无思考模式下运行的请求或模型的默认设置。

在我们的 B200 实验中,系统提示词为 52K token,助手轮次包含约 500 个 token 的思考内容;未改变的下一轮前缀的 TTFT 为 167ms,而变更后的思考内容对应的 TTFT 为 322ms。这相当于增加了 1.9 倍,或者说由于改变下一轮前缀中的推理内容,每个请求约增加 155ms。

关键结论是,测试框架、解析器和模板路径必须就每个模型预期的推理行为保持一致。在普通轮次中丢弃思考内容,对某个模型可能是正确的,对另一个模型则可能是错误的。在工具调用轮次中保留交错的推理内容可能至关重要,即使普通轮次允许将其剥离。实践中,你不应假设第 N 轮生成的 token 会自动原封不动地作为第 N+1 轮的前缀传入。这是否成立取决于你所服务模型的推理解析器、工具解析器和聊天模板。

流式工具调用

流式 token 会让用户体验感觉更及时、更动态。挑战在于既保留这种行为,又仍然以连贯的块形式发出工具调用。在较早的 Dynamo 路径中,推理 token 会正常流式返回,但工具调用会一直被缓冲到该轮结束,然后才一次性释放给测试框架。这降低了响应性,并且即使模型已经决定要调用什么,也会延迟工具执行。

StateWhat the harness seesWhen tool readiness becomes visibleBufferedtool-call chunks withheldonly at finish_reason: "tool_calls"Inline streamingregular tool-call deltasas soon as the model emits themDispatchtyped event: tool_call_dispatch side channelat the same structural completion point, but already parsed

重要的转变发生在从第一行到后两行之间。正是在这里,harness 不再通过等待流结束来得知自己需要执行操作。在没有调度的情况下,harness 看到的是常规的 token 流,必须通过累积增量并等待足够的结构出现,来推断工具调用何时完成。启用调度后,Dynamo 可以改为发出一个带类型的 SSE 侧通道:

event: tool_call_dispatch
data: {"choice_index":0,"tool_call":{"index":0,"id":"call-...","type":"function","function":{"name":"calculator","arguments":"{\"expression\":\"42 * 17\"}"}}}

该事件会一次性告知 harness:工具调用已准备好执行。无需在 harness 端组装增量,无需猜测参数是否完整,也无需在 harness 内部维护自定义解析器。这使 Dynamo 更容易与自定义 harness 兼容。

Horizontal timing diagrams compare a standard server with Dynamo during a single streamed response. In the standard server flow, the tool call is dispatched only at the end of the stream. In the Dynamo flow, a tool_call_dispatch event occurHorizontal timing diagrams compare a standard server with Dynamo during a single streamed response. In the standard server flow, the tool call is dispatched only at the end of the stream. In the Dynamo flow, a tool_call_dispatch event occur
图 3. 时间线对比显示,Dynamo 在解析后会立即调度工具调用,而不是等待响应流结束。

Claude Code 和 OpenClaw 的 Anthropic API 保真度

Claude Code 和 OpenClaw 都使用 Anthropic Messages API,而不是仅调用某个端点背后的文本生成能力。要匹配测试框架的体验,取决于一系列较小的行为,而这些行为在临时测试中很容易被忽略:

  • 在 GET /v1/models 和 GET /v1/models/{model_id} 中都提供模型元数据
  • 正确处理带斜杠的模型 ID
  • 在 message_start 中提供有用的 input_tokens
  • 接受 cache_control

一旦前端可访问且符合要求,两个测试框架都可以指向 Dynamo 的 Anthropic 兼容端点:

ANTHROPIC_API_KEY=local-dev-token \
ANTHROPIC_BASE_URL=http://localhost:8000 \
ANTHROPIC_CUSTOM_MODEL_OPTION=nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4 \
ANTHROPIC_CUSTOM_MODEL_OPTION_NAME="Dynamo NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4" \
claude --model nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4


ANTHROPIC_API_KEY=local-dev-token \
ANTHROPIC_BASE_URL=http://localhost:8000 \
npx openclaw agent --local -m "Say ok" --json

这一领域的修复使自定义部署更接近原生后端行为。一个具体示例比一长串检查清单更能说明这类 bug 的特点。在启动期间,测试框架会直接请求所选模型的详细信息,但 Dynamo 当时尚未提供该端点:

GET /v1/models/nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4
HTTP/1.1 404 Not Found

另一个例子是 message_start 报告 input_tokens: 0,即使最终响应稍后包含真实计数。这会使测试框架中的 token 计数在每次新一轮开始时暂时降至 0。PR #7234 通过在流开始前填充 input_tokens,修复了 Anthropic 路径中的这一问题。这些计数也是长会话的控制平面数据:测试框架使用上下文长度来决定何时在下一次请求超过模型窗口之前压缩对话。更广泛的 tokenizer-service 工作则在 PR #7699 中单独落地,该 PR 添加了 /v1/tokenize 和 /v1/detokenize 端点,用于在请求由引擎处理之前获得准确的 token 计数。

Responses 和 Codex 保真度

同一问题面向 Codex 的版本位于 v1/responses 侧。通过合规性测试并不足以提供用户体验上的一致性。我们发现,一个 Responses API 请求在内部往返后无法保留使其成为 Responses 请求而非 chat completions 请求的字段。保留这些字段需要对 Dynamo 的 ResponseParams 路径进行架构更改,并结合 PR #6089 中的上游类型对齐工作。

Codex 应通过启用请求压缩的 OpenAI 兼容 Responses API 指向 Dynamo:

OPENAI_API_KEY=local-dev-token \
codex exec \
  -c 'openai_base_url="http://localhost:8000/v1"' \
  -c 'features.enable_request_compression=true' \
  -m nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4 \
  "Say ok"

Codex 模型元数据决定请求的形态

Codex 的一致性在第一次 POST /v1/responses 之前就已经开始。CLI 会将配置的模型字符串解析为本地模型目录记录,由此得到的 ModelInfo 会控制围绕该模型构建的运行框架状态:基础指令、历史记录格式化、工具注册表、推理参数、详细程度控制、图像支持、上下文计量、工具输出截断、parallel_tool_calls,以及最终的 Responses 载荷。

即使两个端点服务于同一个底层模型,如果 Codex 附加了不同的目录元数据,它们仍可能驱动不同的智能体行为。请求可能通过 schema 校验,但围绕它的运行框架已经发生变化。

工具输出截断是一个很有用的例子。Codex 不会把无限量的命令输出重放到下一轮模型交互中。Shell 和工具观察结果会根据所选模型的目录策略被截断,然后再重新进入上下文。在我们测试的目录快照中,gpt-5.5 使用的是:

{ "mode": "tokens", "limit": 10000 }

相比之下,自定义端点上的 openai/openai/gpt-5.5 使用的是回退元数据:

{ "mode": "bytes", "limit": 10000 }

这些预算并不等同。10,000 字节的限制会比面向 ASCII 密集型编码输出的 10,000 token 限制更早截断结构化日志、回溯信息、JSON 或测试输出。对于编码代理来说,这会改变模型在测试失败、搜索命令或编译器错误之后能够检查的内容。模型可能需要额外的工具调用来恢复本应由预期目录配置保留的上下文。

推理设置也来源于目录。当所选模型的元数据表明支持推理摘要时,Codex 会发送一个 Responses 推理对象。在该路径中,Codex 还会请求 reasoning.encrypted_content,以便推理状态能够跨轮次重放。回退元数据会移除这一路径。

提示词也会发生变化。在 Codex 中,从 fallback/default 配置切换到 gpt-5.5 目录配置会改变系统提示词。fallback 提示词围绕通用的 Codex 操作组织(# How you work、# AGENTS.md spec、# Tool Guidelines),并强调 AGENTS.md 的优先级、规划、验证以及 shell 搜索习惯。gpt-5.5 提示词则是一份不同的指令文档(# Personality、# General、# Working with the user),它将智能体定位为务实的软件工程师,并在代码库阅读、本地模式复用、限定范围的编辑、脏工作树、apply_patch、协作更新以及最终答案格式方面增加了更强的指导。因此,目录别名不仅会影响截断、推理等请求字段,也会影响基础行为策略。

我们在 SWE-Bench Verified 的一个 50 项任务子集中直接观察到了这一点。在这个设置中,两条路径都到达了由 OpenAI 提供服务的 GPT-5.5——差异在于端点以及 Codex 绑定到该端点的模型目录记录。当自定义端点使用模型 ID openai/openai/gpt-5.5,但未与 gpt-5.5 目录配置关联时,Codex 使用的是通用 fallback 行为。在一次运行中,fallback 配置发出的工具调用数量大约只有一半:

Catalog profileTotal tool callsPer taskgpt-5.5 profile2,08741.7Fallback profile1,04821.0Delta-1,039-20.8

配对比较在每项任务上都指向同一方向:gpt-5.5 配置文件在 50/50 项任务中使用了更多工具,而 fallback 配置文件在 0/50 项任务中使用了更多工具。置换检验显示差异低于 p < 0.001。

在添加模型目录别名,使 openai/openai/gpt-5.5 继承预期的 gpt-5.5 配置文件后,相同的 50 项任务设置变得接近得多:

Catalog profileTotal tool callsPer taskgpt-5.5 profile2,08141.6Alias-backed custom profile2,20544.1Delta+124+2.5

在本次运行中,剩余差异不具有统计显著性:置换检验约为 p = 0.22,配对方向呈混合状态(20/50 项任务偏向原生配置文件,28/50 项任务偏向由别名支持的配置文件,2/50 项任务持平)。

对于 Dynamo,这意味着需要在目录和请求塑形层以及 HTTP 模式层评估 Codex 兼容性。如果 Codex 无法将模型 ID 解析为预期的配置文件,那么在 Dynamo 收到请求之前,fallback 默认值可能会改变截断、搜索工具可用性、详细程度控制、推理摘要支持以及并行工具调用支持。

下一步是什么

Dynamo 现在有了 nvext.agent_hints:latency_sensitivity、priority、osl 和 speculative_prefill。这些字段为 harness 提供了一种方式,使其能够表达比单独的 prompt 更多的关于本轮交互的信息。一个正在等待用户回复的会话,与一个正在处理长时间后台工具序列的会话并不相同,而 API 现在可以承载其中的一部分差异。

在 v1.1.0 系列中,Dynamo 还将更多 agent stack 作为可复用组件提供。protocol、parser 和 tokenizer 层以独立 crate 的形式进行版本化,包括 dynamo-protocols、dynamo-parsers 和 dynamo-tokenizers。这使团队能够构建或定制面向 harness 的 serving path,而无需将 Dynamo 内部实现复制到单独项目中。这也是通向 AutoResearch 等更长时间运行系统的桥梁。第一篇文章解释了为什么 agentic workloads 会给 serving stack 带来压力。本文重点介绍正确运行这些 workload 所需的面向 harness 的 contract,并为由 Dynamo endpoints 支撑的高效长时间运行 agents 奠定基础。

Like

标签

原文标题

Streaming Tokens and Tools: Multi-Turn Agentic Harness Support in NVIDIA Dynamo