1. 为什么“确定性渲染”是 AI 代理做视频的第一道生死线
过去大半年,我一直在折腾 AI 代理自动生成视频这件事。从最早的“让模型写一段脚本,再调 FFmpeg 拼几段素材”,到后来尝试用模板引擎批量套图,中间踩的坑能写满一个笔记本。直到接触 HyperFrames 这个思路,我才意识到一个被大多数人忽略的根本问题:AI 代理做视频,最怕的不是画面丑,而是每次跑出来的结果不一样。
你可能会说,视频渲染有点差异不是很正常吗?帧率抖动、编码器版本不同、字体缺失,这些确实会导致输出有细微差别。但对于 AI 代理来说,这恰恰是致命的。代理的工作模式是“执行动作 → 观察结果 → 决定下一步”,如果同一个输入每次产出的视频都不一样,代理就无法建立稳定的因果判断。它没法知道是自己的决策出了问题,还是渲染环境在捣鬼。这就像你教一个新人做菜,同样的食材和步骤,今天咸了明天淡了,他永远学不会。
HyperFrames 的核心主张就是解决这个问题:把视频渲染变成一个确定性的、可复现的过程。它的基本思路是用 HTML 作为视频的“源描述”,通过一套受控的渲染管线,把 HTML 页面逐帧转成视频。为什么选 HTML?因为 HTML 本身就是声明式的、可版本控制的、结构清晰的。一个 HTML 文件加上固定的 CSS 和资源,在任何符合规范的浏览器里渲染出来的布局应该是一致的。这就给“确定性”打下了基础。
这个框架适合谁?我认为有三类人值得重点关注。第一类是正在做 AI 代理自动化内容生产的开发者,尤其是那些需要代理自己判断“视频做得好不好”的场景。第二类是做批量视频生成的技术团队,比如电商主图视频、数据报告动画、教育课件视频,这些场景对一致性要求极高。第三类是研究 AI 与多媒体交叉方向的同学,想理解怎么把 Web 技术栈和视频管线打通。哪怕你只是好奇“HTML 怎么变成视频”,这篇文章也能给你一条能跑通的路径。
我接下来会从整体设计思路、核心细节、实操流程、常见问题四个维度,把 HyperFrames 这套东西拆开讲清楚。中间会穿插我自己跑通的最小可行方案,以及那些文档里不会写的坑。
2. HyperFrames 整体架构与设计取舍
2.1 为什么用 HTML 而不是直接操作视频帧
传统视频生成有两条路。一条是直接用 FFmpeg 的滤镜链拼素材,另一条是用 Canvas 或 WebGL 逐帧绘制再编码。前者的问题是表达能力有限,复杂的排版、文字动画、CSS 过渡很难用滤镜描述清楚;后者的问题是代码量大,而且 Canvas 的绘制结果在不同设备上可能有细微差异。
HyperFrames 选了第三条路:用 HTML + CSS 描述每一帧的画面,然后用无头浏览器逐帧截图,最后用 FFmpeg 把图片序列编码成视频。这个思路其实不新鲜,早年做数据动画的人就用过类似方法。但 HyperFrames 把它系统化了,加上了确定性控制层。
为什么 HTML 适合做这件事?因为 HTML 的渲染模型是“声明式 + 确定性”的。给定相同的 DOM 结构、相同的 CSS 规则、相同的字体文件和相同的浏览器版本,渲染结果在像素级别上应该是可复现的。这比用代码逐帧画 Canvas 要可靠得多。而且 HTML 的生态极其丰富,你想做个文字淡入、图表增长、图片轮播,CSS 动画和 JS 时间轴都能轻松搞定。
但这里有个关键前提:必须锁定渲染环境。浏览器版本、字体、甚至操作系统的字体渲染引擎都会影响最终像素。HyperFrames 的做法是推荐使用固定版本的 Chromium,并且把字体文件打包进项目,通过@font-face显式加载,而不是依赖系统字体。这一步不做,确定性就是空谈。
2.2 确定性渲染管线的三个关键控制点
要让整个管线可复现,HyperFrames 在三个地方做了强控制。
第一个控制点是时间轴驱动。视频的本质是时间轴上的帧序列。HyperFrames 不用requestAnimationFrame这种跟真实时间挂钩的机制,而是用一个虚拟时钟。代理或脚本告诉渲染器“现在到第 3.5 秒”,渲染器就把页面状态设置到第 3.5 秒对应的样子,然后截图。这样每一帧的生成都是独立的、可重复的。CSS 动画和 JS 动画都需要接入这个虚拟时钟,不能用Date.now()或performance.now()直接驱动。
第二个控制点是资源内联与哈希校验。所有图片、字体、CSS、JS 都尽量内联到 HTML 里,或者放在项目目录下用相对路径引用。HyperFrames 会对每个资源做哈希,渲染前校验哈希是否匹配。如果某个字体文件被替换了,哈希对不上,渲染直接报错而不是悄悄产出不一样的视频。这个设计很硬核,但确实能防止“环境漂移”导致的不可复现。
第三个控制点是编码参数固定。FFmpeg 的编码器有很多参数会影响输出,比如-crf、-preset、-g、-pix_fmt。HyperFrames 会把这些参数写死在配置里,并且记录 FFmpeg 的版本号。不同版本的 FFmpeg 即使参数相同,输出也可能有细微差异。所以生产环境里最好把 FFmpeg 二进制也固定下来,不要用系统自带的。
注意:确定性不等于“绝对相同”。不同硬件上的浮点运算可能有极微小差异,导致个别像素不同。但对于 AI 代理的判断来说,只要宏观结果一致(比如文字位置、颜色、动画节奏),就足够了。追求比特级一致需要更极端的控制,成本很高,一般场景没必要。
2.3 与 FFmpeg 的分工边界
HyperFrames 和 FFmpeg 的关系很清晰:HyperFrames 负责“生成每一帧的画面”,FFmpeg 负责“把帧序列编码成视频文件”。两者通过图片序列或原始帧数据交接。
为什么不让 HyperFrames 直接调用编码器?因为 FFmpeg 在编码这块已经足够成熟,没必要重复造轮子。而且把渲染和编码解耦之后,调试会方便很多。你可以先渲染出 PNG 序列,肉眼检查每一帧对不对,确认没问题再编码。如果渲染和编码耦合在一起,出了问题你都不知道是画面错了还是编码错了。
FFmpeg 这边我常用的命令是这样的:
ffmpeg -y -framerate 30 -i frame_%06d.png \ -c:v libx264 -preset slow -crf 18 \ -pix_fmt yuv420p -movflags +faststart \ output.mp4这里有几个参数值得解释。-framerate 30表示输入图片序列按每秒 30 帧解读,这个值必须和 HyperFrames 渲染时的帧率一致,否则视频速度会不对。-crf 18是质量参数,数值越小质量越高,18 已经接近视觉无损。-pix_fmt yuv420p是兼容性最好的像素格式,不设置的话有些播放器会显示异常。-movflags +faststart把元数据移到文件头部,方便网络播放。
提示:如果你的视频要上传到某些平台,它们对编码参数有要求,比如必须 H.264、必须 yuv420p、音频 AAC。提前确认好,免得渲染完了还要转码。
3. 核心细节拆解:从 HTML 到视频的每一步
3.1 HTML 结构设计:为逐帧渲染做准备
用 HyperFrames 做视频,HTML 的写法和普通网页不太一样。普通网页是“用户滚动、点击、交互”,视频是“时间轴推进,画面变化”。所以你的 HTML 需要围绕“时间”来组织。
我通常会把整个视频拆成几个场景(scene),每个场景是一个独立的<section>,用绝对定位叠在一起。然后通过 JS 控制哪个场景可见、场景内的元素处于什么状态。比如:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <style> body { margin: 0; width: 1920px; height: 1080px; overflow: hidden; } .scene { position: absolute; inset: 0; opacity: 0; } .scene.active { opacity: 1; } .title { font-size: 96px; transform: translateY(40px); opacity: 0; } .title.show { transform: translateY(0); opacity: 1; transition: all 0.6s ease; } </style> </head> <body> <section class="scene" id="scene1"> <h1 class="title">第一幕标题</h1> </section> <section class="scene" id="scene2"> <h1 class="title">第二幕标题</h1> </section> </body> </html>关键点是:所有动画状态都由 class 控制,而不是由 CSS 动画自动播放。因为自动播放的动画跟真实时间绑定,没法精确控制到某一帧。用 class 切换的话,渲染器可以在任意时间点设置好 class,然后截图。
这里有个细节:CSS 的transition在截图时可能处于中间状态。如果你在t=0.3s时给元素加上.show,然后立刻截图,浏览器可能还没完成过渡。解决办法是用transition: none配合手动计算中间状态,或者干脆不用 transition,用 JS 根据时间计算每个元素的transform和opacity。后者更可控,但代码量大一些。
3.2 虚拟时钟与帧调度
虚拟时钟是 HyperFrames 确定性的核心。它的逻辑很简单:渲染器维护一个当前时间t,每次渲染前把t设置到目标值,然后触发页面更新,再截图。
页面怎么知道当前时间?通常有两种方式。一种是通过window.__setTime(t)这样的全局函数,让页面自己根据t更新所有元素状态。另一种是用 CSS 自定义属性,比如--t: 0.5,然后 CSS 里用calc()根据--t计算位置。第二种方式更声明式,但表达能力有限,复杂动画还是得用 JS。
我自己的做法是混合:简单的位移、透明度用 CSS 变量,复杂的路径动画、数字滚动用 JS。页面里会有一个update(t)函数,渲染器每帧调用一次,传入当前时间。这个函数负责把所有元素调整到t时刻应有的状态。
帧率方面,常见的是 24、25、30、60。做数据动画一般 30 就够了,做游戏录屏或高速运动才需要 60。帧率越高,渲染和编码的时间越长。一个 10 秒的 30fps 视频需要渲染 300 帧,每帧截图假设 100ms,光截图就要 30 秒。如果 60fps 就是 600 帧,时间翻倍。所以帧率要按需选择,不要盲目追高。
注意:虚拟时钟的精度要足够。如果你用浮点数累加
t += 1/30,跑几百帧后会有累积误差。建议用整数帧号计算时间:t = frameIndex / fps。这样每一帧的时间都是精确的。
3.3 截图与编码的衔接细节
截图这一步,HyperFrames 通常用无头浏览器提供的截图接口。以 Chromium 为例,可以通过 DevTools Protocol 的Page.captureScreenshot拿到 PNG 数据。这里有几个坑。
第一个坑是截图时机。设置完页面状态后,不能立刻截图,要等浏览器完成一次渲染。通常的做法是等一个requestAnimationFrame,或者用Page.captureScreenshot的fromSurface参数确保捕获的是合成后的画面。如果不等,可能截到旧画面。
第二个坑是图片格式。PNG 是无损的,适合做中间格式,但文件大。300 帧 1080p 的 PNG 可能有好几个 GB。如果磁盘空间紧张,可以用 JPEG,质量设到 95 以上,肉眼几乎看不出差别,文件小很多。但 JPEG 是有损的,如果你追求极致确定性,还是用 PNG。
第三个坑是命名和排序。FFmpeg 读图片序列时按文件名排序,所以文件名必须补零对齐,比如frame_000001.png到frame_000300.png。如果写成frame_1.png、frame_2.png,FFmpeg 会按字典序排成frame_1、frame_10、frame_100,顺序全乱。
编码时还要注意色彩空间。浏览器截图通常是 sRGB,FFmpeg 默认可能按 BT.601 或 BT.709 解读。如果不对齐,颜色会偏。可以在 FFmpeg 里加-colorspace bt709 -color_primaries bt709 -color_trc bt709显式指定。这个细节很多人忽略,导致视频颜色和网页里看到的不一样。
3.4 资源管理与确定性校验
前面提到资源哈希校验,这里展开说一下怎么做。
项目目录大概长这样:
project/ index.html assets/ font-main.woff2 logo.png style.css manifest.jsonmanifest.json里记录每个文件的 SHA-256 哈希:
{ "index.html": "a1b2c3...", "assets/font-main.woff2": "d4e5f6...", "assets/logo.png": "g7h8i9...", "assets/style.css": "j0k1l2..." }渲染前,脚本会重新计算哈希并比对。任何一个文件变了,就报错退出。这个机制在团队协作时特别有用,防止有人改了 CSS 忘了同步,导致渲染结果和预期不符。
字体这块要特别小心。很多系统字体在不同机器上渲染不一样,甚至同一台机器不同版本也不一样。所以必须把字体文件打包进项目,用@font-face加载。不要用font-family: sans-serif这种依赖系统的写法。中文字体文件很大,可以用子集化工具只保留用到的字符,能把几 MB 压到几十 KB。
4. 完整实操流程:从零跑通一个 HyperFrames 视频
4.1 环境准备与依赖安装
先列一下我用的环境。操作系统是 Ubuntu 22.04,但其实 macOS 和 Windows 也能跑,只是路径和命令略有不同。核心依赖有三个:Node.js、Chromium、FFmpeg。
Node.js 建议用 18 或 20 的 LTS 版本。Chromium 用 Puppeteer 自带的就行,它会下载一个固定版本的 Chromium,正好符合确定性要求。FFmpeg 建议用静态编译版,版本固定在 6.x,不要用系统包管理器装的,因为版本可能不一致。
安装命令大概是这样:
# 初始化项目 mkdir hyperframes-demo && cd hyperframes-demo npm init -y # 安装 Puppeteer(自带 Chromium) npm install puppeteer # 下载 FFmpeg 静态版(以 Linux 为例) wget https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz tar -xf ffmpeg-release-amd64-static.tar.xz mv ffmpeg-*-amd64-static ffmpeg export PATH=$PWD/ffmpeg:$PATH # 验证 ffmpeg -versionPuppeteer 下载 Chromium 可能比较慢,可以设置国内镜像加速。具体方法这里不展开,搜一下就有。
提示:如果你在 CI/CD 环境里跑,建议把 Chromium 和 FFmpeg 都缓存起来,不要每次构建都重新下载。可以用 Docker 镜像把环境固化,这样确定性更有保障。
4.2 编写渲染脚本
渲染脚本的核心逻辑是:启动浏览器 → 打开页面 → 循环设置时间并截图 → 关闭浏览器。我用 Node.js 写一个最小版本:
const puppeteer = require('puppeteer'); const fs = require('fs'); const path = require('path'); const FPS = 30; const DURATION = 5; // 秒 const WIDTH = 1920; const HEIGHT = 1080; const OUTPUT_DIR = './frames'; async function render() { if (!fs.existsSync(OUTPUT_DIR)) fs.mkdirSync(OUTPUT_DIR); const browser = await puppeteer.launch({ headless: 'new', args: [`--window-size=${WIDTH},${HEIGHT}`] }); const page = await browser.newPage(); await page.setViewport({ width: WIDTH, height: HEIGHT, deviceScaleFactor: 1 }); const htmlPath = 'file://' + path.resolve('./index.html'); await page.goto(htmlPath, { waitUntil: 'networkidle0' }); // 等待字体加载完成 await page.evaluate(() => document.fonts.ready); const totalFrames = FPS * DURATION; for (let i = 0; i < totalFrames; i++) { const t = i / FPS; await page.evaluate((time) => { if (window.update) window.update(time); }, t); // 等一帧确保渲染完成 await page.evaluate(() => new Promise(r => requestAnimationFrame(r))); const filename = `frame_${String(i).padStart(6, '0')}.png`; await page.screenshot({ path: path.join(OUTPUT_DIR, filename), type: 'png' }); if (i % 30 === 0) console.log(`Rendered ${i}/${totalFrames}`); } await browser.close(); console.log('Done.'); } render().catch(console.error);这个脚本里几个关键点。deviceScaleFactor: 1确保截图是 1:1 像素,不会因为高分屏变成 2 倍。document.fonts.ready等待字体加载,否则可能截到 fallback 字体。每帧截图前等一个requestAnimationFrame,确保页面状态已经应用。
页面里的update(t)函数需要你自己实现。一个简单的例子:
window.update = function(t) { const scene1 = document.getElementById('scene1'); const scene2 = document.getElementById('scene2'); const title1 = scene1.querySelector('.title'); const title2 = scene2.querySelector('.title'); // 0-2秒:场景1,标题淡入 if (t < 2) { scene1.classList.add('active'); scene2.classList.remove('active'); const progress = Math.min(t / 0.6, 1); title1.style.opacity = progress; title1.style.transform = `translateY(${40 * (1 - progress)}px)`; } // 2-5秒:场景2,标题淡入 else { scene1.classList.remove('active'); scene2.classList.add('active'); const progress = Math.min((t - 2) / 0.6, 1); title2.style.opacity = progress; title2.style.transform = `translateY(${40 * (1 - progress)}px)`; } };这个update函数是纯函数,给定t就能算出所有元素的状态。没有副作用,没有随机数,没有依赖真实时间。这就是确定性的来源。
4.3 编码与后处理
渲染完图片序列后,用 FFmpeg 编码:
ffmpeg -y -framerate 30 -i frames/frame_%06d.png \ -c:v libx264 -preset slow -crf 18 \ -pix_fmt yuv420p -movflags +faststart \ -colorspace bt709 -color_primaries bt709 -color_trc bt709 \ output.mp4如果视频需要加背景音乐,可以再加一个音频输入:
ffmpeg -y -framerate 30 -i frames/frame_%06d.png \ -i bgm.mp3 \ -c:v libx264 -preset slow -crf 18 \ -c:a aac -b:a 192k \ -pix_fmt yuv420p -movflags +faststart \ -shortest \ output.mp4-shortest表示以较短的流为准,防止音频比视频长导致视频结束后还有声音。
编码时间取决于视频长度和机器性能。一个 10 秒 1080p 30fps 的视频,用preset slow可能要一两分钟。如果追求速度,可以用preset medium或fast,质量差异不大,但文件会大一些。
注意:
-crf和-preset的组合会影响输出。crf 18+preset slow是质量优先的配置。如果要做流媒体,可以用crf 23+preset medium,文件小很多,肉眼也够看。
4.4 确定性验证:跑两次比对结果
做完上面这些,怎么验证确定性?最简单的办法是跑两次渲染,比对输出文件的哈希。
# 第一次 node render.js ffmpeg -y -framerate 30 -i frames/frame_%06d.png -c:v libx264 -crf 18 -pix_fmt yuv420p output1.mp4 sha256sum output1.mp4 # 清空帧目录,第二次 rm -rf frames node render.js ffmpeg -y -framerate 30 -i frames/frame_%06d.png -c:v libx264 -crf 18 -pix_fmt yuv420p output2.mp4 sha256sum output2.mp4如果两个哈希一样,说明整个管线是确定性的。如果不一样,就要排查是渲染阶段还是编码阶段的问题。可以先比对 PNG 序列的哈希,定位到具体哪一帧开始出现差异。
实测下来,只要环境固定、字体内联、不用随机数,PNG 序列的哈希是可以做到完全一致的。MP4 的哈希有时候会因为编码器内部的多线程调度而有细微差异,但画面内容是一致的。如果要求 MP4 哈希也一致,可以给 FFmpeg 加-threads 1强制单线程,但编码速度会慢很多。
5. 常见问题与排查技巧实录
5.1 渲染结果不一致的排查思路
这是最常见的问题,也是 HyperFrames 要解决的核心问题。排查时按以下顺序检查。
先看字体。如果两次渲染的文字位置或字形不一样,大概率是字体问题。检查是否用了系统字体,是否所有字体都通过@font-face加载,字体文件是否被修改。可以用document.fonts.check()在页面里验证字体是否加载成功。
再看随机数。页面里有没有用Math.random()、Date.now()、performance.now()?这些都会导致每次渲染不一样。全部替换成基于虚拟时钟的确定性计算。
然后看图片加载。如果图片是异步加载的,可能某一帧截图时图片还没加载完。解决办法是在渲染开始前等所有图片complete,或者把图片转成 base64 内联到 HTML 里。
最后看浏览器版本。不同版本的 Chromium 渲染引擎可能有差异。确保每次渲染用的是同一个 Chromium 二进制。Puppeteer 自带的 Chromium 版本是固定的,但如果你手动指定了executablePath,就要自己保证版本一致。
5.2 性能优化:让渲染快起来
渲染慢是另一个痛点。一个 30 秒的视频,如果每帧截图要 200ms,光截图就要 3 分钟,加上编码可能 5 分钟以上。优化可以从几个方向入手。
降低分辨率。如果最终输出是 1080p,但你的素材和文字在 720p 下也够清晰,可以先按 720p 渲染,最后用 FFmpeg 放大。不过放大有画质损失,慎用。
减少帧数。如果视频内容变化不快,可以用 24fps 甚至 15fps。人眼对 24fps 的动画已经觉得流畅了,15fps 在慢速内容下也能接受。
并行渲染。把视频拆成几段,每段用独立的浏览器实例渲染,最后拼接。比如 30 秒的视频拆成 3 段 10 秒,三台机器同时跑,时间降到三分之一。但要注意段与段之间的衔接,确保时间轴连续。
用 JPEG 代替 PNG。JPEG 截图和写入的速度比 PNG 快不少,文件也小。质量设到 95 以上,肉眼几乎看不出差别。如果对确定性要求不是比特级,JPEG 完全够用。
跳过空白帧。如果某些帧的内容和前一帧完全一样,可以复用前一帧的图片,不用重新截图。这个优化需要你在update函数里判断状态是否变化,实现起来稍复杂,但效果显著。
5.3 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 视频速度不对 | 帧率不匹配 | 检查渲染帧率和 FFmpeg-framerate | 两者设为相同值 |
| 颜色偏暗或偏亮 | 色彩空间不一致 | 对比网页和视频截图 | FFmpeg 加-colorspace bt709 |
| 文字位置偏移 | 字体未加载 | 页面里检查document.fonts | 内联字体,等待fonts.ready |
| 某帧画面空白 | 截图时机太早 | 检查该帧的页面状态 | 截图前等requestAnimationFrame |
| 图片序列顺序乱 | 文件名未补零 | 查看文件名 | 用%06d补零 |
| 视频无法播放 | 像素格式不兼容 | 用ffprobe查看 | 加-pix_fmt yuv420p |
| 渲染结果每次不同 | 有随机数或真实时间 | 搜索Math.random、Date.now | 改用虚拟时钟 |
| 编码速度太慢 | preset 太慢 | 查看 FFmpeg 日志 | 改用preset medium |
5.4 几个我踩过的坑
第一个坑是CSS transition 的中间状态。我一开始用transition做淡入,结果截图时经常截到半透明的中间态,导致视频里文字忽明忽暗。后来全部改成 JS 计算opacity,问题消失。如果你非要用 transition,可以在截图前强制transition: none,设置好最终状态,截完再恢复。
第二个坑是图片的object-fit。不同浏览器对object-fit的实现有细微差异,尤其是cover模式下裁剪的位置。如果视频里有图片,建议用background-image+background-size: cover,行为更一致。
第三个坑是FFmpeg 的-shortest。如果音频比视频短,-shortest会让视频也提前结束。我一开始没注意,做出来的视频比预期短了一截。后来改成先确认音频长度,再调整视频时长,或者用-apad给音频补静音。
第四个坑是磁盘空间。1080p PNG 每帧大概 2-3MB,300 帧就是 600-900MB。如果视频更长,很容易把磁盘写满。建议用 JPEG,或者渲染完一段就编码一段,及时清理中间文件。
6. 这套框架还能怎么扩展
跑通基础流程之后,我试过几个扩展方向,效果不错。
一个是接入 AI 代理做自动内容生成。代理根据数据生成 HTML 模板,填充内容,然后调 HyperFrames 渲染。因为渲染是确定性的,代理可以反复调整 HTML 直到满意,每次渲染结果都可预期。这比让代理直接操作视频编辑软件要可靠得多。
另一个是批量生成个性化视频。比如给每个用户生成一份年度报告视频,模板固定,数据不同。HyperFrames 的确定性保证了模板渲染不会出意外,只需要替换数据源就行。我实测过一批 100 个视频,渲染结果全部一致,没有出现某个视频字体错乱或布局崩坏的情况。
还有一个方向是把渲染管线容器化。用 Docker 把 Node.js、Chromium、FFmpeg、字体全部打包进一个镜像,任何机器上跑出来的结果都一样。这对团队协作和 CI/CD 特别有用。镜像可以推到私有仓库,每次渲染拉取固定 tag,彻底杜绝环境漂移。
我个人在实际操作中的体会是,HyperFrames 这套思路的价值不在于技术有多复杂,而在于它把“确定性”这个要求贯彻到了每一个环节。从 HTML 的写法、虚拟时钟的设计、资源的哈希校验,到 FFmpeg 参数的固定,每一步都在为可复现性服务。如果你也在做 AI 代理相关的视频生成,建议尽早把确定性纳入设计,不然后期排查不一致问题会非常痛苦。