Forráskód Böngészése

基于公众号文章RAG问答助手上线

程序员七七 5 hónapja
szülő
commit
303031cd6e

+ 3 - 1
src/main/java/com/webchat/common/enums/PromptTemplateEnum.java

@@ -17,7 +17,9 @@ public enum PromptTemplateEnum {
 
     ROBOT_CHAT("/ftl/ROBOT_CHAT.ftl", "机器人对话"),
 
-    ROBOT_FC("/ftl/ROBOT_FC.ftl", "大模型意图识别")
+    ROBOT_FC("/ftl/ROBOT_FC.ftl", "大模型意图识别"),
+
+    RAG("/ftl/RAG.ftl", "公众号文章RAG问答"),
     ;
 
     private String path;

+ 57 - 0
src/main/java/com/webchat/controller/client/RAGBotController.java

@@ -0,0 +1,57 @@
+package com.webchat.controller.client;
+
+
+import com.webchat.common.bean.APIResponseBean;
+import com.webchat.common.bean.APIResponseBeanUtil;
+import com.webchat.common.exception.BusinessException;
+import com.webchat.common.helper.SessionHelper;
+import com.webchat.common.helper.SseEmitterHelper;
+import com.webchat.common.util.ThreadPoolExecutorUtil;
+import com.webchat.config.annotation.ValidateLogin;
+import com.webchat.domain.vo.llm.LLMChatRequestDTO;
+import com.webchat.service.ai.RAGBotService;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.Assert;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+import javax.servlet.http.HttpServletRequest;
+
+@RestController
+@RequestMapping("/api/rag/bot")
+public class RAGBotController {
+
+
+    @Autowired
+    private RAGBotService ragBotService;
+
+    /**
+     * 获取SSE链接对象
+     *
+     * @param request
+     * @return
+     */
+    @ValidateLogin
+    @GetMapping(path = "/stream", produces = "text/event-stream")
+    public SseEmitter stream(HttpServletRequest request) {
+        String biz = request.getParameter("biz");
+        String userId = SessionHelper.getCurrentUserId();
+        if (StringUtils.isBlank(userId)) {
+            throw new BusinessException("对话Stream流长链接获取异常,用户未登录!");
+        }
+        return SseEmitterHelper.get(biz, userId);
+    }
+
+
+    @ValidateLogin
+    @PostMapping("/chat/send")
+    public APIResponseBean chat(@RequestBody LLMChatRequestDTO llmChatRequestDTO) {
+        Assert.isTrue(llmChatRequestDTO != null, "参数为空");
+        Assert.isTrue(StringUtils.isNotBlank(llmChatRequestDTO.getMessage()), "输入内容为空");
+        Assert.isTrue(StringUtils.isNotBlank(llmChatRequestDTO.getMessage().trim()), "输入内容为空");
+        String userId = SessionHelper.getCurrentUserId();
+        ThreadPoolExecutorUtil.execute(() -> ragBotService.chat(userId, llmChatRequestDTO));
+        return APIResponseBeanUtil.success(true);
+    }
+}

+ 12 - 0
src/main/java/com/webchat/domain/vo/llm/LLMChatRequestDTO.java

@@ -0,0 +1,12 @@
+package com.webchat.domain.vo.llm;
+
+import lombok.Data;
+
+@Data
+public class LLMChatRequestDTO {
+
+
+    private String biz;
+
+    private String message;
+}

+ 73 - 0
src/main/java/com/webchat/service/ai/RAGBotService.java

@@ -0,0 +1,73 @@
+package com.webchat.service.ai;
+
+import com.webchat.common.bean.APIPageResponseBean;
+import com.webchat.common.enums.PromptTemplateEnum;
+import com.webchat.common.helper.SseEmitterHelper;
+import com.webchat.common.util.JsonUtil;
+import com.webchat.domain.vo.dto.ChatMessageSearchResultDTO;
+import com.webchat.domain.vo.llm.ChatCompletionMessage;
+import com.webchat.domain.vo.llm.ChatMessageRole;
+import com.webchat.domain.vo.llm.LLMChatRequestDTO;
+import com.webchat.domain.vo.response.publicaccount.ArticleBaseResponseVO;
+import com.webchat.service.FreeMarkEngineService;
+import com.webchat.service.elastic.WebChatElasticSearchService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+import java.io.IOException;
+import java.util.*;
+
+@Slf4j
+@Service
+public class RAGBotService {
+
+    @Autowired
+    private FreeMarkEngineService freeMarkEngineService;
+
+    @Autowired
+    private WebChatElasticSearchService webChatElasticSearchService;
+
+    @Value("${llm.config.model}")
+    private String model;
+
+    public void chat(String userId, LLMChatRequestDTO requestDTO) {
+        SseEmitter sseEmitter = SseEmitterHelper.get(requestDTO.getBiz(), userId);
+        if (sseEmitter == null) {
+            return;
+        }
+        ArticleBaseResponseVO article = queryDocumentsFormES(userId, requestDTO.getMessage());
+        String document = "-";
+        if (article != null) {
+            try {
+                String resourceUrl = "/article/" + article.getId();
+                if (StringUtils.isNotBlank(resourceUrl)) {
+                    sseEmitter.send("<a href=\""+resourceUrl+"\" target=\"_blank\"><button class='refDocument'>公众号资料源:"+article.getTitle()+"</button><br/></a>");
+                }
+            } catch (IOException e) {}
+            document = JsonUtil.toJsonString("内容标题:" + article.getTitle()+"\n 内容正文"+article.getContent());
+        }
+        Map<String, Object> vars = new HashMap<>();
+        vars.put("input", requestDTO.getMessage());
+        vars.put("document", document);
+        try {
+            String prompt = freeMarkEngineService.getContentByTemplate(PromptTemplateEnum.RAG.getPath(), vars);
+            final List<ChatCompletionMessage> messageList = Arrays.asList(
+                    new ChatCompletionMessage(ChatMessageRole.SYSTEM.value(), String.format("你是《webchat》的基于公众号文章的智能问答助理!")),
+                    new ChatCompletionMessage(ChatMessageRole.USER.value(), prompt));
+            LLMServiceFactory.getLLMService(model).chat(sseEmitter, messageList);
+        } catch (Exception e) {
+            log.error("知识社区RAG问答处理异常====> 参数={}", JsonUtil.toJsonString(requestDTO), e);
+        }
+    }
+
+    private ArticleBaseResponseVO queryDocumentsFormES(String userId, String q) {
+        APIPageResponseBean<ChatMessageSearchResultDTO> apiPageResponseBean = webChatElasticSearchService.query(null, userId, q, 1, 1);
+        List<ChatMessageSearchResultDTO> dataList = apiPageResponseBean.getData();
+        return CollectionUtils.isEmpty(dataList) ? null : dataList.get(0).getArticle();
+    }
+}

+ 1 - 1
src/main/java/com/webchat/service/elastic/AbstractWebChatElasticSearchClient.java

@@ -104,7 +104,7 @@ public abstract class AbstractWebChatElasticSearchClient<T extends AbstractBaseE
      * @param size   搜索条数
      * @return
      */
-    public APIPageResponseBean<List<T>> query(Integer type, String userId, String q , int page, int size) {
+    public APIPageResponseBean<T> query(Integer type, String userId, String q , int page, int size) {
         SearchRequest searchRequest = new SearchRequest(getIndexName());
         SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
         // 创建HighlightBuilder并设置高亮标签

+ 2 - 0
src/main/resources/application-dev.yml

@@ -6,6 +6,8 @@ server:
   servlet:
     session:
       timeout: 60s
+  tomcat:
+    max-threads: 500
 
 # set enable swagger
 swagger:

+ 37 - 0
src/main/resources/static/css/client/chat.css

@@ -750,4 +750,41 @@ body {
     z-index: 999;
     box-shadow: 0px 5px 20px #dfdede;
     display: none;
+}
+#agent-container {
+    position: fixed;
+    right: 40px;
+    bottom: 40px;
+    width: 450px;
+    height: 85%;
+    background-color: white;
+    box-shadow: 0px 0px 30px #d6d3d3;
+    z-index: 1999;
+    border-radius: 5px;
+}
+#agent-title-container {
+    position: absolute;
+    left: 0px;
+    height: 45px;
+    line-height: 45px;
+    text-align: left;
+    padding: 0px 20px;
+    width: calc(100% - 40px);
+    border-bottom: 1px solid whitesmoke;
+    text-align: left;
+}
+#agent-chat-container {
+    position: absolute;
+    left: 0px;
+    top: 46px;
+    width: 100%;
+    height: calc(100% - 46px);
+}
+#closeAgentIcon {
+    position: absolute;
+    right: 20px;
+    top: 15px;
+}
+#closeAgentIcon:hover {
+    cursor: pointer;
 }

+ 12 - 0
src/main/resources/templates/client/chat.html

@@ -120,6 +120,17 @@
             <button id="acceptOffer">接通</button>
         </div>
     </div>
+
+    <div id="agent-container" style="display: none">
+        <div id="agent-title-container" style="color: #324bec">
+            🤖 有问题找“小C”,你的IM内容搜索增强问答助手(测试版)
+            <svg id="closeAgentIcon" t="1734629200504" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5101" width="16" height="16"><path d="M614.693 511.999l308.085 308.09c18.905 18.9 18.905 49.55 0 68.459l-34.23 34.23c-18.906 18.906-49.56 18.906-68.46 0l-308.09-308.084L203.91 922.778c-18.9 18.906-49.55 18.906-68.456 0l-34.234-34.23c-18.905-18.908-18.905-49.56 0-68.458l308.085-308.091L101.22 203.91c-18.905-18.9-18.905-49.55 0-68.46l34.234-34.229c18.906-18.905 49.556-18.905 68.456 0L512 409.306l308.09-308.085c18.9-18.905 49.555-18.905 68.459 0l34.23 34.23c18.905 18.91 18.905 49.564 0 68.46L614.693 511.998z m0 0" fill="#2c2c2c" p-id="5102"></path></svg>
+        </div>
+        <div id="agent-chat-container">
+
+        </div>
+    </div>
+    <a><img id="rag" onmouseover="layer.tips('hi~ 我是小C,你的WebChat知识问答助手', this)" src="https://coderutil.oss-cn-beijing.aliyuncs.com/bbs-image/file_56498919dec7403c8e6accb60ed637a5.png?x-oss-process=image/resize,m_fill,w_100,h_100" style="position: fixed; bottom: 80px; right: 80px; width: 60px; z-index: 999; animation: moveUpDown 3s linear infinite; transfrom:translateY(5px); transform: rotate(-10deg);"></a>
 </center>
 <input type="file" id="fileInput" style="display: none;" />
 <input type="file" id="photoInput" style="display: none;" />
@@ -128,6 +139,7 @@
 <script src="/js/client/chat.js" type="text/javascript"></script>
 <script src="/js/client/user.js" type="text/javascript"></script>
 <script src="/js/client/search.js" type="text/javascript"></script>
+<script src="/js/client/rag.js" type="text/javascript"></script>
 <script>
     var me;
     var userId;

+ 355 - 0
src/main/resources/templates/client/rag.html

@@ -0,0 +1,355 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="referrer" content="no-referrer">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
+    <meta http-equiv="pragma" content="no-cache">
+    <meta http-equiv="cache-control" content="no-cache">
+    <meta http-equiv="expires" content="0">
+
+    <title>社区Agent智能助理,基于RAG技术的垂直问答助手</title>
+
+    <link href="/ref/layui-v2.6.8/layui/css/layui.css" rel="stylesheet" type="text/css" />
+    <link rel="stylesheet" href="/ref/highlight/default.min.css">
+
+    <script src="/ref/highlight/highlight.min.js" type="text/javascript"></script>
+    <script src="/ref/jquery/jquery-3.4.1.js" type="text/javascript"></script>
+    <script src="/ref/layui-v2.6.8/layui/layui.js" type="text/javascript"></script>
+    <script src="/js/client/marked.min.js"></script>
+    <style>
+        #in {
+            position: absolute;
+            left: 5%;
+            bottom: 40px;
+            width: 90%;
+            height: 50px;
+            border: 1px solid #1f77ff;
+            border-radius: 100px;
+            line-height: 50px;
+            font-size: 15px;
+            text-indent: 2em;
+        }
+        #rag-chat-message-container {
+            position: absolute;
+            top: 10px;
+            left: 5%;
+            width: 90%;
+            height: calc(100% - 140px);
+            /*background-color: black;*/
+            overflow-y: scroll;
+        }
+        #rag-chat-message-container::-webkit-scrollbar {/*滚动条整体样式*/
+            width: 0px;     /*高宽分别对应横竖滚动条的尺寸*/
+            height: 0px;
+        }
+        #rag-chat-message-container::-webkit-scrollbar-thumb {/*滚动条里面小方块*/
+            border-radius: 0px;
+            background-color: #ffffff;
+            background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .2) 50%, rgba(255, 255, 255, .2) 75%, transparent 75%, transparent);
+        }
+        #rag-chat-message-container::-webkit-scrollbar-track {/*滚动条里面轨道*/
+            -webkit-box-shadow: inset 0 0 0px rgba(255, 255, 255, 0.2);
+            background: #ffffff;
+        }
+        .chat-message-item {
+            position: relative;
+            min-width: 50px;
+            width: auto;
+            height: auto;
+            display: flex;
+            align-items: center;
+            text-align: left;
+            margin-bottom: 20px;
+        }
+        .chat-message-photo {
+            position: relative;
+            float: left;
+            width: 40px;
+            min-height: 40px;
+            margin-right: 10px;
+        }
+        .user-avatar {
+            position: absolute;
+            top: 0px;
+            left: 0px;
+            width: 40px;
+            height: 40px;
+            border-radius: 100px;
+        }
+        .chat-message-content {
+            position: relative;
+            float: left;
+            width: max-content;
+            max-width: calc(100% - 70px);
+            padding: 10px;
+            border-radius: 10px;
+            background-color: whitesmoke;
+            color: black;
+        }
+        #agentIcon {
+            position: absolute;
+            right: 45px;
+            bottom: 48px;
+            height: 35px;
+        }
+        .chat-message-content img {
+            max-width: 90%;
+            height: auto;
+            margin: 5px;
+            border-radius: 7px;
+        }
+        #agentIcon:hover {
+            cursor: pointer;
+        }
+        .robot-message a {
+            color: #f8ddd5;
+            text-decoration: underline;
+        }
+        /* 定义动画 */
+        @keyframes bounce {
+            0%, 100% {
+                transform: translateY(0);
+            }
+            50% {
+                transform: translateY(-5px);
+            }
+        }
+        @keyframes moveUpDown {
+            0% {
+                transform: translateY(0);
+            }
+            25% {
+                transform: translateY(-5px);
+                transform: rotate(-10deg);
+            }
+            50% {
+                transform: translateY(0);
+                transform: rotate(0deg);
+            }
+            75% {
+                transform: translateY(5px);
+                transform: rotate(10deg);
+            }
+            100% {
+                transform: translateY(0);
+                transform: rotate(0deg);
+            }
+        }
+
+        /* 应用动画到按钮 */
+        .bounce-loading {
+            position: relative;
+            width: 8px;
+            height: 8px;
+            margin-top: 8px;
+            background-color: white;
+            border-radius: 100px;
+            border: none;
+            animation: bounce 0.5s infinite ease-in-out;
+            cursor: progress; /* 显示进度指示器光标 */
+        }
+        .refDocument {
+            position: relative;
+            background-color: #031f7c;
+            color: white;
+            text-align: left;
+            padding: 5px 10px;
+            border: none;
+            border-radius: 8px;
+            float: left;
+            font-size: 13px;
+            max-width: calc(100% - 10px);
+            margin-bottom: 10px;
+        }
+        .refDocument i {
+            font-size: 15px;
+        }
+        .agent-recommend-container {
+            position: relative;
+            width: 80%;
+            min-height: 50px;
+            height: auto;
+            margin-top: 100px;
+            margin-bottom: 30px;
+        }
+        .agent-recommend-title {
+            font-size: 25px;
+            margin-bottom: 20px;
+        }
+        .agent-recommend-question-title {
+            line-height: 60px;
+            width: 100%;
+            color: #a39f9f;
+        }
+        .agent-recommend-question {
+            position: relative;
+            width: 100%;
+            height: 45px;
+            line-height: 45px;
+            background-color: transparent;
+            border-radius: 5px;
+            border: 1px solid #eae7e7;
+            margin-bottom: 10px;
+            text-align: left;
+            text-indent: 2em;
+        }
+        .agent-recommend-question:hover {
+            cursor: pointer;
+            color: #0a71f1;
+            border: 1px solid #0a71f1;
+        }
+    </style>
+</head>
+<body style="overflow-x: hidden; background-color: white;">
+    <center>
+        <div id="rag-chat-message-container">
+
+            <div class='agent-recommend-container'>
+                <div class='agent-recommend-title'>👏 我是WebChat公众号文章内容问答助手,有问题找小C</div>
+                <div class='agent-recommend-question-title'>RAG:基于社区内知识检索增强,生成式机器人</div>
+            </div>
+
+        </div>
+        <input id="in" placeholder="社区内容学习,有问题找“小C”">
+        <img id="agentIcon" src="https://coderutil.oss-cn-beijing.aliyuncs.com/bbs-image/file_56498919dec7403c8e6accb60ed637a5.png?x-oss-process=image/resize,m_fill,w_40,h_40">
+    </center>
+</body>
+<script src="/js/common/login.js"></script>
+<script>
+    var user = "";
+    var chatting = false;
+    var systemMessage = '';
+    var chatChannelApi = "/api/rag/bot/stream?biz=CC_AGENT";
+    var agentMessageId = 0;
+    loadUser();
+    function loadUser() {
+        $.ajax({
+            url:"/api/user/getCurrentUserInfo",
+            type:"get",
+            success:function (data) {
+                data = eval(data);
+                if (data.success){
+                    user = data.data;
+                    chatChannel();
+                } else {
+                    location.href = "/auth";
+                }
+            }, error:function () {
+                location.href = "/auth";
+            }
+        })
+    }
+
+    function chatChannel() {
+        systemMessage = '';
+        chatting = false;
+        var eventSource = new EventSource(chatChannelApi);
+        eventSource.onopen = function(e){
+            console.log("Connection Opened");
+        };
+        eventSource.onmessage = function(e){
+            chatting = true;
+            var message = e.data;
+            systemMessage += message;
+            if (e.data == ' ') {
+                systemMessage = systemMessage + ' ';
+            }
+            if (e.data == 'finished') {
+                console.log("本轮对话结束!");
+                chatting = false;
+                systemMessage = '';
+            } else {
+                document.querySelectorAll('pre code').forEach(function(block) {
+                    hljs.highlightBlock(block);
+                });
+                $("#agent-message-"+agentMessageId).html(marked.parse(systemMessage).replaceAll("&lt;", "<").replaceAll("&gt;", ">"));
+                refreshChatMessContainerScrollBar2Bottom();
+            }
+        };
+        eventSource.onerror = function(e){
+            console.log("Error");
+        };
+    }
+
+    // 监听回车事件
+    $("#in").keydown(function(event) {
+        if (event.keyCode == 13) {
+            doChat();
+        }
+    })
+
+    $("#agentIcon").on('click', function () {
+        doChat();
+    })
+
+    function doChat() {
+        var message = $("#in").val();
+        doChatting(message);
+    }
+
+    function doChatting(message) {
+        if (message == '') {
+            layer.msg("请输入内容");
+            return;
+        }
+        if (chatting) {
+            layer.msg("对话进行中");
+            return;
+        }
+        agentMessageId += 1;
+        createUserChatItem(message);
+        createRobotChatItem(agentMessageId);
+        $.ajax({
+            url: "/api/rag/bot/chat/send",
+            type: "post",
+            dataType: "json",
+            contentType: "application/json;charset=utf-8",
+            data: JSON.stringify({
+                biz: 'CC_AGENT',
+                message: message
+            }),
+            success:function (data) {
+                data = eval(data);
+                if (data.success){
+                    $("#in").val("");
+                } else {
+                    layer.msg(data.msg);
+                }
+            }, error:function () {
+                layer.msg("网络异常,请稍后重试!");
+            }
+        })
+    }
+
+    function createUserChatItem(inMess) {
+        var item = "<div class=\"chat-message-item\">\n" +
+            "                <div class=\"chat-message-photo\">\n" +
+            "                    <img src=\""+user.photo+"?x-oss-process=image/resize,m_fill,w_50,h_50\" class=\"user-avatar\">\n" +
+            "                </div>\n" +
+            "                <div class=\"chat-message-content\">"+inMess+"</div>\n" +
+            "                <div style=\"clear: both\"></div>\n" +
+            "            </div>";
+        $("#rag-chat-message-container").append(item);
+        refreshChatMessContainerScrollBar2Bottom();
+    }
+
+    function createRobotChatItem(messageItemId) {
+        var item = "<div class=\"chat-message-item\">\n" +
+            "                <div class=\"chat-message-photo\">\n" +
+            "                    <img src=\"https://coderutil.oss-cn-beijing.aliyuncs.com/bbs-image/file_56498919dec7403c8e6accb60ed637a5.png?x-oss-process=image/resize,m_fill,w_50,h_50\" class=\"user-avatar\">\n" +
+            "                </div>\n" +
+            "                <div class=\"chat-message-content robot-message\" style='background-color: #144afd; color: white' id='agent-message-"+messageItemId+"'><button class='bounce-loading'></button></div>\n" +
+            "                <div style=\"clear: both\"></div>\n" +
+            "            </div>";
+        $("#rag-chat-message-container").append(item);
+        refreshChatMessContainerScrollBar2Bottom();
+    }
+
+    function refreshChatMessContainerScrollBar2Bottom() {
+        $("#rag-chat-message-container").scrollTop($("#rag-chat-message-container")[0].scrollHeight);
+    }
+</script>
+
+</html>

+ 18 - 0
src/main/resources/templates/ftl/RAG.ftl

@@ -0,0 +1,18 @@
+# 任务说明
+你叫`小C`, 是《WebChat项目》的智能问答助手。
+
+# 任务限制
+- 优先使用`参考资料`内容做回答依据,当没有参考资料,可以自己稍微发挥回答,但要注意保证答案的准确定
+- 要求回答简洁明了,直击关键问题
+
+# 用户问题
+```
+${input}
+```
+
+# 参考资料
+```
+${document}
+```
+
+回答: