retrieval.go 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. package rag
  2. import (
  3. "context"
  4. "fmt"
  5. "strconv"
  6. "time"
  7. "github.com/2930134478/AI-CS/backend/repository"
  8. "github.com/2930134478/AI-CS/backend/service/embedding"
  9. )
  10. // RetrievalService RAG 检索服务
  11. type RetrievalService struct {
  12. vectorStoreService *VectorStoreService
  13. embeddingProvider embedding.EmbeddingProvider
  14. docRepo *repository.DocumentRepository // 按发布状态过滤
  15. kbRepo *repository.KnowledgeBaseRepository // 按知识库「参与 RAG」过滤
  16. cache *Cache
  17. reranker *SimpleReranker
  18. metrics *Metrics
  19. minScore float32 // 相似度阈值,默认 0.22(分段检索分数通常低于整篇文档)
  20. }
  21. // NewRetrievalService 创建 RAG 检索服务实例(仅已发布文档且所属知识库已开启 RAG 的参与检索)
  22. func NewRetrievalService(vectorStoreService *VectorStoreService, embeddingProvider embedding.EmbeddingProvider, docRepo *repository.DocumentRepository, kbRepo *repository.KnowledgeBaseRepository) *RetrievalService {
  23. return &RetrievalService{
  24. vectorStoreService: vectorStoreService,
  25. embeddingProvider: embeddingProvider,
  26. docRepo: docRepo,
  27. kbRepo: kbRepo,
  28. cache: NewCache(),
  29. reranker: NewSimpleReranker(),
  30. metrics: NewMetrics(),
  31. minScore: 0.22,
  32. }
  33. }
  34. // SetMinScore 设置 RAG 相似度阈值(IP/余弦,分段场景建议 0.2~0.35)
  35. func (s *RetrievalService) SetMinScore(score float32) {
  36. if score >= 0 && score <= 1 {
  37. s.minScore = score
  38. }
  39. }
  40. // EnableCache 启用检索缓存(ttl 单位为秒)
  41. func (s *RetrievalService) EnableCache(ttl time.Duration) {
  42. s.cache.SetTTL(int(ttl.Seconds()))
  43. }
  44. // Retrieve 执行 RAG 检索
  45. func (s *RetrievalService) Retrieve(ctx context.Context, query string, topK int, knowledgeBaseID *uint) ([]SearchResult, error) {
  46. startTime := time.Now()
  47. cacheHit := false
  48. var results []SearchResult
  49. var err error
  50. // 检查缓存
  51. if s.cache != nil {
  52. if cached, ok := s.cache.Get(query, topK, knowledgeBaseID); ok {
  53. results = cached
  54. cacheHit = true
  55. }
  56. }
  57. // 如果缓存未命中,执行检索
  58. if !cacheHit {
  59. svc, err := s.embeddingProvider.Get(ctx)
  60. if err != nil {
  61. s.metrics.RecordQuery(false, time.Since(startTime), false)
  62. return nil, fmt.Errorf("获取嵌入服务失败: %w", err)
  63. }
  64. // 向量化查询
  65. queryVectors, err := svc.EmbedTexts(ctx, []string{query})
  66. if err != nil {
  67. s.metrics.RecordQuery(false, time.Since(startTime), false)
  68. return nil, fmt.Errorf("查询向量化失败: %w", err)
  69. }
  70. if len(queryVectors) == 0 {
  71. s.metrics.RecordQuery(false, time.Since(startTime), false)
  72. return nil, fmt.Errorf("未返回查询向量")
  73. }
  74. // 转换知识库 ID
  75. var kbIDStr *string
  76. if knowledgeBaseID != nil {
  77. str := ConvertKnowledgeBaseID(*knowledgeBaseID)
  78. kbIDStr = &str
  79. }
  80. // 多取一些结果,过滤未发布文档后仍能凑满 topK
  81. searchLimit := topK * 3
  82. if searchLimit < 10 {
  83. searchLimit = 10
  84. }
  85. results, err = s.vectorStoreService.SearchVectors(ctx, queryVectors[0], searchLimit, kbIDStr)
  86. if err != nil {
  87. s.metrics.RecordQuery(false, time.Since(startTime), false)
  88. return nil, fmt.Errorf("向量检索失败: %w", err)
  89. }
  90. // 仅保留「已发布」的文档参与 RAG;未在 documents 表中的条目(如 FAQ)视为可展示
  91. results = s.filterByPublished(ctx, results, topK)
  92. // 相似度阈值过滤:Milvus 使用 IP(归一化嵌入时等同余弦相似度)
  93. results = s.filterByScore(results, s.minScore)
  94. // 缓存过滤后的结果(空结果不缓存,避免误伤后续查询)
  95. if s.cache != nil && len(results) > 0 {
  96. s.cache.Set(query, topK, knowledgeBaseID, results)
  97. }
  98. }
  99. // 记录指标
  100. s.metrics.RecordQuery(err == nil, time.Since(startTime), cacheHit)
  101. return results, err
  102. }
  103. // RetrieveWithRerank 执行带重排序的 RAG 检索
  104. func (s *RetrievalService) RetrieveWithRerank(ctx context.Context, query string, topK int, knowledgeBaseID *uint) ([]SearchResult, error) {
  105. // 先执行基础检索
  106. results, err := s.Retrieve(ctx, query, topK, knowledgeBaseID)
  107. if err != nil {
  108. return nil, err
  109. }
  110. // 重排序
  111. if s.reranker != nil {
  112. reranked, err := s.reranker.Rerank(ctx, query, results)
  113. if err != nil {
  114. // 重排序失败不影响主流程,返回原始结果
  115. return results, nil
  116. }
  117. return reranked, nil
  118. }
  119. return results, nil
  120. }
  121. // filterByPublished 仅保留「已发布」且所属知识库已开启 RAG 的文档;FAQ 保留;取前 topK 条
  122. func (s *RetrievalService) filterByPublished(ctx context.Context, results []SearchResult, topK int) []SearchResult {
  123. if s.docRepo == nil || len(results) == 0 {
  124. if len(results) > topK {
  125. return results[:topK]
  126. }
  127. return results
  128. }
  129. docIDs := make([]uint, 0, len(results))
  130. seen := make(map[uint]struct{})
  131. for _, r := range results {
  132. id, err := strconv.ParseUint(r.DocumentID, 10, 32)
  133. if err != nil {
  134. continue
  135. }
  136. uid := uint(id)
  137. if _, ok := seen[uid]; !ok {
  138. seen[uid] = struct{}{}
  139. docIDs = append(docIDs, uid)
  140. }
  141. }
  142. docs, err := s.docRepo.GetByIDs(docIDs)
  143. if err != nil {
  144. return results
  145. }
  146. unpublished := make(map[uint]struct{})
  147. docIDToKBID := make(map[uint]uint)
  148. for _, d := range docs {
  149. if d.Status != "published" {
  150. unpublished[d.ID] = struct{}{}
  151. }
  152. docIDToKBID[d.ID] = d.KnowledgeBaseID
  153. }
  154. // 知识库未参与 RAG 的集合
  155. disabledKBIDs := make(map[uint]struct{})
  156. if s.kbRepo != nil && len(docIDToKBID) > 0 {
  157. kbIDSet := make(map[uint]struct{})
  158. for _, kbID := range docIDToKBID {
  159. kbIDSet[kbID] = struct{}{}
  160. }
  161. kbIDs := make([]uint, 0, len(kbIDSet))
  162. for id := range kbIDSet {
  163. kbIDs = append(kbIDs, id)
  164. }
  165. if kbs, err := s.kbRepo.GetByIDs(kbIDs); err == nil {
  166. for _, kb := range kbs {
  167. if !kb.RAGEnabled {
  168. disabledKBIDs[kb.ID] = struct{}{}
  169. }
  170. }
  171. }
  172. }
  173. filtered := make([]SearchResult, 0, len(results))
  174. for _, r := range results {
  175. id, err := strconv.ParseUint(r.DocumentID, 10, 32)
  176. if err != nil {
  177. filtered = append(filtered, r)
  178. continue
  179. }
  180. uid := uint(id)
  181. if _, ok := unpublished[uid]; ok {
  182. continue
  183. }
  184. if kbID, inDoc := docIDToKBID[uid]; inDoc {
  185. if _, disabled := disabledKBIDs[kbID]; disabled {
  186. continue
  187. }
  188. }
  189. filtered = append(filtered, r)
  190. if len(filtered) >= topK {
  191. break
  192. }
  193. }
  194. return filtered
  195. }
  196. // filterByScore 按相似度阈值过滤结果。
  197. // Milvus 使用 IP 度量;归一化嵌入时分数等同余弦相似度。分段后 chunk 分数普遍低于整篇文档,阈值不宜过高。
  198. func (s *RetrievalService) filterByScore(results []SearchResult, minScore float32) []SearchResult {
  199. if len(results) == 0 {
  200. return results
  201. }
  202. filtered := make([]SearchResult, 0, len(results))
  203. for _, r := range results {
  204. if r.Score >= minScore {
  205. filtered = append(filtered, r)
  206. }
  207. }
  208. return filtered
  209. }
  210. // GetMetrics 获取性能指标
  211. func (s *RetrievalService) GetMetrics() map[string]interface{} {
  212. return s.metrics.GetStats()
  213. }