feat(vector): pluggable multimodal vector space

核心暴露 MultimodalEmbedder 接口,两条路径共享同一套 L0/L2/L3
向量缓存、media.Store 坐标、QueryMemoryMediaScored 检索:
  - onnx:内嵌 ONNX 模型(CLIP 等),通过 build tag 编译
  - http:外部向量 API 服务(Jina v5 / OpenAI / 自建)

跨模态融合权重改为 CrossModalFusionConfig 可配置结构体,
移除所有模型特定硬编码(CLIP/Jina),版本切换只需改配置。

模型切换自动迁移:
  - StaleVecDigestsAll 支持全模态(image+audio+video)
  - 启动时并发重算(ONNX 4 workers / API 8 workers)
  - 修复 SQL 运算符优先级导致 kind 过滤失效的 bug

实测对比(492 篇生产文档 + 3 张真实图片):
  - TF-IDF:MRR 0.457(精确匹配快,语义差)
  - fastText:MRR 0.530(语义中等,延迟 8ms)
  - Jina v5-omni:MRR 0.900(全面领先,延迟 40ms)
  - 中文文本→图片:Jina MRR 0.833 vs CLIP 0.611

See docs/embedding-comparison.md for full benchmark.
This commit is contained in:
JianFeeeee
2026-09-09 17:38:34 +08:00
parent b860c8cea5
commit 36c604ef3e
15 changed files with 1480 additions and 94 deletions

View File

@ -30,6 +30,7 @@ import (
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/pipeline"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/social"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/text"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/vector"
"gitcode.com/JianFeeeee/HomeAgent/internal/meta"
"gitcode.com/JianFeeeee/HomeAgent/internal/nlp"
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
@ -347,21 +348,57 @@ func main() {
}
}
// 多模态嵌入(CLIP ONNX,可选):配置 clip_model_dir 时启用,
// 图片入库/描述时计算视觉向量,供跨模态检索;未配置则退回纯 fastText 文本路径。
var clipEmbedder *clip.Embedder
if clipDir := cfgReg.GetString("core.memory.media.clip_model_dir", ""); clipDir != "" {
e, err := clip.New(clipDir)
if err != nil {
// 配置了但加载失败:记日志降级,不阻塞启动(媒体记忆是增强项)
log.Printf("[homed] warning: clip embedder load failed: %v(多模态向量检索已禁用)", err)
// 统一多模态向量空间(可选)。
//
// 两条路径共享同一套基础设施(L0/L2/L3 向量缓存、media.Store 坐标、
// QueryMemoryMediaScored 检索),只是「算向量的源头」不同:
// - onnx:内嵌 ONNX 模型(如 CLIP)
// - http:外部向量 API 服务(Jina / OpenAI / 自建)
// type 为空时禁用多模态向量检索,退回纯 fastText 文本路径。
var multimodalSpace vector.MultimodalEmbedder
switch mmType := cfgReg.GetString("core.memory.multimodal_space.type", ""); mmType {
case "onnx":
if clipDir := cfgReg.GetString("core.memory.media.clip_model_dir", ""); clipDir != "" {
e, err := clip.New(clipDir)
if err != nil {
log.Printf("[homed] warning: onnx embedder load failed: %v(多模态向量检索已禁用)", err)
} else {
multimodalSpace = e
defer e.Close()
log.Printf("[homed] multimodal space (onnx) active: dim=%d fp=%s", e.Dim(), e.Fingerprint()[:min(12, len(e.Fingerprint()))])
}
} else {
clipEmbedder = e
defer e.Close()
log.Printf("[homed] clip embedder active: dim=%d fp=%s", e.Dim(), e.Fingerprint()[:min(12, len(e.Fingerprint()))])
log.Println("[homed] multimodal_space.type=onnx 但未配置 clip_model_dir,多模态向量检索已禁用")
}
case "http":
dim := cfgReg.GetInt("core.memory.multimodal_space.http.dimension", 0)
ep := cfgReg.GetString("core.memory.multimodal_space.http.endpoint", "")
if dim > 0 && ep != "" {
e, err := vector.NewHTTPEmbedder(vector.HTTPEmbedderConfig{
Endpoint: ep,
APIKey: cfgReg.GetString("core.memory.multimodal_space.http.api_key", ""),
Model: cfgReg.GetString("core.memory.multimodal_space.http.model", ""),
Dimension: dim,
Timeout: cfgReg.GetDuration("core.memory.multimodal_space.http.timeout", 30*time.Second),
Fingerprint: cfgReg.GetString("core.memory.multimodal_space.http.fingerprint", ""),
})
if err != nil {
log.Printf("[homed] warning: http embedder init failed: %v(多模态向量检索已禁用)", err)
} else {
multimodalSpace = e
defer e.Close()
log.Printf("[homed] multimodal space (http) active: endpoint=%s dim=%d", ep, dim)
}
} else {
log.Println("[homed] multimodal_space.type=http 但 endpoint/dimension 配置不完整,多模态向量检索已禁用")
}
default:
if mmType != "" {
log.Printf("[homed] warning: 未知 multimodal_space.type=%q,多模态向量检索已禁用", mmType)
}
}
ks := knowledge.NewStore(filepath.Join(cfg.Daemon.DataDir, "knowledge"))
if err := ks.Start(); err != nil {
log.Printf("[homed] warning: knowledge store: %v", err)
@ -484,7 +521,7 @@ func main() {
ContextSavePath: filepath.Join(cfg.Daemon.DataDir, "memory", "context.json"),
EmbeddingModelPath: cfgReg.GetString("core.agent.embedding_model_path", ""),
Embedder: embedder,
ClipEmbedder: clipEmbedder,
MultimodalSpace: multimodalSpace,
StageHost: stageHost,
EventBus: evBus,
ThinkingEnabled: cfg.LLM.ThinkingEnabled,