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

使用 fetch 流式传输 HTTP 响应

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

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]删除
最新教程 更多>
  • 极简设计初学者指南
    极简设计初学者指南
    我一直是干净和简单的倡导者——这是我的思维最清晰的方式。然而,就像生活中的大多数任务一样,不同的工作有不同的工具,设计也是如此。在这篇文章中,我将分享我发现的极简设计实践,这些实践有助于创建干净简单的网站、模板和图形——在有限的空间内传达必要的内容。 简单可能比复杂更难:你必须努力让你的思维清晰,使...
    编程 发布于2024-11-03
  • 了解 React 应用程序中的渲染和重新渲染:它们如何工作以及如何优化它们
    了解 React 应用程序中的渲染和重新渲染:它们如何工作以及如何优化它们
    当我们在 React 中创建应用程序时,我们经常会遇到术语渲染和重新渲染组件。虽然乍一看这似乎很简单,但当涉及不同的状态管理系统(如 useState、Redux)或当我们插入生命周期钩子(如 useEffect)时,事情会变得有趣。如果您希望您的应用程序快速高效,那么了解这些流程是关键。 ...
    编程 发布于2024-11-03
  • 如何在 Node.js 中将 JSON 文件读入服务器内存?
    如何在 Node.js 中将 JSON 文件读入服务器内存?
    在 Node.js 中将 JSON 文件读入服务器内存为了增强服务器端代码性能,您可能需要读取 JSON 对象从文件到内存以便快速访问。以下是在 Node.js 中实现此目的的方法:同步方法:对于同步文件读取,请利用 fs(文件系统)中的 readFileSync() 方法模块。此方法将文件内容作为...
    编程 发布于2024-11-03
  • 人工智能可以提供帮助
    人工智能可以提供帮助
    我刚刚意识到人工智能对开发人员有很大帮助。它不会很快接管我们的工作,因为它仍然很愚蠢,但是,如果你像我一样正在学习编程,可以用作一个很好的工具。 我要求 ChatGpt 为我准备 50 个项目来帮助我掌握 JavaScript,它带来了令人惊叹的项目,我相信当我完成这些项目时,这些项目将使我成为 J...
    编程 发布于2024-11-03
  • Shadcn UI 套件 - 管理仪表板和网站模板
    Shadcn UI 套件 - 管理仪表板和网站模板
    Shadcn UI 套件是预先设计的多功能仪表板、网站模板和组件的综合集合。它超越了 Shadcn 的标准产品,为那些不仅仅需要基础知识的人提供更先进的设计和功能。 独特的仪表板模板 Shadcn UI Kit 提供了各种精心制作的仪表板模板。目前,有 7 个仪表板模板可用,随着时间...
    编程 发布于2024-11-03
  • 如何使用正则表达式捕获多行文本块?
    如何使用正则表达式捕获多行文本块?
    匹配多行文本块的正则表达式匹配跨多行的文本可能会给正则表达式构造带来挑战。考虑以下示例文本:some Varying TEXT DSJFKDAFJKDAFJDSAKFJADSFLKDLAFKDSAF [more of the above, ending with a newline] [yep, t...
    编程 发布于2024-11-03
  • 软件开发中结构良好的日志的力量
    软件开发中结构良好的日志的力量
    日志是了解应用程序底层发生的情况的关键。 简单地使用 console.log 打印所有值并不是最有效的日志记录方法。日志的用途不仅仅是显示数据,它们还可以帮助您诊断问题、跟踪系统行为以及了解与外部 API 或服务的交互。在您的应用程序在没有用户界面的情况下运行的情况下,例如在系统之间处理和传输数据的...
    编程 发布于2024-11-03
  • 如何在单个命令行命令中执行多行Python语句?
    如何在单个命令行命令中执行多行Python语句?
    在单个命令行命令中执行多行Python语句Python -c 选项允许单行循环执行,但在命令中导入模块可能会导致语法错误。要解决此问题,请考虑以下解决方案:使用 Echo 和管道:echo -e "import sys\nfor r in range(10): print 'rob'&qu...
    编程 发布于2024-11-03
  • 查找数组/列表中的重复元素
    查找数组/列表中的重复元素
    给定一个整数数组,找到所有重复的元素。 例子: 输入:[1,2,3,4,3,2,5] 输出:[2, 3] 暗示: 您可以使用 HashSet 来跟踪您已经看到的元素。如果某个元素已在集合中,则它是重复的。为了保留顺序,请使用 LinkedHashSet 来存储重复项。 使用 HashSet 的 Ja...
    编程 发布于2024-11-03
  • JavaScript 回调何时异步?
    JavaScript 回调何时异步?
    JavaScript 回调:是否异步?JavaScript 回调并非普遍异步。在某些场景下,例如您提供的 addOne 和 simpleMap 函数的示例,代码会同步运行。浏览器中的异步 JavaScript基于回调的 AJAX 函数jQuery 中通常是异步的,因为它们涉及 XHR (XMLHtt...
    编程 发布于2024-11-03
  • 以下是根据您提供的文章内容生成的英文问答类标题:

Why does `char` behave differently from integer types in template instantiation when comparing `char`, `signed char`, and `unsigned char`?
    以下是根据您提供的文章内容生成的英文问答类标题: Why does `char` behave differently from integer types in template instantiation when comparing `char`, `signed char`, and `unsigned char`?
    char、signed char 和 unsigned char 之间的行为差​​异下面的代码可以成功编译,但 char 的行为与整数类型不同。cout << getIsTrue< isX<int8>::ikIsX >() << endl; cou...
    编程 发布于2024-11-03
  • 如何在动态生成的下拉框中设置默认选择?
    如何在动态生成的下拉框中设置默认选择?
    确定下拉框中选定的项目使用 标签创建下拉列表时,您可以可能会遇到需要将特定选项设置为默认选择的情况。这在预填写表单或允许用户编辑其设置时特别有用。在您呈现的场景中, 标记是使用 PHP 动态生成的,并且您希望根据值存储在数据库中。实现此目的的方法如下:设置选定的属性要在下拉框中设置选定的项目,您需...
    编程 发布于2024-11-03
  • Tailwind CSS:自定义配置
    Tailwind CSS:自定义配置
    介绍 Tailwind CSS 是一种流行的开源 CSS 框架,近年来在 Web 开发人员中广受欢迎。它提供了一种独特的可定制方法来创建美观且现代的用户界面。 Tailwind CSS 区别于其他 CSS 框架的关键功能之一是它的可定制配置。在这篇文章中,我们将讨论 Tailwin...
    编程 发布于2024-11-03
  • 使用 jQuery
    使用 jQuery
    什么是 jQuery? jQuery 是一个快速的 Javascript 库,其功能齐全,旨在简化 HTML 文档遍历、操作、事件处理和动画等任务。 “少写多做” MDN 状态: jQuery使得编写多行代码和tsk变得更加简洁,甚至一行代码.. 使用 jQuery 处理事件 jQuery 的另一个...
    编程 发布于2024-11-03
  • CONCAT() 如何增强 MySQL 搜索功能以实现完整名称匹配?
    CONCAT() 如何增强 MySQL 搜索功能以实现完整名称匹配?
    WHERE 子句中使用 MySQL CONCAT() 函数进行高效搜索一种常见的数据库操作是跨多列搜索数据。然而,当分别使用名字和姓氏字段搜索姓名时,可能会存在一些限制,例如捕获不完整的匹配。为了克服这个问题,可以使用 MySQL CONCAT() 函数将列组合成一个用于搜索的单个字段。这提供了更加...
    编程 发布于2024-11-03

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

Copyright© 2022 湘ICP备2022001581号-3