AI 组件库-MateChat × 大模型:DeepSeek、OpenAI 和 阿里通义问 (Qwen)的全流程接入实战(三)

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()

八、总结

  1. MateChat + 统一 Hook 的一套前端,可随时切换 DeepSeek OpenAI 通义千问 (Qwen)
相关推荐
逝缘~10 分钟前
小白学Pinia状态管理
前端·javascript·vue.js·vscode·es6·pinia
光影少年13 分钟前
vite原理
前端·javascript·vue.js
C MIKE21 分钟前
ztree.js前端插件样式文字大小文字背景修改
开发语言·前端·javascript
!win !41 分钟前
uni-app项目怎么实现多服务环境切换
前端·uni-app
源猿人41 分钟前
文化与代码的交汇:OpenCC 驱动的中文语系兼容性解决方案
前端·vue.js
xw544 分钟前
uni-app项目怎么实现多服务环境切换
前端·uni-app
Kjjia1 小时前
到底是 react 性能拉胯?还是吃了机制的亏
前端·react.js
ViceBoy_1 小时前
前端的Promise的方法all,race,any
前端·javascript
飞翔的猪猪1 小时前
GitHub Recovery Codes - 用于 GitHub Two-factor authentication (2FA) 凭据丢失时登录账号
前端·git·github
前端开发熊1 小时前
实时薪资追踪-每秒都让收入看得见的 Chrome 扩展,你还不来试试?
前端