ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

语义事件图:长视频问答的证据组织与图检索实践

语义事件图:长视频问答的证据组织与图检索实践

1. 长视频问答的真实瓶颈不在模型上下文,而在证据组织

去年帮一个做在线课程内容整理的团队看他们的问答功能,问题很典型:一门两小时的课程视频,用户问"讲师在演示第二个案例时提到的那个参数取值是多少",系统要么答"视频中没有提到",要么给出一个完全不相干的时间点。他们第一反应是模型上下文不够长,换了支持更长上下文的模型,结果不但没变好,成本还翻了三倍。这个现象在长视频问答里非常普遍——大家习惯性地把问题归因到"模型看不完",但真正的瓶颈几乎都出在证据组织这一层:模型没看完,是因为你喂给它的东西里根本就没包含答案所在的那几十秒,而不是窗口不够大。语义事件图要解决的就是这件事——它把视频从"一帧帧画面加一条条字幕"重组成"一个个有边界、有参与者、有关系的事件网络",让检索单元从帧、片段升级成语义完整的叙事块,再在这个网络上做有约束的扩展与推理。

这篇内容适合三类人看:正在给视频产品加问答能力的后端与算法同学、需要处理长音频长视频归档的工程同学、以及做多模态检索但被"召回不准、上下文塞满还是答错"折磨过的同行。我不打算把它写成一篇论文综述,而是按"为什么这么设计—具体怎么落地—哪里会翻车"的顺序,把语义事件图这套结构从节点设计、边构建、查询解析到工程压延迟的完整链路拆开讲,参数和伪代码都给到可直接抄的程度,也会把踩过的坑逐条列清楚。

1.1 把两小时视频塞进上下文为什么必然失败

先算一笔账。两小时视频按每秒一帧抽,是七千二百帧;就算压到每三秒一帧,也有两千四百帧。视觉编码器把每帧压成几百到上千个 token 的情况下,光是画面就要几十万到上百万 token,再叠上语音转写文本——按正常语速每分钟两百多字,两小时大概三万字,中文 token 化后也有两万多 token。两者相加,任何主流的长上下文模型都吃不下,即便勉强吃下,注意力在如此长且信息密度极低(大量静止画面、寒暄、重复操作)的序列上被稀释,关键的那十几秒反而被淹没。更现实的问题是成本:每次提问都跑一遍全量视觉编码,单次查询的算力开销根本不可接受,而长视频的查询是高频的,一个视频可能被问几百次。

所以工程上一定要做"离线一次、在线多次"的处理——视频进来时把可复用的中间表示算好存下来,查询时只做轻量的检索与生成。这个思路本身不新鲜,关键是中间表示长什么样。如果中间表示是"帧向量库",那你得到的是碎片;如果中间表示是"事件图",你得到的是带结构的叙事骨架。这两者的差别,决定了后面问答能力的上限。

1.2 帧级检索的碎片化困境:找到了画面却答不出因果

我在多个项目里验证过同一件事:纯粹靠帧级或短片段级的向量检索做长视频问答,天花板很低。原因不在于向量模型不强,而在于检索粒度与问题粒度不匹配。用户的问题绝大多数不是"有没有出现某个物体",而是"为什么""后来怎样""谁先提出来的""这两件事之间隔了多久"。这类问题的答案分布在多个时间点,需要把若干片段串起来才能回答,而帧级检索的返回是彼此孤立、没有顺序标注、没有关系标注的 top-k 片段。

举个具体例子:一段访谈视频里,A 先提出一个方案,B 反对,最后 A 修改后通过了。用户问"这个方案最终为什么被接受"。你在帧级别检索"方案""接受",召回的多是最后的通过画面;检索"反对",召回的是中间那段争论。这些片段被平铺着塞进提示词,模型看到的是三个互不相干的画面描述,它不知道"反对"发生在"修改"之前,也不知道"修改"和"通过"之间是因果关系。于是它要么编一个听起来合理的解释,要么直接说信息不足。人类回答这类问题时会自动依赖事件之间的先后与因果链,而帧级检索把这层结构彻底丢掉了。

1.3 事件为什么是更合适的检索与推理单元

事件作为单元有三个天然优势。第一是语义自洽:一个事件内部的时间、人物、场景、动作是连贯的,用一段话就能把它描述清楚,天然适配语言模型的输入方式;相比之下单帧描述丢掉了动作过程,十分钟的超长片段又混杂了多个主题。第二是可对齐:事件有明确的时间区间,用户问题里"刚开始那段""讲到一半的时候""最后几分钟"这类模糊时间表述可以映射到具体事件集合上,检索时能加时间先验约束,显著提升准确率。第三是可连接:事件之间可以定义时序、因果、共指、包含等关系,这些关系构成了推理路径,让"多跳"变成一次有约束的图遍历而不是盲目的相似度堆叠。

用一个生活化的类比:帧级检索像把一本书拆成一页页散在地上,你按关键词捡起几页,然后试图回答"这本书讲了一个什么故事";语义事件图则是先按章节和段落整理好,再在段落之间标注"这里呼应前面""这里是转折"。后者的成本当然更高,但它把结构信息一次性固化下来了,查询时复用,整体上反而更划算。

2. 语义事件图的节点设计:一个事件节点到底该装什么

节点设计是整个系统里最需要提前想清楚的部分,因为图一旦建成,后期改字段意味着全量重建,成本非常高。我见过几个团队一开始图省事,节点只存了"起始时间+结束时间+一段文本描述",跑到第二周就发现好几类问题没法处理:想做人物追踪但节点里没有稳定的人物标识、想回答"他说了什么"但视觉描述和语音文本混在一起没法分离、想知道某段信息是否可信但没有任何置信度字段。所以这一节我把字段定义、切分策略和多粒度层级分开讲,这几件事的决策顺序应该是先定粒度策略,再定字段,最后调切分参数。

2.1 事件边界的切分策略与参数取舍

事件边界检测的核心矛盾是粒度:切太细,一个完整动作被拆成三段,节点之间关系爆炸且语义残缺;切太粗,一个节点里塞了五件事,检索命中之后模型还得自己二次切分。我目前的实践结论是"以镜头为最小单元,以语义漂移为主判据,以固定时长兜底",三层约束一起用。

具体做法分三步。第一步做镜头检测,用相邻帧的颜色直方图差异加边缘变化率做基础判定,设定一个阈值,超过就断为镜头边界,同时给一个最短镜头时长限制(比如 0.5 秒)过滤掉闪白和转场噪声。第二步把镜头作为原子,用语音转写的句子向量做相邻镜头的语义相似度,相似度高于阈值且间隔时间短就合并;这一步是主力,因为大多数长视频的主题切换在语言层面是有信号的——话题变了、说话人变了、场景描述变了。第三步加硬性兜底:单个事件时长超过上限(比如 45 秒)强制在内部按语句边界再切,低于下限(比如 3 秒)强制向后合并,避免出现大量超短碎片。

给一段我实际用的合并逻辑骨架,参数是调过的,可以直接作为起点:

def build_events(shots, asr_sents, cfg): # shots: [{"t0":..,"t1":..,"keyframe":..}, ...] events, cur = [], None for i, s in enumerate(shots): text = asr_text_in(asr_sents, s["t0"], s["t1"]) if cur is None: cur = new_event(s, text) continue sim = cos_sim(embed(text), embed(cur["text_tail"])) # 语义相似度 gap = s["t0"] - cur["t_end"] # 时间间隔 merge = (sim > cfg.sim_th) and (gap < cfg.gap_th) too_long = (s["t1"] - cur["t_start"]) > cfg.max_dur too_short = (cur["t_end"] - cur["t_start"]) < cfg.min_dur if merge and not too_long: extend(cur, s, text) elif too_short and gap < cfg.gap_th * 2: extend(cur, s, text) # 短事件强制吸附 else: events.append(finalize(cur)) cur = new_event(s, text) events.append(finalize(cur)) return events

参数起点我一般用:语义相似度阈值 0.62 到 0.72 之间(取决于转写质量,转写差就往低了调,否则合并过度),间隔阈值 1.5 到 3 秒,单事件上限 45 秒,下限 3 秒。实测下来两小时的访谈或课程,通常切出 250 到 600 个主事件,中位时长 12 到 20 秒,这个规模在检索和扩展时是可控的。这里有个容易被忽略的细节:纯音乐、纯环境音、无语音的片段,语义相似度算不出来,必须走单独的兜底分支——按镜头视觉描述文本的相似度合并,或者干脆按固定 10 秒切片,否则这些片段会退化成一个个孤立镜头,图上出现大量度数为一的孤立节点,稀释检索效果。

2.2 节点属性字段的完整定义与多模态对齐

节点字段的设计原则是"模态分离、便于拼装、带可信度"。分离是为了后面能按问题类型做字段级加权——问"说了什么"时语音文本权重高,问"画面里有什么"时视觉描述权重高;带可信度是为了在融合排序时能降权低质量证据。我用的 schema 大致如下:

字段类型说明是否参与向量化
event_idstring稳定主键,建议含视频ID与序号否
t_start / t_endfloat秒级时间戳,以音频时间轴为基准否
visual_descstring对事件内 3 到 8 个关键帧的合成描述是
speech_textstring事件时间窗内的语音转写文本是
speakerslist说话人标识与时长占比是
entitieslist人物、物体、场景标签及其来源是(弱权重)
actionslist动作或操作标签是(弱权重)
keyframe_vecvector关键帧聚合向量,用于视觉召回否
confidencefloat融合置信度,含转写质量、描述质量否
modality_maskint位掩码,标记哪些模态实际存在否

多模态对齐这块要说清楚一个坑:时间轴必须统一。视频帧的时间戳来自容器,音频包的时间戳来自音频流,语音转写引擎输出的时间戳又是另一个口径,三者之间常有几十毫秒到几百毫秒的漂移,在长视频里会累积。我的处理是以音频流时间轴为唯一基准,视频帧的时间戳按首帧对齐后做线性校正,转写结果按音频包时间戳回填。不做这一步,你会看到事件边界落在句子中间、语音文本和画面错位一个镜头,而这类错误在人工抽检时非常难发现,只有在回答"某句话是在哪个画面说的"时才会暴露。视觉描述字段的生成也要注意:不要只取事件中间那一帧,要均匀取 3 到 8 帧再合成一段描述,因为很多事件的关键信息在首尾(比如"拿起工具—操作—放下"),只取中间帧会得到"一个人拿着工具"这种废信息。

2.3 多粒度层级:主事件与子事件的嵌套方式

单一粒度很难同时满足"宏观问答"和"细节定位"。用户问"整场会议讨论了几个议题",需要的是主事件层;问"他在第三页 PPT 上标注的那个数值是多少",需要的是子事件甚至帧级。所以图必须是多层的,我用两级:主事件层和子事件层,主事件由若干子事件组成,子事件再向下就是镜头和关键帧,不再建节点。

层级边(contains)让检索可以在两个方向上灵活走:宏观问题从主事件层召回,命中后向下展开子事件取细节;细节问题从子事件层召回,命中后向上补一个父节点作为上下文,避免模型拿到一个孤立的五秒片段不知道前因。这里有个实用的经验参数:向上补父节点时最多补一跳,向下展开时最多展开该父节点的全部子节点但不超过 12 个,否则一个主事件下挂三十个子事件,展开一次就把 token 预算吃光了。

层级设计还有一个隐性收益:它给了你一个天然的摘要机制。主事件节点的描述可以由其子节点的描述压缩而成,构建时自底向上做一次汇总,这样每个主事件都自带一段"这件事的梗概",检索命中主事件后不用再逐个子节点读,直接把梗概给模型,既省 token 又保住了叙事完整性。这个汇总我建议用规则化的模板拼装而不是再调一次模型——模板拼装快且不会引入幻觉,虽然文风生硬,但对检索和推理来说信息量是够的。

3. 关系边的构建:时序、因果、共指三类边怎么可靠地抽出来

边是语义事件图区别于普通片段库的关键。没有边,图退化成一个带时间戳的文档集合,多跳推理无从谈起。但边也是最容易做成"看起来很美、实际全是噪声"的部分。我的整体策略是:时序边做满做准,共指边尽力而为,因果边只做高置信的弱标注,宁可少不可滥。三类边的可靠度差别很大,混在一起不看置信度直接用,是很多项目效果不及预期的直接原因。

3.1 时序边是骨架,但要处理重叠与并发

时序边看起来最简单——相邻事件连一条"之后"的边就行。实际落地时有两个必须处理的复杂情况。第一个是事件重叠:画中画、旁白讲解配合画面演示、多人同时说话,这些场景下事件时间区间是交叉的,不是严格串行。如果只连相邻边,重叠关系会被压平成先后关系,模型会得出"先说了这句话然后才出现画面"的错误时序理解。我的做法是同时建两类边:next(严格后继)和overlap(区间重叠超过一定比例,比如 30%),并在序列化时用不同标记区分。

第二个是并发分支:一段视频里可能同时存在两条叙事线(比如主讲人在讲,旁边有人在演示另一个东西),严格来说这是两张子图。全连会引入错误关联,完全不连又会丢失跨线信息。折中方案是给边加权重,权重由"共享实体数+时间邻近度"决定,检索扩展时按权重阈值裁剪。时序边的权重衰减我一般设成随距离线性下降,距离超过 5 个事件基本衰减到接近零,这样扩展时不会一路走到视频开头。

时序边还有个隐藏价值:它是回答时间类问题的直接依据。"这两件事隔了多久""哪个先发生"这类问题,答案可以直接从图上算出来,不需要模型去数。我在生成阶段会把关键节点的时间戳和间隔显式写进提示词,让模型引用而不是估算,准确率提升非常明显,也顺带解决了证据可追溯的问题——返回答案时可以带上具体时间点,用户点击就能跳转。

3.2 因果边只能靠弱监督加启发式,别指望全自动

因果关系的自动抽取是个深坑。纯靠语言连接词("因为""所以""结果""于是""导致")能抓到一部分显式因果,但视频里的因果大量是隐式的——A 提出了一个方案,B 摇头,A 改了方案,这里的因果关系没有任何连接词提示,全靠语义理解。想要全自动、高准确率地抽因果边,目前不现实,追求这个指标会把项目拖死。

我的做法是三层叠加,全部标为弱置信并记录下来源。第一层是连接词规则:语音文本里出现显式因果连接词,且两个事件在时间上相邻(间隔小于 30 秒),就建一条因果边,置信度中等。第二层是问答式弱标注:对每个事件,用它前一个事件的文本构造"接下来发生了什么"的提示,让语言模型判断是否存在因果或转折关系,输出是/否加一句理由,只保留高置信的正例。这一层成本不低,所以我只对"高信息量事件"跑——判断标准是语音文本长度超过阈值、实体数量超过阈值,把寒暄、过场、重复操作筛掉,通常能过滤掉一半以上。第三层是人工校验抽样:从自动生成的因果边里抽 50 到 100 条人工看,估算准确率,如果低于某个下限(比如 70%),说明规则太松,回去收紧。

提示:因果边宁缺毋滥。宁可让模型在缺边的情况下去读原始文本自己判断,也不要给它一条错误的因果边——错误的强结构提示比没有结构更容易导致错误答案,因为模型倾向于相信显式给出的关系。

实践中还有一个降级策略很管用:如果因果边构建不准,可以全部退化为"时序边+相邻上下文一起给",让语言模型自己在两三个相邻事件的文本里判断因果。这种"轻结构"方案在多数问答场景下效果并不差,只是对多跳长链路问题的表现会明显弱一些。先把轻结构跑通上线,再逐步补因果边,这个节奏比一上来追求完整图谱要稳得多。

3.3 共指边与实体消解:ASR错误是最大污染源

共指边解决的是"同一个人在不同事件里叫什么"的问题。视频里同一个人可能被称呼为名字、职位、昵称、"他"、"这位同学",语音转写还会把名字转错。如果实体消解做不好,跨事件的人物追踪会断成一截一截,"这个人后来做了什么"这类问题就答不了。

我的实体消解走三路融合。第一路是人脸聚类:对事件内的关键帧做人脸检测与特征提取,跨事件做聚类,得到视觉人物 ID 及其出现的事件列表,这一路在画面稳定、人物正脸清晰的场景下非常可靠。第二路是声纹聚类:从语音片段提取声纹特征聚类,得到说话人 ID 及其时间段,这一路在多人对话、画面切换频繁的场景下比人脸更稳。第三路是文本称呼对齐:从语音文本里抽姓名、职位、称呼,按时间邻近和共现频率与视觉/声纹 ID 做映射。三路结果按置信度投票融合,任意两路一致就建立共指边,只有一路时降权保留。

ASR 是这里最大的污染源,必须提前处理。常见错误有两类:专有名词转错(人名、产品名、术语),同音词混淆。我的处理是维护一份领域词表——把视频标题、简介、章节标题、以及从全片高频出现的名词里筛出的候选词做成热词表,在转写阶段注入提升识别率;转写完成后再做一次基于编辑距离和拼音相似度的归一化,把明显转错的词对齐到词表里的标准写法。这套处理能把专有名词的识别准确率拉高不少,共指边也跟着干净很多。还有一类隐性污染是说话人分离错误,两个人声音接近或者有重叠说话时,转写引擎会把两句话合并到一个人名下,导致共指边连错。我的兜底是给每个说话人段落加时长和能量特征做二次校验,异常短且与相邻段落特征冲突的段落标为"说话人不确定",不参与共指建边。

4. 从问题到子图:查询解析与图上检索的完整链路

前面建好的图是静态资产,真正的效果差异出在查询这一侧。我见过同一个图、同一批测试问题,只把检索策略从"向量 top-k 直接喂"改成"解析意图+图扩展+预算控制",答案正确率提升十几个点的情况。原因很简单:图里存的是结构,如果查询阶段不用结构,等于白建。这一节把查询链路拆成解析、召回扩展、序列化三段讲,每段都给可操作的判据。

4.1 把自然语言问题拆成锚点、关系意图与时间约束

先做意图分类,我粗分成四类,因为不同类别的检索策略差别很大。事实型(谁、什么、多少)主要靠实体和属性匹配,一跳就够;因果型(为什么、原因是什么)需要沿因果边和时序边扩展;过程型(顺序、步骤、过程)需要拿到一条完整的事件链,时序边权重最高;计数型(几次、哪些次)需要在图上做遍历聚合而不是召回相似片段。分类不用太细,四类加一个兜底类就够,用一个小模型或者规则加关键词匹配都能做,成本很低但收益直接。

解析时要抽出三类信号。第一类是锚点:问题里出现的实体(人物、物体、术语)直接映射到实体倒排索引,得到一批初始事件 ID;这是最可靠的召回路径,比向量召回准得多。第二类是时间约束:像"开头""中间""最后""在第几分钟""讲到某个话题的时候"这类表述,映射成时间区间或相对位置区间,用作过滤条件。这里要注意,"开头"到底是前 5% 还是前 3 分钟,我的经验是按视频总时长取相对比例更稳,但如果是明确说了分钟数就必须用绝对时间,两者要分开处理。第三类是关系意图:问题里有没有"之后""因为""同时""一直""反复"这类关系词,决定扩展时哪些边类型加权。

注意:否定与缺失型问题是解析层的重灾区。"有没有提到过某个东西"如果答案是"没有",任何基于相似度的召回都可能召回一堆弱相关内容,最后模型强行编一个答案。这类问题我建议单独加一条规则:先做一次全图实体扫描确认该实体是否在任何节点中出现,没有就直接返回"未提及",不走生成。这条规则挡掉了相当一部分幻觉。

4.2 向量召回种子节点与k跳扩展的剪枝策略

种子召回走双通道:向量召回取相似度 top-20,实体召回取命中锚点的全部事件(通常少于 20 个),两路按 ID 去重合并。向量召回用的文本是节点的拼装描述,拼装顺序有讲究——我把语音文本放前面、视觉描述放后面,因为多数问题问的是"说了什么",用语音文本做主导召回更准;如果意图分类判定为画面类问题,就反过来。

扩展阶段是控制质量的关键。我的做法是以种子节点为起点做有界 BFS,跳数上限 2,边类型按关系意图加权,然后对扩展出来的候选节点统一打分排序:

score = w1 * 向量相似度 + w2 * 实体匹配数(归一化) + w3 * 边权重(来自扩展路径) + w4 * 时间先验(命中时间约束则加分) + w5 * 中心性(节点度数归一化,弱权重)

权重起点我用 w1=0.45、w2=0.25、w3=0.2、w4=0.1、w5=0.05,实测这个比例在通用场景下比较稳,如果问题集里时间类问题占比高,把 w4 提到 0.2 左右会有改善。排序完之后按两条规则裁剪:节点数上限(比如 25 个)和时间连续性优先——同一个主事件下的子节点如果被选中,优先整组保留,因为拆散的事件链反而更难理解。这一步的效果比调向量模型明显得多,我经常建议团队先别急着换更强的嵌入模型,先把扩展与裁剪逻辑做扎实。

还有一个常被忽略的动作:扩展后要回填父节点。子节点被选中时,把它所属主事件的梗概作为一个独立的上下文块加进去,让模型知道"这几件事属于同一个大事件"。实测这个动作对因果型和过程型问题的帮助最大,成本只是多几百个 token。

4.3 token预算下的子图序列化格式

选中子图之后要序列化成模型能读的形式。这里的设计目标是三件事:结构清晰可解析、token 可控、时间戳可引用。我用带标记的块状格式,每个节点一块,头部写 ID、时间区间、与前一节点的关系,正文分模态写:

[E0412] 时间 00:18:20-00:18:34 | 关系: 因果→E0415 | 置信 0.82 语音: 那我们把阈值改成0.65试试…… 画面: 演示界面中滑动条被拖到中间位置,右侧数值同步变化。 参与: 说话人A(主讲) / 屏幕录制

这种格式的好处是模型能直接引用[E0412]和时间点作为证据,回答里带上溯源信息,用户能点回具体位置。同时关系标注写在块头,模型一眼能看到事件之间的连接。要注意的是,标记符号要选模型训练语料里常见的(方括号、竖线、箭头这类),别自创奇特符号,否则模型会当成噪声跳过。

序列化还要做token 预算分配。我的分配方式是:主事件梗概块每个 60 到 80 token,子事件详情块每个 120 到 180 token,总预算按模型上下文的 40% 到 50% 控制,剩下的留给系统提示、问题和输出。超预算时按分数从低到高裁掉子事件详情块,主事件梗概块尽量保留——因为梗概保证叙事连贯性,细节缺失比结构缺失危害小。这个策略我在几个长视频问答项目里都用了,效果稳定。

5. 工程落地:建图成本、延迟与增量更新

技术方案能不能上线,最后都归结到两个数字:建一个视频的图要多久、一次查询要多久。语义事件图的建图成本明显高于纯向量索引,因为它多了事件切分、视觉描述生成、实体抽取、边构建等步骤。我一开始也担心成本压不下来,实际做过之后发现,只要做好分层缓存和最右优化,成本完全在可接受范围内,而查询侧因为召回更准、上下文更短,反而比"全量塞上下文"便宜得多。

5.1 离线建图流水线的分层缓存

建图流水线我一般拆成六个可独立缓存和重跑的阶段:抽帧与镜头检测、语音转写、关键帧视觉描述、实体与动作抽取、事件切分与节点装配、边构建与索引写入。拆开的意义在于任何一步改了参数只重跑下游。比如调了事件切分的相似度阈值,只需要重跑第五、六步,前面的转写和视觉描述结果全部复用,这是省时间的关键。开发阶段改参数很频繁,不做好这层缓存,每次调参都全量重跑,效率低到无法迭代。

实测耗时(单机加单张推理卡,两小时视频):抽帧与镜头检测 1 到 2 分钟,语音转写 3 到 6 分钟(取决于音频长度和是否做说话人分离),关键帧视觉描述最慢,按事件数算,400 个事件每事件 6 帧、每帧一次推理,加上批处理优化大概 8 到 15 分钟,实体抽取和边构建 1 到 2 分钟。总计十几到二十分钟一个视频。这个数字用于离线归档完全没问题,但如果有大量积压需要做,就得加并发和批处理。

几个实测有效的优化点:视觉描述按帧做而不是按事件做,同一帧在多事件复用时直接命中缓存;描述生成用较小的模型加更长的输出约束,够用就行,别上最大模型;事件切分和边构建纯 CPU 逻辑,和推理任务并行跑不占卡;索引写入批量提交,不要一个节点一次写。

5.2 线上查询的延迟拆解与优化

查询侧我拆成五段计时,每一段都要单独看分布而不是只看均值,因为长尾会毁掉体验。意图解析与锚点抽取 50 到 200 毫秒(规则加小模型);向量召回 20 到 50 毫秒(用内存索引,别查远端服务);图扩展与打分 20 到 60 毫秒(邻接表常驻内存,节点数几百到几千量级,完全够);序列化 10 到 30 毫秒;生成 2 到 8 秒(取决于模型和输出长度)。可以看到前面四段加起来不到半秒,瓶颈全在生成。所以优化重点应该是压缩输入 token 数和控制输出长度,而不是去优化图遍历的几十毫秒——那个优化意义不大。

压缩输入的手段除了前面的预算裁剪,还有一个很有效的做法:按问题类型决定是否给视觉描述。很多问题纯靠语音文本就能回答,视觉描述块可以省略,直接省掉三到五成的输入 token。判断方式很粗糙但有效:问题里出现"画面""显示""颜色""动作""演示""长什么样"这类词就带上视觉描述,否则只给语音文本和实体标签。这个规则我记得当时一加上,平均输入 token 降了三成多,答案质量没有可感知的下降。

缓存策略也值得做。同一个视频的高频问题(比如课程里的常见疑问)可以用问题归一化后的哈希做答案缓存,命中直接返回,同时把子图缓存起来,同视频的相似问题复用扩展结果,跳过召回与扩展两步。这部分对整体成本的贡献比想象中大,因为长视频的查询分布往往是长尾的,头部问题重复率很高。

5.3 长视频流式场景的增量建图

录播场景建图是一次性的,但直播、会议实时记录这类场景需要增量建图,复杂度高一个量级。核心难点是事件边界当时不确定——一个事件可能还在进行中,你不能提前给它定结束时间,也不能提前判断它和下个事件的关系。我的做法是维护一个"开放事件"窗口:最近 60 到 90 秒内的事件状态是开放的,允许被延长、拆分、合并;超过窗口就冻结为关闭状态并写入索引,之后不再修改。

冻结有个副作用:如果后面出现了和已冻结事件强相关的新事件,边就建不上了。缓解办法是允许"回填边"——新事件生成时,在时间上向回看最近 N 个已关闭事件,满足条件就补一条边写回去。这一步是异步的,不影响实时查询的延迟。另外,实时场景下检索必须能同时查"已关闭索引"和"内存中的开放事件",我一般把开放事件放在一个很小的内存结构里直接线性扫描,量级小(十几个)完全不影响性能。

实践中还有一个取舍要说清楚:实时场景下,越靠近当前时间的问题越难答准,因为事件还没结束、上下文还在变化。产品上要么把可提问的时间范围限制在"至少 1 分钟前"的内容,要么在答案里明确标注"截至某时刻的信息"。强行回答正在进行中的事件,错误率会明显上升,用户对错误的容忍度远低于对"暂时无法回答"的容忍度。

6. 踩过的坑:五类高频故障与排查路径

前面讲的都是设计意图,实际跑起来会遇到一堆和设计预期不符的情况。这一节我把印象最深的几类故障和排查路径写出来,都是当时花了不少时间才定位的,希望能帮后来的人少走弯路。排查的总体思路是一样的:先确认是数据问题还是逻辑问题,再确认是召回问题还是生成问题。这个二分法能快速缩小范围——把召回的子图人工看一遍,如果答案明显在子图里而模型答错,那是生成问题;如果子图里根本没有答案,那是召回或建图问题。

6.1 图规模爆炸与边剪枝

第一个坑是边数量失控。最初没做任何剪枝,每个事件和前后各连边,同时对所有共享实体的事件连共指边,结果一个两小时视频生出几万条边,内存占用和扩展耗时可观不说,检索扩展时几乎每个节点都能连到另外几十个节点,跳数限制形同虚设,一次查询扩展出上百个候选,排序后还是塞不下。

排查过程很直接:统计每个节点的度分布,发现长尾极其严重,少数高频实体(主讲人、主讲台、常见物品)几乎连接了所有事件。根因是共指边没有做区分——"这个人出现在所有事件里"是事实,但它对推理几乎没有区分度,反而制造噪声。修复方案是按区分度剪枝:统计每个实体的出现频次,频次高到接近全片事件数的(比如超过 60%),视为背景实体,不为它建共指边,只保留在节点实体列表里;同时给每个节点设边数上限(8 到 12 条),超出时按"关系类型优先级 + 边权重"裁剪,因果边和层级边优先于共指边,共指边优先于远距离时序边。

剪完之后边数量降了一个量级,检索扩展的候选数回到可控范围,答案质量反而上升了——因为噪声边被去掉,排序分数更可信。这个教训我印象很深:图的完整性不等于图的可用性,边多不代表信息多,很多时候是干扰多。

6.2 时间戳漂移导致的证据错位

第二个坑更隐蔽:答案内容大致对,但给出的时间点总是差几秒到十几秒,用户点回去看到的画面和答案对不上。一开始怀疑是生成阶段模型自己算错了,加了强约束让它只引用给定时间戳,问题依旧,说明时间戳本身就错了。

往上追,问题出在转写结果的回填环节。语音转写引擎输出的是相对于音频起点的时间,而音频流在容器里的起始时间不一定是 0(有时前面有一段静音或元数据偏移),两者之间有个固定偏差。再加上部分转写引擎在长音频上会分段处理,段与段之间有微小的累积误差。这两个偏差叠在一起,越往后越长,最终导致事件边界和真实语音错位。

定位方法很简单也很有效:在视频开头、中间、结尾各选一句特征明显的话(比如包含专有名词的句子),人工核对这句话在播放器里的真实时间点和图上记录的时间点,算出偏差曲线。如果是固定偏移,就是起始时间对齐问题;如果是线性增长,就是累积误差问题。修复方案是以音频流时间轴为基准做一次全局校正,映射到视频时间轴;对分段处理引入的累积误差,用每一段的音频包起始时间重新对齐,而不是简单累加每段时长。修完之后偏差降到一帧以内,证据跳转终于准了。

这个坑值得单独强调,因为它不会让答案"明显变错",只会让溯源体验变差,很容易被当成小问题搁置,但它直接影响用户对系统的信任——点回去对不上,用户就会认为整个答案是编的。

6.3 大模型无视结构标记时的处理

第三个坑是结构化提示词被模型忽略。我们精心设计的块头标记、关系箭头、ID 引用,模型在回答时完全不用,既不引用事件 ID,也不按关系顺序组织答案,有时候还把几个不同事件的内容揉成一段,说得像同一时刻发生的。

排查发现两个原因。一是标记格式太花,用了模型不熟悉的符号组合,模型把它当格式噪声跳过了。二是提示词里对输出格式的要求只在系统提示里说了一次,没有给示例,模型对"要引用 ID"这件事没有形成强约束。修复做了三件事:把标记换成更常规的写法(方括号加竖线),在提示词里放两三条正例示范怎么引用,以及加一层输出后校验——检查回答里引用的 ID 是否都在给定子图内、引用的时间点是否落在对应事件区间内,不合规就降级重生成一次或者直接剥掉无法验证的引用。

这层后校验的价值比预期大。除了格式,它还能挡住一部分幻觉:如果模型引用了不存在的 ID 或者编了一个区间外的时间,几乎可以肯定这段内容是编的。我发现把它当作一个轻量的"幻觉探测信号"来用,比事后再去人工审效率高得多。

6.4 检索召回正常但答案缺细节

第四个坑有点反直觉:子图召回了正确的主事件,模型也说对了大方向,但细节缺失,比如问"具体数值是多少",回答是"提到了一个参数"却给不出数字。查下来是序列化阶段的 token 裁剪把包含数值的那个子事件块裁掉了——因为裁剪按分数排序,而那个子事件的向量相似度不高(描述里数值只占一小部分,被大量其他文本稀释)。

修复思路是让裁剪感知问题类型。事实型问题(问数值、名称、时间)对细节块敏感,裁剪时给"含数字、含引号内容、含专有名词"的块加权;过程型问题对结构敏感,优先保留梗概块和链路完整的一组子事件。这个改动改动量很小,就是在打分函数里加两三条特征,但对细节类问题的正确率提升明显。这件事让我意识到,裁剪策略不能只看相似度,得看问题要什么。

6.5 边界场景:无语音视频与超长视频

最后说两个边界场景。无语音视频(纯演示、纯监控、纯风景)的事件切分完全依赖视觉信号,这时候前面那套基于语义相似度的合并逻辑失效,我改用关键帧特征的相邻差异做切分,阈值要调得更敏感一些,同时把单事件时长上限压到 20 秒左右,因为无语音场景的信息密度通常在画面变化处,事件太长会把多个动作混在一起。这类视频的语音文本字段为空,检索时视觉描述权重必须调高,否则向量召回的输入几乎全是空的模板文本,等于没召回。

超长视频(三小时以上)的问题主要是图规模和时间距离衰减。事件数上千之后,向量索引的召回质量会下降,因为相似度分布变平;时序边衰减又让远端事件连不上。我的处理是加一层"章节级"的粗粒度索引——用主事件的聚类或固定时间窗(比如每 15 分钟)做一个超节点,先定位到章节再在章节内检索。这层索引不参与精细推理,只做范围收敛,成本很低但能明显改善超长视频的召回准确率。另外这类视频我建议在序列化时始终带上"当前检索范围在整片的哪个位置"这类全局提示,帮助模型建立时间感,否则它容易把开头的内容当成结尾。

这套东西做下来,我的整体感受是:语义事件图不是那种"换个模型就能提升"的优化,它是一次结构性投入——建图阶段要多花十几到二十分钟,要为字段设计和边构建的取舍反复权衡,但换来的是一次查询只读几百个 token 就能拿到有顺序、有关系、可溯源的证据链。真正决定效果的不是图有多复杂,而是三件事做得够不够扎实:事件粒度是否匹配问题粒度、检索是否真的用上了结构、裁剪是否知道问题要什么。这三件事任何一件偷懒,图就退化成一个带时间戳的文档库,那还不如老老实实做向量检索。

返回列表