NodeJS创建流式接口&AI大模型接口流式调用记录

什么是流式文本传输?

流式文本传输是一种在数据生成后立即传输,而不是等待所有数据生成完毕再一次性传输的技术。这种传输方式可以显著提升用户体验,特别是在处理大量数据或需要实时响应的场景中。

流式传输的主要优势:

  • 实时响应:用户可以立即看到部分结果,而不必等待整个过程完成
  • 减少内存占用:数据可以边生成边传输,避免在内存中缓存大量数据
  • 更好的用户体验:特别是在AI生成内容、长文本处理等场景下

NodeJS + Koa 流式接口示例

javascript 复制代码
const Koa = require('koa');
const { Readable } = require('stream');

const app = new Koa();

// 流式响应中间件
app.use(async (ctx) => {
  // 设置响应头
  ctx.set('Content-Type', 'text/plain');
  ctx.set('Transfer-Encoding', 'chunked');
  
  // 创建一个可读流
  const readableStream = new Readable({
    read(size) {
      // 这个方法会在流需要更多数据时被调用
    }
  });
  
  // 模拟异步生成数据的过程
  async function generateData() {
    const data = '这是,第一部分,数据,\n这是,第二部分,数据\n这是,第三部分,数据,\n这是,第四部分,数据,\n这是最后一部分数据\n'.split(',');
    
    for (const chunk of data) {
      // 模拟处理延迟
      await new Promise(resolve => setTimeout(resolve, 100));
      // 将数据推入流中
      readableStream.push(chunk);
    }
    
    // 结束流
    readableStream.push(null);
  }
  
  // 启动数据生成
  generateData();
  
  // 将流管道到响应对象
  ctx.body = readableStream;
});

// 启动服务器
const PORT = 3000;
app.listen(PORT, () => {
  console.log(`服务器运行在 http://localhost:${PORT}`);
});

NodeJS 发送流式请求

javascript 复制代码
const https = require('https');
const http = require('http');

/**
 * Node.js版本的流式查询函数
 * @param {Object} options - 配置选项
 * @param {Object} options.params - 请求参数
 * @param {Function} [options.onStart] - 开始回调
 * @param {Function} [options.onChange] - 数据变化回调
 * @param {Function} [options.onFinish] - 完成回调
 * @param {Function} [options.onError] - 错误回调
 * @param {Function} [options.chunkTransform] - 数据块转换函数
 * @returns {Object} 返回包含abort方法的对象
 */
const streamRequest = ({
  params,
  onStart,
  onChange,
  onFinish,
  onError,
  chunkTransform,
}) => {
  const { url, method = 'GET', headers = {} } = params;
  const { searchParams } = params;
  const { body } = params;
  const parsedUrl = new URL(url);
  const isHttps = parsedUrl.protocol === 'https:';
  const requestModule = isHttps ? https : http;

  if (searchParams) {
    Object.entries(searchParams).forEach(([key, value]) => {
      parsedUrl.searchParams.set(key, value);
    });
  }

  const requestOptions = {
    hostname: parsedUrl.hostname,
    port: parsedUrl.port || (isHttps ? 443 : 80),
    path: parsedUrl.pathname + parsedUrl.search,
    method,
    headers: {
      'Content-Type': 'application/json',
      ...headers,
    },
  };

  let fullText = '';
  let aborted = false;

  const req = requestModule.request(requestOptions, (res) => {
    if (res.statusCode !== 200) {
      let errorData = '';
      res.on('data', (chunk) => {
        errorData += chunk.toString();
      });
      res.on('end', () => {
        const error = new Error(`${res.statusMessage}\n${errorData}`);
        onError?.(error);
      });
      return;
    }

    res.setEncoding('utf8');

    // 监听第一个数据块
    res.once('data', () => {
      onStart?.();
    });

    res.on('data', (chunk) => {
      if (aborted) return;
      const chunkStr = chunk?.toString();
      const transformedChunk = chunkTransform ? chunkTransform(chunkStr) : chunkStr;
      fullText += transformedChunk;
      onChange?.(transformedChunk, fullText);
    });

    res.on('end', () => {
      if (!aborted) {
        onFinish?.(fullText);
      }
    });

    res.on('error', (error) => {
      if (typeof onError !== 'function') {
          console.error(error);
          return;
      }
      if (!aborted) {
        console.error('Response error:', error);
        onError?.(error);
      }
    });
  });

  req.on('error', (error) => {
    if (!aborted) {
      console.error('Request error:', error);
      onError?.(error);
    }
  });

  // 发送请求体(如果是POST请求)
  if (method === 'POST' && body) {
    req.write(JSON.stringify(body));
  }

  req.end();

  return {
    abort: () => {
      aborted = true;
      req.destroy();
      console.log('Request aborted');
    },
  };
};

浏览器中发送流式请求

  • 基于 fetch 实现
javascript 复制代码
/**
 * 浏览器版本的流式查询函数
 * @param options - 配置选项
 * @returns 返回包含abort方法的对象
 */
export const browserStreamRequest = ({
  params,
  onStart,
  onChange,
  onFinish,
  onError,
  chunkTransform,
}) => {
  const { url, method = 'GET', headers = {} } = params;
  const { searchParams } = params;
  const { body } = params;
  
  let controller = null;
  let fullText = '';
  let isStarted = false;
  let isFinished = false;
  
  const buildFetchUrl = () => {
    if (!searchParams) return url;
    
    const urlObj = new URL(url, window.location.origin);
    Object.entries(searchParams).forEach(([key, value]) => {
      urlObj.searchParams.set(key, value);
    });
    return urlObj.toString();
  };
  
  const fetchOptions = {
    method,
    headers: {
      'Content-Type': 'application/json',
      ...headers,
    },
    // 允许跨域凭证
    credentials: 'include',
  };
  
  // 设置信号用于中断请求
  controller = new AbortController();
  fetchOptions.signal = controller.signal;
  
  // 添加请求体(如果是POST请求)
  if (method === 'POST' && body) {
    fetchOptions.body = JSON.stringify(body);
  }
  
  const processStream = async () => {
    try {
      const response = await fetch(buildFetchUrl(), fetchOptions);
      
      if (!response.ok) {
        let errorData = '';
        try {
          errorData = await response.text();
        } catch (e) {
          // 忽略解析错误
        }
        const error = new Error(`${response.status} ${response.statusText}\n${errorData}`);
        onError?.(error);
        return;
      }
      
      // 检查响应是否支持流式处理
      if (!response.body) {
        const error = new Error('Response body is not streamable');
        onError?.(error);
        return;
      }
      
      // 创建TextDecoder用于解码二进制流
      const decoder = new TextDecoder('utf-8');
      const reader = response.body.getReader();
      
      // 流式处理数据
      const readStream = async () => {
        try {
          const { done, value } = await reader.read();
          
          if (done) {
            if (!isFinished) {
              isFinished = true;
              onFinish?.(fullText);
            }
            return;
          }
          
          // 首次收到数据时触发onStart
          if (!isStarted) {
            isStarted = true;
            onStart?.();
          }
          
          // 解码并处理数据块
          const chunkStr = decoder.decode(value, { stream: true });
          const transformedChunk = chunkTransform ? chunkTransform(chunkStr) : chunkStr;
          fullText += transformedChunk;
          onChange?.(transformedChunk, fullText);
          
          // 继续读取流
          readStream();
        } catch (error) {
          if (!isFinished && !controller?.signal.aborted) {
            console.error('Stream processing error:', error);
            onError?.(error instanceof Error ? error : new Error(String(error)));
          }
        }
      };
      
      readStream();
    } catch (error) {
      if (!isFinished && !controller?.signal.aborted) {
        console.error('Fetch request error:', error);
        onError?.(error instanceof Error ? error : new Error(String(error)));
      }
    }
  };
  
  // 立即开始处理流
  processStream();
  
  return {
    abort: () => {
      if (controller) {
        controller.abort();
        isFinished = true;
        console.log('Request aborted');
      }
    },
  };
};
  • 低版本浏览器(如 IE11)不支持 fetch 流式传输,需要使用 XHR 实现
javascript 复制代码
/**
 * 浏览器兼容性检查
 * @returns 是否支持流式请求
 */
export const isStreamSupported = () => {
  return !!(window.fetch && 
    window.ReadableStream && 
    window.TextDecoder && 
    window.AbortController);
};

/**
 * 降级版本的请求函数(当浏览器不支持流式处理时使用)
 * @param options - 配置选项
 * @returns 返回包含abort方法的对象
 */
export const fallbackRequest = ({
  params,
  onStart,
  onChange,
  onFinish,
  onError,
  chunkTransform,
}) => {
  const { url, method = 'GET', headers = {} } = params;
  const { searchParams } = params;
  const { body } = params;
  
  let xhr = null;
  let fullText = '';
  
  const buildUrl = () => {
    if (!searchParams) return url;
    
    const urlObj = new URL(url, window.location.origin);
    Object.entries(searchParams).forEach(([key, value]) => {
      urlObj.searchParams.set(key, value);
    });
    return urlObj.toString();
  };
  
  try {
    xhr = new XMLHttpRequest();
    xhr.open(method, buildUrl(), true);
    
    // 设置响应类型为文本
    xhr.responseType = 'text';
    
    // 设置请求头
    Object.entries({
      'Content-Type': 'application/json',
      ...headers,
    }).forEach(([key, value]) => {
      xhr?.setRequestHeader(key, value);
    });
    
    // 监听进度事件(非标准流式处理)
    xhr.onprogress = (event) => {
      if (event.lengthComputable || xhr?.responseText) {
        const currentText = xhr?.responseText || '';
        const newChunk = currentText.substring(fullText.length);
        
        if (newChunk && fullText.length === 0) {
          // 首次收到数据
          onStart?.();
        }
        
        if (newChunk) {
          const transformedChunk = chunkTransform ? chunkTransform(newChunk) : newChunk;
          fullText += transformedChunk;
          onChange?.(transformedChunk, fullText);
        }
      }
    };
    
    xhr.onload = () => {
      if (xhr?.status === 200) {
        // 确保处理完所有数据
        const currentText = xhr.responseText || '';
        if (currentText !== fullText) {
          const newChunk = currentText.substring(fullText.length);
          const transformedChunk = chunkTransform ? chunkTransform(newChunk) : newChunk;
          fullText += transformedChunk;
          onChange?.(transformedChunk, fullText);
        }
        onFinish?.(fullText);
      } else {
        const error = new Error(`${xhr?.status} ${xhr?.statusText}\n${xhr?.responseText || ''}`);
        onError?.(error);
      }
    };
    
    xhr.onerror = () => {
      const error = new Error('Network error occurred');
      console.error('XHR error:', error);
      onError?.(error);
    };
    
    // 发送请求
    if (method === 'POST' && body) {
      xhr.send(JSON.stringify(body));
    } else {
      xhr.send();
    }
    
  } catch (error) {
    console.error('Fallback request initialization error:', error);
    onError?.(error instanceof Error ? error : new Error(String(error)));
  }
  
  return {
    abort: () => {
      if (xhr) {
        xhr.abort();
        console.log('Request aborted');
      }
    },
  };
};

/**
 * 智能流式请求函数 - 根据浏览器支持情况自动选择实现方式
 * @param options - 配置选项
 * @returns 返回包含abort方法的对象
 */
export const smartStreamRequest = (options) => {
  if (isStreamSupported()) {
    console.log('Using native fetch stream implementation');
    return browserStreamRequest(options);
  } else {
    console.log('Using fallback XHR implementation');
    return fallbackRequest(options);
  }
};

浏览器端流式请求使用示例

javascript 复制代码
// 基本使用示例
const streamInstance = smartStreamRequest({
  params: {
    url: '/api/stream',
    method: 'POST',
    body: {
      prompt: 'Tell me a story',
      stream: true
    }
  },
  onStart: () => {
    console.log('Stream started');
    document.getElementById('status')?.textContent('Streaming...');
  },
  onChange: (chunk, fullText) => {
    console.log(`Received chunk: ${chunk}`);
    document.getElementById('output')?.textContent(fullText);
  },
  onFinish: (fullText) => {
    console.log(`Stream finished, total text: ${fullText.length} characters`);
    document.getElementById('status')?.textContent('Completed');
  },
  onError: (error) => {
    console.error('Stream error:', error);
    document.getElementById('status')?.textContent(`Error: ${error.message}`);
  },
  // 可选的数据转换函数
  chunkTransform: (chunk) => {
    // 例如处理特定格式的流式数据
    return chunk.replace(/^data: /, '');
  }
});

// 中断请求的示例
setTimeout(() => {
  streamInstance.abort();
}, 5000);

常见问题处理

  1. CORS 问题:确保服务器端正确配置了 CORS 响应头,特别是当需要流式传输时
  2. 超时处理 :可以通过 setTimeoutabort() 方法实现请求超时控制
  3. 内存管理:对于长时间运行的流,注意监控内存使用情况,避免内存泄漏
  4. 错误恢复 :可以实现重试逻辑,在 onError 回调中重新发起请求

案例: 流式调用免费的AI大模型接口(来自硅基流动)

备注:硅基流动(SiliconFlow)是一家提供免费AI大模型服务的公司,其API接口支持流式传输。其中token可以在 硅基流动API密钥管理 处创建。API介绍见:API手册

typescript 复制代码
async function callSiliconFlowAPI() {
  const options = {
    url: 'https://api.siliconflow.cn/v1/chat/completions',
    method: 'POST',
    headers: { Authorization: 'Bearer <token>', 'Content-Type': 'application/json' },
    body: {
      model: 'Qwen/Qwen2.5-7B-Instruct',
      messages: [{ role: 'user', content: '你好' }],
      stream: true,
    },
  };

  try {
    streamRequest({
      params: options,
      onChange: (chunk, fullText) => {
        process.stdout.write(chunk);
      },
      chunkTransform: extractContentFromSiliconFlowChunk,
    });
  } catch (error) {
    console.error(error);
  }
}
/**
 * 从 SiliconFlow 模型的响应 chunk 中提取实际内容
 * @param {string} chunk - 包含 SiliconFlow 响应的 chunk 字符串
 * @returns {string} - 提取后的实际内容
 */
function extractContentFromSiliconFlowChunk (chunk) {
  return chunk?.trim()
    .split('data: ')
    .filter(item => item && !item.includes('[DONE]'))
    .map(item => {
      const json = JSON.parse(item.trim());
      const delta = json?.choices?.[0]?.message || json?.choices?.[0]?.delta;
      const res = delta?.content ?? delta?.reasoning_content ?? '';
      return res;
    })
    .filter(Boolean).join('');
}
相关推荐
wangruofeng2 小时前
OpenCode 上手初体验:从安装到基础使用
github·ai编程
饼干哥哥3 小时前
1 个人用AI编程开发的产品卖了8000万美金——Base44的增长策略全拆解
人工智能·ai编程
aou3 小时前
让表格式录入像 Excel 一样顺滑
前端·ai编程
去哪儿技术沙龙4 小时前
去哪儿网前端代码自动生成技术实践
前端·ai编程
HyperAI超神经4 小时前
实现高选择性底物设计,MIT联手哈佛用生成式AI发现全新蛋白酶切割模式
人工智能·深度学习·机器学习·开源·ai编程
陈佬昔没带相机5 小时前
AI 编程更可控,SDD 开源工具之 OpenSpec
ai编程
草帽lufei6 小时前
国内网络体验Claude全系列!Kiro AI开发工具实测
aigc·ai编程·claude
程序员鱼皮8 小时前
我的免费 Vibe Coding 教程,爆了!
程序员·ai编程·vibecoding
程序员猫哥_8 小时前
前端开发,挑战用一句话做个后台管理系统
ai编程
装不满的克莱因瓶9 小时前
Cursor超长会话跨窗口关联解决方案
人工智能·ai·agent·ai编程·cursor·智能体