fix(polish): 长稿润色不再卡死在 30 秒——超时按稿子长度伸缩,流式改用首字/空闲两把尺子 - #966
Merged
H-Chris233 merged 1 commit intoAug 19, 2026
Merged
Conversation
7 分钟录音那条(1758 字)落进 Notion 的是满屏「嗯、呃、然后呢」的原始转写, 事后手动重润色 3 次全失败。查下来模型没出错——它每次都返回了完整结果,只是 step-3.7-flash 在吐正文之前先思考了 43 秒(实测 43~75s,思考量 8572 字), 而我们第 30 秒就撒手了。 两处判据不对: 1. 流式用的是 reqwest 的整请求超时。这把尺子分不清「模型还在正常吐字,只是 这段稿子本来就长」和「服务端卡死了」,30s 一到把两者一起砍。现在拆成首字 预算(用户盯着空屏干等的上限)和空闲预算(出字中途卡多久算死),总时长不 再单独设限——还在稳定出字就让它写完。 推理模型思考期的 reasoning_content 是一串正常 chunk,特意不让它给首字预算 续命,否则「干等多久」就失去上限。 2. 30s 不随输入长度伸缩。ASR 侧早有三套 max(30, 系数 × 量 + 余量) 的动态公式, 润色这条路上漏了。现在流式和非流式(重润色)都按输入字数算预算。 顺带补上首字延迟日志——之前日志里没有这个读数,这次只能靠外部实测才量出 43s。 验证:拿失败那条原文真打 stepfun 重放两次,首字 44.3s / 95.7s,旧判据全被砍, 新判据(首字 118s / 空闲 20s)两次都跑完拿到完整润色。单测新增 8 个,其中两个 关键行为做了变异验证:把超时退回整请求语义、让 reasoning chunk 续命,对应测试 都如期失败。全量 1082 passed。 未覆盖:QA 划词追问的流式路径仍是老语义,Gemini / Codex provider 同理。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
H-Chris233
approved these changes
Aug 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
这个 PR 解决什么
录一段长的——一次会议纪要、一篇口述日记——落到编辑器里的可能是没润色过的原始转写,满屏「嗯、呃、然后呢」。点重新润色,还是一样。
不是模型出错。以 StepFun step-3.7-flash 为例,它在吐正文之前先跑一整段思考:一段 1758 字的稿子,实测要 43~75 秒才吐出第一个字(思考量 8572 字,是正文的 9 倍)。而我们第 30 秒就把请求砍了。服务端每次都返回了完整结果,是我们的判据太短。
短句也会中招。同一段 119 字的输入连跑三次,首字延迟 8.3s / 19.4s / 32.7s —— 第三次就撞线了。任何思考型模型都有这个特征,长稿只是把它放大到必然发生。
改了什么
一、流式不再用「整个请求 30 秒」这一把尺子。
这把尺子分不清两件事:模型还在正常吐字、只是这段稿子本来就长;和服务端真的卡死了。30 秒一到,两者一起砍。
现在拆成两个判据:
总时长不再单独设限:只要还在稳定出字,长稿就该让它写完。
这里有个容易踩的点:推理模型思考期间
reasoning_content是一串正常 chunk。特意不让它给首字预算续命,否则「干等多久」就失去上限——一段 8572 字的思考能把人晾在空屏前一分多钟还不超时。二、预算随输入长度伸缩,流式和非流式(重新润色)两条路都改。
ASR 侧早就有三套
max(30, 系数 × 量 + 余量)的动态超时(whisper_transcribe_timeout一族),润色这条路上一直漏着。现在补上,公式沿用同样的写法。短输入仍落在 30 秒地板上,行为与改动前逐字节一致。三、补首字延迟日志。 之前日志里没有这个读数,排查时只能靠外部实测才量出那 43 秒。有了它,下次看日志就能分清「模型思考太久」和「网络卡住」。
技术细节
polish_first_token_timeout_secs(chars)=max(30, ceil(chars × 0.05) + 30);1758 字 → 118s,覆盖实测最坏的 75s 仍有余量。polish_total_timeout_secs(chars)= 首字预算 +max(30, ceil(chars × 0.03) + 20),供拿不到「第一个字」这个中间信号的非流式路径使用。cached_client以 timeout 为缓存键,每句话长度不同就会造出一个新 client,连接池全部作废——每次润色都要重新 TLS 握手,正是那层缓存当初要消灭的成本。改为新增一个只带连接硬顶(900s,纯防连接泄漏)的polish_client,业务判据全部下放到调用点,缓存键因此保持唯一。tokio::time::timeout包住response.chunk(),按「首字是否已到」在两个预算之间切换。LLMError::Timeout的语义不变,上层dictation的失败分支照旧拿已落屏的typed_text当 final_text,屏幕 / history / 剪贴板保持一致。未覆盖:划词追问(QA)的流式路径仍是老语义;Gemini 与 Codex provider 同理。留给后续。
验证
cargo test --lib1137 passed / 0 failed;tsc --noEmit干净。