返回首页
Multi-Agent · Architecture · 架构实录

多 Agent 视频创作平台 · 架构实录

这是一篇架构视角的实录,讨论的是「构建多 Agent 视频创作平台」这件事在工程上要回答的几个问题:provider 抽象的边界在哪、用 Agent Loop 还是固定管线、如何让重构不慌、哪些地方必须留人在回路里。所有论点对应同一份开源代码仓的真实实现(Provider 抽象、契约测试、固定 5 阶段管线),不是纸上谈兵。

01

问题定义:多 Agent 视频创作的四个工程挑战

"用 AI 把剧本变成视频" 在 demo 里很容易,从一段 prompt 跑出一张图、一段视频,10 分钟就能演出来。但要做成可持续运营的平台,工程上有四个挑战会立刻浮出水面,每一个都不是模型能力问题。

挑战一:不确定性

同一个 prompt 跑两次,LLM 返回的 JSON 结构可能不同;图像模型可能拒答、超时、返回 NSFW 警告;视频生成是异步任务,状态机有 queued / running / succeeded / failed。系统的每一层都在和不确定性打交道,必须在边界处把不确定性收敛掉,而不是让它扩散到上层业务逻辑。

挑战二:长任务与跨模态

分镜脚本 → 概念图 → 视频素材,是一条分钟到小时级别的管线,中途任何一步失败都不能让上一步的产出丢失。同时跨三个模态(文本 / 图像 / 视频),每个模态的厂商接口、调用语义、计费维度都不同。统一调用层的价值主要在 LLM 侧(协议收敛),图像和视频的跨厂商统一代价远高于收益——不是不可行,是在"现在只有一家"的阶段做这件事属于非关键优化。

挑战三:厂商绑定风险

视频生成市场近两年仍在快速演化,模型能力、价格、可用区域每季度都在变。把生产代码直接绑死在某一家的 SDK 上,等于把架构决策权外包给厂商路线图。但完全的"任意厂商可替换"也是过度设计——不同厂商在图像/视频接口上差异极大,硬抽象会让代码变成"为了兼容而兼容"的胶水层。

挑战四:可观测性与成本

多 Agent 系统的调用链比单次推理深一个数量级。一条管线下来 10–15 次 LLM 调用 + 10+ 次图像调用 + N 次视频调用,没有 token / 任务级追踪就等于在烧钱时蒙眼。同时由于产出有质量分布,需要评估闭环判断哪些输出可以下发、哪些要回流到下一轮迭代。

本篇要回答的核心问题:面对这四个挑战,在不绑定具体厂商的前提下,怎么搭一个能跑起来、改得动、看得见、扛得住的多 Agent 视频创作平台?
02

架构总览:三层分离

整个平台按连接 / 协作 / 治理三层切分。这不是为了好看的图,而是三层有不同的变更频率——连接层每季度都可能换厂商,协作层每个新业务都可能改,治理层一旦定下来就要稳定数月。把变更频率不同的东西混在一起,是后期返工的主要来源。

┌──────────────────────────────────────────────┐
│  治理层 — 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。

03

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 行
实测验证不是空话。DeepSeek 的 smoke test 在 CI 里真跑通:tests/integration/test_deepseek_llm_smoke PASSED。这套抽象不是"理论上可换",是"换了一家、测试绿了、生产用了"。

图像 / 视频:最小公约数基类 + 厚 Adapter

图像和视频是另一种情况——主流厂商的接口范式差异很大(text-to-X、关键帧、图+音驱动、摄像机控制,本质上是不同的模态行为),高级参数几乎每家命名空间完全独立。LLM 那边一个 OpenAICompatProvider 可以共享 ~100 行代码;图像和视频共享基类能省的代码远不如那么多,ROI 到不了硬对齐的标准。

这种情况下抽象目标是薄基类 + 厚 Adapter:

接新厂商的路径是确定的:写一个 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 抽象的真正价值——厂商决策成为配置项,不是代码改造工程。

04

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      概念图 + 镜头规格 → 质量分 + 改进建议

四个工程上的好处

什么时候应该考虑 Agent Loop

不是说固定管线总是对的。Agent Loop 在下面这些场景里有真价值:

视频创作平台不满足任何一条。所以选固定管线。这是"正确范式匹配正确问题"原则的应用——不是因为 Agent Loop 不好,是因为这个问题不需要它。

05

治理与可观测:让重构不慌

治理层是最容易被 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 个调用点。

重构信心来自机器,不来自勇气。14 行 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 的硬约束——不是说人不能批准低分输出,是说低分输出不能默认下发。

06

三条可迁移的工程纪律

这套架构的具体实现不一定直接复用——不是所有团队都在做视频创作平台。但下面三条工程纪律,跟领域无关,做任何多 Agent / LLM 应用都成立。

Discipline · 01
测试网先行
非平凡重构前先花 30 分钟搭契约测试。红 → 实现 → 绿,不依赖肉眼。14 行 inspect 检查比 review 100 个调用点靠谱。这是入场券,不是优化项。
Discipline · 02
bisect-friendly commit
拆 commit 时让中间状态可独立跑测试。需要时用过渡别名 / shim 换 bisect 价值。一个跑不了的 commit 在 git bisect 里就是污染源。
Discipline · 03
信息隐藏 ≠ 数据封装
隐藏的是设计决策(将来会变的东西,比如用哪家厂商),不是数据。接口暴露内部实现选择 = 信息泄漏。Provider 抽象的本质是把厂商选择关在连接层里。

关于 "不要过度抽象"

这套架构里有几个地方故意没有抽象:

抽象的成本是真实的:每个接口、每个参数、每个抽象类都是维护负担。"将来可能用得上"不是抽象的理由,"现在已经有第二个用例"才是。