Просмотр исходного кода

ai回复(切换到qdrant向量库)

yh 2 месяцев назад
Родитель
Сommit
006c4055f2

+ 1 - 0
fs-ai-api/src/main/java/com/fs/ai/rag/service/impl/QdrantServiceImpl.java

@@ -128,6 +128,7 @@ public class QdrantServiceImpl implements QdrantService {
         body.put("limit", req.getTopK() == null ? 5 : req.getTopK());
         body.put("with_payload", true);
         body.put("with_vector", true);
+        body.put("score_threshold", 0.5);
         if (req.getFilter() != null && !req.getFilter().isEmpty()) {
             body.put("filter", buildFilter(req.getFilter()));
         }

+ 4 - 0
fs-ai-api/src/main/resources/application.yml

@@ -21,6 +21,10 @@ ai:
     embedding-url: https://ark.cn-beijing.volces.com/api/v3/embeddings/multimodal
     embedding-api-key: ark-5c19ef54-158a-4e9d-ba90-be9c745a8ff6-5cc00
     embedding-model: doubao-embedding-vision-251215
+    # LLM模型
+    llm-url: https://ark.cn-beijing.volces.com/api/v3/chat/completions
+    llm-api-key: ark-5c19ef54-158a-4e9d-ba90-be9c745a8ff6-5cc00
+    llm-model: doubao-seed-1-6-lite-251015
 
     # 多模型路由(可选)。配置后优先走 routing/models/providers,旧 embedding-* 作为回退。
     routing:

+ 1 - 2
fs-qw-api-msg/src/main/java/com/fs/FsQwMsgAppApplication.java

@@ -4,7 +4,6 @@ import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
 import org.springframework.scheduling.annotation.EnableAsync;
-import org.springframework.scheduling.annotation.EnableScheduling;
 import org.springframework.transaction.annotation.EnableTransactionManagement;
 
 /**
@@ -13,7 +12,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
 @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
 @EnableTransactionManagement
 @EnableAsync
-@EnableScheduling
+//@EnableScheduling
 public class FsQwMsgAppApplication
 {
     public static void main(String[] args)

+ 256 - 3
fs-qw-api-msg/src/main/java/com/fs/app/controller/QwMsgController.java

@@ -1,11 +1,15 @@
 package com.fs.app.controller;
 
 import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.HttpUtil;
 import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.fs.chat.service.IChatDatasetService;
 import com.fs.common.core.domain.R;
-import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.uuid.IdUtils;
+import com.fs.company.domain.AiKnowledgeBase;
+import com.fs.company.service.AiKnowledgeBaseService;
 import com.fs.fastGpt.domain.FastGptRole;
 import com.fs.fastGpt.service.AiHookService;
 import com.fs.fastGpt.service.IFastGptRoleService;
@@ -28,7 +32,6 @@ import com.fs.qw.service.IQwUserVoiceLogService;
 import com.fs.sop.mapper.QwSopLogsMapper;
 import com.fs.sop.mapper.SopUserLogsInfoMapper;
 import com.fs.sop.params.GetQwSopLogsByJsApiParam;
-import com.fs.tenant.service.TenantInfoService;
 import com.fs.voice.utils.StringUtil;
 import com.fs.wxwork.dto.*;
 import com.fs.wxwork.service.WxWorkService;
@@ -37,6 +40,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.json.JSONObject;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.web.bind.annotation.*;
 
@@ -88,6 +92,14 @@ public class QwMsgController {
     @Autowired
     private TenantDataSourceUtil tenantDataSourceUtil;
 
+    @Value("${ai.api.base-url}")
+    private String aiApiBaseUrl;
+
+    @Autowired
+    private IChatDatasetService chatDatasetService;
+    @Autowired
+    private AiKnowledgeBaseService aiKnowledgeBaseService;
+
 
     @GetMapping("/sendExpressInfo/{orderId}")
     public R sendExpressInfo(@PathVariable Long orderId){
@@ -371,7 +383,10 @@ public class QwMsgController {
                         final String finalContent = content;
                         if (2000000000000000L-receiver>0){
                             log.info("id:{}, 客户发送", id);
-                            aiHookService.qwHookNotifyAiReply(id,sender,finalContent,wxWorkMsgResp.getUuid(),wxWorkMessageDTO.getMsgtype(),tenantId);
+//                            aiHookService.qwHookNotifyAiReply(id,sender,finalContent,wxWorkMsgResp.getUuid(),wxWorkMessageDTO.getMsgtype(),tenantId);
+                            // ====== Qdrant RAG AI 回复(替代 FastGPT) ======
+                            qdrantRagAiReply(id, sender, finalContent, wxWorkMsgResp.getUuid(),
+                                    wxWorkMessageDTO.getMsgtype(), tenantId, serverId);
                         }else {
                             log.info("销售发送");
                             aiHookService.qwHookNotifyAddMsgNew(id,receiver,content,wxWorkMsgResp.getUuid(),1);
@@ -582,4 +597,242 @@ public class QwMsgController {
         }
     }
 
+    // ========== Qdrant RAG AI 回复辅助方法 ==========
+
+    /**
+     * Qdrant RAG AI 回复主流程
+     * 向量化 → Qdrant 搜索 → 组装 Prompt → LLM 生成 → 回复用户
+     */
+    private void qdrantRagAiReply(Long qwUserId, Long sender, String content, String uid,
+                                   Integer msgType, Long tenantId, Long serverId) {
+        // 1. 校验用户和角色
+        QwUser user = qwUserMapper.selectQwUserById(qwUserId);
+        if (user == null) {
+            log.warn("qdrantRagAiReply: 用户为空, qwUserId={}", qwUserId);
+            return;
+        }
+        if (user.getFastGptRoleId() == null) {
+            log.warn("qdrantRagAiReply: 未绑定角色, qwUserId={}", qwUserId);
+            return;
+        }
+        if (user.getAiStatus() != null && user.getAiStatus() == 1) {
+            log.warn("qdrantRagAiReply: AI已下线, qwUserId={}", qwUserId);
+            return;
+        }
+
+        FastGptRole role = fastGptRoleService.selectFastGptRoleByRoleId(user.getFastGptRoleId());
+        if (role == null) {
+            log.warn("qdrantRagAiReply: 角色不存在, roleId={}", user.getFastGptRoleId());
+            return;
+        }
+
+        try {
+            // 2. 调用 Embedding API 获取向量
+            log.info("qdrantRagAiReply: 开始向量化, qwUserId={}", qwUserId);
+            List<Float> vector = callEmbeddingApi(content);
+            if (vector == null || vector.isEmpty()) {
+                log.error("qdrantRagAiReply: 向量化返回空结果");
+                return;
+            }
+            log.info("qdrantRagAiReply: 向量化成功, 维度={}", vector.size());
+
+            // 3. 查询知识库
+            LambdaQueryWrapper<AiKnowledgeBase> lqw = new LambdaQueryWrapper<>();
+            lqw.eq(AiKnowledgeBase::getDelFlag, 0);
+            List<AiKnowledgeBase> list = aiKnowledgeBaseService.list(lqw);
+
+            // 4. 调用 Qdrant 搜索,获取知识片段
+            StringBuilder knowledgeSb = new StringBuilder();
+            if (list != null && !list.isEmpty()) {
+                log.info("qdrantRagAiReply: 找到 {} 个知识库", list.size());
+                int chunkIdx = 1;
+                for (AiKnowledgeBase kb : list) {
+                    String collectionName = kb.getCollectionName();
+                    List<Map<String, Object>> results = callQdrantSearch(collectionName, vector, 5);
+                    if (results != null && !results.isEmpty()) {
+                        for (Map<String, Object> item : results) {
+                            Map<String, Object> payload = (Map<String, Object>) item.get("payload");
+                            if (payload != null && payload.get("document") != null) {
+                                knowledgeSb.append("【知识片段").append(chunkIdx++).append("】\n")
+                                        .append(payload.get("document")).append("\n\n");
+                            }
+                        }
+                    }
+                }
+                log.info("qdrantRagAiReply: 共获取 {} 个知识片段", chunkIdx - 1);
+            } else {
+                log.info("qdrantRagAiReply: 角色未关联知识库, 仅使用角色提示词");
+            }
+
+            // 5. 构建 Prompt
+            List<Map<String, String>> messages = new ArrayList<>();
+            StringBuilder systemPrompt = new StringBuilder();
+
+            // 角色提示词
+            if (role.getReminderWords() != null && !role.getReminderWords().isEmpty()) {
+                systemPrompt.append(role.getReminderWords()).append("\n\n");
+            }
+
+            // 知识库上下文
+            if (knowledgeSb.length() > 0) {
+                systemPrompt.append("请根据以下知识库内容回答用户问题:\n\n")
+                        .append(knowledgeSb)
+                        .append("\n")
+                        .append("要求:\n")
+                        .append("1. 优先基于以上参考内容回答\n")
+                        .append("2. 如果参考内容不足以回答,请根据你的知识回答\n")
+                        .append("3. 回答需专业、准确、易懂");
+            }
+
+            if (systemPrompt.length() > 0) {
+                Map<String, String> sysMsg = new HashMap<>();
+                sysMsg.put("role", "system");
+                sysMsg.put("content", systemPrompt.toString());
+                messages.add(sysMsg);
+            }
+
+            // 用户消息
+            Map<String, String> userMsg = new HashMap<>();
+            userMsg.put("role", "user");
+            userMsg.put("content", content);
+            messages.add(userMsg);
+
+            // 6. 调用 LLM
+            log.info("qdrantRagAiReply: 开始调用LLM, qwUserId={}", qwUserId);
+            String reply = callLlmApi(messages);
+            if (reply == null || reply.trim().isEmpty()) {
+                log.error("qdrantRagAiReply: LLM 返回为空");
+                return;
+            }
+            log.info("qdrantRagAiReply: LLM回复成功, 内容长度={}", reply.length());
+
+            // 7. 发送回复给客户
+            sendAiMsgToUser(reply, sender, uid, serverId);
+            log.info("qdrantRagAiReply: 消息发送成功, qwUserId={}, sender={}", qwUserId, sender);
+
+        } catch (Exception e) {
+            log.error("qdrantRagAiReply 异常: {}", e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 调用 Embedding API 获取文本向量
+     */
+    @SuppressWarnings("unchecked")
+    private List<Float> callEmbeddingApi(String text) {
+        try {
+            Map<String, Object> reqBody = new HashMap<>();
+            reqBody.put("text", text);
+            String url = aiApiBaseUrl + "/ai/embedding/create";
+            String respStr = HttpUtil.createPost(url)
+                    .header("Content-Type", "application/json")
+                    .body(JSON.toJSONString(reqBody))
+                    .timeout(30000)
+                    .execute()
+                    .body();
+
+            Map<String, Object> respMap = JSON.parseObject(respStr);
+            Object dataObj = respMap.get("data");
+            if (dataObj instanceof List) {
+                List<Float> vector = new ArrayList<>();
+                for (Object item : (List<?>) dataObj) {
+                    if (item instanceof Number) {
+                        vector.add(((Number) item).floatValue());
+                    }
+                }
+                return vector;
+            }
+            log.error("callEmbeddingApi: data 不是数组, resp={}", respStr);
+            return null;
+        } catch (Exception e) {
+            log.error("callEmbeddingApi 异常: {}", e.getMessage(), e);
+            return null;
+        }
+    }
+
+    /**
+     * 调用 Qdrant 搜索 API
+     */
+    @SuppressWarnings("unchecked")
+    private List<Map<String, Object>> callQdrantSearch(String collectionName, List<Float> vector, int topK) {
+        try {
+            Map<String, Object> reqBody = new HashMap<>();
+            reqBody.put("collectionName", collectionName);
+            reqBody.put("vector", vector);
+            reqBody.put("topK", topK);
+
+            String url = aiApiBaseUrl + "/qdrant/point/search";
+            String respStr = HttpUtil.createPost(url)
+                    .header("Content-Type", "application/json")
+                    .body(JSON.toJSONString(reqBody))
+                    .timeout(30000)
+                    .execute()
+                    .body();
+
+            Map<String, Object> respMap = JSON.parseObject(respStr);
+            Object dataObj = respMap.get("data");
+            if (dataObj instanceof List) {
+                List<Map<String, Object>> results = new ArrayList<>();
+                for (Object item : (List<?>) dataObj) {
+                    if (item instanceof Map) {
+                        Map<String, Object> result = (Map<String, Object>) item;
+                        results.add(result);
+                    }
+                }
+                return results;
+            }
+            return null;
+        } catch (Exception e) {
+            log.error("callQdrantSearch 异常, collectionName={}: {}", collectionName, e.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * 调用 LLM API
+     */
+    @SuppressWarnings("unchecked")
+    private String callLlmApi(List<Map<String, String>> messages) {
+        try {
+            Map<String, Object> reqBody = new HashMap<>();
+            reqBody.put("messages", messages);
+
+            String url = aiApiBaseUrl + "/ai/llm/chat";
+            String respStr = HttpUtil.createPost(url)
+                    .header("Content-Type", "application/json")
+                    .body(JSON.toJSONString(reqBody))
+                    .timeout(60000)
+                    .execute()
+                    .body();
+
+            Map<String, Object> respMap = JSON.parseObject(respStr);
+            Object dataObj = respMap.get("data");
+            if (dataObj instanceof Map) {
+                Map<String, Object> data = (Map<String, Object>) dataObj;
+                Object content = data.get("content");
+                return content != null ? content.toString() : null;
+            }
+            log.error("callLlmApi: data 格式异常, resp={}", respStr);
+            return null;
+        } catch (Exception e) {
+            log.error("callLlmApi 异常: {}", e.getMessage(), e);
+            return null;
+        }
+    }
+
+    /**
+     * 通过企微 iPad 协议发送文本消息给客户
+     */
+    private void sendAiMsgToUser(String content, Long sendId, String uuid, Long serverId) {
+        try {
+            WxWorkSendTextMsgDTO textDTO = new WxWorkSendTextMsgDTO();
+            textDTO.setSend_userid(sendId);
+            textDTO.setUuid(uuid);
+            textDTO.setContent(content);
+            textDTO.setIsRoom(false);
+            wxWorkService.SendTextMsg(textDTO, serverId);
+        } catch (Exception e) {
+            log.error("sendAiMsgToUser 异常: {}", e.getMessage(), e);
+        }
+    }
 }

+ 4 - 0
fs-qw-api-msg/src/main/resources/application.yml

@@ -8,3 +8,7 @@ spring:
 #    active: druid-hdt
 #    active: druid-sxjz
 #    active: druid-hzyy-test
+
+ai:
+  api:
+    base-url: http://localhost:9009

+ 2 - 9
fs-service/src/main/java/com/fs/qw/service/impl/QwUserServiceImpl.java

@@ -6,12 +6,6 @@ import cn.hutool.http.HttpRequest;
 import com.alibaba.fastjson.JSON;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
-//import com.cloud.host.CloudHostConfig;
-//import com.cloud.host.PoolInfoEnum;
-//import com.ecloud.sdk.ecs.v1.Client;
-//import com.ecloud.sdk.ecs.v1.model.VmRebootPath;
-//import com.ecloud.sdk.ecs.v1.model.VmRebootRequest;
-//import com.ecloud.sdk.ecs.v1.model.VmRebootResponse;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.CloudHostUtils;
@@ -25,7 +19,6 @@ import com.fs.company.service.ICompanyUserService;
 import com.fs.config.ai.AiHostProper;
 import com.fs.config.cloud.CloudHostProper;
 import com.fs.his.config.FsSysConfig;
-import com.fs.his.domain.FsDfAccount;
 import com.fs.his.utils.ConfigUtil;
 import com.fs.ipad.IpadSendUtils;
 import com.fs.ipad.param.WxSendAtMsgParam;
@@ -1317,8 +1310,8 @@ public class QwUserServiceImpl implements IQwUserService
         WxWorkSetCallbackUrlDTO wxWorkSetCallbackUrlDTO = new WxWorkSetCallbackUrlDTO();
 
         System.out.println("回调地址"+"http://saasqwapimsg.ylrzcloud.com/msg/callback/"+serverId + "/"+loginParam.getTenantId());
-        wxWorkSetCallbackUrlDTO.setUrl("http://saasqwapimsg.ylrzcloud.com/msg/callback/"+serverId+ "/"+loginParam.getTenantId());
-        //wxWorkSetCallbackUrlDTO.setUrl("http://cn-hk-bgp-4.ofalias.net:55081/msg/callback/"+serverId+ "/"+loginParam.getTenantId());
+//        wxWorkSetCallbackUrlDTO.setUrl("http://saasqwapimsg.ylrzcloud.com/msg/callback/"+serverId+ "/"+loginParam.getTenantId());
+        wxWorkSetCallbackUrlDTO.setUrl("http://wf89b6de.natappfree.cc/msg/callback/"+serverId+ "/"+loginParam.getTenantId());
         wxWorkSetCallbackUrlDTO.setUuid(data.getUuid());
         wxWorkService.SetCallbackUrl(wxWorkSetCallbackUrlDTO,serverId);