Node.js 开发 AI 聊天机器人完全指南:流式对话 + 多模型切换(2026)

Node.js 开发 AI 聊天机器人完全指南:流式对话 + 多模型切换(2026)

摘要

  • Node.js + Express 从零搭建 AI 聊天机器人后端,支持 SSE 流式输出,首字响应 < 500ms
  • 通过 OpenAI 兼容协议,一套代码接入 GPT-5.4、Claude Opus 4.6、Gemini 3.1 Pro 等 50+ 模型
  • 附完整前端 UI(纯 HTML + Vanilla JS),复制即用,无需框架
  • 覆盖生产环境要点:对话历史管理、错误处理、安全防护、Docker 部署

目录

为什么选 Node.js 构建 AI 聊天机器人

2026 年,AI 聊天机器人已经从”玩具”变成了企业刚需——客服、销售助手、内部知识库问答、代码 Review 助手……场景遍地开花。构建这些应用,Node.js 是最佳选择之一:

天然适合流式场景。Node.js 的事件循环和非阻塞 I/O 模型,天生就是为”等待远程 API 响应”这种场景设计的。大模型一次生成可能耗时 10-30 秒,Node.js 在等待期间可以同时服务数百个并发请求,不会阻塞线程。

生态成熟。OpenAI 官方的 openai npm 包已经是 v5.x,原生支持流式输出、Function Calling、多模态输入。而且,由于 Claude、Gemini 等模型都提供 OpenAI 兼容接口,同一个 SDK 就能通吃所有主流模型。

全栈统一。前端 React/Vue,后端 Express/Fastify,部署用 Vercel/Railway——全程 JavaScript/TypeScript,一个人就能搞定从前端到后端到部署的全链路。

生产验证。ChatGPT 网页版、Cursor、Vercel AI SDK——大量明星 AI 产品的后端基于 Node.js / TypeScript 构建,这不是实验性技术,而是经过亿级用户验证的成熟方案。

特性Node.jsPython (FastAPI)Go
流式输出支持⭐⭐⭐ 原生异步⭐⭐ async/await⭐⭐⭐ goroutine
AI SDK 生态⭐⭐⭐ openai/vercel ai⭐⭐⭐ openai/langchain⭐⭐ 社区方案
前后端统一⭐⭐⭐ 全栈 JS⭐⭐ 需配合前端⭐ 需配合前端
部署便利性⭐⭐⭐ Vercel/Railway⭐⭐ Docker/云函数⭐⭐ Docker
学习曲线⭐⭐⭐ 前端可直接上手⭐⭐ 需学 Python⭐ 需学 Go

核心原理:流式输出如何工作

在写代码之前,先理解流式输出的技术原理——这是构建优秀 AI 聊天体验的关键。

普通请求 vs 流式请求

普通请求(非流式):

客户端 → 发送消息 → 服务器 → 调用大模型 API → 等待 10-30 秒 → 一次性返回完整回复

用户体验:点击发送后盯着空白屏幕等半天,然后”啪”一下全部出现。

流式请求(Streaming):

客户端 → 发送消息 → 服务器 → 调用大模型 API(stream: true)

                              首个 token(~300ms)→ 实时推送给客户端
                              第二个 token(~50ms)→ 实时推送
                              ...逐个 token 推送...
                              [DONE] → 结束

用户体验:几百毫秒就看到第一个字,文字像打字机一样逐字出现——这就是 ChatGPT 的体验。

SSE:流式传输的标准协议

流式输出在 Web 端的标准实现方式是 SSE(Server-Sent Events)

  • 基于 HTTP,不需要 WebSocket 那样的额外协议升级
  • 服务端设置 Content-Type: text/event-stream
  • 每条消息格式:data: {...}\n\n
  • 客户端用 EventSourcefetch + ReadableStream 接收
  • OpenAI API 原生返回 SSE 格式,可以直接透传给前端

这意味着你的 Node.js 服务器实际上是一个 SSE 代理:接收大模型的流式响应,实时转发给浏览器。

为什么不用 WebSocket?

SSE 在聊天机器人场景下比 WebSocket 更合适:

特性SSEWebSocket
方向服务器 → 客户端(单向)双向
协议普通 HTTP需要协议升级
自动重连浏览器原生支持需要自己实现
负载均衡标准 HTTP,无特殊配置需要 sticky session
适用场景AI 聊天(请求-响应模式)实时协作、游戏

聊天机器人本质上还是”用户提问 → AI 回答”的请求-响应模式,SSE 完全够用,而且运维复杂度低很多。

环境准备与项目搭建

前置条件

  • Node.js 20+(推荐 22 LTS)
  • npm 10+ 或 pnpm
  • 一个 AI API Key(推荐从 Ofox.ai 获取,一个 Key 即可调用 GPT、Claude、Gemini 等 50+ 模型,注册送免费额度)

初始化项目

mkdir ai-chatbot && cd ai-chatbot
npm init -y
npm install express openai dotenv cors

包说明:

用途
expressWeb 框架,处理 HTTP 请求
openaiOpenAI 官方 SDK(也兼容其他模型)
dotenv从 .env 文件加载环境变量
cors跨域支持(前后端分离开发用)

配置环境变量

创建 .env 文件:

# AI API 配置
OPENAI_API_KEY=sk-your-api-key-here
OPENAI_BASE_URL=https://api.ofox.ai/v1

# 服务器配置
PORT=3000

关键点OPENAI_BASE_URL 指向 Ofox.ai 的 API 地址。OpenAI SDK 会自动读取这个环境变量,无需在代码中硬编码。这样同一套代码就能通过 Ofox.ai 调用 GPT、Claude、Gemini 等任意模型。

项目结构

ai-chatbot/
├── server.js          # Express 服务器(后端)
├── public/
│   └── index.html     # 聊天 UI(前端)
├── .env               # 环境变量(不要提交到 Git!)
└── package.json

第一个聊天机器人:基础问答

先从最简单的非流式版本开始,确保 API 连通:

// server.js
import 'dotenv/config';
import express from 'express';
import cors from 'cors';
import OpenAI from 'openai';

const app = express();
app.use(cors());
app.use(express.json());
app.use(express.static('public'));

// 初始化 OpenAI 客户端
// SDK 自动读取 OPENAI_API_KEY 和 OPENAI_BASE_URL 环境变量
const openai = new OpenAI();

// 基础聊天接口(非流式)
app.post('/api/chat', async (req, res) => {
  try {
    const { messages, model = 'gpt-4o' } = req.body;

    const completion = await openai.chat.completions.create({
      model,
      messages: [
        { role: 'system', content: '你是一个友好的 AI 助手。' },
        ...messages,
      ],
    });

    res.json({
      content: completion.choices[0].message.content,
      model: completion.model,
      usage: completion.usage,
    });
  } catch (error) {
    console.error('Chat error:', error.message);
    res.status(500).json({ error: error.message });
  }
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running at http://localhost:${PORT}`);
});

注意:在 package.json 中添加 "type": "module" 以支持 ES Module 的 import 语法。

测试一下:

node server.js

# 另一个终端
curl -X POST http://localhost:3000/api/chat \
  -H "Content-Type: application/json" \
  -d '{"messages": [{"role": "user", "content": "你好,介绍一下你自己"}]}'

如果返回了 JSON 格式的 AI 回复,说明 API 连接成功。接下来加入流式输出。

升级:实现 SSE 流式输出

这是整篇文章的核心——把”等 10 秒出结果”变成”实时逐字输出”:

// 流式聊天接口(SSE)
app.post('/api/chat/stream', async (req, res) => {
  const { messages, model = 'gpt-4o' } = req.body;

  // 设置 SSE 响应头
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  res.setHeader('X-Accel-Buffering', 'no'); // Nginx 反代时禁用缓冲

  try {
    const stream = await openai.chat.completions.create({
      model,
      messages: [
        { role: 'system', content: '你是一个友好的 AI 助手。' },
        ...messages,
      ],
      stream: true, // 开启流式输出
    });

    // 逐 chunk 转发给客户端
    for await (const chunk of stream) {
      const content = chunk.choices[0]?.delta?.content;
      if (content) {
        res.write(`data: ${JSON.stringify({ content })}\n\n`);
      }
    }

    // 发送结束标记
    res.write('data: [DONE]\n\n');
    res.end();
  } catch (error) {
    console.error('Stream error:', error.message);
    res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
    res.end();
  }

  // 客户端断开时清理
  req.on('close', () => {
    res.end();
  });
});

代码解析

  1. SSE 响应头Content-Type: text/event-stream 告诉浏览器这是一个事件流,X-Accel-Buffering: no 防止 Nginx 缓冲导致内容不能实时推送

  2. stream: true:告诉 OpenAI SDK 开启流式模式,返回一个 async iterator

  3. for await...of:逐个消费流式 chunk,每个 chunk 包含一小段文本(通常 1-3 个 token)

  4. data: ...\n\n:SSE 协议格式,每条消息以两个换行符结尾

  5. [DONE] 标记:遵循 OpenAI 的流式结束约定,前端据此判断生成完毕

  6. req.on('close'):用户关闭页面或网络断开时,清理连接,避免内存泄漏

测试流式输出

curl -N -X POST http://localhost:3000/api/chat/stream \
  -H "Content-Type: application/json" \
  -d '{"messages": [{"role": "user", "content": "用 100 字介绍 Node.js"}]}'

你会看到回复像打字机一样逐行出现,而不是等好几秒才一次性输出。

多模型切换:一套代码调 GPT / Claude / Gemini

通过 Ofox.ai 的 OpenAI 兼容接口,切换模型只需要改一个 model 参数:

// 支持的模型列表
const MODELS = {
  'gpt-4o':              { name: 'GPT-4o',             provider: 'OpenAI' },
  'gpt-5.4':             { name: 'GPT-5.4',            provider: 'OpenAI' },
  'claude-opus-4-6':     { name: 'Claude Opus 4.6',    provider: 'Anthropic' },
  'claude-sonnet-4-6':   { name: 'Claude Sonnet 4.6',  provider: 'Anthropic' },
  'gemini-3.1-pro':      { name: 'Gemini 3.1 Pro',     provider: 'Google' },
  'deepseek-v4':         { name: 'DeepSeek V4',        provider: 'DeepSeek' },
  'qwen-max':            { name: 'Qwen Max',           provider: 'Alibaba' },
};

// 获取可用模型列表
app.get('/api/models', (req, res) => {
  res.json(MODELS);
});

// 流式聊天接口(支持模型切换)
app.post('/api/chat/stream', async (req, res) => {
  const { messages, model = 'gpt-4o' } = req.body;

  if (!MODELS[model]) {
    return res.status(400).json({ error: `不支持的模型: ${model}` });
  }

  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  res.setHeader('X-Accel-Buffering', 'no');

  try {
    const stream = await openai.chat.completions.create({
      model, // 只需切换这个参数!
      messages: [
        { role: 'system', content: '你是一个友好的 AI 助手。' },
        ...messages,
      ],
      stream: true,
    });

    for await (const chunk of stream) {
      const content = chunk.choices[0]?.delta?.content;
      if (content) {
        res.write(`data: ${JSON.stringify({ content })}\n\n`);
      }
    }

    res.write('data: [DONE]\n\n');
    res.end();
  } catch (error) {
    res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
    res.end();
  }

  req.on('close', () => res.end());
});

核心要点:整个后端代码没有任何模型特定的逻辑。GPT、Claude、Gemini 全部通过 OpenAI SDK 的 chat.completions.create() 调用,区别仅在 model 字段。这就是 OpenAI 兼容协议的威力——一次对接,随意切换。

为什么要支持多模型?

场景推荐模型原因
日常对话GPT-4o / Claude Sonnet 4.6速度快、成本低
复杂推理Claude Opus 4.6 / GPT-5.4推理能力强
长文处理Gemini 3.1 Pro(2M 上下文)超长上下文
代码生成Claude Opus 4.6代码质量最高
成本敏感DeepSeek V4 / Qwen性价比高

让用户根据场景选择模型,而不是锁死在某一个模型上——这才是 2026 年 AI 应用的正确架构。

构建前端聊天 UI

创建 public/index.html,一个纯 HTML + CSS + JS 的聊天界面,无需任何框架:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>AI 聊天助手</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      background: #f5f5f5;
      height: 100vh;
      display: flex;
      flex-direction: column;
    }
    .header {
      background: #fff;
      padding: 16px 24px;
      border-bottom: 1px solid #e0e0e0;
      display: flex;
      align-items: center;
      gap: 12px;
    }
    .header h1 { font-size: 18px; color: #333; }
    .header select {
      margin-left: auto;
      padding: 6px 12px;
      border: 1px solid #ddd;
      border-radius: 8px;
      font-size: 14px;
    }
    .messages {
      flex: 1;
      overflow-y: auto;
      padding: 24px;
      display: flex;
      flex-direction: column;
      gap: 16px;
    }
    .message {
      max-width: 720px;
      padding: 12px 16px;
      border-radius: 12px;
      line-height: 1.6;
      white-space: pre-wrap;
      word-break: break-word;
    }
    .message.user {
      align-self: flex-end;
      background: #007AFF;
      color: #fff;
    }
    .message.assistant {
      align-self: flex-start;
      background: #fff;
      color: #333;
      box-shadow: 0 1px 3px rgba(0,0,0,0.1);
    }
    .input-area {
      background: #fff;
      padding: 16px 24px;
      border-top: 1px solid #e0e0e0;
      display: flex;
      gap: 12px;
    }
    .input-area textarea {
      flex: 1;
      padding: 10px 14px;
      border: 1px solid #ddd;
      border-radius: 12px;
      font-size: 15px;
      resize: none;
      height: 44px;
      font-family: inherit;
    }
    .input-area button {
      padding: 10px 20px;
      background: #007AFF;
      color: #fff;
      border: none;
      border-radius: 12px;
      font-size: 15px;
      cursor: pointer;
    }
    .input-area button:disabled {
      background: #ccc;
      cursor: not-allowed;
    }
  </style>
</head>
<body>
  <div class="header">
    <h1>AI 聊天助手</h1>
    <select id="modelSelect">
      <option value="gpt-4o">GPT-4o</option>
      <option value="claude-sonnet-4-6">Claude Sonnet 4.6</option>
      <option value="claude-opus-4-6">Claude Opus 4.6</option>
      <option value="gemini-3.1-pro">Gemini 3.1 Pro</option>
    </select>
  </div>

  <div class="messages" id="messages"></div>

  <div class="input-area">
    <textarea id="input" placeholder="输入消息..."
      onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendMessage()}">
    </textarea>
    <button id="sendBtn" onclick="sendMessage()">发送</button>
  </div>

  <script>
    const messagesDiv = document.getElementById('messages');
    const input = document.getElementById('input');
    const sendBtn = document.getElementById('sendBtn');
    const modelSelect = document.getElementById('modelSelect');

    // 对话历史(发给 API 用)
    let chatHistory = [];

    function addMessage(role, content) {
      const div = document.createElement('div');
      div.className = `message ${role}`;
      div.textContent = content;
      messagesDiv.appendChild(div);
      messagesDiv.scrollTop = messagesDiv.scrollHeight;
      return div;
    }

    async function sendMessage() {
      const text = input.value.trim();
      if (!text) return;

      // 显示用户消息
      addMessage('user', text);
      chatHistory.push({ role: 'user', content: text });
      input.value = '';
      sendBtn.disabled = true;

      // 创建 AI 回复占位
      const aiDiv = addMessage('assistant', '');
      let fullContent = '';

      try {
        // 发起流式请求
        const response = await fetch('/api/chat/stream', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            messages: chatHistory,
            model: modelSelect.value,
          }),
        });

        // 读取 SSE 流
        const reader = response.body.getReader();
        const decoder = new TextDecoder();

        while (true) {
          const { done, value } = await reader.read();
          if (done) break;

          const text = decoder.decode(value, { stream: true });
          const lines = text.split('\n');

          for (const line of lines) {
            if (!line.startsWith('data: ')) continue;
            const data = line.slice(6);

            if (data === '[DONE]') break;

            try {
              const parsed = JSON.parse(data);
              if (parsed.content) {
                fullContent += parsed.content;
                aiDiv.textContent = fullContent;
                messagesDiv.scrollTop = messagesDiv.scrollHeight;
              }
              if (parsed.error) {
                aiDiv.textContent = `错误: ${parsed.error}`;
              }
            } catch {}
          }
        }

        // 保存 AI 回复到历史
        chatHistory.push({ role: 'assistant', content: fullContent });
      } catch (error) {
        aiDiv.textContent = `请求失败: ${error.message}`;
      }

      sendBtn.disabled = false;
      input.focus();
    }
  </script>
</body>
</html>

代码要点

  1. fetch + ReadableStream:用 response.body.getReader() 逐块读取 SSE 流,比 EventSource 更灵活(EventSource 只支持 GET 请求,我们需要 POST)

  2. 增量更新 DOM:每收到一个 chunk 就追加到 aiDiv.textContent,实现逐字输出效果

  3. 对话历史管理chatHistory 数组保存完整对话,每次请求都发送给 API,这样模型能理解上下文

  4. 模型切换:下拉框选择模型,modelSelect.value 直接传给后端

启动服务器,打开 http://localhost:3000,你就有了一个支持流式输出、多模型切换的 AI 聊天机器人。

生产环境最佳实践

从 Demo 到生产,还需要处理这些关键问题:

1. 对话历史管理

Demo 中对话历史存在浏览器内存里,刷新就丢了。生产环境推荐用 Redis:

import { createClient } from 'redis';

const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();

// 保存对话历史
async function saveHistory(sessionId, messages) {
  await redis.setEx(
    `chat:${sessionId}`,
    3600 * 24, // 24 小时过期
    JSON.stringify(messages)
  );
}

// 读取对话历史
async function getHistory(sessionId) {
  const data = await redis.get(`chat:${sessionId}`);
  return data ? JSON.parse(data) : [];
}

2. 速率限制

防止单个用户疯狂调用,耗光你的 API 额度:

import rateLimit from 'express-rate-limit';

const chatLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 分钟窗口
  max: 20,             // 每分钟最多 20 次
  message: { error: '请求太频繁,请稍后再试' },
});

app.post('/api/chat/stream', chatLimiter, async (req, res) => {
  // ...
});

3. 输入校验

永远不要信任用户输入:

app.post('/api/chat/stream', async (req, res) => {
  const { messages, model } = req.body;

  // 校验 messages 格式
  if (!Array.isArray(messages) || messages.length === 0) {
    return res.status(400).json({ error: 'messages 必须是非空数组' });
  }

  // 限制消息数量(防止超长上下文导致高额费用)
  if (messages.length > 50) {
    return res.status(400).json({ error: '对话轮数超过限制' });
  }

  // 限制单条消息长度
  for (const msg of messages) {
    if (typeof msg.content !== 'string' || msg.content.length > 10000) {
      return res.status(400).json({ error: '消息格式无效或超长' });
    }
  }

  // ... 继续处理
});

4. 错误重试

API 调用偶尔会失败,加一个简单的重试机制:

async function createStreamWithRetry(params, maxRetries = 2) {
  for (let i = 0; i <= maxRetries; i++) {
    try {
      return await openai.chat.completions.create(params);
    } catch (error) {
      if (i === maxRetries) throw error;

      // 429 限流或 500+ 服务器错误时重试
      if (error.status === 429 || error.status >= 500) {
        await new Promise(r => setTimeout(r, 1000 * (i + 1)));
        continue;
      }

      throw error; // 其他错误不重试
    }
  }
}

5. Docker 部署

FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
docker build -t ai-chatbot .
docker run -d -p 3000:3000 \
  -e OPENAI_API_KEY=sk-xxx \
  -e OPENAI_BASE_URL=https://api.ofox.ai/v1 \
  ai-chatbot

6. 安全清单

检查项说明
API Key 不硬编码使用环境变量或密钥管理服务
HTTPS生产环境必须启用
CORS 白名单只允许你的域名,不要用 *
速率限制按 IP / 用户限流
输入长度限制防止超长文本导致高额 API 费用
日志脱敏不要把用户输入完整记录到日志

常见报错与排查

401 Unauthorized

原因:API Key 无效或未配置。

# 检查环境变量是否生效
node -e "console.log(process.env.OPENAI_API_KEY?.slice(0,10) + '...')"

确认 .env 文件中的 Key 正确,且 dotenv/config 在代码最前面导入。

429 Too Many Requests

原因:超过 API 速率限制。

解决

  1. 加入重试逻辑(见上文)
  2. 降低并发数
  3. 如果使用 Ofox.ai,升级套餐获取更高配额

ECONNREFUSED / ETIMEDOUT

原因:无法连接到 API 服务器。

解决

  1. 检查 OPENAI_BASE_URL 是否正确
  2. 检查网络连接
  3. 如果在国内直连 OpenAI/Anthropic 官方 API,建议切换到 Ofox.ai 等国内加速平台

流式输出卡在第一个字

原因:Nginx 或 CDN 缓冲了 SSE 响应。

解决

# Nginx 配置
location /api/ {
    proxy_pass http://localhost:3000;
    proxy_buffering off;          # 关键!
    proxy_cache off;
    proxy_set_header Connection '';
    chunked_transfer_encoding off;
}

中文乱码

原因:响应缺少字符编码声明。

解决

res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');

总结与下一步

本文从零构建了一个完整的 AI 聊天机器人:

  • 后端:Node.js + Express,15 行核心代码实现 SSE 流式输出
  • 多模型:通过 OpenAI 兼容协议 + Ofox.ai,一套代码调用 GPT / Claude / Gemini 等 50+ 模型
  • 前端:纯 HTML + JS 聊天 UI,支持流式渲染和模型切换
  • 生产就绪:对话管理、速率限制、错误重试、Docker 部署

进一步扩展方向

方向技术方案
支持图片输入使用多模态模型(GPT-4o Vision / Gemini),传 base64 图片
添加知识库RAG 架构:Embedding + 向量数据库 + 检索增强
Function Calling让机器人查天气、订机票、查数据库
多轮记忆优化上下文压缩、摘要滚动窗口
语音对话Whisper STT + TTS API

快速开始

  1. Ofox.ai 注册,获取 API Key(注册送免费额度)
  2. 复制本文代码,配置 .env
  3. node server.js,打开 http://localhost:3000
  4. 开始和 AI 聊天

完整代码已整理到一个文件中,可以直接运行。有问题可以在下方评论区交流。

参考资料