SmallThinker-3B-Preview企业级应用基于Vue.js的管理后台智能问答模块集成最近在做一个企业知识库项目前端用的是Vue 3后端对接了几个大模型API。客户提了个需求想在管理后台里直接集成一个智能问答的模块让运营人员能快速查询内部文档和流程。我们评估了几个轻量级模型最后选了SmallThinker-3B-Preview来试试水。用下来发现这个3B参数的模型虽然不大但在企业内部的问答场景里表现挺扎实的。响应速度快对硬件要求也不高特别适合集成到现有的Vue前端应用里。今天我就结合这个实际项目聊聊怎么把SmallThinker的对话能力平滑地塞进一个企业级Vue.js管理后台里重点说说前端这块儿怎么设计和实现。1. 为什么选择SmallThinker-3B-Preview做前端集成在做技术选型的时候我们主要考虑了这么几个点。首先这个模型是纯文本对话模型接口相对简单对我们前端开发来说对接起来不复杂。其次3B的参数量意味着它推理速度比较快在普通的服务器上就能跑起来不需要昂贵的GPU集群这对很多中小企业来说是个好消息。更重要的是它的对话能力足够应对我们企业内部的知识库问答。比如问“今年的报销流程有什么变化”或者“项目立项需要哪些审批”它都能基于我们提供的文档给出准确的回答。虽然不像那些百亿参数的大模型那样能天马行空地创作但在这种有明确边界的企业场景里反而更可靠、更可控。从集成的角度看SmallThinker提供了标准的HTTP API这和我们现在Vue应用里调用其他后端服务的方式一模一样。前端只需要关心怎么发请求、怎么收响应、怎么把数据展示出来就行不需要额外学习一套复杂的通信协议。2. 前端架构设计与API调用层封装我们项目的管理后台是用Vue 3 TypeScript Pinia Element Plus这套技术栈搭建的。为了集成智能问答我们在前端专门设计了一个独立的模块。2.1 目录结构规划首先看看我们怎么组织代码的。在src目录下我们新建了一个modules/chat的文件夹里面是这么安排的src/modules/chat/ ├── api/ # API调用相关 │ ├── smallthinker.ts # SmallThinker API封装 │ └── types.ts # TypeScript类型定义 ├── components/ # Vue组件 │ ├── ChatWindow.vue # 主聊天窗口 │ ├── MessageList.vue # 消息列表 │ └── InputArea.vue # 输入区域 ├── stores/ # Pinia状态管理 │ └── chatStore.ts # 聊天状态管理 └── views/ # 页面视图 └── ChatView.vue # 聊天主页面这种结构把聊天相关的代码都放在一起维护起来方便也符合Vue 3的模块化思想。2.2 API调用层核心实现API调用层是整个集成的关键。我们在smallthinker.ts里封装了一个专门的类来处理和模型的通信。// src/modules/chat/api/smallthinker.ts import axios, { AxiosInstance, AxiosResponse } from axios; import type { ChatMessage, ChatRequest, ChatResponse } from ./types; export class SmallThinkerClient { private client: AxiosInstance; private baseURL: string; constructor(baseURL: string, apiKey?: string) { this.baseURL baseURL; this.client axios.create({ baseURL, timeout: 30000, // 30秒超时 headers: { Content-Type: application/json, ...(apiKey { Authorization: Bearer ${apiKey} }) } }); } /** * 发送单轮消息 */ async sendMessage(message: string, context?: string): PromiseChatMessage { try { const request: ChatRequest { messages: [ { role: user, content: message } ], ...(context { context }) // 可选的上下文信息 }; const response: AxiosResponseChatResponse await this.client.post( /v1/chat/completions, request ); return { id: Date.now().toString(), role: assistant, content: response.data.choices[0].message.content, timestamp: new Date() }; } catch (error) { console.error(调用SmallThinker API失败:, error); throw new Error(模型服务暂时不可用请稍后重试); } } /** * 发送多轮对话消息 */ async sendConversation(messages: ChatMessage[]): PromiseChatMessage { const request: ChatRequest { messages: messages.map(msg ({ role: msg.role, content: msg.content })) }; const response await this.client.post(/v1/chat/completions, request); return { id: Date.now().toString(), role: assistant, content: response.data.choices[0].message.content, timestamp: new Date() }; } /** * 流式响应支持如果后端支持 */ async sendMessageStream( message: string, onChunk: (chunk: string) void, onComplete: () void ): Promisevoid { // 这里可以根据后端是否支持SSE或WebSocket来实现 // 当前示例使用模拟实现 const response await this.sendMessage(message); // 模拟流式输出效果 const words response.content.split( ); for (let i 0; i words.length; i) { setTimeout(() { onChunk(words[i] (i words.length - 1 ? : )); if (i words.length - 1) { onComplete(); } }, i * 50); } } }这个封装有几个好处。第一把HTTP请求的细节都隐藏起来了业务组件里只需要调用sendMessage这样的方法就行。第二统一了错误处理网络问题或者服务异常都能在这里捕获给用户友好的提示。第三TypeScript的类型定义让代码更安全不容易出错。2.3 类型定义类型定义文件让我们的代码更有可读性和可维护性// src/modules/chat/api/types.ts export interface ChatMessage { id: string; role: user | assistant | system; content: string; timestamp: Date; error?: boolean; } export interface ChatRequest { messages: Array{ role: user | assistant | system; content: string; }; context?: string; max_tokens?: number; temperature?: number; } export interface ChatResponse { id: string; choices: Array{ message: { role: string; content: string; }; finish_reason: string; }; usage: { prompt_tokens: number; completion_tokens: number; total_tokens: number; }; } export interface Conversation { id: string; title: string; messages: ChatMessage[]; createdAt: Date; updatedAt: Date; }3. 状态管理与组件设计有了API层接下来就要考虑怎么在前端应用里管理聊天状态了。我们用的是Pinia这是Vue官方推荐的状态管理库。3.1 Pinia Store设计聊天状态管理Store是这么设计的// src/modules/chat/stores/chatStore.ts import { defineStore } from pinia; import { ref, computed } from vue; import type { ChatMessage, Conversation } from ../api/types; import { SmallThinkerClient } from ../api/smallthinker; export const useChatStore defineStore(chat, () { // 状态 const currentConversation refConversation({ id: default, title: 新对话, messages: [], createdAt: new Date(), updatedAt: new Date() }); const conversations refConversation[]([]); const isLoading ref(false); const error refstring | null(null); // SmallThinker客户端实例 const smallThinkerClient refSmallThinkerClient | null(null); // 初始化客户端 const initClient (baseURL: string, apiKey?: string) { smallThinkerClient.value new SmallThinkerClient(baseURL, apiKey); }; // 发送消息 const sendMessage async (content: string) { if (!smallThinkerClient.value) { throw new Error(SmallThinker客户端未初始化); } if (!content.trim()) return; // 添加用户消息 const userMessage: ChatMessage { id: Date.now().toString(), role: user, content: content.trim(), timestamp: new Date() }; currentConversation.value.messages.push(userMessage); currentConversation.value.updatedAt new Date(); // 设置加载状态 isLoading.value true; error.value null; try { // 调用SmallThinker API const assistantMessage await smallThinkerClient.value.sendMessage( content, getConversationContext() // 获取对话上下文 ); // 添加助手回复 currentConversation.value.messages.push(assistantMessage); currentConversation.value.updatedAt new Date(); // 如果这是第一条消息设置对话标题 if (currentConversation.value.messages.length 2) { currentConversation.value.title generateTitleFromMessage(content); } } catch (err) { error.value err instanceof Error ? err.message : 发送消息失败; // 添加错误消息 const errorMessage: ChatMessage { id: Date.now().toString(), role: assistant, content: 抱歉我暂时无法处理您的请求。请稍后重试。, timestamp: new Date(), error: true }; currentConversation.value.messages.push(errorMessage); } finally { isLoading.value false; } }; // 获取对话上下文最近几条消息 const getConversationContext (): string { const recentMessages currentConversation.value.messages.slice(-5); return recentMessages.map(msg ${msg.role}: ${msg.content}).join(\n); }; // 生成对话标题 const generateTitleFromMessage (message: string): string { const trimmed message.trim(); if (trimmed.length 20) return trimmed; return trimmed.substring(0, 20) ...; }; // 开始新对话 const startNewConversation () { // 保存当前对话 if (currentConversation.value.messages.length 0) { conversations.value.push({ ...currentConversation.value }); } // 创建新对话 currentConversation.value { id: Date.now().toString(), title: 新对话, messages: [], createdAt: new Date(), updatedAt: new Date() }; }; // 切换对话 const switchConversation (conversationId: string) { const conversation conversations.value.find(c c.id conversationId); if (conversation) { currentConversation.value { ...conversation }; } }; // 计算属性 const messageCount computed(() currentConversation.value.messages.length); const hasMessages computed(() messageCount.value 0); return { // 状态 currentConversation, conversations, isLoading, error, // 方法 initClient, sendMessage, startNewConversation, switchConversation, // 计算属性 messageCount, hasMessages }; });这个Store的设计考虑了企业应用的实际需求。比如对话历史管理用户可以创建多个对话每个对话都有自己的消息记录。还有错误处理网络异常或者服务不可用的时候能给用户清晰的反馈。3.2 聊天窗口组件实现有了Store组件实现就简单多了。主聊天窗口组件大概是这样的!-- src/modules/chat/components/ChatWindow.vue -- template div classchat-window !-- 对话历史侧边栏 -- div classconversation-sidebar v-ifshowSidebar div classsidebar-header h3对话历史/h3 el-button typeprimary sizesmall clickhandleNewChat 新对话 /el-button /div div classconversation-list div v-forconv in conversations :keyconv.id :class[conversation-item, { active: conv.id currentConversation.id }] clickswitchConversation(conv.id) div classconversation-title{{ conv.title }}/div div classconversation-time {{ formatDate(conv.updatedAt) }} /div /div /div /div !-- 主聊天区域 -- div classchat-main !-- 消息列表 -- MessageList :messagescurrentConversation.messages :is-loadingisLoading / !-- 输入区域 -- InputArea send-messagehandleSendMessage :disabledisLoading / !-- 错误提示 -- el-alert v-iferror :titleerror typeerror show-icon classerror-alert closeclearError / /div /div /template script setup langts import { computed } from vue; import { useChatStore } from ../stores/chatStore; import MessageList from ./MessageList.vue; import InputArea from ./InputArea.vue; const chatStore useChatStore(); // 计算属性 const currentConversation computed(() chatStore.currentConversation); const conversations computed(() chatStore.conversations); const isLoading computed(() chatStore.isLoading); const error computed(() chatStore.error); const showSidebar computed(() conversations.value.length 0); // 方法 const handleSendMessage (content: string) { chatStore.sendMessage(content); }; const handleNewChat () { chatStore.startNewConversation(); }; const switchConversation (id: string) { chatStore.switchConversation(id); }; const clearError () { chatStore.error null; }; const formatDate (date: Date) { return new Date(date).toLocaleDateString(zh-CN, { month: short, day: numeric, hour: 2-digit, minute: 2-digit }); }; /script style scoped .chat-window { display: flex; height: 600px; border: 1px solid #e4e7ed; border-radius: 8px; overflow: hidden; } .conversation-sidebar { width: 250px; border-right: 1px solid #e4e7ed; background-color: #fafafa; display: flex; flex-direction: column; } .sidebar-header { padding: 16px; border-bottom: 1px solid #e4e7ed; display: flex; justify-content: space-between; align-items: center; } .conversation-list { flex: 1; overflow-y: auto; padding: 8px; } .conversation-item { padding: 12px; border-radius: 6px; margin-bottom: 8px; cursor: pointer; transition: background-color 0.2s; } .conversation-item:hover { background-color: #f0f0f0; } .conversation-item.active { background-color: #e6f4ff; border: 1px solid #91caff; } .conversation-title { font-weight: 500; margin-bottom: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .conversation-time { font-size: 12px; color: #8c8c8c; } .chat-main { flex: 1; display: flex; flex-direction: column; position: relative; } .error-alert { position: absolute; bottom: 80px; left: 16px; right: 16px; } /style这个组件把聊天界面分成了几个部分左侧的对话历史列表中间的消息展示区域底部的输入框。布局清晰交互逻辑也简单明了。4. 实际应用中的优化技巧在实际项目里集成SmallThinker我们遇到了一些具体问题也总结了一些优化经验。4.1 处理长文本和复杂问题SmallThinker-3B-Preview对输入长度有限制太长的文本它处理不了。我们的做法是在前端做预处理把用户的长问题拆分成几个短问题或者提取关键信息。// 文本预处理工具函数 export const preprocessQuestion (question: string): string { // 移除多余的空格和换行 let processed question.trim().replace(/\s/g, ); // 如果问题太长提取核心部分 if (processed.length 500) { // 尝试提取问题的主要部分以问号分割 const parts processed.split(?); if (parts.length 1) { processed parts[0] ?; } else { // 如果没有问号取前500个字符 processed processed.substring(0, 500) ...; } } // 添加企业上下文提示 const contextHint 请基于公司内部知识库回答以下问题; return contextHint processed; };4.2 提升用户体验的细节为了让用户用起来更舒服我们加了几个小功能实时打字效果虽然SmallThinker本身不支持流式响应但我们可以在前端模拟这个效果让回复看起来是一个字一个字打出来的。!-- 消息气泡组件中的打字效果 -- template div classmessage-bubble assistant div v-ifisTyping classtyping-indicator span classdot/span span classdot/span span classdot/span /div div v-else classmessage-content {{ displayText }} /div /div /template script setup langts import { ref, watch, onMounted } from vue; const props defineProps{ content: string; showTypingEffect?: boolean; }(); const displayText ref(); const isTyping ref(true); onMounted(() { if (props.showTypingEffect) { simulateTyping(); } else { displayText.value props.content; isTyping.value false; } }); const simulateTyping () { let index 0; const typingInterval setInterval(() { if (index props.content.length) { displayText.value props.content.charAt(index); index; } else { clearInterval(typingInterval); isTyping.value false; } }, 30); // 每个字符30毫秒 }; /script消息持久化我们用了浏览器的localStorage来保存对话历史这样用户刷新页面或者关掉浏览器再打开之前的聊天记录还在。// 在Store中添加持久化逻辑 export const useChatStore defineStore(chat, () { // ... 其他代码 ... // 保存对话到本地存储 const saveToLocalStorage () { const data { conversations: conversations.value, currentConversationId: currentConversation.value.id }; localStorage.setItem(smallthinker_chat, JSON.stringify(data)); }; // 从本地存储加载对话 const loadFromLocalStorage () { const saved localStorage.getItem(smallthinker_chat); if (saved) { try { const data JSON.parse(saved); conversations.value data.conversations.map((conv: any) ({ ...conv, createdAt: new Date(conv.createdAt), updatedAt: new Date(conv.updatedAt), messages: conv.messages.map((msg: any) ({ ...msg, timestamp: new Date(msg.timestamp) })) })); const currentConv conversations.value.find( c c.id data.currentConversationId ); if (currentConv) { currentConversation.value currentConv; } } catch (error) { console.error(加载本地存储失败:, error); } } }; // 在发送消息后自动保存 watch(currentConversation, () { saveToLocalStorage(); }, { deep: true }); // 初始化时加载 onMounted(() { loadFromLocalStorage(); }); // ... 其他代码 ... });4.3 企业级功能扩展在实际的企业应用里我们还需要一些额外的功能对话导出让用户能把重要的对话内容导出成PDF或者Word文档。// 导出对话为Markdown export const exportConversation (conversation: Conversation): string { let markdown # ${conversation.title}\n\n; markdown 创建时间${conversation.createdAt.toLocaleString()}\n; markdown 最后更新${conversation.updatedAt.toLocaleString()}\n\n; markdown ## 对话记录\n\n; conversation.messages.forEach(message { const role message.role user ? 用户 : 助手; const time message.timestamp.toLocaleTimeString(); markdown ### ${role} (${time})\n\n; markdown ${message.content}\n\n; markdown ---\n\n; }); return markdown; };敏感信息过滤在企业环境里有时候需要过滤掉一些敏感信息。// 简单的敏感词过滤 const sensitiveWords [密码, 密钥, token, 身份证号, 手机号]; export const filterSensitiveInfo (text: string): string { let filtered text; sensitiveWords.forEach(word { const regex new RegExp(word [:]\\s*[^\\s], gi); filtered filtered.replace(regex, ${word}: ***); }); return filtered; };5. 总结把SmallThinker-3B-Preview集成到Vue.js管理后台里整个过程比想象中要顺利。这个模型虽然不大但在企业内部的问答场景里完全够用响应速度快部署成本也低对于很多中小企业来说是个不错的选择。从前端集成的角度看关键是要设计好API调用层和状态管理。把HTTP请求封装好错误处理做好状态管理清晰剩下的就是常规的Vue组件开发了。我们项目里用的这套架构从开发到上线运行都挺稳定的团队成员也容易理解和维护。实际用下来用户反馈最多的是希望回复速度能再快一点还有对复杂问题的理解能力可以再强一些。不过考虑到这是个3B的模型能有现在的表现已经很不错了。如果你们团队也在考虑在管理后台里加智能问答功能可以试试SmallThinker先从简单的场景开始跑通了再慢慢扩展功能。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。