本文介绍如何用阿里云 Milvus 的四个 AI Function 串联一条「生成 → 检索」流水线:从分镜参考图生成关键帧、由关键帧生成运动镜头、为素材自动生成检索描述,再把图片与视频写入同一个多模态 Collection,实现以文搜图、以文搜视频与以图搜视频。
方案概述
AIGC 短剧、竖屏微短剧、品牌短视频的生产节奏很快:一个团队一周要产出几十条甚至上百条成片,每条成片背后又有大量关键帧、分镜与候选素材需要生成和筛选。典型生产链路可以拆成四段:
分镜文案:先写出一段段分镜脚本,例如「雨夜,女主撑伞站在便利店门口,暖色灯光打在脸上」。
关键帧生图:把分镜文案转成关键帧图片,作为镜头的视觉锚点,也可在已有素材上做背景替换或风格调整。
图生视频:把选定的关键帧作为首帧,生成一段运动镜头。
素材沉淀与检索复用:为每天产生的图片与视频自动生成描述并转成向量入库,之后就能以文搜图、以图搜视频、以文搜视频,让素材从一次性消耗品变成可复用资产。
如果不借助统一的向量数据库平台,自己从零对接各家生成模型再拼一套检索系统,通常会遇到以下问题:生图与生视频模型分散在不同服务、鉴权与返回格式各异;视频生成是长耗时异步任务,需要自己管理轮询、终态判定与超时兜底;图片、视频、描述文本与向量分散在四处,检索时要跨系统拼装;跨模态检索需要把文本、图片、视频映射到同一向量空间,缺少语义检索时只能靠文件名和记忆找素材。
阿里云 Milvus 用四个 AI Function 把这条链路串起来,分为「生成」与「检索」两个阶段、五个步骤:
Function | 作用 | 本文中的用途 |
| 图像生成与编辑,支持单图编辑和多参考图融合。 | 把分镜参考图按文案转成关键帧图片。 |
| 图生视频与文生视频,异步任务。 | 以关键帧为首帧生成运动镜头。 |
| 多模态内容理解,为图片与视频生成标题、描述、标签。 | 为素材生成用于检索的客观描述。 |
| 写入即向量化,把文本、图片、视频映射到同一向量空间。 | 素材入库并支持跨模态检索。 |
这条链路的关键在三点:生成即入库,关键帧与运动镜头连同描述、标签一起落库,每个生成物都带着结构化信息和向量;同一向量空间,图片和视频共享同一个多模态模型与同一向量字段,才能做以文搜图、以图搜视频这类跨模态检索;写入即向量化、查询即推理,应用侧无需自己调用 embedding 模型。
前提条件
已创建 Milvus 2.6 版本实例。AI Function 依赖 2.6 版本内核,创建后无需单独绑定模型服务。
如需从公网访问实例,已在实例详情页的 安全配置 页签开启 公网访问 并将客户端出口 IP 加入公网访问白名单。
已安装 pymilvus。本文示例基于 pymilvus 3.0.0 验证。
RESTful 接口与 gRPC 共用 19530 端口,调用时必须在地址中显式带上端口,例如 http://c-xxx.milvus.aliyuncs.com:19530;若省略端口将默认访问 80 端口并导致连接超时。
操作步骤
准备公共代码
以下代码包含连接配置与 REST 调用工具,供步骤 1~3 共用。请将 MILVUS_ENDPOINT 与 MILVUS_TOKEN 替换为实际实例信息。
from __future__ import annotations
import json
import time
from typing import Any
from urllib.error import HTTPError
from urllib.request import Request, urlopen
from pymilvus import DataType, Function, FunctionType, MilvusClient
# ==== 全局配置(REST 与 gRPC 同在 19530 端口)====
MILVUS_ENDPOINT = "c-xxx.milvus.aliyuncs.com:19530"
MILVUS_TOKEN = "root:xxx"
MILVUS_REST_BASE_URL = f"http://{MILVUS_ENDPOINT}"
MILVUS_URI = MILVUS_REST_BASE_URL
def post_json(path: str, body: dict[str, Any], timeout: int = 200) -> tuple[int, dict[str, Any]]:
"""步骤 1~3 共用的 REST 调用工具。"""
request = Request(
f"{MILVUS_REST_BASE_URL.rstrip('/')}{path}",
data=json.dumps(body, ensure_ascii=False).encode("utf-8"),
headers={"Authorization": f"Bearer {MILVUS_TOKEN}", "Content-Type": "application/json"},
method="POST",
)
try:
with urlopen(request, timeout=timeout) as response:
return response.status, json.loads(response.read().decode("utf-8"))
except HTTPError as exc:
return exc.code, json.loads(exc.read().decode("utf-8"))
步骤一:关键帧生图
拿一张分镜参考图,按分镜文案做背景替换或风格调整,产出一张关键帧图片,作为后续图生视频的首帧。
项 | 内容 |
Function / 模型 |
|
输入 | 参考图 URL + 分镜文案 |
输出 | 关键帧图片 URL |
调用方式 | 同步返回 |
# ========== 步骤 1:关键帧生图 AI_IMAGE_EDIT (wan2.7-image) ==========
IMAGE_URL = "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg"
PROMPT = (
"雨夜,一位女子撑着伞站在便利店门口,暖色的店内灯光打在她脸上,"
"电影级构图,保留画面主体。"
)
status, data = post_json(
"/v2/vectordb/ai/image_edit",
{
"model_name": "wan2.7-image",
"texts": [IMAGE_URL],
"params": {"prompt": PROMPT, "n": 1, "size": "1024*1024",
"watermark": False, "timeout_sec": 180},
},
)
assert status == 200 and data.get("code") == 0, data
keyframe_url = data["data"]["output"]["outputs"][0]
print(f"关键帧图片 URL:{keyframe_url}")
单图输入用 texts 传参考图 URL,编辑指令写在 params.prompt 中。n 表示生成数量,本例为 1。
步骤二:图生视频
把关键帧作为首帧生成一段运动镜头。视频生成是长耗时异步任务,需要先创建任务拿到 task_id,再轮询查询任务状态。
项 | 内容 |
Function / 模型 |
|
输入 | 首帧图 + 运镜文案 |
输出 | 运动镜头 |
调用方式 | 异步: |
# ========== 步骤 2:图生视频 AI_VIDEO_EDIT (happyhorse-1.1-i2v),异步任务 ==========
VIDEO_MODEL = "happyhorse-1.1-i2v" # 创建与查询必须使用同一模型名
# 用步骤 1 的关键帧作首帧(media.type=first_frame)
status, data = post_json(
"/v2/vectordb/ai/video_edit",
{
"model_name": VIDEO_MODEL,
"prompt": "镜头缓缓推进,雨丝飘落,暖光在伞面上轻轻闪动,保留首帧图中的主体。",
"media": [{"type": "first_frame", "url": keyframe_url}],
"params": {"resolution": "720P", "audio_setting": "none",
"watermark": False, "timeout_sec": 180},
},
)
assert status == 200 and data.get("code") == 0, data
task_id = data["data"]["output"]["task_id"]
print(f"任务已创建 task_id = {task_id}")
# 轮询查询:tasks/describe 一次只查一个 task_id,带 provider 与相同 model_name
video_url = None
for attempt in range(60): # 本地轮询上限,达到上限只记录 task_id,不判定服务端失败
status, data = post_json(
"/v2/vectordb/ai/tasks/describe",
{"provider": "aliyun_milvus", "model_name": VIDEO_MODEL, "task_id": task_id},
)
task_status = data.get("data", {}).get("output", {}).get("task_status")
print(f" 轮询 #{attempt:<2} -> {task_status}")
if task_status == "SUCCEEDED":
video_url = data["data"]["output"]["video_url"]
break
if task_status in ("FAILED", "CANCELED"):
raise RuntimeError(f"task ended in {task_status}: {json.dumps(data, ensure_ascii=False)}")
time.sleep(10)
assert video_url, f"轮询达到本地上限,请稍后凭 task_id={task_id} 继续查询"
print(f"运动镜头 video_url:{video_url}")
轮询返回的 task_status 取值说明:
task_status | 含义 | 处理方式 |
| 任务进行中,尚未完成。这是任务执行期间的正常返回值。 | 继续轮询,不要判定为异常。 |
| 任务成功,可从 | 结束轮询。 |
| 任务失败。 | 终态,结束轮询并排查。 |
| 任务被取消。 | 终态,结束轮询。 |
实测一次 720P、5 秒的图生视频任务约需 2~3 分钟,期间 task_status 会持续返回 UNKNOWN。tasks/describe 的返回体包含 task_id、task_status、video_url 三个字段,且必须携带与创建时相同的 model_name 和 provider。
params.resolution 指定的是像素量档位,不是固定的宽高。实际输出尺寸由首帧图的宽高比决定:本例首帧为 1024×1024 正方形,设置 720P 后实际输出为 960×960(像素总量与 1280×720 相当),时长约 5 秒。如需 16:9 横屏输出,请使用对应宽高比的首帧图。
步骤三:素材理解,生成描述与标签
在素材入库前,用多模态大模型给图片和视频各生成一句客观的检索描述,作为标题、简介或标签来源,随素材一起在步骤四入库。
项 | 内容 |
Function / 模型 |
|
输入 | 图片或视频 URL |
输出 | 一句中文描述,可作为标题、描述或标签来源 |
# ========== 步骤 3:素材理解 AI_MULTI_MODAL_GENERATE (qwen3.7-plus) ==========
def describe_media(url: str, media_type: str, prompt: str) -> str:
status, data = post_json(
"/v2/vectordb/ai/multi_modal_generate",
{
"model_name": "qwen3.7-plus",
"texts": [url],
"params": {"media_type": media_type, "prompt": prompt, "temperature": 0},
},
)
assert status == 200 and data.get("code") == 0, data
return data["data"]["output"]["outputs"][0]
image_caption = describe_media(
keyframe_url, "image",
"用一句中文客观描述这张关键帧图片的主体、场景和色调,便于以文搜图。",
)
video_caption = describe_media(
video_url, "video",
"用一句中文客观描述这段视频的主体、动作和画面氛围,便于按镜头检索。",
)
print(f"[图片描述] {image_caption}")
print(f"[视频描述] {video_caption}")
图片用 media_type=image,视频用 media_type=video,prompt 为必填项。实测生成的描述示例:
[图片描述] 雨夜街头,一位穿格子衬衫的女子撑着伞站在便利店门口,身旁坐着一只金毛犬,
整体色调偏冷蓝,灯光温暖映衬湿漉地面,营造静谧而略带忧郁的氛围。
[视频描述] 这段视频呈现了雨夜街头,一名身穿格子衬衫的女子撑着透明雨伞与一只黄狗站在亮灯的
店铺外,镜头由全景逐渐推近至女子面部的特写,整体画面色调偏冷且带有湿润的反光。描述文案的质量直接决定后续以文搜图的召回效果,建议在 prompt 中明确要求「客观描述主体、场景、色调、动作」等检索时会用到的维度,避免生成带主观评价的文案。
步骤四:多模态向量入库
建一个「写入即向量化」的多模态素材 Collection,把图片 URL、视频 URL 连同描述、标签写入,Milvus 会自动为素材生成向量并落库。
项 | 内容 |
Function / 模型 |
|
关键参数 |
|
输入 | 步骤 1~3 产出的 URL + 描述 + 标签 |
输出 | 落库的 2560 维向量 |
# ========== 步骤 4:建多模态 Collection 并向量入库 AI_EMBEDDING (qwen3-vl-embedding) ==========
EMBED_MODEL = "qwen3-vl-embedding"
VECTOR_DIM = 2560 # qwen3-vl-embedding 多模态维度,向量字段维度必须与 dim 一致
client = MilvusClient(uri=MILVUS_URI, token=MILVUS_TOKEN)
collection_name = "aigc_assets_multimodal"
if client.has_collection(collection_name):
client.drop_collection(collection_name)
schema = MilvusClient.create_schema(auto_id=True, enable_dynamic_field=False)
schema.add_field("id", DataType.INT64, is_primary=True)
schema.add_field("media_type", DataType.VARCHAR, max_length=16) # image / video
schema.add_field("media_ref", DataType.VARCHAR, max_length=4096) # 用于向量化的图片/视频 URL
schema.add_field("caption", DataType.VARCHAR, max_length=2048) # 步骤 3 生成的描述
schema.add_field("tags", DataType.VARCHAR, max_length=512)
schema.add_field("embedding", DataType.FLOAT_VECTOR, dim=VECTOR_DIM)
# 写入 media_ref 时自动调用 qwen3-vl-embedding 生成 2560 维向量
schema.add_function(
Function(
name="embed_media",
function_type=FunctionType.TEXTEMBEDDING,
input_field_names=["media_ref"],
output_field_names=["embedding"],
params={
"provider": "aliyun_milvus",
"model_name": EMBED_MODEL,
"dim": VECTOR_DIM,
"is_multimodal": "true",
},
)
)
index_params = client.prepare_index_params()
index_params.add_index(field_name="embedding", index_type="AUTOINDEX", metric_type="COSINE")
client.create_collection(collection_name=collection_name, schema=schema,
index_params=index_params)
# 把步骤 1~3 的产出(url + caption + tags)向量化入库
client.insert(
collection_name,
[
{"media_type": "image", "media_ref": keyframe_url,
"caption": image_caption, "tags": "关键帧,生成图"},
{"media_type": "video", "media_ref": video_url,
"caption": video_caption, "tags": "运动镜头,生成视频"},
],
)
client.flush(collection_name)
client.load_collection(collection_name)
print(f"图片与视频已向量化入库,Collection:{collection_name}")
多模态 Function 必须带 "is_multimodal": "true",否则在 create_collection 阶段即报 multimodal=false ... 400 InvalidParameter url error。向量字段的 dim 必须与 Function 参数中的 dim 一致(2560)。
写入后必须调用 flush(),否则紧接着执行检索可能返回空结果。图片和视频要落在同一个向量字段、使用同一个模型,才能在同一向量空间里做跨模态检索。
生成物 URL 是临时签名地址,入库前建议转存。生图与生视频返回的 URL 均带 OSS 签名,实测有效期约 24 小时。这会带来两类影响:
场景 | 影响 |
库中 | 仍能正常召回(向量在写入时已生成并落库),但返回的 URL 已无法访问,业务侧展示素材会失败。 |
用已失效的 URL 作为查询做以图搜图 | 整个 |
因此生产环境中应在入库前把生成物转存到自己的 OSS,并把长期有效的地址写入 media_ref;同时保证查询侧传入的媒体 URL 可访问。
步骤五:内容搜索
素材入库后,用同一个多模态模型把查询映射到同一向量空间,检索最相似的素材实现复用。由于 Collection 已绑定 qwen3-vl-embedding 的 Function,search 的 data 里直接传原始文本或媒体 URL 即可,Milvus 会自动向量化查询。
# ========== 步骤 5:内容搜索 以文搜图 / 以文搜视频 / 以图搜视频 ==========
def search_assets(query: str, top_k: int = 5, media_type: str | None = None,
min_score: float = 0.0):
"""query 可以是一段中文/英文文案,也可以是一张查询图片的 URL。
min_score:相似度阈值,低于该值的结果视为不相关,直接丢弃。"""
filter_expr = f'media_type == "{media_type}"' if media_type else ""
results = client.search(
collection_name=collection_name,
data=[query], # 文本或图片 URL,均由 qwen3-vl-embedding 向量化
anns_field="embedding",
limit=top_k,
filter=filter_expr,
output_fields=["media_type", "media_ref", "caption", "tags"],
)
for rank, hit in enumerate(results[0], 1):
if hit["distance"] < min_score:
continue
e = hit["entity"]
url = e["media_ref"].split("?")[0] # 去掉 OSS 签名参数,展示更干净
print(f" {rank}. [相似度 {hit['distance']:.4f}] [{e['media_type']}] {e['caption'][:42]}")
print(f" {url}")
# 以文搜图:限定只搜图片
print("[5.1] 以文搜图:")
search_assets("雨夜撑伞的女子站在便利店门口", media_type="image")
# 以文搜视频:限定只搜视频
print("[5.2] 以文搜视频:")
search_assets("雨夜街头镜头缓慢推进的运动镜头", media_type="video")
# 以图搜视频:用一张关键帧图片 URL 去召回风格相近的视频素材
print("[5.3] 以图搜视频(用关键帧图召回同源视频):")
search_assets(keyframe_url, media_type="video")
# 跨模态混合检索:去掉 filter,在图片与视频混合库中检索
print("[5.4] 跨模态混合检索:")
search_assets("雨夜便利店暖光镜头")
实测三路检索的召回结果与相似度:
检索方式 | 查询 | 命中 | 相似度 |
以文搜图 | 雨夜撑伞的女子站在便利店门口 | image | 0.4834 |
以文搜视频 | 雨夜街头镜头缓慢推进的运动镜头 | video | 0.4967 |
以图搜视频 | 关键帧图片 URL | video | 0.8715 |
跨模态混合(无 filter) | 雨夜便利店暖光镜头 | video / image | 0.4967 / 0.3816 |
其中以图搜视频达到 0.8715,是四种方式中最高的——该视频正是由这张关键帧生成的,同源素材在同一向量空间下相似度极高。这也直观体现了「图片和视频共享同一向量空间」的价值:把查询文案换成一张图片 URL,同一个 search 调用即可完成以图搜图与以图搜视频;去掉 filter 则在图片与视频混合库中跨模态检索。
查询文案要与素材主题相关,并配合相似度阈值。多模态检索按相似度排序返回 Top-K,即使查询与素材完全无关也会返回结果。实测对比:
查询文案 | 与素材的关系 | 相似度 |
雨夜撑伞的女子站在便利店门口 | 主题匹配 | 0.4834 |
室内暖光下的条纹毛衣造型 | 与素材无关 | 0.0715 |
角色近景,镜头缓慢推进的概念片 | 与素材无关 | 0.0694 |
相差近 7 倍。因此应用侧应结合相似度阈值过滤(本例 search_assets 的 min_score 参数),把明显不相关的长尾结果丢弃,避免把 0.07 这类结果当作有效召回展示给用户。具体阈值需按业务素材分布调参。
方案价值
与自己从零对接各家生成模型、再拼一套检索系统相比:
维度 | 传统自建方案 | 阿里云 Milvus |
生成模型接入 | 文生图、图生图、文生视频各家 SDK、鉴权与返回格式分散对接 | AI Function 统一封装生图、生视频与内容理解 |
异步任务管理 | 自建任务队列、轮询退避、终态判定与超时兜底 |
|
素材理解 | 自建或外接标题、描述、标签服务 |
|
向量化链路 | 应用侧先调 embedding 再写入 | 写入即向量化,由 Collection Function 自动生成 |
检索入口 | 图片、视频、文本、向量四份数据割裂,跨系统拼装 | 单库单次 |
这套方案的价值不只是生成快,更在于把每一次生成都沉淀为可检索、可复用的素材资产,让内容团队从一次性消耗走向资产化经营。新剧本出现相似镜头需求时,先在历史素材库里语义检索一遍,能复用的直接复用,不能复用的再走生成,边际成本持续下降。