RAG 本地素材库:从 Embedding 选型到召回调优
- 约 8 分钟读完 - 全文 2946 字RAG 本地素材库:从 Embedding 选型到召回调优
在我开发的方糖猫 AI 视频平台中,一个核心能力是「根据文案自动匹配视频素材」。用户的文案可能是「讲一个关于坚持的故事」,系统需要从本地素材库中找到最契合的画面。我们最终用 RAG(检索增强生成) 的思路搭建了本地素材库。这篇文章会完整复盘从 Embedding 选型到召回调优的全过程。
一、业务背景:为什么需要 RAG?
短视频生成平台里的素材通常有几类:
- 图片素材:背景图、人物图、概念图
- 视频片段:B-roll、转场、动画
- 文案片段:金句、案例、开场白
早期我们用关键词标签做检索,很快就发现几个问题:
- 标签覆盖不全。一个海边日落镜头,可能被标注为「海」「日落」「风景」「旅行」,但用户文案写的是「人生低谷后的曙光」,关键词完全对不上。
- 标签质量参差不齐。不同运营人员打的标签不一致,导致召回结果波动大。
- 无法跨模态匹配。图片素材没有文字描述,靠文件名搜索基本不可用。
RAG 的思路是:用 Embedding 把素材和查询都映射到同一向量空间,通过语义相似度做召回。这样即使关键词不同,只要语义接近,就能被检索出来。
二、系统设计
整个 RAG 素材库分为三个核心环节:
flowchart LR
subgraph S1["素材入库"]
A1["原始素材"] --> A2["内容提取"] --> A3["Embedding"] --> A4["ChromaDB"]
end
subgraph S2["查询召回"]
B1["用户文案"] --> B2["Embedding"] --> B3["Top-K 检索"] --> B4["重排"] --> B5["返回结果"]
end
subgraph S3["持续更新"]
C1["新增素材"] --> C2["增量 Embedding"] --> C3["索引更新"]
end
2.1 素材入库流程
flowchart LR
A["图片/视频"] --> B["抽帧/缩略图"] --> C["视觉特征提取(CLIP/ResNet)"] --> D["文本描述生成"] --> E["Embedding"] --> F["写入 ChromaDB"]
G["文本片段"] --> H["清洗/分块"] --> I["Embedding"] --> F
对于图片和视频,我们会:
- 视频抽帧为关键帧
- 用多模态模型生成一句话描述
- 同时保存原始文件路径、文件类型、使用次数等元数据
2.2 查询召回流程
# 简化版召回流程
def retrieve_materials(query: str, top_k: int = 10):
# 1. 查询向量化
query_embedding = embedding_model.encode(query)
# 2. 向量库初筛
candidates = chroma_collection.query(
query_embeddings=[query_embedding],
n_results=top_k * 3, # 多召回一些,留给重排
where={"status": "active"}
)
# 3. 重排序
ranked = reranker.rerank(query, candidates)
# 4. 返回 Top-K
return ranked[:top_k]三、Embedding 选型实测
Embedding 模型的选择直接决定召回质量。我们测试了以下几种:
| 模型 | 类型 | 维度 | 中文语义 | 多模态 | 延迟 | 成本 |
|---|---|---|---|---|---|---|
| text-embedding-ada-002 | 文本 | 1536 | 中等 | 否 | 快 | 低 |
| text-embedding-3-small | 文本 | 1536 | 较好 | 否 | 快 | 低 |
| BGE-M3 | 文本 | 1024 | 优秀 | 否 | 中等 | 本地 |
| OpenAI CLIP | 多模态 | 512 | 一般 | 是 | 中等 | 中等 |
| 中文 CLIP | 多模态 | 512 | 较好 | 是 | 中等 | 本地 |
3.1 文本 Embedding 选型
因为我们的查询主要是中文文案,最终选择了 BGE-M3,原因:
- 中文语义理解能力明显优于 OpenAI 的 Embedding
- 支持本地部署,无调用成本
- 向量维度 1024,检索速度和存储成本可接受
3.2 多模态 Embedding 选型
对于图片/视频素材,CLIP 系列可以把视觉内容和文本映射到同一空间。但实测发现:
- 通用 CLIP 对中文概念的支持较弱
- 视频场景下,关键帧的质量直接影响召回效果
最终方案:
- 关键帧用 中文 CLIP 生成视觉 Embedding
- 同时用图像描述模型生成一句话中文描述,再用 BGE-M3 生成文本 Embedding
- 向量库中同时存储两种 Embedding,查询时分别召回再融合
四、向量库设计
我们选用 ChromaDB 作为向量存储,主要考虑:
- 部署简单,适合小团队
- 支持元数据过滤(按项目、类型、状态筛选)
- Python 生态集成好
- 支持内存模式和持久化模式,开发方便
4.1 Collection 划分
# 按素材类型分 Collection
image_collection = chroma_client.get_or_create_collection("materials_image")
video_collection = chroma_client.get_or_create_collection("materials_video")
text_collection = chroma_client.get_or_create_collection("materials_text")分 Collection 的好处是查询更聚焦,也方便针对不同类型的素材调优。
4.2 文档结构设计
documents = [{
"id": "img_001",
"embedding": image_embedding,
"metadata": {
"type": "image",
"project_id": "proj_123",
"tags": ["海边", "日落", "风景"],
"file_url": "oss://ftmao/materials/img_001.jpg",
"width": 1920,
"height": 1080,
"status": "active",
"usage_count": 12
},
"document": "海边日落,宁静的风景画面" # 用于可读性和调试
}]五、召回调优实战
5.1 评测数据集构建
召回调优的前提是有评测数据。我们标注了 200 条查询-素材对:
{
"query": "人生低谷后重新出发",
"relevant": ["img_001", "img_015", "vid_008"],
"irrelevant": ["img_099", "vid_045"]
}评测指标:
- Recall@K:相关素材在 Top-K 中的占比
- Precision@K:Top-K 中相关素材的占比
- MRR:第一个相关素材的平均排名倒数
5.2 Embedding 调优
通过对比实验,我们发现:
| 方案 | Recall@10 | Precision@10 | MRR |
|---|---|---|---|
| 关键词标签 | 42% | 35% | 0.18 |
| BGE-M3 文本 Embedding | 68% | 54% | 0.31 |
| BGE-M3 + 中文 CLIP 融合 | 79% | 63% | 0.40 |
| + 重排序模型 | 86% | 71% | 0.48 |
5.3 查询改写
用户的原始查询往往比较抽象,比如「做一个励志视频」。直接用这个查询做向量检索,召回结果会比较散。
我们的做法是让 LLM 先对查询做改写和扩展:
输入:做一个励志视频
输出:
- 核心语义:励志、奋斗、坚持不懈
- 扩展查询:逆境中成长、永不放弃、追逐梦想、突破自我
- 情绪基调:激昂、温暖、希望
然后用扩展后的多个查询分别检索,最后合并结果。这个策略让 Recall@10 提升了约 8%。
5.4 重排序模型
初筛召回 30 个候选后,用轻量级重排序模型做二次排序。我们用的是 bge-reranker-base,原因:
- 模型小,CPU 推理足够快
- 对中文语义匹配效果好
- 可以 fine-tune 适应平台特定素材
六、工程化细节
6.1 索引更新策略
素材库是动态变化的,每天都有新增、删除、状态变更。我们采用:
- 增量更新:新增素材立即 Embedding 入库
- 批量补偿:每天晚上全量校验一次,修复漏入/错入
- 软删除:素材状态改为 inactive,不立即物理删除,便于恢复和审计
6.2 缓存与去重
热门查询的 Embedding 结果会被缓存到 Redis,缓存 key 是查询文本的 hash。同时 TTS 结果也按文案 hash 去重,避免重复调用。
def get_cached_or_compute(query: str):
key = f"rag:query:{hashlib.md5(query.encode()).hexdigest()}"
cached = redis.get(key)
if cached:
return json.loads(cached)
result = retrieve_materials(query)
redis.setex(key, 3600, json.dumps(result)) # 缓存 1 小时
return result6.3 多项目隔离
同一个平台可能服务多个创作者或项目,不同项目的素材库需要隔离。我们通过 project_id 元数据过滤实现:
results = collection.query(
query_embeddings=[embedding],
n_results=10,
where={"project_id": "proj_123", "status": "active"}
)七、踩过的坑
7.1 向量维度不一致
早期我们同时用了 OpenAI Embedding(1536 维)和 BGE-M3(1024 维),混在一个 Collection 里,结果查询时报维度错误。
解决:严格按模型维度分 Collection,每个 Collection 只存同一模型产出的向量。
7.2 关键帧质量影响召回
视频素材如果关键帧选得不好(比如黑屏、转场瞬间),CLIP 生成的 Embedding 会偏离视频真实内容。
解决:
- 用场景检测算法选取稳定画面
- 一个视频保存多个关键帧,查询时合并召回
- 人工抽检低质量素材
7.3 查询过短导致语义漂移
用户查询只有一个词,比如「奋斗」,召回结果可能包含大量不相关但语义宽泛的素材。
解决:
- 短查询自动扩展为完整句子
- 提高重排序模型的权重
- 对短查询增加元数据过滤
八、当前效果与下一步
目前这套 RAG 素材库已经支撑方糖猫平台的核心素材检索能力,覆盖了图片、视频片段、文案片段三类素材。
已验证的收益:
- 素材召回质量明显高于关键词检索
- 新素材无需人工打标签即可被检索到
- 跨项目复用素材变得可行
下一步计划:
- 引入用户反馈数据,做在线学习优化
- 针对视频内容做更细粒度的时序检索
- 探索多模态大模型直接做端到端素材匹配
九、总结
RAG 不只是 LLM 应用的一个热门概念,对于内容生成类平台来说,它是连接用户意图和内容资产的关键基础设施。
几个关键经验:
- Embedding 选型要以业务语言为主。中文场景不要迷信 OpenAI,BGE 系列性价比更高。
- 一定要有评测数据集。没有数据就没有优化方向。
- 召回 pipeline 通常是多阶段的:初筛 + 重排 + 业务规则过滤。
- 工程化比算法更重要。索引更新、缓存、隔离、监控,这些决定系统能不能长期稳定运行。
如果你也在做 RAG 相关的项目,欢迎交流 Embedding 选型和召回调优的经验。