在实际开发的项目中,企业与组织还需要关注用户的使用:高效、准确地将内部积累的海量知识转化为即时可用的客户服务能力。传统的客服模式依赖于人力记忆与有限的关键词匹配,往往难以应对用户复杂、多样且即时性强的咨询需求,导致响应迟缓、答案机械、用户体验割裂等问题。与此同时,大型语言模型(LLM)虽展现出强大的自然语言理解和生成能力,但其固有的“幻觉”问题及内部知识的滞后性,使其在直接应用于对准确性与实时性要求极高的客服场景时仍存在显著风险。

本项目——《基于RAG的智能客服系统》,正是对这一核心挑战的深刻回应与前沿实践。我们旨在突破传统客服与纯LLM应用的局限,通过引入检索增强生成(Retrieval-Augmented Generation, RAG)这一创新技术框架,将外部权威知识库与大型语言模型的深层语义理解和流畅生成能力有机地融为一体。

本系统的核心目标,是借助现有的ai工具来构建一个不仅能够“对答如流”、更能“言之有据”的新一代智能客服助手。它能够精准理解用户以自然语言提出的各类问题,实时地从企业文档、产品手册、知识库等指定资料中检索出最相关、最权威的信息片段,并以此为基础,生成准确、可靠、上下文连贯且易于理解的回答。这不仅极大地提升了信息检索的精准度和效率,有效遏制了“AI幻觉”,更通过一种更智能、更自然的方式,重塑了用户获取信息和帮助的体验,为客户服务领域的智能化升级提供了切实可行的技术路径与解决方案。

这个ai工具首选阿里旗下的百炼,因为它在这方面的功能已经相当完善。

http:// https://www.aliyun.com/product/tongyi

实现这个功能我们服务端需要做的事情就是拿到客户的需求传给百炼,然后把自己已有的数据库里面的信息也发送给百炼,让百炼帮我们处理,甚至连客户的口语百炼都可以帮我们自动转换,最后得到答案后给客户反馈。所以作为服务端我们要做的事情就非常轻松:只是相当于做了一个转发的事情。整个流程如下图:

那么如何建立起和百炼的连接呢?

首先为了处理某个项目中的业务,比如这里要实现关于汽车的智能客服,在建立连接之前需要先在阿里百炼中创建一个专门的应用管理,在应用管理下点击右上角的创建应用

然后选择默认的智能体应用就直接创建。在API配置中选择模型。要是想命名为车辆咨询客服可以在上面重命名。

选择模型都选择默认的即可。然后第二条里面有个知识中有个知识库,把它选上并添加知识库。

如果已经有了针对于自己业务的知识库,直接勾选即可,而如果没有,就点击创建新的知识库。

填写名称后点击下一步,如果本地也没有文件,就点击前往数据中心。

在新弹出的页面中点击导入数据,然后将文件上传,确认后即可。当返回上一个页面的时候如果没有看到这个文件就刷新,文件出来了勾选点击下一步,接着导入完成。知识库加载完成后返回上一个页面添加进来即可,如果没有加载进来还是刷新,最后点击发布命名版本就正式上线了。

然后在我们的工程中建立起和我们刚刚发布的智能客服建立连接,先在我们工程项目下的pom.xml文件中添加依赖:

        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
            <version>1.0.0.2</version>
        </dependency>

并确保使用的spring Boot的版本是3.3.3,然后在application.properties配置:

# 阿里百炼大模型配置
# 大模型应用ID,如未配置环境变量,请在这里替换为实际的值
spring.ai.dashscope.agent.options.app-id=自己的智能体应用ID
# 百炼API Key,请在这里替换为自己的API-KEY
spring.ai.dashscope.api-key=自己的API-KEY

注意这里的app-id是刚刚在阿里百炼中发布的那个应用,复制并粘贴它的应用ID,而自己的API-KEY是可以在百炼中左下角的密钥管理是可以看到的。

在我们的controller层中新建一个类用于与阿里百炼服务进行流式交互

@RestController
@RequestMapping("/ai")
public class BaiLian {

    private DashScopeAgent agent;
    @Value("${spring.ai.dashscope.agent.options.app-id}")
    private String appId;

    public BaiLian(DashScopeAgentApi api)
    {
        this.agent=new DashScopeAgent(api,
                DashScopeAgentOptions.builder().
                        withIncrementalOutput(true).build());
    }
    @GetMapping(value="/bailian/agent/stream", produces="text/event-stream")
    public Flux<String> stream(String message) {
        return agent.stream(
                        new Prompt(
                                message,
                                DashScopeAgentOptions.builder() //使用建造者模式创建DashScopeAgentOptions配置对象
                                        .withAppId(appId) // 设置应用ID,用于标识要调用的百炼智能体应用
                                        .build()) // 构建最终的配置对象
                )
                .map(response -> {
                    AssistantMessage app_output = response.getResult().getOutput();
                    String content = app_output.getText();
                    return content;
                });
    }
}

至此,后端人员需要完成的任务就结束了,为了进行前端页面测试,可以在resource包下的static中新建一个测试的.html文件,当然,这些都是前端人员需要做的。

​
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>达内车辆资讯智能客服系统</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <style>
        :root {
            --primary-color: #1890ff;
            --primary-light: #e6f7ff;
            --success-color: #52c41a;
            --error-color: #ff4d4f;
            --warning-color: #faad14;
            --text-color: #333;
            --text-secondary: #666;
            --bg-color: #f5f8ff;
            --card-bg: #fff;
            --border-color: #d9d9d9;
            --sidebar-bg: #2c3e50;
            --sidebar-hover: #34495e;
            --gradient-start: #6a11cb;
            --gradient-end: #2575fc;
        }

        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
            background: linear-gradient(135deg, var(--gradient-start) 0%, var(--gradient-end) 100%);
            color: var(--text-color);
            line-height: 1.6;
            min-height: 100vh;
            padding: 0;
        }

        .app-container {
            display: flex;
            max-width: 1600px;
            height: 100vh;
            margin: 0 auto;
            background: var(--card-bg);
            box-shadow: 0 0 40px rgba(0, 0, 0, 0.15);
            border-radius: 12px;
            overflow: hidden;
            margin: 20px;
        }

        .sidebar {
            width: 320px;
            background: var(--sidebar-bg);
            color: white;
            display: flex;
            flex-direction: column;
            border-right: 1px solid rgba(255, 255, 255, 0.1);
        }

        .logo-area {
            padding: 24px;
            text-align: center;
            border-bottom: 1px solid rgba(255, 255, 255, 0.1);
            background: rgba(0, 0, 0, 0.1);
        }

        .logo {
            font-size: 24px;
            font-weight: 700;
            margin-bottom: 8px;
            color: white;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 10px;
        }

        .logo i {
            color: var(--primary-color);
        }

        .logo-text {
            font-size: 14px;
            opacity: 0.8;
        }

        .new-chat-btn {
            margin: 20px;
            padding: 12px 20px;
            background: var(--primary-color);
            color: white;
            border: none;
            border-radius: 8px;
            font-weight: 500;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 8px;
            transition: all 0.3s;
        }

        .new-chat-btn:hover {
            background: #40a9ff;
            transform: translateY(-2px);
            box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
        }

        .history-title {
            padding: 16px 24px 8px;
            font-size: 14px;
            text-transform: uppercase;
            letter-spacing: 1px;
            opacity: 0.7;
            display: flex;
            align-items: center;
            justify-content: space-between;
        }

        .history-actions {
            display: flex;
            gap: 10px;
            padding: 0 16px 8px;
        }

        .history-action-btn {
            flex: 1;
            padding: 6px 10px;
            background: rgba(255, 255, 255, 0.1);
            color: white;
            border: none;
            border-radius: 6px;
            font-size: 12px;
            cursor: pointer;
            transition: all 0.2s;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 4px;
        }

        .history-action-btn:hover {
            background: rgba(255, 255, 255, 0.2);
        }

        .history-action-btn.delete {
            background: rgba(255, 77, 79, 0.2);
        }

        .history-action-btn.delete:hover {
            background: rgba(255, 77, 79, 0.3);
        }

        .history-list {
            flex: 1;
            overflow-y: auto;
            padding: 0 16px 16px;
        }

        .history-item {
            position: relative;
            padding: 12px 16px;
            margin-bottom: 8px;
            border-radius: 8px;
            background: rgba(255, 255, 255, 0.05);
            cursor: pointer;
            transition: all 0.2s;
            display: flex;
            align-items: center;
            gap: 10px;
        }

        .history-item:hover {
            background: var(--sidebar-hover);
        }

        .history-item.active {
            background: var(--primary-color);
        }

        .history-item i {
            font-size: 14px;
            opacity: 0.7;
        }

        .history-content {
            flex: 1;
            overflow: hidden;
        }

        .history-question {
            font-size: 14px;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            margin-bottom: 4px;
        }

        .history-answer {
            font-size: 12px;
            opacity: 0.7;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }

        .history-time {
            font-size: 11px;
            opacity: 0.5;
            margin-top: 4px;
        }

        .history-item-delete {
            position: absolute;
            top: 6px;
            right: 6px;
            width: 20px;
            height: 20px;
            border-radius: 50%;
            background: rgba(255, 255, 255, 0.1);
            color: white;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 10px;
            opacity: 0;
            transition: all 0.2s;
            cursor: pointer;
        }

        .history-item:hover .history-item-delete {
            opacity: 1;
        }

        .history-item-delete:hover {
            background: var(--error-color);
        }

        .main-content {
            flex: 1;
            display: flex;
            flex-direction: column;
            background: var(--bg-color);
            overflow: hidden;
        }

        .header {
            padding: 16px 24px;
            background: white;
            border-bottom: 1px solid var(--border-color);
            display: flex;
            align-items: center;
            justify-content: space-between;
        }

        .header-title h1 {
            font-size: 20px;
            font-weight: 600;
            color: var(--primary-color);
        }

        .header-title .subtitle {
            color: var(--text-secondary);
            font-size: 13px;
        }

        .user-area {
            display: flex;
            align-items: center;
            gap: 12px;
        }

        .user-avatar {
            width: 40px;
            height: 40px;
            border-radius: 50%;
            background: var(--primary-light);
            display: flex;
            align-items: center;
            justify-content: center;
            color: var(--primary-color);
            font-weight: bold;
        }

        .chat-container {
            flex: 1;
            display: flex;
            flex-direction: column;
            padding: 24px;
            overflow: hidden;
        }

        .messages-container {
            flex: 1;
            overflow-y: auto;
            margin-bottom: 20px;
            padding: 16px;
            background: white;
            border-radius: 12px;
            box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
        }

        .message {
            margin-bottom: 16px;
            padding: 16px;
            border-radius: 12px;
            background: white;
            border: 1px solid var(--border-color);
            animation: fadeIn 0.3s ease;
        }

        @keyframes fadeIn {
            from { opacity: 0; transform: translateY(10px); }
            to { opacity: 1; transform: translateY(0); }
        }

        .message.user {
            background: var(--primary-light);
            border-color: #91d5ff;
        }

        .message.ai {
            background: #f9f9f9;
            border-color: #e8e8e8;
        }

        .message.error {
            background: #fff2f0;
            border-color: #ffccc7;
        }

        .message.success {
            background: #f6ffed;
            border-color: #b7eb8f;
        }

        .message-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 12px;
            padding-bottom: 8px;
            border-bottom: 1px solid rgba(0, 0, 0, 0.06);
        }

        .message-type {
            font-weight: 600;
            font-size: 12px;
            text-transform: uppercase;
            letter-spacing: 0.5px;
            display: flex;
            align-items: center;
            gap: 6px;
        }

        .message.user .message-type {
            color: #096dd9;
        }

        .message.ai .message-type {
            color: #389e0d;
        }

        .message-time {
            font-size: 11px;
            color: var(--text-secondary);
        }

        .message-content {
            white-space: pre-wrap;
            word-break: break-word;
            line-height: 1.6;
        }

        .input-area {
            background: white;
            padding: 20px;
            border-radius: 12px;
            box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
        }

        .input-group {
            margin-bottom: 16px;
        }

        textarea {
            width: 100%;
            min-height: 100px;
            padding: 16px;
            border: 1px solid var(--border-color);
            border-radius: 8px;
            resize: vertical;
            font-family: inherit;
            font-size: 14px;
            line-height: 1.5;
            transition: all 0.3s;
        }

        textarea:focus {
            outline: none;
            border-color: var(--primary-color);
            box-shadow: 0 0 0 3px rgba(24, 144, 255, 0.2);
        }

        .button-group {
            display: flex;
            gap: 12px;
            flex-wrap: wrap;
        }

        button {
            padding: 12px 24px;
            border: none;
            border-radius: 8px;
            font-size: 14px;
            font-weight: 500;
            cursor: pointer;
            transition: all 0.3s;
            display: flex;
            align-items: center;
            gap: 8px;
        }

        .btn-primary {
            background: var(--primary-color);
            color: white;
        }

        .btn-primary:hover:not(:disabled) {
            background: #40a9ff;
            transform: translateY(-2px);
            box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
        }

        .btn-danger {
            background: var(--error-color);
            color: white;
        }

        .btn-danger:hover:not(:disabled) {
            background: #ff7875;
            transform: translateY(-2px);
            box-shadow: 0 4px 12px rgba(255, 77, 79, 0.3);
        }

        .btn-secondary {
            background: var(--bg-color);
            color: var(--text-color);
            border: 1px solid var(--border-color);
        }

        .btn-secondary:hover:not(:disabled) {
            background: #e6e6e6;
            transform: translateY(-2px);
        }

        button:disabled {
            opacity: 0.6;
            cursor: not-allowed;
            transform: none !important;
            box-shadow: none !important;
        }

        .status-bar {
            display: flex;
            align-items: center;
            gap: 12px;
            padding: 16px;
            background: #fafafa;
            border-radius: 8px;
            border: 1px solid var(--border-color);
            margin-top: 16px;
        }

        .status-dot {
            width: 10px;
            height: 10px;
            border-radius: 50%;
            background: var(--error-color);
        }

        .status-dot.connected {
            background: var(--success-color);
            animation: pulse 2s infinite;
        }

        @keyframes pulse {
            0%, 100% { opacity: 1; }
            50% { opacity: 0.5; }
        }

        .stats {
            display: flex;
            gap: 16px;
            font-size: 12px;
            color: var(--text-secondary);
            margin-left: auto;
        }

        .stat-item {
            display: flex;
            align-items: center;
            gap: 4px;
        }

        .typing-indicator {
            display: inline-flex;
            align-items: center;
            gap: 4px;
            color: var(--text-secondary);
            font-style: italic;
            padding: 8px 0;
        }

        .dot {
            width: 4px;
            height: 4px;
            border-radius: 50%;
            background: currentColor;
            animation: bounce 1.4s infinite;
        }

        .dot:nth-child(2) { animation-delay: 0.2s; }
        .dot:nth-child(3) { animation-delay: 0.4s; }

        @keyframes bounce {
            0%, 80%, 100% { transform: scale(0); }
            40% { transform: scale(1); }
        }

        @media (max-width: 1024px) {
            .app-container {
                flex-direction: column;
                height: auto;
                margin: 10px;
            }

            .sidebar {
                width: 100%;
                height: auto;
                max-height: 300px;
            }

            .history-list {
                max-height: 200px;
            }
        }

        @media (max-width: 768px) {
            .button-group {
                flex-direction: column;
            }

            button {
                width: 100%;
                justify-content: center;
            }

            .stats {
                flex-wrap: wrap;
            }
        }

        /* 滚动条样式 */
        ::-webkit-scrollbar {
            width: 6px;
        }

        ::-webkit-scrollbar-track {
            background: rgba(0, 0, 0, 0.05);
            border-radius: 10px;
        }

        ::-webkit-scrollbar-thumb {
            background: rgba(0, 0, 0, 0.2);
            border-radius: 10px;
        }

        ::-webkit-scrollbar-thumb:hover {
            background: rgba(0, 0, 0, 0.3);
        }
    </style>
</head>
<body>
<div class="app-container">
    <!-- 左侧边栏 -->
    <div class="sidebar">
        <div class="logo-area">
            <div class="logo">
                <i class="fas fa-robot"></i>
                <span>车辆资讯智能客服</span>
            </div>
            <div class="logo-text">基于Spring AI Alibaba</div>
        </div>

        <button class="new-chat-btn" onclick="createNewChat()">
            <i class="fas fa-plus"></i>
            <span>新建会话</span>
        </button>

        <div class="history-title">
            <span>对话历史</span>
            <span><i class="fas fa-history"></i></span>
        </div>

        <div class="history-actions">
            <button class="history-action-btn delete" onclick="clearAllHistory()">
                <i class="fas fa-trash"></i>
                <span>清空历史</span>
            </button>
        </div>

        <div class="history-list" id="historyList">
            <!-- 历史记录将通过JS动态添加 -->
        </div>
    </div>

    <!-- 主内容区 -->
    <div class="main-content">
        <div class="header">
            <div class="header-title">
                <h1>车辆资讯智能客服</h1>
                <div class="subtitle">tarena</div>
            </div>

            <div class="user-area">
                <div class="user-avatar">
                    <i class="fas fa-user"></i>
                </div>
            </div>
        </div>

        <div class="chat-container">
            <div class="messages-container" id="output">
                <div class="message success">
                    <div class="message-header">
                        <span class="message-type">
                            <i class="fas fa-info-circle"></i>
                            系统
                        </span>
                        <span class="message-time" id="currentTime"></span>
                    </div>
                    <div class="message-content">
                        欢迎使用车辆资讯智能客服系统!您可以开始对话了。
                    </div>
                </div>
            </div>

            <div class="input-area">
                <div class="input-group">
                    <textarea
                            id="messageInput"
                            placeholder="请输入您的问题,例如:我想了解一下汽车保养的知识"
                    ></textarea>
                </div>

                <div class="button-group">
                    <button class="btn-primary" onclick="startStream()" id="startBtn">
                        <i class="fas fa-paper-plane"></i>
                        <span>开始对话</span>
                    </button>
                    <button class="btn-danger" onclick="stopStream()" id="stopBtn" disabled>
                        <i class="fas fa-stop-circle"></i>
                        <span>停止接收</span>
                    </button>
                    <button class="btn-secondary" onclick="insertExample()">
                        <i class="fas fa-lightbulb"></i>
                        <span>插入示例</span>
                    </button>
                    <button class="btn-secondary" onclick="clearInput()">
                        <i class="fas fa-eraser"></i>
                        <span>清空输入</span>
                    </button>
                </div>

                <div class="status-bar">
                    <div class="status-dot" id="statusDot"></div>
                    <span id="statusText">未连接</span>
                    <div class="stats">
                        <div class="stat-item">
                            <i class="fas fa-comments"></i>
                            <span>消息:</span>
                            <span id="messageCount">0</span>
                        </div>
                        <div class="stat-item">
                            <i class="fas fa-clock"></i>
                            <span>时长:</span>
                            <span id="duration">0s</span>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

<script>
    let eventSource = null;
    let messageCount = 0;
    let startTime = null;
    let connectionTimer = null;
    let isTyping = false;
    let currentConversationId = null;
    let currentAiMessageElement = null;
    let chatHistory = [];
    let activeChatId = null;

    // 初始化
    document.addEventListener('DOMContentLoaded', function() {
        updateCurrentTime();
        loadChatHistory();
        document.getElementById('messageInput').focus();

        // 添加回车键监听
        document.getElementById('messageInput').addEventListener('keydown', function(e) {
            if (e.key === 'Enter' && !e.shiftKey) {
                e.preventDefault();
                startStream();
            }
        });
    });

    // 更新当前时间
    function updateCurrentTime() {
        const now = new Date();
        document.getElementById('currentTime').textContent = now.toLocaleTimeString();
    }
    setInterval(updateCurrentTime, 1000);

    // 创建新聊天
    function createNewChat() {
        activeChatId = 'chat_' + Date.now();
        chatHistory.push({
            id: activeChatId,
            title: '新对话',
            timestamp: new Date(),
            messages: []
        });

        saveChatHistory();
        renderChatHistory();
        clearChatWindow();

        // 添加欢迎消息
        addMessage('已创建新对话,您可以开始提问了。', 'success');
    }

    // 加载聊天历史
    function loadChatHistory() {
        const savedHistory = localStorage.getItem('chatHistory');
        if (savedHistory) {
            chatHistory = JSON.parse(savedHistory);
            // 按时间升序排列
            chatHistory.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
            renderChatHistory();

            if (chatHistory.length > 0) {
                activeChatId = chatHistory[chatHistory.length - 1].id;
                loadChat(activeChatId);
            } else {
                createNewChat();
            }
        } else {
            createNewChat();
        }
    }

    // 保存聊天历史
    function saveChatHistory() {
        localStorage.setItem('chatHistory', JSON.stringify(chatHistory));
    }

    // 渲染聊天历史
    function renderChatHistory() {
        const historyList = document.getElementById('historyList');
        historyList.innerHTML = '';

        // 按时间升序排列
        const sortedHistory = [...chatHistory].sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));

        sortedHistory.forEach(chat => {
            const historyItem = document.createElement('div');
            historyItem.className = `history-item ${chat.id === activeChatId ? 'active' : ''}`;
            historyItem.onclick = () => loadChat(chat.id);

            historyItem.innerHTML = `
                <i class="fas fa-comment-dots"></i>
                <div class="history-content">
                    <div class="history-question">${chat.title}</div>
                    <div class="history-answer">${chat.messages.length > 0 ?
                (chat.messages[0].content.length > 40 ?
                    chat.messages[0].content.substring(0, 40) + '...' :
                    chat.messages[0].content) :
                '暂无对话'}</div>
                    <div class="history-time">${formatTime(chat.timestamp)}</div>
                </div>
                <div class="history-item-delete" onclick="deleteChatHistory('${chat.id}', event)">
                    <i class="fas fa-times"></i>
                </div>
            `;

            historyList.appendChild(historyItem);
        });
    }

    // 删除指定聊天历史
    function deleteChatHistory(chatId, event) {
        if (event) {
            event.stopPropagation(); // 阻止事件冒泡
        }

        if (confirm('确定要删除这条对话历史吗?')) {
            const index = chatHistory.findIndex(c => c.id === chatId);
            if (index !== -1) {
                chatHistory.splice(index, 1);
                saveChatHistory();

                // 如果删除的是当前活跃的聊天,则加载最新的聊天或创建新聊天
                if (chatId === activeChatId) {
                    if (chatHistory.length > 0) {
                        activeChatId = chatHistory[chatHistory.length - 1].id;
                        loadChat(activeChatId);
                    } else {
                        createNewChat();
                    }
                } else {
                    renderChatHistory();
                }
            }
        }
    }

    // 清空所有聊天历史
    function clearAllHistory() {
        if (confirm('确定要清空所有对话历史吗?此操作不可撤销。')) {
            chatHistory = [];
            saveChatHistory();
            createNewChat();
        }
    }

    // 加载特定聊天
    function loadChat(chatId) {
        activeChatId = chatId;
        const chat = chatHistory.find(c => c.id === chatId);

        if (chat) {
            clearChatWindow();
            chat.messages.forEach(msg => {
                addMessage(msg.content, msg.type, false);
            });

            renderChatHistory();
        }
    }

    // 清空聊天窗口
    function clearChatWindow() {
        const output = document.getElementById('output');
        output.innerHTML = '';
        messageCount = 0;
        document.getElementById('messageCount').textContent = '0';
    }

    // 格式化时间
    function formatTime(dateString) {
        const date = new Date(dateString);
        const now = new Date();
        const diff = now - date;
        const days = Math.floor(diff / (1000 * 60 * 60 * 24));

        if (days === 0) {
            return '今天 ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
        } else if (days === 1) {
            return '昨天 ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
        } else {
            return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
        }
    }

    function updateStatus(connected, text) {
        const dot = document.getElementById('statusDot');
        const statusText = document.getElementById('statusText');
        const startBtn = document.getElementById('startBtn');
        const stopBtn = document.getElementById('stopBtn');

        dot.className = connected ? 'status-dot connected' : 'status-dot';
        statusText.textContent = text;
        startBtn.disabled = connected;
        stopBtn.disabled = !connected;

        if (connected) {
            startTime = Date.now();
            startConnectionTimer();
        } else {
            stopConnectionTimer();
        }
    }

    function startConnectionTimer() {
        stopConnectionTimer();
        connectionTimer = setInterval(() => {
            const duration = Math.floor((Date.now() - startTime) / 1000);
            document.getElementById('duration').textContent = `${duration}s`;
        }, 1000);
    }

    function stopConnectionTimer() {
        if (connectionTimer) {
            clearInterval(connectionTimer);
            connectionTimer = null;
        }
    }

    function addMessage(content, type = 'ai', saveToHistory = true) {
        const output = document.getElementById('output');
        const now = new Date();
        const timestamp = now.toLocaleTimeString();

        const messageDiv = document.createElement('div');
        messageDiv.className = `message ${type}`;

        let icon = 'fas fa-robot';
        if (type === 'user') icon = 'fas fa-user';
        if (type === 'error') icon = 'fas fa-exclamation-circle';
        if (type === 'success') icon = 'fas fa-info-circle';

        let messageHtml = `
            <div class="message-header">
                <span class="message-type">
                    <i class="${icon}"></i>
                    ${type.toUpperCase()}
                </span>
                <span class="message-time">${timestamp}</span>
            </div>
            <div class="message-content">${content}</div>
        `;

        messageDiv.innerHTML = messageHtml;
        output.appendChild(messageDiv);
        output.scrollTop = output.scrollHeight;

        messageCount++;
        document.getElementById('messageCount').textContent = messageCount;

        // 保存到历史记录
        if (saveToHistory && activeChatId) {
            const chatIndex = chatHistory.findIndex(c => c.id === activeChatId);
            if (chatIndex !== -1) {
                chatHistory[chatIndex].messages.push({
                    content: content,
                    type: type,
                    timestamp: new Date()
                });

                // 更新聊天标题(使用第一个用户消息)
                if (type === 'user' && chatHistory[chatIndex].title === '新对话') {
                    chatHistory[chatIndex].title = content.length > 30 ?
                        content.substring(0, 30) + '...' : content;
                }

                saveChatHistory();
                renderChatHistory();
            }
        }

        return messageDiv;
    }

    function showTypingIndicator() {
        if (isTyping) return;

        const output = document.getElementById('output');
        const indicator = document.createElement('div');
        indicator.id = 'typingIndicator';
        indicator.className = 'message ai';

        let indicatorHtml = `
            <div class="message-header">
                <span class="message-type">
                    <i class="fas fa-robot"></i>
                    AI
                </span>
                <span class="message-time">${new Date().toLocaleTimeString()}</span>
            </div>
            <div class="message-content">
                <div class="typing-indicator">
                    <span>思考中</span>
                    <span class="dot"></span>
                    <span class="dot"></span>
                    <span class="dot"></span>
                </div>
            </div>
        `;

        indicator.innerHTML = indicatorHtml;
        output.appendChild(indicator);
        output.scrollTop = output.scrollHeight;
        isTyping = true;
    }

    function hideTypingIndicator() {
        const indicator = document.getElementById('typingIndicator');
        if (indicator) {
            indicator.remove();
        }
        isTyping = false;
    }

    function startStream() {
        const messageInput = document.getElementById('messageInput');
        const message = messageInput.value.trim();
        if (!message) {
            alert('请输入消息内容');
            return;
        }

        // 如果没有活跃聊天,创建一个
        if (!activeChatId) {
            createNewChat();
        }

        // 生成新的会话ID
        currentConversationId = 'conv_' + Date.now();

        // 显示用户消息
        addMessage(message, 'user');
        showTypingIndicator();

        updateStatus(true, '连接中...');

        // 清空输入框
        messageInput.value = '';

        // 关闭现有连接
        if (eventSource) {
            eventSource.close();
        }

        try {
            const encodedMessage = encodeURIComponent(message);
            eventSource = new EventSource(`/ai/bailian/agent/stream?message=${encodedMessage}`);

            // 为当前会话创建新的AI消息元素
            currentAiMessageElement = addMessage('', 'ai', false);
            const contentElement = currentAiMessageElement.querySelector('.message-content');

            eventSource.onopen = function() {
                updateStatus(true, '已连接');
                hideTypingIndicator();
            };

            eventSource.onmessage = function(event) {
                hideTypingIndicator();

                if (event.data) {
                    // 只更新当前会话的消息内容
                    if (currentAiMessageElement && contentElement) {
                        contentElement.textContent += event.data;
                        output.scrollTop = output.scrollHeight;
                    }
                }
            };

            eventSource.onerror = function(error) {
                console.error('SSE错误:', error);
                updateStatus(false, '回答完成');
                hideTypingIndicator();

                if (currentAiMessageElement && contentElement) {
                    // 保存完整的AI消息到历史记录
                    const aiMessageContent = contentElement.textContent;
                    const chatIndex = chatHistory.findIndex(c => c.id === activeChatId);
                    if (chatIndex !== -1) {
                        chatHistory[chatIndex].messages.push({
                            content: aiMessageContent,
                            type: 'ai',
                            timestamp: new Date()
                        });
                        saveChatHistory();
                        renderChatHistory();
                    }
                }

                if (eventSource) {
                    eventSource.close();
                    eventSource = null;
                }
                currentAiMessageElement = null;
            };

        } catch (error) {
            console.error('创建连接失败:', error);
            updateStatus(false, '连接失败');
            hideTypingIndicator();
            addMessage('创建连接失败: ' + error.message, 'error');
        }
    }

    function stopStream() {
        if (eventSource) {
            eventSource.close();
            eventSource = null;
            updateStatus(false, '已断开');
            hideTypingIndicator();
            currentAiMessageElement = null;
            currentConversationId = null;
        }
    }

    function clearInput() {
        document.getElementById('messageInput').value = '';
    }

    function insertExample() {
        const examples = [
            "你好,请介绍一下你自己",
            "你的知识库包含哪些内容?",
            "请用简洁的语言总结你的功能",
            "如何最好地使用你的服务?",
            "你能帮我解决什么问题?"
        ];
        const randomExample = examples[Math.floor(Math.random() * examples.length)];
        document.getElementById('messageInput').value = randomExample;
    }

    // 键盘快捷键
    document.addEventListener('keydown', function(event) {
        if (event.key === 'Enter' && event.ctrlKey) {
            event.preventDefault();
            startStream();
        }
        if (event.key === 'Escape') {
            stopStream();
        }
    });

    // 页面卸载时清理
    window.addEventListener('beforeunload', function() {
        if (eventSource) {
            eventSource.close();
        }
        stopConnectionTimer();
    });
</script>
</body>
</html>

​

接下来就测试一下,先启动程序,再访问网址http://localhost:8080/test_call.html。为了安全,显示要我们登录一下,在服务器终端下面显示了密码,复制过来,而用户名就输入user

那么这时候就完全可以根据知识库中的内容来给客户提的需求进行反馈,这样智能客服就可以具体问题具体分析,而不是给出很抽象的回答。

这种模式固然轻松了后端程序员,把数据库和客户需求都交给了阿里百炼来处理,但是连自己的库都交给了别人,会不会存在一定的安全隐患呢?

阿里百炼作为阿里云推出的企业级大模型应用开发平台,在安全方面投入了大量资源,比如做了数据传输加密:所有数据上传和API调用都通过HTTPS等加密协议进行,防止在传输过程中被窃听。静态加密:存储在百炼平台上的数据(包括上传的数据库文件)会进行加密存储,通常使用高强度加密算法。即使是阿里云的管理员,也无法直接访问我们的明文数据。租户隔离:采用严格的多租户隔离技术,确保我们的数据与其他客户的数据完全逻辑隔离,不会发生串户。

同时也做了访问控制与权限管理:身份认证强制要求通过阿里云访问控制(RAM)进行身份验证,支持子账户和临时安全令牌(STS)。权限授权可以通过RAM精细地控制哪个子账号有权限访问百炼上的哪些资源(例如,只能使用某个应用,但不能查看或修改底层数据),遵循“最小权限原则”。

如果非要在自己本地建立数据库,那么代价也是比较大的,尤其是后端程序员。他们不仅要将拿到请求(自然语言)转换为向量,还需要进行向量相似度匹配,找到最匹配的数据,然后再发送给阿里百炼生成答案,最后返回给客户。在进行自然语言转换过程中,需要进行一个分块的操作,使用NLP技术(如句子 transformers)计算相邻句子的相似度,在语义变化大的地方进行分割。最先进,但也最复杂,对于程序员要求相当高。

不过即使是自己的本地数据库,也未必比阿里更安全,阿里百炼本身是一个安全可信的平台,其基础设施的安全性远高于大多数企业自建的机房。 数据泄露的风险主要不在于平台被“黑客攻破”,而更可能来自于我们自身的误配置、权限管理疏忽、以及应用逻辑设计上的缺陷。将数据库文件放到阿里百炼,总体上是安全的,但“绝对安全”取决于我们的具体操作和配置。并且完全不用担心阿里有随意查看我们数据库的可能,阿里云的内部人员(包括运维、开发、管理员等)既无法随意查看,更无法随意修改存储在百炼上的数据库文件。 阿里云通过严格的技术手段、流程制度和法律合同来杜绝这一点。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐