Skip to content

05 多供应商编排与多段一致性合成深挖问答(简历 bullet ①) ​

简历第一条写的是"多源 AI 视频编排:集成 6+ 视频服务供应商、统一 BullMQ 调度、支持多段一致性合并 / 分镜合并 / 首尾帧桥接、覆盖 10+ 种生成方式"。 这一篇就是为它准备的。注意:这条 bullet 有三处需要修正(多源动态路由、首尾帧桥接的实际效果、供应商数量语义),🚩 标注处给了准确说法与话术。 格式:【考点】 → 【口述答案】(背这段)→ 【备注讲解】 → 【代码依据】。


〇、一页总览:一条分段视频是怎么被造出来的 ​

文案/脚本
   │
   ├─ ① 规划分段:planSegments(total, segMax)  ← 段上限由「模型能力表」决定(2.0 是 15s,2.5 是 30s)
   │     余数分摊给靠前的段,保证总时长精确
   │
   ├─ ② 逐段编译提示词 compileProductionPrompt(段序号, 段总数)
   │     承接块(首帧即上段末帧)+ 尾帧定格约束 + 硬约束置底
   │
   ├─ ③ 组装该段的输入素材 buildSegmentFiles()
   │     [产品图/模特图 …参考图(补边归一化)]
   │     + {role:'first_frame',  url: 上一段尾帧}      ← 连贯性第 1 层
   │     + {role:'reference_audio', url: 音色锚 wav}   ← 连贯性第 3 层
   │
   ├─ ④ 调 provider.generateVideo() → videoUrl
   │     立刻把该段落库为 status='generated'(钱已经花了,绝不能因为后续抽帧失败而重生成)
   │
   ├─ ⑤ 非末段:ffmpeg 抽尾帧 → 上传 → 存 lastFrameUrl → 下一段当首帧锚
   │     首段:抽 [1.0s,6.0s] 人声 → 上传 → 全批后续段共用同一把嗓子
   │     然后把该段置为 status='done'
   │
   ├─ ⑥ 全部完成 → ffmpeg 拼接(逐段归一化 → concat)
   │     单段则跳过拼接,但同样下载 + 转存到自有存储
   │
   └─ ⑦ 后处理(整条成片只做一次):超分 480P→720P/1080P → 封面 → 按真实秒数结算

三个必讲的点

  1. 段状态是三态的(不存在 / generated / done),generated 的含义是"视频已生成、钱已花,但锚点还没抽出来"——这样重试时只重跑抽帧,不会重新烧钱。
  2. 连贯性有四层,但腾讯通道会把"真帧链"降级成"参考图 + 提示词接续"(🚩 这是必须主动说清的事实)。
  3. 拼接是自己用 ffmpeg 编排的,不是云剪辑;腾讯 VOD 只负责"生成"和"拼接之后的超分"。

Q1. 你们到底接了哪些视频供应商?现在还剩几个在用? ​

【考点】 简历第一句就写"6+ 个供应商",这是必被追问的一题。含糊就翻车。

【口述答案】历史上真实集成过 6 个视频供应商,但现在新任务只走 1 个:

  • 腾讯云 VOD AIGC(tencentVod.ts)——当前唯一路由目标,TC3 签名,走 CreateAigcVideoTask + DescribeTaskDetail,支持 Kling 3.0 Omni、MiniMax H3、VS 2.0/2.5 系列(含高清档和免审档)、首尾帧、参考图(2.5 支持到 50 个)。
  • 火山引擎方舟 Ark(ark.ts)——直连字节的 Seedance 2.0,Bearer 鉴权,异步任务 + 轮询。已停用。
  • Seedance 2.0 代理网关(seedance.ts)——提供素材库能力(asset:// 穿透真人审核),是 Ark 的补充。已停用。
  • Seedance 2.5 网关(seedance25.ts)——30 秒 / 50 个参考位 / 支持 operation:'edit' 整片编辑。已停用。
  • 筷子科技丽帧 2.0(kuaizi.ts)——最早的供应商,已废弃成死代码。
  • 快乐马 HappyHorse——参考视频 + 参考图做视频编辑,Key 已失效,事实死代码。 另有 3 类非生成上游:火山 VOD(字幕擦除、升清)、腾讯 MPS(字幕擦除、换脸)、阿里云 DashScope(TTS、声音克隆、分镜理解、生图)。

有一件必须主动说的事:为什么这些"已停用"的实现一个都没删? 因为轮询是拿数据库里存的 task.provider 字段去取实现的。发布时如果直接把火山实现摘掉,那么所有发布瞬间还在途的火山任务都会轮询失败——结果是"钱已经付给供应商了,但片子永远拿不到"。所以废弃通道的实现必须保留到最后一个在途任务结束。

【备注讲解】 "6+ 供应商"这个表述的问题在于它暗示了"多源动态路由"。事实是 resolveProvider() 现在是个恒等函数,忽略全部入参直接返回 'tencent-vod'(🚩 见 Q2)。诚实的说法是:"历史接入过 6 家、能力抽象都还在、当前生产链路主动使用 1 家"。

【代码依据】 backend/services/video/providers/index.ts:1-27(工厂 + 停用注释 + 恒等 resolveProvider)、backend/services/video/providers/(6 个实现文件)、backend/services/modelCatalog.ts:67-97(火山通道标 retired: true、provider: '火山引擎(已停用)')。


Q2. 🚩 Provider 抽象是怎么设计的?resolveProvider 恒返回腾讯云,这个抽象还有价值吗? ​

【考点】 这是这组题里最锋利的一问。答得好,说明你既能讲抽象又能承认退化。

【口述答案】 抽象是一个接口,定义在 backend/types/providers.ts:

ts
export interface VideoProvider {
  name: string;
  createTask(params): Promise<Record<string, unknown>>;
  queryStatus(taskId): Promise<Record<string, unknown>>;
  poll(taskId, onProgress?): Promise<PollResult>;
  generateVideo(params): Promise<{ id: string; status: string; url: string }>;
}

上层(提交 Worker、轮询 Worker、脚本、单测)只认这四个方法,不认具体厂商。 但我要承认两件事:第一,路由已经退化了。 resolveProvider() 现在忽略所有入参、恒定返回 'tencent-vod'。所以严格说,今天这套抽象服务的是"按历史任务号回捞实现",不是"按能力动态选源"。resolveProviderForTaskId() 就是干这个的——拿 taskId 的前缀(s25: 之类)判断该用哪个历史实现来轮询。 第二,契约的类型强度很弱。 返回值都是宽松的 Record<string, unknown>,适配器普遍带 @ts-nocheck,所以"契约"更多靠约定而不是编译期约束。 那它还有什么价值? 三点:① 轮询/结算/对账这些链路完全不关心厂商差异,换供应商时它们一行不用改;② 能力差异被收敛到一处(modelCapabilities),调用方只问"这个模型能做什么"、不认模型名;③ 新增供应商的成本是可枚举的 6 步。

【备注讲解】 诚实但不自贬的说法结构是:"抽象本身是对的(价值 ①②③)→ 但路由这一层确实退化了(原因:上游通道收敛)→ 如果要恢复动态路由,需要把 resolveProvider 从恒等函数改成真正的策略函数"。 另外要能说出"能力差异收敛在一处"这个设计(见 Q3),它比接口本身更有价值。

【代码依据】 backend/types/providers.ts:36-42(接口)、providers/index.ts:29-41(resolveProvider 恒等 + resolveProviderForTaskId 前缀回捞)、backend/services/video/modelCapabilities.ts(能力表)。


Q3. 不同模型能力不一样(参考图数量、单段秒数、能否整片编辑),你们怎么处理? ​

【考点】 设计判断题:能力差异该放在哪里。

【口述答案】 专门有一个模块 modelCapabilities.ts 来回答"这个模型能做什么",收敛成一个函数:

ts
getRemakeModelCaps(model) → { maxRefImages, maxSegmentSec, supportsEdit }

比如旧模型(Seedance 2.0 / Kling 3.0 Omni / MiniMax H3)保守上限是9 张参考图、单段 15 秒、不支持整片编辑;2.5 是 30 秒、50 个参考位、支持 operation:'edit'。 为什么要单独一个模块? 因为原来这些数字是写死在 generate-long 里的常量,跟模型无关。结果 Seedance 2.5 接进来之后,它 30 秒 / 50 张图的能力白给了——用户选了 2.5,系统还是只塞 9 张图、还是按 15 秒切段。更糟的是一次真实的不一致:planForJob 里判断模型的地方用了局部值,而 durationMode 的短档守卫用的是全局值,导致"把短档上限配到 20 秒时,2.0 的任务会被静默切成两段而校验一声不吭"。 所以现在的设计原则是:调用方只问能力,不认模型名;而且查表前要把"交付档位 id"(vs-2.5-hd)归一到"基础模型 id"(vs-2.5),否则高清档会查不到表、静默掉进默认的 15 秒,还没有任何报错。

【备注讲解】 这道题的通用教训非常好用:"能力/限制类常量一旦写死,就会让新接入的上游能力变成死代码——因为它不会报错,只是悄悄用旧值。"这是"配置漂移"的一种,面试官会觉得你踩过坑。

【代码依据】 backend/services/video/modelCapabilities.ts:1-70(含"写死的常量让 2.5 的能力白给"的注释、基础 id 归一)、backend/services/modelCatalog.ts:20-31(baseModelId 字段与"30 多处按 === 'vs-2.5' 精确匹配"的风险)。


Q4. 新增一个供应商要改哪些地方? ​

【考点】 抽象质量的度量尺。

【口述答案】 六步,是可枚举的:

  1. 写适配器,实现那 4 个方法;
  2. 注册进 providers/index.ts 的工厂表;
  3. 在 config 里加密钥读取;
  4. 更新 .env.example;
  5. 在 modelCatalog 里加条目(原生分辨率、计价键、档位、账号);
  6. 写一条迁移 INSERT service_pricing 把价格配进去。 但最关键的是第 0 步:改 resolveProvider。 因为它现在是个恒等函数,不改它,新供应商永远走不到——这恰恰就是火山那批实现删不掉的根因:路由已经被钉死在一个通道上,而这些实现只能作为"历史任务的轮询器"存在。

【备注讲解】 这题的答法建议以"第 0 步"收尾——把面试官的注意力从"扩展成本清单"引到"路由层是当前架构的真正瓶颈"上。这是个主动暴露架构弱点的动作,比单纯背六步强。

【代码依据】 providers/index.ts:4-27、backend/services/video/modelCapabilities.ts、backend/migrations/ 中 service_pricing 相关迁移。


Q5. 说说多段视频的"一致性"是怎么保证的?这是简历里的核心卖点。 ​

【考点】 主战场。必须能讲出"分层",而不是只说"用上一段尾帧"。

【口述答案】 一致性我按四层来讲,从强到弱: 第 1 层,画面连续:上一段的尾帧 → 本段的首帧锚。 每段生成完后用 ffmpeg 抽出尾帧、上传拿到公网 URL,作为下一段的 first_frame 素材传下去。代码里有个硬判断:第 2 段及以后如果没有拿到首帧锚,直接抛错("段 k+1 缺少上一段尾帧,无法保证连贯")——宁可失败也不出一个必然跳帧的片子。 第 2 层,尾帧静止约束。 提示词里加一段"尾帧定格"约束:最后 0.5 秒画面定格、人物闭嘴、不要运动模糊。原因是抽出来的尾帧如果带运动模糊或者嘴型半开,下一段从这里起手就会糊,这个约束被注释称为"整条连贯性链的单点故障"。 第 3 层,音色锚。 第一段生成完后,从成片里抽 [1.0s, 6.0s] 的单声道 16k 人声,作为后续所有段的 reference_audio,让全批用同一把嗓子。如果预设里配了音色参考,就优先用预设的(全批统一人设)。 第 4 层,提示词承接块。 编译每段提示词时显式告诉模型"首帧就是上一段的末帧,不要重建场景、不要重新介绍产品"。 在这四层之外还有"参考图身份锁":我们会把参考图按顺序打标签(图片N=某某模特图),并强制"标签必须与实际下发的图片严格同序同长"——注释里写得很重:"错位会让身份锁指到产品图上,比不加身份锁更糟"。

【备注讲解】 🚩 必须主动补的这一段(否则面试官读代码后会觉得你在夸大):

"不过要说明,这套"真帧链"在腾讯云通道上会被降级——腾讯 VOD 在存在任何参考图或参考视频的时候,会把 FirstFrame/LastFrame 强制降级成 Reference。所以在大量爆款/生产场景里,实际起作用的主力是"参考图 + 提示词接续",而不是真正的首尾帧锚。而且在爆款复刻的分段链路里,我们是显式关闭桥接的(needsBridgingToPrev: false)——因为原片的首尾帧带着旧产品、旧人脸,跟替换目标冲突,硬接反而更差。"

主动说出这句,这道题的分数会从"讲了个亮点"变成"对亮点有清醒认识"。

【代码依据】 backend/services/queue/productionWorker.ts:186-197(组装首帧 + 音色素材)、:276-280(缺首帧直接 throw)、:342-359(抽尾帧/抽人声)、backend/services/production/promptCompiler.ts:66-80(承接块 + TAIL_FREEZE_BLOCK)、backend/services/video/providers/tencentVod.ts:269-302(首尾帧在有参考媒体时降级为 Reference)、backend/services/assistantChatRemakeLong.ts:397-398(显式 needsBridgingToPrev:false)、backend/services/production/characterReferenceGuard.ts:15-31(同序同长 + "比不加更糟")。


Q6. 段的重跑怎么避免重复烧钱?"这段能不能复用"的判据是什么? ​

【考点】 又是钱的问题——这是这套系统里最考验设计的地方。

【口述答案】 段状态是三态机:

  • 不在 job.segments 里 → 完整流程:生成 + 抽锚点;
  • status='generated' → 视频已经生成、钱已经花了,只是抽尾帧/抽音色/上传锚点没走完。重试时跳过 generateVideo,复用已落库的 videoUrl,只重跑锚点提取;
  • status='done' → 生成和锚点都齐了,完全跳过。 关键实现细节是:生成完立刻落库成 generated,因为"视频已经花钱生成出来了,哪怕接下来抽尾帧/抽音色/上传任一步抛错,下次重试也绝不能让这段被判定为'不存在'而重新调用生成"。这是用落库时机来保护已经花掉的钱。 但复用还有一个前提:段必须属于当前的分段计划。我们给每个 plan 算一个指纹(planFingerprint),只有"状态是 done/generated + 有 videoUrl + 指纹与当前计划一致"的段才可复用。如果配置中途变了(比如改了单段秒数),旧段与新计划不兼容,必须整批丢弃重跑——虽然这意味着重新花钱,但相比"安静地把一条 15 秒当 30 秒成片交付",这个取舍是对的,而且丢弃时必须打 warn,因为重新烧钱这件事不能静默发生。 还有一个细节:判断"能不能复用"的谓词只有一处(一个 reusable 函数),因为注释里写着"两处各写各的谓词,就会出现一处认为可信所以不告警、另一处认为不可信所以丢弃重烧的静默重复扣费"。

【备注讲解】 "用落库时机保护已经花出去的钱"这句话值得慢一点说。它背后的通用原则是:在调用付费外部服务之后,第一件事是把"我已经调用过了"这个事实持久化,而不是继续做后续处理。

【代码依据】 backend/services/queue/productionWorker.ts:206-256(三态机注释 + reusable 单谓词 + 丢弃 warn)、:326-334(生成后立刻落 generated)。


Q7. 多段是怎么拼接的?为什么不用云剪辑? ​

【考点】 技术选型判断题。

【口述答案】 拼接是我们自己在 Node 里编排 ffmpeg,通过 execFile 直接调系统二进制(FFMPEG_PATH 可覆盖),没有用 fluent-ffmpeg,也没有 ffmpeg-static。腾讯云 VOD 只出现在两个地方:生成(CreateAigcVideoTask)和拼接之后的超分(PullUpload → ProcessMedia);全仓没有任何 EditMedia/ComposeMedia 云剪辑调用。 为什么自研:① 拼接要和"抽尾帧/抽音色/响度归一化/字幕烧录"这些本地处理串在一条链里,走云剪辑就得来回上传下载;② 成本可控,转码不额外计费;③ 时间是我们的主要瓶颈,本地 ffmpeg 在几十秒级片段上是够快的。 代价:强依赖宿主机装好 ffmpeg/ffprobe——启动时会做一次探测,探测失败会明确警告"视频时长探测、截取、拼接功能将失败"。

【备注讲解】 🚩 这里有个简历措辞要小心:写"自研视频合成服务"会让人以为你有转码集群;准确说法是"自研 ffmpeg 编排"。 另外一个值得主动讲的现状:仓库里现在有四套 concat 实现并存(多段生产、长视频、长视频 master 单趟、创意/标准复刻各一份),各自处理细节不同。这是真实的技术债(重复实现),我建议的收敛方向是抽一个统一的"分段归一化 + 拼接"服务。

【代码依据】 backend/services/media/mediaUtils.ts:4-14(ffmpeg/ffprobe 封装 + 可覆盖路径)、backend/services/production/stitcher.ts、backend/services/longVideo/generator.js:1354(concatVideos)、backend/services/viral/creativeRemakeVideoService.ts:410(crConcatVideos)、backend/index.ts:223-229(启动探测 ffmpeg)。


Q8. 拼接踩过哪些坑?挑一个讲讲。 ​

【考点】 挖工程细节。这一题最能体现"你真的调过 ffmpeg"。

【口述答案】(挑 2~3 个,我最推荐前两个) 坑一:响度归一化和 -shortest 组合会吞掉段尾的真实人声。 我们原来用 loudnorm 直接编码,但 loudnorm 是双 pass 的、会改变音频长度,配合 -shortest 就把段尾 90~95 毫秒吞掉了——而我们的台词收尾是刻意切在段尾停顿处的,正好炸在最后几个字上,用户听感就是"话被掐了"。 修法是改成**"先测量、再线性增益":先用 loudnorm 的 print_format=json 只做测量拿 input_i / input_tp,然后按 min(TARGET_I - input_i, TARGET_TP - input_tp) 算一个零延迟的线性 volume 增益**。这样既归一了响度,又不会改变长度、不会削波。 坑二:-c copy 拼接时的"卡帧"。 AAC 的帧粒度是 23.2ms,24fps 的帧是 41.7ms,两者互质,音频会溢出到 stts/mdhd 里;于是 concat 时段边界会被插入一个畸长的音频包,表现就是拼接处卡一下。根因是 -shortest 当时只加在了"无音轨"分支里。 坑三:抽尾帧用容器时长会抽出 0 帧。 容器时长是音频长度(通常比视频长 0.05 秒),按末尾 seek 会落在视频已结束的位置,mjpeg 直接报错 -22。修法是在输入侧做粗跳 + 逐帧覆盖写同一个文件。

【备注讲解】 这三个坑有一个共同点值得点出来:它们都是"两个看似无关的参数互相作用"产生的(loudnorm × shortest、AAC 帧粒度 × fps、容器时长 × 视频时长)。可以补一句自省:"而我们当时的时长/fps 二次校验对这类根因并不敏感——校验能发现问题,但指不出原因。这也是为什么我们把拼接线上的坑都写进了注释。"

【代码依据】 backend/services/production/stitcher.ts:8-14,133-143(loudnorm 改为测量 + 线性增益)、:164-171(AAC 帧粒度与 stts 卡帧)、:297-313(抽尾帧的容器时长问题)。


Q9. 生成方式到底有几种?积分怎么定价? ​

【考点】 简历"10+ 种生成方式"的核实 + 计费一致性。

【口述答案】 导航目录里有 19 个标签,真正算独立生成方式的是 14 种: 创意复刻、元素替换(单条/精修/批量)、视频编辑、脚本视频、剧情模式、对话生成、通用生成、生图、分镜头生成、数字人口播、首尾帧生成、字幕擦除、批量生产、图文成片。 另外还有不单独计费的:人脸替换(不扣积分)、声音克隆、画布编辑(按视频编辑或模型价走)。 定价的单一真相源是 service_pricing 表,计算是 max(min_cost, round(units × rate, 2)),Redis 缓存 600 秒。视频模型这边还有按档位分价:普通档、高清档、免审档、带参考视频档,各自独立的 pricingServiceKey。举几个真实价:创意复刻 1.0 积分/秒、视频编辑 1.5 积分/秒(高清 2.5)、字幕擦除 0.1 积分/秒、元素替换 4.0 积分/批 + 按秒。

【备注讲解】 主动补充一个"按真实时长结算"的点:批量生产这条链路是按拼接后的真实秒数结算,而不是按计划秒数——因为分段拼接后总时长会与计划有偏差,按计划结算会让用户付了没拿到的时长。

【代码依据】 backend/services/navCatalog.ts:29-50(19 条导航)、backend/services/pricing/pricingService.ts:62-71、backend/services/queue/productionWorker.ts:405-406(按真实拼接秒数结算)。


Q10. 提示词工程你们是怎么做的?量大吗? ​

【考点】 AI 应用岗位的核心能力之一。

【口述答案】 我们的提示词不是散在各处的字符串,而是有一个权威规则源:backend/services/prompts/videoPromptRules.ts(整个 prompts/ 目录只有 4 个文件、439 行)。所有的硬规则都在里面,前端有一份镜像需要双边同步。 核心设计是**"优先级 = 数组顺序"**:拼装函数按固定顺序把 7 段约束拼起来——模特身份锁(最高优先级)→ 镜头转场 → 口型同步 / 无对白(互斥)→ 音色参考 → 产品文字 → 噪声防护 → 质量守卫,并且硬约束一律放在最末尾。原因很直接:模型对末尾的约束最敏感,所以最重要的规则放最后。 另外几个真实的中文电商场景技巧:

  1. @图片N 必须显式绑定业务语义。我们会在提示词里写清"图片1 是产品图、图片2 是模特图",否则"图虽然绑定了但没人指认",模型基本会忽略它。
  2. 口播语速有硬约束:时长 × 3~5 字,并要求模型粘贴计数自检(中文口播的语速直接决定成片能不能塞下台词)。
  3. 零画面字幕:Seedance 看到台词会在画面上渲染字幕,而 AI 渲染中文必然乱码,所以我们禁止模型渲染字幕,改成后期用 ASS 精确烧录。
  4. 多音字同音正字替换:比如"东阿阿胶"喂给 TTS 之前替换成"东婀婀胶",但只改喂引擎的文本、不动 copywriting 字段,保证字幕显示是正确的。
  5. 去雷同:有一个四维词表(hook 6 × opening 5 × persona 5 × camera 4 = 600 种组合),用 FNV-1a 播种轮转 + Fisher-Yates 打散,零额外生成成本地让批量生产的视频不撞脸;更重的方案是 7 步 LLM 创意差异化(含陈词滥调审计)。

【备注讲解】 "模型对末尾约束最敏感,所以硬约束置底"是个很具体、很实用的经验,比泛泛说"我们做了提示词工程"强太多。另外要交底一处矛盾:分段链路刻意禁用了逐镜 @图片N 绑定——因为分段参考图会被重新排序(分镜板置顶、每段最前插首尾帧),两边对"图片3"的理解不是同一张图,挂上绑定等于把错绑讲得更斩钉截铁,比不挂更糟。

【代码依据】 backend/services/prompts/videoPromptRules.ts:92-178(无字幕规则、语音闭集、硬约束拼装顺序)、backend/services/production/promptCompiler.ts:54("模型对末尾约束最敏感")、backend/services/video/providers/shared.ts(素材标签与产品指认)、backend/services/production/diversityAxes.ts(600 组合零成本去雷同)、backend/services/shots/creativeDiversificationPipeline.ts(7 步 LLM 差异化)、backend/services/longVideo/generator.js:2790-2795(分段链路禁用逐镜绑定)、backend/services/longVideo/tts.js:52-53(多音字替换)。


Q11. 上游返回的失败,你们怎么区分"我们的问题"和"上游的问题"? ​

【考点】 归因能力。

【口述答案】 先看一个数字:PixelCountTooSmall(参考图分辨率太小)在生产日志里出现 1348 次,是第二名的 70 倍——也就是说绝大多数失败是素材问题,不是模型问题。 所以我们的做法是三层:

  1. 入口预防:参考图在提交前统一做长宽比归一化(补边到 [0.4, 2.5]),否则超范围的图会被上游素材库直接拒收。这里踩过一个坑:这个归一化曾经只挂在 4 个路由上,漏挂的路由就会"同一张图连挂 9 次"。
  2. 失败时取原文:失败分支会再问一次供应商,把上游的错误原文取出来(extractProviderFailDetail),而不是写死"供应商返回任务失败"这种套话。之前那句套话是生产 error_message 里出现最多的一条(近 60 天 21 次),真实原因全丢了,客服只能去 grep 日志。
  3. 面向用户的翻译:有一份前后端共用的错误词典 shared/generation-error-dict.json,把上游错误正则分类成"审核类 / 素材类 / 我们的问题 / 计费类 / 未知",给用户出中文文案,并且在管理端保留原始英文,方便定位。

【备注讲解】 "1348 次 vs 第二名 70 倍"这个数字非常有说服力——它把"失败归因"从感觉变成了数据。如果你能顺口说出这个比例,面试官会认为你真的看过生产日志。

【代码依据】 backend/services/video/providers/tencentVod.ts:628-633(归一化漏挂导致"同一张图连挂 9 次")、backend/services/queue/videoPollWorker.ts:163-170(失败取原文 + "近 60 天 21 次")、shared/generation-error-dict.json。


Q12. 上游限流和并发额度怎么治理? ​

【考点】 资源治理。

【口述答案】 用 Redis 分布式槽租约。原因是腾讯云账号的 AIGC 并发是账号级额度,而我们在同一账号上除了主站还有另一个转售 API 服务,进程内计数一定会超发,所以必须做成跨进程、跨服务的全局租约:globalLimit 100,其中生产配额 50。槽要 bindSlot(taskId),没绑上就在 finally 里释放——如果泄漏,全站会排队卡死。 另外我们有两个腾讯账号(主账号 + 一个"免审"通道),并发池是按账号 id 分的——因为账号之间有三样东西硬隔离:SubAppId(跨账号签名会鉴权失败)、素材 assetId(跨账号引用会报"素材不存在",看起来像素材过期)、并发额度。混用这三样的报错都不会说"你用错账号了",只会报一些指向别处的错。 还有一层是队列本身的并发度(production 队列默认 3),那是"一天能吐多少条"的总闸。

【备注讲解】 通用原则:"配额池必须按资源主体分片,共享一个计数器会导致两个租户互相饿死。"这个抽象在限流、多租户、多集群调度里都成立。

【代码依据】 backend/services/tencent/slotAdmission.ts、slotLease.ts、backend/services/tencent/vodAccounts.ts:1-45(账号三样硬隔离 + 槽池按账号)、backend/config/index.ts:709(PRODUCTION_CONCURRENCY 注释)。


Q13. 🚩 是不是所有生成都走 BullMQ 队列? ​

【考点】 又是一道"简历措辞 vs 代码"的题。必须诚实。

【口述答案】 不是,我要把这块讲准确。 BullMQ 真正承担的是"轮询与部分生产链路",不是全部提交路径。 具体说:

  • video-generate 队列(提交给供应商)现在只有 taskRecovery 在用——常规的 enqueueVideoTask() 生产者没有任何调用方。
  • 主流链路(通用生成 / 分镜 / 爆款复刻)是"路由里直接调供应商拿 taskId,然后进 video-poll 队列轮询"。
  • 另外有三条链路是完全不进队列、由内存自管的:长视频、批量镜头复刻、脚本库一键生成(后者走自己的 production-generate 队列)。 所以准确的说法是:"统一的任务队列与状态管理",而不是"所有生成都统一走一个队列"。README 里那句"BullMQ 流程目前只有 taskRecovery 会用"反而是对的。

【备注讲解】 主动说出这个会更可信,而且它自然带出下一题(内存自管的债)。不要说"我们全部走队列"——面试官一句"那 enqueueVideoTask 的调用方在哪"就穿帮了。

【代码依据】 backend/services/queue/videoQueue.ts:30-51(enqueueVideoTask,全仓无调用方)、backend/services/queue/videoWorker.ts:67-94(三条自管链路的 skip)、backend/services/queue/videoPollQueue.ts(多个生产者:subtitleErase / faceSwap / standardRemake / creativeRemake / taskRecovery)。


Q14. 那"内存自管"的链路有什么问题? ​

【考点】 架构债的自我认知。

【口述答案】 两个问题。 第一是恢复能力。 长视频、批量复刻这类任务的状态在进程内存里(长视频用 Redis lease 续命来间接判断存活),进程重启就丢。所以恢复逻辑里必须按 generateMode / sourceMode 逐个拦截它们,否则会被通用逻辑误判失败、甚至清零积分。这是一长串 if 分支,靠注释维持清单,新增一种生成方式很容易漏一处。 第二是水平扩展能力。 这些链路的状态不共享,多副本部署时状态对不上,taskRecovery 只能靠 Redis lease 猜"这个任务是不是还活着"。要做水平扩展,这些链路必须先改成状态外置。 正确的收敛方向是:给任务加一个显式的"执行类型"字段(平台编排 / 自管),让恢复逻辑、Worker、轮询三处用同一个判据,而不是三处各自维护字符串清单。

【备注讲解】 这题回答完,可以顺势接到"负载涨 100 倍怎么办"(见 11-白板手写题与设计题.md 的设计题 16),把话题主动权拿回来。

【代码依据】 backend/services/taskRecovery.ts:26-207(九条自管链路的拦截)、backend/services/queue/videoWorker.ts:67-94(Worker 侧同样的拦截)。


Q15. 🚩 "首尾帧桥接"在生产上真的生效吗? ​

【考点】 硬核核实题。这是本套材料里最"值钱"的一处诚实。

【口述答案】不是普遍生效。 我说三个层次: 第一,通道层的强制降级。 腾讯云 VOD 在请求里存在任何参考图或参考视频的时候,会把 FirstFrame / LastFrame 强制降级成 Reference。而我们的生产场景(带产品图、带模特图)基本上必然带参考图,所以**"真正的首尾帧锚"在多数场景里用不上**,实际主力是"参考图 + 提示词接续"。 第二,有的链路是刻意关闭桥接的。 爆款复刻的分段链路里显式写死了 needsBridgingToPrev: false——因为原片的首尾帧里带着旧产品、旧人脸,跟我们的替换目标冲突,硬接比不接更差。标准复刻和创意复刻走的是"显式降级版":尾帧以 reference_image 角色注入,并改写提示词。 第三,接口层是支持的。 适配器里首尾帧的参数、素材角色、first_last_frame 这些能力都实现了(筷子那条链路的 first_last_frame 也是真的),所以"支持首尾帧桥接"作为能力描述是成立的,作为默认生效路径是不成立的。

【备注讲解】 这道题的正确答法结构:"接口支持 → 通道降级 → 部分链路刻意关闭"。三层都讲清楚,面试官会认为你对第三方能力的边界有真实认知。千万不要嘴硬说"我们靠首尾帧保证了 100% 连贯"——读过腾讯 VOD 文档的面试官一句话就能问穿。

【代码依据】 backend/services/video/providers/tencentVod.ts:269-302(降级逻辑)、backend/services/assistantChatRemakeLong.ts:397-398(显式关闭桥接)、backend/services/standardRemake/standardRemakePrompt.ts(尾帧以 reference_image 注入)。


Q16. 供应商任务不存在(not_found)怎么判?为什么这个判定要特别小心? ​

【考点】 保守归一的意识。

【口述答案】 判定函数是 isProviderTaskNotFoundError,它会先剥掉响应里随机生成的 RequestId 再匹配错误串——因为 RequestId 每次都不同,直接匹配会失败。 这个判定必须小心,原因是:判成 not_found 会导致直接退款 + 标失败。如果误判,用户的任务其实还在跑,我们却把钱退了、把任务标失败了,等于白送一条成片。 所以我们的原则是归一到保守的那一侧:不确定的时候当成 processing(继续轮询),而不是 not_found。宁可多转几轮,也不误退款。

【备注讲解】 "错误归一化要往"代价小"的那一侧归"是一条通用原则:处理失败时,要让"误判"的代价最小,而不是让"判断更准"。

【代码依据】 backend/services/queue/providerNotFound.ts:20-25(剥 RequestId 再匹配)、backend/services/queue/videoPollWorker.ts:1875-1891(四态归一化)。


Q17. 你从这个模块学到最重要的三件事? ​

【考点】 收尾总结题,考察抽象能力。

【口述答案】第一,调用付费外部服务之后,第一件事是把"我调用过了"持久化。 我们的段状态里专门有 generated 这个中间态,就是为了这个——它保护的不是状态正确性,是已经花掉的几十块钱。 第二,能力常量写死会让新接入的上游"白给"。 Seedance 2.5 支持 30 秒和 50 张参考图,但因为旧代码把 15 秒/9 张图写死了,用户选了 2.5 也拿不到它的能力,而且不报错。所有"上游能做什么"必须收敛到一个能力表里。 第三,第三方能力要用"实测行为"而不是"文档能力"来判断。 腾讯云文档写支持首尾帧,但只要有参考图就会被降级——这是我们读代码才发现的。做 AI 应用接入,上游的"参数互斥/隐式降级"是最大的坑源。

【备注讲解】 加一句更有分量的话:"这三件事里,第一件是钱、第二件是架构、第三件是认知方式——如果只能留一句,我会留第三句,因为它能防止前两类问题再发生。"


Q18. 口述速记版(90 秒) ​

视频生成这块,我们历史接入过 6 个供应商(腾讯云 VOD、火山 Ark、两个 Seedance 网关、筷子、HappyHorse),现在新任务只走腾讯云 VOD。老实现一个都没删,因为轮询是拿数据库里的 provider 取实现的,删掉会让在途任务"钱付了、片子拿不到"。 抽象是一个 4 方法的 VideoProvider 接口(createTask / queryStatus / poll / generateVideo),但路由已经退化成恒等函数——今天它服务的是"按历史任务号回捞实现",不是动态选源。能力差异收敛在 modelCapabilities 里,调用方只问"这个模型能做什么",不认模型名。 多段一致性有四层:上一段尾帧 → 本段首帧锚、尾帧定格约束、音色锚(第一段抽 6 秒人声给后续段共用)、提示词承接块;再加参考图身份锁(标签必须与下发图片严格同序同长)。 但要诚实说:腾讯通道在有任何参考图时会把首尾帧强制降级成参考图,所以多数场景实际靠"参考图 + 提示词接续";爆款复刻链路甚至显式关闭桥接,因为原片的旧人脸会污染替换目标。 段的复用有指纹保护:段状态三态(不存在 / generated / done),生成完立刻落 generated 保护已花的钱;配置变更导致指纹不符就整批丢弃重跑,并且丢弃必须打 warn——重新花钱不能静默发生。 拼接是自己编排 ffmpeg(腾讯 VOD 只做生成和拼接后的超分),踩过"loudnorm + -shortest 吞掉段尾 90ms 人声"、"AAC 帧粒度和 24fps 互质导致拼接卡帧"这些坑,响度改成"先测量再线性增益"。 提示词有权威规则源,优先级就是数组顺序,硬约束一律置底(模型对末尾最敏感);中文场景的特殊处理有:@图片N 必须绑定业务语义、口播 3~5 字/秒自检、禁止模型渲染中文字幕(必乱码)改用 ASS 后期烧录、多音字同音替换。 生成方式 14 种,定价单一真相源是 service_pricing 表,模型按档位分价;批量生产按真实拼接秒数结算。

持续学习,持续构建。