豆包快速/深度思考模型流式调用:Vue3.5 + Node.js 的 SSE 实践与 TaoToken 统一接入

发布时间:2026/10/20 6:45:28
豆包快速/深度思考模型流式调用:Vue3.5 + Node.js 的 SSE 实践与 TaoToken 统一接入 1. 豆包双模型流式调用到底难在哪Vue3.5 前端与 Node.js 服务端协作的真实场景豆包快速思考模型和深度思考模型是字节跳动火山引擎方舟平台上的两类对话模型。快速模型主打低延迟适合日常问答、文案润色深度思考模型会先输出一段思维链reasoning再给最终答案适合数学解题、复杂推理。两者都支持 SSE 流式输出但请求参数和响应结构并不一致这就是很多人在 Vue3.5 Node.js 项目里踩坑的起点。我最近在做一个对话式 AI 应用前端用 Vue3.5 的组合式 API后端用 Node.js Express 做代理。需求很明确用户点一下按钮就能在快速和深度之间切换回答要逐字渲染深度模型的思考过程也要单独展示。听起来简单实际动手才发现几个绕不开的问题。第一个问题是参数差异。快速模型用max_tokens控制输出长度深度模型要用max_output_tokens而且深度模型多了一个thinking字段控制思考模式。如果前端直接拼参数切换模型时逻辑会散落在组件里维护起来很痛苦。第二个问题是响应结构差异。快速模型的流式分片在choices[0].delta.content深度模型的思维链在output数组里type: reasoning是思考过程type: message才是最终回答。前端如果同时处理两种结构代码会变得很脏。第三个问题是 SSE 本身。浏览器原生EventSource只支持 GET 请求但对话接口必须用 POST 传 messages 数组。所以必须自己封装一个支持 POST 的 SSE 客户端还要处理分片粘包、错误重试、连接关闭这些细节。第四个问题是密钥管理。豆包 API Key 不能硬编码在前端必须放在 Node.js 服务端由服务端统一转发。但如果每个模型都配一套 Key 和 Base URL环境变量会越来越多。这时候用 TaoToken 做统一接入就省事很多——一个 Key、一个 Base URL切换模型只改 Model ID。这篇文章就把这套方案完整拆开从 Node.js 的 SSE 代理配置到 Vue3.5 的组合式函数封装再到用 TaoToken 统一 Key 调用 API 的验证步骤。你可以直接复制代码跑起来也可以按自己的项目结构调整。适合正在做 AI 对话应用、需要流式输出和双模型切换的前端和后端开发者。2. TaoToken 统一接入前置一个 Key 管住豆包快速与深度思考模型在讲代码之前先把接入层的事情说清楚。豆包模型本身要通过火山引擎方舟平台调用你需要拿到 API Key、Base URL 和 Model ID。但如果你同时用多个模型比如快速、深度、甚至其他厂商的模型每个都去配一套密钥和环境变量项目里会到处都是XXX_API_KEY切换环境时容易漏改。TaoToken 的思路是做一个统一的 API 网关你只需要一个 TaoToken 的 API Key就能调用它支持的多个模型。Base URL 统一是https://taotoken.net/api模型通过 Model ID 区分。这样 Node.js 服务端的配置就简化成三个变量TAOTOKEN_API_KEY、BASE_URL、以及各个模型的MODEL_ID。具体怎么拿 Key打开 TaoToken 官网https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content注册后在控制台创建 API Key。控制台地址是https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewriteAPI Keys 管理页在https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite。创建完复制那串sk-开头的 Key后面 Node.js 的.env里要用。模型 ID 怎么填TaoToken 的模型列表和文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite你可以查到豆包快速和深度模型对应的 ID。一般来说快速模型类似doubao-seed-1-6-flash深度模型类似doubao-seed-1-6-thinking具体以文档为准。如果你用的是 Claude Code 做编码辅助TaoToken 也有对应的接入方式文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite里能找到 ClaudeCodeAnthropic 的配置说明。这里要强调一点TaoToken 是合规的 API 接入服务不是所谓的“中转”或“代理”。它的作用是帮你统一管理多个模型的调用凭证减少配置复杂度。你在 Node.js 服务端调用时请求格式和直接调豆包 API 基本一致只是 Base URL 和 Key 换成 TaoToken 的。配置上我建议用.env文件加dotenv加载.env一定要加进.gitignore。三个核心变量# .env TAOTOKEN_API_KEYsk-你的TaoToken密钥 BASE_URLhttps://taotoken.net/api QUICK_MODEL_IDdoubao-seed-1-6-flash DEEP_MODEL_IDdoubao-seed-1-6-thinking这样 Node.js 里读process.env.QUICK_MODEL_ID和process.env.DEEP_MODEL_ID就能区分双模型切换模型只需要改这两个值不用动代码。如果你后面要加新模型也只需要在.env里加一行 Model ID服务端逻辑不用改。注意TaoToken 的 API Key 只在服务端使用绝对不要暴露给前端。前端只调用你自己的 Node.js 接口由 Node.js 去请求 TaoToken。3. 可复制配置Node.js SSE 代理与 Vue3.5 组合式函数完整代码这一节是全文的核心给出可以直接复制的配置和代码。分三部分Node.js 服务端的 SSE 代理、Vue3.5 的 SSE 客户端封装、以及前端组件的调用方式。3.1 Node.js 服务端Express SSE 代理配置先装依赖npm init -y npm install express axios dotenv cors然后建server.js核心逻辑是接收前端 POST 请求根据modelType拼装豆包参数用axios以stream模式请求 TaoToken再把流式分片转发给前端。// server.js require(dotenv).config(); const express require(express); const axios require(axios); const cors require(cors); const app express(); app.use(cors()); app.use(express.json()); const CONFIG { apiKey: process.env.TAOTOKEN_API_KEY, baseUrl: process.env.BASE_URL, models: { quick: process.env.QUICK_MODEL_ID, deep: process.env.DEEP_MODEL_ID, }, }; // 统一处理双模型 async function handleChat(req, res, modelType) { const { messages, temperature 0.7, maxTokens 2000 } req.body; if (!messages || !Array.isArray(messages)) { return res.status(400).json({ error: messages 必须是数组 }); } // SSE 响应头 res.setHeader(Content-Type, text/event-stream); res.setHeader(Cache-Control, no-cache); res.setHeader(Connection, keep-alive); res.flushHeaders(); // 按模型类型拼参数 const requestData { model: CONFIG.models[modelType], messages, stream: true, temperature, ...(modelType deep ? { thinking: { type: auto }, max_output_tokens: maxTokens } : { max_tokens: maxTokens }), }; try { const response await axios.post( ${CONFIG.baseUrl}/chat/completions, requestData, { headers: { Authorization: Bearer ${CONFIG.apiKey}, Content-Type: application/json, }, responseType: stream, } ); let buffer ; response.data.on(data, (chunk) { buffer chunk.toString(); const lines buffer.split(\n); buffer lines.pop() || ; lines.forEach((line) { if (!line.trim().startsWith(data: )) return; const payload line.slice(6).trim(); if (payload [DONE]) return; try { const data JSON.parse(payload); let content ; let reasoning ; if (modelType deep) { // 深度模型output 数组 const output data.output || []; output.forEach((item) { if (item.type reasoning) { reasoning item.content?.[0]?.text || ; } else if (item.type message) { content item.content?.[0]?.text || ; } }); } else { // 快速模型choices content data.choices?.[0]?.delta?.content || ; } if (content || reasoning) { res.write( data: ${JSON.stringify({ code: 0, content, reasoning })}\n\n ); } } catch (e) { // 单条解析失败不影响整体 } }); }); response.data.on(end, () { res.write(data: ${JSON.stringify({ code: 0, status: done })}\n\n); res.end(); }); response.data.on(error, (err) { res.write( data: ${JSON.stringify({ code: 1, status: error, msg: err.message })}\n\n ); res.end(); }); } catch (err) { const msg err.response?.data?.error?.message || err.message; res.write(data: ${JSON.stringify({ code: 1, status: error, msg })}\n\n); res.end(); } } app.post(/api/chat/quick, (req, res) handleChat(req, res, quick)); app.post(/api/chat/deep, (req, res) handleChat(req, res, deep)); app.listen(3000, () console.log(Server running on http://localhost:3000));这段代码的关键点responseType: stream必须加否则 axios 会把整个响应缓冲完再返回流式就失效了。buffer用来处理分片粘包——SSE 的data:行可能被 TCP 拆成两半必须保留不完整的最后一行等下一个 chunk 拼上再解析。3.2 Vue3.5 组合式函数封装 SSE 客户端前端建一个useChatStream.js把 SSE 连接、逐字渲染、错误重试都封装进去。Vue3.5 的组合式 API 让状态和逻辑可以抽离成独立函数组件里只负责调用。// composables/useChatStream.js import { ref, onUnmounted } from vue; export function useChatStream() { const chatHistory ref([]); const isLoading ref(false); const errorMsg ref(); let abortController null; // 支持 POST 的 SSE 客户端 async function connectSSE(url, body, onMessage, onError) { abortController new AbortController(); try { const response await fetch(url, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(body), signal: abortController.signal, }); if (!response.ok) { throw new Error(HTTP ${response.status}); } const reader response.body.getReader(); const decoder new TextDecoder(utf-8); let buffer ; while (true) { const { done, value } await reader.read(); if (done) break; buffer decoder.decode(value, { stream: true }); const lines buffer.split(\n); buffer lines.pop() || ; lines.forEach((line) { if (!line.trim().startsWith(data: )) return; const payload line.slice(6).trim(); if (!payload) return; try { onMessage(JSON.parse(payload)); } catch (e) { // 忽略解析失败 } }); } } catch (err) { if (err.name ! AbortError) { onError(err); } } } async function sendMessage(content, modelType) { if (!content.trim() || isLoading.value) return; errorMsg.value ; chatHistory.value.push({ role: user, content }); const aiIndex chatHistory.value.length; chatHistory.value.push({ role: assistant, content: , reasoning: , model: modelType, }); isLoading.value true; const messages chatHistory.value .filter((m) m.role user || m.content) .map((m) ({ role: m.role user ? user : assistant, content: m.content, })); await connectSSE( /api/chat/${modelType}, { messages, maxTokens: 2000 }, (data) { if (data.code ! 0) { errorMsg.value data.msg || 请求失败; isLoading.value false; return; } if (data.content) { chatHistory.value[aiIndex].content data.content; } if (data.reasoning) { chatHistory.value[aiIndex].reasoning data.reasoning; } if (data.status done || data.status error) { isLoading.value false; } }, (err) { errorMsg.value err.message; isLoading.value false; } ); } function stopStream() { if (abortController) { abortController.abort(); isLoading.value false; } } onUnmounted(() { stopStream(); }); return { chatHistory, isLoading, errorMsg, sendMessage, stopStream }; }这个组合式函数把 SSE 的细节全藏起来了。组件里只需要const { chatHistory, sendMessage } useChatStream()然后调sendMessage(你好, quick)就行。深度模型的reasoning和content分开存前端可以折叠展示思考过程。3.3 前端组件调用template div classchat-page div classchat-history div v-for(msg, idx) in chatHistory :keyidx classchat-item div classrole{{ msg.role user ? 我 : msg.model }}/div details v-ifmsg.reasoning classreasoning summary思考过程/summary pre{{ msg.reasoning }}/pre /details div classcontent{{ msg.content }}/div /div /div div classinput-area textarea v-modelinput keydown.enter.preventhandleSend / div classmodel-switch labelinput typeradio v-modelmodel valuequick / 快速/label labelinput typeradio v-modelmodel valuedeep / 深度/label /div button clickhandleSend :disabledisLoading发送/button /div /div /template script setup import { ref } from vue; import { useChatStream } from ./composables/useChatStream; const input ref(); const model ref(quick); const { chatHistory, isLoading, sendMessage } useChatStream(); function handleSend() { sendMessage(input.value, model.value); input.value ; } /script到这里Node.js 和 Vue3.5 的配置就齐了。你可以先跑node server.js再用npm run dev起前端打开页面测试。4. 验证请求与成功结果用 curl 和浏览器确认流式输出代码写完不能直接信得先验证服务端能不能正常拿到流式响应。我习惯先用curl测 Node.js 接口确认 SSE 分片正常再开前端。先启动服务端node server.js然后开另一个终端用 curl 发一个快速模型的请求curl -N -X POST http://localhost:3000/api/chat/quick \ -H Content-Type: application/json \ -d {messages:[{role:user,content:用一句话介绍杭州}],maxTokens:200}-N参数关闭 curl 的缓冲这样你能看到流式输出。正常的话终端会一行行蹦出data: {code:0,content:杭}这样的分片最后是data: {code:0,status:done}。如果一次性全出来说明服务端没走流式检查responseType: stream有没有加。再测深度模型curl -N -X POST http://localhost:3000/api/chat/deep \ -H Content-Type: application/json \ -d {messages:[{role:user,content:解方程 3x520}],maxTokens:2000}深度模型的返回里你会先看到reasoning字段有内容然后是content字段。这说明思维链和最终回答被正确分开了。如果reasoning一直是空检查modelType deep的分支有没有走到以及 TaoToken 返回的output数组结构是否和代码里解析的一致。前端验证打开浏览器输入问题点发送。快速模型应该几乎立刻开始逐字蹦字深度模型会先显示“思考过程”折叠块里面文字逐渐增多然后最终回答开始输出。如果前端一直转圈不出字打开 DevTools 的 Network 面板看/api/chat/quick这个请求的 Response 是不是text/event-stream以及有没有数据分片返回。还有一个验证点切换模型。点“深度”再发一个问题看请求是不是打到了/api/chat/deep返回的reasoning有没有被渲染到折叠块里。这一步过了说明双模型切换和流式渲染都通了。如果你用的是 TaoToken 的 Coding Plan 做长期编码辅助可以在https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite了解套餐详情。验证模型对话效果可以用https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite直接试。5. 本篇常见错误排查401、local proxy failed、reading choices、OAuth这一节列几个我实际踩过的报错以及对应的排查思路。你遇到问题时可以对照着看。401 Unauthorized最常见的原因是 API Key 没配对。检查.env里的TAOTOKEN_API_KEY是不是sk-开头有没有多余空格。Node.js 里读的是process.env.TAOTOKEN_API_KEY如果你变量名写成了TAOTOKEN_KEY读出来就是undefined请求头会变成Bearer undefined直接 401。另外确认.env文件和server.js在同一目录dotenv默认从当前工作目录加载。local proxy failed这个报错通常出现在你本地网络环境有额外代理设置时。Node.js 的 axios 会读取系统代理环境变量如果HTTP_PROXY或HTTPS_PROXY指向了一个不可用的地址请求就会失败。解决办法是在启动服务前清掉这些变量unset HTTP_PROXY HTTPS_PROXY或者在 axios 配置里显式设置proxy: false。如果你用的是公司网络确认防火墙没有拦截taotoken.net的 443 端口。reading choices类似Cannot read properties of undefined (reading choices)的报错说明代码在解析响应时data.choices是undefined。快速模型的流式分片确实在choices[0].delta.content但如果 TaoToken 返回了错误信息比如额度不足、模型 ID 写错响应体里就没有choices。排查方法在JSON.parse(payload)之后先打印data看实际返回结构。如果是{error:{message:...}}那就是模型 ID 或 Key 的问题。OAuth 相关报错如果你在配置 Claude Code 或某些 CLI 工具时看到 OAuth 失败通常是因为工具默认走了 Anthropic 官方登录流程而你要用 TaoToken 的 Key 接入。这时候需要改配置文件把 Base URL 指向 TaoTokenKey 填 TaoToken 的 Key。以 Claude Code 为例配置文件里要同时写全三件套Base URL 用https://taotoken.net/apiKey 用你的 TaoToken KeyModel ID 填文档里对应的模型。三件套缺一个都会导致认证失败。Codex 的auth.json也是类似base_url、api_key、model三个字段都要填对。流式输出断断续续或卡住检查 Node.js 的res.flushHeaders()有没有调用。Express 默认会缓冲响应头不 flush 的话前端可能等很久才收到第一个分片。另外确认没有中间件比如 compression对 SSE 响应做了压缩压缩会破坏流式效果。深度模型 reasoning 为空先确认你请求的 Model ID 确实是深度思考模型。有些模型虽然名字里有 thinking但实际返回结构可能不同。用 curl 直接打 TaoToken 的接口看原始返回里output数组的type字段是什么。如果返回的是choices而不是output说明这个模型走的是快速模型的响应格式代码里的分支判断要调整。6. 从验证到长期使用TaoToken 接入的下一步代码跑通之后你可能会想把它用到实际项目里。这时候有几个方向可以继续。如果你只是验证模型效果用https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite直接对话就行不用写代码。如果你要把这套 SSE 方案接入生产环境记得在 Node.js 层加限流和 CORS 白名单别让接口裸奔。生产环境的.env不要提交到 Git用部署平台的密钥管理功能注入。如果你长期做编码辅助比如用 Claude Code 写代码TaoToken 的 Coding Plan 可以看https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite。它的好处是一个 Key 覆盖多个模型切换模型不用改环境变量。API Keys 管理在https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite。回到这套方案本身最值得优化的点是错误重试。现在的代码遇到网络抖动会直接报错你可以加一个指数退避的重试逻辑第一次失败等 1 秒第二次等 2 秒第三次等 4 秒最多重试 3 次。重试时只重发最后一个用户消息不要把整个历史再发一遍省 token。另一个优化点是思考过程的展示。深度模型的reasoning可能很长全部展开会占满屏幕。可以用details标签默认折叠用户想看再点开。如果 reasoning 是流式输出的折叠状态下也要继续接收展开时能看到完整内容。最后聊天记录持久化。现在刷新页面记录就没了你可以加一个后端接口把 messages 存到数据库前端加载时拉取。存的时候把reasoning和content分开存这样回看历史时还能区分思考过程和最终回答。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询