ai_service.go 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. package service
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "log"
  8. "strings"
  9. "github.com/2930134478/AI-CS/backend/models"
  10. "github.com/2930134478/AI-CS/backend/repository"
  11. "github.com/2930134478/AI-CS/backend/service/rag"
  12. "github.com/2930134478/AI-CS/backend/utils"
  13. "gorm.io/gorm"
  14. )
  15. // AIService AI 服务(负责调用 AI 生成回复)
  16. type AIService struct {
  17. aiConfigRepo *repository.AIConfigRepository
  18. messageRepo *repository.MessageRepository
  19. conversationRepo *repository.ConversationRepository
  20. retrievalService *rag.RetrievalService // RAG 检索服务
  21. providerFactory *AIProviderFactory
  22. }
  23. // NewAIService 创建 AI 服务实例。
  24. func NewAIService(
  25. aiConfigRepo *repository.AIConfigRepository,
  26. messageRepo *repository.MessageRepository,
  27. conversationRepo *repository.ConversationRepository,
  28. retrievalService *rag.RetrievalService, // 添加 RAG 检索服务
  29. ) *AIService {
  30. return &AIService{
  31. aiConfigRepo: aiConfigRepo,
  32. messageRepo: messageRepo,
  33. conversationRepo: conversationRepo,
  34. retrievalService: retrievalService,
  35. providerFactory: NewAIProviderFactory(),
  36. }
  37. }
  38. // GenerateAIResponse 为对话生成 AI 回复。
  39. // conversationID: 对话ID
  40. // userMessage: 用户消息
  41. // userID: 用户ID(用于回退查找 AI 配置)
  42. // 返回: AI 回复内容,如果失败返回错误
  43. func (s *AIService) GenerateAIResponse(conversationID uint, userMessage string, userID uint) (string, error) {
  44. // 1. 获取对话信息,优先使用对话绑定的 AI 配置
  45. conversation, err := s.conversationRepo.GetByID(conversationID)
  46. if err != nil {
  47. return "", fmt.Errorf("获取对话失败: %v", err)
  48. }
  49. var config *models.AIConfig
  50. if conversation.AIConfigID != nil {
  51. // 使用对话绑定的配置(多厂商支持)
  52. config, err = s.aiConfigRepo.GetByID(*conversation.AIConfigID)
  53. if err != nil {
  54. return "", fmt.Errorf("获取 AI 配置失败: %v", err)
  55. }
  56. // 验证配置是否启用
  57. if !config.IsActive {
  58. return "", errors.New("该模型配置已禁用")
  59. }
  60. } else {
  61. // 回退:使用用户默认配置(向后兼容)
  62. config, err = s.aiConfigRepo.GetActiveByUserID(userID, "text")
  63. if err != nil {
  64. if errors.Is(err, gorm.ErrRecordNotFound) {
  65. return "", errors.New("未找到 AI 配置,请先在设置中配置 AI 服务")
  66. }
  67. return "", fmt.Errorf("获取 AI 配置失败: %v", err)
  68. }
  69. }
  70. // 2. 解密 API Key
  71. apiKey, err := utils.DecryptAPIKey(config.APIKey)
  72. if err != nil {
  73. return "", fmt.Errorf("解密 API Key 失败: %v", err)
  74. }
  75. // 3. 获取对话历史(用于上下文)
  76. history, err := s.buildConversationHistory(conversationID)
  77. if err != nil {
  78. log.Printf("⚠️ 获取对话历史失败: %v", err)
  79. // 即使获取历史失败,也继续处理(使用空历史)
  80. history = []MessageHistory{}
  81. }
  82. // 4. RAG 检索:从知识库中检索相关文档
  83. ragContext := ""
  84. if s.retrievalService != nil {
  85. ragContext, err = s.retrieveRAGContext(context.Background(), userMessage, conversation)
  86. if err != nil {
  87. log.Printf("⚠️ RAG 检索失败: %v,继续使用无知识库上下文", err)
  88. // RAG 检索失败不影响主流程,继续处理
  89. }
  90. }
  91. // 5. 构建增强的用户消息(包含 RAG 上下文)
  92. enhancedUserMessage := userMessage
  93. if ragContext != "" {
  94. enhancedUserMessage = s.buildRAGPrompt(userMessage, ragContext)
  95. }
  96. // 6. 解析适配器配置(如果有)
  97. var adapterConfig *AdapterConfig
  98. if config.AdapterConfig != "" {
  99. if err := json.Unmarshal([]byte(config.AdapterConfig), &adapterConfig); err != nil {
  100. log.Printf("⚠️ 解析适配器配置失败: %v,使用默认配置", err)
  101. }
  102. }
  103. // 7. 创建 AI 提供商
  104. aiConfig := AIConfig{
  105. APIURL: config.APIURL,
  106. APIKey: apiKey,
  107. Model: config.Model,
  108. ModelType: config.ModelType,
  109. Provider: config.Provider,
  110. AdapterConfig: adapterConfig,
  111. }
  112. provider, err := s.providerFactory.CreateProvider(aiConfig)
  113. if err != nil {
  114. return "", fmt.Errorf("创建 AI 提供商失败: %v", err)
  115. }
  116. // 8. 调用 AI 生成回复(使用增强的消息)
  117. response, err := provider.GenerateResponse(history, enhancedUserMessage)
  118. if err != nil {
  119. // AI 调用失败,返回友好的错误消息
  120. log.Printf("❌ AI 调用失败: %v", err)
  121. return "AI客服好像出了点差错,请联系人工客服解决", nil
  122. }
  123. return response, nil
  124. }
  125. // buildConversationHistory 构建对话历史(用于 AI 上下文)。
  126. func (s *AIService) buildConversationHistory(conversationID uint) ([]MessageHistory, error) {
  127. // 获取最近的对话消息(最多 10 条,避免上下文过长)
  128. messages, err := s.messageRepo.ListByConversationID(conversationID)
  129. if err != nil {
  130. return nil, err
  131. }
  132. // 只取最近 10 条消息
  133. startIdx := 0
  134. if len(messages) > 10 {
  135. startIdx = len(messages) - 10
  136. }
  137. history := make([]MessageHistory, 0)
  138. for i := startIdx; i < len(messages); i++ {
  139. msg := messages[i]
  140. // 跳过系统消息
  141. if msg.MessageType == "system_message" {
  142. continue
  143. }
  144. role := "user"
  145. if msg.SenderIsAgent {
  146. role = "assistant"
  147. }
  148. history = append(history, MessageHistory{
  149. Role: role,
  150. Content: msg.Content,
  151. })
  152. }
  153. return history, nil
  154. }
  155. // retrieveRAGContext 从知识库中检索相关文档内容
  156. // query: 用户查询文本
  157. // conversation: 对话信息(可能包含知识库 ID)
  158. // 返回: 检索到的文档内容(格式化后的字符串)
  159. func (s *AIService) retrieveRAGContext(ctx context.Context, query string, conversation *models.Conversation) (string, error) {
  160. // 确定知识库 ID(可以从对话中获取,或为空表示搜索所有知识库)
  161. // TODO: 后续在 Conversation 模型增加 KnowledgeBaseID 字段
  162. var knowledgeBaseID *uint
  163. // knowledgeBaseID = conversation.KnowledgeBaseID // 暂时注释,等模型字段添加后启用
  164. // 执行 RAG 检索(Top-K = 5,返回最相关的 5 个文档片段)
  165. // 使用重排序优化检索结果
  166. topK := 5
  167. results, err := s.retrievalService.RetrieveWithRerank(ctx, query, topK, knowledgeBaseID)
  168. if err != nil {
  169. return "", fmt.Errorf("RAG 检索失败: %w", err)
  170. }
  171. if len(results) == 0 {
  172. // 没有检索到相关文档
  173. return "", nil
  174. }
  175. // 格式化检索结果
  176. var contextParts []string
  177. for i, result := range results {
  178. // 只使用相似度较高的结果(Score 越小表示相似度越高)
  179. // 如果使用余弦相似度,通常阈值在 0.7-0.9 之间
  180. // 这里我们暂时不过滤,让所有结果都参与
  181. contextParts = append(contextParts, fmt.Sprintf("文档片段 %d:\n%s", i+1, result.Content))
  182. }
  183. return strings.Join(contextParts, "\n\n"), nil
  184. }
  185. // buildRAGPrompt 构建包含 RAG 上下文的 Prompt
  186. // userMessage: 用户原始消息
  187. // ragContext: RAG 检索到的文档内容
  188. // 返回: 增强后的用户消息(包含知识库上下文)
  189. func (s *AIService) buildRAGPrompt(userMessage string, ragContext string) string {
  190. // 构建 RAG Prompt 模板
  191. // 参考 PandaWiki 的 Prompt 格式
  192. prompt := fmt.Sprintf(`你是一个智能客服助手,请基于以下知识库内容回答用户的问题。
  193. 知识库内容:
  194. %s
  195. 用户问题:%s
  196. 请根据知识库内容回答用户的问题。如果知识库中没有相关信息,请礼貌地告知用户,并建议联系人工客服。
  197. 回答要求:
  198. 1. 基于知识库内容,提供准确、有用的回答
  199. 2. 如果知识库中有相关信息,请直接引用并解释
  200. 3. 如果知识库中没有相关信息,请诚实告知
  201. 4. 保持友好、专业的语气
  202. 5. 回答要简洁明了,避免冗长`, ragContext, userMessage)
  203. return prompt
  204. }