欢迎光临
我们一直在努力

GLM-4-9B-Chat-1M前端集成:Vue.js调用本地模型示例

GLM-4-9B-Chat-1M前端集成:Vue.js调用本地模型示例

你是不是也遇到过这样的场景?公司内部有个强大的本地大模型,比如能处理百万字长文的GLM-4-9B-Chat-1M,但它被部署在服务器上,只能通过命令行或者简陋的接口调用。产品经理、运营同事想用,却总说“太麻烦”、“不会用”。

这时候,一个漂亮、易用的前端界面就成了刚需。今天,我就来手把手教你,如何用大家最熟悉的Vue.js框架,快速搭建一个能直接调用本地GLM-4-9B-Chat-1M模型的前端应用。整个过程就像给一台强大的发动机装上一个舒适好用的方向盘和仪表盘。

我们将从零开始,构建一个具备对话界面、支持长文本上传、并能实时显示模型响应的Web应用。学完这篇,你不仅能掌握前端调用本地大模型的核心方法,还能获得一个可以直接复用的项目模板。

1. 项目目标与环境准备

在开始敲代码之前,我们先明确要做什么,以及需要准备好哪些“食材”。

1.1 我们要实现什么?

想象一下,最终的产品应该长这样:

  • 一个清爽的聊天界面:左侧是对话历史,中间是主要的输入和展示区域,布局清晰。
  • 核心的文本输入框:用户可以在这里输入问题,或者粘贴一整段长文本(比如一篇报告、一章小说)。
  • 文件上传功能:用户可以直接上传.txt、.pdf、.docx等格式的文档,前端自动读取内容并发送给模型。
  • 实时交互与流式响应:用户点击发送后,能立刻看到模型正在“思考”(Thinking…),然后答案像打字一样逐字逐句地显示出来,体验流畅。
  • 与后端顺畅通信:前端通过HTTP请求,调用我们本地部署的GLM-4-9B-Chat-1M模型的API接口。
  • 1.2 环境与工具清单

    请确保你的电脑上已经安装了以下工具:

    • Node.js 与 npm:这是Vue.js的运行基础。建议安装最新的LTS版本。安装后,在终端输入 node -v 和 npm -v 检查是否成功。
    • Vue CLI 或 Vite:这是创建Vue项目的脚手架。本文推荐使用更轻快的 Vite。如果你还没安装,可以通过npm安装:npm install -g @vue/cli 或直接使用Vite。
    • 一个现代浏览器:Chrome、Edge或Firefox的最新版。
    • 本地运行的GLM-4-9B-Chat-1M后端:这是前提!你需要已经按照官方或相关教程,在本地(通常是 http://localhost:8080 或类似地址)成功部署了模型服务,并且知道它的API调用地址(例如 http://localhost:8080/v1/chat/completions)。本文假设你的后端已经就绪。

    2. 创建Vue项目与核心依赖安装

    万事俱备,我们开始动手创建项目。

    2.1 使用Vite快速创建项目

    打开你的终端,进入一个你喜欢的目录,执行以下命令:

    # 使用 npm 7+ 的额外双横杠
    npm create vue@latest glm-4-vue-frontend

    执行后,命令行会交互式地询问你一些配置选项。为了简单起见,我们做如下选择:

    • Add TypeScript? -> No (本文使用JavaScript,更易懂)
    • Add JSX Support? -> No
    • Add Vue Router for Single Page Application? -> Yes (虽然本项目可能用不到多页面,但装上以备不时之需)
    • Add Pinia for state management? -> Yes (状态管理很重要,推荐安装)
    • Add Vitest for Unit Testing? -> No (教程阶段可跳过)
    • Add an End-to-End Testing Solution? -> No
    • Add ESLint for code quality? -> Yes (保持代码规范)

    选择完成后,Vite会自动创建项目并安装初始依赖。进入项目目录并安装额外需要的库:

    cd glm-4-vue-frontend
    npm install

    2.2 安装项目必需的第三方库

    我们的应用需要一些额外的库来增强功能:

    • axios: 用于向后端API发送HTTP请求,比原生fetch更易用、功能更全。
    • element-plus: 一个基于Vue 3的桌面端UI组件库,能让我们快速搭建出美观的界面。
    • markdown-it: 将模型返回的Markdown格式文本渲染成漂亮的HTML。

    在项目根目录下,运行:

    npm install axios element-plus markdown-it

    安装完成后,我们需要在 main.js 中全局引入Element Plus。打开 src/main.js 文件,修改如下:

    import { createApp } from 'vue'
    import App from './App.vue'
    import router from './router'

    // 引入Element Plus及其样式
    import ElementPlus from 'element-plus'
    import 'element-plus/dist/index.css'

    // 创建应用实例
    const app = createApp(App)

    // 使用路由和Element Plus
    app.use(router)
    app.use(ElementPlus)

    // 挂载应用
    app.mount('#app')

    3. 构建聊天界面与核心逻辑

    接下来是重头戏,我们将创建应用的核心页面组件。

    3.1 创建聊天主页面组件

    在 src/views 目录下(如果没有就创建一个),新建一个文件 ChatView.vue。这个文件将包含我们应用的所有前端逻辑和界面。

    我们先搭建基础的模板结构:

    <template>
    <div class="chat-container">
    <el-container>
    <!– 左侧边栏:对话历史 –>
    <el-aside width="250px" class="sidebar">
    <div class="sidebar-header">
    <h3>对话历史</h3>
    <el-button type="primary" size="small" @click="startNewChat">新对话</el-button>
    </div>
    <el-scrollbar class="history-list">
    <div
    v-for="(session, index) in chatSessions"
    :key="index"
    :class="['history-item', { active: currentSessionIndex === index }]"
    @click="switchSession(index)"
    >
    {{ session.title || `对话 ${index + 1}` }}
    </div>
    </el-scrollbar>
    </el-aside>

    <!– 主内容区 –>
    <el-main class="main-content">
    <!– 消息展示区域 –>
    <div class="messages-container" ref="messagesContainer">
    <div v-for="(msg, index) in currentMessages" :key="index" :class="['message', msg.role]">
    <div class="avatar">
    <el-avatar :style="{ background: msg.role === 'user' ? '#409EFF' : '#67C23A' }">
    {{ msg.role === 'user' ? '我' : 'AI' }}
    </el-avatar>
    </div>
    <div class="message-content">
    <div class="message-header">
    <strong>{{ msg.role === 'user' ? '你' : 'GLM-4-9B-Chat' }}</strong>
    <span class="time">{{ msg.timestamp }}</span>
    </div>
    <div class="message-body" v-html="renderMarkdown(msg.content)"></div>
    </div>
    </div>
    <!– 加载指示器 –>
    <div v-if="isLoading" class="message assistant">
    <div class="avatar">
    <el-avatar style="background: #67C23A;">AI</el-avatar>
    </div>
    <div class="message-content">
    <div class="message-header">
    <strong>GLM-4-9B-Chat</strong>
    <span class="time">正在思考…</span>
    </div>
    <div class="message-body">
    <el-icon class="is-loading"><Loading /></el-icon> 正在生成回答,请稍候…
    </div>
    </div>
    </div>
    </div>

    <!– 输入区域 –>
    <div class="input-area">
    <div class="input-tools">
    <el-tooltip content="上传文档">
    <el-button :icon="Upload" circle @click="triggerFileUpload" />
    </el-tooltip>
    <input
    type="file"
    ref="fileInput"
    style="display: none"
    @change="handleFileUpload"
    accept=".txt,.pdf,.docx,.md"
    />
    </div>
    <el-input
    v-model="userInput"
    type="textarea"
    :rows="4"
    placeholder="请输入您的问题,或粘贴长文本…(支持上传txt/pdf/docx文件)"
    @keyup.enter.exact="sendMessage"
    :disabled="isLoading"
    />
    <div class="input-actions">
    <el-button type="primary" :loading="isLoading" @click="sendMessage">
    发送
    </el-button>
    <el-button @click="clearCurrentSession">清空</el-button>
    </div>
    </div>
    </el-main>
    </el-container>
    </div>
    </template>

    <script setup>
    // 组件逻辑将在下一步编写
    </script>

    <style scoped>
    /* 样式部分将在下一步编写 */
    </style>

    3.2 实现组件的核心逻辑

    现在,我们来填充 <script setup> 部分,实现数据管理和核心功能。

    <script setup>
    import { ref, computed, onMounted, nextTick, watch } from 'vue'
    import { useChatStore } from '@/stores/chat' // 我们将使用Pinia管理状态
    import { Upload, Loading } from '@element-plus/icons-vue'
    import axios from 'axios'
    import MarkdownIt from 'markdown-it'

    // 1. 初始化Markdown渲染器
    const md = new MarkdownIt()

    // 2. 定义响应式数据
    const userInput = ref('') // 用户输入
    const isLoading = ref(false) // 加载状态
    const fileInput = ref(null) // 文件输入框引用
    const messagesContainer = ref(null) // 消息容器引用,用于自动滚动

    // 3. 使用Pinia Store获取状态和方法
    const chatStore = useChatStore()
    const { chatSessions, currentSessionIndex } = chatStore

    // 4. 计算属性:获取当前会话的消息
    const currentMessages = computed(() => {
    if (chatStore.chatSessions[chatStore.currentSessionIndex]) {
    return chatStore.chatSessions[chatStore.currentSessionIndex].messages
    }
    return []
    })

    // 5. 核心方法:发送消息到本地模型
    const sendMessage = async () => {
    const inputText = userInput.value.trim()
    if (!inputText || isLoading.value) return

    // 将用户消息添加到当前会话
    chatStore.addMessageToCurrentSession({
    role: 'user',
    content: inputText,
    timestamp: new Date().toLocaleTimeString()
    })

    // 清空输入框
    userInput.value = ''
    isLoading.value = true

    // 自动滚动到底部
    scrollToBottom()

    try {
    // 配置你的本地模型API地址
    const API_URL = 'http://localhost:8080/v1/chat/completions' // 请根据你的后端实际地址修改

    // 构建请求数据
    const requestData = {
    model: 'glm-4-9b-chat-1m', // 模型名称,需与后端匹配
    messages: chatStore.getCurrentSessionMessagesForAPI(), // 获取格式化的历史消息
    stream: true, // 启用流式输出,获得更好的体验
    max_tokens: 4096 // 控制回复的最大长度
    }

    // 发送请求(流式)
    const response = await fetch(API_URL, {
    method: 'POST',
    headers: {
    'Content-Type': 'application/json',
    },
    body: JSON.stringify(requestData)
    })

    if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`)
    }

    const reader = response.body.getReader()
    const decoder = new TextDecoder('utf-8')
    let assistantMessageContent = ''

    // 创建AI的初始消息条目
    chatStore.addMessageToCurrentSession({
    role: 'assistant',
    content: '',
    timestamp: new Date().toLocaleTimeString()
    })

    // 读取流式数据
    while (true) {
    const { done, value } = await reader.read()
    if (done) break

    const chunk = decoder.decode(value)
    const lines = chunk.split('\\n').filter(line => line.trim() !== '')

    for (const line of lines) {
    if (line.startsWith('data: ')) {
    const data = line.slice(6)
    if (data === '[DONE]') {
    break
    }

    try {
    const parsed = JSON.parse(data)
    const content = parsed.choices[0]?.delta?.content || ''
    if (content) {
    assistantMessageContent += content
    // 更新最后一条消息(即AI的消息)的内容
    chatStore.updateLastAssistantMessage(assistantMessageContent)
    scrollToBottom()
    }
    } catch (e) {
    console.error('解析流数据出错:', e)
    }
    }
    }
    }

    } catch (error) {
    console.error('调用模型API失败:', error)
    // 在界面上显示错误信息
    chatStore.addMessageToCurrentSession({
    role: 'assistant',
    content: `抱歉,请求模型时出现错误:${error.message}`,
    timestamp: new Date().toLocaleTimeString()
    })
    } finally {
    isLoading.value = false
    scrollToBottom()
    }
    }

    // 6. 工具方法:渲染Markdown为HTML
    const renderMarkdown = (content) => {
    return md.render(content || '')
    }

    // 7. 工具方法:自动滚动到消息底部
    const scrollToBottom = () => {
    nextTick(() => {
    if (messagesContainer.value) {
    messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
    }
    })
    }

    // 8. 文件上传相关方法
    const triggerFileUpload = () => {
    fileInput.value?.click()
    }

    const handleFileUpload = (event) => {
    const file = event.target.files[0]
    if (!file) return

    const reader = new FileReader()
    reader.onload = (e) => {
    const text = e.target.result
    // 简单处理:将文件内容放入输入框
    userInput.value = `【文件内容】\\n${text}\\n\\n请分析以上内容。`
    // 可选:也可以直接发送
    // userInput.value = text;
    // sendMessage();
    }

    // 这里只处理文本文件,对于PDF/DOCX需要更复杂的解析库(如pdf.js, mammoth)
    if (file.type === 'text/plain' || file.name.endsWith('.txt') || file.name.endsWith('.md')) {
    reader.readAsText(file)
    } else {
    alert('目前仅支持.txt和.md格式的文本文件。PDF/DOCX解析需要额外集成前端库。')
    }

    // 清空input,以便再次选择同一文件
    event.target.value = ''
    }

    // 9. 其他界面交互方法
    const startNewChat = () => {
    chatStore.createNewSession()
    }

    const switchSession = (index) => {
    chatStore.switchToSession(index)
    }

    const clearCurrentSession = () => {
    chatStore.clearCurrentSession()
    }

    // 10. 监听消息变化,自动滚动
    watch(currentMessages, () => {
    scrollToBottom()
    }, { deep: true })

    // 11. 组件挂载时初始化一个会话
    onMounted(() => {
    if (chatStore.chatSessions.length === 0) {
    chatStore.createNewSession()
    }
    scrollToBottom()
    })
    </script>

    3.3 添加组件样式

    最后,将以下样式代码填入 <style scoped> 部分,让界面变得美观。

    <style scoped>
    .chat-container {
    height: 100vh;
    display: flex;
    background-color: #f5f7fa;
    }

    .sidebar {
    background-color: #fff;
    border-right: 1px solid #e4e7ed;
    display: flex;
    flex-direction: column;
    }

    .sidebar-header {
    padding: 20px;
    border-bottom: 1px solid #e4e7ed;
    display: flex;
    justify-content: space-between;
    align-items: center;
    }

    .sidebar-header h3 {
    margin: 0;
    font-size: 16px;
    color: #303133;
    }

    .history-list {
    flex: 1;
    padding: 10px;
    }

    .history-item {
    padding: 12px 15px;
    margin-bottom: 8px;
    border-radius: 6px;
    cursor: pointer;
    font-size: 14px;
    color: #606266;
    transition: all 0.3s;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    }

    .history-item:hover {
    background-color: #f0f2f5;
    color: #409EFF;
    }

    .history-item.active {
    background-color: #ecf5ff;
    color: #409EFF;
    font-weight: 500;
    }

    .main-content {
    display: flex;
    flex-direction: column;
    padding: 0;
    background-color: #fff;
    }

    .messages-container {
    flex: 1;
    overflow-y: auto;
    padding: 20px;
    display: flex;
    flex-direction: column;
    gap: 20px;
    }

    .message {
    display: flex;
    gap: 15px;
    max-width: 85%;
    }

    .message.user {
    align-self: flex-end;
    flex-direction: row-reverse;
    }

    .message.assistant {
    align-self: flex-start;
    }

    .avatar {
    flex-shrink: 0;
    }

    .message-content {
    flex: 1;
    }

    .message.user .message-content {
    align-items: flex-end;
    }

    .message-header {
    margin-bottom: 8px;
    display: flex;
    align-items: center;
    gap: 10px;
    }

    .message.user .message-header {
    flex-direction: row-reverse;
    }

    .message-header strong {
    font-size: 14px;
    color: #303133;
    }

    .message-header .time {
    font-size: 12px;
    color: #909399;
    }

    .message-body {
    padding: 12px 16px;
    border-radius: 8px;
    font-size: 14px;
    line-height: 1.6;
    word-wrap: break-word;
    }

    .message.user .message-body {
    background-color: #409EFF;
    color: white;
    border-top-right-radius: 2px;
    }

    .message.assistant .message-body {
    background-color: #f0f2f5;
    color: #303133;
    border-top-left-radius: 2px;
    }

    .message-body :deep(pre) {
    background-color: #2d2d2d;
    color: #f8f8f2;
    padding: 12px;
    border-radius: 6px;
    overflow-x: auto;
    }

    .message-body :deep(code) {
    font-family: 'Courier New', monospace;
    padding: 2px 4px;
    border-radius: 3px;
    }

    .message-body :deep(blockquote) {
    border-left: 4px solid #409EFF;
    padding-left: 12px;
    margin: 8px 0;
    color: #606266;
    }

    .input-area {
    border-top: 1px solid #e4e7ed;
    padding: 20px;
    background-color: #fff;
    }

    .input-tools {
    margin-bottom: 10px;
    display: flex;
    gap: 10px;
    }

    .input-actions {
    margin-top: 15px;
    display: flex;
    justify-content: flex-end;
    gap: 10px;
    }
    </style>

    4. 使用Pinia管理应用状态

    为了更优雅地管理对话历史、当前会话等状态,我们使用Vue 3官方推荐的状态管理库Pinia。在 src/stores 目录下创建 chat.js 文件。

    // src/stores/chat.js
    import { defineStore } from 'pinia'
    import { ref, computed } from 'vue'

    export const useChatStore = defineStore('chat', () => {
    // 状态定义
    const chatSessions = ref([
    {
    id: 1,
    title: '新对话',
    messages: [
    { role: 'assistant', content: '你好!我是GLM-4-9B-Chat,一个支持百万字长文本的本地大模型。你可以直接向我提问,或者粘贴长文本让我分析。', timestamp: new Date().toLocaleTimeString() }
    ]
    }
    ])

    const currentSessionIndex = ref(0)

    // 计算属性
    const currentSession = computed(() => chatSessions.value[currentSessionIndex.value])

    // 操作方法
    const createNewSession = () => {
    const newSession = {
    id: Date.now(),
    title: `对话 ${chatSessions.value.length + 1}`,
    messages: [
    { role: 'assistant', content: '你好!我是GLM-4-9B-Chat,一个支持百万字长文本的本地大模型。你可以直接向我提问,或者粘贴长文本让我分析。', timestamp: new Date().toLocaleTimeString() }
    ]
    }
    chatSessions.value.push(newSession)
    currentSessionIndex.value = chatSessions.value.length – 1
    }

    const switchToSession = (index) => {
    if (index >= 0 && index < chatSessions.value.length) {
    currentSessionIndex.value = index
    }
    }

    const addMessageToCurrentSession = (message) => {
    if (chatSessions.value[currentSessionIndex.value]) {
    chatSessions.value[currentSessionIndex.value].messages.push(message)
    // 如果这是用户的第一条消息,可以更新会话标题
    if (message.role === 'user' && chatSessions.value[currentSessionIndex.value].messages.filter(m => m.role === 'user').length === 1) {
    const shortContent = message.content.length > 20 ? message.content.substring(0, 20) + '…' : message.content
    chatSessions.value[currentSessionIndex.value].title = shortContent
    }
    }
    }

    const updateLastAssistantMessage = (content) => {
    const session = chatSessions.value[currentSessionIndex.value]
    if (session && session.messages.length > 0) {
    const lastMessage = session.messages[session.messages.length – 1]
    if (lastMessage.role === 'assistant') {
    lastMessage.content = content
    }
    }
    }

    const clearCurrentSession = () => {
    if (chatSessions.value[currentSessionIndex.value]) {
    chatSessions.value[currentSessionIndex.value].messages = [
    { role: 'assistant', content: '对话已清空。请问有什么可以帮您?', timestamp: new Date().toLocaleTimeString() }
    ]
    }
    }

    const getCurrentSessionMessagesForAPI = () => {
    const session = chatSessions.value[currentSessionIndex.value]
    if (!session) return []
    // 转换为我们本地模型API需要的格式(通常是OpenAI兼容格式)
    return session.messages.map(msg => ({
    role: msg.role,
    content: msg.content
    }))
    }

    return {
    chatSessions,
    currentSessionIndex,
    currentSession,
    createNewSession,
    switchToSession,
    addMessageToCurrentSession,
    updateLastAssistantMessage,
    clearCurrentSession,
    getCurrentSessionMessagesForAPI
    }
    })

    5. 配置路由与运行项目

    5.1 配置路由

    修改 src/router/index.js 文件,将我们创建的聊天页面设置为首页。

    import { createRouter, createWebHistory } from 'vue-router'
    import ChatView from '@/views/ChatView.vue' // 确保路径正确

    const router = createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    routes: [
    {
    path: '/',
    name: 'chat',
    component: ChatView
    }
    // 你可以在这里添加更多路由
    ]
    })

    export default router

    5.2 修改App.vue并运行

    最后,简化 src/App.vue 文件,让它只作为路由的容器。

    <template>
    <RouterView />
    </template>

    <script setup>
    import { RouterView } from 'vue-router'
    </script>

    <style>
    body {
    margin: 0;
    padding: 0;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
    }
    </style>

    现在,一切就绪!在项目根目录下运行开发服务器:

    npm run dev

    打开浏览器,访问 http://localhost:5173(Vite默认端口),你应该能看到一个功能完整的聊天界面了。

    6. 关键点总结与进阶思考

    恭喜你!你已经成功搭建了一个能够调用本地GLM-4-9B-Chat-1M模型的Vue.js前端应用。让我们回顾一下核心步骤和关键点:

  • 项目初始化与依赖安装:使用Vite创建项目,并安装Axios、Element Plus等必要库。
  • 构建聊天界面:利用Element Plus的布局和组件,快速搭建了包含侧边栏、消息区和输入框的主界面。
  • 实现核心通信逻辑:在 sendMessage 函数中,我们使用 fetch API 以流式(stream: true)方式调用本地模型接口。这是最关键的一步,请务必根据你实际的后端API地址和参数格式进行调整。
  • 状态管理:使用Pinia集中管理所有对话会话和消息,使得状态清晰、易于维护。
  • 优化用户体验:实现了消息的Markdown渲染、自动滚动、加载状态提示和简单的文件上传功能。
  • 6.1 你可能遇到的挑战与解决方案

    • 跨域问题(CORS):如果你的前端(localhost:5173)和后端(localhost:8080)端口不同,浏览器会因安全策略阻止请求。解决方案:在后端服务中配置CORS头,允许前端域名访问。或者,在开发阶段使用Vite的代理功能(配置 vite.config.js)。
    • API格式不匹配:本文假设后端提供的是OpenAI兼容的API格式(/v1/chat/completions)。如果你的后端接口格式不同,需要调整 requestData 的结构和请求方式。
    • 流式响应解析:我们使用了简单的 data: 前缀来解析Server-Sent Events (SSE)。确保你的后端流式响应格式与此匹配。
    • 长文本处理:GLM-4-9B-Chat-1M支持百万上下文,但前端直接粘贴巨量文本可能影响体验。可以考虑分片上传或提供进度提示。

    6.2 下一步可以做什么?

    这个基础版本已经可用,但还有很大的优化和扩展空间:

    • 美化与响应式:进一步优化CSS,让界面在手机端也能良好显示。
    • 对话持久化:将对话历史保存到浏览器的LocalStorage或IndexedDB,刷新页面不丢失。
    • 高级文件处理:集成 pdf.js 和 mammoth.js 等库,真正实现前端解析PDF和Word文档。
    • 参数调节界面:在侧边栏或输入框附近添加滑动条,让用户可以实时调整 temperature、max_tokens 等模型参数。
    • 多模型切换:如果你的本地部署了多个模型,可以增加一个下拉框让用户选择。
    • 错误处理与重试:增加更完善的网络错误、模型超时等情况的处理与重试机制。

    通过这个项目,你不仅学会了前端集成大模型的方法,更重要的是掌握了一套将强大后端AI能力“包装”成用户友好产品的思路。动手试试吧,把你的本地大模型变成一个大家都能轻松使用的智能工具!


    获取更多AI镜像

    想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

    赞(0)
    未经允许不得转载:171主机测评 » GLM-4-9B-Chat-1M前端集成:Vue.js调用本地模型示例
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址