faq_service.go 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "log"
  6. "strconv"
  7. "strings"
  8. "github.com/2930134478/AI-CS/backend/models"
  9. "github.com/2930134478/AI-CS/backend/repository"
  10. "github.com/2930134478/AI-CS/backend/service/rag"
  11. "gorm.io/gorm"
  12. )
  13. // FAQService 负责 FAQ(常见问题)管理领域的业务编排。
  14. type FAQService struct {
  15. faqs *repository.FAQRepository
  16. retrievalService *rag.RetrievalService
  17. documentEmbeddingService *rag.DocumentEmbeddingService
  18. }
  19. // NewFAQService 创建 FAQService 实例。
  20. func NewFAQService(
  21. faqs *repository.FAQRepository,
  22. retrievalService *rag.RetrievalService,
  23. documentEmbeddingService *rag.DocumentEmbeddingService,
  24. ) *FAQService {
  25. return &FAQService{
  26. faqs: faqs,
  27. retrievalService: retrievalService,
  28. documentEmbeddingService: documentEmbeddingService,
  29. }
  30. }
  31. // CreateFAQ 创建新的 FAQ 记录。
  32. // 创建后会自动进行向量化(异步处理)
  33. func (s *FAQService) CreateFAQ(input CreateFAQInput) (*FAQSummary, error) {
  34. // 验证必填字段
  35. if input.Question == "" {
  36. return nil, errors.New("问题不能为空")
  37. }
  38. if input.Answer == "" {
  39. return nil, errors.New("答案不能为空")
  40. }
  41. // 创建 FAQ 记录
  42. faq := &models.FAQ{
  43. Question: input.Question,
  44. Answer: input.Answer,
  45. Keywords: input.Keywords,
  46. EmbeddingStatus: "pending", // 初始状态为待处理
  47. }
  48. if err := s.faqs.Create(faq); err != nil {
  49. return nil, err
  50. }
  51. // 异步进行向量化(避免阻塞)
  52. go s.embedFAQAsync(context.Background(), faq.ID, faq)
  53. return s.toSummary(faq), nil
  54. }
  55. // embedFAQAsync 异步进行 FAQ 向量化
  56. func (s *FAQService) embedFAQAsync(ctx context.Context, faqID uint, faq *models.FAQ) {
  57. // 更新状态为处理中
  58. faq.EmbeddingStatus = "processing"
  59. if err := s.faqs.Update(faq); err != nil {
  60. log.Printf("更新 FAQ %d 向量化状态失败: %v", faqID, err)
  61. return
  62. }
  63. // 构建向量化的内容(使用 Question + Answer)
  64. content := faq.Question + "\n" + faq.Answer
  65. // 获取知识库 ID(如果为空,使用默认值 0)
  66. kbID := uint(0)
  67. if faq.KnowledgeBaseID != nil {
  68. kbID = *faq.KnowledgeBaseID
  69. }
  70. // 进行向量化
  71. err := s.documentEmbeddingService.EmbedDocument(ctx, faqID, kbID, content)
  72. if err != nil {
  73. log.Printf("FAQ %d 向量化失败: %v", faqID, err)
  74. // 更新状态为失败
  75. faq.EmbeddingStatus = "failed"
  76. if updateErr := s.faqs.Update(faq); updateErr != nil {
  77. log.Printf("更新 FAQ %d 向量化状态失败: %v", faqID, updateErr)
  78. }
  79. return
  80. }
  81. // 更新状态为已完成,并保存向量 ID
  82. vectorID := strconv.FormatUint(uint64(faqID), 10)
  83. faq.VectorID = &vectorID
  84. faq.EmbeddingStatus = "completed"
  85. if err := s.faqs.Update(faq); err != nil {
  86. log.Printf("更新 FAQ %d 向量化状态失败: %v", faqID, err)
  87. }
  88. }
  89. // GetFAQ 获取 FAQ 详情。
  90. func (s *FAQService) GetFAQ(id uint) (*FAQSummary, error) {
  91. faq, err := s.faqs.GetByID(id)
  92. if err != nil {
  93. if errors.Is(err, gorm.ErrRecordNotFound) {
  94. return nil, errors.New("FAQ 不存在")
  95. }
  96. return nil, err
  97. }
  98. return s.toSummary(faq), nil
  99. }
  100. // ListFAQs 获取 FAQ 列表,支持关键词搜索。
  101. // query: 查询字符串
  102. // 搜索策略:优先使用向量检索,如果失败或查询为空,则使用关键词搜索
  103. func (s *FAQService) ListFAQs(query string) ([]FAQSummary, error) {
  104. // 如果查询为空,直接返回所有 FAQ(按关键词搜索的空查询)
  105. if query == "" {
  106. faqs, err := s.faqs.List(nil)
  107. if err != nil {
  108. return nil, err
  109. }
  110. summaries := make([]FAQSummary, 0, len(faqs))
  111. for _, faq := range faqs {
  112. summaries = append(summaries, *s.toSummary(&faq))
  113. }
  114. return summaries, nil
  115. }
  116. // 优先使用向量检索
  117. results, err := s.SearchByVector(context.Background(), query, 10, "")
  118. if err == nil && len(results) > 0 {
  119. // 向量检索成功,转换为 FAQSummary
  120. summaries := make([]FAQSummary, 0, len(results))
  121. for _, result := range results {
  122. // 从结果中提取 FAQ ID
  123. faqID, parseErr := strconv.ParseUint(result.DocumentID, 10, 32)
  124. if parseErr != nil {
  125. continue
  126. }
  127. // 获取完整的 FAQ 信息
  128. faq, getErr := s.faqs.GetByID(uint(faqID))
  129. if getErr != nil {
  130. continue
  131. }
  132. summaries = append(summaries, *s.toSummary(faq))
  133. }
  134. return summaries, nil
  135. }
  136. // 向量检索失败,回退到关键词搜索
  137. log.Printf("向量检索失败,使用关键词搜索: %v", err)
  138. keywords := s.parseKeywords(query)
  139. faqs, err := s.faqs.List(keywords)
  140. if err != nil {
  141. return nil, err
  142. }
  143. // 转换为 Summary
  144. summaries := make([]FAQSummary, 0, len(faqs))
  145. for _, faq := range faqs {
  146. summaries = append(summaries, *s.toSummary(&faq))
  147. }
  148. return summaries, nil
  149. }
  150. // SearchByVector 使用向量检索搜索 FAQ
  151. // query: 查询文本
  152. // topK: 返回前 K 个结果
  153. // knowledgeBaseID: 知识库 ID(可选,为空字符串则不过滤)
  154. func (s *FAQService) SearchByVector(ctx context.Context, query string, topK int, knowledgeBaseID string) ([]rag.SearchResult, error) {
  155. if s.retrievalService == nil {
  156. return nil, errors.New("检索服务未初始化")
  157. }
  158. var kbID *uint
  159. if knowledgeBaseID != "" {
  160. if id, err := strconv.ParseUint(knowledgeBaseID, 10, 64); err == nil {
  161. u := uint(id)
  162. kbID = &u
  163. }
  164. }
  165. return s.retrievalService.Retrieve(ctx, query, topK, kbID)
  166. }
  167. // UpdateFAQ 更新 FAQ 记录。
  168. // 如果 Question 或 Answer 发生变化,会同步更新向量
  169. func (s *FAQService) UpdateFAQ(id uint, input UpdateFAQInput) (*FAQSummary, error) {
  170. // 获取现有记录
  171. faq, err := s.faqs.GetByID(id)
  172. if err != nil {
  173. if errors.Is(err, gorm.ErrRecordNotFound) {
  174. return nil, errors.New("FAQ 不存在")
  175. }
  176. return nil, err
  177. }
  178. // 记录是否有内容变化(需要重新向量化)
  179. needReembed := false
  180. // 验证必填字段
  181. if input.Question != nil && *input.Question == "" {
  182. return nil, errors.New("问题不能为空")
  183. }
  184. if input.Answer != nil && *input.Answer == "" {
  185. return nil, errors.New("答案不能为空")
  186. }
  187. // 更新字段
  188. if input.Question != nil {
  189. if faq.Question != *input.Question {
  190. needReembed = true
  191. faq.Question = *input.Question
  192. }
  193. }
  194. if input.Answer != nil {
  195. if faq.Answer != *input.Answer {
  196. needReembed = true
  197. faq.Answer = *input.Answer
  198. }
  199. }
  200. if input.Keywords != nil {
  201. faq.Keywords = *input.Keywords
  202. }
  203. // 保存更新
  204. if err := s.faqs.Update(faq); err != nil {
  205. return nil, err
  206. }
  207. // 如果内容发生变化,需要重新向量化
  208. if needReembed {
  209. // 先删除旧的向量
  210. if faq.VectorID != nil {
  211. ctx := context.Background()
  212. if deleteErr := s.documentEmbeddingService.DeleteDocumentEmbedding(ctx, id); deleteErr != nil {
  213. log.Printf("删除 FAQ %d 旧向量失败: %v", id, deleteErr)
  214. }
  215. }
  216. // 异步重新向量化
  217. go s.embedFAQAsync(context.Background(), id, faq)
  218. }
  219. return s.toSummary(faq), nil
  220. }
  221. // DeleteFAQ 删除 FAQ 记录。
  222. // 删除时也会删除对应的向量
  223. func (s *FAQService) DeleteFAQ(id uint) error {
  224. // 检查记录是否存在
  225. faq, err := s.faqs.GetByID(id)
  226. if err != nil {
  227. if errors.Is(err, gorm.ErrRecordNotFound) {
  228. return errors.New("FAQ 不存在")
  229. }
  230. return err
  231. }
  232. // 删除向量(如果存在)
  233. if faq.VectorID != nil {
  234. ctx := context.Background()
  235. if deleteErr := s.documentEmbeddingService.DeleteDocumentEmbedding(ctx, id); deleteErr != nil {
  236. log.Printf("删除 FAQ %d 向量失败: %v", id, deleteErr)
  237. // 不中断删除流程,记录日志即可
  238. }
  239. }
  240. // 删除记录
  241. return s.faqs.Delete(id)
  242. }
  243. // QuickSearch 快速搜索 FAQ(用于聊天输入框),OR 宽松匹配。
  244. func (s *FAQService) QuickSearch(q string, limit int) ([]FAQSummary, error) {
  245. if strings.TrimSpace(q) == "" {
  246. return []FAQSummary{}, nil
  247. }
  248. faqs, err := s.faqs.QuickSearch(q, limit)
  249. if err != nil {
  250. return nil, err
  251. }
  252. summaries := make([]FAQSummary, 0, len(faqs))
  253. for _, faq := range faqs {
  254. summaries = append(summaries, *s.toSummary(&faq))
  255. }
  256. return summaries, nil
  257. }
  258. // parseKeywords 解析关键词查询字符串。
  259. // 输入格式:关键词之间用 % 分隔,例如 "openai%api%调用"
  260. // 返回:关键词数组
  261. func (s *FAQService) parseKeywords(query string) []string {
  262. if query == "" {
  263. return nil
  264. }
  265. // 按 % 分隔
  266. parts := strings.Split(query, "%")
  267. keywords := make([]string, 0, len(parts))
  268. for _, part := range parts {
  269. // 去除首尾空格
  270. keyword := strings.TrimSpace(part)
  271. if keyword != "" {
  272. keywords = append(keywords, keyword)
  273. }
  274. }
  275. return keywords
  276. }
  277. // toSummary 将 FAQ 模型转换为 Summary。
  278. func (s *FAQService) toSummary(faq *models.FAQ) *FAQSummary {
  279. return &FAQSummary{
  280. ID: faq.ID,
  281. Question: faq.Question,
  282. Answer: faq.Answer,
  283. Keywords: faq.Keywords,
  284. CreatedAt: faq.CreatedAt,
  285. UpdatedAt: faq.UpdatedAt,
  286. }
  287. }