Skip to main content

一、整体架构概览

LangChat 的 RAG 系统分为两条核心链路:文档入库(Ingestion)检索增强生成(Retrieval),共享同一套工厂层基础设施。
┌─────────────────────────────────────────────────────────────────┐
│                        工厂层(Factory)                          │
│  ┌──────────────────┐  ┌──────────────────┐  ┌───────────────┐  │
│  │ KnowledgeFactory │──│ VectorStoreFactory│  │EmbeddingModel │  │
│  │   (门面入口)    │  │  (向量库实例)   │  │   Factory     │  │
│  └──────────────────┘  └──────────────────┘  └───────────────┘  │
│         │                      │                     │          │
│         │              ┌───────┴───────┐             │          │
│         │              │   Caffeine    │             │          │
│         │              │   本地缓存    │             │          │
│         │              └───────────────┘             │          │
└─────────┼──────────────────────┼─────────────────────┼──────────┘
          │                      │                     │
    ┌─────┴──────┐         ┌─────┴──────┐        ┌────┴─────┐
    │  PgVector  │         │Elasticsearch│       │  Milvus  │
    │  (Hybrid)  │         │  (Hybrid)   │       │ (Vector) │
    └────────────┘         └─────────────┘       └──────────┘

二、向量存储设计

2.1 多后端支持

后端搜索模式特点
PgVectorHybrid(RRF 融合)向量 + PostgreSQL 全文检索,开箱即用
ElasticsearchHybrid(kNN + BM25)向量 + 全文检索,需企业版许可
MilvusVector Only纯向量检索,适合大规模数据集

2.2 实例缓存策略

向量存储实例通过 Caffeine 本地缓存管理,缓存键由连接参数(provider、host、port、username、password、database、dimension、tableName)的 MD5 哈希生成。
  • 相同配置参数的请求共享同一个连接实例
  • 每个知识库使用 knowledgeId 作为表名/集合名,实现数据隔离
  • 配置变更时通过 clearCache() 手动失效

2.3 Hybrid 搜索与 minScore 适配

PgVector 采用 RRF(Reciprocal Rank Fusion)算法融合两路排序:
RRF_Score = 1/(K + rank_vector) + 1/(K + rank_keyword)

K = 80 时:
  双路最高分 = 2/81 ≈ 0.0247(两路都排名 #1)
  单路最高分 = 1/81 ≈ 0.0123(只被一路搜索命中)
核心问题:用户配置的 minScore(如 0.69)是基于余弦相似度(0~1)的语义,直接用于 RRF 会过滤掉全部结果。 解决方案adjustMinScoreForHybrid() 通过 instanceof 检测存储类型,对 PgVector 以单路最高分为基准做比例映射:
effectiveMinScore = userMinScore × 1/(K + 1)

用户设 0.69 → 0.69 × 1/81 ≈ 0.0085
为什么用单路最高分而非双路最高分? Hybrid 搜索的价值在于两路互补。实际场景中,一个好结果往往只被其中一路命中:
场景向量排名关键词排名RRF 分数能否通过 0.0085?
两路都 #1110.0247
向量 #1,关键词 #5150.0241
向量 #1,关键词没命中10.0123
向量 #20,关键词没命中200.0100
向量 #50+,关键词没命中50+<0.0077✗(合理过滤)
如果用双路最高分 2/(K+1) 做映射,阈值为 0.017,第三、四行的好结果会被错误过滤。 用户无需感知底层搜索模式差异,0.69 默认值同时适用于 Vector 和 Hybrid 两种模式。

三、文档入库链路(Ingestion)

AigcDocsController                    ← HTTP 入口


EmbeddingServiceImpl                  ← 业务编排层

  ├─ DocumentSplitterService          ← 文档分割
  │    ├─ 纯文本 → SplitterUtil(LangChain4j 分割器)
  │    └─ 文件   → DocumentParseService
  │         ├─ SMART/CUSTOM 模式 → ParserFactory(PDF/Word/Excel/CSV/图片解析器)
  │         ├─ MINERU 模式     → 外部 OCR 服务 → Markdown 分割
  │         └─ QA 模式         → 逐行解析为问答对

  ├─ KnowledgeFactory                 ← 获取模型和存储
  │    ├─ EmbeddingModelFactory       → EmbeddingModel 实例
  │    └─ VectorStoreFactory          → EmbeddingStore 实例

  └─ EmbeddingExecutor                ← 批量向量化
       ├─ 分批(每批 60 条)
       ├─ embeddingModel.embedAll()   → 生成向量
       ├─ embeddingStore.addAll()     → 写入向量库
       └─ ConsumptionRecorder        → 记录 Token 消耗

关键数据流

原始文档 → 解析/分割 → TextSegment[] → 向量化 → 向量库

                          ├─ metadata.knowledgeId  知识库归属
                          ├─ metadata.docsId       文档归属
                          └─ metadata.indexHash    段落唯一标识(UUID)

                                 └─ 同步写入 MySQL(AigcSegment 表)
                                    用于运行时启用/禁用控制

四、检索链路(Retrieval)

系统提供两条检索路径:

4.1 直接搜索(知识库搜索 UI)

搜索请求


EmbeddingSearchService
  ├─ 1. 获取 EmbeddingModel + EmbeddingStore
  ├─ 2. 查询 MySQL 获取已启用的段落 hashId 集合
  ├─ 3. 向量化查询文本
  ├─ 4. adjustMinScoreForHybrid() 适配 minScore
  └─ 5. embeddingStore.search(EmbeddingSearchRequest)
         ├─ queryEmbedding  → 向量相似度检索
         ├─ query           → 关键词检索(Hybrid 模式)
         ├─ filter          → INDEX_HASH.isIn(hashIds)
         ├─ maxResults       → 召回数量
         └─ minScore         → 适配后的阈值

4.2 RAG 对话检索(Chat with RAG)

POST /aigc/chat/completions


ChatEndpoint → ChatServiceImpl
  ├─ handleAppInfo()     ← 从 App 配置加载 knowledgeIds
  └─ LcChatServiceImpl.streamingChat()

       ├─ ChatModelFactory       → StreamingChatModel

       └─ AiServiceBuilder.build()

            ├─ ChatMemory         → Redis 支持的消息窗口
            ├─ SystemMessage       → PromptProcessor 处理
            ├─ Tools               → DynamicToolFactory(插件 + MCP)

            └─ RAG(DefaultRetrievalAugmentor)

                 ├─ QueryRouter(DefaultQueryRouter)
                 │    将查询路由到所有注册的检索器

                 ├─ KnowledgeRetrieverBuilder ─────────────────┐
                 │    per 知识库:                               │
                 │    ├─ EmbeddingModel + EmbeddingStore        │
                 │    ├─ 查询已启用段落 → hashIds              │
                 │    ├─ adjustMinScoreForHybrid()             │
                 │    └─ EmbeddingStoreContentRetriever        │
                 │         (dynamicFilter, maxResults, minScore)│
                 │                                              │
                 ├─ GraphRetrieverBuilder ──────────────────────┤
                 │    per 知识库(需启用知识图谱 + Neo4j):     │
                 │    └─ GraphContentRetriever                 ├→ 合并
                 │         (Cypher 查询, maxHops=3)            │
                 │                                              │
                 ├─ SqlRetrieverBuilder ────────────────────────┤
                 │    per 数据源:                              │
                 │    └─ Text2SQL 检索器                        │
                 │                                              │
                 └─ ContentAggregator ─────────────────────────┘
                      ├─ 无 Rerank → 默认按分数排序
                      └─ 有 Rerank → ReRankingContentAggregator
                           └─ ScoringModel(RerankModelFactory)

五、段落启用/禁用机制

向量库中的数据不会被删除。运行时通过 MySQL 的 AigcSegment 表控制可见性:
AigcSegment 表
  ├─ status = true     入库成功
  ├─ enabled = true    用户启用
  └─ indexHash         对应向量库中的 metadata.INDEX_HASH
检索时查询 status=true AND enabled=true 的段落,收集 indexHash 集合,注入 MetadataFilter 过滤。这样用户禁用某个段落时,无需操作向量库,只需修改 MySQL 字段即可生效。

六、多路检索融合

当一次对话关联多个知识库时,系统会创建多个检索器实例。LangChain4j 的 DefaultQueryRouter 将同一查询发送给所有检索器,各自独立返回结果,最终由 ContentAggregator 合并:
场景聚合策略
未启用 Rerank按各检索器返回的原始分数排序
启用 Rerank所有结果送入 ScoringModel 重新打分排序
Rerank 模型支持通义、GiteeAI、SiliconFlow、Xinference 等多个提供商。

七、可观测性

组件职责
ConsumptionRecorder记录 Embedding Token 消耗(入库 + 检索)
ChatListener / ChatTrace记录对话生命周期(推理、RAG 来源、Tool 调用、Token 用量)
日志各层关键节点均有结构化日志,包含 knowledgeId、resultCount、score 等字段