全部產品
Search
文件中心

Vector Retrieval Service for Milvus:通過阿里雲Milvus構建智能駕駛截幀分析與多模態檢索鏈路

更新時間:Aug 14, 2026

本文介紹如何用阿里雲 Milvus 的 AI Function 處理智能駕駛行車截幀:一次建表掛載多個 Function,寫入幀地址時即自動完成多模態向量化、情境分類、交通元素結構化抽取與具名實體識別,隨後支援以文搜幀、以圖搜幀、結構化過濾的 Corner Case 挖掘與多模態重排。

方案概述

在智能駕駛研發鏈路裡,車端網路攝影機是資料的第一入口。一輛測試車通常搭載 6~12 路環視相機,以 20~30 FPS 持續採集行車畫面,單車單日路測即可產生數 TB 視頻。這些視頻是訓練感知模型、複現事故、迭代規控策略的核心資產,但它們海量、非結構化、難以檢索。

工程團隊關心的不是「某段視頻」本身,而是視頻裡發生了什麼。按幀做情境理解,需要從畫面中識別出交通參與者(行人、非機動車、前車)、交通管制元素(紅綠燈狀態、車道線、限速牌)、道路環境(路口、高速、隧道、施工區)以及例外狀況事件(加塞、闖紅燈、急刹、拋灑物)。把這些理解結果結構化沉澱下來,才能支撐以下高價值情境:

  • 資料回灌:把真實路況幀回灌到模擬與訓練管線,持續迭代感知模型。

  • Corner Case 挖掘:從海量幀中精準撈出「雨夜隧道口的施工區」這類長尾情境,這正是模型最容易失效、也最缺樣本的地方。

  • 自動標註:用大模型對幀做粗標註,替代大量人工拉框打標,人工只做審核與精修。

  • 路測報告:按情境與事件彙總統計,快速產出可量化的周期報告。

若不藉助統一平台,通常要自建「抽幀服務 → 目標檢測/多模態模型 → 向量庫 → 中繼資料庫」,需要自我維護 GPU 推理叢集的擴縮容與故障恢複,幀資料在Object Storage Service、推理叢集、向量庫之間反覆搬運,鏈路長、故障點多;人工標註還存在口徑不統一導致標籤雜訊大的問題。

阿里雲 Milvus 2.6 的 AI Function 把這些能力收斂進同一個向量資料庫:推理即查詢。模型推理不再是獨立的外部鏈路,而是在 insert 與 search 時由 Milvus 內部自動觸發的函數調用。針對截幀分析,本文用到以下 Function:

Function

作用

在截幀分析中的用途

AI_EMBEDDING

用 qwen3-vl-embedding 把截幀圖片轉成 2560 維向量;多模態模型讓文本與影像地圖到同一向量空間。

支援以文搜幀與以圖搜幀,「雨天施工區」這類語義需求可被向量檢索命中。

AI_CLASSIFY

從預設標籤集中為每幀選出最匹配的一項,寫入類別欄位。

給每幀打上路口、高速、隧道、施工區等情境標籤。

AI_EXTRACT

按指定標籤從幀中抽取要素,以 JSON 寫入結構化欄位。

抽取紅綠燈狀態、車道數、天氣、例外狀況事件,用於精確過濾。

AI_ENTITY_EXTRACT

識別幀中明確出現的具名實體。

抽取標誌牌上的地名、路名與限速數值。

AI_RERANK

向量召回 Top-N 後用 qwen3-vl-rerank 二次打分(可選)。

按主體、動作、情境一致性重排,提升 Corner Case 挖掘的精排品質。

說明

前四個 Function 在寫入時自動執行,構成「寫入即推理」的主鏈路;AI_RERANK 作用於檢索階段,是可選的精排增強。

前提條件

  • 已建立 Milvus 2.6 版本執行個體。AI Function 依賴 2.6 版本核心,建立後無需單獨綁定模型服務。

  • 如需從公網訪問執行個體,已在執行個體詳情頁的 安全配置 頁簽開啟 公網訪問 並將用戶端出口 IP 加入公網訪問白名單。

  • 已安裝 pymilvus,本文樣本基於 pymilvus 3.0.0 驗證;如需自行抽幀還需安裝 ffmpeg。

  • 已準備好截幀圖片,並上傳到模型可訪問的公網地址(如 OSS)。

說明

RESTful 介面與 gRPC 共用 19530 連接埠,調用時必須顯式帶連接埠,例如 http://c-xxx.milvus.aliyuncs.com:19530;省略連接埠會預設訪問 80 連接埠並導致連線逾時。

操作步驟

步驟一:視頻抽幀

車端相機採集的是連續視頻,需先用 ffmpeg 抽幀,得到幀圖片並上傳到模型可訪問的地址(如 OSS),再把 frame_url 交給後續代碼入庫。抽幀時同步記錄每幀的 clip_id 與 ts_ms(視頻片段 ID 與幀時間戳記),便於檢索命中後定位回原視頻。

# 定時截幀:每隔 1 秒取一幀,縮放到寬 960
ffmpeg -i clip001.mp4 -vf "fps=1,scale=960:-1" -q:v 3 frames/clip001_%04d.jpg

# 只取主要畫面格(I 幀),資訊量更高、冗餘更少
ffmpeg -i clip001.mp4 -vf "select='eq(pict_type,I)',scale=960:-1" -fps_mode vfr -q:v 3 frames/clip001_key_%04d.jpg

# 截取指定時間點的單幀(例如第 5.2s,對應 ts_ms=5200)
ffmpeg -ss 5.2 -i clip001.mp4 -frames:v 1 -vf scale=960:-1 -q:v 3 frames/clip001_5200ms.jpg
說明

scale=960:-1 中的 -1 表示按原寬高比自動計算高度。若使用 ffmpeg 8.x 及以上版本,取 I 幀請用 -fps_mode vfr;舊寫法 -vsync vfr 仍可運行但會提示已廢棄。

步驟二:準備公用代碼

以下程式碼封裝含串連配置、REST 調用工具與 TEXTTRANSFORM 函數類型的相容封裝。請將 MILVUS_URI 與 MILVUS_TOKEN 替換為實際執行個體資訊。

from __future__ import annotations

import json
from typing import Any
from urllib.error import HTTPError
from urllib.request import Request, urlopen

from pymilvus import DataType, Function, FunctionType, MilvusClient

MILVUS_URI = "http://c-xxxx.milvus.aliyuncs.com:19530"
MILVUS_TOKEN = "root:xxx"


def post_json(path: str, body: dict[str, Any], timeout: int = 120) -> tuple[int, dict[str, Any]]:
    request = Request(
        f"{MILVUS_URI.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"))


# 阿里雲 Milvus 將 TEXTTRANSFORM 作為託管擴充暴露,函數類型值為 9。
# 部分 pymilvus 版本的 FunctionType 枚舉中沒有該成員,下面做相容封裝。
TEXTTRANSFORM_FUNCTION_TYPE = 9


def texttransform_function_type() -> Any:
    for type_name in ("TEXTTRANSFORM", "TEXT_TRANSFORM", "TextTransform"):
        function_type = getattr(FunctionType, type_name, None)
        if function_type is not None:
            return function_type
    existing = getattr(FunctionType, "_value2member_map_", {}).get(TEXTTRANSFORM_FUNCTION_TYPE)
    if existing is not None:
        return existing
    extension = int.__new__(FunctionType, TEXTTRANSFORM_FUNCTION_TYPE)
    extension._name_ = "TEXTTRANSFORM"
    extension._value_ = TEXTTRANSFORM_FUNCTION_TYPE
    FunctionType._value2member_map_[TEXTTRANSFORM_FUNCTION_TYPE] = extension
    FunctionType._member_map_["TEXTTRANSFORM"] = extension
    return extension


VECTOR_DIM = 2560
EMBED_MODEL = "qwen3-vl-embedding"
VLM_MODEL = "qwen3.7-plus"          # 多模態理解(分類/抽取/實體)模型
client = MilvusClient(uri=MILVUS_URI, token=MILVUS_TOKEN)
說明

AI_CLASSIFY、AI_EXTRACT、AI_ENTITY_EXTRACT 都屬於 TEXTTRANSFORM 類型的 Function(函數類型值為 9),通過 task 參數區分具體任務。部分 pymilvus 版本的 FunctionType 枚舉中沒有該成員,因此需要上面的相容封裝。

步驟三:建 Collection,一次掛載 4 個 AI Function

向量欄位用於語義檢索,另外三個欄位分別承接分類、結構化抽取與實體識別的結果。四個 Function 都以 frame_url 為輸入,在寫入時自動執行。

# ===== 建 Collection:一次建表掛 4 個 AI Function =====
collection_name = "driving_frames"
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("frame_url", DataType.VARCHAR, max_length=4096)   # 截幀圖片地址
schema.add_field("clip_id", DataType.VARCHAR, max_length=128)      # 所屬視頻片段 ID
schema.add_field("ts_ms", DataType.INT64)                          # 幀時間戳記(ms)
schema.add_field("embedding", DataType.FLOAT_VECTOR, dim=VECTOR_DIM)
schema.add_field("scene", DataType.VARCHAR, max_length=64)         # AI_CLASSIFY 輸出
schema.add_field("attributes", DataType.JSON)                      # AI_EXTRACT 輸出
schema.add_field("entities", DataType.JSON)                        # AI_ENTITY_EXTRACT 輸出

# 1) 多模態向量化:幀圖片 -> 2560 維向量
schema.add_function(Function(
    name="embed_frame", function_type=FunctionType.TEXTEMBEDDING,
    input_field_names=["frame_url"], output_field_names=["embedding"],
    params={"provider": "aliyun_milvus", "model_name": EMBED_MODEL,
            "dim": VECTOR_DIM, "is_multimodal": "true"}))

# 2) 情境分類:路口/高速/隧道/施工區/普通道路
schema.add_function(Function(
    name="classify_scene", function_type=texttransform_function_type(),
    input_field_names=["frame_url"], output_field_names=["scene"],
    params={"provider": "aliyun_milvus", "model_name": VLM_MODEL,
            "task": "ai_classify", "media_type": "image",
            "labels": "路口,高速,隧道,施工區,普通道路",
            "prompt": "根據行車畫面所處的道路環境分類。", "temperature": "0"}))

# 3) 結構化抽取:紅綠燈/車道數/天氣/例外狀況事件
#    注意:為每個標籤都規定預設取值,避免模型返回 null(詳見下方說明)
schema.add_function(Function(
    name="extract_traffic", function_type=texttransform_function_type(),
    input_field_names=["frame_url"], output_field_names=["attributes"],
    params={"provider": "aliyun_milvus", "model_name": VLM_MODEL,
            "task": "ai_extract", "media_type": "image",
            "labels": "traffic_light,lane_count,weather,anomaly_event",
            "prompt": "traffic_light 取值 red/green/yellow/none;"
                      "weather 取值 sunny/rainy/cloudy/night/unknown,無法判斷時填 unknown;"
                      "lane_count 填整數,無法判斷時填 0;"
                      "anomaly_event 描述加塞、闖紅燈、事故等異常,無則填 none。",
            "temperature": "0"}))

# 4) 具名實體:路名/地名/限速數值
schema.add_function(Function(
    name="extract_entities", function_type=texttransform_function_type(),
    input_field_names=["frame_url"], output_field_names=["entities"],
    params={"provider": "aliyun_milvus", "model_name": VLM_MODEL,
            "task": "ai_entity_extract", "media_type": "image",
            "entity_types": "LOCATION,PRODUCT",
            "prompt": "僅抽取畫面中交通標誌牌上明確出現的地名、路名與限速數值,不要根據外觀猜測。",
            "temperature": "0"}))

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)
警告

多模態 Function 必須帶 "is_multimodal": "true",且向量欄位的 dim 必須與 Function 參數中的 dim 一致(2560)。

務必為每個抽取標籤規定預設取值。實測中若未規定,模型在無法判斷時會返回 null(例如隧道內的幀因看不到天空,weather 被判為 null)。而 JSON 欄位為 null 時,!= 條件不會匹配該行——例如 6 幀資料中執行 attributes["weather"] != "rainy" 只返回 4 條,null 那行被靜默排除。在 Corner Case 挖掘情境下這類靜默漏幀的後果很嚴重,因此上面的 prompt 為每個標籤都給出了預設值。

步驟四:截幀入庫(寫入即推理)

只需寫入 frame_url、clip_id、ts_ms 三個欄位,其餘四個欄位由 AI Function 自動填滿,無需人工標註。

# ===== 截幀入庫:寫入即推理,embedding/scene/attributes/entities 自動填滿 =====
# frame_url 需替換為你自己的、模型可訪問的公網圖片地址;
# clip_id 與 ts_ms 記錄幀的來源片段與時間戳記,便於檢索後定位回原視頻。
frames = [
    {"frame_url": "https://<your-bucket>.oss-cn-hangzhou.aliyuncs.com/frames/clip_dashcam_5000.jpg",
     "clip_id": "clip_dashcam", "ts_ms": 5000},
    {"frame_url": "https://<your-bucket>.oss-cn-hangzhou.aliyuncs.com/frames/clip_dashcam_12000.jpg",
     "clip_id": "clip_dashcam", "ts_ms": 12000},
    {"frame_url": "https://<your-bucket>.oss-cn-hangzhou.aliyuncs.com/frames/clip_highway_3000.jpg",
     "clip_id": "clip_highway", "ts_ms": 3000},
    {"frame_url": "https://<your-bucket>.oss-cn-hangzhou.aliyuncs.com/frames/clip_highway_9000.jpg",
     "clip_id": "clip_highway", "ts_ms": 9000},
    {"frame_url": "https://<your-bucket>.oss-cn-hangzhou.aliyuncs.com/frames/clip_urban_2000.jpg",
     "clip_id": "clip_urban", "ts_ms": 2000},
    {"frame_url": "https://<your-bucket>.oss-cn-hangzhou.aliyuncs.com/frames/clip_urban_8000.jpg",
     "clip_id": "clip_urban", "ts_ms": 8000},
]

# qwen3-vl-embedding 多模態批量上限為 10,超過會報 image batch size can should be [1, 10]
_BATCH = 8
for _i in range(0, len(frames), _BATCH):
    client.insert(collection_name, frames[_i:_i + _BATCH])
client.flush(collection_name)
client.load_collection(collection_name)

# 寫入完成後直接 query 出結構化結果驗證
ingested_rows = client.query(
    collection_name, filter="",
    output_fields=["frame_url", "clip_id", "ts_ms", "scene", "attributes", "entities"],
    limit=100)
for row in ingested_rows:
    print(f"{row['clip_id']}@{row['ts_ms']}ms  scene={row['scene']}  "
          f"attributes={json.dumps(row['attributes'], ensure_ascii=False)}  "
          f"entities={json.dumps(row['entities'], ensure_ascii=False)}")
說明

qwen3-vl-embedding 的多模態批量上限為 10,單批超過會報 image batch size can should be [1, 10],因此按批寫入。寫入後必須調用 flush(),否則緊接著檢索可能返回空結果。

實測 6 幀寫入 + flush + load 共約 12 秒,結構化結果樣本:

幀

scene

attributes

entities

dashcam@5000ms

路口

traffic_light=green, lane_count=4, weather=sunny, anomaly_event=none

[]

dashcam@12000ms

施工區

traffic_light=none, lane_count=3, weather=rainy, anomaly_event=none

[]

highway@3000ms

高速

traffic_light=none, lane_count=4, weather=sunny, anomaly_event=none

[{"text":"EXIT 111","type":"LOCATION"}]

highway@9000ms

隧道

traffic_light=none, lane_count=2, anomaly_event=none

[]

urban@2000ms

普通道路

weather=cloudy, anomaly_event=道路上有行人與動物

[]

urban@8000ms

路口

traffic_light=red, lane_count=4, weather=sunny, anomaly_event=none

[]

六幀的情境分類與畫面內容全部一致,紅綠燈狀態、天氣、車道數的抽取也與畫面相符。具名實體只在出現可讀標誌牌文字的幀上抽到結果,模型未憑畫面外觀臆測地名,符合 prompt 中「不要根據外觀猜測」的約束。

步驟五:以文搜幀 / 結構化過濾 / 以圖搜幀

寫入階段產出的向量與結構化欄位,在檢索階段可以組合使用:向量負責語義召回,結構化欄位負責精確過濾。

# ===== 以文搜幀 / 結構化過濾 / 以圖搜幀 =====
# 1) 以文搜幀:查詢文本經同一多模態模型映射到映像向量空間,直接召回
query = "雨天施工區,路面有錐形桶和施工牌"
text_search = client.search(
    collection_name=collection_name, data=[query], anns_field="embedding",
    limit=10, output_fields=["frame_url", "scene", "attributes"])
for hit in text_search[0]:
    print(f"score={hit['distance']:.4f} scene={hit['entity']['scene']}")

# 2) 向量召回 + 結構化過濾(Corner Case 挖掘)
#    結構化欄位來自寫入時的 AI_EXTRACT / AI_CLASSIFY,可直接參与 filter
corner_query = "夜間道路上的例外狀況事件"
corner_search = client.search(
    collection_name=collection_name, data=[corner_query], anns_field="embedding",
    limit=10, filter='attributes["anomaly_event"] != "none"',
    output_fields=["frame_url", "scene", "attributes"])
print(f"命中 {len(corner_search[0])} 條長尾情境")

# 3) 以圖搜幀:把 data 換成圖片 URL 即可,其餘不變
image_query_url = frames[0]["frame_url"]
image_search = client.search(
    collection_name=collection_name, data=[image_query_url],
    anns_field="embedding", limit=10, output_fields=["frame_url", "scene"])
for hit in image_search[0]:
    print(f"score={hit['distance']:.4f} scene={hit['entity']['scene']}")

實測結果:

檢索方式

查詢

結果

以文搜幀

雨天施工區,路面有錐形桶和施工牌

施工區幀 0.5904 居首,次名僅 0.1797,區分度明顯

以圖搜幀

以一張路口幀的 URL 作為查詢

查詢圖自身 1.0000,同類路口幀 0.5534,最不相關幀 0.1162

結構化過濾

attributes["anomaly_event"] != "none"

命中資料中確實存在例外狀況事件的幀

以圖搜幀時查詢圖自身相似性為 1.0000,可用於驗證映像編碼的一致性。

警告

結構化過濾支援字串等值與數值比較,例如 attributes["lane_count"] >= 4 可篩出四車道及以上的幀。組合條件時要注意:如果某個 JSON 欄位的值為 null,!= 條件不會匹配該行,可能導致長尾樣本被靜默漏掉——這也是步驟三中為每個標籤規定預設取值的原因。

Corner Case 挖掘的組合條件要與素材庫實際情況匹配。例如 scene == "隧道" and attributes["anomaly_event"] != "none" 要求同時滿足「隧道情境」且「存在例外狀況事件」,如果素材庫中尚無這類幀,查詢會正常返回 0 條。返回空說明當前庫內沒有該長尾情境,這本身就是 Corner Case 挖掘的有效結論,而不是功能異常;調試階段建議先用單一條件確認鏈路通暢,再逐步疊加條件收窄範圍。

步驟六:AI_RERANK 多模態重排(可選)

向量相似性衡量的是語義接近程度,與「和查詢意圖有多相關」並不完全等價。可用 qwen3-vl-rerank 對召回的幀做二次精排,提升長尾情境的排序品質。提供兩種用法:在 search 中掛 ranker,或對已有的一批幀 URL 走 REST 介面獨立重排。

# ===== AI_RERANK 多模態重排(可選)=====
QUERY = "高速匝道處的加塞行為"

# 方式一:search 掛 ranker,向量召回 Top-N 後由 qwen3-vl-rerank 精排
reranker = Function(
    name="rerank_frames", function_type=FunctionType.RERANK,
    input_field_names=["frame_url"],
    params={"reranker": "model", "provider": "aliyun_milvus",
            "model_name": "qwen3-vl-rerank", "queries": [QUERY],
            "is_multimodal": "true",
            "instruct": "Rank candidate frames by relevance to the query, "
                        "prioritizing subject, action, scene and fine-grained visual details.",
            "timeout_sec": 10})
rerank_search = client.search(
    collection_name=collection_name, data=[QUERY], anns_field="embedding",
    limit=20, output_fields=["frame_url", "scene"], ranker=reranker)
for hit in rerank_search[0]:
    print(f"rerank_score={hit['distance']:.4f} scene={hit['entity']['scene']}")

# 方式二:只對已召回的一批幀 URL 做獨立重排,走 REST /v2/vectordb/ai/rerank
rest_docs = [f["frame_url"] for f in frames[:3]]
status, data = post_json("/v2/vectordb/ai/rerank", {
    "model_name": "qwen3-vl-rerank", "query": QUERY,
    "documents": rest_docs,
    "params": {"is_multimodal": True,
               "instruct": "Rank candidate frames by relevance to the query, "
                           "prioritizing subject, action, scene and fine-grained visual details.",
               "timeout_sec": 10}})
assert status == 200 and data.get("code") == 0, data
for item in sorted(data["data"]["output"]["results"],
                   key=lambda x: x["relevance_score"], reverse=True):
    print(f"index={item['index']} relevance_score={item['relevance_score']:.4f}")

實測查詢「高速匝道處的加塞行為」的重排結果:

排名

幀情境

重排分

1

高速

0.5898

2

施工區

0.5018

3

隧道

0.4858

4

路口

0.4830

5

路口

0.4089

6

普通道路

0.3819

高速幀排在首位,符合查詢語義。兩種調用方式對同一幀給出的分數完全一致,可按工程需要選擇:需要「召回 + 精排」一步完成時用 ranker,已經拿到候選幀列表、只想重排時用 REST 介面。

說明

多模態重排需要在 params 中帶 is_multimodal,並可通過 instruct 指定排序側重(如優先考慮主體、動作、情境與細粒度視覺細節)。REST 介面的 documents 為幀 URL 字串數組,且不支援 top_n,會為每個候選各返回一個得分。

方案價值

維度

傳統自建方案

Milvus AI Function

開發週期

抽幀、推理、向量庫、中繼資料庫多系統聯調

一次建表掛函數,寫入與檢索即用

推理營運

自建 GPU 推理叢集,需擴縮容與故障恢複

由 Milvus 託管調用,業務側零營運

資料流轉

幀在Object Storage Service、推理叢集、向量庫間多次搬運

寫入即推理,資料不出 Milvus 執行個體

多模態檢索

需自建文本與映像向量空間對齊

qwen3-vl-embedding 原生支援以文搜幀與以圖搜幀

標註口徑

人工標註慢、成本高、判定口徑不一

prompt 統一判定規則,標籤口徑一致可複現

對智能駕駛團隊而言,這套方案的直接價值是:

  • Corner Case 挖掘提速:以文搜幀加結構化過濾,再疊加多模態重排,長尾情境可以用一句自然語言撈出。

  • 自動標註降本:AI_CLASSIFY、AI_EXTRACT、AI_ENTITY_EXTRACT 在寫入時以統一口徑產出標籤,人工只做審核與精修。

  • 鏈路收斂:推理內聚進向量資料庫,寫入即向量化與結構化理解,檢索即多模態語義召回,行車資料全程不出 Milvus 執行個體。