跳到主要内容

双通道检索与精读:VideoRAG 的价值核心

本章讲 videorag_query_op.py:580)——提问进来后,怎么从两套索引里召回片段、怎么过滤、怎么「精读」、怎么生成带时间戳的答案。这是整个框架最有含金量的一段。

3.1 为什么要「双通道」

它要解决的小问题: 一个问题里既有「语义/实体」成分(「老师讲了哪个算法」),也有「视觉/场景」成分(「画面里出现了什么动物」)。单一检索通道照顾不全。

思路: 同一个问题,改写成两种检索句,分别走两条通道:

通道改写成什么拿什么去检索召回什么
实体(图谱)通道陈述句实体向量库 → 图谱反查文字相关的视频段
视觉通道场景描述句ImageBind 片段向量库画面相似的视频段
naive 通道原问题文字 chunk 向量库直接当上下文的文字块

两通道召回的片段取并集——任一通道认为相关,就进候选。

3.2 主流程拆解

步骤一:naive chunk —— 先拿一份文字上下文

videorag_query 开头直接用原始 query 查 chunks_vdb_op.py:598),截断到 token 预算内(naive_max_token_for_text_unit=12000base.py:17),拼成 retreived_chunk_context。这份文字会和后面的视频精读结果一起进最终提示词。若一条都没召回,直接返回 fail_response_op.py:599)。

步骤二:实体通道 —— 改写 → 查实体 → 图谱反查片段

原问题
└─▶ _refine_entity_retrieval_query 把问题改写成陈述句 _op.py:547
└─▶ entities_vdb.query 在实体向量库找 top_k 实体 _op.py:619
└─▶ 对每个命中实体取 node + node_degree(度) _op.py:622
└─▶ _find_most_related_segments_from_entities 反查片段 _op.py:635

改写提示词 query_rewrite_for_entity_retrievalprompt.py:200)会把「Q&A 式问题」转成「检索友好的陈述句」,多选题还会把选项写进括号当提示。

反查逻辑见 03-knowledge-graph.md——核心是「命中实体 → 它所在的源 chunk → chunk 的 video_segment_id → 视频段」。

步骤三:视觉通道 —— 改写 → ImageBind 文搜视频

# 示意,源自 _op.py:640 — 把问题改写成「场景描述」,再用文本查视频段向量库
query_for_visual = await _refine_visual_retrieval_query(query, ...) # prompt.py:234
segment_results = await video_segment_feature_vdb.query(query_for_visual)
visual_retrieved_segments = {n['__id__'] for n in segment_results}

这里 video_segment_feature_vdb.queryvdb_nanovectordb.py:128)用 ImageBind 把这句文字编码成向量,去和视频段向量算相似度。注意: 它的 better_than_threshold=-1vdb_nanovectordb.py:137)——即不设阈值,强制返回 top_k,保证视觉通道总有候选。

步骤四:合并 + 稳定排序

两通道并集后按「视频名, 段号」排序(_op.py:653),让候选片段在时间轴上有序——这对后面拼上下文和给引用都重要:

# 示意,源自 _op.py:653 — 先按视频名、再按段号数值排序
retrieved_segments = sorted(
list(entity_segments | visual_segments),
key=lambda x: ('_'.join(x.split('_')[:-1]), eval(x.split('_')[-1]))
)

步骤五:逐段过滤 —— 让 LLM 判「这段值不值得精读」

候选片段可能召回了一堆其实无关的。这里并发对每段调一次 LLM,拿该段的「粗字幕」(索引期生成的 content)问「这段可能含相关信息吗?」:

# 示意,源自 _op.py:666 + prompt.py:302 — 用粗字幕做一次相关性判定
filter_prompt = PROMPTS["filtering_segment"].format(caption=粗字幕, knowledge=query)
result = await use_model_func(filter_prompt) # 回答以 yes/no 开头
# ...
remain_segments = [k for (k, r) in results if 'yes' in r.lower()]

关键容错(_op.py:694): 如果过滤后一个都不剩,退回用全部候选——「宁可多读,不可无答」。提示词(prompt.py:306)还特意告诉模型「这是粗字幕,可能不直接含答案,但能指示该段是否值得看」,降低误杀。

步骤六:精读重述 —— 视觉语言模型「带着关键词」再看一遍

这是「精读」环节。索引期的字幕是泛泛的;现在针对具体问题,重新采样更多帧fine_num_frames_per_segment=15 vs 索引期的 5,videorag.py:69/68),让视觉语言模型生成针对查询的详细描述

先从问题里抽关键词(_extract_keywords_query_op.py:569 / prompt.py:268),再喂给 retrieved_segment_captioncaption.py:56):

# 示意,源自 caption.py:73 — 精读时把「需要的关键信息」写进提示,定向描述
query = f"The transcript of the current video:\n{transcript}.\n" \
f"Now provide a very detailed description (caption) of the video in English " \
f"and extract relevant information about: {refine_knowledge}"

为什么分两次字幕(粗 + 精): 索引期对所有段做廉价粗字幕(5 帧),查询期只对少数命中段做昂贵精字幕(15 帧、定向)。这是「成本 vs 质量」的经典分级——把贵的视觉推理省到刀刃上。

步骤七:组装上下文 + 时间戳引用作答

精读结果连同每段的起止时间,被组织成一张 CSV 表(_op.py:716),时间从段名里解析并格式化成 H:M:S_op.py:723):

video_name, start_time, end_time, content
lecture, 0:5:30, 0:8:0, "Caption: ... Transcript: ..."

最后塞进 videorag_response 系统提示(prompt.py:324)。这个提示强制要求带编号引用,并给了范例:

# prompt.py:355 节选 —— 强制「视频名 + 起止时间戳」引用格式
Reference relevant video segments within the answers, specifying the video name
and start & end timestamps. Use the following reference format:
In one segment, the film highlights ... [1]. Another part illustrates ... [2].
#### Reference:
[1] video_name_1, 05:30, 08:00
[2] video_name_2, 25:00, 28:00

wo_reference 开关(诚实标注): _op.py:730 会根据 query_param.wo_reference 在「带引用」videorag_response 和「不带引用」videorag_response_wo_referenceprompt.py:369)之间切。但 wo_reference 不是 QueryParambase.py:10)里声明的字段——它是在 example 脚本里动态赋到实例上的(process_videos_deepseek.py 的查询版 query_videos_deepseek.py:26 写了 param.wo_reference = True)。所以默认 QueryParam() 实例并没有这个属性,直接走 mode="videorag" 而没设它会在 _op.py:730 触发 AttributeError(inferred,基于字段未声明)。

3.3 多选题模式的差异

videorag_query_multiple_choice_op.py:746)是上面流程的「孪生副本」,给 benchmark 评测用,差异有三:

  • naive chunk 召回为空时不报错,填 "No Content" 继续(_op.py:781)。
  • 用不同系统提示 videorag_response_for_multiple_choice_questionprompt.py:405),要求输出 JSON {Answer, Explanation}
  • 强制 JSON + 重试循环:解析失败就 use_cache=False 重新问,直到能 json.loads 且含 Answer/Explanation_op.py:914)。这是一个不设上限的 while 循环——靠模型最终给出合法 JSON 收敛。

代码地图

主题文件符号
查询主流程videorag/_op.pyvideorag_query
多选题模式videorag/_op.pyvideorag_query_multiple_choice
实体检索改写videorag/_op.py_refine_entity_retrieval_query
视觉检索改写videorag/_op.py_refine_visual_retrieval_query
关键词抽取videorag/_op.py_extract_keywords_query
视觉文搜视频videorag/_storage/vdb_nanovectordb.pyNanoVectorDBVideoSegmentStorage.query
逐段相关性过滤videorag/_op.py_filter_single_segment(内嵌于 videorag_query
定向精读重述videorag/_videoutil/caption.pyretrieved_segment_caption
各类提示词videorag/prompt.pyPROMPTS["filtering_segment"], PROMPTS["videorag_response"]
查询入口/分发videorag/videorag.pyVideoRAG.aquery