Ultra-Mai / agent-v2 / subagent · frame_extractor

把抽帧子代理换成 doubao

迁移 Handoff · 目标 bytedance/doubao-seed-2.1-pro (via ZenMux) · 2026-06-30
可落地 · 最小改动

一句话:把 vertex-ai/gemini-3-flash-preview 换成 doubao-seed-2.1-pro。模型本身能看视频,所以这事能做;难的不是协议,是视频喂给模型的格式跟我们现在的管线对不上。复用已有的 ChatOpenAI 分支,改动能压到 4 处。

背景原理

01谁在"看"视频?

先搞清一件容易踩坑的事:frame_extractor 抽帧靠的是它自己那个 LLM 在看视频,不是某个独立的视频服务。所以换了模型 = 换了那双眼睛,新模型必须能吃视频。

1
读 inbox 拿到视频的 file_id,video_probe 先量时长
probe
2
subagent_analyze_media 只把视频上传 GCS、准备好"零件",自己不调 LLM
prepare
3
pre_model_hook 把视频塞进子代理本轮的模型调用 — 真正"看视频"的就是这一步,看的人 = default_model
这里看视频
4
模型看懂后规划时间点 → 一次性调 extract-frame 抽帧 → 自审去重 → exit_subagent
extract
结论:视频是作为隐藏 HumanMessage 注入子代理本轮调用的(subagent_executor.py ~line 788)。换 default_model 就是换看视频的那个模型 —— 它不能看视频,这个子代理就瞎了。
为何不是改一行

02三个卡点

好消息先说:doubao-seed-2.1-pro 原生支持 文本/图像/视频/音频,256K context,能看 2h+ 视频。ZenMux 两个端点都支持多模态。所以"模型看不了视频"这条已作废。真正卡住的是下面三个。

卡点 1

注入格式是 Gemini 专属的

现在注入的是 Vertex/Gemini 形状,doubao 不认:

// 现状(Gemini)
{"type": "media", "file_uri": "...", "mime_type": "video/mp4"}

// doubao / ZenMux 要的(OpenAI 协议)
图片 → {"type":"image_url", "image_url":{"url":"..."}}
视频 → {"type":"file", "file":{"filename":"v.mp4","file_data":"..."}}

这是本任务唯一有工作量的地方(改动 D)

卡点 2

现有 zenmux 通道被绑死在 ChatAnthropic

config_service.py:372 把 provider zenmux 硬绑 ChatAnthropic + /api/anthropic 端点,而且 enable_reasoning=True 时强塞 Anthropic thinking —— doubao 不吃。

所以不复用 zenmux,改走已存在的 openaiChatOpenAI 分支(line 254),它不加 thinking,最干净。

卡点 3

视频 URL 必须公网可达

_upload_to_gcs 返回的 data["url"]subagent_analyze_media.py:126)—— Gemini 走的是同 GCP 生态。doubao 要自己去拉这个 URL,若它是私有 gs:// 就拉不到,需要换签名/公开 URL 或 base64。必须实测确认。

最小改动方案

034 处改动

A / B / C 都是几行的配置和路由,D 是格式适配(主要工作量,且老 Gemini 路径必须零改动,回滚才干净)。

#文件位置改动
A config.toml provider 区 新增 [zenmux_openai]url=https://zenmux.ai/api/v1)+ bytedance/doubao-seed-2.1-pro 模型条目 几行
B config_service.py line 254 让新 provider 复用 ChatOpenAI 分支(顺手给 268 行加 or {} 防御) 1 行
C subagent_configs.yaml frame_extractor ~107 default_model只改这一个 1 行
D subagent_executor.py pre_model_hook ~712–793 按 provider 构造 image_url / type:"file" part;Gemini 老路径不动 主要
改动 B — config_service.py:254
- if model_config.provider == 'openai':
+ if model_config.provider in ('openai', 'zenmux_openai'):
  ...
-     params.update(model_config.extra_params)
+     params.update(model_config.extra_params or {})
改动 C — subagent_configs.yaml ~107
  frame_extractor:
    ...
-   default_model: "vertex-ai/gemini-3-flash-preview"
+   default_model: "zenmux_openai/bytedance/doubao-seed-2.1-pro"
* 命名约定:模型全名 = provider/<toml-key>,model_config.name = bytedance/doubao-seed-2.1-pro,正好是 ZenMux 要的 model 值,无需额外映射。
先验证 · 再改后端

04别先动后端

成本最低、风险最高的点在 ZenMux 视频通道本身。先用独立脚本验证它真能用,过了再做 A–D,否则后端改完照样是瞎的。

直接打 ZenMux OpenAI 端点 · 验视频 type:"file"
curl https://zenmux.ai/api/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ZENMUX_API_KEY" \
  -d '{
    "model": "bytedance/doubao-seed-2.1-pro",
    "messages": [{"role":"user","content":[
      {"type":"text","text":"描述视频关键画面,给时间点"},
      {"type":"file","file":{"filename":"v.mp4","file_data":"<公网视频URL>"}}
    ]}]
  }'

要验的 3 件事

① 视频真被解析,不是只读文件名 ② 抽帧 / 时间轴质量够用 ③ 我们的 GCS URL doubao 能直接拉

还要验 langchain 透传

用最小 python 跑 ChatOpenAI(...).invoke([...]),确认它原样透传 {"type":"file"}。不支持就改 base64 data-url 或升级 langchain-openai

风险清单

05按优先级排

优先级风险对策
最高ZenMux 视频 type:"file" + 我们的 GCS URL 端到端能否解析§04 必须先验
langchain ChatOpenAI 是否透传 type:"file" 视频 block不行就 base64 / 升级依赖
_upload_to_gcs 返回若是私有 gs://,doubao 拉不到换公开 / 签名 URL 或 base64
tool_choice="any"(doubao 非 anthropic 会保留)能否映射成 OpenAI requiredframe_extractor 依赖 exit_subagent 收尾,须确认强制工具调用可用
256K context vs 长视频 token —— 2h 视频可能撑爆frame_extractor 多为短广告素材,留意即可
安全 · 回滚

06上线前最后两件事

🔑 Key 已泄露

用户在对话里明文贴过 ZenMux key(sk-ai-v1-e140…),视为已泄露 —— 务必去 ZenMux 后台轮换后再写进 config.toml

config.toml 含明文密钥,不要提交进 git

↩ 回滚

单点回滚:把改动 C 的 default_model 改回 vertex-ai/gemini-3-flash-preview 即恢复。A/B/D 是新增 / 旁路逻辑,不影响其它子代理 —— 前提是 D 严格保证 Gemini 老路径零改动。

别误伤:advanced_creatorresearcher 也用 gemini-3-flash-preview,只改 frame_extractor 这一个
1

先验证

§04 的 curl + langchain 双验,过了再动后端。

2

改 A→B→C→D

配置和路由先上,D 的格式适配最后做。

3

跑真流程

触发一条 clone-video 抽帧,看日志 injected N part(s),确认模型真看到视频、extract-frameexit_subagent 正常。

4

A/B 对照

同一条视频跟旧 Gemini 版比抽帧质量,确认没退化。