结论
三处 LLM processor 用 max_workers = min(len(输入列表), 配置上限) 算线程池大小。
输入列表为空时算出 max_workers=0,ThreadPoolExecutor(max_workers=0) 直接抛
ValueError: max_workers must be greater than 0,任务在 LLM 阶段被判 failed。
这是确定性缺陷,与网络/服务状态/负载无关:同一个任务重新提交多少次都会以同一行错误失败。
生产上已经这么失败过两轮(2026-08-12 首次、2026-10-06 重提交复现)。
证据(生产实测,2026-10-06,n305 video-transcript-api 容器)
VTA 任务 task_ac8c5370bae3462ba9cd2be6ba018003,来源 live-recorder 任务
9a4d28020d5348f788bfe33932ea226e(音频时长 12 秒,标题「183冯律」):
[perf] task_ac8c5370bae3462ba9cd2be6ba018003 | llm_processing: 2770ms (FAILED: max_workers must be greater than 0)
ERROR llm_ops._handle_llm_task:918 - LLM任务处理异常: task_ac8c5370..., 错误: max_workers must be greater than 0
cache_manager.update_task_status:2470 - 任务状态更新: task_ac8c5370... -> failed
terminal_status.finalize_terminal_status_and_notify:101 - terminal CAS won: task_ac8c5370... -> failed
[perf-summary] task_ac8c5370... | total: 7839ms
ASR 阶段本身是成功的(日志有 转录完成,生成文件: [...]),死在 LLM 阶段。
12 秒音频转出的文本为空 → 零分段 → min(0, 10) = 0。
该任务在 live-recorder 侧的表现:2026-10-06 重新提交后仍失败,vta_status 回到
confirm_timeout,页面显示「转录失败」。录音文件本身完好(0.4 MB,12 秒)。
缺陷点位(origin/main @ d968df8,行号实测)
| 文件 |
行 |
表达式 |
空输入可达性 |
llm/processors/plain_text_processor.py |
420 |
min(len(segments), self.config.concurrent_workers) |
已实测复现 |
llm/processors/speaker_aware_processor.py |
970 |
min(len(chunks), self.config.calibration_concurrent_limit) |
代码路径等价可达:normalized_dialogs 为空 → DialogSegmenter.segment([]) → chunks=[](speaker_aware_processor.py:252),未实测 |
llm/processors/notes_processor.py |
551 |
min(len(mapping.slices), self.config.notes_concurrency) |
未确认,取决于章节映射能否为空 |
三处都在 ThreadPoolExecutor(max_workers=max_workers) 之前没有空列表守卫。
影响面
触发条件是「转录文本为空」。真实来源不是罕见的格式损坏,而是极短录制:
live-recorder 用户中途停止 / 临场切 mode 会产出几十秒碎片,其侧有
vta_min_duration_s(默认 120s)门槛,但本仓 API 侧没有对应门槛,直接
/api/transcribe 提交或 recorder 门槛被调低都会把空文本送进 LLM 阶段。
后果:任务永久失败且无法通过重提交恢复——用户能拿到的只有一段没有文字的录音。
建议修法
空输入时在该函数内直接返回与空输入同构的空结果(例 _calibrate_segments 返回
([], [])),不建池、不 submit、不打 LLM 请求;非空路径的并发行为、分段顺序、
contextvars.copy_context().run 传播与日志一律不变。
不建议改写成 max(1, ...):那只是让空输入白建一个空线程池,语义仍然错。
附:另一批「转录文件失败」历史样本今日重提交已全部恢复
live-recorder 生产库有 23 个 vta_status=confirm_timeout 的历史转录失败样本,
其中 4 个的错误是 转录文件失败: data/temp/task_xxx/xxx.mp4(VTA 侧 ASR 阶段失败)。
2026-10-06 抽样重提交 60ffbf1bb460487094768504e0a6aa6d(8815.69 秒音频):
transcription_deadline duration=8815.69 value=35382.8
capswriter_start attempt=1/5 media=.../强哥本人号.mp4 duration=8815.688667 deadline_total=35382.754668
capswriter_done ... processed=4520.1s ...
→ 任务完成
即当前版本能拿到时长、给出 4 倍实时预算,这批历史失败在今天的链路上不复现。
与 #170 描述的「recorder:// 路径时长预算恒失效」相关,但本次实测拿到的是
duration=8815.69(非 unknown),说明该路径当前已能取到时长——修 #170 前建议先确认
线上镜像是否已含相关改动,避免按过期机制排查。
结论
三处 LLM processor 用
max_workers = min(len(输入列表), 配置上限)算线程池大小。输入列表为空时算出
max_workers=0,ThreadPoolExecutor(max_workers=0)直接抛ValueError: max_workers must be greater than 0,任务在 LLM 阶段被判failed。这是确定性缺陷,与网络/服务状态/负载无关:同一个任务重新提交多少次都会以同一行错误失败。
生产上已经这么失败过两轮(2026-08-12 首次、2026-10-06 重提交复现)。
证据(生产实测,2026-10-06,n305
video-transcript-api容器)VTA 任务
task_ac8c5370bae3462ba9cd2be6ba018003,来源 live-recorder 任务9a4d28020d5348f788bfe33932ea226e(音频时长 12 秒,标题「183冯律」):ASR 阶段本身是成功的(日志有
转录完成,生成文件: [...]),死在 LLM 阶段。12 秒音频转出的文本为空 → 零分段 →
min(0, 10) = 0。该任务在 live-recorder 侧的表现:2026-10-06 重新提交后仍失败,
vta_status回到confirm_timeout,页面显示「转录失败」。录音文件本身完好(0.4 MB,12 秒)。缺陷点位(origin/main @ d968df8,行号实测)
llm/processors/plain_text_processor.pymin(len(segments), self.config.concurrent_workers)llm/processors/speaker_aware_processor.pymin(len(chunks), self.config.calibration_concurrent_limit)normalized_dialogs为空 →DialogSegmenter.segment([])→chunks=[](speaker_aware_processor.py:252),未实测llm/processors/notes_processor.pymin(len(mapping.slices), self.config.notes_concurrency)三处都在
ThreadPoolExecutor(max_workers=max_workers)之前没有空列表守卫。影响面
触发条件是「转录文本为空」。真实来源不是罕见的格式损坏,而是极短录制:
live-recorder 用户中途停止 / 临场切 mode 会产出几十秒碎片,其侧有
vta_min_duration_s(默认 120s)门槛,但本仓 API 侧没有对应门槛,直接/api/transcribe提交或 recorder 门槛被调低都会把空文本送进 LLM 阶段。后果:任务永久失败且无法通过重提交恢复——用户能拿到的只有一段没有文字的录音。
建议修法
空输入时在该函数内直接返回与空输入同构的空结果(例
_calibrate_segments返回([], [])),不建池、不 submit、不打 LLM 请求;非空路径的并发行为、分段顺序、contextvars.copy_context().run传播与日志一律不变。不建议改写成
max(1, ...):那只是让空输入白建一个空线程池,语义仍然错。附:另一批「转录文件失败」历史样本今日重提交已全部恢复
live-recorder 生产库有 23 个
vta_status=confirm_timeout的历史转录失败样本,其中 4 个的错误是
转录文件失败: data/temp/task_xxx/xxx.mp4(VTA 侧 ASR 阶段失败)。2026-10-06 抽样重提交
60ffbf1bb460487094768504e0a6aa6d(8815.69 秒音频):即当前版本能拿到时长、给出 4 倍实时预算,这批历史失败在今天的链路上不复现。
与 #170 描述的「
recorder://路径时长预算恒失效」相关,但本次实测拿到的是duration=8815.69(非 unknown),说明该路径当前已能取到时长——修 #170 前建议先确认线上镜像是否已含相关改动,避免按过期机制排查。