探索 WebLLM:前端与大模型的完美邂逅

90 阅读10分钟

在当今科技飞速发展的时代,前端技术也在不断地革新与进步。WebLLM(Web Large Language Model)作为智能前端的新战场,正逐渐走进开发者的视野。本文将结合实际代码和详细笔记,深入探讨如何把 DeepSeek 大模型引入前端,以及 HTTP 协议在其中的关键作用。

开始前记得先创建好基本文件:

image.png

这里是使用 trae的Builder智能体帮助创建的项目,可以基于它给的示例进行修改。

大模型与前端的连接:远程 HTTP API 请求

在传统的开发模式中,前端页面往往依赖服务器端返回的 HTML 字符串来展示内容。例如,当我们输入 URL 或者点击一个链接时,浏览器会向服务器端发送请求,服务器端(如 Node 或 Java)会从数据库中取数据,生成 HTML 字符串并返回给前端。这种方式相对死板,缺乏动态性和交互性。

而在 AI 时代,大模型的出现为前端开发带来了新的机遇。DeepSeek 作为一款强大的大模型,它位于远程服务器,我们可以通过 HTTP API 请求与之交互。这种方式让前端能够主动获取大模型的能力,开启了智能前端时代。

认识 Fetch API:赋予 JS 新生命

在 Web 1.0 时代,HTML、CSS 和 JS 主要用于简单的页面交互,JS 只是被动地展示服务器端返回的内容。而到了 Web 2.0 时代,Fetch API 的出现赋予了 JS 新的生命,它让 JS 能够主动请求后端服务器,实现动态页面的效果。每次下拉页面右侧 Fecth 拉取数据展示。

屏幕录制 2025-06-06 112606_20250606112701.gif

Fetch API 可以用于多种场景,比如滚动到底部后加载更多数据。在传统的页面中,当我们滚动到底部时,需要刷新页面才能看到新的内容。而使用 Fetch API,我们可以在不刷新页面的情况下,主动向服务器端请求数据,并通过 DOM 操作更新页面,为用户带来富应用体验。另外,在点赞等交互场景中,也可以使用 JS Fetch API 来实现与服务器的交互。

在 AI 时代,Fetch API 的作用更加凸显。我们可以通过它来获取大模型的能力,实现智能交互。下面我们就来详细看看如何使用 Fetch API 与 DeepSeek 大模型进行交互。

深入解析代码:使用 Fetch 请求 DeepSeek API

代码整体思路

我们使用 Fetch API 向 DeepSeek 的 API 发送请求,获取大模型生成的回复,并将其展示在页面上。

image.png

请求行

在 HTTP 请求中,请求行包含请求方法和请求的 URL。在这个例子中,我们使用的是 POST 方法,请求的 URL 是 https://api.deepseek.com/chat/completions,这是 DeepSeek 提供的聊天完成接口。

const endpoint = "https://api.deepseek.com/chat/completions"

请求头

请求头用于设置各种头部信息,告诉服务器请求的相关信息。在这个例子中,我们设置了两个重要的头部信息:

  • Content-Type: application/json:表示请求体的内容类型是 JSON 格式。
  • Authorization: Bearer sk-xxxxxxxxxx:用于身份验证,这里的 sk-xxxxxxxxxx 是授权凭证。
const headers = {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer sk-xxxxxxxxxx'
}

请求体

请求体包含了我们要发送给服务器的数据。在与 DeepSeek 大模型交互时,我们需要指定模型名称和消息内容。消息内容采用了聊天的方式,包含系统角色和用户角色的消息。

  • 系统角色:只会出现一次,用于设置系统的角色,开始会话时使用。
  • 用户角色:用户输入的消息。
const payload = {
    model:'deepseek-chat',
    messages: [
        {
            role: 'system',
            content: 'You are a helpful assistant.'
        },
        {
            role:'user',
            content: '你好'
        }
    ]
}

发送请求

使用 Fetch API 发送请求,将请求方法、请求头和请求体传递给 fetch 函数。由于 HTTP 请求传输只能是字符串或二进制流,所以需要使用 JSON.stringify 将请求体对象转换为字符串。

fetch(endpoint, {
    method: 'POST',
    headers: headers,
    body: JSON.stringify(payload)
}).then(res => res.json()).then(data => {
    console.log(data)
    document.querySelector('#reply').innerHTML = data.choices[0].message.content
})

请求发送后,我们使用 .then 方法处理响应。首先将响应转换为 JSON 格式,然后将大模型生成的回复展示在页面的 #reply 元素中。

这里使用了两次.then

  • 第一次:请求 + LLM 生成需要花时间

  • 第二次:解析返回的json 数据 也要花时间

升级

上面基础部分就讲完了,既然我们都可以设计和 ai 对话并响应到页面了,那为什么不直接手搓一个小deepseek,因为用的还是人家的模型和密钥,叫这个比较贴切。

作为 AI 玩家,自己写肯定不行,直接看deepseek给了我们什么吧:

html 部分:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AI对话助手</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <!-- 动态背景粒子 -->
    <div class="bg-particles" id="particles"></div>

    <div class="chat-container">
        <!-- 头部 -->
        <div class="chat-header">
            <div class="header-content">
                <div style="display: flex; align-items: center;">
                    <div class="ai-avatar">🤖</div>
                    <div class="ai-info" style="margin-left: 15px;">
                        <h1>AI助手</h1>
                        <div class="ai-status">
                            <div class="status-dot"></div>
                            在线服务中
                        </div>
                    </div>
                </div>
            </div>
        </div>

        <!-- 聊天消息区域 -->
        <div class="chat-messages" id="chatMessages">
            <div class="message ai-message">
                <div class="message-content">
                    你好!我是你的AI助手,有什么可以帮助你的吗?我可以回答问题、协助思考问题或者只是陪你聊天。
                </div>
                <div class="message-time">刚刚</div>
            </div>
        </div>

        <!-- 输入区域 -->
        <div class="chat-input-container">
            <div class="input-wrapper">
                <textarea 
                    class="chat-input" 
                    id="chatInput" 
                    placeholder="输入你的消息...(Enter发送,Shift+Enter换行)"
                    rows="1"
                ></textarea>
                <button class="send-button" id="sendButton"></button>
            </div>
        </div>
    </div>

    <script src="script.js"></script>
</body>
</html>

css 部分(因为我让它给点动态粒子效果,所以有点多):

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

        body {
            font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            background: linear-gradient(135deg, #0c0c0c 0%, #1a1a2e 50%, #16213e 100%);
            min-height: 100vh;
            color: #ffffff;
            overflow-x: hidden;
        }

        /* 动态背景粒子效果 */
        .bg-particles {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            pointer-events: none;
            z-index: -1;
        }

        .particle {
            position: absolute;
            width: 2px;
            height: 2px;
            background: rgba(0, 255, 255, 0.5);
            border-radius: 50%;
            animation: float 6s ease-in-out infinite;
        }

        @keyframes float {
            0%, 100% { transform: translateY(0px) rotate(0deg); opacity: 0.5; }
            50% { transform: translateY(-20px) rotate(180deg); opacity: 1; }
        }

        /* 主容器 */
        .chat-container {
            display: flex;
            flex-direction: column;
            height: 100vh;
            max-width: 1200px;
            margin: 0 auto;
            backdrop-filter: blur(10px);
            border-radius: 20px;
            box-shadow: 0 25px 50px rgba(0, 255, 255, 0.1);
            overflow: hidden;
            position: relative;
            z-index: 1;
        }

        /* 头部区域 */
        .chat-header {
            background: linear-gradient(90deg, rgba(0, 255, 255, 0.1) 0%, rgba(255, 0, 255, 0.1) 100%);
            padding: 20px 30px;
            border-bottom: 1px solid rgba(0, 255, 255, 0.2);
            position: relative;
            z-index: 2;
        }

        .chat-header::before {
            content: '';
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            height: 2px;
            background: linear-gradient(90deg, #00ffff, #ff00ff, #00ffff);
            background-size: 200% 100%;
            animation: gradient-flow 3s ease-in-out infinite;
        }

        @keyframes gradient-flow {
            0%, 100% { background-position: 0% 50%; }
            50% { background-position: 100% 50%; }
        }

        .header-content {
            display: flex;
            align-items: center;
            justify-content: space-between;
        }

        .ai-avatar {
            width: 50px;
            height: 50px;
            background: linear-gradient(45deg, #00ffff, #ff00ff);
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            animation: pulse 2s ease-in-out infinite;
            font-size: 24px;
        }

        @keyframes pulse {
            0%, 100% { transform: scale(1); box-shadow: 0 0 20px rgba(0, 255, 255, 0.5); }
            50% { transform: scale(1.05); box-shadow: 0 0 30px rgba(255, 0, 255, 0.7); }
        }

        .ai-info h1 {
            font-size: 28px;
            font-weight: 700;
            background: linear-gradient(45deg, #00ffff, #ff00ff);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            background-clip: text;
            margin-bottom: 5px;
        }

        .ai-status {
            color: #00ff88;
            font-size: 14px;
            display: flex;
            align-items: center;
        }

        .status-dot {
            width: 8px;
            height: 8px;
            background: #00ff88;
            border-radius: 50%;
            margin-right: 8px;
            animation: blink 1.5s ease-in-out infinite;
        }

        @keyframes blink {
            0%, 100% { opacity: 1; }
            50% { opacity: 0.3; }
        }

        /* 聊天区域 */
        .chat-messages {
            flex: 1;
            padding: 30px;
            overflow-y: auto;
            scroll-behavior: smooth;
            position: relative;
            z-index: 2;
        }

        .message {
            margin-bottom: 25px;
            animation: slideUp 0.5s ease-out;
        }

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

        .message-content {
            max-width: 70%;
            padding: 18px 24px;
            border-radius: 20px;
            position: relative;
            word-wrap: break-word;
            font-size: 16px;
            line-height: 1.6;
        }

        .user-message .message-content {
            background: linear-gradient(135deg, #00ffff20, #ff00ff20);
            border: 1px solid rgba(0, 255, 255, 0.3);
            margin-left: auto;
            border-bottom-right-radius: 5px;
        }

        .ai-message .message-content {
            background: linear-gradient(135deg, #ffffff10, #00ffff10);
            border: 1px solid rgba(255, 255, 255, 0.1);
            border-bottom-left-radius: 5px;
        }

        .message-time {
            font-size: 12px;
            color: rgba(255, 255, 255, 0.5);
            margin-top: 8px;
            text-align: right;
        }

        .ai-message .message-time {
            text-align: left;
        }

        /* 输入区域 - 修复版本 */
        .chat-input-container {
            padding: 30px;
            background: rgba(0, 0, 0, 0.3);
            border-top: 1px solid rgba(0, 255, 255, 0.2);
            position: relative;
            z-index: 10; /* 提高层级 */
        }

        .input-wrapper {
            display: flex;
            align-items: center;
            background: rgba(255, 255, 255, 0.05);
            border: 2px solid rgba(0, 255, 255, 0.3);
            border-radius: 25px;
            padding: 5px;
            transition: all 0.3s ease;
            position: relative;
            z-index: 11; /* 确保在最上层 */
        }

        .input-wrapper:focus-within {
            border-color: #00ffff;
            box-shadow: 0 0 20px rgba(0, 255, 255, 0.3);
        }

        .chat-input {
            flex: 1;
            background: transparent;
            border: none;
            outline: none;
            color: #ffffff;
            font-size: 16px;
            padding: 15px 20px;
            resize: none;
            max-height: 120px;
            min-height: 50px;
            position: relative;
            z-index: 12; /* 确保输入框可点击 */
        }

        .chat-input::placeholder {
            color: rgba(255, 255, 255, 0.5);
        }

        .send-button {
            background: linear-gradient(45deg, #00ffff, #ff00ff);
            border: none;
            width: 50px;
            height: 50px;
            border-radius: 50%;
            color: white;
            font-size: 20px;
            cursor: pointer;
            transition: all 0.3s ease;
            display: flex;
            align-items: center;
            justify-content: center;
            margin-right: 5px;
            position: relative;
            z-index: 12;
        }

        .send-button:hover {
            transform: scale(1.1);
            box-shadow: 0 10px 25px rgba(0, 255, 255, 0.4);
        }

        .send-button:disabled {
            opacity: 0.5;
            cursor: not-allowed;
            transform: none;
        }

        /* 加载动画 */
        .typing-indicator {
            display: flex;
            align-items: center;
            padding: 20px 24px;
            color: rgba(255, 255, 255, 0.7);
        }

        .typing-dots {
            display: flex;
            margin-left: 10px;
        }

        .typing-dot {
            width: 8px;
            height: 8px;
            background: #00ffff;
            border-radius: 50%;
            margin: 0 2px;
            animation: typing 1.4s ease-in-out infinite both;
        }

        .typing-dot:nth-child(1) { animation-delay: -0.32s; }
        .typing-dot:nth-child(2) { animation-delay: -0.16s; }

        @keyframes typing {
            0%, 80%, 100% { transform: scale(0.8); opacity: 0.5; }
            40% { transform: scale(1.2); opacity: 1; }
        }

        /* 响应式设计 */
        @media (max-width: 768px) {
            .chat-container {
                height: 100vh;
                border-radius: 0;
            }
            
            .chat-messages {
                padding: 20px 15px;
            }
            
            .message-content {
                max-width: 85%;
                padding: 12px 16px;
                font-size: 14px;
            }
            
            .chat-input-container {
                padding: 20px 15px;
            }
        }

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

        .chat-messages::-webkit-scrollbar-track {
            background: rgba(255, 255, 255, 0.1);
            border-radius: 10px;
        }

        .chat-messages::-webkit-scrollbar-thumb {
            background: linear-gradient(45deg, #00ffff, #ff00ff);
            border-radius: 10px;
        }

js 部分:

// 模拟API响应(替换成你的实际API配置)
    const API_CONFIG = {
        endpoint: "https://api.deepseek.com/chat/completions",
        headers: {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer sk-xxxxxxxxxx' // 请替换为你的实际API密钥
        }
    };

    // DOM 元素
    const chatMessages = document.getElementById('chatMessages');
    const chatInput = document.getElementById('chatInput');
    const sendButton = document.getElementById('sendButton');

    // 消息历史
    let messageHistory = [
        {
            role: 'system',
            content: 'You are a helpful assistant.'
        },
        {
            role: 'assistant',
            content: '你好!我是你的AI助手,有什么可以帮助你的吗?我可以回答问题、协助思考问题或者只是陪你聊天。'
        }
    ];

    // 创建动态背景粒子
    function createParticles() {
        const particlesContainer = document.getElementById('particles');
        const particleCount = 50;

        for (let i = 0; i < particleCount; i++) {
            const particle = document.createElement('div');
            particle.className = 'particle';
            particle.style.left = Math.random() * 100 + '%';
            particle.style.top = Math.random() * 100 + '%';
            particle.style.animationDelay = Math.random() * 6 + 's';
            particle.style.animationDuration = (Math.random() * 3 + 3) + 's';
            particlesContainer.appendChild(particle);
        }
    }

    // 自适应文本框高度
    function autoResizeTextarea() {
        chatInput.style.height = 'auto';
        chatInput.style.height = Math.min(chatInput.scrollHeight, 120) + 'px';
    }

    // 添加消息到聊天界面
    function addMessage(content, isUser = false) {
        const messageDiv = document.createElement('div');
        messageDiv.className = `message ${isUser ? 'user-message' : 'ai-message'}`;
        
        const currentTime = new Date().toLocaleTimeString('zh-CN', {
            hour: '2-digit',
            minute: '2-digit'
        });

        messageDiv.innerHTML = `
            <div class="message-content">${content}</div>
            <div class="message-time">${currentTime}</div>
        `;

        chatMessages.appendChild(messageDiv);
        chatMessages.scrollTop = chatMessages.scrollHeight;
    }

    // 显示打字指示器
    function showTypingIndicator() {
        const typingDiv = document.createElement('div');
        typingDiv.className = 'typing-indicator';
        typingDiv.id = 'typingIndicator';
        typingDiv.innerHTML = `
            AI正在思考
            <div class="typing-dots">
                <div class="typing-dot"></div>
                <div class="typing-dot"></div>
                <div class="typing-dot"></div>
            </div>
        `;
        chatMessages.appendChild(typingDiv);
        chatMessages.scrollTop = chatMessages.scrollHeight;
    }

    // 隐藏打字指示器
    function hideTypingIndicator() {
        const typingIndicator = document.getElementById('typingIndicator');
        if (typingIndicator) {
            typingIndicator.remove();
        }
    }

    // 获取AI回复 - 实际API调用
    async function getAIResponse(userMessage) {
        // 将用户消息添加到历史记录
        messageHistory.push({
            role: 'user',
            content: userMessage
        });

        try {
            // 调用API获取回复
            const response = await fetch(API_CONFIG.endpoint, {
                method: 'POST',
                headers: API_CONFIG.headers,
                body: JSON.stringify({
                    model: 'deepseek-chat', // 或其他你使用的模型
                    messages: messageHistory,
                    temperature: 0.7,
                    max_tokens: 2000
                })
            });

            if (!response.ok) {
                throw new Error(`API请求失败: ${response.status}`);
            }

            const data = await response.json();
            const aiResponse = data.choices[0].message.content;

            // 将AI回复添加到历史记录
            messageHistory.push({
                role: 'assistant',
                content: aiResponse
            });

            return aiResponse;
        } catch (error) {
            console.error('API调用错误:', error);
            throw error;
        }
    }

    // 发送消息
    async function sendMessage() {
        const message = chatInput.value.trim();
        if (!message) return;

        // 禁用发送按钮
        sendButton.disabled = true;
        
        // 添加用户消息到界面
        addMessage(message, true);
        
        // 清空输入框
        chatInput.value = '';
        autoResizeTextarea();
        
        // 显示打字指示器
        showTypingIndicator();

        try {
            // 获取AI回复
            const aiResponse = await getAIResponse(message);
            
            // 隐藏打字指示器
            hideTypingIndicator();
            
            // 添加AI回复
            addMessage(aiResponse);
        } catch (error) {
            hideTypingIndicator();
            addMessage('抱歉,发生了错误,请稍后再试。');
            console.error('Error:', error);
        } finally {
            // 重新启用发送按钮
            sendButton.disabled = false;
            chatInput.focus();
        }
    }

    // 事件监听器
    chatInput.addEventListener('input', autoResizeTextarea);
    
    chatInput.addEventListener('keydown', (e) => {
        if (e.key === 'Enter' && !e.shiftKey) {
            e.preventDefault();
            sendMessage();
        }
    });

    sendButton.addEventListener('click', sendMessage);

    // 初始化
    document.addEventListener('DOMContentLoaded', () => {
        createParticles();
        chatInput.focus();
    });

手搓一个小deepseek就完成了:

image.png

总结

通过本文的介绍,我们了解了如何使用 Fetch API 与 DeepSeek 大模型进行交互,以及 HTTP 协议在其中的重要作用。WebLLM 为前端开发带来了新的可能性,让前端能够更加智能和动态。希望本文能帮助你更好地理解和应用这些技术,开启智能前端开发的新篇章。

以上就是关于将 DeepSeek 引入前端以及 HTTP 请求的详细介绍,希望对你有所帮助。如果你有任何问题或想法,欢迎在评论区留言讨论。