《猫猫钓游记》可爱+收集+钓鱼游戏试玩
2026-06-30
2026-07-02 0
最近在思考 LLM 多 Agent 协同的工程化落地,翻到一个叫 Auteur 的项目,作者用 Spring Boot 把"AI 生产中文短视频"这个场景拆成了 16 个独立 Service + 1 个 Agent 对话循环。读完源码觉得里面几个设计决策挺值得拿出来聊——特别是对所有"用 LLM 串多步流水线"的同学都有参考意义。

这篇不是项目介绍,是把里面的工程取舍单独拎出来分析。每个点都附独立可复用的代码模式,看完能直接搬到自己的 LLM 项目里。
「LLM 串流水线」最直觉的写法就是 chain:
复制代码// 反面教材:chain 写法
public Video generate(String topic) {
Script s = scriptWriter.write(topic);
Storyboard sb = storyboardArtist.draw(s);
List imgs = imageGen.render(sb);
Audio a = ttsService.speak(s);
return composer.merge(imgs, a);
}
这种写法在 demo 里没问题,工程化场景会很快崩。Auteur 作者在 README 里复盘了三个具体的痛点,跟我自己踩过的坑高度重合:
Auteur 的解法是:每个角色拆成独立 Service,角色之间不直接互相调用,全部通过数据库表解耦。
复制代码topic → script → storyboard_shot → image_asset → voice_asset
→ video_asset → published_video → weekly_review
每张表对应一个产物。读上游表,写下游表。任何一段失败可以单独重跑,不影响别的。
抽象出来的通用模式,可以直接搬到自己的 LLM 流水线项目里:
复制代码// 通用 LLM 流水线 Service 范式
@Service
public class StoryboardService {
private final ScriptRepository upstream; // 读上游表
private final StoryboardShotRepository self; // 写自己的表 public void run(Long topicId) {
Script s = upstream.findByTopicId(topicId);
List shots = llmDraw(s);
self.saveAll(shots); // 落库,下游异步消费
}
}
要点:
作者还提到一个有意思的细节:第二版他试过用 Spring Events 解耦,结果"事件链一长跟 chain 没本质区别,调试更难"。最后还是回到「产物落库 + 显式触发」。这个结论我蛮赞同——事件驱动在边界清晰的领域很好,但 LLM 流水线的边界本身就是模糊的,事件链一长根本理不清谁触发了谁。
LLM 生成的内容经常像草稿。最朴素的应对是换大模型 / 写更细 prompt。前者贵,后者反噬——prompt 越细 LLM 越抓不住重点。
Auteur 给关键角色(编剧、摄影、美术)配了一个自审 Service。骨架很简单:
复制代码public ScriptResult writeWithSelfReview(Long topicId) {
ScriptResult draft = scriptWriter.write(topicId);
CriticResult review = scriptCritic.review(draft);
if (review.score() < threshold) {
return scriptWriter.rewriteWithFeedback(draft, review.feedback());
}
return draft;
}
代码两行,但里面有三个工程化细节是踩出来的:
(1) 自审 prompt 必须"找问题"导向,不能"打分"导向
让 LLM"列出 3 个最大的问题"比让它"给个 80 分"有用得多。打分版本会出现"凑分数"现象——LLM 看见草稿写得不错就给个高分混过去。我自己也验证过这个:同一份草稿,"打分"版本均分 82,"找问题"版本能稳定挖出 2-3 个具体的修改点。
(2) 重写最多重跑 1 次
再不行就放过原稿。LLM 钻牛角尖比放过去还糟——会把一个本来还行的稿子改得越来越奇怪。这个在多轮 critic loop 里特别明显,一定要硬限。
(3) 自审失败必须降级
复制代码try {
review = scriptCritic.review(draft);
} catch (Exception e) {
log.warn("自审失败,使用原稿", e);
return draft; // 不让自审挂了导致整个流水线挂
}
这条是最容易忽略的。自审本身也是 LLM 调用,会失败、会超时、会返回格式异常。绝对不能让自审失败传染下游。
这是我看完整个项目觉得最巧的一个设计。
普通流水线让 LLM 给每个镜头估时长("这个镜头 3.5 秒"),后端按这个时长拼图。问题是 LLM 估的时长跟真实 TTS 音频对不上,剪出来字幕飘 1-2 秒。加更细的 prompt 让 LLM 估准也没用——它压根不知道你的 TTS 模型每秒念几个字。
Auteur 的解法是反过来:让 LLM 不再估秒数,只负责"指认"脚本里的一段连续文本。后端拿到 SRT 字幕后,去音频时间轴上反查这段文本的真实秒数。
复制代码镜头 5 anchor: "她推开门的那一刻"
↓
SRT 反查: 这句在 12.34s - 14.78s 之间
↓
镜头 5 时长 = 2.44s(真实音频时长)
抽象出来是一个通用模式——把 LLM 不擅长的"量化估算"换成它擅长的"定位指认"。这个思路不止能用于视频,任何需要 LLM 跟外部信号对齐的场景都适用:
复制代码// 通用「锚定」模式
public Anchor resolveAnchor(String llmAnchor, List segments) {
String normalized = normalize(llmAnchor); // 去标点 / 全半角 / 大小写
for (Segment seg : segments) {
if (normalize(seg.text()).contains(normalized)) {
return new Anchor(seg.startMs(), seg.endMs(), true);
}
}
return Anchor.unmatched(); // 标记未命中,降级处理
}
校验链作者做得比较狠:
anchor_match=false,视频还能渲,但日志和 UI 都会提示这个"严格校验 + 软降级"的组合也很值得抄——校验严是为了保证数据可信,但失败了不能让整个流水线挂。
光有流水线还不够,作者在上面加了一个 Agent 聊天工作台——本质是带工具调用 + 审批门槛 + Skill 加载的对话循环。
工具用 @Tool 注解扫描自动注册:
复制代码@Component
public class StoryboardTools {
@Tool(name = "regenerate_image_for_shot",
description = "重新为指定 shot 生成图片")
public RunRef regenerateImageForShot(Long shotId, String stylePatch) {
// ...
}
}
启动时 ToolRegistry 扫所有 @Tool 标注方法,反射拿参数类型生成 JSON Schema 注册给 LLM。加新工具不用改 Agent 主循环代码。这个机制本身没什么新意——LangChain、Spring AI 都是类似做法——但作者多做了两件事我觉得是 LLM Agent 工程化的关键:
(1) 写操作必须实现 PreviewableHandler
复制代码public interface PreviewableHandler {
PreviewCard preview(ToolCall call); // 返回前端展示的审批卡
Object execute(ToolCall call); // 用户点确认才调
}
任何「改预设、删数据、触发长任务」的工具都强制走审批。前端弹一张卡,用户点确认才执行。作者在 README 里写:"一开始没加这个,调试时 Agent 自作主张把一个预设的 prompt 改了,发现的时候改回去花了我半小时"。
这是把 LLM 不可控性挡在副作用之外的最后一道闸。任何让 LLM 自动执行写操作的项目都应该有类似机制——哪怕信任度高,也要有"用户最后看一眼"的环节。
(2) Skill 按需加载,不全塞 system prompt
复制代码agent/skills/
├── adjust-content.md
├── trigger-pipeline.md
├── create-topic.md
├── edit-preset.md
└── edit-text.md
把不同类型的剧本写成 markdown,Agent 自己根据当前对话决定加载哪份。作者一开始全塞 system prompt 里,"token 涨得很快,回答质量反而下降"。
这个现象有共识——LLM 的 prompt 不是越长越准。Anthropic 自己也讲过 context rot:当上下文塞太多东西,模型会丢失对关键指令的注意力。Skill 按需加载是个低成本优化,值得在所有 Agent 项目里加。
(3) 长任务异步化,工具返 runId
生图、视频合成这种动辄几十秒的任务,如果同步等会让 Agent 阻塞。作者的做法是工具立刻返回 runId,前端轮询 /api/runs/{id} 看进度。Agent 主循环不被卡住。
复制代码@Tool(name = "regenerate_image", description = "...")
public RunRef regenerate(Long shotId) {
String runId = runService.startAsync(() -> doRegenerate(shotId));
return RunRef.of(runId); // Agent 立刻拿到引用,可以继续对话
}
外部依赖缺失时后端不能挂。作者把降级写得比较彻底:
| 依赖 | 缺失时的行为 |
|---|---|
| TOS(对象存储) | 走本地路径 + /api/files/... 静态服务 |
| 火山 TTS | 配音环节 disabled,前端显示 notice |
| Jamendo(BGM) | BGM 推荐 off,制片照常合成 |
| Remotion | 走纯 ffmpeg 路径 |
| LLM 网关 | 走 OpenAI 兼容协议,自部署 vLLM / DeepSeek / 智谱 / Anthropic 都行 |
写起来到处都是 if + 兜底,但这事不能省。开源项目你不知道用户机器上装了啥,启动失败一次基本就被 uninstall 了。
通用模式:
复制代码@Component
public class TtsService {
@Value("${app.tts.enabled:false}")
boolean enabled; public Optional
if (!enabled) {
log.info("TTS disabled, skipping voice synthesis");
return Optional.empty();
}
return Optional.of(doSynthesize(text));
}
}
调用方都拿 Optional,处理"没有这个能力"是常态而非异常。比 throw + 上层 catch 干净得多。
作者在 CLAUDE.md(写给 AI 助手的项目说明文件)里列了一些踩坑,几个跟所有 Spring Boot 项目都相关:
1. .gitignore 必须用前导 / 锚定
git 的目录模式会递归匹配所有层级——storage/ 不光忽略 backend/storage/ 产物目录,还会一并忽略 backend/src/main/java/com/auteur/storage/ 这个业务包。CI 编译挂了你都不知道为啥。
正确写法:/storage/,只匹配 git 根下的同名目录。
.dockerignore 语义不同(用 Go filepath.Match,不递归),所以同样的 storage/ 在 dockerignore 里没问题。这种语义差异会让本地 docker build 通过、CI 编译挂掉,非常隐蔽。
2. Alpine 容器 localhost 优先解析 IPv6
但 nginx 默认只监听 IPv4,所以 healthcheck 永远失败。改用 127.0.0.1 即可。这个坑不写 Dockerfile 的同学很少遇到,但遇到了能调一下午。
3. Spring Boot ddl-auto: validate 严格校验 entity ↔ schema
加字段忘 migration 启动失败。强制走 Flyway,跟生产保持一致。
4. Remotion 不支持 file:// 协议
本地静态文件得走 HTTP URL,配 auteur.video.remotion.public-base-url。所有"浏览器内渲染本地文件"的项目都有这个问题。
把 LLM 流水线写得能用很容易,写得能跑能改能恢复需要做大量工程化决策。Auteur 这个项目里值得拿走的几个模式:
Optional这些模式不依赖具体业务,所有"LLM + 多步流水线"的项目都能套。技术栈是 Spring Boot 3.3 + JPA + Flyway + MySQL + Java 21 + Vue 3 + Remotion,整体偏 Java/JVM 生态。
文中提到的项目源码:github.com/nxin-github… (MIT 协议)