”工欲善其事,必先利其器。“—孔子《论语.录灵公》
首页 > 编程 > 使用 fetch 流式传输 HTTP 响应

使用 fetch 流式传输 HTTP 响应

发布于2024-07-31
浏览:895

Streaming HTTP Responses using fetch

这篇文章将着眼于使用 JavaScript Streams API,它允许进行 fetch HTTP 调用并以块的形式接收流响应,这允许客户端开始更多地响应服务器响应快速构建像 ChatGPT 这样的 UI。

作为一个激励性的例子,我们将实现一个函数来处理来自 OpenAI(或任何使用相同 http 流 API 的服务器)的流式 LLM 响应,不使用 npm 依赖项,仅使用内置的 fetch。完整的代码在这里,包括指数退避重试、嵌入、非流式聊天以及用于与聊天完成和嵌入交互的更简单的 API。

如果您有兴趣了解如何将 HTTP 流返回给客户端,请查看这篇文章。

完整示例代码

这是完整的示例。我们将看看下面的每一个部分:

async function createChatCompletion(body: ChatCompletionCreateParams) {
  // Making the request
  const baseUrl = process.env.LLM_BASE_URL || "https://api.openai.com";
  const response = await fetch(baseUrl   "/v1/chat/completions", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": "Bearer "   process.env.LLM_API_KEY,
    },
    body: JSON.stringify(body),
  });
  // Handling errors
  if (!response.ok) {
    const error = await response.text();
    throw new Error(`Failed (${response.status}): ${error}`,
  }
  if (!body.stream) { // the non-streaming case
    return response.json();
  }
  const stream = response.body;
  if (!stream) throw new Error("No body in response");
  // Returning an async iterator
  return {
    [Symbol.asyncIterator]: async function* () {
      for await (const data of splitStream(stream)) {
        // Handling the OpenAI HTTP streaming protocol
        if (data.startsWith("data:")) {
          const json = data.substring("data:".length).trimStart();
          if (json.startsWith("[DONE]")) {
            return;
          }
          yield JSON.parse(json);
        }
      }
    },
  };
}

// Reading the stream  
async function* splitStream(stream: ReadableStream) {
  const reader = stream.getReader();
  let lastFragment = "";
  try {
    while (true) {
      const { value, done } = await reader.read();
      if (done) {
        // Flush the last fragment now that we're done
        if (lastFragment !== "") {
          yield lastFragment;
        }
        break;
      }
      const data = new TextDecoder().decode(value);
      lastFragment  = data;
      const parts = lastFragment.split("\n\n");
      // Yield all except for the last part
      for (let i = 0; i 



请参阅此处的代码,了解具有流式和非流式参数变体的良好类型重载的版本,以及重试和其他改进。

这篇文章的其余部分是关于理解这段代码的作用。

提出请求

这部分其实很简单。流式 HTTP 响应来自普通 HTTP 请求:

const baseUrl = process.env.LLM_BASE_URL || "https://api.openai.com";
const response = await fetch(baseUrl   "/v1/chat/completions", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer "   process.env.LLM_API_KEY,
  },
  body: JSON.stringify(body),
});

HTTP 标头按平常方式发送,无需特别设置任何内容即可启用流式传输。您仍然可以利用常规缓存标头进行 HTTP 流式传输。

处理错误

关于客户端错误的故事对于 HTTP 流来说有点不幸。好处是,对于 HTTP 流式传输,客户端会在初始响应中立即获取状态代码,并可以检测到故障。 http 协议的缺点是,如果服务器返回成功,但随后在流中中断,则协议级别没有任何内容可以告诉客户端流已中断。我们将在下面看到 OpenAI 如何在最后编码“全部完成”哨兵来解决这个问题。

if (!response.ok) {
  const error = await response.text();
  throw new Error(`Failed (${response.status}): ${error}`,
}

读取流

为了读取 HTTP 流响应,客户端可以使用 response.body 属性,该属性是一个 ReadableStream,允许您使用 .getReader() 方法迭代从服务器传入的块。 1

const reader = request.body.getReader();
try {
    while (true) {
      const { value, done } = await reader.read();
      if (done) break;
      const text = TextDecoder().decode(value);
      //... do something with the chunk
    }
} finally {
  reader.releaseLock();
}

这会处理我们返回的每一位数据,但对于 OpenAI HTTP 协议,我们期望数据是由换行符分隔的 JSON,因此我们将拆分响应正文并“生成”每一行。重新完成。我们将进行中的行缓冲到lastFragment中,并且只返回由两个换行符分隔的完整行:

// stream here is request.body
async function* splitStream(stream: ReadableStream) {
  const reader = stream.getReader();
  let lastFragment = "";
  try {
    while (true) {
      const { value, done } = await reader.read();
      if (done) {
        // Flush the last fragment now that we're done
        if (lastFragment !== "") {
          yield lastFragment;
        }
        break;
      }
      const data = new TextDecoder().decode(value);
      lastFragment  = data;
      const parts = lastFragment.split("\n\n");
      // Yield all except for the last part
      for (let i = 0; i 



如果你不熟悉这个 function* 和yield 语法,只需将 function* 视为可以在循环中返回多个内容的函数,并将yield视为从函数中多次返回内容的方式。

然后您可以循环此 splitStream 函数,例如:

for await (const data of splitStream(response.body)) {
  // data here is a full line of text. For OpenAI, it might look like
  // "data: {...some json object...}" or "data: [DONE]" at the end
}

如果这个“for wait”语法让您感到困惑,那么它使用了所谓的“异步迭代器”——就像您在 for 循环中使用的常规迭代器一样,但每次它获取下一个值时,都会等待它。

对于我们的示例,当我们从 OpenAI 获取一些文本并且正在等待更多文本时,for 循环将等待,直到 splitStream 产生另一个值,这将在 wait reader.read() 返回一个完成的值时发生一行或多行文本。

接下来我们将研究另一种返回异步迭代器的方法,该迭代器不是 splitStream 等函数,因此调用者可以使用“for wait”循环来迭代此数据。

返回一个异步迭代器

现在我们有一个返回整行文本的异步迭代器,我们可以只返回 splitStream(response.body),但我们希望拦截每一行并转换它们,同时仍然让函数的调用者进行迭代。

该方法类似于上面的 async function* 语法。这里我们将直接返回一个异步迭代器,而不是调用时返回一个的异步函数。不同之处在于类型是 AsyncIterator 而不是需要首先调用的 AsyncGenerator。 AsyncIterator 可以通过某个命名函数来定义:Symbol.asyncIterator.2

      return {
        [Symbol.asyncIterator]: async function* () {
          for await (const data of splitStream(stream)) {
            //handle the data
            yield data;
          }
        },
      };

当您想要返回与来自 splitStream 的数据不同的内容时,这非常有用。每次从流式 HTTP 请求中传入新行时,sp​​litStream 都会生成它,该函数将在数据中接收它,并可以在将其生成给调用者之前执行一些操作。

接下来我们将看看如何在 OpenAI 的流式聊天完成 API 的情况下具体解释这些数据。

处理 OpenAI HTTP 流协议

OpenAI 响应协议是一系列以 data: 或 event: 开头的行,但我们只处理数据响应,因为这是完成聊天的有用部分。如果流完成,则有一个 [DONE] 标记,否则它只是 JSON。

for await (const data of splitStream(stream)) {
  if (data.startsWith("data:")) {
    const json = data.substring("data:".length).trimStart();
    if (json.startsWith("[DONE]")) {
      return;
    }
    yield JSON.parse(json);
  } else {
    console.debug("Unexpected data:", data);
  }
}

将所有内容整合在一起

既然您了解了 HTTP 流,您就可以放心地直接使用流 API,而无需依赖 sdk 或库。这使您可以隐藏延迟,因为您的 UI 可以立即开始更新,而不会因为多个请求而消耗更多带宽。您可以像使用官方 openai npm 包一样使用上述功能:

  const response = await createChatCompletion({
    model: "llama3",
    messages: [...your messages...],
    stream: true,
  });
  for await (const chunk of response) {
    if (chunk.choices[0].delta?.content) {
      console.log(chunk.choices[0].delta.content);
    }
  }

请参阅此处的代码,它还允许您创建一些实用函数,通过预先配置模型并提取 .choices[0].delta.content:
使这变得更加容易

const response = await chatStream(messages);
for await (const content of response) {
  console.log(content);
}

在复制代码之前,尝试自己实现它作为异步函数的练习。

更多资源

  • 有关从您自己的服务器端点返回 HTTP 流数据的信息,请查看关于 AI Chat with HTTP Streaming 的这篇文章,该文章既将数据从 OpenAI(或类似的)流式传输到您的服务器,又同时将其流式传输到客户端,同时执行自定义逻辑(例如将块保存到数据库)。
  • MDN 文档一如既往地很棒。除了上面的链接之外,这里还有关于可读流 API 的指南,它展示了如何将可读流连接到 使用 fetch 流式传输 HTTP 响应 标签以在图像请求中进行流式传输。注意:本指南使用 response.body 作为异步迭代器,但目前尚未广泛实现,并且不在 TypeScript 类型中。

  1. 注意:一次只能有一个流的读取器,因此您通常不会多次调用 .getReader() - 在这种情况下您可能需要 .tee() ,并且如果您想使用 .由于某种原因多次 getReader() ,请确保首先拥有第一个 .releaseLock() 。 ↩

  2. 或者,如果您不熟悉 Symbol,它的用途是在对象中包含非字符串或数字的键。这样,如果您添加了名为 asyncIterator 的键,它们就不会发生冲突。您可以使用 myIterator[Symbol.asyncIterator]() 访问该函数。 ↩

版本声明 本文转载于:https://dev.to/ianmacartney/streaming-http-responses-using-fetch-1fm2?1如有侵犯,请联系[email protected]删除
最新教程 更多>
  • 如何限制动态大小的父元素中元素的滚动范围?
    如何限制动态大小的父元素中元素的滚动范围?
    在交互式接口中实现垂直滚动元素的CSS高度限制,控制元素的滚动行为对于确保用户体验和可访问性是必不可少的。一种这样的方案涉及限制动态大小的父元素中元素的滚动范围。问题:考虑一个布局,其中我们具有与用户垂直滚动一起移动的可滚动地图div,同时与固定的固定sidebar保持一致。但是,地图的滚动无限期...
    编程 发布于2025-03-25
  • PHP阵列键值异常:了解07和08的好奇情况
    PHP阵列键值异常:了解07和08的好奇情况
    PHP数组键值问题,使用07&08 在给定数月的数组中,键值07和08呈现令人困惑的行为时,就会出现一个不寻常的问题。运行print_r($月)返回意外结果:键“ 07”丢失,而键“ 08”分配给了9月的值。此问题源于PHP对领先零的解释。当一个数字带有0(例如07或08)的前缀时,PHP将其...
    编程 发布于2025-03-25
  • 如何使用替换指令在GO MOD中解析模块路径差异?
    如何使用替换指令在GO MOD中解析模块路径差异?
    在使用GO MOD时,在GO MOD 中克服模块路径差异时,可能会遇到冲突,其中3个Party Package将另一个PAXPANCE带有导入式套件之间的另一个软件包,并在导入式套件之间导入另一个软件包。如回声消息所证明的那样: go.etcd.io/bbolt [&&&&&&&&&&&&&&&&...
    编程 发布于2025-03-25
  • 如何使用Regex在PHP中有效地提取括号内的文本
    如何使用Regex在PHP中有效地提取括号内的文本
    php:在括号内提取文本在处理括号内的文本时,找到最有效的解决方案是必不可少的。一种方法是利用PHP的字符串操作函数,如下所示: 作为替代 $ text ='忽略除此之外的一切(text)'; preg_match('#((。 &&& [Regex使用模式来搜索特...
    编程 发布于2025-03-25
  • 如何在JavaScript对象中动态设置键?
    如何在JavaScript对象中动态设置键?
    在尝试为JavaScript对象创建动态键时,如何使用此Syntax jsObj['key' i] = 'example' 1;不工作。正确的方法采用方括号: jsobj ['key''i] ='example'1; 在JavaScript中,数组是一...
    编程 发布于2025-03-25
  • 如何在Java中正确显示“ DD/MM/YYYY HH:MM:SS.SS”格式的当前日期和时间?
    如何在Java中正确显示“ DD/MM/YYYY HH:MM:SS.SS”格式的当前日期和时间?
    如何在“ dd/mm/yyyy hh:mm:mm:ss.ss”格式“ gormat 解决方案: args)抛出异常{ 日历cal = calendar.getInstance(); SimpleDateFormat SDF =新的SimpleDateFormat(“...
    编程 发布于2025-03-25
  • 如何使用不同数量列的联合数据库表?
    如何使用不同数量列的联合数据库表?
    合并列数不同的表 当尝试合并列数不同的数据库表时,可能会遇到挑战。一种直接的方法是在列数较少的表中,为缺失的列追加空值。 例如,考虑两个表,表 A 和表 B,其中表 A 的列数多于表 B。为了合并这些表,同时处理表 B 中缺失的列,请按照以下步骤操作: 确定表 B 中缺失的列,并将它们添加到表的末...
    编程 发布于2025-03-25
  • 为什么不````''{margin:0; }`始终删除CSS中的最高边距?
    为什么不````''{margin:0; }`始终删除CSS中的最高边距?
    在CSS 问题:不正确的代码: 全球范围将所有余量重置为零,如提供的代码所建议的,可能会导致意外的副作用。解决特定的保证金问题是更建议的。 例如,在提供的示例中,将以下代码添加到CSS中,将解决余量问题: body H1 { 保证金顶:-40px; } 此方法更精确,避免了由全局保证金重置引...
    编程 发布于2025-03-25
  • 为什么我的CSS背景图像出现?
    为什么我的CSS背景图像出现?
    故障排除:CSS背景图像未出现 ,您的背景图像尽管遵循教程说明,但您的背景图像仍未加载。图像和样式表位于相同的目录中,但背景仍然是空白的白色帆布。而不是不弃用的,您已经使用了CSS样式: bockent {背景:封闭图像文件名:背景图:url(nickcage.jpg); 如果您的html,css...
    编程 发布于2025-03-25
  • 在细胞编辑后,如何维护自定义的JTable细胞渲染?
    在细胞编辑后,如何维护自定义的JTable细胞渲染?
    在JTable中维护jtable单元格渲染后,在JTable中,在JTable中实现自定义单元格渲染和编辑功能可以增强用户体验。但是,至关重要的是要确保即使在编辑操作后也保留所需的格式。在设置用于格式化“价格”列的“价格”列,用户遇到的数字格式丢失的“价格”列的“价格”之后,问题在设置自定义单元格...
    编程 发布于2025-03-25
  • 您可以使用CSS在Chrome和Firefox中染色控制台输出吗?
    您可以使用CSS在Chrome和Firefox中染色控制台输出吗?
    在javascript console 中显示颜色是可以使用chrome的控制台显示彩色文本,例如红色的redors,for for for for错误消息?回答是的,可以使用CSS将颜色添加到Chrome和Firefox中的控制台显示的消息(版本31或更高版本)中。要实现这一目标,请使用以下模...
    编程 发布于2025-03-25
  • 如何在php中使用卷发发送原始帖子请求?
    如何在php中使用卷发发送原始帖子请求?
    如何使用php 创建请求来发送原始帖子请求,开始使用curl_init()开始初始化curl session。然后,配置以下选项: curlopt_url:请求 [要发送的原始数据指定内容类型,为原始的帖子请求指定身体的内容类型很重要。在这种情况下,它是文本/平原。要执行此操作,请使用包含以下标头...
    编程 发布于2025-03-25
  • 如何使用PHP从XML文件中有效地检索属性值?
    如何使用PHP从XML文件中有效地检索属性值?
    从php $xml = simplexml_load_file($file); foreach ($xml->Var[0]->attributes() as $attributeName => $attributeValue) { echo $attributeName,...
    编程 发布于2025-03-25
  • 如何在Java字符串中有效替换多个子字符串?
    如何在Java字符串中有效替换多个子字符串?
    在java 中有效地替换多个substring,需要在需要替换一个字符串中的多个substring的情况下,很容易求助于重复应用字符串的刺激力量。 However, this can be inefficient for large strings or when working with nu...
    编程 发布于2025-03-25
  • Android如何向PHP服务器发送POST数据?
    Android如何向PHP服务器发送POST数据?
    在android apache httpclient(已弃用) httpclient httpclient = new defaulthttpclient(); httppost httppost = new httppost(“ http://www.yoursite.com/script.p...
    编程 发布于2025-03-25

免责声明: 提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发到邮箱:[email protected] 我们会第一时间内为您处理。

Copyright© 2022 湘ICP备2022001581号-3