MateChat × 大模型:DeepSeek、OpenAI 和 阿里 通义问 (Qwen)的全流程接入实战
一、MateChat 为何能"一键切换"多种模型?
MateChat 并不内置任何推理逻辑,而是把 "请求大模型 → 拆解响应 → 填充 messages" 这一段完全交给业务方。
只要你的模型 遵循 OpenAI-兼容的 Chat Completion 协议,改 3 行代码就能完成切换。
DeepSeek、OpenAI、阿里百炼的 通义千问 (Qwen) 都额外提供了 OpenAI-兼容的 Chat Completion 协议------
只要你换 API Key、BASE_URL、model 名称,MateChat 侧无需变动,即可来回切。
二、环境准备与包安装
依赖 | 描述 |
---|---|
openai NPM 包 | 官方 SDK,DeepSeek 同样使用它 |
bash
pnpm add openai # 或 npm / yarn 官方 SDK,可同时调用 DeepSeek / OpenAI / Qwen
三、环境变量
将密钥写入环境变量最安全;在浏览器侧演示时可暂时放到 .env.*,但务必做好打包替换/代理。
.env.* 按模型划分:
bash
# .env.deepseek
VITE_LLM_URL=https://api.deepseek.com
VITE_LLM_MODEL=deepseek-reasoner
VITE_LLM_KEY=<DeepSeek-Key>
# .env.openai
VITE_LLM_URL=https://api.openai.com/v1
VITE_LLM_MODEL=gpt-4o
VITE_LLM_KEY=<OpenAI-Key>
# .env.qwen
VITE_LLM_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 # 官方 BASE_URL [oai_citation:1‡help.aliyun.com](https://help.aliyun.com/zh/model-studio/compatibility-of-openai-with-dashscope)
VITE_LLM_MODEL=qwen-max # 支持列表见文档 [oai_citation:2‡help.aliyun.com](https://help.aliyun.com/zh/model-studio/compatibility-of-openai-with-dashscope)
VITE_LLM_KEY=<DashScope-API-Key>
四、通用「模型适配器」 Hook
1. Hook 代码
typescript
// hooks/useChatModel.ts
import OpenAI from 'openai';
import { ref } from 'vue';
// ★ 1. 用环境变量决定当前供应商
export const client = new OpenAI({
apiKey: import.meta.env.VITE_LLM_KEY,
baseURL: import.meta.env.VITE_LLM_URL,
dangerouslyAllowBrowser: true
});
export function useChatModel(messages) {
async function send(question: string) {
messages.value.push({ from: 'user', content: question });
const idx = messages.value.push({ from: 'model', content: '', loading: true }) - 1;
// ★ 2. 发起流式请求------DeepSeek / OpenAI / Qwen 三选一,由 env 决定
const stream = await client.chat.completions.create({
model: import.meta.env.VITE_LLM_MODEL, // deepseek-reasoner / gpt-4o / qwen-max ...
messages: [{ role: 'user', content: question }],
stream: true // 非流式时设 false
});
messages.value[idx].loading = false;
// ★ 3. 不同厂商的增量字段完全一致 → 统一解析
for await (const chunk of stream) {
messages.value[idx].content += chunk.choices[0]?.delta?.content || '';
}
}
return { send };
}
- 非流式:把 stream:false,直接拿 completion.choices[0].message.content。
- 多轮上下文:将 messages.value 过滤并转换成 { role, content }[] 继续传给模型即可。
2. Hooks 使用
vue
// App.vue (片段)
const messages = ref([]);
const { send } = useChatModel(messages);
function onSubmit(text: string) {
if (!text) return;
send(text).catch(console.error);
}
五、模型配置与切换
1. package.json 配置
json
{
"scripts": {
"dev:deepseek": "vite --mode deepseek",
"dev:openai": "vite --mode openai",
"dev:qwen": "vite --mode qwen",
"build:deepseek": "vite build --mode deepseek",
"build:openai": "vite build --mode openai",
"build:qwen": "vite build --mode qwen"
}
}
2. 命令执行
执行命令 | Vite 会自动加载的文件(优先级从低到高) |
---|---|
vite --mode deepseek | .env → .env.local → .env.deepseek → .env.deepseek.local |
vite build --mode qwen | .env → .env.local → .env.qwen → .env.qwen.local |
无 --mode 参数 (vite dev) | .env → .env.local → .env.development → .env.development.local |
六、使用 Hooks 切换
1. 项目结构
shell
src/
├─ App.vue
├─ constants/
│ └─ llmProviders.ts
└─ hooks/
└─ useChatModelDynamic.ts
2. llmProviders.ts 文件
typescript
/**
* LLM 提供商
*/
export interface LLMProvider {
label: string
value: 'deepseek' | 'openai' | 'qwen'
baseURL: string
model: string
apiKey: string
}
/**
* LLM 提供商
*/
export const LLM_PROVIDERS: LLMProvider[] = [
{
label: 'DeepSeek',
value: 'deepseek',
baseURL: 'https://api.deepseek.com',
model: 'deepseek-reasoner',
apiKey: '<DeepSeek_API_Key>'
},
{
label: 'OpenAI',
value: 'openai',
baseURL: 'https://api.openai.com/v1',
model: 'gpt-4o',
apiKey: '<OpenAI_API_Key>'
},
{
label: 'Qwen (通义千问)',
value: 'qwen',
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
model: 'qwen-max',
apiKey: '<DashScope_API_Key>'
}
]
3. useChatModelDynamic.ts 文件
typescript
import { ref, watch } from 'vue'
import OpenAI from 'openai'
import type { LLMProvider } from '../constants/llmProviders'
/**
* 聊天消息
*/
interface ChatMessage {
from: 'user' | 'model';
content: string;
loading?: boolean;
}
/**
* 使用动态的 LLM 模型
* @param messages 聊天消息
* @param provider LLM 提供商
* @returns
*/
export function useChatModelDynamic(
messages: ReturnType<typeof ref<ChatMessage[]>>,
provider: ReturnType<typeof ref<LLMProvider>>
) {
let client = createClient()
watch(provider, () => { client = createClient() })
function createClient() {
return new OpenAI({
apiKey: provider.value?.apiKey,
baseURL: provider.value?.baseURL,
dangerouslyAllowBrowser: true
})
}
const send = async (question: string) => {
if (!messages.value) return;
messages.value.push({ from: 'user', content: question })
const idx = messages.value.push({ from: 'model', content: '', loading: true }) - 1
const stream = await client.chat.completions.create({
model: provider.value?.model || '',
messages: [{ role: 'user', content: question }],
stream: true
})
if (!messages.value) return;
messages.value[idx].loading = false
for await (const chunk of stream) {
messages.value[idx].content += chunk.choices[0]?.delta?.content || ''
}
}
return { send }
}
4. App.vue
tsx
<template>
<div class="container">
<!-- 顶部:模型切换 -->
<div class="toolbar">
<label>选择模型:</label>
<select v-model="currentValue">
<option v-for="p in PROVIDERS" :key="p.value" :value="p.value">
{{ p.label }}
</option>
</select>
</div>
<!-- MateChat 对话区 -->
<McLayout class="board">
<McLayoutContent class="content">
<template v-for="(m, i) in messages" :key="i">
<McBubble :content="m.content" :loading="m.loading" :align="m.from === 'user' ? 'right' : 'left'"
:avatarConfig="m.from === 'user' ? userAvatar : modelAvatar" />
</template>
</McLayoutContent>
<McLayoutSender>
<McInput :value="input" @change="(v: string) => input = v" @submit="onSubmit" />
</McLayoutSender>
</McLayout>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { LLM_PROVIDERS as PROVIDERS } from './constants/llmProviders'
import { useChatModelDynamic } from './hooks/useChatModelDynamic'
const currentValue = ref<'deepseek' | 'openai' | 'qwen'>('deepseek')
const provider = computed(() => PROVIDERS.find(p => p.value === currentValue.value)!)
const messages = ref<any[]>([])
const input = ref('')
const { send } = useChatModelDynamic(messages, provider)
function onSubmit(text: string) {
if (!text.trim()) return
send(text).catch(err => alert(err.message))
input.value = ''
}
const userAvatar = { imgSrc: 'https://matechat.gitcode.com/png/demo/userAvatar.svg' }
const modelAvatar = { imgSrc: 'https://matechat.gitcode.com/logo.svg' }
</script>
<style scoped>
.container {
max-width: 800px;
margin: 24px auto;
display: flex;
flex-direction: column;
gap: 16px;
}
.toolbar {
display: flex;
gap: 8px;
align-items: center;
}
.board {
height: calc(100vh - 150px);
display: flex;
flex-direction: column;
}
.content {
flex: 1;
overflow: auto;
display: flex;
flex-direction: column;
gap: 8px;
}
</style>
5. UI

七、常见问题&解决方案
问题 | 可能原因 | 处理方案 |
---|---|---|
401: Authentication failed | Key 过期 / 填错 | 检查 VITE_LLM_KEY 是否正确;Qwen Key 请到「百炼控制台 → API Key」重新生成 |
404: model_not_found | VITE_LLM_MODEL 拼错 | DeepSeek 用 deepseek-,Qwen 用 qwen-,注意大小写 |
SSE 一直断线 | 本地 HTTP 或代理剪掉 text/event-stream | 开启 vite preview --https 或通过自家后端代理 |
中文 Markdown 乱码 | 未给 McMarkDown 注入高亮器 | import 'highlight.js/styles/github-dark.css' 并在组件挂载后 hljs.highlightAll() |
八、总结
- MateChat + 统一 Hook 的一套前端,可随时切换 DeepSeek ↔ OpenAI ↔ 通义千问 (Qwen)。