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 同样使用它 |
pnpm add openai # 或 npm / yarn 官方 SDK,可同时调用 DeepSeek / OpenAI / Qwen
三、环境变量
将密钥写入环境变量最安全;在浏览器侧演示时可暂时放到 .env.*,但务必做好打包替换/代理。
.env.* 按模型划分:
# .env.deepseekVITE_LLM_URL=https://api.deepseek.comVITE_LLM_MODEL=deepseek-reasonerVITE_LLM_KEY=<DeepSeek-Key># .env.openaiVITE_LLM_URL=https://api.openai.com/v1VITE_LLM_MODEL=gpt-4oVITE_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 代码
// hooks/useChatModel.tsimport 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 使用
// App.vue (片段)const messages = ref([]);const { send } = useChatModel(messages);function onSubmit(text: string) { if (!text) return; send(text).catch(console.error);}
五、模型配置与切换
1. package.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. 项目结构
src/ ├─ App.vue ├─ constants/ │ └─ llmProviders.ts └─ hooks/ └─ useChatModelDynamic.ts
2. llmProviders.ts 文件
/** * 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 文件
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
<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)。