用 DevUI 和 MateChat 做企业级 AI 助手
企业中后台接 AI,最先碰到的通常不是模型,而是界面和交互怎么落到现有体系里。组件风格不统一、对话流程和业务页面割裂、接入成本高,这些问题比'选哪个大模型'更早冒出来。DevUI 和 MateChat 的组合,解决的正是这一层。
DevUI 适合承接企业中后台的基础界面,风格克制,组件也比较完整;MateChat 则把 GenAI 场景里常见的对话、输入、引导、消息展示这些东西先整理好了。两者放在一起,能少写很多重复工作,也更容易把 AI 能力塞进现有产品里,而不是另起一套页面语言。
架构和选型
我把整个方案拆成四层:
- 展示层:DevUI 负责基础组件,MateChat 负责 AI 交互组件
- 业务层:处理消息流、上下文和页面状态
- 服务层:封装模型调用、数据处理和安全校验
- 模型层:对接盘古大模型、ChatGPT 等大语言模型
技术栈保持得比较常规:
| 技术栈 | 版本 | 作用 | 选择理由 |
|---|---|---|---|
| Vue 3 | 3.4+ | 前端框架 | 响应式性能好,生态成熟 |
| DevUI | 16.0+ | UI 组件库 | 企业级组件齐全,设计统一 |
| MateChat | 1.5+ | AI 交互组件 | 适合直接搭 GenAI 场景 |
| TypeScript | 5.0+ | 开发语言 | 类型约束足够,后期维护省心 |
| Vite | 5.0+ | 构建工具 | 启动快,开发体验好 |
| OpenAI SDK | 4.0+ | 模型对接 | 接口标准化,接入成本低 |
基础接入
先起一个 Vue 项目,再把 DevUI 和 MateChat 装进去:
# 创建 Vite 项目
npm create vite@latest ai-assistant -- --template vue-ts
cd ai-assistant
# 安装 DevUI 和 MateChat
npm install vue-devui @matechat/core @devui-design/icons
# 安装模型对接依赖
npm install openai
main.ts 里做全局注册就够了:
import { createApp } from 'vue'
import App from './App.vue'
// 引入 DevUI 和 MateChat
import DevUI from 'vue-devui'
import 'vue-devui/style.css'
import MateChat from '@matechat/core'
import '@matechat/core/style.css'
import '@devui-design/icons/icomoon/devui-icon.css'
const app = createApp(App)
app.use(DevUI)
app.use(MateChat)
app.mount('#app')
对话界面怎么搭
MateChat 的核心价值,不是'有一个聊天框',而是把企业里常见的 AI 交互细节都预先整理好了:欢迎页、提示词、气泡消息、输入区、操作按钮,这些东西拼起来就能形成一个完整的助手界面。
<template>
<McLayout>
<McHeader :title="'智能助手'" :logoImg="'https://example.com/logo.svg'">
<template #operationArea>
<div>
<i title="帮助文档"></i>
<i title="设置"></i>
</div>
</template>
</McHeader>
<McLayoutContent>
<div v-if="messages.length === 0">
<McIntroduction
:logoImg="'https://example.com/logo2x.svg'"
:title="'欢迎使用智能助手'"
:subTitle="'AI 助手为您服务'"
:description="welcomeDescription"
/>
<McPrompt
:list="welcomePrompts"
:direction="'horizontal'"
@itemClick="handlePromptClick"
/>
</div>
<template v-for="(message, index) in messages" :key="index">
<McBubble
v-if="message.role === 'user'"
:content="message.content"
:align="'right'"
:avatarConfig="{ imgSrc: '/user-avatar.svg' }"
/>
<McBubble
v-else
:content="message.content"
:avatarConfig="{ imgSrc: 'https://example.com/logo.svg' }"
:loading="message.loading"
:type="message.error ? 'error' : 'default'"
>
<template v-if="message.hasActions" #actions>
<div>
<Button icon="op-favorite" size="sm" @click="handleLike(index)">点赞</Button>
<Button icon="op-report" size="sm" @click="handleReport(index)">反馈</Button>
</div>
</template>
</McBubble>
</template>
</McLayoutContent>
<McLayoutSender>
<McInput
v-model="inputMessage"
:maxLength="2000"
placeholder="请输入您的问题..."
@submit="handleSubmit"
:disabled="isProcessing"
>
<template #extra>
<div>
<div>
<i title="@智能体"></i>
<i title="知识库"></i>
<i title="上传文件"></i>
</div>
<div>{{ inputMessage.length }}/2000</div>
</div>
</template>
</McInput>
</McLayoutSender>
</McLayout>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { Button } from 'vue-devui/button'
import 'vue-devui/button/style.css'
const welcomeDescription = [
'我是您的智能助手,可以帮助您解答技术问题、编写代码、查询文档等。',
'作为 AI 模型,我的回答可能不总是准确的,但您的反馈可以帮助我做得更好。'
]
const welcomePrompts = [
{ label: '如何优化前端性能?', value: 'performance' },
{ label: '帮我写一个排序算法', value: 'sort-algorithm' },
{ label: '解释 Vue3 的响应式原理', value: 'vue3-reactivity' }
]
const messages = ref<any[]>([])
const inputMessage = ref('')
const isProcessing = ref(false)
const handleSubmit = async () => {
if (!inputMessage.value.trim() || isProcessing.value) return
const userMessage = inputMessage.value.trim()
inputMessage.value = ''
// 添加用户消息
messages.value.push({ role: 'user', content: userMessage })
// 添加 AI 回复占位符
messages.value.push({ role: 'assistant', content: '', loading: true, hasActions: false })
isProcessing.value = true
try {
// 调用 AI 服务
const response = await callAIModel(userMessage)
// 更新 AI 回复
const lastIndex = messages.value.length - 1
messages.value[lastIndex] = { ...messages.value[lastIndex], content: response, loading: false, hasActions: true }
} catch (error) {
const lastIndex = messages.value.length - 1
messages.value[lastIndex] = { ...messages.value[lastIndex], content: '抱歉,服务暂时不可用,请稍后再试。', loading: false, error: true }
} finally {
isProcessing.value = false
}
}
// 模拟 AI 模型调用
const callAIModel = async (question: string): Promise<string> => {
return new Promise((resolve) => {
setTimeout(() => {
if (question.includes('性能')) {
resolve('前端性能优化的关键点包括:1. 代码分割和懒加载 2. 图片优化和 CDN 加速 3. 减少重绘重排 4. 使用 Web Workers 处理复杂计算 5. 服务端渲染 (SSR) 等。具体方案需要根据业务场景选择。')
} else if (question.includes('排序')) {
resolve('```typescript\nfunction quickSort(arr: number[]): number[] {\n if (arr.length <= 1) return arr;\n const pivot = arr[Math.floor(arr.length / 2)];\n const left = arr.filter(x => x < pivot);\n const right = arr.filter(x => x > pivot);\n return [...quickSort(left), pivot, ...quickSort(right)];\n}\n\n// 使用示例\nconst numbers = [5, 2, 8, 1, 9, 3];\nconsole.log(quickSort(numbers)); // [1, 2, 3, 5, 8, 9]\n```')
} else {
resolve('这是 AI 助手的回复内容。在实际应用中,这里会调用真实的大模型 API 获取响应。')
}
}, 1500)
})
}
</script>
<style scoped>
.ai-container { width: 100%; max-width: 1200px; margin: 0 auto; height: calc(100vh - 40px); display: flex; flex-direction: column; }
.chat-content { flex: 1; overflow-y: auto; padding: 20px; }
.welcome-page { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; text-align: center; }
.message-actions { display: flex; gap: 8px; margin-top: 8px; }
.input-extra { display: flex; justify-content: space-between; width: 100%; padding: 0 16px; }
.input-controls { display: flex; gap: 12px; color: #5e7ce0; cursor: pointer; }
.input-count { color: #71757f; font-size: 14px; }
.header-operations { display: flex; gap: 16px; cursor: pointer; }
</style>
实际项目里,这段代码还要再收一层。比如消息状态、错误态、流式输出,都不适合直接堆在一个组件里,但作为原型,它已经足够说明 MateChat 的接入方式。
模型服务对接
真正的 AI 能力,还是要接模型接口。这里用 OpenAI SDK 做了一个流式返回的封装,思路不复杂:把对话历史拼好,发给模型,然后一段段把返回内容更新到界面上。
import OpenAI from 'openai'
import { ref } from 'vue'
// 配置模型服务
const openai = new OpenAI({
apiKey: import.meta.env.VITE_OPENAI_API_KEY,
baseURL: import.meta.env.VITE_OPENAI_BASE_URL,
dangerouslyAllowBrowser: true
})
// 对话历史管理
const conversationHistory = ref<any[]>([])
// 流式响应处理
const handleStreamResponse = async (messages: any[], callback: (content: string) => void) => {
try {
let fullResponse = ''
const completion = await openai.chat.completions.create({
model: import.meta.env.VITE_MODEL_NAME || 'gpt-3.5-turbo',
messages: messages.map(msg => ({ role: msg.role, content: msg.content })),
stream: true,
temperature: 0.7,
max_tokens: 2000
})
for await (const chunk of completion) {
const content = chunk.choices[0]?.delta?.content || ''
fullResponse += content
callback(content)
}
return fullResponse
} catch (error) {
console.error('模型调用失败:', error)
throw new Error('服务调用失败,请稍后重试')
}
}
// 完整的对话处理函数
const processConversation = async (userInput: string) => {
// 添加用户消息到历史
conversationHistory.value.push({ role: 'user', content: userInput })
// 准备 API 调用的消息
const apiMessages = [
{ role: 'system', content: '你是一个专业的技术助手,专注于帮助开发者解决编程问题、优化代码、解释技术概念。请用中文回答,并保持专业、准确、简洁。' },
...conversationHistory.value.slice(-6) // 保留最近 6 条对话上下文
]
return new Promise<string>((resolve, reject) => {
let responseContent = ''
handleStreamResponse(apiMessages, (chunk) => {
responseContent += chunk
}).then(() => {
// 添加 AI 回复到历史
conversationHistory.value.push({ role: 'assistant', content: responseContent })
resolve(responseContent)
}).catch(reject)
})
}
这里有个取舍:直接浏览器里调用 OpenAI 方便调试,但正式环境里通常还是要把密钥放到服务端,不然安全性说不过去。示例代码保留 dangerouslyAllowBrowser: true,主要是为了演示链路。
让它更像一个能用的产品
如果只是把消息发出去,产品感还不够。下面几件事在实际场景里更常见,也更值得先做:
// 1. 消息缓存与本地存储
const saveConversationToLocalStorage = () => {
localStorage.setItem('ai_conversation_history', JSON.stringify(conversationHistory.value))
}
const loadConversationFromLocalStorage = () => {
const saved = localStorage.getItem('ai_conversation_history')
if (saved) {
conversationHistory.value = JSON.parse(saved)
}
}
// 2. 节流与防抖处理
const throttle = (func: Function, limit: number) => {
let inThrottle = false
return (...args: any[]) => {
if (!inThrottle) {
func.apply(this, args)
inThrottle = true
setTimeout(() => inThrottle = false, limit)
}
}
}
const debouncedSearch = throttle((query: string) => {
// 执行搜索逻辑
}, 300)
// 3. 虚拟滚动优化长列表
const useVirtualScroll = (items: any[], containerHeight: number, itemHeight: number) => {
const visibleItems = ref<any[]>([])
const scrollTop = ref(0)
const updateVisibleItems = () => {
const startIndex = Math.floor(scrollTop.value / itemHeight)
const endIndex = Math.min(startIndex + Math.ceil(containerHeight / itemHeight) + 2, items.value.length)
visibleItems.value = items.value.slice(startIndex, endIndex)
}
return { visibleItems, updateVisibleItems }
}
这些优化都不花哨,但够实用。尤其是消息历史、长列表和输入节流,后面都会直接影响到用户对'AI 助手稳不稳'的判断。
DevUI 和 MateChat 各自负责什么
DevUI
DevUI 更像企业中后台的地基。它不是专门为 AI 场景设计的,但它把常规业务界面需要的东西做得比较完整,适合承接复杂表格、树、表单这些页面结构。
它的几个特点比较明确:
- 组件库完整,适合做中后台页面
- 设计语言统一,页面不容易散
- 主题能力比较灵活,能贴企业品牌
- 代码开源,MIT 协议,接入成本低
DevUI 目前支持 Angular ^10.0.0 版本,项目也已经积累了比较长时间的内部实践。下面这个示例保留了它在 Angular 里的基本用法:
// DevUI 组件使用示例
import { Component } from '@angular/core';
import { DButtonComponent } from 'ng-devui/button';
@Component({
selector: 'app-example',
template: `
<d-button bsStyle="primary" (click)="handleClick()">主要按钮</d-button>
<d-table [data]="tableData" [columns]="columns"></d-table>
`,
standalone: true,
imports: [DButtonComponent]
})
export class ExampleComponent {
tableData = [
{ id: 1, name: '张三', age: 28 },
{ id: 2, name: '李四', age: 32 }
];
columns = [
{ field: 'id', header: 'ID' },
{ field: 'name', header: '姓名' },
{ field: 'age', header: '年龄' }
];
handleClick() {
console.log('按钮被点击');
}
}
MateChat
MateChat 的定位更直接,就是把 GenAI 场景常用的交互骨架搭好。它关心的是:用户怎么唤醒助手、怎么开始提问、模型回复怎么展示、过程中状态怎么表达。
它的价值在于少走弯路。对研发工具、协作平台或者知识助手这类场景来说,不需要从零设计一套聊天系统,直接用它的组件体系起步更快。
它常见的几个能力包括:
- 快速唤醒
- 输入区域扩展
- 消息气泡展示
- Markdown 渲染
- 过程态反馈
下面这段是 MateChat 的一个简化示例:
<!-- MateChat 组件使用示例 -->
<template>
<div>
<mc-header :title="'智能助手'" :logo-img="'https://example.com/logo.svg'" >
<template #operation-area>
<div>
<i></i>
<i></i>
</div>
</template>
</mc-header>
<mc-layout-content>
<mc-bubble v-for="(msg, index) in messages" :key="index"
:content="msg.content"
:align="msg.role === 'user' ? 'right' : 'left'"
:avatar-config="{ imgSrc: msg.role === 'user' ? '/user-avatar.svg' : 'https://example.com/logo.svg' }"
/>
</mc-layout-content>
<mc-layout-sender>
<mc-input v-model="inputValue" :max-length="2000" @submit="handleSend" placeholder="输入您的问题..." >
<template #extra>
<div>
<span>智能体</span>
<span>词库</span>
<span>{{ inputValue.length }}/2000</span>
</div>
</template>
</mc-input>
</mc-layout-sender>
</div>
</template>
<script setup>
import { ref } from 'vue';
const messages = ref([
{ role: 'assistant', content: '您好!我是您的智能助手,有什么可以帮助您?' }
]);
const inputValue = ref('');
const handleSend = () => {
if (!inputValue.value.trim()) return;
// 添加用户消息
messages.value.push({ role: 'user', content: inputValue.value });
// 模拟 AI 回复
setTimeout(() => {
messages.value.push({ role: 'assistant', content: `您输入了:"${inputValue.value}",这是一个 AI 助手的回复示例。` });
}, 1000);
inputValue.value = '';
};
</script>
<style scoped>
.chat-container { width: 100%; max-width: 800px; height: 600px; margin: 0 auto; border: 1px solid #ddd; border-radius: 8px; overflow: hidden; }
.chat-content { padding: 20px; overflow-y: auto; }
.input-footer { display: flex; justify-content: space-between; padding: 0 16px; color: #666; }
.icon-at, .icon-standard { cursor: pointer; margin-right: 16px; }
.input-count { color: #999; }
</style>
知识库、多模态和安全
把 AI 助手真正放到企业里,通常还会补三块:知识库、文件处理和安全控制。
知识库这块更像 RAG 的基础设施。模型本身不一定知道企业内部文档,但如果能先检索再回答,命中率会高很多:
interface KnowledgeDocument {
id: string
title: string
content: string
source: string
embedding: number[]
}
// 向量检索实现
const searchKnowledgeBase = async (query: string, topK: number = 3): Promise<KnowledgeDocument[]> => {
try {
// 调用向量数据库 API
const response = await fetch('/api/knowledge/search', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({ query: query, top_k: topK })
})
if (!response.ok) {
throw new Error('知识库搜索失败')
}
const results = await response.json()
return results.documents
} catch (error) {
console.error('知识库搜索出错:', error)
return []
}
}
// 增强对话上下文
const enhanceContextWithKnowledge = async (messages: any[]) => {
const lastUserMessage = messages[messages.length - 1]?.content || ''
// 检测是否需要知识库检索
const needsKnowledge = /如何 | 为什么 | 解释 | 文档 | 规范/.test(lastUserMessage)
if (needsKnowledge) {
const knowledgeDocs = await searchKnowledgeBase(lastUserMessage, 3)
if (knowledgeDocs.length > 0) {
const knowledgeContext = knowledgeDocs.map(doc => `【${doc.title}】\n${doc.content.substring(0, 500)}...\n来源:${doc.source}`).join('\n\n')
messages.unshift({ role: 'system', content: `以下是相关的技术文档和知识库内容,请基于这些信息回答用户问题:\n\n${knowledgeContext}` })
}
}
return messages
}
多模态这部分,重点其实不是'支持多少格式',而是把文件上传和预处理先接起来,别让入口卡在第一步:
<template>
<div>
<McInput v-model="inputMessage" placeholder="输入文字或上传文件..." @submit="handleSubmit" >
<template #prefix>
<div>
<label>
<i></i>
<input type="file" @change="handleFileUpload" accept=".txt,.md,.pdf,.png,.jpg,.jpeg" hidden >
</label>
</div>
</template>
</McInput>
<div v-if="uploadedFiles.length > 0">
<div v-for="(file, index) in uploadedFiles" :key="index">
<i :class="getFileIcon(file.type)"></i>
<span>{{ file.name }}</span>
<i @click="removeFile(index)"></i>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const uploadedFiles = ref<File[]>([])
const inputMessage = ref('')
const handleFileUpload = async (event: Event) => {
const input = event.target as HTMLInputElement
const files = input.files
if (files && files.length > 0) {
for (let i = 0; i < files.length; i++) {
const file = files[i]
// 文件大小限制
if (file.size > 10 * 1024 * 1024) { // 10MB
alert(`文件 "${file.name}" 大小超过限制(10MB)`)
continue
}
// 图片文件预处理
if (file.type.startsWith('image/')) {
const processedImage = await processImage(file)
uploadedFiles.value.push({ ...file, processedImage } as File)
} else {
uploadedFiles.value.push(file)
}
}
// 重置 input
input.value = ''
}
}
const processImage = async (file: File): Promise<string> => {
return new Promise((resolve) => {
const reader = new FileReader()
reader.onload = (e) => {
const img = new Image()
img.onload = () => {
// 创建 canvas 进行缩放
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')!
// 计算缩放比例
const maxWidth = 800
const ratio = Math.min(1, maxWidth / img.width)
canvas.width = img.width * ratio
canvas.height = img.height * ratio
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
// 转换为 base64
const base64 = canvas.toDataURL('image/jpeg', 0.8)
resolve(base64)
}
img.src = e.target?.result as string
}
reader.readAsDataURL(file)
})
}
</script>
安全这块没有捷径,输入校验、敏感词过滤、重试和错误上报这些东西,往往比'多一个炫酷组件'更重要:
// 输入验证与过滤
const sanitizeInput = (input: string): string => {
// XSS 防护
const div = document.createElement('div')
div.textContent = input
let safeInput = div.innerHTML
// 敏感词过滤
const sensitiveWords = ['password', 'secret', 'token', 'admin']
sensitiveWords.forEach(word => {
const regex = new RegExp(word, 'gi')
safeInput = safeInput.replace(regex, '***')
})
// 长度限制
if (safeInput.length > 2000) {
safeInput = safeInput.substring(0, 2000) + '...'
}
return safeInput
}
// API 调用安全封装
const safeAPICall = async <T>(apiCall: () => Promise<T>, retries = 3): Promise<T> => {
for (let i = 0; i < retries; i++) {
try {
return await apiCall()
} catch (error) {
if (i === retries - 1) throw error
// 指数退避重试
const delay = Math.pow(2, i) * 1000
await new Promise(resolve => setTimeout(resolve, delay))
}
}
throw new Error('API 调用失败')
}
部署和运维
到部署阶段,容器化和流水线是比较标准的做法。代码不复杂,但能把交付方式固定下来:
# Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install --production=false
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
# .gitlab-ci.yml
stages:
- build
- test
- deploy
build:
stage: build
image: node:18
script:
- npm install
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 hour
test:
stage: test
image: node:18
script:
- npm install
- npm run test:unit
- npm run test:e2e
deploy-production:
stage: deploy
image: alpine
script:
- apk add --no-cache curl
- curl -X POST -H "Authorization: Bearer $DEPLOY_TOKEN" https://api.example.com/deploy
only:
- main
结尾
这套组合的价值不在'AI'两个字本身,而在于它把企业中后台最容易散掉的那几部分——基础 UI、对话交互、模型接入和工程化落地——拉到了一起。DevUI 负责稳住界面底盘,MateChat 负责把 AI 场景的交互骨架搭出来,模型和知识库再往上补能力,链路就完整了。
如果只是做一个演示,很多东西可以再简化;但真要进企业系统,安全、上下文、错误处理和部署方式都不能省。这个方案的好处是起步快,坏处也很明显:它不是'一套代码通吃所有场景',后面还是要根据业务再做收敛和拆分。


