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('');
}
相关推荐
付十一3 小时前
更新!Figma MCP + Cursor:大前端时代的UI到代码自动化
android·前端·ai编程
骑猪兜风2336 小时前
Windsurf Codemaps 深度解析:重新定义 AI 时代的代码理解方式
ai编程
AndCo20 小时前
和AI用TDD结对编程:1天开发一个完整的 Python 库
ai编程
Patrick_Wilson20 小时前
AI会如何评价一名前端工程师的技术人格
前端·typescript·ai编程
量子位1 天前
李飞飞最新长文火爆硅谷
ai编程
量子位1 天前
从“给答案”到“教动脑”:这届小学生被AI教会主动思考
ai编程
用户4099322502121 天前
Vue3响应式系统中,对象新增属性、数组改索引、原始值代理的问题如何解决?
前端·ai编程·trae
信码由缰1 天前
使用 Java、Spring Boot 和 Spring AI 开发符合 A2A 标准的 AI 智能体
ai编程