Spring Boot + LLM 工程化:把短视频流水线拆成 16 个独立角色的踩坑记录

📅 2026/6/16 11:14:32
Spring Boot + LLM 工程化:把短视频流水线拆成 16 个独立角色的踩坑记录
写在前面如果你最近也在用 LLM 做内容流水线,大概率踩过这几个坑:prompt 链一长就脆,任何一步出错整条链报废画面和字幕对不上,差 1-2 秒字幕就飘改一个细节得重跑全流程,中间产物没落库平台数据回写不知道怎么用我业余时间做了一个开源项目Auteur,把这些问题逐个解决了。这篇文章把我踩过的坑和解决思路一次性写出来,代码 配置都是真实可跑的。仓库地址:https://github.com/nxin-github/Auteur在线 demo:https://nxin-github.github.io/Auteur/技术栈:Spring Boot 3.3 JPA Flyway MySQL 8.0 Java 21 Vue 3 ffmpeg / Remotion一、为什么不能用 prompt chain最早做的就是 LangChain 风格的 chain:brainstorm → script → storyboard → image → voice → video跑了一周发现几个事:LLM API 本来就不稳定,链越长越脆中间产物只在内存里,UI 看不到也没法人工介入改一处要全跑,5 分钟起步加自审到 chain 里要么改框架要么塞 if,丑根本问题是 chain 把流程和状态耦合在一起。后来推翻重做,每个 AI 角色一个独立 Spring Service,角色之间不直接互相调用,全部通过 DB 表解耦。读上游表,写下游表。topic选题 ↓ script脚本 ↓ storyboard_shot分镜,每镜一行 ↓ image_asset图片资产 ↓ voice_asset配音资产 ↓ video_asset合成视频 ↓ published_video平台回写的真实数据 ↓ weekly_review周复盘每张表对应一个产物,任何一段失败可以单独重跑,不影响别的。中间产物全部落库,UI 上能看能改。ServicepublicclassScriptWriterService{// 读上游:topic 表// 写下游:script 表publicScriptResultwrite(LongtopicId){...}}ServicepublicclassScriptCriticService{// 读:script 表// 写:script_review 表 触发 ScriptWriterService 重写一稿publicCriticResultreview(LongscriptId){...}}加新角色 加新 Service 新表,对其它角色零侵入。⚠️ 踩坑:第二版我还试过用 Spring Events 解耦——不行,事件链一长跟 chain 没本质区别,异步事件链调试起来更难。回到产物落库 显式触发,基于 DB 状态机才是正解。二、自审反馈环:让 LLM 给自己批稿子LLM 输出会漂移这是事实。常见处理方式:换更大的模型 → 贵写更细的 prompt → 反噬,prompt 越复杂 LLM 越抓不住重点加 retry → 不知道为什么错Auteur 的方案:给关键角色配一个自审角色。publicScriptResultwriteWithCritic(LongtopicId,intthreshold){ScriptResultdraftscriptWriter.write(topicId);CriticResultreviewscriptCritic.review(draft);if(review.score()threshold){returnscriptWriter.rewriteWithFeedback(draft,review.feedback());}returndraft;}调试这个 loop 时踩了三个坑,每个都让我返工过:坑 1:自审 prompt 必须找问题导向不能打分导向。让 LLM 列出 3 个最大的问题,比让它给个 80 分有用得多。打分版本会出现凑分数——看见草稿写得不错就给个高分混过去。坑 2:重写最多 1 次,再不行放过。我见过 LLM 把一个本来还行的剧本越改越奇怪,钻牛角尖比放过去还糟。坑 3:自审反馈环必须是闭合的小循环,不能让自审失败传染下游。如果自审本身挂了(LLM 返回格式不对),跳过自审走原稿,不要让整个流水线挂在自审上。目前编剧、摄影、美术三个角色都接了自审。生产跑下来编剧自审能把逻辑断层和信息密度过低两类问题捕住八成以上。三、镜头时长锚定:画面与字幕一帧不差这是我自己最得意的一个设计。问题:普通流水线让 LLM 给每个镜头估时长(比如 3.5s),后端按这个时长拼图。但 LLM 估的时长跟真实 TTS 音频对不上,剪出来字幕飘 1-2 秒。我试过加更详细的 prompt 让 LLM 估准。没用,它压根不知道你的 TTS 模型每秒念几个字。解法:让摄影指导给每个镜头一段 anchor_text(必须是脚本里的连续子串),后端 SRT 解析后在音频时间轴上反查这段文本的真实秒数:publicDurationresolveShotDuration(Shotshot,ListSrtCuesrtCues){Stringanchornormalize(shot.anchorText());SrtRangerangesrtCues.stream().filter(cue-normalize(cue.text()).contains(anchor)).findFirst().orElseThrow();returnDuration.between(range.start(),range.end());}LLM 不再负责估算,只负责指认。LLM 擅长的是语义匹配,不是数值估算——把它放在它擅长的位置上。校验链写得比较狠:✅ anchor 必须真的是脚本子串(normalize 之后比对,去标点 / 全半角 / 大小写)✅ 相邻 shot 的 anchor 在脚本里位置必须单调递增(防 LLM 把镜头顺序搞乱)⚠️ 没命中的镜头标anchor_matchfalse,视频还能渲,但日志和 UI 都会提示踩坑:normalize 一定要包括去全半角和繁简体,不然 LLM 输出的 anchor 跟脚本对不上。我一开始只去了标点,匹配率惨不忍睹。四、Agent 工具系统:自然语言驱动整条流水线光有流水线还不够,用户得点 N 个按钮才能把活干完。所以又加了一个 Agent 聊天工作台。底层是带工具调用 审批门槛 Skill 上下文加载的对话循环。几个工程关键点:1. 工具自动注册ComponentpublicclassStoryboardTools{Tool(nameregenerate_image_for_shot,description重新为指定 shot 生成图片)publicRunRefregenerateImageForShot(LongshotId,StringstylePatch){...}}ToolRegistry启动时扫描所有Tool注解,自动注册到 Agent 上下文,不用改 Agent 主循环代码。这是 Spring 注解扫描在 LLM 工程里的一个绝佳应用场景。2. 写操作必须实现审批接口publicinterfacePreviewableHandlerT{PreviewCardpreview(Targs);// 给前端的审批卡Objectexecute(Targs);// 用户点确认后才跑}前端弹一张即将做这件事,确认吗的卡片,用户按一下才执行。这是把 LLM 不可控性挡在副作用之外的最后一道闸。踩坑:我一开始没加这个,调试时 Agent 自作主张把一个预设的 prompt 改了,改回去花了我半小时。从那以后所有写操作必须走审批门槛。3. Skill 按需加载,不是 system prompt 塞所有上下文把调整内容“触发流水线”“创建选题”“改预设”“编辑文本” 5 类剧本写成 markdown 放在agent/skills/,Agent 自己根据当前对话决定加载哪份。避免 system prompt 越写越长占满 context window。4. 长任务异步 runId 轮询生图、合成视频这类任务要几十秒。Agent 工具返回runId,前端轮询GET /api/runs/{id}看进度。Agent 不阻塞在长任务上。五、模型 ID 不在代码里写死这是后期重构的一个关键决策。早期我把 LLM 模型 ID 散落在各 Service 里:// ❌ 散落各处的字面量privatestaticfinalStringSCRIPT_MODELdeepseek-chat;LlmCallSpec.builder().model(gpt-4o-mini).build();后来改型号、做 A/B、降级模型时全是 grep 改代码。改成ModelRegistry:ServicepublicclassModelRegistry{publicStringmodelFor(Stringstep){...}publicStringmodelOrDefault(StringpresetValue,Stringstep){...}}所有 LLM / 图像 / Agent 模型走ModelRegistry.modelFor(step)读app_config表(categorymodel,key 形如auteur.model.step),前端配置 → AI 模型页面统一编辑。预设可覆盖的步骤(脚本/分镜/批评/脑暴/图像主模型)用modelOrDefault(presetValue, step),preset 优先,本页兜底。加新流水线步骤的标准动作:写下一个V*__model_step.sql迁移,INSERT IGNORE注册 UPDATE COALESCE灌默认值在ModelRegistry.KNOWN_STEPS列表里加上新 step,启动自检会校验Service 注入ModelRegistry,调modelFor(step)或modelOrDefault(presetValue, step)前端ModelConfig.vue的GROUPS数组里把新 step 加进合适分组六、本地优先 可降级任何外部依赖缺失,后端不能挂。这条对开源项目特别重要——你不知道用户机器上装了啥。依赖没配会怎样火山 TOS走本地路径 /api/files/...静态服务火山 TTS配音 disabled,前端提示Jamendo BGMBGM 不推荐,制片照常合成Remotion走纯 ffmpeg 路径LLM 网关OpenAI 兼容协议,vLLM / DeepSeek / 智谱 / Anthropic 都行写起来到处都是兜底,但这事不能省。七、几个让我返工过的踩坑1..gitignore必须用前导/锚定storage/会递归忽略所有层级——不光忽略backend/storage/产物目录,还会一并忽略backend/src/main/java/com/auteur/storage/业务包。CI 编译失败你都不知道为啥。正确写法:/storage/,只匹配 git 根下的同名目录。.dockerignore语义不同(用 Gofilepath.Match不递归),所以同样写法在 dockerignore 里没问题——这种语义差异让我本地 docker build 通过、CI 编译挂。改完.gitignore用git check-ignore -v file抽查关键源码目录验证。2. Spring Bootddl-auto: validate严格校验加字段忘 Flyway migration → 启动失败。强制走 Flyway,不要直接改 entity 让 JPA 自动建表。正确做法:在backend/src/main/resources/db/migration/加V下一个数字__描述.sql数字递增不能跳号,描述用下划线分隔同时改 entity 类Flyway 启动时自动执行3. alpine 容器里localhost优先解析 IPv6但 nginx 默认只监听 IPv4 → healthcheck 永远失败。改成127.0.0.1立刻好。4. Remotion 不支持file://协议本地静态文件得走 HTTP URL,配auteur.video.remotion.public-base-url拼成http://host:port/api/files/...。数据回写驱动复盘extension/目录是个 Chrome 扩展,插到抖音 / B 站 / 视频号 / 快手 的创作者后台,自动抓播放数和完播率 POST 回 Auteur,落published_video表。WeeklyReviewService每周根据这些数据算哪些题材和钩子组合表现好,给下周的选题脑暴一份权重表。下一次选题策划就读这份周报。流水线越跑越懂受众。怎么跑起来gitclone https://github.com/nxin-github/AuteurcdAuteurcp.env.example .envdockercompose up-d--build3-5 分钟,打开 http://localhost:5174,右上角切到 admin → 系统设置 → 填 LLM key(OpenAI 兼容协议都行),其它依赖留空就降级。最后完全开源 MIT 协议,随便用、随便改、随便商用。仓库:https://github.com/nxin-github/Auteur在线 demo:https://nxin-github.github.io/Auteur/给 AI 编程助手的 onboarding cheat sheet:CLAUDE.md(项目根目录)LLM 工程化是个大坑,这个项目把我四个月里踩过的坑基本全暴露了。希望对你有用。如果觉得思路有意思,star 是对独立开发者最直接的鼓励。有问题欢迎评论或者 GitHub issue。