一次模型请求:流式、工具并发与恢复

runModelBackedTurnStep 的内部:请求怎样发出、输出预算怎么算,流式事件怎样被消费与投影,只读工具怎样边流边执行、账本记什么;输出截断后如何续写,断流与取消怎样恢复或落盘,错误如何分类、各层重试几次,用量与缓存命中怎样统计。

作者 David更新于 9 篇(共 47 篇)

回合循环每转一圈调用一次 runModelBackedTurnStepapps/zcode-cli/packages/core/src/runtime/methods/turn-model-step.ts:88),本书把它叫作“模型步”:一次模型请求,对应一条助手消息。它把上一篇准备好的消息发出去,边收流边落盘,把完整的工具调用交给执行器,并在截断、断流、超窗、取消时决定续跑、重发还是报错,最后返回 continueoutput_continuationbreak 交回循环(turn-model-step.ts:86)。

代码在 apps/zcode-cli/packages/core/src/runtimemethodshelpers 两处。适配层(Vercel AI SDK 之上的 provider 实现与它自己的重试)只点到交界,内部见模型适配层。下表路径相对 runtime

文件职责
methods/turn-model-step.ts模型步主体:落盘、请求、结果分类、交给工具执行
methods/model.tsrunModelTextRequest:组装调用上下文、消费流式事件
methods/model-streaming-event*.tsreasoning-stream.ts流式事件的有序写队列;思考块归并
methods/streaming-tool-coordinator.tsstreaming-tool-synthetic-result.tshelpers/streaming-tool-ledger.ts边流执行、合成结果、工具账本与恢复锚点
methods/turn-output-token-continuation.ts输出截断后的续写
methods/streaming-recovery.tscancelled-stream-persistence.ts断流恢复;取消时落盘已到达的输出
helpers/model-errors.tsturn-errors.tsprovider-business-error.tsmodel-tool-call-validation.ts错误识别、分类与归一
methods/turn-model-step-usage.tsturn-nested-model-usage.tsusage-observability.ts用量、缓存命中与用量表
methods/model-token-limits.tsruntime-model.tsturn-model.ts输出预算;模型句柄上的调用上下文

一次模型步的时序

图表加载中…

发请求之前

模型步先落一条空的助手消息和一个 step-start part,再发 ModelRequest 事件(turn-model-step.ts:175turn-model-step.ts:193)。事件里的消息是去掉了续写提示的“可记录”版本,自动续写提示只属于本次请求,不进持久轨迹(turn-model-step.ts:196)。

输出预算分两步算。基线取模型声明的输出上限,模型没声明时用 32000(apps/zcode-cli/packages/core/src/runtime/methods/model-token-limits.ts:5model-token-limits.ts:7);再按剩余窗口封顶,取基线与“上下文窗口减去当前输入估算再减 1000”中较小的一个,窗口未知或算出来不是正数就保留基线(model-token-limits.ts:34)。当前输入的估算以最近一条已提交助手消息的 provider 用量为基线,加上其后消息的本地估算,找不到基线才全部本地估算(apps/zcode-cli/packages/core/src/runtime/methods/compact.ts:313compact.ts:342)。运行时配置 modelContextBudgetStrategylegacy 取值只为入参兼容保留,所有请求都走这套封顶(model-token-limits.ts:23)。

调用上下文分三层。createRuntimeModel 在建句柄时绑定重试预算与准入端口,这样工具内部的模型调用、压缩、标题生成也都受同一道闸门约束(apps/zcode-cli/packages/core/src/runtime/methods/runtime-model.ts:32);工作流子会话拿无上限的重试预算,其余沿用适配层默认(apps/zcode-cli/packages/core/src/runtime/methods/model-request-session-type.ts:21)。createTurnModel 再挂一个“每次尝试前刷新运行时请求头”的回调(apps/zcode-cli/packages/core/src/runtime/methods/turn-model.ts:27)。runModelTextRequest 最后补上本次调用的语义:operationagent_step,断流恢复发起的请求标 callCause: "recovery",状态汇把网络状态转成 ModelNetworkStatus 事件,并把恢复序号传下去以放宽空闲超时(apps/zcode-cli/packages/core/src/runtime/methods/model.ts:80)。

发出前,消息里的图片、PDF 等附件还会按模型支持的输入格式与媒体预算改写(model.ts:53model.ts:62)。运行时配置 modelStreaming 不是 on 时走非流式的 generateTextmodel.ts:141),CLI 的 -p 与 TUI 都显式设为 onapps/zcode-cli/packages/cli/src/prompt-command.ts:233apps/zcode-cli/packages/cli/src/tui-prompt-handler.ts:163),下文只讲流式路径。

流式事件:消费与投影

适配层产出的事件类型定义在 apps/zcode-cli/packages/contracts/src/model/index.ts:715runModelTextRequest 用一个 for await 逐个消费(model.ts:233),一边累加结果,一边把大部分事件投影成 ModelStreaming 会话事件(载荷见 apps/zcode-cli/packages/contracts/src/events/session.events.ts:707):

适配层事件怎样消费出处
starttext_starttext_end原样投影model.ts:235
text_delta累加正文,通知协调器计字节,刷新快照model.ts:255
reasoning_startreasoning_deltareasoning_end按 id 归并思考块;有的 provider 不发 start,没有 id 的增量落到默认块model.ts:278apps/zcode-cli/packages/core/src/runtime/methods/reasoning-stream.ts:3
tool_input_starttool_input_deltatool_input_end参数增量按调用缓冲,遇到真实或 JSON 转义的换行、攒满 4096 个字符或参数结束时刷出model.ts:28model.ts:211
tool_call归一工具名、按 id 去重,排空写队列后交给协调器model.ts:378model.ts:390
finish记下 finishReasonusageproviderMetadatamodel.ts:423
error排空写队列后抛出归一化的错误,保留业务错误码model.ts:437
compact_stream_boundary不处理,只供压缩回放model/index.ts:721

参数增量遇换行就刷,是为了让 Write、Edit 的行数尽快出现在界面上(model.ts:221)。写事件则经过一条有序写队列,而不是每个 token 同步落库(apps/zcode-cli/packages/core/src/runtime/methods/model-streaming-event-queue.ts:39):

    enqueue(payload: ModelStreamingPayload): void {
      assertNoWriteFailure();
      pendingWrites += 1;
      // 逐个 token 同步 append 会让 provider SSE reader 停在
      // iterator.next() 之外,已经到达的帧要等落库/通知完成后才被消费。
      // 这里把 append 串成有序写队列,读取侧继续 drain provider 队列;
      // finish / error / tool_call 边界再显式 drain,保持原有顺序语义。
      tail = tail
        .then(async () => {
          if (writeFailure) {
            return;
          }
          await params.runtime.emitModelStreamingEvent(payload, params.traceContext, params.events);
        })
        .catch((error: unknown) => {
          writeFailure ??= error;
        })
        .finally(() => {
          pendingWrites -= 1;
        });
    },

积压的写达到 128 条时,读取侧先等队列排空,形成背压(model-streaming-event-queue.ts:4model-streaming-event-queue.ts:61);任何一次写失败都会在下一次入队时抛出。

流结束后还有两道检查。结束原因表明上下文超窗时只记日志、照常返回:这里提前抛错会被通用的断流恢复接管,绕过回合层的被动压缩(model.ts:463)。没有文本、没有工具调用、结束原因既不是 stop 也不是 tool-calls、用量又是 0 的“可疑空结果”则直接抛错(model.ts:481),分类见后文。

边流边执行:流式工具协调器

每个定稿的 tool_call 都会交给协调器的 acceptturn-model-step.ts:258apps/zcode-cli/packages/core/src/runtime/methods/streaming-tool-coordinator.ts:73)。provider 自己执行的调用直接跳过,其余先登记,再看能不能不等流结束就开跑(streaming-tool-coordinator.ts:339):

function shouldExecuteToolDuringStream(
  runtime: AgentRuntimeInternal,
  toolCall: ModelToolCall,
): boolean {
  if (toolCall.providerExecuted) return false;
  if (toolCall.name.trim().length === 0) return false;
  if (runtime.config.modelStreaming !== "on") return false;
  if ((runtime.config.streamingToolExecution ?? STREAMING_TOOL_EXECUTION_MODE) === "off") {
    return false;
  }
  const entry = runtime.registry.get(toolCall.name);
  if (!entry) return false;
  const metadata = entry.metadata;
  const sideEffectScope = entry.permission?.sideEffectScope ?? metadata.sideEffectScope;
  const requiresUserInteraction = entry.requiresUserInteraction ?? metadata.requiresUserInteraction;
  return (
    metadata.readOnly &&
    metadata.concurrentSafe &&
    !metadata.destructive &&
    !metadata.needsApproval &&
    !requiresUserInteraction &&
    sideEffectScope === "none"
  );
}

streamingToolExecution 只有 offreadOnly 两个值,缺省 readOnlystreaming-tool-coordinator.ts:39apps/zcode-cli/packages/core/src/runtime/types.ts:128)。按工具声明,ReadGrepGlob 这类满足条件(apps/zcode-cli/packages/core/src/tool/handlers/read.ts:470);WebFetchWebSearch 虽只读但副作用范围是 networkapps/zcode-cli/packages/core/src/tool/handlers/webfetch.ts:206apps/zcode-cli/packages/core/src/tool/handlers/websearch.ts:144),Bash 注册时 readOnlyfalseapps/zcode-cli/packages/core/src/tool/handlers/bash.ts:451),都要等流结束。工具声明的各个字段见工具契约、注册表与可见性

并发规则因此很简单:

  • 合格的调用一到就单独成批。 executeDuringStream 先落 pending part,声明序号取它在已登记调用里的位置,再 scheduleTools([toolCall]) 生成只含一项的计划交给执行器(streaming-tool-coordinator.ts:85streaming-tool-coordinator.ts:271)。它与仍在进行的流并行,多个合格调用之间是否重叠只取决于到达时机,不经过同一个批次。
  • 其余调用等流结束再成批。 它们在模型步末尾由 executeToolCallsForModelStep 一次交给执行器,由调度器按只读、并发安全等声明分组(见执行器:调度、审批、超时与结果)。
  • 合并按声明顺序。 drain 按声明顺序等待所有边流句柄(streaming-tool-coordinator.ts:128),结果和其余调用的结果一起按声明顺序回填历史,模型看到的顺序与它写出的一致。
  • 失败就退回。 边流执行本身出错时记一条日志,这个调用退回流结束后的常规执行(streaming-tool-coordinator.ts:89)。模型请求失败或被取消时,abandon 中止所有边流句柄、写账本,最多再等 250 毫秒(streaming-tool-coordinator.ts:38streaming-tool-coordinator.ts:103)。

账本与锚点

工具从定稿到结果提交的每一步都写一条 StreamingToolLedgerUpdated 事件(apps/zcode-cli/packages/core/src/runtime/helpers/streaming-tool-ledger.ts:61),载荷带工具的只读、破坏性、并发安全与副作用范围,以及 executionTimingduring_streamend_of_stream)。状态取值定义在 apps/zcode-cli/packages/contracts/src/events/stream-recovery.events.ts:6

状态含义
tool_call_closed调用已定稿,已落 pending part
tool_queued已排进执行计划
tool_started所在批次开始执行,part 改为 running
tool_result_committed结果已写回历史,带提交时间、结果 part 与恢复锚点 ID
tool_cancelledtool_abandoned取消,或模型请求失败后放弃
tool_input_streamingrecovery_blocked契约里有,但非测试代码没有任何地方产出;StreamRecoveryBlocked 事件同样没有产出方

每提交一个结果,还会写一个恢复锚点 StreamRecoveryAnchorCreated,ID 形如 ${assistantMessageId}:${toolCallId}:tool-resultstreaming-tool-ledger.ts:54streaming-tool-ledger.ts:92)。锚点标记“到这里为止的历史可以安全重放”,断流恢复就从最近的锚点接着请求。账本的 attemptId 不论时机都是 ${assistantMessageId}:end-of-streamstreaming-tool-ledger.ts:50)。

输出被截断:自动续写

分类在 apps/zcode-cli/packages/core/src/runtime/methods/turn-output-token-continuation.ts:12

const OUTPUT_TOKEN_CONTINUE_PROMPT =
  "Output token limit hit. Resume directly — no apology, no recap of what you were doing. Pick up mid-thought if that is where the cut happened. Break remaining work into smaller pieces.";

export const OUTPUT_TOKEN_LIMIT_ERROR_MESSAGE =
  "The model's response exceeded the output token maximum.";

const MAX_OUTPUT_TOKEN_CONTINUATIONS = 3;
const OUTPUT_LIMIT_RAW_REASONS = new Set([
  "max_tokens",
  "max_output_tokens",
  "model_context_window_exceeded",
]);

type OutputTokenContinuationDecision = "continue" | "exhausted" | "none";

export function classifyOutputTokenContinuation(input: {
  finishReason: string | undefined;
  rawFinishReason: string | undefined;
  toolCallCount: number;
  continuationCount: number;
}): OutputTokenContinuationDecision {
  if (input.toolCallCount > 0) return "none";
  if (!isOutputTokenLimitFinishReason(input.finishReason, input.rawFinishReason)) {
    return "none";
  }
  return input.continuationCount < MAX_OUTPUT_TOKEN_CONTINUATIONS ? "continue" : "exhausted";
}

有工具调用时不续写。结束原因为 length,或 provider 原始原因落在上面三个值里,就算截断;成功响应带 model_context_window_exceeded 也归入续写,注释说明这是有意为之:输入没超窗、只是输入加输出填满了窗口,不等于请求失败,不该抢先触发被动压缩(turn-output-token-continuation.ts:44)。续写的过程:

  1. 结束原因统一改记为 lengthturn-model-step.ts:550),这段半截回答作为一条独立的助手消息提交、落盘(turn-model-step.ts:651)。
  2. 把续写提示作为 user 条目追加进回合内请求历史,它带 queryScope 标记,不进规范历史、不落库(turn-output-token-continuation.ts:139);返回 output_continuationturn-model-step.ts:664)。
  3. 下一轮循环照常做 microcompact 与自动压缩,但跳过吸收运行时命令和几类提醒(apps/zcode-cli/packages/core/src/runtime/methods/turn-loop.ts:49),模型接着上文往下写。
  4. 次数在一次正常结束、或断流恢复提交了工具结果时清零(turn-model-step.ts:707turn-model-step.ts:284)。第 4 次仍被截断,就另落一条“错误载体”助手消息,与半截输出分成两条,压缩时才能重放半截输出、排除这条只供展示的错误(apps/zcode-cli/packages/core/src/runtime/methods/turn-stop.ts:120),然后抛出可恢复的 ModelErrorproviderCodemodel_output_limit_exceededturn-model-step.ts:667)。

所谓“拼接”并不发生在字符串层面:每一段都是一条单独的助手消息,按顺序排在历史里。代价是 state.modelResponse 每步都被覆盖(turn-model-step.ts:444),TurnComplete.response 只含最后一段;从代码看,-p 文本模式打印的正是它(prompt-command.ts:330prompt-command.ts:416),续写过的回答只会打出最后一段,见命令行入口、无头模式与打包

断流、繁忙与取消

重试分两层。适配层先把 starttool_input_*、空增量这类“前奏”事件暂存,只要还没吐出真正的正文、思考或完整工具调用,失败就能透明重试(apps/zcode-cli/packages/adapters/src/model/stream-retry-boundary.ts:6);默认重试 10 次,间隔 2 秒起、每次翻倍、封顶 60 秒并加抖动,可用 ZCODE_MODEL_RETRY_MAX_RETRIES 等环境变量调整(apps/zcode-cli/packages/adapters/src/model/retry-policy.ts:13retry-policy.ts:18)。越过这条边界之后,就轮到 core 的断流恢复,每个回合最多 10 次(apps/zcode-cli/packages/core/src/runtime/methods/streaming-recovery.ts:14)。模型步的 catch 按下图决策(turn-model-step.ts:264):

图表加载中…

已有工具调用定稿时不看错误类型,直接进入工具恢复(streaming-tool-coordinator.ts:169):

      abortController.abort();
      const recoveryAttempt = beginStreamRecoveryAttempt(state);
      const toolCalls = Array.from(acceptedToolCalls.values());
      const settledResults = await collectCompletedResults(handles, toolCalls);
      const settledResultById = new Map(
        settledResults.map((result) => [result.toolCallId, result]),
      );
      const streamedToolResults = toolCalls.map(
        (toolCall) =>
          settledResultById.get(toolCall.id as ToolCallId) ??
          createSyntheticStreamedToolResult(
            toolCall,
            handles.has(toolCall.id) ? "unknown_execution_state" : "not_executed",
          ),
      );
      await emitStreamRecoveryStarted(runtime, state, recoveryEventOptions, error, recoveryAttempt);
      state.modelResponse = "";
      state.modelStepCount += 1;
      recordModelHistoryRound(state);
      state.toolCallCount += streamedToolResults.length;
      // 合并修复:恢复请求依赖 assistant tool-call 与随后 tool result 成对出现。
      // 因此必须同步推进本轮 request history,不能只更新 canonical history。
      commitTurnRequestEntries(runtime, state.turnRequestState, [
        createRuntimeAssistantEntry("", toolCalls, undefined, options.model),
      ]);
      state.turnMachine = new TurnMachineImpl(state.turnMachine.receiveModelResponse(""));

边流执行已完成的结果(每个最多再等 250 毫秒)原样保留;其余调用得到合成的失败结果(apps/zcode-cli/packages/core/src/runtime/methods/streaming-tool-synthetic-result.ts:15):从没开跑的,告诉模型“按失败处理,不要盲目重试”;已开跑但没交结果的,告诉模型“副作用未知,重试前先检查现状”。流里已吐出的正文被丢弃,只保留工具调用;这些结果随后按常规路径回填历史,下一次请求带着恢复状态发出。

没有工具调用、但已吐出正文或思考时,只有错误属于瞬态(错误自带 retryable,或错误码、原因、消息表明是超时、限流、服务端错误、网络错误、流空闲超时)才恢复(streaming-recovery.ts:223streaming-recovery.ts:297)。思考增量同样算输出,因为它为了实时展示也越过了适配层的重试边界(streaming-recovery.ts:230)。半截输出所在的助手消息被标成 StreamRecoveryDiscardedstreaming-recovery.ts:242),同一请求从上一条消息的锚点重发。两种恢复都先发 StreamRecoveryStarted,再依次发出选定锚点、丢弃尾部、开始重试三个事件,并把来源请求 ID 挂到下一次 model_request_started 上(streaming-recovery.ts:140streaming-recovery.ts:198)。适配层还据恢复序号放宽流空闲超时:基准是 modelStream.idleTimeoutMs,默认 600000 毫秒(apps/zcode-cli/packages/contracts/src/config/index.ts:284),每多恢复一次加 30000 毫秒(apps/zcode-cli/packages/adapters/src/model/stream-idle-timeout.ts:23)。

Start Plan 繁忙是给 Start Plan 账号开的特例:provider 为 account:bigmodel-start-planaccount:zai-start-plan、业务码为 3008、3009、3010 之一、且不是会话的第一个回合时,首 token 前被并发限制拒绝可以再试(streaming-recovery.ts:16streaming-recovery.ts:103)。等待时长按与断流恢复共用的计数取:计数为 0、1 时分别等 1000、2000 毫秒,到 2 以后不再等;此后再遇到这类繁忙错误,只要本回合恢复或重试过,就改报为“自动重试已达上限”(turn-model-step.ts:356streaming-recovery.ts:66),界面横幅显示“当前系统繁忙,当前自动重试已达到最大次数”(packages/ui/src/i18n/locales/zh-CN.ts:5210)。界面靠比对错误消息原文识别这种情况(packages/ui/src/lib/providerBusinessError.ts:227),而 apps/zcode-cli/AGENTS.md:92 要求不依赖错误文本做流程判断。套餐本身见账号、Coding Plan 与闲时计划

用户取消时,成功路径上的落盘不会执行,persistCancelledStreamSnapshot 把已到达本进程的思考与正文补写成 part(apps/zcode-cli/packages/core/src/runtime/methods/cancelled-stream-persistence.ts:7),同时把这段半截输出提交进内存历史,保证当前进程与冷恢复看到的历史一致(turn-model-step.ts:378);助手消息的错误里记 turnResult: "cancelled",冷恢复据此区分用户停止与真实失败(turn-model-step.ts:420)。回合最终发的是 resultTypecancelledTurnComplete

界面上,适配层重试与 core 恢复都投影成协议快照的 control.apiRetryapps/zcode-cli/packages/bootstrap/src/zcode-protocol-v4/product-projection.ts:2378),桌面端输入栏显示“重新连接中... n/N”(zh-CN.ts:4143);丢弃尾部的事件会把半截的输出行标为中断,恢复出来的流另起新行(product-projection.ts:2391)。

错误分类

回合层的归类入口是 createTurnFailureErrorapps/zcode-cli/packages/core/src/runtime/helpers/turn-errors.ts:88)。识别都顺着 cause 链最多查 7 层,看类型、错误码与原因字段,部分判断也匹配消息文本。表中“可恢复、可重试”对应错误上的 recoverableretryable,未显式设置时两者都是 falseapps/zcode-cli/packages/contracts/src/errors/index.ts:88):

情形归为可恢复、可重试出处
中止信号已触发,或 AbortErrorMODEL_REQUEST_CANCELLEDTurnCancelled是、否turn-errors.ts:218
类型、码或原因命中 context_length_exceededprompt_too_long 等标记,或消息含“context 超出”一类字样;结束原因带超窗标记ModelContextExceeded是、是apps/zcode-cli/packages/core/src/runtime/helpers/model-errors.ts:202model-errors.ts:304model-errors.ts:26
快速回填熔断ModelContextExceeded,原因 compact_rapid_refill_breaker是、是model-errors.ts:44
provider 业务错误:successfalse 或业务码非零ModelError,带 providerCode是、否apps/zcode-cli/packages/core/src/runtime/helpers/provider-business-error.ts:40provider-business-error.ts:61
可疑空结果;若 providerMetadata 里藏着业务错误则改报业务错误ModelError,原因 empty_model_response是、是model-errors.ts:67model-errors.ts:126
工具名为空且无法闭合ModelError是、否apps/zcode-cli/packages/core/src/runtime/helpers/model-tool-call-validation.ts:82
续写耗尽ModelErrormodel_output_limit_exceeded是、否turn-model-step.ts:667
宿主以专用标记中止回合UnknownError,事件里用宿主给的码,发 TurnError 而不按取消处理是、否turn-errors.ts:33turn-errors.ts:93
其他UnknownError 包装,消息为 Turn execution failed否、否turn-errors.ts:120

几条补充:

  • 除了上一节的断流与繁忙重试,超窗是模型步里唯一就地恢复的错误:每个模型步最多被动压缩一次,成功后重建状态机重跑(turn-model-step.ts:749turn-model-step.ts:793);这个标记在一个工具批次完成或进入续写时复位(apps/zcode-cli/packages/core/src/runtime/methods/turn-loop-state.ts:182turn-model-step.ts:662)。压缩本身见上下文压缩
  • 空工具名不一定报错:只要调用带着合法 ID、又不是 provider 执行的,就原样保留空名,让执行器走“注册表查无此工具”的路径给模型一个可恢复的结果(model-tool-call-validation.ts:68)。
  • 异常检测方面,ModelAnomalyWarning 的类别有四个(session.events.ts:721),实际产出的只有上一篇讲的 tool_call_budgetrepeated_tool_callprovider_finish_mismatchmalformed_tool_call 没有产出方。provider 在结果里报告的模型身份也不采信,助手消息一律归到本轮绑定的模型(turn-output-token-continuation.ts:118)。

用量与缓存命中

模型步成功后发一条 ModelCompleteturn-model-step.ts:595,载荷见 session.events.ts:761):正文、结束原因、用量与工具调用数之外,主会话还带上下文窗口大小,免得长程任务里输入栏的上下文计量条拿不到分母而隐藏(turn-model-step.ts:599);主会话与子 Agent 的步骤在没有工具调用时附带本回合的文件变更摘要(turn-model-step.ts:590);主会话还带缓存命中统计(apps/zcode-cli/packages/core/src/runtime/methods/turn-model-step-usage.ts:126):

  // AI SDK v6 已把 Anthropic cache read/write 并入 inputTokens;
  // 缓存命中率的分母应使用 total input,不能再把 cache 字段重复加到分母或上下文用量里。
  const inputTokens = modelUsageInputWindowTokens(usage) ?? 0;
  const cacheReadTokens = nonNegativeInteger(usage.cacheReadTokens) ?? 0;
  const cacheWriteTokens = nonNegativeInteger(usage.cacheWriteTokens) ?? 0;
  // ...
  const aggregate = runtime.mainTurnCacheHitAggregate;
  return {
    inputTokens,
    cacheReadTokens,
    cacheWriteTokens,
    latestHitRate: inputTokens > 0 ? cacheReadTokens / inputTokens : null,
    hitRate:
      aggregate.totalInputTokens > 0
        ? aggregate.totalCacheReadTokens / aggregate.totalInputTokens
        : null,

latestHitRate 是本次请求的命中率,hitRate 是会话累计值。累计数挂在运行时上,恢复会话或回退时从落库的助手消息 tokens 重算,压缩摘要不计入(turn-model-step-usage.ts:66apps/zcode-cli/packages/core/src/runtime/methods/resume.ts:220)。另有一份更粗的缓存状态:回合开始时置为未命中,某步读到缓存就置为命中,随 TurnCompletecacheStats 发出(apps/zcode-cli/packages/core/src/runtime/methods/turn.ts:556turn-model-step.ts:448)。

回合用量等于本回合所有 ModelComplete 的用量之和(session.events.ts:1155)。工具内部也会调模型,结果里带 modelUsage 的,额外补发一条结束原因为 tool_internalModelComplete,一并计入(apps/zcode-cli/packages/core/src/runtime/methods/turn-nested-model-usage.ts:18)。落库方面,每次模型请求、每个回合、每个工具调用各写一行,分别进 model_usageturn_usagetool_usage 三张表(apps/zcode-cli/packages/adapters/src/storage/session-store/repositories/usage.ts:32usage.ts:169usage.ts:262,表结构见SQLite 会话库)。模型请求那一行的首 token 时间取第一个非空的正文或思考增量,重试次数数的是 model_retry_scheduled 状态事件(apps/zcode-cli/packages/core/src/runtime/methods/usage-observability.ts:65usage-observability.ts:373);归属的模型取模型步开始时的快照,运行中切模不会把旧请求记到新模型头上(turn-model-step-usage.ts:33)。这些写入都是尽力而为,失败只记警告(usage-observability.ts:120)。

下一篇:会话事件流与持久化投影——这些 ModelStreamingModelComplete、账本与恢复事件,怎样被投影成消息与 part,落进会话库。

本页目录