百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

大模型实战:Flask+H5三件套实现大模型基础聊天界面

zhezhongyun 2025-07-09 00:20 1 浏览

本文使用 Flask 和 H5 三件套(HTML+JS+CSS)实现大模型聊天应用的基本方式

话不多说,先贴上实现效果:

流式输出:

思考输出:

聊天界面

模型设置:

模型设置

会话切换:

前言

大模型的聊天应用从功能到 UI 设计来说都已经非常标准化了,然而身为小白的我至今还天真地以为页面上的流式响应是一门了不起的技术。 于是在得空的时候亲手实现了一个名为 Chat Mate 的聊天应用,该应用主打低代码量和简单易用,并且实现了 Chat 应用需要具备的流式输出、历史记录、模型思考等功能。 项目采用前后端分离的方式,前端完全由原生 HTML、JS、CSS 编写,没有使用任何封装好的框架,后端使用 Python 的 Flask 编写,实现简单。 用户可以输入自己已经购买的 API 调用商用 LLM,也可以调用通过 Ollama 部署的本地模型。 完整的项目代码放在了 Github 上(见文末),欢迎小伙伴们下载学习和二次开发

关键实现

项目采用前后端分离的方式设计,分别使用 Flask 框架编写前端服务器和后端服务器。

  • 页面文件:见 /templates/index.html 文件,其中 /templates 目录方便 Flask 直接读取和渲染。
  • 样式文件:见 /static/styles.css 文件。
  • 脚本文件:见 /static/script.js 文件,其中 /static 目录也是为了方便 Flask 直接读取和渲染。
  • 后端文件:见 /web_server.py 和 /openai_server.py 文件,其中 /web_server.py 为前端服务器,/openai_server.py 为后端服务器。

一、页面设计

仿照主流聊天应用,前端页面主要包含侧边栏和主要内容区域,其中侧边栏用于显示历史记录和设置,主要内容区域用于显示聊天记录和输入框。


<html lang="zh-CN"><head>    <meta charset="UTF-8">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    <title>Chat Mate - 聊天搭子title>    <link rel="stylesheet" type="text/css" href="../static/styles.css">head><body>        <div class="sidebar" id="sidebar">        <div class="sidebar-header">            <div class="sidebar-title">历史对话div>            <button class="close-sidebar" id="close-sidebar">×button>        div>        <div class="history-list" id="history-list">                    div>        <div class="sidebar-footer">            <button class="new-chat-btn" id="new-chat-btn">开始新对话button>            设置 -->        div>    div>        <div class="settings-modal" id="settings-modal">        <div class="settings-content">            <div class="settings-header">                <h3 class="settings-title">API 设置h3>                <button class="close-settings" id="close-settings">×button>            div>            <form class="settings-form" id="settings-form">                <div class="form-group">                    <label for="api-url" class="form-label">API 地址label>                    <input type="text" id="api-url" class="form-input" placeholder="https://api.example.com/v1">                div>                <div class="form-group">                    <label for="api-key" class="form-label">API 密钥label>                    <input type="password" id="api-key" class="form-input" placeholder="输入您的API密钥">                div>                <div class="form-group">                    <label for="model-name" class="form-label">模型名称label>                    <input type="text" id="model-name" class="form-input" placeholder="Qwen/QwQ-32B">                div>            form>            <div class="settings-footer">                <button type="button" class="btn btn-secondary" id="clear-settings">清除button>                <button type="button" class="btn btn-primary" id="save-settings">保存button>            div>        div>    div>        <div class="sidebar-overlay" id="sidebar-overlay">div>        <div class="main-content" id="main-content">        <button class="menu-btn" id="settings-btn">            <img src="../static/logo/a-more2.svg" alt="更多" width="24" height="24">        button>        <button class="menu-btn" id="menu-btn" style="top: 80px;">            <img src="../static/logo/lishijilu.svg" alt="历史记录" width="24" height="24">        button>        <button class="menu-btn" id="clear-history-btn" style="top: 140px;">            <img src="../static/logo/shuaxin.svg" alt="刷新" width="24" height="24">        button>        <main>            <div class="chat-container">                <div class="chat-header">                    <div class="status">div>                    <span>Chat Mate - 你的在线聊天伙伴span>                div>                <div class="chat-messages" id="chat-messages">                    <div class="message-container bot">                        <div class="avatar bot-avatar">AIdiv>                        <div class="message bot-message">                            你好!我是你的聊天搭子。你可以跟我说说你的感受和想法,我会认真倾听并给予温暖的回应。今天有什么想分享的吗?                            <div class="message-time">刚刚div>                        div>                    div>                div>                <div class="typing-indicator" id="typing-indicator">                    <div class="typing-dot">div>                    <div class="typing-dot">div>                    <div class="typing-dot">div>                div>                <div class="input-area">                    <textarea class="message-input" id="message-input" placeholder="向 AI 发送消息 嗖嗖~咻~" rows="1">textarea>                    <button class="send-button" id="send-button">                        <svg class="send-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">                            <path d="M22 2L11 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>                            <path d="M22 2L15 22L11 13L2 9L22 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>                        svg>                    button>                div>            div>        main>    div>    <script src="../static/scripts.js">script>body>html>

流式输出

流式输出的实现主要依赖如下三个函数,前端中依赖 generateResponseupdateStreamingMessage 函数,后端中依赖 stream_openai_generate 函数。

  1. generateResponse 函数中,使用 fetch API 发送 POST 请求,并使用 response.body.getReader() 获取可读流。
async function generateResponse(userMessage) {    // ... 其他代码 ...    try {        const response = await fetch(`http://localhost:${SERVER_PORT}/stream_openai_generate`, {            method: 'POST',            headers: {'Content-Type': 'application/json'},            body: JSON.stringify({/* 请求数据 */})        });        const reader = response.body.getReader(); // 获取可读流        const decoder = new TextDecoder();        let solution = '';        let responseText = '';        while (true) {            const {done, value} = await reader.read(); // 读取数据块            if (done) break;            const chunk = decoder.decode(value); // 解码数据            const lines = chunk.split('\n');            for (const line of lines) {                if (line.startsWith('data:')) {                    const data = JSON.parse(line.substring(5).trim());                    // 根据字段类型累积内容                    if (data.reason != null && data.reason) {                        solution += data.text;                        updateStreamingMessage(solution, 'reasoner'); // 实时更新解决方案                    } else {                        responseText += data.text;                        updateStreamingMessage(responseText, 'bot'); // 实时更新回复                    }                }            }        }        // ... 返回最终结果 ...    } catch (error) {        console.error('Error:', error);        // ... 错误处理 ...    }}

2. 在 updateStreamingMessage 函数中,使用 innerHTML 更新消息内容,并支持 HTML 换行。

function updateStreamingMessage(text, sender) {    // 查找或创建消息容器    let messageContainer = document.querySelector(`.message-container.${sender}:last-child`);    if (!messageContainer) {        // 创建新消息容器(头像+消息框)        messageContainer = document.createElement('div');        messageContainer.classList.add('message-container', sender);        // ... 创建avatar和messageDiv ...        chatMessages.appendChild(messageContainer);    }    // 更新消息内容(支持HTML换行)    const messageDiv = messageContainer.querySelector('.message');    messageDiv.innerHTML = text.replace(/\n/g, '
'
); // 自动滚动到底部 chatMessages.scrollTop = chatMessages.scrollHeight;}

3. 前端使用 fetch API 发送 POST 请求,将调用大模型所需的 base_url、api_key 和 model 三个参数发送给后端服务器(见
script.js/generateResponse
函数)。

try {    const response = await fetch(`http://localhost:${SERVER_PORT}/stream_openai_generate`, {        method: 'POST',        headers: {            'Content-Type': 'application/json',        },        body: JSON.stringify({            messages: currentConversation.messages,            base_url: apiUrlInput.value,            api_key: apiKeyInput.value,            model: apiModelInput.value,            newMessage: userMessage,        })    });    if (!response.ok) {        throw new Error(`HTTP error! status: ${response.status}`);    }    const reader = response.body.getReader();    const decoder = new TextDecoder();    let solution = '';    let responseText = '';    let currentField = '';    while (true) {        const {done, value} = await reader.read();        // console.log('value:', value);        // console.log('done:', done);        if (done) break;        const chunk = decoder.decode(value);        console.log('chunk:', chunk);        const lines = chunk.split('\n');        for (const line of lines) {            if (line.startsWith('data:')) {                const data = JSON.parse(line.substring(5).trim());                if (data.reason != null && data.reason) {                    solution += data.text;                    currentField = 'solution';                } else {                    responseText += data.text;                    currentField = 'response';                }                // 实时更新消息                if (currentField === 'solution') {                    updateStreamingMessage(solution, 'reasoner');                } else if (currentField === 'response') {                    updateStreamingMessage(responseText, 'bot');                }            }        }    }    // 返回最终结果    return {        solution: solution,        responseText: responseText    };} catch (error) {    console.error('Error:', error);    // 处理错误}

4. 在后端 openai_server.py 文件中,使用 stream_openai_generate 函数,将请求数据发送给 OpenAI API 或 Ollama,并使用 yield 逐行返回结果。

from flask import Flask, request, Responsefrom flask_cors import CORS  # 导入CORSimport jsonimport ollamaapp = Flask(__name__)CORS(app)  # 启用CORS支持role = {    'bot': 'assistant',    'user': 'user'}def generate_stream_response_by_openai(messages=None, model='Qwen/QwQ-32B', base_url=None, api_key=None):    from openai import OpenAI    client = OpenAI(        base_url=base_url,        api_key=api_key,  # ModelScope Token    )    response = client.chat.completions.create(        model=model,  # ModelScope Model-Id        messages=messages,        stream=True    )    for chunk in response:        reasoning_chunk = chunk.choices[0].delta.reasoning_content  # delta.reasoning_content 是推理        answer_chunk = chunk.choices[0].delta.content  # delta.content 是响应内容        if reasoning_chunk != '':            yield f"data: {json.dumps({'text': reasoning_chunk, 'reason': True}, ensure_ascii=False)}\n"        elif answer_chunk != '':            yield f"data: {json.dumps({'text': answer_chunk, 'reason': False}, ensure_ascii=False)}\n"        yield ""  # 结束标记def generate_stream_response_by_ollama(messages=None, model='qwen2'):    response = ollama.chat(        model=model,        messages=messages,        stream=True    )    for chunk in response:        answer_chunk = chunk['message']['content']        if answer_chunk != '':            yield f"data: {json.dumps({'text': answer_chunk, 'reason': False}, ensure_ascii=False)}\n"        yield ""  # 结束标记@app.route('/stream_openai_generate', methods=['POST'])def stream_generate_openai():    print(request.json)    # 获取请求中的输入数据    data = request.json['messages']    model = request.json['model']    base_url = request.json['base_url']    api_key = request.json['api_key']    messages = []    for line in data[1:]:        messages.append({            'role': role[line['sender']],            'content': line['text']        })    print(messages)    if base_url != 'Ollama':        # 返回流式响应        return Response(            generate_stream_response_by_openai(                messages=messages,                model=model,                base_url=base_url,                api_key=api_key            ),            mimetype='text/event-stream',  # Server-Sent Events类型            headers={                'X-Accel-Buffering': 'no',  # 禁用Nginx缓存                'Cache-Control': 'no-cache'            }        )    else:        # 返回流式响应        return Response(            generate_stream_response_by_ollama(                messages=messages,                model=model            ),            mimetype='text/event-stream',  # Server-Sent Events类型            headers={                'X-Accel-Buffering': 'no',  # 禁用Nginx缓存                'Cache-Control': 'no-cache'            }        )if __name__ == '__main__':    app.run(host='0.0.0.0', port=8000)

侧边栏的显示与隐藏

侧边栏的实现原理如下:

  1. 初始状态:
  • 侧边栏通过transform: translateX(-100%)隐藏在屏幕左侧
  • 遮罩层透明度为0且不可点击

2. 显示侧边栏

  • 点击菜单按钮触发toggleSidebar()
  • 为侧边栏添加active类,使其平移到可视区域
  • 同时显示半透明遮罩层

3. 隐藏侧边栏

  • 点击关闭按钮或遮罩层再次触发toggleSidebar()
  • 移除active类,侧边栏平移回隐藏位置
  • 隐藏遮罩层

4. 动画效果

  • 通过CSS的transition属性实现平滑的滑动动画
  • 遮罩层也有淡入淡出效果

HTML结构

<div id="sidebar">...div><div id="sidebar-overlay">div>

CSS样式

.sidebar {    transform: translateX(-100%); /* 默认隐藏 */    transition: transform 0.3s ease; /* 添加过渡动画 */}.sidebar.active {    transform: translateX(0); /* 显示状态 */}.sidebar-overlay {    opacity: 0;    pointer-events: none; /* 默认不可点击 */    transition: opacity 0.3s ease;}.sidebar-overlay.active {    opacity: 1;    pointer-events: all; /* 激活时可点击 */}

JavaScript控制

// 获取DOM元素const sidebar = document.getElementById('sidebar');const sidebarOverlay = document.getElementById('sidebar-overlay');const menuBtn = document.getElementById('menu-btn');const closeSidebar = document.getElementById('close-sidebar');// 切换侧边栏函数function toggleSidebar() {    sidebar.classList.toggle('active');    sidebarOverlay.classList.toggle('active');    mainContent.classList.toggle('sidebar-open');}// 事件监听menuBtn.addEventListener('click', toggleSidebar);closeSidebar.addEventListener('click', toggleSidebar);sidebarOverlay.addEventListener('click', toggleSidebar);

总结

以上仅展示了项目的部分功能和关键代码,完整代码和功能说明请查阅项目仓库。

代码地址:

相关推荐

一篇文章带你了解SVG 渐变知识(svg动画效果)

渐变是一种从一种颜色到另一种颜色的平滑过渡。另外,可以把多个颜色的过渡应用到同一个元素上。SVG渐变主要有两种类型:(Linear,Radial)。一、SVG线性渐变<linearGradie...

Vue3 实战指南:15 个高效组件开发技巧解析

Vue.js作为一款流行的JavaScript框架,在前端开发领域占据着重要地位。Vue3的发布,更是带来了诸多令人兴奋的新特性和改进,让开发者能够更高效地构建应用程序。今天,我们就来深入探讨...

CSS渲染性能优化(低阻抗喷油器阻值一般为多少欧)

在当今快节奏的互联网环境中,网页加载速度直接影响用户体验和业务转化率。页面加载时间每增加100毫秒,就会导致显著的流量和收入损失。作为前端开发的重要组成部分,CSS的渲染性能优化不容忽视。为什么CSS...

前端面试题-Vue 项目中,你做过哪些性能优化?

在Vue项目中,以下是我在生产环境中实践过且用户反馈较好的性能优化方案,整理为分类要点:一、代码层面优化1.代码分割与懒加载路由懒加载:使用`()=>import()`动态导入组件,结...

如何通过JavaScript判断Web页面按钮是否置灰?

在JavaScript语言中判断Web页面按钮是否置灰(禁用状态),可以通过以下几种方式实现,其具体情形取决于按钮的禁用方式(原生disabled属性或CSS样式控制):一、检查原生dis...

「图片显示移植-1」 尝试用opengl/GLFW显示图片

GLFW【https://www.glfw.org】调用了opengl来做图形的显示。我最近需要用opengl来显示图像,不能使用opencv等库。看了一个glfw的官网,里面有github:http...

大模型实战:Flask+H5三件套实现大模型基础聊天界面

本文使用Flask和H5三件套(HTML+JS+CSS)实现大模型聊天应用的基本方式话不多说,先贴上实现效果:流式输出:思考输出:聊天界面模型设置:模型设置会话切换:前言大模型的聊天应用从功能...

ae基础知识(二)(ae必学知识)

hi,大家好,我今天要给大家继续分享的还是ae的基础知识,今天主要分享的就是关于ae的路径文字制作步骤(时间关系没有截图)、动态文字的制作知识、以及ae特效的扭曲的一些基本操作。最后再次复习一下ae的...

YSLOW性能测试前端调优23大规则(二十一)---避免过滤器

AlphalmageLoader过滤器是IE浏览器专有的一个关于图片的属性,主要是为了解决半透明真彩色的PNG显示问题。AlphalmageLoader的语法如下:filter:progid:DX...

Chrome浏览器的渲染流程详解(chrome预览)

我们来详细介绍一下浏览器的**渲染流程**。渲染流程是浏览器将从网络获取到的HTML、CSS和JavaScript文件,最终转化为用户屏幕上可见的、可交互的像素画面的过程。它是一个复杂但高度优...

在 WordPress 中如何设置背景色透明度?

最近开始写一些WordPress专业的知识,阅读数奇低,然后我发一些微信昵称技巧,又说我天天发这些小学生爱玩的玩意,写点文章真不容易。那我两天发点专业的东西,两天发点小学生的东西,剩下三天我看着办...

manim 数学动画之旅--图形样式(数学图形绘制)

manim绘制图形时,除了上一节提到的那些必需的参数,还有一些可选的参数,这些参数可以控制图形显示的样式。绘制各类基本图形(点,线,圆,多边形等)时,每个图形都有自己的默认的样式,比如上一节的图形,...

Web页面如此耗电!到了某种程度,会是大损失

现在用户上网大多使用移动设备或者笔记本电脑。对这两者来说,电池寿命都很重要。在这篇文章里,我们将讨论影响电池寿命的因素,以及作为一个web开发者,我们如何让网页耗电更少,以便用户有更多时间来关注我们的...

11.mxGraph的mxCell和Styles样式(graph style)

3.1.3mxCell[翻译]mxCell是顶点和边的单元对象。mxCell复制了模型中可用的许多功能。使用上的关键区别是,使用模型方法会创建适当的事件通知和撤销,而使用单元进行更改时没有更改记...

按钮重复点击:这“简单”问题,为何难住大半面试者与开发者?

在前端开发中,按钮重复点击是一个看似不起眼,实则非常普遍且容易引发线上事故的问题。想象一下:提交表单时,因为网络卡顿或手抖,重复点击导致后端创建了多条冗余数据…这些场景不仅影响用户体验,更可能造成实...