Vue3 实战:5分钟搭建智能对话界面,通过API Key接入大模型(通义/文心/OpenAI通用)

发布时间:2026/10/3 11:58:19
Vue3 实战:5分钟搭建智能对话界面,通过API Key接入大模型(通义/文心/OpenAI通用) 1. 从零搭建 Vue3 智能对话界面为什么我选 axios 而不是 fetch如果你正在搜「Vue3 接入大模型 API Key 教程」大概率会遇到两个卡点一是不知道请求该怎么封装二是流式输出打字机效果总是调不出来。我试过用原生 fetch 手写 ReadableStream 解析代码又长又容易在分段数据上翻车后来换成 axios 配合responseType: stream整个请求层清爽了很多。先说清楚这个界面能做什么一个纯前端的聊天窗口输入问题后调用大模型接口AI 回复逐字显示支持通义千问、文心一言、OpenAI 以及兼容 OpenAI 协议的模型。适合谁适合想快速做原型的前端同学、想练手 Vue3 组合式 API 的初学者以及需要给内部工具加个 AI 入口的开发者。核心检索词先摆出来Vue3 组合式 API、axios 封装、API Key 配置、大模型流式输出。这四个词贯穿全文你跟着做就能跑通。为什么不用 fetchfetch 处理流需要手动拿response.body.getReader()再配合TextDecoder循环读取遇到data:分段还要自己拼 buffer。axios 虽然底层也是 XHR但它在浏览器端对流的处理更顺手拦截器还能统一加请求头、统一处理 401。当然 axios 的responseType: stream在浏览器里拿到的是 XHR 的 progress 事件流不是 Node 的 Readable这点后面排障会细说。环境准备很简单Vite 创建项目装 axiosnpm create vitelatest vue3-chat -- --template vue cd vue3-chat npm install npm install axios --save npm run dev浏览器打开http://127.0.0.1:5173/看到默认页就说明环境 OK。接下来所有代码都围绕一个App.vue展开不需要路由、不需要状态管理库组合式 API 的ref和nextTick足够。这里有个认知要先建立纯前端直连大模型接口API Key 会暴露在浏览器网络面板里。个人练手没问题正式项目必须加一层后端转发。本文先把前端链路跑通Key 的安全问题在第五节单独讲。2. TaoToken 前置准备统一 Base URL 与 API Key 管理多模型切换最烦的是什么每个平台一个域名、一套鉴权、一种请求体格式。通义是dashscope.aliyuncs.comOpenAI 是api.openai.com文心又是另一套aip.baidubce.com还要先换 access_token。代码里到处写 if-else 判断平台维护起来很痛苦。我的做法是找一个兼容 OpenAI 协议的统一入口把 Base URL 收敛成一个变量。TaoToken 提供的就是这种 OpenAI 兼容接口官网在https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI 地址是https://taotoken.net/api。它的价值在于你只需要维护一份请求封装换模型只改model字段不用动 URL 和鉴权逻辑。具体怎么拿 Key登录后进控制台在 API Keys 页面创建一个新 Key复制出来形如sk-xxxx。这个 Key 就是请求头里Authorization: Bearer sk-xxxx的那串。注意创建后只显示一次丢了只能重建。模型 ID 怎么填这是新手最容易错的地方。不是填「通义千问」这种中文名而是填平台定义的模型标识比如qwen-turbo、gpt-3.5-turbo这类。你可以在模型对话页面先手动试一条确认模型 ID 能通再写进代码。模型对话入口https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite。如果你后面要做长期编码或 Agent 类应用可以了解下 Coding Plan入口在https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite。本文聚焦对话界面用按量计费的 API Key 就够。把三件套记牢Base URL、API Key、Model ID。后面所有配置片段都围绕这三个值展开。控制台地址https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteAPI Keys 管理页https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite接入文档https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite。注意不要把 Key 硬编码后提交到 Git 仓库。本文为了演示直接写在配置对象里实际项目请用.env文件配合import.meta.env.VITE_API_KEY读取并把.env加入.gitignore。3. 可复制配置axios 封装与多模型 settings 片段这一节是全文核心给你能直接粘贴的配置。先建一个src/api/chat.js把 axios 实例和请求逻辑抽出来组件里只负责 UI。// src/api/chat.js import axios from axios // 统一 Base URL换平台只改这里 const BASE_URL https://taotoken.net/api // 创建 axios 实例统一超时和请求头 const client axios.create({ baseURL: BASE_URL, timeout: 60000, headers: { Content-Type: application/json } }) // 请求拦截器自动注入 API Key client.interceptors.request.use( (config) { const key import.meta.env.VITE_API_KEY || sk-你的Key config.headers.Authorization Bearer ${key} return config }, (error) Promise.reject(error) ) // 响应拦截器统一错误提示 client.interceptors.response.use( (res) res, (error) { const status error.response?.status if (status 401) { console.error(API Key 无效或已过期请检查 Authorization 头) } else if (status 429) { console.error(请求过于频繁触发限流) } return Promise.reject(error) } ) export default client然后是模型配置用一个 JSON 结构管理切换模型只改activeModel{ models: { qwen: { label: 通义千问, model: qwen-turbo, baseURL: https://taotoken.net/api }, ernie: { label: 文心一言, model: ernie-lite-8k, baseURL: https://taotoken.net/api }, openai: { label: OpenAI, model: gpt-3.5-turbo, baseURL: https://taotoken.net/api } }, activeModel: qwen }如果你用 Vite 的环境变量建一个.env.local# .env.local VITE_API_KEYsk-你的真实Key VITE_BASE_URLhttps://taotoken.net/api三件套对照表照着填不会错配置项值说明Base URLhttps://taotoken.net/api所有模型共用API Keysk-xxxx控制台创建Bearer 鉴权Model IDqwen-turbo/gpt-3.5-turbo按模型填不是中文名流式请求的封装函数// src/api/stream.js import client from ./chat export async function streamChat({ model, messages, onDelta, onDone, onError }) { try { const response await client.post( /v1/chat/completions, { model, messages, stream: true }, { responseType: stream, onDownloadProgress: (e) { // axios 浏览器端流式通过 progress 事件拿增量 const chunk e.event?.target?.responseText || const lines chunk.split(\n).filter((l) l.startsWith(data: )) for (const line of lines) { const payload line.replace(data: , ).trim() if (payload [DONE]) { onDone onDone() return } try { const json JSON.parse(payload) const delta json.choices?.[0]?.delta?.content || if (delta) onDelta onDelta(delta) } catch (err) { // 分段数据可能不完整跳过 } } } } ) return response } catch (err) { onError onError(err) } }这里要说明一个坑axios 在浏览器端并没有真正的response.data.on(data)那是 Node 流才有的 API。网上很多教程直接抄 Node 写法在浏览器里会报response.data.on is not a function。正确做法是用onDownloadProgress配合responseText增量解析或者干脆用 fetch 的 reader。本文用 axios 的 progress 方案代码更短。组件里调用import { ref } from vue import { streamChat } from ./api/stream const messages ref([{ role: assistant, content: 你好有什么可以帮你 }]) const inputText ref() const loading ref(false) const activeModel ref(qwen) const sendMessage async () { const text inputText.value.trim() if (!text || loading.value) return messages.value.push({ role: user, content: text }) inputText.value loading.value true const aiMsg { role: assistant, content: } messages.value.push(aiMsg) await streamChat({ model: activeModel.value qwen ? qwen-turbo : gpt-3.5-turbo, messages: messages.value.filter((m) m.content), onDelta: (delta) { aiMsg.content delta }, onDone: () { loading.value false }, onError: (err) { aiMsg.content 请求失败 (err.response?.data?.error?.message || err.message) loading.value false } }) }模板部分保持简洁消息列表加输入框template div classchat-container div classchat-header Vue3 智能对话 select v-modelactiveModel option valueqwen通义千问/option option valueernie文心一言/option option valueopenaiOpenAI/option /select /div div classmessage-box refmessageBox div v-for(item, i) in messages :keyi :class[message, item.role] div classavatar{{ item.role user ? 我 : AI }}/div div classcontent{{ item.content }}/div /div /div div classinput-box textarea v-modelinputText keydown.enter.preventsendMessage rows2 / button clicksendMessage :disabledloading {{ loading ? 思考中... : 发送 }} /button /div /div /template样式部分按需调整重点是.message.user靠右、.message.assistant靠左气泡圆角和阴影让界面不那么生硬。自动滚动用nextTick把scrollTop设成scrollHeight。4. 验证请求从 401 到流式打字机效果的完整排查配置写完了怎么确认真的通了分三步验证。第一步先用 curl 验证 Key 和 Base URL 是否正确排除前端代码干扰curl https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer sk-你的Key \ -d { model: qwen-turbo, messages: [{role: user, content: 你好}], stream: false }如果返回 JSON 里有choices[0].message.content说明 Key 和模型 ID 都对。如果返回 401看错误信息是invalid_api_key还是missing_authorization前者是 Key 错后者是请求头没带上。第二步在浏览器里发一条消息打开 DevTools 的 Network 面板找到/v1/chat/completions请求。看三个地方Request Headers 里有没有Authorization: Bearer sk-xxxResponse Headers 的content-type是不是text/event-streamResponse 面板里是不是一行行data: {...}往下刷。如果 Response 是一次性返回的完整 JSON说明stream: true没生效。第三步观察界面。成功的标志是 AI 气泡里的文字逐字增加像打字机一样。如果文字一次性全出来回到streamChat检查responseType: stream和onDownloadProgress是否都写了。我实测下来最容易出问题的是onDownloadProgress里responseText的累积特性。axios 的e.event.target.responseText返回的是从请求开始到当前的完整响应文本不是增量。所以如果你每次都从头解析会导致内容重复拼接。正确做法是记录一个已处理长度只解析新增部分let processedLen 0 onDownloadProgress: (e) { const full e.event?.target?.responseText || const fresh full.slice(processedLen) processedLen full.length const lines fresh.split(\n).filter((l) l.startsWith(data: )) // ...后续解析 }这个细节网上很少讲但不处理就会出现「你好你好你好」这种重复。踩过一次就记住了。验证模型切换把下拉框切到 OpenAI发一条消息看 Network 里请求体的model字段是不是变成了gpt-3.5-turbo。如果没变检查activeModel的绑定和streamChat里的映射逻辑。验证错误处理故意把 Key 改错一位发消息看界面是否显示「请求失败401」而不是白屏或卡死。再故意断网看是否走onError分支。5. 常见报错排查401、local proxy failed、reading choices 逐个击破这一节按真实报错来你遇到哪个查哪个。报错一401 Unauthorized或invalid_api_key原因通常是三种Key 复制时带了空格、Key 已过期或被删除、请求头格式不对。检查Authorization的值必须是Bearer sk-xxxBearer 和 Key 之间一个空格。如果你用环境变量确认.env.local里没有引号包裹VITE_API_KEYsk-xxx而不是VITE_API_KEYsk-xxx。改完重启npm run devVite 的环境变量需要重启才生效。报错二local proxy failed或net::ERR_CONNECTION_REFUSED这个多半是 Base URL 写错或本地代理配置冲突。先确认BASE_URL是https://taotoken.net/api没有多余斜杠。如果你本地开了抓包工具或系统代理可能拦截了请求临时关掉再试。还有一种情况是 Vite 的server.proxy配置了转发但目标地址写错检查vite.config.js里有没有多余的 proxy 规则。报错三Cannot read properties of undefined (reading choices)这是解析响应时choices不存在。原因通常是返回的不是标准 OpenAI 格式或者流式数据里混入了非 JSON 行。加一层防御const json JSON.parse(payload) const delta json?.choices?.[0]?.delta?.content if (delta) onDelta(delta)用可选链避免直接崩。另外确认model字段填的是平台支持的 ID填错模型有时会返回错误对象而不是标准结构。报错四response.data.on is not a function前面提过这是把 Node 流写法搬到浏览器了。浏览器端 axios 没有.on(data)改用onDownloadProgress。如果你确实想用 reader 风格换成 fetchconst res await fetch(url, { method: POST, headers, body }) const reader res.body.getReader() const decoder new TextDecoder() while (true) { const { done, value } await reader.read() if (done) break const text decoder.decode(value) // 解析 text }报错五OAuth 相关错误或invalid_grant如果你接的是需要 OAuth 换 token 的平台比如某些文心接口要先拿 access_token会出现这类错误。本文用统一 Base URL 的 Bearer 鉴权绕开了 OAuth 流程如果你坚持直连原平台需要先调 token 接口换 access_token再拼到 URL 参数里。建议直接用兼容 OpenAI 协议的入口省掉这一步。报错六CORS 跨域blocked by CORS policy纯前端直连时如果目标接口没开 CORS 就会报这个。用统一 Base URL 的兼容接口通常已经配好 CORS。如果还报检查是不是请求打到了错误的域名。线上部署时用 Nginx 反代同源路径可以彻底规避。排查顺序建议先 curl 验证 Key → 再看 Network 请求头 → 再看响应格式 → 最后查前端解析逻辑。从外到内别一上来就改代码。6. 语义一致 CTA把对话界面接到你的真实工作流界面跑通只是起点。接下来你可以做三件事让它真正有用。第一把 API Key 从代码里挪到环境变量再挪到后端。前端直连适合原型正式用一定要加一层 Node 或 Serverless 转发Key 只存在服务端。转发层还能做限流、日志、敏感词过滤。第二把模型切换做成配置驱动。本文的 JSON 结构可以扩展成从接口拉取模型列表用户在前端选请求时带上对应 Model ID。三件套始终是 Base URL、API Key、Model ID换任何模型都是改这三个值。第三如果你要做长期编码助手或 Agent按量计费的 Key 可能不够划算可以看看 Coding Plan入口https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite。需要管理多个 Key 或查看用量去 API Keys 页面https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite。接入过程中遇到协议细节查接入文档https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite。想先手动验证模型 ID 是否可用用模型对话https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite。最后留一个实用技巧在streamChat里加一个AbortController用户点「停止生成」时中断请求避免长回复卡住界面。axios 支持signal参数传进去即可。这个功能在真实使用中比想象中重要尤其是模型抽风一直输出的时候。