# Node.js+Vue3.5 实战:豆包快速 / 深度思考模型的流式调用方案

Node.js+Vue3.5 实战:豆包快速 / 深度思考模型的流式调用方案

开篇:为什么要做这个封装?

前阵子做 AI 对话功能时,直接调用豆包 API 遇到了些小麻烦 ------ 比如快速模型和深度模型的参数要区分;想做前端逐字渲染,得自己处理 SSE 流式数据。

索性花了一天把这些逻辑整理成可复用的方案,后端用 Node.js 统一处理双模型的差异,前端 Vue3.5 做简洁的交互界面。现在不管是基础问答还是复杂解题,切换模型点个按钮就行,流式渲染的打字效果还挺直观。这篇就把整个思路拆解开,从参数哪里找、代码怎么写,到遇到的小问题,都跟大家聊一聊。

一、先搞定核心配置:这些参数从哪来?

要调用豆包模型,首先得拿到「钥匙」和「地址」,这些都从火山引擎平台获取,关键配置整理如下:

配置项 示例值 从哪找?
快速思考模型 ID doubao-seed-1-6-flash-250615 火山引擎控制台 → 豆包开发者平台 → 模型管理 → 复制对应模型 ID www.volcengine.com/docs/82379/...
深度思考模型 ID doubao-seed-1-6-thinking-250715 同上
API 密钥(MODEL_API_KEY) sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 密钥管理 → 新建 / 复制 Access Key(console.volcengine.com/ark/region:ark+cn-beijing/apiKey
基础地址(BASE_URL) ark.cn-beijing.volces.com/api/v3 www.volcengine.com/docs/82379/1494384#RxN8G2nH
对话接口路径 /chat/completions 同上

重要提醒 :API 密钥千万别硬写在代码里!建议建.env文件存储,用dotenv加载,既安全又方便切换环境:

ini 复制代码
\# .env文件(需加入.gitignore)

QUICK\_MODEL\_ID='doubao-seed-1-6-flash-250615'

DEEP\_MODEL\_ID='doubao-seed-1-6-thinking-250715'

MODEL\_API\_KEY='你的密钥'

BASE\_URL='https://ark.cn-beijing.volces.com/api/v3'

CHAT\_ENDPOINT='/chat/completions'

快速思考(基础对话)模型与深度思考模型请求 / 响应参数差异表

对比维度 快速思考(基础对话)模型 深度思考模型
核心请求参数 包含modelmessagestemperaturetop_pmax_tokensstream 基础参数 + 3 个特殊参数:- thinking:控制思考模式(enabled/disabled/auto)- max_output_tokens:控制输出总长度(含思维链)- instructions:替换系统提示词
messages参数 仅需包含对话角色(system/user/assistant)与基础内容,无需思维过程记录 可额外添加模型思考过程的历史记录(如 assistant 角色的 "我需要先回忆公式再计算..."),辅助复杂推理
模型 ID 示例 doubao-seed-1-6-250615 doubao-seed-1-6-thinking-250715
响应结构 核心字段:idobjectcreatedchoices(含messagefinish_reason)、usage 核心字段:output数组,含两类数据:- type: reasoning:思维过程总结- type: message:最终回答内容
响应内容组成 仅返回最终回答(choices[0].message.content 同时返回思维链(推理过程)和最终回答,明确展示模型思考逻辑
Token 控制 仅通过max_tokens控制输出长度,不区分 "回答" 与 "思维链" 通过max_output_tokens控制 "思维链 + 回答" 总长度,支持更大取值(如最大 65536)

二、后端封装:用 Node.js 搞定双模型差异

后端选用 Express 框架,核心思路:将双模型差异隐藏在内部,给前端暴露统一调用接口(如/api/chat/quick对应快速模型,/api/chat/deep对应深度模型),无需前端关心参数拼接。

1. 第一步:搭基础架子(配置 + 工具类)

config.js统一管理配置和通用方法(参数验证、流式数据解析等),后续修改更便捷:

javascript 复制代码
// config.js
require('dotenv').config(); // 加载.env文件

// 环境配置
const CONFIG = {
  models: {
    quick: process.env.QUICK_MODEL_ID,
    deep: process.env.DEEP_MODEL_ID
  },
  api: {
    baseUrl: process.env.BASE_URL,
    chatPath: process.env.CHAT_ENDPOINT,
    key: process.env.MODEL_API_KEY
  }
};

// 工具类:封装通用逻辑
class ApiTool {
  // 验证必填参数(如messages不可为空)
  static checkParams(params, required) {
    const missing = required.filter(key => !params[key]);
    if (missing.length) throw new Error(`缺参数:${missing.join(', ')}`);
  }

  // 解析流式数据:区分双模型返回格式
  static parseStreamChunk(chunk, buffer, modelType) {
    buffer += chunk.toString();
    const lines = buffer.split('\n');
    buffer = lines.pop() || ''; // 保存不完整行,下次继续解析
    const contentList = [];
    lines.forEach(line => {
      if (line.trim().startsWith('data: ')) {
        try {
          const data = JSON.parse(line.slice(6));
          let content = '';
          // 深度模型从output取,快速模型从choices取
          if (modelType === 'deep') {
            content = data.output?.[0]?.content?.[0]?.text || '';
          } else {
            content = data.choices?.[0]?.delta?.content || '';
          }
          if (content) contentList.push(content);
        } catch (e) {
          console.log('解析流式数据错误:', e.message); // 单条错误不影响整体
        }
      }
    });
    return { contentList, buffer };
  }

  // 发送SSE数据:给前端统一格式
  static sendSSE(res, data) {
    res.write(`data: ${JSON.stringify({ code: 0, data })}\n\n`);
  }
}

module.exports = { CONFIG, ApiTool };

2. 第二步:写核心处理器(双模型逻辑)

核心解决两个问题:按模型类型动态拼参数、处理流式响应并转发给前端,用modelType区分双模型逻辑:

javascript 复制代码
// chatHandler.js
const axios = require('axios');
const { CONFIG, ApiTool } = require('./config');

// 快速模型接口
exports.quickChat = (req, res) => handleChat(req, res, 'quick');

// 深度模型接口
exports.deepChat = (req, res) => handleChat(req, res, 'deep');

// 统一处理双模型的核心方法
async function handleChat(req, res, modelType) {
  try {
    const { messages, temperature = 0.7, maxTokens = 1000 } = req.body;
    // 1. 验证参数:messages不可为空
    ApiTool.checkParams({ messages }, ['messages']);
    // 2. 配置SSE响应头:声明流式数据,禁止缓存
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');
    // 3. 动态拼接请求参数:处理双模型差异
    const requestData = {
      model: CONFIG.models[modelType], // 自动选择模型ID
      messages,
      stream: true, // 开启流式响应(必传)
      temperature, // 随机性(0-2)
      // 深度模型额外参数,快速模型用max_tokens
      ...(modelType === 'deep'
        ? {
            thinking: { type: 'auto' }, // 自动判断是否需要思考
            max_output_tokens: maxTokens // 含思维链的总长度
          }
        : { max_tokens: maxTokens }) // 快速模型仅限制输出长度
    };
    // 4. 调用豆包API,获取流式响应
    const response = await axios.post(
      `${CONFIG.api.baseUrl}${CONFIG.api.chatPath}`,
      requestData,
      {
        headers: {
          Authorization: `Bearer ${CONFIG.api.key}`,
          'Content-Type': 'application/json'
        },
        responseType: 'stream' // 声明流式响应(关键)
      }
    );
    // 5. 处理流式数据,转发给前端
    let buffer = '';
    response.data.on('data', (chunk) => {
      const { contentList, buffer: newBuffer } = ApiTool.parseStreamChunk(
        chunk, buffer, modelType
      );
      buffer = newBuffer;
      contentList.forEach(content => ApiTool.sendSSE(res, { content }));
    });
    // 6. 响应结束,通知前端
    response.data.on('end', () => {
      ApiTool.sendSSE(res, { status: 'done' });
      res.end();
    });
  } catch (err) {
    // 错误处理:统一格式返回
    ApiTool.sendSSE(res, {
      status: 'error',
      msg: err.response?.data?.msg || err.message || '服务器异常'
    });
    res.end();
  }
}

三、前端实现:Vue3.5 做流式交互

前端核心需求:切换模型、逐字渲染。注意:原生EventSource仅支持 GET 请求,需自定义支持 POST 的 SSE 客户端。

1. 组件结构:简洁直观

模板分「聊天记录区」和「输入区」,模型切换用单选按钮:

ini 复制代码
<template>
  <div class="chat-page">
    <div class="chat-history">
      <div
        v-for="(msg, idx) in chatHistory"
        :key="idx"
        :class="['chat-item', msg.role === 'user' ? 'user-item' : 'ai-item']"
      >
        <div class="chat-role">{{ msg.role === 'user' ? '我' : msg.model }}</div>
        <div class="chat-content">{{ msg.content }}</div>
      </div>
    </div>
    <div class="chat-input-area">
      <textarea
        v-model="inputContent"
        placeholder="输入问题,比如'解方程式3x+5=20'..."
        @keydown.enter.prevent="sendMessage"
        :disabled="isLoading"
      ></textarea>
      <div class="model-switch">
        <label class="switch-item">
          <input
            type="radio"
            v-model="selectedModel"
            value="quick"
            checked
          >
          快速对话(快)
        </label>
        <label class="switch-item">
          <input
            type="radio"
            v-model="selectedModel"
            value="deep"
          >
          深度思考(细)
        </label>
      </div>
      <button
        class="send-btn"
        @click="sendMessage"
        :disabled="isLoading || !inputContent.trim()"
      >
        {{ isLoading ? '发送中...' : '发送' }}
      </button>
    </div>
  </div>
</template>

2. 核心逻辑:SSE 连接 + 逐字渲染

脚本重点处理 SSE 连接和逐字渲染,自定义 SSE 客户端内置组件:

ini 复制代码
<script setup>
import { ref, onUnmounted } from 'vue';

// 状态管理
const inputContent = ref(''); // 输入框内容
const chatHistory = ref([]); // 聊天记录
const selectedModel = ref('quick'); // 当前选中模型
const isLoading = ref(false); // 发送状态
let sseInstance = null; // SSE连接实例

// 发送消息
const sendMessage = async () => {
  const content = inputContent.value.trim();
  if (!content || isLoading.value) return;
  // 1. 添加用户消息到历史
  chatHistory.value.push({ role: 'user', content, model: '' });
  // 2. 添加AI回复占位符(后续逐字填充)
  const aiMsgIdx = chatHistory.value.length;
  chatHistory.value.push({
    role: 'ai',
    model: selectedModel.value === 'quick' ? '快速模型' : '深度模型',
    content: ''
  });
  // 3. 重置输入与状态
  inputContent.value = '';
  isLoading.value = true;
  try {
    // 4. 准备请求参数(聊天历史)
    const requestData = {
      messages: chatHistory.value.map(item => ({
        role: item.role === 'user' ? 'user' : 'assistant',
        content: item.content
      })),
      maxTokens: 2000 // 最大输出长度
    };
    // 5. 关闭旧连接(避免多开)
    if (sseInstance) sseInstance.close();
    // 6. 建立SSE连接(调用自定义客户端)
    sseInstance = new CustomEventSource(
      `/api/chat/${selectedModel.value}`,
      { method: 'POST', body: JSON.stringify(requestData) }
    );
    // 7. 监听SSE消息:逐字渲染
    sseInstance.addEventListener('message', (event) => {
      const { code, data } = JSON.parse(event.data);
      if (code !== 0) return;
      // 填充AI回复
      if (data.content) {
        chatHistory.value[aiMsgIdx].content += data.content;
      }
      // 响应结束:关闭状态与连接
      if (data.status === 'done' || data.status === 'error') {
        isLoading.value = false;
        sseInstance.close();
      }
      // 错误提示
      if (data.status === 'error') {
        chatHistory.value[aiMsgIdx].content += `\n\n❌ ${data.msg}`;
      }
    });
    // 8. 监听SSE错误
    sseInstance.addEventListener('error', () => {
      isLoading.value = false;
      chatHistory.value[aiMsgIdx].content += '\n\n❌ 连接失败,请重试';
      sseInstance.close();
    });
  } catch (err) {
    isLoading.value = false;
    chatHistory.value[aiMsgIdx].content += `\n\n❌ 发送失败:${err.message}`;
  }
};

// 组件卸载:关闭SSE连接(避免内存泄漏)
onUnmounted(() => {
  if (sseInstance) sseInstance.close();
});

// 自定义SSE客户端:支持POST请求
class CustomEventSource {
  constructor(url, options = {}) {
    this.url = url;
    this.options = options;
    this.listeners = {};
    this.abortCtrl = new AbortController();
    this.connect();
  }

  // 建立连接
  async connect() {
    try {
      const response = await fetch(this.url, {
        method: this.options.method || 'GET',
        headers: { 'Content-Type': 'application/json', ...this.options.headers },
        body: this.options.body,
        signal: this.abortCtrl.signal,
        keepalive: true
      });
      if (!response.ok) throw new Error(`HTTP错误:${response.status}`);
      // 处理流式响应
      const reader = response.body.getReader();
      const decoder = new TextDecoder('utf-8');
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        // 解析SSE格式,触发message事件
        const chunk = decoder.decode(value, { stream: true });
        chunk.split('\n').forEach(line => {
          if (line.trim().startsWith('data: ')) {
            this.dispatchEvent('message', { data: line.slice(6) });
          }
        });
      }
      // 连接关闭:触发close事件
      this.dispatchEvent('close', {});
    } catch (err) {
      this.dispatchEvent('error', err);
    }
  }

  // 添加事件监听
  addEventListener(type, callback) {
    if (!this.listeners[type]) this.listeners[type] = [];
    this.listeners[type].push(callback);
  }

  // 触发事件
  dispatchEvent(type, data) {
    (this.listeners[type] || []).forEach(cb => cb(data));
  }

  // 关闭连接
  close() {
    this.abortCtrl.abort();
    this.dispatchEvent('close', {});
  }
}
</script>

3. 样式:简洁舒适(Tailwind CSS)

重点区分用户 / AI 消息,优化加载状态提示:

xml 复制代码
<style scoped>
.chat-page {
  max-width: 800px;
  margin: 20px auto;
  padding: 0 15px;
  font-family: 'Inter', sans-serif;
}

.chat-history {
  min-height: 500px;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  padding: 16px;
  margin-bottom: 16px;
  overflow-y: auto;
  background-color: #f9fafb;
}

.chat-item {
  margin-bottom: 12px;
  padding: 12px;
  border-radius: 8px;
  max-width: 80%;
}

.user-item {
  margin-left: auto;
  background-color: #3b82f6;
  color: white;
}

.ai-item {
  margin-right: auto;
  background-color: white;
  border: 1px solid #e5e7eb;
}

.chat-role {
  font-size: 14px;
  font-weight: 600;
  margin-bottom: 4px;
}

.chat-content {
  font-size: 15px;
  line-height: 1.6;
  white-space: pre-wrap; /* 保留换行 */
}

.chat-input-area {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

textarea {
  height: 100px;
  padding: 12px;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  resize: vertical;
  font-size: 15px;
}

.model-switch {
  display: flex;
  gap: 20px;
  padding: 4px 0;
  font-size: 14px;
  color: #4b5563;
}

.switch-item {
  display: flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
}

.send-btn {
  align-self: flex-end;
  padding: 10px 24px;
  background-color: #3b82f6;
  color: white;
  border: none;
  border-radius: 8px;
  font-size: 15px;
  cursor: pointer;
  transition: background-color 0.2s;
}

.send-btn:disabled {
  background-color: #94a3b8;
  cursor: not-allowed;
}
</style>

四、可以优化的点

  1. 聊天记录持久化:当前刷新页面记录丢失,可加后端接口存数据库,前端加载时拉取;

  2. 参数可配置 :目前temperaturemaxTokens固定,可加前端设置面板让用户自定义;

  3. 加载状态优化:在 AI 回复末尾加 "..." 打字动画,提升交互体验;

  4. 生产环境配置:限制 CORS 来源(避免全跨域),加接口限流防止被刷。

五、最后

这个方案从开发到调试约 2 天,封装后后续项目调用豆包双模型,只需拷贝后端代码、修改.env配置、引入前端组件即可使用。

如果您也需要做豆包模型的流式调用,希望这篇能帮您少走弯路。若有更好的实现方式,欢迎在评论区交流~

相关推荐
k01k012 小时前
ROS通信机制(一)
人工智能·机器人
RoyLin2 小时前
C++ 基础与核心概念
前端·后端·node.js
记得坚持2 小时前
vue2插槽
前端·vue.js
带只拖鞋去流浪2 小时前
Vue.js响应式API
前端·javascript·vue.js
前端小灰狼2 小时前
Ant Design Vue Vue3 table 表头筛选重置不清空Bug
前端·javascript·vue.js·bug
NiceAIGC2 小时前
万亿参数!阿里 Qwen3-Max 大模型正式发布!
人工智能
Copper peas2 小时前
Vue 中的 v-model 指令详解
前端·javascript·vue.js
别惹CC2 小时前
Spring AI 进阶之路03:集成RAG构建高效知识库
java·人工智能·spring
GHOME2 小时前
vue3中setup语法糖和setup函数的区别?
前端·vue.js·面试