拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Qwen Code CLI 的 Usage-only 流内存治理:从无界增长到常量级保留的设计与实现

Qwen Code CLI 的 Usage-only 流内存治理从无界增长到常量级保留的设计与实现【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code导读本文围绕 Qwen Code终端内开源 AI 编码 Agent中一类容易被忽视的内存风险展开某些 OpenAI 兼容端点可以长时间保持一个流式请求却只持续推送 usage 元数据token 统计而不产生任何用户可见内容。当这种仅 usage 流长时间运行时旧版实现的两条保留路径会让内存无界增长最终在 512 MiB 堆限制下直接 OOM。本文基于 usage-only-stream-memory 设计文档 及仓库源码完整还原问题成因、修复设计常量级流内存保留 /quit主动取消活动请求、验证方法与实测结果。读完本文你将掌握OpenAI 流转换管线中 finish/usage 分片合并的常量级实现原理、日志包装层的选择性保留策略以及退出路径如何关闭服务端流可直接用于排查类似 SSE 长连接导致的内存泄漏问题。问题一个只推 usage 的合法流如何击穿内存OpenAI 兼容端点有一个合法但不常见的行为保持流式请求长时间开启却只发送 usage 元数据例如stream_options: { include_usage: true }带来的周期性 usage chunk不产生任何内容分片。对于 SDK 解析器而言这种流有效——它依然能持续产出可解析的 chunk因此解析器不会中断它。真正的问题在于旧实现的两条保留路径它们会在流的整个生命周期内保留每一个转换后的响应OpenAI 转换管线openaiContentGenerator/pipeline.ts为了在流结束时合并出最终的 finish chunk保留了所有历史响应日志包装层loggingContentGenerator/loggingContentGenerator.ts为了在结束时汇总一条完整的响应记录用于日志同样保留了全部响应。在一条持续不断的 metadata-only 流下两个数组都会无界增长——尽管每个新 chunk 只是在反复覆盖同一个逻辑 usage 值。VP虚拟视口模式只影响渲染不参与任何一条保留路径因此对内存无济于事。与此同时/quit动作旧实现只请求后台记忆任务停止不会中止当前活动中的模型请求。这意味着在退出窗口期内流仍可能持续分配内存进一步放大风险。修复一OpenAI 转换管线改为单响应悬挂合并核心机制pendingFinishResponse修复后的管线不再保留全部历史响应而是只悬挂一个待合并的 finish 响应pendingFinishResponse。相关实现位于 pipeline.ts// State for handling chunk merging. // pendingFinishResponse holds a finish chunk waiting to be merged with // a subsequent usage-metadata chunk before yielding. // finishYielded is set to true once the merged finish response has been // yielded, so that any further trailing chunks are treated as normal // chunks instead of triggering another merge (which would duplicate the // function-call parts from the finish chunk). let pendingFinishResponse: GenerateContentResponse | null null; let finishYielded false;整个 Stage 2 流处理流程processStreamWithLogging见 pipeline.ts分四步Stage 2a逐 chunk 转换OpenAIContentConverter.convertOpenAIChunkToLlm并检测以finish_reasonerror_finish内嵌在流里的 API 错误如 TPM 限流抛出StreamContentError交给上游重试Stage 2b过滤空响应——当响应既无 parts、无 finishReason、无 usageMetadata 且无 tool-call 准备分片时直接continueStage 2c调用handleChunkMerging决定当前 chunk 是否需要与悬挂的 finish 响应合并Stage 2d流结束时若仍有未产出的pendingFinishResponse即 finish chunk 之后始终没有 usage chunk 到达则补产出它。合并后的尾部吸收finishYielded关键点在于finishYielded标志。一旦合并后的 finish 响应被 yield 出去finishYielded置为true此后所有尾随 chunk 走吸收分支pipeline.ts只把response.usageMetadata原地写回悬挂的pendingFinishResponse不再 yield 任何新响应。这就是 usage-only 流下内存保持常量级的直接原因——后续海量 usage chunk 只是反复覆盖同一个对象上的usageMetadata字段数组不会增长。handleChunkMerging本身还覆盖了多种 provider 的畸形行为pipeline.ts重复 finish chunk如 OpenRouter 对 tool call 发送两个finish_reasonchunk以第一个 finish 响应为准只吸收后续 chunk 的 usageMetadata 与 modelVersionfinish chunk 与 usage chunk 分离合并时保留原 finish 的 candidates含 function-call parts从后续 chunk 补充 usageMetadata、responseId、createTime、modelVersion、promptFeedbackfinish 之后出现新的内容分片抛出InvalidStreamErrorPROTOCOL_TAG_LEAK阻止协议标签泄漏。保留量证明从源码结构可以明确推断管线内存中最多同时存在一个pendingFinishResponse且它在 finish 已 yield 后仍被刻意保留Keep pendingFinishResponse alive so late-arriving usage metadata can still be merged其占用与流持续时长、chunk 总数完全无关——这就是设计文档所称的常量级保留。修复二日志包装层改为候选分片 最新 usage选择性保留保留策略的精确边界日志包装层LoggingContentGenerator负责把流汇总成一条完整响应用于 telemetry 与可选的 OpenAI 交互日志。修复后的保留策略loggingContentGenerator.ts非常克制const candidate response.candidates?.[0]; if (shouldCollectResponses) { lastResponseForLogging response; if ( (candidate?.content?.parts?.length ?? 0) 0 || candidate?.finishReason ) { responses.push(response); } } if (response.usageMetadata) { lastUsageMetadata response.usageMetadata; }即responses数组只接收两类响应含候选分片candidate parts的响应或带 finish reason 的响应。纯 usage/纯 role 的响应不会进入数组只会更新一个独立的lastUsageMetadata变量——每次都被最新值覆盖O(1) 内存。流结束后loggingContentGenerator.ts先补收最后一条响应responses.at(-1) ! lastResponseForLogging时 push再调用consolidateLlmResponsesForLogging(responses)合并出完整响应最后用lastUsageMetadata覆盖合并结果的 usageMetadata。这样既保留了合并输出 最新 usage的语义又让 metadata-only 流的保留量恒定。配套的时序与判定细节TTFT 判定首块用户可见内容的计时使用hasUserVisibleContent(response)来自 streamContentDetection.ts该函数会跳过 role-only / usageMetadata-only 的 chunk见 loggingContentGenerator.ts 注释确保 TTFT 不被 usage-only 流污染流空闲保护该层存在 5 分钟空闲超时STREAM_IDLE_TIMEOUT_MS 5 * 60_000loggingContentGenerator.ts若消费方放弃生成器且无新 chunk会自动以失败语义关闭 OTel span防止 span 无限泄漏每个 chunk 到达都会重置定时器因此合法长流不受影响。注意这是 span 生命周期保护不是新增的流超时见下文 Scope。修复三/quit 与 /exit 主动取消活动请求设计文档明确指出旧/quit只请求后台记忆工作停止未中止活动中的模型请求导致流在退出窗口期内继续分配内存。修复后quit 动作在请求客户端关闭、运行退出清理之前先走既有请求取消路径同时字面量/quit、/exit输入在存在活动响应时会绕过消息队列直接处理。quit命令本身在 quitCommand.ts 中定义名称为quit别名exit类型CommandKind.BUILT_IN仅支持交互模式动作产出type: quit的消息并附带会话壁钟时长formatDuration(wallDuration)。非交互 CLI 侧也有对应的quit分支处理nonInteractiveCliCommands.ts。请求取消复用既有的AbortController传播链整个代码库中模型请求普遍通过new AbortController()与signal传递取消信号如 acpAgent.ts 等调用点流转换管线侧的drainThenCleanup也会在流结束的finally中执行perRequestAc.abort()pipeline.ts保证请求级取消信号能一路传递到 HTTP/SSE 层从而关闭服务端流。修复边界Scope哪些行为刻意未改变设计文档明确了本次修复的三条边界理解它们有助于避免误用不新增流超时没有为流式请求引入新的最长时长限制5 分钟空闲超时属于既有 span 保护且会随每个 chunk 重置不改 VP 行为虚拟视口只影响渲染不参与保留路径本次也不调整其行为不限制真实模型输出产生用户可见内容的流仍完整保留这些内容用于 telemetry 与可选的 OpenAI 交互日志——常量级保留只针对 metadata-only 流。验证单测、既有回归与本地复现单元测试覆盖有界日志保留loggingContentGenerator相关测试loggingContentGenerator.test.ts与流内容检测测试streamContentDetection.test.ts覆盖 metadata-only 场景下的有界保留行为/quit 取消活动请求quit 命令测试见 quitCommand.test.ts消息队列相关测试见 useMessageQueue.test.ts既有 finish/usage 与重复 tool-call 流测试转换管线测试覆盖了丰富场景pipeline.test.ts包括stream_options: { include_usage: true }L3596、finish 与 usage 分离 chunk 的合并L3958-L3998、finish 与 usage 之间的空 choices chunkL4909、finishusage 同 chunk 的理想情形L5038、provider 在 finish chunk 中发送全零 usage如 modelscopeL5120、重复 finish chunkL3958 一带的finish-1, finish-2, usage序列以及合并后尾随 chunk 不重复产生 function callL5317等。本地复现与实测结果验证流程包含一个针对本地 SSE 服务器的交互式复现该服务器依次返回一个 429、一个空成功响应然后是一条无界的 usage-only 流。复现同时进行内存采样与 quit 延迟取证且分别在 VP 启用与禁用两种配置下执行。在 512 MiB 堆限制下完整数据见 usage-only-stream-memory 设计文档场景修复前修复后VP 启用66 秒内从约 100 MiB 涨至 515 MiBOOM 崩溃177 秒内稳定在 101–131 MiB处理 1357 万条 usage-only chunk/quit正常关闭服务端流并退出VP 禁用36 秒内已达 368 MiB 且持续上升116 秒内稳定在 101–130 MiB处理 962 万条 chunk/quit正常关闭服务端流并退出从数据可以直观看到修复前堆内存几乎呈线性上涨并在 512 MiB 限制附近崩溃修复后无论 VP 开关如何堆内存都收敛在同一低位平台区间且/quit能真正终止活动流退出延迟恢复正常。总结usage-only 流内存治理是一次典型的非法输入之外的合法畸形输入防御端点行为完全符合协议却能让两条看似无害的全量保留策略把进程拖入 OOM。Qwen Code 的解法是双管齐下——转换管线用单悬挂 finish 响应 finishYielded 尾部吸收实现合并语义下的常量级内存日志包装层用候选分片/ finishReason 才入数组 最新 usage 单独覆盖保住汇总输出再加上/quit先取消活动请求再退出堵住退出窗口期的残留分配。对于任何基于 OpenAI 兼容 SSE 流做长连接消费的实现这套分片合并的悬挂窗口与元数据与内容分层保留的思路都具备直接的移植参考价值。【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门