多 Agent 视频创作平台 · 架构实录
这是一篇架构视角的实录,讨论的是「构建多 Agent 视频创作平台」这件事在工程上要回答的几个问题:provider 抽象的边界在哪、用 Agent Loop 还是固定管线、如何让重构不慌、哪些地方必须留人在回路里。所有论点对应同一份开源代码仓的真实实现(Provider 抽象、契约测试、固定 5 阶段管线),不是纸上谈兵。
问题定义:多 Agent 视频创作的四个工程挑战
"用 AI 把剧本变成视频" 在 demo 里很容易,从一段 prompt 跑出一张图、一段视频,10 分钟就能演出来。但要做成可持续运营的平台,工程上有四个挑战会立刻浮出水面,每一个都不是模型能力问题。
挑战一:不确定性
同一个 prompt 跑两次,LLM 返回的 JSON 结构可能不同;图像模型可能拒答、超时、返回 NSFW 警告;视频生成是异步任务,状态机有 queued / running / succeeded / failed。系统的每一层都在和不确定性打交道,必须在边界处把不确定性收敛掉,而不是让它扩散到上层业务逻辑。
挑战二:长任务与跨模态
分镜脚本 → 概念图 → 视频素材,是一条分钟到小时级别的管线,中途任何一步失败都不能让上一步的产出丢失。同时跨三个模态(文本 / 图像 / 视频),每个模态的厂商接口、调用语义、计费维度都不同。统一调用层的价值主要在 LLM 侧(协议收敛),图像和视频的跨厂商统一代价远高于收益——不是不可行,是在"现在只有一家"的阶段做这件事属于非关键优化。
挑战三:厂商绑定风险
视频生成市场近两年仍在快速演化,模型能力、价格、可用区域每季度都在变。把生产代码直接绑死在某一家的 SDK 上,等于把架构决策权外包给厂商路线图。但完全的"任意厂商可替换"也是过度设计——不同厂商在图像/视频接口上差异极大,硬抽象会让代码变成"为了兼容而兼容"的胶水层。
挑战四:可观测性与成本
多 Agent 系统的调用链比单次推理深一个数量级。一条管线下来 10–15 次 LLM 调用 + 10+ 次图像调用 + N 次视频调用,没有 token / 任务级追踪就等于在烧钱时蒙眼。同时由于产出有质量分布,需要评估闭环判断哪些输出可以下发、哪些要回流到下一轮迭代。
架构总览:三层分离
整个平台按连接 / 协作 / 治理三层切分。这不是为了好看的图,而是三层有不同的变更频率——连接层每季度都可能换厂商,协作层每个新业务都可能改,治理层一旦定下来就要稳定数月。把变更频率不同的东西混在一起,是后期返工的主要来源。
┌──────────────────────────────────────────────┐ │ 治理层 — Governance │ │ 契约测试 · 输入校验 · Token 追踪 · 优雅降级 │ │ ↑ 变更最慢(数月) │ ├──────────────────────────────────────────────┤ │ 协作层 — Orchestration │ │ Agent Mesh:固定 5 阶段管线 │ │ script-breakdown → shot-designer → │ │ visual-prompt → image-gen → shot-evaluator │ │ ↑ 中等变更频率(每个新业务可能调整) │ ├──────────────────────────────────────────────┤ │ 连接层 — Connectivity │ │ Provider 抽象(types · llm/image/video ABC) │ │ ├ LLM:openai / deepseek / 其他兼容厂商(可换)│ │ ├ Image:单家实现,留 ABC 接口 │ │ └ Video:单家实现,留 ABC 接口 │ │ ↑ 变更最快(每季度可能换厂商) │ └──────────────────────────────────────────────┘
为什么不再分一层"业务层"
常见的做法是再加一层"业务编排" 把客户场景塞进去。这里不单独开层,因为这套架构本身就是"通用 + 可替换"的,业务场景应该通过Skill 配置(每个 Skill 是一个独立的 SKILL.md + prompt 模板)注入到协作层,而不是新开一层。少一层就少一组接口,少一组接口就少一组要维护的契约——这是"信息隐藏 + 接口成本"原则的直接应用。
三层之间的依赖方向
严格自上而下单向依赖:协作层只依赖连接层暴露的抽象(LLMProvider / ImageProvider / VideoProvider),治理层包裹在外层做横切。连接层永远不知道上层是哪个 Agent 在调用——这让连接层可以被独立替换、独立测试、独立 mock。
Provider 抽象层:可换与不可换的边界
这是整个架构里最有工程价值的部分,也是最容易做错的部分。常见的错误有两种:过度抽象(为了支持理论上的多厂商,写一个适配 7 家厂商但只在 1 家厂商上跑过的胶水层);抽象不足(直接把厂商 SDK 暴露给上层,结果换厂商时全代码库都要改)。
判断标准很简单:看协议。协议一致的厂商,写共享基类;协议差异大的厂商,留接口只实现一家,不假装通用。
LLM:三家可换(OpenAI 兼容协议)
主流 LLM 厂商(OpenAI、DeepSeek 以及多家国内云厂商的 OpenAI 兼容托管服务)都提供 chat/completions 协议,差异只在 base_url 和默认 model。这种情况下抽象一个 OpenAICompatProvider 基类是真有价值的——它把 ~100 行的 chat 实现共享掉,每个具体 provider 子类只剩 15 行。
gateway/llm/ ├── base.py # LLMProvider ABC ├── openai_compat.py # OpenAICompatProvider(共享基类,~100 行) ├── openai.py # OpenAILLM(OpenAICompatProvider) — 15 行 ├── deepseek.py # DeepSeekLLM(OpenAICompatProvider) — 15 行 └── ... # 其他 OpenAI 兼容厂商按需新增,每家 ~15 行
tests/integration/test_deepseek_llm_smoke PASSED。这套抽象不是"理论上可换",是"换了一家、测试绿了、生产用了"。图像 / 视频:最小公约数基类 + 厚 Adapter
图像和视频是另一种情况——主流厂商的接口范式差异很大(text-to-X、关键帧、图+音驱动、摄像机控制,本质上是不同的模态行为),高级参数几乎每家命名空间完全独立。LLM 那边一个 OpenAICompatProvider 可以共享 ~100 行代码;图像和视频共享基类能省的代码远不如那么多,ROI 到不了硬对齐的标准。
这种情况下抽象目标是薄基类 + 厚 Adapter:
- 定义
ImageProvider/VideoProvider的 ABC,只规定最通用操作(generate(prompt, **kwargs) -> bytes/submit() -> task_id,poll(task_id) -> status) - 高级参数走
**kwargs透传,每家 Adapter 自己解释 - 目前只实现一家具体 provider,验证基类设计可跑
接新厂商的路径是确定的:写一个 Adapter 类实现 ABC → 加契约测试锁住接口形状 → 在 Facade 里注册一条路由。换厂商只需改 env,接新厂商只需加 Adapter——两条路径走同一套抽象,互不干扰。基类设计是否扛得住第二家厂商,是检验抽象质量的唯一标准。没经过第二家厂商验证的接口,不算设计完成。
Facade 模式:上层调用零迁移成本
所有 provider 通过 gateway/facade.py 暴露的 Gateway 类调用。Facade 内部根据 env 配置路由到具体 provider,上层 Skill 完全不感知。换 LLM provider 只需要:
# .env LLM_PROVIDER=deepseek # 之前是 openai DEEPSEEK_API_KEY=sk-xxx DEEPSEEK_MODEL=deepseek-chat
业务代码零修改,14 个契约测试全绿,端到端管线继续跑。这才是 Provider 抽象的真正价值——厂商决策成为配置项,不是代码改造工程。
Agent 协作模式:固定管线 vs Agent Loop
多 Agent 系统的协作模式是过去两年讨论得最多、最容易踩坑的部分。市面上的实现大致分两种范式,各有适用场景:
Agent Loop(自主调度)
一个主 Agent 在循环里决定下一步调哪个工具/子 Agent,直到任务完成。代表:AutoGPT、LangGraph、各种 ReAct 变体。
优势:灵活,能处理开放任务,工具组合空间大。
代价:不可控(同一输入跑两次可能走不同路径)、难调试、token 成本不可预测、生产环境难审计。
固定管线(确定性串行)
开发者预先定义阶段顺序,每个阶段是一个 Skill,输入输出 schema 明确。代表:传统数据 pipeline + LLM 节点。
优势:可控、可测、可监控、token 成本可预算,每个阶段独立优化。
代价:不适合任务边界模糊的场景,新增能力需要改管线代码。
选择:固定 5 阶段管线
视频创作的业务流程本身就是确定的——剧本拆解 → 镜头设计 → 视觉描述 → 图像生成 → 质量评估。这不是一个需要 LLM 自主决定"接下来该做什么"的场景,反而是用 Agent Loop 是浪费:流程本身是确定的,让 LLM 重新发现一遍已知流程,等于额外烧 token 换不确定性。
script-breakdown 剧本 → 场景列表(结构化 JSON)
↓
shot-designer 场景 → 镜头序列(含景别 / 运镜 / 时长)
↓
visual-prompt-engineer 镜头 → 图像 prompt(针对具体 image model 优化)
↓
image-generator prompt → 概念图 bytes
↓
shot-evaluator 概念图 + 镜头规格 → 质量分 + 改进建议
四个工程上的好处
- 每阶段输出落盘:
output/<run_id>/01_script.json、02_shots.json... 中间产物可单独审查、可单独重跑,不必每次从头开始 - token 成本可预算:5 个阶段,每个阶段的 prompt 长度大致已知,单次跑管线的 token 上限可估算到 ±10%
- 失败可定位:哪一阶段输出 schema 校验不过,立刻报错并停在该节点,不会污染下游
- Skill 可独立演进:每个 Skill 是独立目录(SKILL.md + prompt 模板 + 测试),可以由不同人维护,互不阻塞
什么时候应该考虑 Agent Loop
不是说固定管线总是对的。Agent Loop 在下面这些场景里有真价值:
- 任务边界本身模糊("帮我优化这段代码" / "调研一下这个市场"),需要 LLM 探索
- 工具数量极多(> 20 个工具),固定枚举所有组合不现实
- 容忍单次响应慢、成本高,但希望端到端少干预
视频创作平台不满足任何一条。所以选固定管线。这是"正确范式匹配正确问题"原则的应用——不是因为 Agent Loop 不好,是因为这个问题不需要它。
治理与可观测:让重构不慌
治理层是最容易被 demo 阶段忽略、生产阶段最痛的部分。一个能跑的 demo 和一个能演进 6 个月的代码库之间,差距大部分在治理层。
契约测试:锁住公共 API 形状
把一个 435 行的单体 Gateway 类拆成分层 provider 抽象时,第一件事不是写新代码,是写 14 个契约测试。这些测试不验证业务正确性,只验证公共 API 的形状不漂移:
def test_gateway_chat_signature_unchanged():
sig = inspect.signature(Gateway.chat)
params = list(sig.parameters.keys())
assert params == ["self", "messages", "model", "temperature", "max_tokens"]
assert sig.parameters["temperature"].default == 0.7
assert sig.parameters["max_tokens"].default == 2048
def test_chat_response_has_required_fields():
resp = Gateway().chat([{"role": "user", "content": "ping"}])
assert hasattr(resp, "content")
assert hasattr(resp, "usage")
assert hasattr(resp.usage, "prompt_tokens")
assert hasattr(resp.usage, "completion_tokens")
这套测试在重构前先写、看到红、再做实现、看到绿。中间任何一次 signature 漂移(多了个参数、改了默认值、改了字段名)都会立刻被机器抓住,不依赖肉眼比对 100 个调用点。
inspect.signature 检查 + 集成 smoke 比"小心翼翼地手动比对"靠谱一个数量级。这是非平凡重构的入场券,不是可选项。输入输出校验:在边界处收敛不确定性
每个 Skill 的输入和输出都用 JSON Schema 校验。LLM 返回的 JSON 即使格式有偏差(多了字段、少了字段、字段类型变了),也在该 Skill 的边界内报错,不会污染下游。这把"LLM 输出不稳定" 这个根本性的不确定性,从全管线扩散问题降级成单节点重试问题。
Token 与任务级追踪
每次 LLM 调用记录:run_id / stage / provider / model / prompt_tokens / completion_tokens / latency_ms。视频任务记录:task_id / status / submitted_at / succeeded_at / cost_estimate。这些数据落到 output/<run_id>/_trace.jsonl,可以离线聚合分析。
没有这一步,"为什么这次跑 1.5 万 token,上次跑 8 千"会变成玄学问题。有了 trace,10 分钟能定位到是哪个 Skill 的 prompt 涨了。
优雅降级:能力缺失不是崩溃理由
如果 VIDEO_PROVIDER 环境变量留空,整条管线不该崩,应该停在出图阶段,把已生成的概念图正常交付。这是"默认安全"原则——能力缺失是常见状态(试用期、配额耗尽、厂商故障),不该让用户面对"系统挂了",而该看到"这是目前能给到的东西"。
if not video_provider:
log.info("VIDEO_PROVIDER not configured, pipeline stops at image stage")
return PipelineResult(stages_completed=["script", "shots", "prompts", "images"])
评估闭环:质量分数 + HITL 拦截
每个 shot 生成后由 shot-evaluator Skill 打分(构图 / 光影 / 主体清晰度 / 风格一致性,0–5 分)。低于阈值(默认 3.5)的 shot 自动暂停,等待人工确认。这是Human-in-the-Loop 的硬约束——不是说人不能批准低分输出,是说低分输出不能默认下发。
三条可迁移的工程纪律
这套架构的具体实现不一定直接复用——不是所有团队都在做视频创作平台。但下面三条工程纪律,跟领域无关,做任何多 Agent / LLM 应用都成立。
inspect 检查比 review 100 个调用点靠谱。这是入场券,不是优化项。git bisect 里就是污染源。关于 "不要过度抽象"
这套架构里有几个地方故意没有抽象:
- 图像和视频 provider 只实现一家——没有第二家用例之前,不要构造想象中的"通用接口"
- 没有"业务编排层"——少一层就少一组要维护的契约
- 没有插件机制 / DSL / 配置驱动的 Agent 编排——固定 5 阶段管线是代码,不是配置
抽象的成本是真实的:每个接口、每个参数、每个抽象类都是维护负担。"将来可能用得上"不是抽象的理由,"现在已经有第二个用例"才是。