NestJS 构建 AI 流式聊天服务:从 0 到 1 实现实时对话功能

在 AI 应用开发中,流式响应已经成为提升用户体验的关键特性。本文将带你从零开始,使用 NestJS 框架构建一个支持实时流式输出的 AI 聊天服务,集成阿里云百炼大模型,并实现完整的停止功能。

🚀 项目概述

这是一个基于 NestJS 构建的 AI 流式聊天服务,核心特性包括:

  • 实时流式响应: AI 回复逐字实时显示,避免用户长时间等待
  • 停止功能: 支持用户中断 AI 生成过程
  • SSE 支持: 基于 Server-Sent Events 实现高效服务器推送
  • 阿里云百炼: 集成阿里云百炼大模型 API
  • TypeScript: 完整的类型安全支持
  • 可视化测试界面: 内置前端页面方便调试

📁 项目结构

ruby 复制代码
stream-serve/
├── src/
│   ├── ai/                    # AI 模块
│   │   ├── ai.controller.ts   # AI 控制器
│   │   ├── ai.service.ts      # AI 服务
│   │   └── ai.module.ts       # AI 模块定义
│   ├── app.controller.ts      # 应用控制器
│   ├── app.service.ts         # 应用服务
│   ├── app.module.ts          # 应用模块
│   └── main.ts               # 应用入口
├── public/
│   └── test-stream.html      # 测试页面
├── package.json              # 依赖配置
├── tsconfig.json            # TypeScript 配置
└── README.md                # 项目文档

🔍 核心实现详解

1. AI 服务层:实现流式调用逻辑

ai.service.ts 是与阿里云百炼大模型交互的核心,负责处理流式数据:

typescript 复制代码
import { Injectable } from '@nestjs/common';
import OpenAI from 'openai';

@Injectable()
export class AiService {
  private openai: OpenAI;

  constructor() {
    // 初始化阿里云百炼客户端
    this.openai = new OpenAI({
      apiKey: 'sk-your-api-key-here',
      baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
    });
  }

  // 流式聊天 - 使用异步生成器逐步返回数据
  async *streamChat(
    messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[],
  ) {
    try {
      const stream = await this.openai.chat.completions.create({
        model: 'qwen-plus',
        messages,
        stream: true, // 开启流式响应
      });

      // 使用 for await 循环处理流式数据
      for await (const chunk of stream) {
        const content = chunk.choices[0]?.delta?.content;
        if (content) {
          yield content; // 逐个产出数据块
        }
      }
    } catch (error) {
      console.error('AI服务错误:', error);
      throw new Error('AI服务调用失败');
    }
  }
}

技术亮点::

  • async *streamChat(): 异步生成器函数,支持逐步产出数据
  • yield content: 每次产出一个数据块,实现流式传输
  • for await: 处理异步迭代器,等待每个数据块

2. 控制器层:处理 SSE 协议

ai.controller.ts 负责接收客户端请求,通过 SSE 协议推送实时数据:

typescript 复制代码
import { Controller, Post, Body, Res, HttpStatus } from '@nestjs/common';
import type { Response } from 'express';
import { AiService } from './ai.service';

interface ChatRequest {
  message: string;
  systemPrompt?: string;
}

@Controller('ai')
export class AiController {
  constructor(private readonly aiService: AiService) {}

  @Post('chat/stream-sse')
  async streamChatSSE(@Body() body: ChatRequest, @Res() res: Response) {
    const messages = [
      {
        role: 'system' as const,
        content: body.systemPrompt || '你是一个专业的编程助手',
      },
      { role: 'user' as const, content: body.message },
    ];

    try {
      // 设置 SSE 响应头
      res.setHeader('Content-Type', 'text/event-stream');
      res.setHeader('Cache-Control', 'no-cache');
      res.setHeader('Connection', 'keep-alive');
      res.setHeader('Access-Control-Allow-Origin', '*');

      res.status(HttpStatus.OK);

      // 发送开始信号
      res.write('data: {"type": "start"}\n\n');

      // 流式返回 AI 数据
      for await (const chunk of this.aiService.streamChat(messages)) {
        const data = JSON.stringify({ type: 'chunk', content: chunk });
        res.write(`data: ${data}\n\n`);
      }

      // 发送结束信号
      res.write('data: {"type": "end"}\n\n');
      res.end();
    } catch (error) {
      // 错误处理
      const errorData = JSON.stringify({
        type: 'error',
        error: '服务器内部错误',
        message: error.message,
      });
      res.write(`data: ${errorData}\n\n`);
      res.end();
    }
  }
}

SSE 协议要点:

  • SSE 响应头: 设置正确的 Content-Type 和缓存策略
  • 消息格式 : 使用 data: {...}\n\n 格式符合 SSE 标准
  • 流式处理 : for await 循环实时处理 AI 响应
  • 缓存处理: 使用 keep-alive 保持长连接

3. 前端实现:接收流式数据并支持停止

test-stream.html 实现了完整的客户端逻辑:

javascript 复制代码
// 全局变量
let currentController = null; // 用于控制停止

// 使用原生fetch实现SSE,支持停止功能
async function testEventSourceSSE() {
  setLoading(true);
  responseDiv.className = 'response';
  responseDiv.textContent = '';

  // 创建 AbortController 用于停止
  currentController = new AbortController();

  try {
    const response = await fetch('/ai/chat/stream-sse', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(getRequestBody()),
      signal: currentController.signal, // 支持停止
    });

    if (!response.ok) {
      throw new Error(`HTTP错误: ${response.status}`);
    }

    const reader = response.body.getReader();
    const decoder = new TextDecoder();

    // 流式读取响应数据
    while (true) {
      const { done, value } = await reader.read();

      if (done) break;

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

      // 解析 SSE 消息
      for (const line of lines) {
        if (line.startsWith('data: ') && line.length > 6) {
          try {
            const data = JSON.parse(line.slice(6));

            if (data.type === 'chunk') {
              responseDiv.textContent += data.content; // 实时显示
            } else if (data.type === 'end') {
              break; // 结束
            } else if (data.type === 'error') {
              responseDiv.className = 'response error';
              responseDiv.textContent = `错误: ${data.message}`;
              return;
            }
          } catch (parseError) {
            console.warn('解析SSE数据失败:', line);
          }
        }
      }
    }
  } catch (error) {
    if (error.name === 'AbortError') {
      responseDiv.textContent += '\n\n[用户停止生成]';
    } else {
      responseDiv.className = 'response error';
      responseDiv.textContent = `连接错误: ${error.message}`;
    }
  } finally {
    setLoading(false);
    currentController = null;
  }
}

// 停止流式生成
function stopStream() {
  if (currentController) {
    currentController.abort(); // 中断请求
    currentController = null;
  }
}

客户端关键技术:

  • AbortController: 实现请求的中断控制
  • 流式读取 : 使用 getReader() 逐块读取响应
  • SSE 解析 : 正确解析 data: 格式的消息
  • 实时显示: 每收到数据块立即更新界面

🔧 环境配置

环境变量

创建 .env 文件:

bash 复制代码
# 阿里云百炼 API 密钥
DASHSCOPE_API_KEY=sk-your-api-key-here

# 服务端口(可选)
PORT=3000

依赖安装

bash 复制代码
# 安装依赖
pnpm install

# 启动开发服务器
pnpm run start:dev

# 构建生产版本
pnpm run build

# 启动生产服务器
pnpm run start:prod

🌐 API 接口

POST /ai/chat/stream-sse

流式聊天接口,返回 SSE 格式的实时数据。

请求参数:

json 复制代码
{
  "message": "你好,请介绍一下自己",
  "systemPrompt": "你是一个专业的编程助手"
}

响应格式:

css 复制代码
data: {"type": "start"}

data: {"type": "chunk", "content": "你"}

data: {"type": "chunk", "content": "好"}

data: {"type": "end"}

🧪 测试

访问测试页面:http://localhost:3000/test-stream.html

  • 输入问题并点击"开始聊天"
  • 观察 AI 回复的实时显示效果
  • 可随时点击"停止生成"中断对话

🔍 技术要点

1. 异步生成器 (Async Generator)

typescript 复制代码
async *streamChat(messages) {
  // 异步生成器函数
  for await (const chunk of stream) {
    yield chunk; // 逐个产出数据
  }
}

2. Server-Sent Events (SSE)

typescript 复制代码
// 设置 SSE 响应头
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');

// 发送 SSE 消息
res.write('data: {"type": "chunk", "content": "hello"}\n\n');

3. AbortController 停止机制

javascript 复制代码
// 创建控制器
const controller = new AbortController();

// 传递 signal
fetch(url, { signal: controller.signal });

// 中断请求
controller.abort();

🎯 总结

本文详细介绍了如何使用 NestJS 构建 AI 流式聊天服务,核心是通过异步生成器处理 AI 接口的流式响应,结合 SSE 协议实现服务器到客户端的实时推送,并利用 AbortController 实现请求中断功能。

这种架构不仅可以应用于 AI 聊天场景,还可以扩展到任何需要实时数据推送的业务中,如实时日志、进度展示等。

相关推荐
安思派Anspire4 小时前
你不需要 GraphRAG!构建一个多策略图谱思维(Multi-Strategy Graph Thinking)AI Agent
aigc·openai·agent
阑梦清川5 小时前
mulerun上线了一个重磅级的智能体--educard generator(知识可视化)
aigc
Zhikes5 小时前
MOSS-TTSD: 文本对话式语音克隆
aigc
饼干哥哥5 小时前
Vibe Marketing|用Claude code+kimi k2爬取了120个 b站视频做内容选题数据分析
数据分析·aigc
用户5191495848456 小时前
揭秘LedgerCTF的AES白盒挑战:逆向工程与密码学分析
人工智能·aigc
用户5191495848456 小时前
SonicWall防火墙安全态势深度分析:固件解密与漏洞洞察
人工智能·aigc
沧海一粟青草喂马6 小时前
对口型视频怎么制作?从脚本到成片的全流程解析
aigc
用户5191495848458 小时前
Apache服务器自动化运维与安全加固脚本详解
人工智能·aigc
多恩Stone10 小时前
【3DV 进阶-2】Hunyuan3D2.1 训练代码详细理解下-数据读取流程
人工智能·python·算法·3d·aigc